diff --git a/go.mod b/go.mod index 487c652a587..a95a78d6cbb 100644 --- a/go.mod +++ b/go.mod @@ -224,6 +224,7 @@ require ( github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect + github.com/DATA-DOG/go-sqlmock v1.5.2 // @grafana/grafana-search-and-storage github.com/FZambia/eagle v0.1.0 // indirect github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect github.com/Masterminds/goutils v1.1.1 // indirect diff --git a/go.sum b/go.sum index ff54aefd824..ebb105ae342 100644 --- a/go.sum +++ b/go.sum @@ -1284,6 +1284,8 @@ github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Code-Hex/go-generics-cache v1.3.1 h1:i8rLwyhoyhaerr7JpjtYjJZUcCbWOdiYO3fZXLiEC4g= github.com/Code-Hex/go-generics-cache v1.3.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/FZambia/eagle v0.1.0 h1:9gyX6x+xjoIfglgyPTcYm7dvY7FJ93us1QY5De4CyXA= github.com/FZambia/eagle v0.1.0/go.mod h1:YjGSPVkQTNcVLfzEUQJNgW9ScPR0K4u/Ky0yeFa4oDA= @@ -2459,6 +2461,7 @@ github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 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/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= diff --git a/pkg/services/store/entity/db/dbimpl/db.go b/pkg/services/store/entity/db/dbimpl/db.go new file mode 100644 index 00000000000..76450b56c0e --- /dev/null +++ b/pkg/services/store/entity/db/dbimpl/db.go @@ -0,0 +1,59 @@ +package dbimpl + +import ( + "context" + "database/sql" + "fmt" + + entitydb "github.com/grafana/grafana/pkg/services/store/entity/db" +) + +func NewDB(d *sql.DB, driverName string) entitydb.DB { + return sqldb{ + DB: d, + driverName: driverName, + } +} + +type sqldb struct { + *sql.DB + driverName string +} + +func (d sqldb) DriverName() string { + return d.driverName +} + +func (d sqldb) BeginTx(ctx context.Context, opts *sql.TxOptions) (entitydb.Tx, error) { + t, err := d.DB.BeginTx(ctx, opts) + if err != nil { + return nil, err + } + return tx{ + Tx: t, + }, nil +} + +func (d sqldb) WithTx(ctx context.Context, opts *sql.TxOptions, f entitydb.TxFunc) error { + t, err := d.BeginTx(ctx, opts) + if err != nil { + return fmt.Errorf("begin tx: %w", err) + } + + if err := f(ctx, t); err != nil { + if rollbackErr := t.Rollback(); rollbackErr != nil { + return fmt.Errorf("tx err: %w; rollback err: %w", err, rollbackErr) + } + return fmt.Errorf("tx err: %w", err) + } + + if err = t.Commit(); err != nil { + return fmt.Errorf("commit err: %w", err) + } + + return nil +} + +type tx struct { + *sql.Tx +} diff --git a/pkg/services/store/entity/db/dbimpl/db_test.go b/pkg/services/store/entity/db/dbimpl/db_test.go new file mode 100644 index 00000000000..2a6b93a871a --- /dev/null +++ b/pkg/services/store/entity/db/dbimpl/db_test.go @@ -0,0 +1,54 @@ +package dbimpl + +import ( + "context" + "testing" + "time" + + sqlmock "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + + entitydb "github.com/grafana/grafana/pkg/services/store/entity/db" +) + +func newCtx(t *testing.T) context.Context { + t.Helper() + + d, ok := t.Deadline() + if !ok { + // provide a default timeout for tests + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + t.Cleanup(cancel) + + return ctx + } + + ctx, cancel := context.WithDeadline(context.Background(), d) + t.Cleanup(cancel) + + return ctx +} + +func TestDB_WithTx(t *testing.T) { + t.Parallel() + + newTxFunc := func(err error) entitydb.TxFunc { + return func(context.Context, entitydb.Tx) error { + return err + } + } + + t.Run("happy path", func(t *testing.T) { + t.Parallel() + + sqldb, mock, err := sqlmock.New() + require.NoError(t, err) + db := NewDB(sqldb, "sqlmock") + + mock.ExpectBegin() + mock.ExpectCommit() + err = db.WithTx(newCtx(t), nil, newTxFunc(nil)) + + require.NoError(t, err) + }) +} diff --git a/pkg/services/store/entity/db/dbimpl/dbimpl.go b/pkg/services/store/entity/db/dbimpl/dbimpl.go index b064f268a25..7f9863b2fb6 100644 --- a/pkg/services/store/entity/db/dbimpl/dbimpl.go +++ b/pkg/services/store/entity/db/dbimpl/dbimpl.go @@ -4,6 +4,10 @@ import ( "fmt" "github.com/dlmiddlecote/sqlstats" + "github.com/jmoiron/sqlx" + "github.com/prometheus/client_golang/prometheus" + "xorm.io/xorm" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -12,9 +16,6 @@ import ( entitydb "github.com/grafana/grafana/pkg/services/store/entity/db" "github.com/grafana/grafana/pkg/services/store/entity/db/migrations" "github.com/grafana/grafana/pkg/setting" - "github.com/jmoiron/sqlx" - "github.com/prometheus/client_golang/prometheus" - "xorm.io/xorm" ) var _ entitydb.EntityDBInterface = (*EntityDB)(nil) @@ -128,3 +129,14 @@ func (db *EntityDB) GetSession() (*session.SessionDB, error) { func (db *EntityDB) GetCfg() *setting.Cfg { return db.cfg } + +func (db *EntityDB) GetDB() (entitydb.DB, error) { + engine, err := db.GetEngine() + if err != nil { + return nil, err + } + + ret := NewDB(engine.DB().DB, engine.Dialect().DriverName()) + + return ret, nil +} diff --git a/pkg/services/store/entity/db/migrations/entity_store_mig.go b/pkg/services/store/entity/db/migrations/entity_store_mig.go index 81f194562a5..54051883719 100644 --- a/pkg/services/store/entity/db/migrations/entity_store_mig.go +++ b/pkg/services/store/entity/db/migrations/entity_store_mig.go @@ -187,6 +187,20 @@ func initEntityTables(mg *migrator.Migrator) string { }, }) + tables = append(tables, migrator.Table{ + Name: "kind_version", + Columns: []*migrator.Column{ + {Name: "group", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, + {Name: "resource", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, + {Name: "resource_version", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "created_at", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "updated_at", Type: migrator.DB_BigInt, Nullable: false}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"group", "resource"}, Type: migrator.UniqueIndex}, + }, + }) + // Initialize all tables for t := range tables { mg.AddMigration("drop table "+tables[t].Name, migrator.NewDropTableMigration(tables[t].Name)) diff --git a/pkg/services/store/entity/db/service.go b/pkg/services/store/entity/db/service.go index 7a5414dacd6..58801051904 100755 --- a/pkg/services/store/entity/db/service.go +++ b/pkg/services/store/entity/db/service.go @@ -1,16 +1,58 @@ package db import ( + "context" + "database/sql" + "xorm.io/xorm" - // "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/session" "github.com/grafana/grafana/pkg/setting" ) +const ( + DriverPostgres = "postgres" + DriverMySQL = "mysql" + DriverSQLite3 = "sqlite3" +) + +// EntityDBInterface provides access to a database capable of supporting the +// Entity Server. type EntityDBInterface interface { Init() error + GetCfg() *setting.Cfg + GetDB() (DB, error) + + // TODO: deprecate. GetSession() (*session.SessionDB, error) GetEngine() (*xorm.Engine, error) - GetCfg() *setting.Cfg +} + +// DB is a thin abstraction on *sql.DB to allow mocking to provide better unit +// testing. We purposefully hide database operation methods that would use +// context.Background(). +type DB interface { + ContextExecer + BeginTx(context.Context, *sql.TxOptions) (Tx, error) + WithTx(context.Context, *sql.TxOptions, TxFunc) error + PingContext(context.Context) error + Stats() sql.DBStats + DriverName() string +} + +type TxFunc = func(context.Context, Tx) error + +type Tx interface { + ContextExecer + Exec(query string, args ...any) (sql.Result, error) + Query(query string, args ...any) (*sql.Rows, error) + QueryRow(query string, args ...any) *sql.Row + Commit() error + Rollback() error +} + +type ContextExecer interface { + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row } diff --git a/pkg/services/store/entity/sqlstash/data/entity_delete.sql b/pkg/services/store/entity/sqlstash/data/entity_delete.sql new file mode 100644 index 00000000000..6948c19c38e --- /dev/null +++ b/pkg/services/store/entity/sqlstash/data/entity_delete.sql @@ -0,0 +1,7 @@ +DELETE FROM {{ .Ident "entity" }} + WHERE 1 = 1 + AND {{ .Ident "namespace" }} = {{ .Arg .Key.Namespace }} + AND {{ .Ident "group" }} = {{ .Arg .Key.Group }} + AND {{ .Ident "resource" }} = {{ .Arg .Key.Resource }} + AND {{ .Ident "name" }} = {{ .Arg .Key.Name }} +; diff --git a/pkg/services/store/entity/sqlstash/data/entity_folder_insert.sql b/pkg/services/store/entity/sqlstash/data/entity_folder_insert.sql new file mode 100644 index 00000000000..0f21145d267 --- /dev/null +++ b/pkg/services/store/entity/sqlstash/data/entity_folder_insert.sql @@ -0,0 +1,35 @@ +INSERT INTO {{ .Ident "entity_folder" }} + ( + {{ .Ident "guid" }}, + {{ .Ident "namespace" }}, + {{ .Ident "name" }}, + {{ .Ident "slug_path" }}, + {{ .Ident "tree" }}, + {{ .Ident "depth" }}, + {{ .Ident "lft" }}, + {{ .Ident "rgt" }}, + {{ .Ident "detached" }} + ) + + VALUES + {{ $this := . }} + {{ $addComma := false }} + {{ range .Items }} + {{ if $addComma }} + , + {{ end }} + {{ $addComma = true }} + + ( + {{ $this.Arg .GUID }}, + {{ $this.Arg .Namespace }}, + {{ $this.Arg .UID }}, + {{ $this.Arg .SlugPath }}, + {{ $this.Arg .JS }}, + {{ $this.Arg .Depth }}, + {{ $this.Arg .Left }}, + {{ $this.Arg .Right }}, + {{ $this.Arg .Detached }} + ) + {{ end }} +; diff --git a/pkg/services/store/entity/sqlstash/data/entity_insert.sql b/pkg/services/store/entity/sqlstash/data/entity_insert.sql new file mode 100644 index 00000000000..78622ccc6c1 --- /dev/null +++ b/pkg/services/store/entity/sqlstash/data/entity_insert.sql @@ -0,0 +1,93 @@ +INSERT INTO + + {{/* Determine which table to insert into */}} + {{ if .TableEntity }} {{ .Ident "entity" }} + {{ else }} {{ .Ident "entity_history" }} + {{ end }} + + {{/* Explicitly specify fields that will be set */}} + ( + {{ .Ident "guid" }}, + {{ .Ident "resource_version" }}, + + {{ .Ident "key" }}, + + {{ .Ident "group" }}, + {{ .Ident "group_version" }}, + {{ .Ident "resource" }}, + {{ .Ident "namespace" }}, + {{ .Ident "name" }}, + + {{ .Ident "folder" }}, + + {{ .Ident "meta" }}, + {{ .Ident "body" }}, + {{ .Ident "status" }}, + + {{ .Ident "size" }}, + {{ .Ident "etag" }}, + + {{ .Ident "created_at" }}, + {{ .Ident "created_by" }}, + {{ .Ident "updated_at" }}, + {{ .Ident "updated_by" }}, + + {{ .Ident "origin" }}, + {{ .Ident "origin_key" }}, + {{ .Ident "origin_ts" }}, + + {{ .Ident "title" }}, + {{ .Ident "slug" }}, + {{ .Ident "description" }}, + + {{ .Ident "message" }}, + {{ .Ident "labels" }}, + {{ .Ident "fields" }}, + {{ .Ident "errors" }}, + + {{ .Ident "action" }} + ) + + {{/* Provide the values */}} + VALUES ( + {{ .Arg .Entity.Guid }}, + {{ .Arg .Entity.ResourceVersion }}, + + {{ .Arg .Entity.Key }}, + + {{ .Arg .Entity.Group }}, + {{ .Arg .Entity.GroupVersion }}, + {{ .Arg .Entity.Resource }}, + {{ .Arg .Entity.Namespace }}, + {{ .Arg .Entity.Name }}, + + {{ .Arg .Entity.Folder }}, + + {{ .Arg .Entity.Meta }}, + {{ .Arg .Entity.Body }}, + {{ .Arg .Entity.Status }}, + + {{ .Arg .Entity.Size }}, + {{ .Arg .Entity.ETag }}, + + {{ .Arg .Entity.CreatedAt }}, + {{ .Arg .Entity.CreatedBy }}, + {{ .Arg .Entity.UpdatedAt }}, + {{ .Arg .Entity.UpdatedBy }}, + + {{ .Arg .Entity.Origin.Source }}, + {{ .Arg .Entity.Origin.Key }}, + {{ .Arg .Entity.Origin.Time }}, + + {{ .Arg .Entity.Title }}, + {{ .Arg .Entity.Slug }}, + {{ .Arg .Entity.Description }}, + + {{ .Arg .Entity.Message }}, + {{ .Arg .Entity.Labels }}, + {{ .Arg .Entity.Fields }}, + {{ .Arg .Entity.Errors }}, + + {{ .Arg .Entity.Action }} + ) +; diff --git a/pkg/services/store/entity/sqlstash/data/entity_labels_delete.sql b/pkg/services/store/entity/sqlstash/data/entity_labels_delete.sql new file mode 100644 index 00000000000..3f9794d95ed --- /dev/null +++ b/pkg/services/store/entity/sqlstash/data/entity_labels_delete.sql @@ -0,0 +1,18 @@ +DELETE FROM {{ .Ident "entity_labels" }} + WHERE 1 = 1 + AND {{ .Ident "guid" }} = {{ .Arg .GUID }} + {{ if gt (len .KeepLabels) 0 }} + AND {{ .Ident "label" }} NOT IN ( + {{ $this := . }} + {{ $addComma := false }} + {{ range .KeepLabels }} + {{ if $addComma }} + , + {{ end }} + {{ $addComma = true }} + + {{ $this.Arg . }} + {{ end }} + ) + {{ end }} +; diff --git a/pkg/services/store/entity/sqlstash/data/entity_labels_insert.sql b/pkg/services/store/entity/sqlstash/data/entity_labels_insert.sql new file mode 100644 index 00000000000..daf3ec6e977 --- /dev/null +++ b/pkg/services/store/entity/sqlstash/data/entity_labels_insert.sql @@ -0,0 +1,29 @@ +INSERT INTO {{ .Ident "entity_labels" }} + ( + {{ .Ident "guid" }}, + {{ .Ident "label" }}, + {{ .Ident "value" }} + ) + + VALUES + {{/* + When we enter the "range" loop the "." will be changed, so we need to + store the current ".GUID" in a variable to be able to use its value + */}} + {{ $guid := .GUID }} + + {{ $this := . }} + {{ $addComma := false }} + {{ range $name, $value := .Labels }} + {{ if $addComma }} + , + {{ end }} + {{ $addComma = true }} + + ( + {{ $this.Arg $guid }}, + {{ $this.Arg $name }}, + {{ $this.Arg $value }} + ) + {{ end }} +; diff --git a/pkg/services/store/entity/sqlstash/data/entity_list_folder_elements.sql b/pkg/services/store/entity/sqlstash/data/entity_list_folder_elements.sql new file mode 100644 index 00000000000..7dc4778dc8f --- /dev/null +++ b/pkg/services/store/entity/sqlstash/data/entity_list_folder_elements.sql @@ -0,0 +1,14 @@ +SELECT + {{ .Ident "guid" | .Into .FolderInfo.GUID }}, + {{ .Ident "name" | .Into .FolderInfo.UID }}, + {{ .Ident "folder" | .Into .FolderInfo.ParentUID }}, + {{ .Ident "name" | .Into .FolderInfo.Name }}, + {{ .Ident "slug" | .Into .FolderInfo.Slug }} + + FROM {{ .Ident "entity" }} + + WHERE 1 = 1 + AND {{ .Ident "group" }} = {{ .Arg .Group }} + AND {{ .Ident "resource" }} = {{ .Arg .Resource }} + AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }} +; diff --git a/pkg/services/store/entity/sqlstash/data/entity_read.sql b/pkg/services/store/entity/sqlstash/data/entity_read.sql new file mode 100644 index 00000000000..11143daef20 --- /dev/null +++ b/pkg/services/store/entity/sqlstash/data/entity_read.sql @@ -0,0 +1,78 @@ +SELECT + {{ .Ident "guid" | .Into .Entity.Guid }}, + {{ .Ident "resource_version" | .Into .Entity.ResourceVersion }}, + + {{ .Ident "key" | .Into .Entity.Key }}, + + {{ .Ident "group" | .Into .Entity.Group }}, + {{ .Ident "group_version" | .Into .Entity.GroupVersion }}, + {{ .Ident "resource" | .Into .Entity.Resource }}, + {{ .Ident "namespace" | .Into .Entity.Namespace }}, + {{ .Ident "name" | .Into .Entity.Name }}, + + {{ .Ident "folder" | .Into .Entity.Folder }}, + + {{ .Ident "meta" | .Into .Entity.Meta }}, + {{ .Ident "body" | .Into .Entity.Body }}, + {{ .Ident "status" | .Into .Entity.Status }}, + + {{ .Ident "size" | .Into .Entity.Size }}, + {{ .Ident "etag" | .Into .Entity.ETag }}, + + {{ .Ident "created_at" | .Into .Entity.CreatedAt }}, + {{ .Ident "created_by" | .Into .Entity.CreatedBy }}, + {{ .Ident "updated_at" | .Into .Entity.UpdatedAt }}, + {{ .Ident "updated_by" | .Into .Entity.UpdatedBy }}, + + {{ .Ident "origin" | .Into .Entity.Origin.Source }}, + {{ .Ident "origin_key" | .Into .Entity.Origin.Key }}, + {{ .Ident "origin_ts" | .Into .Entity.Origin.Time }}, + + {{ .Ident "title" | .Into .Entity.Title }}, + {{ .Ident "slug" | .Into .Entity.Slug }}, + {{ .Ident "description" | .Into .Entity.Description }}, + + {{ .Ident "message" | .Into .Entity.Message }}, + {{ .Ident "labels" | .Into .Entity.Labels }}, + {{ .Ident "fields" | .Into .Entity.Fields }}, + {{ .Ident "errors" | .Into .Entity.Errors }}, + + {{ .Ident "action" | .Into .Entity.Action }} + + FROM + {{ if gt .ResourceVersion 0 }} + {{ .Ident "entity_history" }} + {{ else }} + {{ .Ident "entity" }} + {{ end }} + + WHERE 1 = 1 + AND {{ .Ident "namespace" }} = {{ .Arg .Key.Namespace }} + AND {{ .Ident "group" }} = {{ .Arg .Key.Group }} + AND {{ .Ident "resource" }} = {{ .Arg .Key.Resource }} + AND {{ .Ident "name" }} = {{ .Arg .Key.Name }} + + {{/* + Resource versions work like snapshots at the kind level. Thus, a request + to retrieve a specific resource version should be interpreted as asking + for a resource as of how it existed at that point in time. This is why we + request matching entities with at most the provided resource version, and + return only the one with the highest resource version. In the case of not + specifying a resource version (i.e. resource version zero), it is + interpreted as the latest version of the given entity, thus we instead + query the "entity" table (which holds only the latest version of + non-deleted entities) and we don't need to specify anything else. The + "entity" table has a unique constraint on (namespace, group, resource, + name), so we're guaranteed to have at most one matching row. + */}} + {{ if gt .ResourceVersion 0 }} + AND {{ .Ident "resource_version" }} <= {{ .Arg .ResourceVersion }} + + ORDER BY {{ .Ident "resource_version" }} DESC + LIMIT 1 + {{ end }} + + {{ if .SelectForUpdate }} + {{ .SelectFor "UPDATE" }} + {{ end }} +; diff --git a/pkg/services/store/entity/sqlstash/data/entity_ref_find.sql b/pkg/services/store/entity/sqlstash/data/entity_ref_find.sql new file mode 100644 index 00000000000..a382e609261 --- /dev/null +++ b/pkg/services/store/entity/sqlstash/data/entity_ref_find.sql @@ -0,0 +1,53 @@ +SELECT + e.{{ .Ident "guid" | .Into .Entity.Guid }}, + e.{{ .Ident "resource_version" | .Into .Entity.ResourceVersion }}, + + e.{{ .Ident "key" | .Into .Entity.Key }}, + + e.{{ .Ident "group" | .Into .Entity.Group }}, + e.{{ .Ident "group_version" | .Into .Entity.GroupVersion }}, + e.{{ .Ident "resource" | .Into .Entity.Resource }}, + e.{{ .Ident "namespace" | .Into .Entity.Namespace }}, + e.{{ .Ident "name" | .Into .Entity.Name }}, + + e.{{ .Ident "folder" | .Into .Entity.Folder }}, + + e.{{ .Ident "meta" | .Into .Entity.Meta }}, + e.{{ .Ident "body" | .Into .Entity.Body }}, + e.{{ .Ident "status" | .Into .Entity.Status }}, + + e.{{ .Ident "size" | .Into .Entity.Size }}, + e.{{ .Ident "etag" | .Into .Entity.ETag }}, + + e.{{ .Ident "created_at" | .Into .Entity.CreatedAt }}, + e.{{ .Ident "created_by" | .Into .Entity.CreatedBy }}, + e.{{ .Ident "updated_at" | .Into .Entity.UpdatedAt }}, + e.{{ .Ident "updated_by" | .Into .Entity.UpdatedBy }}, + + e.{{ .Ident "origin" | .Into .Entity.Origin.Source }}, + e.{{ .Ident "origin_key" | .Into .Entity.Origin.Key }}, + e.{{ .Ident "origin_ts" | .Into .Entity.Origin.Time }}, + + e.{{ .Ident "title" | .Into .Entity.Title }}, + e.{{ .Ident "slug" | .Into .Entity.Slug }}, + e.{{ .Ident "description" | .Into .Entity.Description }}, + + e.{{ .Ident "message" | .Into .Entity.Message }}, + e.{{ .Ident "labels" | .Into .Entity.Labels }}, + e.{{ .Ident "fields" | .Into .Entity.Fields }}, + e.{{ .Ident "errors" | .Into .Entity.Errors }}, + + e.{{ .Ident "action" | .Into .Entity.Action }} + + FROM + {{ .Ident "entity_ref" }} AS r + INNER JOIN + {{ .Ident "entity" }} AS e + ON r.{{ .Ident "guid" }} = e.{{ .Ident "guid" }} + + WHERE 1 = 1 + AND r.{{ .Ident "namespace" }} = {{ .Arg .Request.Namespace }} + AND r.{{ .Ident "group" }} = {{ .Arg .Request.Group }} + AND r.{{ .Ident "resource" }} = {{ .Arg .Request.Resource }} + AND r.{{ .Ident "resolved_to" }} = {{ .Arg .Request.Name }} +; diff --git a/pkg/services/store/entity/sqlstash/data/entity_update.sql b/pkg/services/store/entity/sqlstash/data/entity_update.sql new file mode 100644 index 00000000000..8acb73e150c --- /dev/null +++ b/pkg/services/store/entity/sqlstash/data/entity_update.sql @@ -0,0 +1,34 @@ +UPDATE {{ .Ident "entity" }} SET + {{ .Ident "resource_version" }} = {{ .Arg .Entity.ResourceVersion }}, + + {{ .Ident "group_version" }} = {{ .Arg .Entity.GroupVersion }}, + + {{ .Ident "folder" }} = {{ .Arg .Entity.Folder }}, + + {{ .Ident "meta" }} = {{ .Arg .Entity.Meta }}, + {{ .Ident "body" }} = {{ .Arg .Entity.Body }}, + {{ .Ident "status" }} = {{ .Arg .Entity.Status }}, + + {{ .Ident "size" }} = {{ .Arg .Entity.Size }}, + {{ .Ident "etag" }} = {{ .Arg .Entity.ETag }}, + + {{ .Ident "updated_at" }} = {{ .Arg .Entity.UpdatedAt }}, + {{ .Ident "updated_by" }} = {{ .Arg .Entity.UpdatedBy }}, + + {{ .Ident "origin" }} = {{ .Arg .Entity.Origin.Source }}, + {{ .Ident "origin_key" }} = {{ .Arg .Entity.Origin.Key }}, + {{ .Ident "origin_ts" }} = {{ .Arg .Entity.Origin.Time }}, + + {{ .Ident "title" }} = {{ .Arg .Entity.Title }}, + {{ .Ident "slug" }} = {{ .Arg .Entity.Slug }}, + {{ .Ident "description" }} = {{ .Arg .Entity.Description }}, + + {{ .Ident "message" }} = {{ .Arg .Entity.Message }}, + {{ .Ident "labels" }} = {{ .Arg .Entity.Labels }}, + {{ .Ident "fields" }} = {{ .Arg .Entity.Fields }}, + {{ .Ident "errors" }} = {{ .Arg .Entity.Errors }}, + + {{ .Ident "action" }} = {{ .Arg .Entity.Action }} + + WHERE {{ .Ident "guid" }} = {{ .Arg .Entity.Guid }} +; diff --git a/pkg/services/store/entity/sqlstash/data/kind_version_inc.sql b/pkg/services/store/entity/sqlstash/data/kind_version_inc.sql new file mode 100644 index 00000000000..1625cf764f6 --- /dev/null +++ b/pkg/services/store/entity/sqlstash/data/kind_version_inc.sql @@ -0,0 +1,7 @@ +UPDATE {{ .Ident "kind_version" }} + SET {{ .Ident "resource_version" }} = {{ .Arg .ResourceVersion }} + 1 + WHERE 1 = 1 + AND {{ .Ident "group" }} = {{ .Arg .Group }} + AND {{ .Ident "resource" }} = {{ .Arg .Resource }} + AND {{ .Ident "resource_version" }} = {{ .Arg .ResourceVersion }} +; diff --git a/pkg/services/store/entity/sqlstash/data/kind_version_insert.sql b/pkg/services/store/entity/sqlstash/data/kind_version_insert.sql new file mode 100644 index 00000000000..52cab7794c1 --- /dev/null +++ b/pkg/services/store/entity/sqlstash/data/kind_version_insert.sql @@ -0,0 +1,13 @@ +INSERT INTO {{ .Ident "kind_version" }} + ( + {{ .Ident "group" }}, + {{ .Ident "resource" }}, + {{ .Ident "resource_version" }} + ) + + VALUES ( + {{ .Arg .Group }}, + {{ .Arg .Resource }}, + 1 + ) +; diff --git a/pkg/services/store/entity/sqlstash/data/kind_version_lock.sql b/pkg/services/store/entity/sqlstash/data/kind_version_lock.sql new file mode 100644 index 00000000000..fe514ac77a6 --- /dev/null +++ b/pkg/services/store/entity/sqlstash/data/kind_version_lock.sql @@ -0,0 +1,7 @@ +SELECT {{ .Ident "resource_version" | .Into .ResourceVersion }} + FROM {{ .Ident "kind_version" }} + WHERE 1 = 1 + AND {{ .Ident "group" }} = {{ .Arg .Group }} + AND {{ .Ident "resource" }} = {{ .Arg .Resource }} + {{ .SelectFor "UPDATE" }} +; diff --git a/pkg/services/store/entity/sqlstash/queries.go b/pkg/services/store/entity/sqlstash/queries.go new file mode 100644 index 00000000000..836f1fc8752 --- /dev/null +++ b/pkg/services/store/entity/sqlstash/queries.go @@ -0,0 +1,164 @@ +package sqlstash + +import ( + "embed" + "fmt" + "text/template" + + "github.com/grafana/grafana/pkg/services/store/entity" + "github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate" +) + +// Templates. +var ( + //go:embed data + templatesFs embed.FS + + // all templates + templates = template.Must(template.ParseFS(templatesFs, `data/*.sql`)) + + sqlEntityDelete = getTemplate("entity_delete.sql") + sqlEntityInsert = getTemplate("entity_insert.sql") + sqlEntityListFolderElements = getTemplate("entity_list_folder_elements.sql") + sqlEntityUpdate = getTemplate("entity_update.sql") + sqlEntityRead = getTemplate("entity_read.sql") + + sqlEntityFolderInsert = getTemplate("entity_folder_insert.sql") + + sqlEntityRefFind = getTemplate("entity_ref_find.sql") + + sqlEntityLabelsDelete = getTemplate("entity_labels_delete.sql") + sqlEntityLabelsInsert = getTemplate("entity_labels_insert.sql") + + sqlKindVersionInc = getTemplate("kind_version_inc.sql") + sqlKindVersionInsert = getTemplate("kind_version_insert.sql") + sqlKindVersionLock = getTemplate("kind_version_lock.sql") +) + +func getTemplate(filename string) *template.Template { + if t := templates.Lookup(filename); t != nil { + return t + } + panic(fmt.Sprintf("template file not found: %s", filename)) +} + +type sqlEntityFolderInsertRequest struct { + *sqltemplate.SQLTemplate + Items []*sqlEntityFolderInsertRequestItem +} + +type sqlEntityFolderInsertRequestItem struct { + GUID string + Namespace string + UID string + SlugPath string + JS string + Depth int32 + Left int32 + Right int32 + Detached bool +} + +type sqlEntityRefFindRequest struct { + *sqltemplate.SQLTemplate + Request *entity.ReferenceRequest + Entity *withSerialized +} + +type sqlEntityLabelsInsertRequest struct { + *sqltemplate.SQLTemplate + GUID string + Labels map[string]string +} + +type sqlEntityLabelsDeleteRequest struct { + *sqltemplate.SQLTemplate + GUID string + KeepLabels []string +} + +type sqlKindVersionLockRequest struct { + *sqltemplate.SQLTemplate + Group string + GroupVersion string + Resource string + ResourceVersion int64 +} + +type sqlKindVersionIncRequest struct { + *sqltemplate.SQLTemplate + Group string + GroupVersion string + Resource string + ResourceVersion int64 +} + +type sqlKindVersionInsertRequest struct { + *sqltemplate.SQLTemplate + Group string + GroupVersion string + Resource string +} + +type sqlEntityListFolderElementsRequest struct { + *sqltemplate.SQLTemplate + Group string + Resource string + Namespace string + FolderInfo *folderInfo +} + +type sqlEntityReadRequest struct { + *sqltemplate.SQLTemplate + Key *entity.Key + ResourceVersion int64 + SelectForUpdate bool + Entity *withSerialized +} + +type sqlEntityDeleteRequest struct { + *sqltemplate.SQLTemplate + Key *entity.Key +} + +type sqlEntityInsertRequest struct { + *sqltemplate.SQLTemplate + Entity *withSerialized + + // TableEntity, when true, means we will insert into table "entity", and + // into table "entity_history" otherwise. + TableEntity bool +} + +type sqlEntityUpdateRequest struct { + *sqltemplate.SQLTemplate + Entity *withSerialized +} + +// withSerialized provides access to the wire Entiity DTO as well as the +// serialized version of some of its fields suitable to be read from or written +// to the database. +type withSerialized struct { + *entity.Entity + + Labels []byte + Fields []byte + Errors []byte +} + +// TODO: remove once we start using these symbols. Prevents `unused` linter +// until the next PR. +var ( + _, _, _ = sqlEntityDelete, sqlEntityInsert, sqlEntityListFolderElements + _, _, _ = sqlEntityUpdate, sqlEntityRead, sqlEntityFolderInsert + _, _, _ = sqlEntityRefFind, sqlEntityLabelsDelete, sqlEntityLabelsInsert + _, _, _ = sqlKindVersionInc, sqlKindVersionInsert, sqlKindVersionLock + _, _ = sqlEntityFolderInsertRequest{}, sqlEntityFolderInsertRequestItem{} + _, _ = sqlEntityRefFindRequest{}, sqlEntityLabelsInsertRequest{} + _, _ = sqlEntityLabelsInsertRequest{}, sqlEntityLabelsDeleteRequest{} + _, _ = sqlKindVersionLockRequest{}, sqlKindVersionIncRequest{} + _, _ = sqlKindVersionInsertRequest{}, sqlEntityListFolderElementsRequest{} + _, _ = sqlEntityReadRequest{}, sqlEntityDeleteRequest{} + _, _ = sqlEntityInsertRequest{}, sqlEntityUpdateRequest{} + _ = withSerialized{} +)