From 8bb8cb30ec7d82de723897ecfa3a803193b69e38 Mon Sep 17 00:00:00 2001 From: Tigor Hutasuhut Date: Thu, 15 Aug 2024 09:36:06 +0700 Subject: [PATCH] refactor: now uses jet sql as much as possible --- go/api/devices_get_by_slug.go | 44 +++++++++++++------ go/api/devices_list.go | 64 ++++++++++++++++------------ go/cmd/bluemage/serve/serve.go | 16 +++++-- go/converts/converter.go | 5 ++- go/pkg/log/sql.go | 2 +- go/server/device_handlers.go | 2 +- go/server/interceptor.go | 25 +++++++++++ schemas/proto/device/v1/device.proto | 17 ++++++++ schemas/proto/device/v1/get.proto | 16 +------ 9 files changed, 128 insertions(+), 63 deletions(-) diff --git a/go/api/devices_get_by_slug.go b/go/api/devices_get_by_slug.go index 430a62e..bc56e3b 100644 --- a/go/api/devices_get_by_slug.go +++ b/go/api/devices_get_by_slug.go @@ -2,25 +2,43 @@ package api import ( "context" + "errors" "connectrpc.com/connect" - "github.com/tigorlazuardi/bluemage/go/gen/models" "github.com/tigorlazuardi/bluemage/go/pkg/errs" - "github.com/tigorlazuardi/bluemage/go/pkg/log" + "github.com/tigorlazuardi/bluemage/go/pkg/telemetry" + + "github.com/go-jet/jet/v2/qrm" + "github.com/tigorlazuardi/bluemage/go/gen/jet/model" + + . "github.com/go-jet/jet/v2/sqlite" + . "github.com/tigorlazuardi/bluemage/go/gen/jet/table" ) -func (api *API) GetDevice(ctx context.Context, slug string) (device *models.Device, err error) { - ctx, coll := log.WithQueryCollector(ctx) - device, err = models.FindDevice(ctx, api.Executor, slug) - if err != nil { - if err.Error() == "sql: no rows in result set" { - return device, errs.Wrapw(err, "device not found", - "slug", slug, - "query", coll, - ).Code(connect.CodeNotFound) - } +func (api *API) GetDevice(ctx context.Context, slug string) (device model.Devices, err error) { + ctx, span := tracer.Start(ctx, "GetDevice") + defer func() { telemetry.EndWithStatus(span, err) }() - return device, errs.Wrapw(err, "failed to find device", "slug", slug) + stmt := SELECT(Devices.AllColumns). + FROM(Devices). + WHERE(Devices.Slug.EQ(String(slug))) + + err = stmt.QueryContext(ctx, api.DB, &device) + if err != nil { + if errors.Is(err, qrm.ErrNoRows) { + return device, errs. + Wrapf(err, "device '%s' does not exist", slug). + Details( + "slug", slug, + "query", stmt.DebugSql(), + ). + Code(connect.CodeNotFound) + } + return device, errs.Wrapf(err, "failed to get device '%s'", slug). + Details( + "slug", slug, + "query", stmt.DebugSql(), + ) } return device, nil diff --git a/go/api/devices_list.go b/go/api/devices_list.go index cbaa532..2aeb6bf 100644 --- a/go/api/devices_list.go +++ b/go/api/devices_list.go @@ -3,66 +3,74 @@ package api import ( "strings" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/sqlite" - "github.com/stephenafamo/bob/dialect/sqlite/dialect" - "github.com/stephenafamo/bob/dialect/sqlite/sm" - "github.com/tigorlazuardi/bluemage/go/gen/models" device "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1" "github.com/tigorlazuardi/bluemage/go/pkg/errs" "golang.org/x/net/context" + + . "github.com/go-jet/jet/v2/sqlite" + "github.com/tigorlazuardi/bluemage/go/gen/jet/model" + . "github.com/tigorlazuardi/bluemage/go/gen/jet/table" ) -func queryFromListDeviceRequest(req *device.ListDevicesRequest) (expr []bob.Mod[*dialect.SelectQuery]) { +func listDevicesRequestSelectStatement(req *device.ListDevicesRequest) SelectStatement { + cond := Bool(true) switch req.Disabled { case device.DisabledFilter_DISABLED_FILTER_TRUE: - expr = append(expr, models.SelectWhere.Devices.Disabled.EQ(1)) + cond.AND(Devices.Disabled.EQ(Int(1))) case device.DisabledFilter_DISABLED_FILTER_FALSE: - expr = append(expr, models.SelectWhere.Devices.Disabled.EQ(0)) + cond.AND(Devices.Disabled.EQ(Int(0))) } if req.Search != "" { - arg := sqlite.Arg("%" + req.Search + "%") - expr = append(expr, - sm.Where( - models.DeviceColumns.Name.Like(arg).Or(models.DeviceColumns.Slug.Like(arg)), - ), + cond.AND( + Devices.Name.LIKE(String("%" + req.Search + "%")). + OR(Devices.Slug.LIKE(String("%" + req.Search + "%"))), ) } + stmt := SELECT(Devices.AllColumns). + FROM(Devices). + WHERE(cond) + if req.Limit > 0 { - expr = append(expr, sm.Limit(req.Limit)) + stmt.LIMIT(int64(req.Limit)) } if req.Offset > 0 { - expr = append(expr, sm.Offset(req.Offset)) + stmt.OFFSET(int64(req.Offset)) } if req.OrderBy == device.OrderBy_ORDER_BY_UNSPECIFIED { - return append(expr, sm.OrderBy(models.DeviceColumns.CreatedAt).Desc()) + return stmt.ORDER_BY(Devices.CreatedAt.DESC()) } orderByField, _ := strings.CutPrefix(device.OrderBy_name[int32(req.OrderBy)], "ORDER_BY_") orderByField = strings.ToLower(orderByField) - orderBy := sm.OrderBy(sqlite.Quote(orderByField)) - if req.Sort == device.Sort_SORT_DESCENDING { - expr = append(expr, orderBy.Desc()) - } else { - expr = append(expr, orderBy.Asc()) - } + orderBy := StringColumn(orderByField) - return expr + if req.Sort == device.Sort_SORT_DESCENDING { + return stmt.ORDER_BY(orderBy.DESC()) + } else { + return stmt.ORDER_BY(orderBy.ASC()) + } } func (api *API) DevicesList(ctx context.Context, req *device.ListDevicesRequest) (resp *device.ListDevicesResponse, err error) { + ctx, span := tracer.Start(ctx, "DevicesList") + defer span.End() + resp = &device.ListDevicesResponse{} - results, err := models.Devices.Query(ctx, api.Executor, queryFromListDeviceRequest(req)...).All() - if err != nil { - return resp, errs.Wrapw(err, "failed to list devices", "request", req) + stmt := listDevicesRequestSelectStatement(req) + var out []model.Devices + if err := stmt.QueryContext(ctx, api.DB, &out); err != nil { + return resp, errs.Wrapw(err, "failed to list devices", + "request", req, + "query", stmt.DebugSql(), + ) } - for _, result := range results { - resp.Devices = append(resp.Devices, convert.ModelsDeviceToGetDeviceResponse(result)) + for _, result := range out { + resp.Devices = append(resp.Devices, convert.JetModelDeviceToGetDeviceResponse(result)) } return resp, err } diff --git a/go/cmd/bluemage/serve/serve.go b/go/cmd/bluemage/serve/serve.go index 6fd965d..726a3d7 100644 --- a/go/cmd/bluemage/serve/serve.go +++ b/go/cmd/bluemage/serve/serve.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "os" + "path/filepath" "time" "connectrpc.com/connect" @@ -58,18 +59,24 @@ var Cmd = &cobra.Command{ return errs.Wrapw(err, "failed to create download directory", "path", cfg.String("download.directory")) } - dbPath := fmt.Sprintf("file:%s", cfg.String("db.path")) - sqldb, err := sql.Open(cfg.String("db.driver"), dbPath) + dbpath := cfg.String("db.path") + dir := filepath.Dir(dbpath) + if err := os.MkdirAll(dir, 0755); err != nil { + return errs.Wrapw(err, "failed to create database directory", "path", dir) + } + + dsn := fmt.Sprintf("file:%s", cfg.String("db.path")) + sqldb, err := sql.Open(cfg.String("db.driver"), dsn) if err != nil { return errs.Wrapw(err, "failed to open database", "driver", cfg.String("db.driver"), - "dsn", cfg.String("db.dsn"), + "dsn", dsn, ) } cleanups = append(cleanups, sqldb.Close) sqldb = sqldblogger.OpenDriver( - dbPath, + dsn, sqldb.Driver(), log.SQLLogger{}, sqldblogger.WithSQLQueryAsMessage(true), @@ -101,6 +108,7 @@ var Cmd = &cobra.Command{ mux.Handle(v1connect.NewDeviceServiceHandler(handler, connect.WithInterceptors( validationInterceptor, otelInterceptor, + server.ErrorMessageInterceptor(), server.LogInterceptor(), ))) diff --git a/go/converts/converter.go b/go/converts/converter.go index 8870458..ee1bb8b 100644 --- a/go/converts/converter.go +++ b/go/converts/converter.go @@ -2,6 +2,7 @@ package converts import ( "github.com/aarondl/opt/omit" + "github.com/tigorlazuardi/bluemage/go/gen/jet/model" "github.com/tigorlazuardi/bluemage/go/gen/models" device "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1" ) @@ -30,8 +31,8 @@ type DeviceConverter interface { // goverter:ignore state sizeCache unknownFields ModelsDeviceToCreateDeviceResponse(*models.Device) *device.CreateDeviceResponse // goverter:ignore state sizeCache unknownFields - // goverter:map NSFW Nsfw - ModelsDeviceToGetDeviceResponse(*models.Device) *device.GetDeviceResponse + // goverter:useZeroValueOnPointerInconsistency + JetModelDeviceToGetDeviceResponse(model.Devices) *device.GetDeviceResponse // goverter:ignore Slug SingleFolderMode CreatedAt UpdatedAt // goverter:map Nsfw NSFW diff --git a/go/pkg/log/sql.go b/go/pkg/log/sql.go index 5dbdc92..d4f283c 100644 --- a/go/pkg/log/sql.go +++ b/go/pkg/log/sql.go @@ -21,7 +21,7 @@ type QueryCollector struct { func (qu *QueryCollector) LogValue() slog.Value { return slog.GroupValue( - slog.String("statement", strings.ReplaceAll(qu.Statement, `"`, "")), + slog.String("statement", qu.Statement), slog.Any("args", qu.Args), ) } diff --git a/go/server/device_handlers.go b/go/server/device_handlers.go index a21b375..46d9ca9 100644 --- a/go/server/device_handlers.go +++ b/go/server/device_handlers.go @@ -39,7 +39,7 @@ func (d *DeviceHandler) GetDevice(ctx context.Context, request *connect.Request[ return nil, errs.IntoConnectError(err) } - devResp := deviceConvert.ModelsDeviceToGetDeviceResponse(dev) + devResp := deviceConvert.JetModelDeviceToGetDeviceResponse(dev) return connect.NewResponse(devResp), nil } diff --git a/go/server/interceptor.go b/go/server/interceptor.go index e99cd41..860ab26 100644 --- a/go/server/interceptor.go +++ b/go/server/interceptor.go @@ -2,6 +2,7 @@ package server import ( "context" + "errors" "fmt" "log/slog" "time" @@ -45,3 +46,27 @@ func LogInterceptor() connect.UnaryInterceptorFunc { } return connect.UnaryInterceptorFunc(interceptor) } + +func ErrorMessageInterceptor() connect.UnaryInterceptorFunc { + return func(uf connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, ar connect.AnyRequest) (connect.AnyResponse, error) { + resp, err := uf(ctx, ar) + if err == nil { + return resp, err + } + span := trace.SpanFromContext(ctx) + if !span.SpanContext().IsValid() { + return resp, err + } + + if cerr := new(connect.Error); errors.As(err, &cerr) { + if e := errs.FindError(cerr); e != nil { + msg := e.GetMessage() + e.Message("[%s] %s", span.SpanContext().SpanID().String(), msg) + } + } + + return resp, err + } + } +} diff --git a/schemas/proto/device/v1/device.proto b/schemas/proto/device/v1/device.proto index 1e8b365..4ba968f 100644 --- a/schemas/proto/device/v1/device.proto +++ b/schemas/proto/device/v1/device.proto @@ -38,3 +38,20 @@ service DeviceService { // CountDevices count the number of devices. rpc CountDevices(CountDevicesRequest) returns (CountDevicesResponse) {} } + +message Device { + string slug = 1; + bool disabled = 2; + string name = 3; + double resolution_x = 4; + double resolution_y = 5; + double aspect_ratio_tolerance = 6; + int32 min_x = 7; + int32 min_y = 8; + int32 max_x = 9; + int32 max_y = 10; + bool nsfw = 11; + bool single_folder_mode = 12; + int64 created_at = 13; + int64 updated_at = 14; +} diff --git a/schemas/proto/device/v1/get.proto b/schemas/proto/device/v1/get.proto index c0a99c3..18982c9 100644 --- a/schemas/proto/device/v1/get.proto +++ b/schemas/proto/device/v1/get.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package device.v1; import "buf/validate/validate.proto"; +import "device/v1/device.proto"; option go_package = "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1"; @@ -12,18 +13,5 @@ message GetDeviceRequest { } message GetDeviceResponse { - string slug = 1; - bool disabled = 2; - string name = 3; - double resolution_x = 4; - double resolution_y = 5; - double aspect_ratio_tolerance = 6; - int32 min_x = 7; - int32 min_y = 8; - int32 max_x = 9; - int32 max_y = 10; - bool nsfw = 11; - bool single_folder_mode = 12; - int64 created_at = 13; - int64 updated_at = 14; + Device device = 1; }