diff --git a/app/db/db.go b/app/db/db.go new file mode 100644 index 0000000..cb31650 --- /dev/null +++ b/app/db/db.go @@ -0,0 +1,194 @@ +package db + +import ( + "context" + _ "embed" + "fmt" + "time" + + "github.com/jmoiron/sqlx" + "github.com/temporalio/reference-app-orders-go/app/config" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + mongodb "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// OrdersCollection is the name of the MongoDB collection to use for Orders. +const OrdersCollection = "orders" + +// ShipmentCollection is the name of the MongoDB collection to use for Shipment data. +const ShipmentCollection = "shipments" + +// DB is an interface that defines the methods that a database driver must implement +type DB interface { + Connect(ctx context.Context) error + Setup() error + Close() error + InsertOrder(context.Context, interface{}) error + UpdateOrderStatus(context.Context, string, string) error + GetOrders(context.Context, interface{}) error + UpdateShipmentStatus(context.Context, string, string) error + GetShipments(context.Context, interface{}) error +} + +// CreateDB creates a new DB instance based on the configuration +func CreateDB(config config.AppConfig) DB { + if config.MongoURL != "" { + return &MongoDB{uri: config.MongoURL} + } + + return &SQLiteDB{path: "./api-data.db"} +} + +// MongoDB is a struct that implements the DB interface for MongoDB +type MongoDB struct { + uri string + client *mongo.Client + db *mongo.Database +} + +// Connect connects to a MongoDB instance +func (m *MongoDB) Connect(ctx context.Context) error { + client, err := mongo.Connect(ctx, options.Client().ApplyURI(m.uri)) + if err != nil { + return err + } + m.client = client + m.db = client.Database("orders") + return nil +} + +// Setup sets up the MongoDB instance +func (m *MongoDB) Setup() error { + orders := m.db.Collection(OrdersCollection) + _, err := orders.Indexes().CreateOne(context.TODO(), mongodb.IndexModel{ + Keys: map[string]interface{}{"received_at": 1}, + }) + if err != nil { + return fmt.Errorf("failed to create orders index: %w", err) + } + + shipments := m.db.Collection(ShipmentCollection) + _, err = shipments.Indexes().CreateOne(context.TODO(), mongodb.IndexModel{ + Keys: map[string]interface{}{"booked_at": 1}, + }) + if err != nil { + return fmt.Errorf("failed to create shipment index: %w", err) + } + + return nil +} + +// InsertOrder inserts an Order into the MongoDB instance +func (m *MongoDB) InsertOrder(ctx context.Context, order interface{}) error { + _, err := m.db.Collection(OrdersCollection).InsertOne(ctx, order) + return err +} + +// UpdateOrderStatus updates an Order in the MongoDB instance +func (m *MongoDB) UpdateOrderStatus(ctx context.Context, id string, status string) error { + _, err := m.db.Collection(OrdersCollection).UpdateOne(ctx, bson.M{"id": id}, bson.M{"$set": bson.M{"status": status}}) + return err +} + +// GetOrders returns a list of Orders from the MongoDB instance +func (m *MongoDB) GetOrders(ctx context.Context, result interface{}) error { + res, err := m.db.Collection(OrdersCollection).Find(ctx, bson.M{}, &options.FindOptions{ + Sort: bson.M{"received_at": 1}, + }) + if err != nil { + return err + } + + return res.All(ctx, result) +} + +// UpdateShipmentStatus updates a Shipment in the MongoDB instance +func (m *MongoDB) UpdateShipmentStatus(ctx context.Context, id string, status string) error { + _, err := m.db.Collection(ShipmentCollection).UpdateOne( + ctx, + bson.M{"id": id}, + bson.M{ + "$set": bson.M{"status": status}, + "$setOnInsert": bson.M{"booked_at": time.Now().UTC()}, + }, + ) + return err +} + +// GetShipments returns a list of Shipments from the MongoDB instance +func (m *MongoDB) GetShipments(ctx context.Context, result interface{}) error { + res, err := m.db.Collection(ShipmentCollection).Find(ctx, bson.M{}, &options.FindOptions{ + Sort: bson.M{"booked_at": 1}, + }) + if err != nil { + return err + } + + return res.All(ctx, result) +} + +// Close closes the connection to the MongoDB instance +func (m *MongoDB) Close() error { + return m.client.Disconnect(context.Background()) +} + +// SQLiteDB is a struct that implements the DB interface for SQLite +type SQLiteDB struct { + path string + db *sqlx.DB +} + +//go:embed schema.sql +var sqliteSchema string + +// Connect connects to a SQLite instance +func (s *SQLiteDB) Connect(_ context.Context) error { + db, err := sqlx.Connect("sqlite3", s.path) + if err != nil { + return err + } + s.db = db + db.SetMaxOpenConns(1) // SQLite does not support concurrent writes + return nil +} + +// Setup sets up the SQLite instance +func (s *SQLiteDB) Setup() error { + _, err := s.db.Exec(sqliteSchema) + return err +} + +// Close closes the connection to the SQLite instance +func (s *SQLiteDB) Close() error { + return s.db.Close() +} + +// InsertOrder inserts an Order into the SQLite instance +func (s *SQLiteDB) InsertOrder(ctx context.Context, order interface{}) error { + _, err := s.db.NamedExecContext(ctx, "INSERT INTO orders (id, received_at, status) VALUES (:id, :received_at, :status)", order) + return err +} + +// UpdateOrderStatus updates an Order in the SQLite instance +func (s *SQLiteDB) UpdateOrderStatus(ctx context.Context, id string, status string) error { + _, err := s.db.ExecContext(ctx, "UPDATE orders SET status = ? WHERE id = ?", status, id) + return err +} + +// GetOrders returns a list of Orders from the SQLite instance +func (s *SQLiteDB) GetOrders(ctx context.Context, result interface{}) error { + return s.db.SelectContext(ctx, result, "SELECT * FROM orders ORDER BY received_at") +} + +// UpdateShipmentStatus updates a Shipment in the SQLite instance +func (s *SQLiteDB) UpdateShipmentStatus(ctx context.Context, id string, status string) error { + _, err := s.db.ExecContext(ctx, "INSERT INTO shipments (id, booked_at, status) VALUES (?, ?, ?) ON CONFLICT(id) DO UPDATE SET status = ?", id, time.Now().UTC(), status, status) + return err +} + +// GetShipments returns a list of Shipments from the SQLite instance +func (s *SQLiteDB) GetShipments(ctx context.Context, result interface{}) error { + return s.db.SelectContext(ctx, result, "SELECT * FROM shipments ORDER BY booked_at") +} diff --git a/app/db/schema.sql b/app/db/schema.sql new file mode 100644 index 0000000..b22b717 --- /dev/null +++ b/app/db/schema.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS orders ( + id TEXT PRIMARY KEY, + customer_id TEXT NOT NULL, + received_at TIMESTAMP NOT NULL, + status TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS orders_received_at ON orders(received_at DESC); + +CREATE TABLE IF NOT EXISTS shipments ( + id TEXT PRIMARY KEY, + status TEXT NOT NULL, + booked_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS shipments_booked_at ON shipments (booked_at DESC); diff --git a/app/order/api.go b/app/order/api.go index 77fb326..6bff30d 100644 --- a/app/order/api.go +++ b/app/order/api.go @@ -8,9 +8,7 @@ import ( "strings" "time" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" + "github.com/temporalio/reference-app-orders-go/app/db" "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/sdk/client" @@ -23,9 +21,6 @@ const TaskQueue = "orders" // StatusQuery is the name of the query to use to fetch an Order's status. const StatusQuery = "status" -// OrdersCollection is the name of the MongoDB collection to use for Orders. -const OrdersCollection = "orders" - // OrderWorkflowID returns the workflow ID for an Order. func OrderWorkflowID(id string) string { return "Order:" + id @@ -201,16 +196,15 @@ type OrderResult struct { type handlers struct { temporal client.Client - orders *mongo.Collection + db db.DB logger *slog.Logger } // Router implements the http.Handler interface for the Billing API -func Router(client client.Client, db *mongo.Database, logger *slog.Logger) http.Handler { +func Router(client client.Client, db db.DB, logger *slog.Logger) http.Handler { r := http.NewServeMux() - orders := db.Collection(OrdersCollection) - h := handlers{temporal: client, orders: orders, logger: logger} + h := handlers{temporal: client, db: db, logger: logger} r.HandleFunc("POST /orders", h.handleCreateOrder) r.HandleFunc("GET /orders", h.handleListOrders) @@ -223,18 +217,9 @@ func Router(client client.Client, db *mongo.Database, logger *slog.Logger) http. func (h *handlers) handleListOrders(w http.ResponseWriter, _ *http.Request) { ctx := context.TODO() - orders := []ListOrderEntry{} - - res, err := h.orders.Find(ctx, bson.M{}, &options.FindOptions{ - Sort: bson.M{"received_at": 1}, - }) - if err != nil { - h.logger.Error("Failed to list orders", "error", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - err = res.All(ctx, &orders) + var orders []ListOrderEntry + err := h.db.GetOrders(ctx, &orders) if err != nil { h.logger.Error("Failed to list orders", "error", err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -282,7 +267,7 @@ func (h *handlers) handleCreateOrder(w http.ResponseWriter, r *http.Request) { Status: OrderStatusPending, } - _, err = h.orders.InsertOne(context.Background(), status) + err = h.db.InsertOrder(context.Background(), status) if err != nil { h.logger.Error("Failed to record workflow status", "error", err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -334,7 +319,7 @@ func (h *handlers) handleUpdateOrderStatus(w http.ResponseWriter, r *http.Reques return } - _, err = h.orders.UpdateOne(context.Background(), bson.M{"id": status.ID}, bson.M{"$set": bson.M{"status": status.Status}}) + err = h.db.UpdateOrderStatus(context.Background(), status.ID, status.Status) if err != nil { h.logger.Error("Failed to update order status", "error", err) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/app/server/server.go b/app/server/server.go index 26dc699..d8b4ea1 100644 --- a/app/server/server.go +++ b/app/server/server.go @@ -3,7 +3,6 @@ package server import ( "context" "crypto/tls" - _ "embed" "fmt" "log/slog" "net/http" @@ -13,11 +12,10 @@ import ( "github.com/temporalio/reference-app-orders-go/app/billing" "github.com/temporalio/reference-app-orders-go/app/config" + "github.com/temporalio/reference-app-orders-go/app/db" "github.com/temporalio/reference-app-orders-go/app/fraud" "github.com/temporalio/reference-app-orders-go/app/order" "github.com/temporalio/reference-app-orders-go/app/shipment" - mongodb "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" "go.temporal.io/sdk/client" "go.temporal.io/sdk/log" "golang.org/x/sync/errgroup" @@ -68,27 +66,6 @@ func CreateClientOptionsFromEnv() (client.Options, error) { return clientOpts, nil } -// SetupDB creates indexes in the database. -func SetupDB(db *mongodb.Database) error { - orders := db.Collection(order.OrdersCollection) - _, err := orders.Indexes().CreateOne(context.TODO(), mongodb.IndexModel{ - Keys: map[string]interface{}{"received_at": 1}, - }) - if err != nil { - return fmt.Errorf("failed to create database index: %w", err) - } - - shipments := db.Collection(shipment.ShipmentCollection) - _, err = shipments.Indexes().CreateOne(context.TODO(), mongodb.IndexModel{ - Keys: map[string]interface{}{"booked_at": 1}, - }) - if err != nil { - return fmt.Errorf("failed to create database index: %w", err) - } - - return nil -} - // RunWorkers runs workers for the requested services. func RunWorkers(ctx context.Context, config config.AppConfig, client client.Client, services []string) error { ctx, cancel := context.WithCancel(ctx) @@ -184,16 +161,15 @@ func RunAPIServers(ctx context.Context, config config.AppConfig, client client.C g, ctx := errgroup.WithContext(ctx) - var db *mongodb.Database + db := db.CreateDB(config) if slices.Contains(services, "orders") || slices.Contains(services, "shipment") { - c, err := mongodb.Connect(context.TODO(), options.Client().ApplyURI(config.MongoURL)) - db = c.Database("orders") + err := db.Connect(context.TODO()) if err != nil { - return fmt.Errorf("failed to open database: %w", err) + return fmt.Errorf("failed to connect to database: %w", err) } - if err := SetupDB(db); err != nil { + if err := db.Setup(); err != nil { return err } } diff --git a/app/shipment/api.go b/app/shipment/api.go index 1fa981b..4ead4bf 100644 --- a/app/shipment/api.go +++ b/app/shipment/api.go @@ -8,9 +8,7 @@ import ( "strings" "time" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" + "github.com/temporalio/reference-app-orders-go/app/db" "go.temporal.io/api/serviceerror" "go.temporal.io/sdk/client" ) @@ -21,9 +19,6 @@ const TaskQueue = "shipments" // StatusQuery is the name of the query to use to fetch a Shipment's status. const StatusQuery = "status" -// ShipmentCollection is the name of the MongoDB collection to use for Shipment data. -const ShipmentCollection = "shipments" - // ShipmentWorkflowID returns the workflow ID for a Shipment. func ShipmentWorkflowID(id string) string { return "Shipment:" + id @@ -35,9 +30,9 @@ func ShipmentIDFromWorkflowID(id string) string { } type handlers struct { - temporal client.Client - shipments *mongo.Collection - logger *slog.Logger + temporal client.Client + db db.DB + logger *slog.Logger } // ShipmentStatus holds the status of a Shipment. @@ -61,12 +56,10 @@ type ListShipmentEntry struct { } // Router implements the http.Handler interface for the Shipment API -func Router(client client.Client, db *mongo.Database, logger *slog.Logger) http.Handler { +func Router(client client.Client, db db.DB, logger *slog.Logger) http.Handler { r := http.NewServeMux() - shipments := db.Collection(ShipmentCollection) - - h := handlers{temporal: client, shipments: shipments, logger: logger} + h := handlers{temporal: client, db: db, logger: logger} r.HandleFunc("GET /shipments", h.handleListShipments) r.HandleFunc("GET /shipments/{id}", h.handleGetShipment) @@ -79,14 +72,7 @@ func Router(client client.Client, db *mongo.Database, logger *slog.Logger) http. func (h *handlers) handleListShipments(w http.ResponseWriter, _ *http.Request) { shipments := []ListShipmentEntry{} - res, err := h.shipments.Find(context.Background(), bson.M{}) - if err != nil { - h.logger.Error("Failed to list shipments: %v", "error", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - err = res.All(context.TODO(), &shipments) + err := h.db.GetShipments(context.Background(), &shipments) if err != nil { h.logger.Error("Failed to list shipments: %v", "error", err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -143,12 +129,7 @@ func (h *handlers) handleUpdateShipmentStatus(w http.ResponseWriter, r *http.Req return } - _, err = h.shipments.UpdateOne( - context.Background(), - bson.M{"id": status.ID}, - bson.M{"$set": bson.M{"status": status.Status}, "$setOnInsert": bson.M{"booked_at": time.Now().UTC()}}, - options.Update().SetUpsert(true), - ) + err = h.db.UpdateShipmentStatus(context.Background(), status.ID, status.Status) if err != nil { h.logger.Error("Failed to update shipment status: %v", "error", err) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/app/shipment/api_test.go b/app/shipment/api_test.go index 2b29981..750e0fd 100644 --- a/app/shipment/api_test.go +++ b/app/shipment/api_test.go @@ -14,11 +14,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/temporalio/reference-app-orders-go/app/server" + "github.com/temporalio/reference-app-orders-go/app/config" + "github.com/temporalio/reference-app-orders-go/app/db" "github.com/temporalio/reference-app-orders-go/app/shipment" "github.com/testcontainers/testcontainers-go/modules/mongodb" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" "go.temporal.io/sdk/mocks" ) @@ -30,18 +29,18 @@ func TestShipmentUpdate(t *testing.T) { mongoDBContainer, err := mongodb.Run(ctx, "mongo:6") require.NoError(t, err) - defer mongoDBContainer.Terminate(context.Background()) + defer mongoDBContainer.Terminate(ctx) - port, err := mongoDBContainer.MappedPort(context.Background(), "27017/tcp") + port, err := mongoDBContainer.MappedPort(ctx, "27017/tcp") require.NoError(t, err) - mc, err := mongo.Connect(context.Background(), options.Client().ApplyURI(fmt.Sprintf("mongodb://localhost:%s", port.Port()))) - require.NoError(t, err) + uri := fmt.Sprintf("mongodb://localhost:%s", port.Port()) - db := mc.Database("testdb") + config := config.AppConfig{MongoURL: uri} - err = server.SetupDB(db) - require.NoError(t, err) + db := db.CreateDB(config) + require.NoError(t, db.Connect(ctx)) + require.NoError(t, db.Setup()) logger := slog.Default() diff --git a/app/test/order_test.go b/app/test/order_test.go index c0bddde..384ebde 100644 --- a/app/test/order_test.go +++ b/app/test/order_test.go @@ -18,13 +18,11 @@ import ( "github.com/stretchr/testify/require" "github.com/temporalio/reference-app-orders-go/app/billing" "github.com/temporalio/reference-app-orders-go/app/config" + "github.com/temporalio/reference-app-orders-go/app/db" "github.com/temporalio/reference-app-orders-go/app/fraud" "github.com/temporalio/reference-app-orders-go/app/order" - "github.com/temporalio/reference-app-orders-go/app/server" "github.com/temporalio/reference-app-orders-go/app/shipment" "github.com/testcontainers/testcontainers-go/modules/mongodb" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" "go.temporal.io/sdk/client" "go.temporal.io/sdk/testsuite" "golang.org/x/sync/errgroup" @@ -108,25 +106,25 @@ func Test_Order(t *testing.T) { port, err := mongoDBContainer.MappedPort(ctx, "27017/tcp") require.NoError(t, err) - mc, err := mongo.Connect(ctx, options.Client().ApplyURI(fmt.Sprintf("mongodb://localhost:%s", port.Port()))) - require.NoError(t, err) + uri := fmt.Sprintf("mongodb://localhost:%s", port.Port()) - db := mc.Database("testdb") + config := config.AppConfig{ + MongoURL: uri, + BillingURL: billingAPI.URL, + FraudURL: fraudAPI.URL, + } - err = server.SetupDB(db) - require.NoError(t, err) + db := db.CreateDB(config) + require.NoError(t, db.Connect(ctx)) + require.NoError(t, db.Setup()) orderAPI := httptest.NewServer(order.Router(c, db, logger)) defer orderAPI.Close() shipmentAPI := httptest.NewServer(shipment.Router(c, db, logger)) defer shipmentAPI.Close() - config := config.AppConfig{ - BillingURL: billingAPI.URL, - OrderURL: orderAPI.URL, - ShipmentURL: shipmentAPI.URL, - FraudURL: fraudAPI.URL, - } + config.OrderURL = orderAPI.URL + config.ShipmentURL = shipmentAPI.URL g, ctx := errgroup.WithContext(ctx) diff --git a/go.mod b/go.mod index 8344a54..a379323 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.22.2 require ( github.com/google/uuid v1.6.0 + github.com/jmoiron/sqlx v1.4.0 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 diff --git a/go.sum b/go.sum index 354cc9f..8472815 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= @@ -53,6 +55,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -81,6 +85,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= @@ -93,10 +99,14 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=