mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Implement SQL store
This commit is contained in:
parent
95ffa3486b
commit
774ae238cb
@ -22,7 +22,7 @@ import (
|
||||
|
||||
// Package-level errors.
|
||||
var (
|
||||
ErrNotFound = errors.New("entity not found")
|
||||
ErrNotFound = errors.New("resource not found")
|
||||
ErrOptimisticLockingFailed = errors.New("optimistic locking failed")
|
||||
ErrUserNotFoundInContext = errors.New("user not found in context")
|
||||
ErrUnableToReadResourceJSON = errors.New("unable to read resource json")
|
||||
|
7
pkg/storage/unified/sql/data/resource_delete.sql
Normal file
7
pkg/storage/unified/sql/data/resource_delete.sql
Normal file
@ -0,0 +1,7 @@
|
||||
DELETE FROM {{ .Ident "resource" }}
|
||||
WHERE 1 = 1
|
||||
AND {{ .Ident "namespace" }} = {{ .Arg .WriteEvent.Key.Namespace }}
|
||||
AND {{ .Ident "group" }} = {{ .Arg .WriteEvent.Key.Group }}
|
||||
AND {{ .Ident "resource" }} = {{ .Arg .WriteEvent.Key.Resource }}
|
||||
AND {{ .Ident "name" }} = {{ .Arg .WriteEvent.Key.Name }}
|
||||
;
|
23
pkg/storage/unified/sql/data/resource_insert.sql
Normal file
23
pkg/storage/unified/sql/data/resource_insert.sql
Normal file
@ -0,0 +1,23 @@
|
||||
INSERT INTO {{ .Ident "resource" }}
|
||||
|
||||
(
|
||||
{{ .Ident "guid" }},
|
||||
{{ .Ident "group" }},
|
||||
{{ .Ident "resource" }},
|
||||
{{ .Ident "namespace" }},
|
||||
{{ .Ident "name" }},
|
||||
|
||||
{{ .Ident "value" }},
|
||||
{{ .Ident "action" }}
|
||||
)
|
||||
VALUES (
|
||||
{{ .Arg .GUID }},
|
||||
{{ .Arg .WriteEvent.Key.Group }},
|
||||
{{ .Arg .WriteEvent.Key.Resource }},
|
||||
{{ .Arg .WriteEvent.Key.Namespace }},
|
||||
{{ .Arg .WriteEvent.Key.Name }},
|
||||
|
||||
{{ .Arg .WriteEvent.Value }},
|
||||
{{ .Arg .WriteEvent.Type }}
|
||||
)
|
||||
;
|
10
pkg/storage/unified/sql/data/resource_read.sql
Normal file
10
pkg/storage/unified/sql/data/resource_read.sql
Normal file
@ -0,0 +1,10 @@
|
||||
SELECT
|
||||
{{ .Ident "resource_version" | .Into .ResourceVersion }},
|
||||
{{ .Ident "value" | .Into .Value }}
|
||||
FROM {{ .Ident "resource" }}
|
||||
WHERE 1 = 1
|
||||
AND {{ .Ident "namespace" }} = {{ .Arg .Request.Key.Namespace }}
|
||||
AND {{ .Ident "group" }} = {{ .Arg .Request.Key.Group }}
|
||||
AND {{ .Ident "resource" }} = {{ .Arg .Request.Key.Resource }}
|
||||
AND {{ .Ident "name" }} = {{ .Arg .Request.Key.Name }}
|
||||
;
|
11
pkg/storage/unified/sql/data/resource_update.sql
Normal file
11
pkg/storage/unified/sql/data/resource_update.sql
Normal file
@ -0,0 +1,11 @@
|
||||
UPDATE {{ .Ident "resource" }}
|
||||
SET
|
||||
{{ .Ident "guid" }} = {{ .Arg .GUID }},
|
||||
{{ .Ident "value" }} = {{ .Arg .WriteEvent.Value }},
|
||||
{{ .Ident "action" }} = {{ .Arg .WriteEvent.Type }}
|
||||
WHERE 1 = 1
|
||||
AND {{ .Ident "group" }} = {{ .Arg .WriteEvent.Key.Group }}
|
||||
AND {{ .Ident "resource" }} = {{ .Arg .WriteEvent.Key.Resource }}
|
||||
AND {{ .Ident "namespace" }} = {{ .Arg .WriteEvent.Key.Namespace }}
|
||||
AND {{ .Ident "name" }} = {{ .Arg .WriteEvent.Key.Name }}
|
||||
;
|
59
pkg/storage/unified/sql/db/dbimpl/db.go
Normal file
59
pkg/storage/unified/sql/db/dbimpl/db.go
Normal file
@ -0,0 +1,59 @@
|
||||
package dbimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
resourcedb "github.com/grafana/grafana/pkg/storage/unified/sql/db"
|
||||
)
|
||||
|
||||
func NewDB(d *sql.DB, driverName string) resourcedb.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) (resourcedb.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 resourcedb.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
|
||||
}
|
105
pkg/storage/unified/sql/db/dbimpl/dbEngine.go
Normal file
105
pkg/storage/unified/sql/db/dbimpl/dbEngine.go
Normal file
@ -0,0 +1,105 @@
|
||||
package dbimpl
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/store/entity/db"
|
||||
)
|
||||
|
||||
func getEngineMySQL(getter *sectionGetter, _ tracing.Tracer) (*xorm.Engine, error) {
|
||||
config := mysql.NewConfig()
|
||||
config.User = getter.String("db_user")
|
||||
config.Passwd = getter.String("db_pass")
|
||||
config.Net = "tcp"
|
||||
config.Addr = getter.String("db_host")
|
||||
config.DBName = getter.String("db_name")
|
||||
config.Params = map[string]string{
|
||||
// See: https://dev.mysql.com/doc/refman/en/sql-mode.html
|
||||
"@@SESSION.sql_mode": "ANSI",
|
||||
}
|
||||
config.Collation = "utf8mb4_unicode_ci"
|
||||
config.Loc = time.UTC
|
||||
config.AllowNativePasswords = true
|
||||
config.ClientFoundRows = true
|
||||
|
||||
// TODO: do we want to support these?
|
||||
// config.ServerPubKey = getter.String("db_server_pub_key")
|
||||
// config.TLSConfig = getter.String("db_tls_config_name")
|
||||
|
||||
if err := getter.Err(); err != nil {
|
||||
return nil, fmt.Errorf("config error: %w", err)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(config.Addr, "/") {
|
||||
config.Net = "unix"
|
||||
}
|
||||
|
||||
// FIXME: get rid of xorm
|
||||
engine, err := xorm.NewEngine(db.DriverMySQL, config.FormatDSN())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open database: %w", err)
|
||||
}
|
||||
|
||||
engine.SetMaxOpenConns(0)
|
||||
engine.SetMaxIdleConns(2)
|
||||
engine.SetConnMaxLifetime(4 * time.Hour)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func getEnginePostgres(getter *sectionGetter, _ tracing.Tracer) (*xorm.Engine, error) {
|
||||
dsnKV := map[string]string{
|
||||
"user": getter.String("db_user"),
|
||||
"password": getter.String("db_pass"),
|
||||
"dbname": getter.String("db_name"),
|
||||
"sslmode": cmp.Or(getter.String("db_sslmode"), "disable"),
|
||||
}
|
||||
|
||||
// TODO: probably interesting:
|
||||
// "passfile", "statement_timeout", "lock_timeout", "connect_timeout"
|
||||
|
||||
// TODO: for CockroachDB, we probably need to use the following:
|
||||
// dsnKV["options"] = "-c enable_experimental_alter_column_type_general=true"
|
||||
// Or otherwise specify it as:
|
||||
// dsnKV["enable_experimental_alter_column_type_general"] = "true"
|
||||
|
||||
// TODO: do we want to support these options in the DSN as well?
|
||||
// "sslkey", "sslcert", "sslrootcert", "sslpassword", "sslsni", "krbspn",
|
||||
// "krbsrvname", "target_session_attrs", "service", "servicefile"
|
||||
|
||||
// More on Postgres connection string parameters:
|
||||
// https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
|
||||
|
||||
hostport := getter.String("db_host")
|
||||
|
||||
if err := getter.Err(); err != nil {
|
||||
return nil, fmt.Errorf("config error: %w", err)
|
||||
}
|
||||
|
||||
host, port, err := splitHostPortDefault(hostport, "127.0.0.1", "5432")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid db_host: %w", err)
|
||||
}
|
||||
dsnKV["host"] = host
|
||||
dsnKV["port"] = port
|
||||
|
||||
dsn, err := MakeDSN(dsnKV)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error building DSN: %w", err)
|
||||
}
|
||||
|
||||
// FIXME: get rid of xorm
|
||||
engine, err := xorm.NewEngine(db.DriverPostgres, dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open database: %w", err)
|
||||
}
|
||||
|
||||
return engine, nil
|
||||
}
|
92
pkg/storage/unified/sql/db/dbimpl/dbEngine_test.go
Normal file
92
pkg/storage/unified/sql/db/dbimpl/dbEngine_test.go
Normal file
@ -0,0 +1,92 @@
|
||||
package dbimpl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetEngineMySQLFromConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
getter := newTestSectionGetter(map[string]string{
|
||||
"db_type": "mysql",
|
||||
"db_host": "/var/run/mysql.socket",
|
||||
"db_name": "grafana",
|
||||
"db_user": "user",
|
||||
"db_password": "password",
|
||||
})
|
||||
engine, err := getEngineMySQL(getter, nil)
|
||||
assert.NotNil(t, engine)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalid string", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
getter := newTestSectionGetter(map[string]string{
|
||||
"db_type": "mysql",
|
||||
"db_host": "/var/run/mysql.socket",
|
||||
"db_name": string(invalidUTF8ByteSequence),
|
||||
"db_user": "user",
|
||||
"db_password": "password",
|
||||
})
|
||||
engine, err := getEngineMySQL(getter, nil)
|
||||
assert.Nil(t, engine)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrInvalidUTF8Sequence)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetEnginePostgresFromConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
getter := newTestSectionGetter(map[string]string{
|
||||
"db_type": "mysql",
|
||||
"db_host": "localhost",
|
||||
"db_name": "grafana",
|
||||
"db_user": "user",
|
||||
"db_password": "password",
|
||||
})
|
||||
engine, err := getEnginePostgres(getter, nil)
|
||||
|
||||
assert.NotNil(t, engine)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalid string", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
getter := newTestSectionGetter(map[string]string{
|
||||
"db_type": "mysql",
|
||||
"db_host": string(invalidUTF8ByteSequence),
|
||||
"db_name": "grafana",
|
||||
"db_user": "user",
|
||||
"db_password": "password",
|
||||
})
|
||||
engine, err := getEnginePostgres(getter, nil)
|
||||
|
||||
assert.Nil(t, engine)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrInvalidUTF8Sequence)
|
||||
})
|
||||
|
||||
t.Run("invalid hostport", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
getter := newTestSectionGetter(map[string]string{
|
||||
"db_type": "mysql",
|
||||
"db_host": "1:1:1",
|
||||
"db_name": "grafana",
|
||||
"db_user": "user",
|
||||
"db_password": "password",
|
||||
})
|
||||
engine, err := getEnginePostgres(getter, nil)
|
||||
|
||||
assert.Nil(t, engine)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
154
pkg/storage/unified/sql/db/dbimpl/db_test.go
Normal file
154
pkg/storage/unified/sql/db/dbimpl/db_test.go
Normal file
@ -0,0 +1,154 @@
|
||||
package dbimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sqlmock "github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
resourcedb "github.com/grafana/grafana/pkg/storage/unified/sql/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
|
||||
}
|
||||
|
||||
var errTest = errors.New("because of reasons")
|
||||
|
||||
const driverName = "sqlmock"
|
||||
|
||||
func TestDB_BeginTx(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqldb, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
db := NewDB(sqldb, driverName)
|
||||
require.Equal(t, driverName, db.DriverName())
|
||||
|
||||
mock.ExpectBegin()
|
||||
tx, err := db.BeginTx(newCtx(t), nil)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tx)
|
||||
})
|
||||
|
||||
t.Run("fail begin", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqldb, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
db := NewDB(sqldb, "sqlmock")
|
||||
|
||||
mock.ExpectBegin().WillReturnError(errTest)
|
||||
tx, err := db.BeginTx(newCtx(t), nil)
|
||||
|
||||
require.Nil(t, tx)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errTest)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDB_WithTx(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
newTxFunc := func(err error) resourcedb.TxFunc {
|
||||
return func(context.Context, resourcedb.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)
|
||||
})
|
||||
|
||||
t.Run("fail begin", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqldb, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
db := NewDB(sqldb, "sqlmock")
|
||||
|
||||
mock.ExpectBegin().WillReturnError(errTest)
|
||||
err = db.WithTx(newCtx(t), nil, newTxFunc(nil))
|
||||
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errTest)
|
||||
})
|
||||
|
||||
t.Run("fail tx", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqldb, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
db := NewDB(sqldb, "sqlmock")
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectRollback()
|
||||
err = db.WithTx(newCtx(t), nil, newTxFunc(errTest))
|
||||
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errTest)
|
||||
})
|
||||
|
||||
t.Run("fail tx; fail rollback", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqldb, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
db := NewDB(sqldb, "sqlmock")
|
||||
errTest2 := errors.New("yet another err")
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectRollback().WillReturnError(errTest)
|
||||
err = db.WithTx(newCtx(t), nil, newTxFunc(errTest2))
|
||||
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errTest)
|
||||
require.ErrorIs(t, err, errTest2)
|
||||
})
|
||||
|
||||
t.Run("fail commit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqldb, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
db := NewDB(sqldb, "sqlmock")
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectCommit().WillReturnError(errTest)
|
||||
err = db.WithTx(newCtx(t), nil, newTxFunc(nil))
|
||||
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errTest)
|
||||
})
|
||||
}
|
166
pkg/storage/unified/sql/db/dbimpl/dbimpl.go
Normal file
166
pkg/storage/unified/sql/db/dbimpl/dbimpl.go
Normal file
@ -0,0 +1,166 @@
|
||||
package dbimpl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"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"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
resourcedb "github.com/grafana/grafana/pkg/storage/unified/sql/db"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/db/migrations"
|
||||
)
|
||||
|
||||
var _ resourcedb.ResourceDBInterface = (*ResourceDB)(nil)
|
||||
|
||||
func ProvideResourceDB(db db.DB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer tracing.Tracer) (*ResourceDB, error) {
|
||||
return &ResourceDB{
|
||||
db: db,
|
||||
cfg: cfg,
|
||||
features: features,
|
||||
log: log.New("entity-db"),
|
||||
tracer: tracer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ResourceDB struct {
|
||||
once sync.Once
|
||||
onceErr error
|
||||
|
||||
db db.DB
|
||||
features featuremgmt.FeatureToggles
|
||||
engine *xorm.Engine
|
||||
cfg *setting.Cfg
|
||||
log log.Logger
|
||||
tracer tracing.Tracer
|
||||
}
|
||||
|
||||
func (db *ResourceDB) Init() error {
|
||||
db.once.Do(func() {
|
||||
db.onceErr = db.init()
|
||||
})
|
||||
|
||||
return db.onceErr
|
||||
}
|
||||
|
||||
func (db *ResourceDB) GetEngine() (*xorm.Engine, error) {
|
||||
if err := db.Init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db.engine, db.onceErr
|
||||
}
|
||||
|
||||
func (db *ResourceDB) init() error {
|
||||
if db.engine != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var engine *xorm.Engine
|
||||
var err error
|
||||
|
||||
// TODO: This should be renamed resource_api
|
||||
getter := §ionGetter{
|
||||
DynamicSection: db.cfg.SectionWithEnvOverrides("entity_api"),
|
||||
}
|
||||
|
||||
dbType := getter.Key("db_type").MustString("")
|
||||
|
||||
// if explicit connection settings are provided, use them
|
||||
if dbType != "" {
|
||||
if dbType == "postgres" {
|
||||
engine, err = getEnginePostgres(getter, db.tracer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME: this config option is cockroachdb-specific, it's not supported by postgres
|
||||
// FIXME: this only sets this option for the session that we get
|
||||
// from the pool right now. A *sql.DB is a pool of connections,
|
||||
// there is no guarantee that the session where this is run will be
|
||||
// the same where we need to change the type of a column
|
||||
_, err = engine.Exec("SET SESSION enable_experimental_alter_column_type_general=true")
|
||||
if err != nil {
|
||||
db.log.Error("error connecting to postgres", "msg", err.Error())
|
||||
// FIXME: return nil, err
|
||||
}
|
||||
} else if dbType == "mysql" {
|
||||
engine, err = getEngineMySQL(getter, db.tracer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = engine.Ping(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// TODO: sqlite support
|
||||
return fmt.Errorf("invalid db type specified: %s", dbType)
|
||||
}
|
||||
|
||||
// register sql stat metrics
|
||||
if err := prometheus.Register(sqlstats.NewStatsCollector("unified_storage", engine.DB().DB)); err != nil {
|
||||
db.log.Warn("Failed to register unified storage sql stats collector", "error", err)
|
||||
}
|
||||
|
||||
// configure sql logging
|
||||
debugSQL := getter.Key("log_queries").MustBool(false)
|
||||
if !debugSQL {
|
||||
engine.SetLogger(&xorm.DiscardLogger{})
|
||||
} else {
|
||||
// add stack to database calls to be able to see what repository initiated queries. Top 7 items from the stack as they are likely in the xorm library.
|
||||
// engine.SetLogger(sqlstore.NewXormLogger(log.LvlInfo, log.WithSuffix(log.New("sqlstore.xorm"), log.CallerContextKey, log.StackCaller(log.DefaultCallerDepth))))
|
||||
engine.ShowSQL(true)
|
||||
engine.ShowExecTime(true)
|
||||
}
|
||||
|
||||
// otherwise, try to use the grafana db connection
|
||||
} else {
|
||||
if db.db == nil {
|
||||
return fmt.Errorf("no db connection provided")
|
||||
}
|
||||
|
||||
engine = db.db.GetEngine()
|
||||
}
|
||||
|
||||
db.engine = engine
|
||||
|
||||
if err := migrations.MigrateResourceStore(engine, db.cfg, db.features); err != nil {
|
||||
db.engine = nil
|
||||
return fmt.Errorf("run migrations: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *ResourceDB) GetSession() (*session.SessionDB, error) {
|
||||
engine, err := db.GetEngine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session.GetSession(sqlx.NewDb(engine.DB().DB, engine.DriverName())), nil
|
||||
}
|
||||
|
||||
func (db *ResourceDB) GetCfg() *setting.Cfg {
|
||||
return db.cfg
|
||||
}
|
||||
|
||||
func (db *ResourceDB) GetDB() (resourcedb.DB, error) {
|
||||
engine, err := db.GetEngine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := NewDB(engine.DB().DB, engine.Dialect().DriverName())
|
||||
|
||||
return ret, nil
|
||||
}
|
111
pkg/storage/unified/sql/db/dbimpl/util.go
Normal file
111
pkg/storage/unified/sql/db/dbimpl/util.go
Normal file
@ -0,0 +1,111 @@
|
||||
package dbimpl
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidUTF8Sequence = errors.New("invalid UTF-8 sequence")
|
||||
)
|
||||
|
||||
type sectionGetter struct {
|
||||
*setting.DynamicSection
|
||||
err error
|
||||
}
|
||||
|
||||
func (g *sectionGetter) Err() error {
|
||||
return g.err
|
||||
}
|
||||
|
||||
func (g *sectionGetter) String(key string) string {
|
||||
v := g.DynamicSection.Key(key).MustString("")
|
||||
if !utf8.ValidString(v) {
|
||||
g.err = fmt.Errorf("value for key %q: %w", key, ErrInvalidUTF8Sequence)
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// MakeDSN creates a DSN from the given key/value pair. It validates the strings
|
||||
// form valid UTF-8 sequences and escapes values if needed.
|
||||
func MakeDSN(m map[string]string) (string, error) {
|
||||
b := new(strings.Builder)
|
||||
|
||||
ks := keys(m)
|
||||
sort.Strings(ks) // provide deterministic behaviour
|
||||
for _, k := range ks {
|
||||
v := m[k]
|
||||
if !utf8.ValidString(v) {
|
||||
return "", fmt.Errorf("value for DSN key %q: %w", k,
|
||||
ErrInvalidUTF8Sequence)
|
||||
}
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if b.Len() > 0 {
|
||||
_ = b.WriteByte(' ')
|
||||
}
|
||||
_, _ = b.WriteString(k)
|
||||
_ = b.WriteByte('=')
|
||||
writeDSNValue(b, v)
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func keys(m map[string]string) []string {
|
||||
ret := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
ret = append(ret, k)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func writeDSNValue(b *strings.Builder, v string) {
|
||||
numq := strings.Count(v, `'`)
|
||||
numb := strings.Count(v, `\`)
|
||||
if numq+numb == 0 && v != "" {
|
||||
b.WriteString(v)
|
||||
|
||||
return
|
||||
}
|
||||
b.Grow(2 + numq + numb + len(v))
|
||||
|
||||
_ = b.WriteByte('\'')
|
||||
for _, r := range v {
|
||||
if r == '\\' || r == '\'' {
|
||||
_ = b.WriteByte('\\')
|
||||
}
|
||||
_, _ = b.WriteRune(r)
|
||||
}
|
||||
_ = b.WriteByte('\'')
|
||||
}
|
||||
|
||||
// splitHostPortDefault is similar to net.SplitHostPort, but will also accept a
|
||||
// specification with no port and apply the default port instead. It also
|
||||
// applies the given defaults if the results are empty strings.
|
||||
func splitHostPortDefault(hostport, defaultHost, defaultPort string) (string, string, error) {
|
||||
host, port, err := net.SplitHostPort(hostport)
|
||||
if err != nil {
|
||||
// try appending the port
|
||||
host, port, err = net.SplitHostPort(hostport + ":" + defaultPort)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("invalid hostport: %q", hostport)
|
||||
}
|
||||
}
|
||||
host = cmp.Or(host, defaultHost)
|
||||
port = cmp.Or(port, defaultPort)
|
||||
|
||||
return host, port, nil
|
||||
}
|
108
pkg/storage/unified/sql/db/dbimpl/util_test.go
Normal file
108
pkg/storage/unified/sql/db/dbimpl/util_test.go
Normal file
@ -0,0 +1,108 @@
|
||||
package dbimpl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var invalidUTF8ByteSequence = []byte{0xff, 0xfe, 0xfd}
|
||||
|
||||
func setSectionKeyValues(section *setting.DynamicSection, m map[string]string) {
|
||||
for k, v := range m {
|
||||
section.Key(k).SetValue(v)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestSectionGetter(m map[string]string) *sectionGetter {
|
||||
section := setting.NewCfg().SectionWithEnvOverrides("entity_api")
|
||||
setSectionKeyValues(section, m)
|
||||
|
||||
return §ionGetter{
|
||||
DynamicSection: section,
|
||||
}
|
||||
}
|
||||
|
||||
func TestSectionGetter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
key = "the key"
|
||||
val = string(invalidUTF8ByteSequence)
|
||||
)
|
||||
|
||||
g := newTestSectionGetter(map[string]string{
|
||||
key: val,
|
||||
})
|
||||
|
||||
v := g.String("whatever")
|
||||
require.Empty(t, v)
|
||||
require.NoError(t, g.Err())
|
||||
|
||||
v = g.String(key)
|
||||
require.Empty(t, v)
|
||||
require.Error(t, g.Err())
|
||||
require.ErrorIs(t, g.Err(), ErrInvalidUTF8Sequence)
|
||||
}
|
||||
|
||||
func TestMakeDSN(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s, err := MakeDSN(map[string]string{
|
||||
"db_name": string(invalidUTF8ByteSequence),
|
||||
})
|
||||
require.Empty(t, s)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, ErrInvalidUTF8Sequence)
|
||||
|
||||
s, err = MakeDSN(map[string]string{
|
||||
"skip": "",
|
||||
"user": `shou'ld esc\ape`,
|
||||
"pass": "noescape",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `pass=noescape user='shou\'ld esc\\ape'`, s)
|
||||
}
|
||||
|
||||
func TestSplitHostPort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
hostport string
|
||||
defaultHost string
|
||||
defaultPort string
|
||||
fails bool
|
||||
|
||||
host string
|
||||
port string
|
||||
}{
|
||||
{hostport: "192.168.0.140:456", defaultHost: "", defaultPort: "", host: "192.168.0.140", port: "456"},
|
||||
{hostport: "192.168.0.140", defaultHost: "", defaultPort: "123", host: "192.168.0.140", port: "123"},
|
||||
{hostport: "[::1]:456", defaultHost: "", defaultPort: "", host: "::1", port: "456"},
|
||||
{hostport: "[::1]", defaultHost: "", defaultPort: "123", host: "::1", port: "123"},
|
||||
{hostport: ":456", defaultHost: "1.2.3.4", defaultPort: "", host: "1.2.3.4", port: "456"},
|
||||
{hostport: "xyz.rds.amazonaws.com", defaultHost: "", defaultPort: "123", host: "xyz.rds.amazonaws.com", port: "123"},
|
||||
{hostport: "xyz.rds.amazonaws.com:123", defaultHost: "", defaultPort: "", host: "xyz.rds.amazonaws.com", port: "123"},
|
||||
{hostport: "", defaultHost: "localhost", defaultPort: "1433", host: "localhost", port: "1433"},
|
||||
{hostport: "1:1:1", fails: true},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("test index #%d", i), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
host, port, err := splitHostPortDefault(tc.hostport, tc.defaultHost, tc.defaultPort)
|
||||
if tc.fails {
|
||||
require.Error(t, err)
|
||||
require.Empty(t, host)
|
||||
require.Empty(t, port)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.host, host)
|
||||
require.Equal(t, tc.port, port)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
24
pkg/storage/unified/sql/db/migrations/migrator.go
Normal file
24
pkg/storage/unified/sql/db/migrations/migrator.go
Normal file
@ -0,0 +1,24 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func MigrateResourceStore(engine *xorm.Engine, cfg *setting.Cfg, features featuremgmt.FeatureToggles) error {
|
||||
// Skip if feature flag is not enabled
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorage) {
|
||||
return nil
|
||||
}
|
||||
|
||||
mg := migrator.NewScopedMigrator(engine, cfg, "resource")
|
||||
mg.AddCreateMigration()
|
||||
|
||||
initResourceTables(mg)
|
||||
|
||||
// since it's a new feature enable migration locking by default
|
||||
return mg.Start(true, 0)
|
||||
}
|
101
pkg/storage/unified/sql/db/migrations/resource_mig.go
Normal file
101
pkg/storage/unified/sql/db/migrations/resource_mig.go
Normal file
@ -0,0 +1,101 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
)
|
||||
|
||||
func initResourceTables(mg *migrator.Migrator) string {
|
||||
marker := "Initialize resource tables"
|
||||
mg.AddMigration(marker, &migrator.RawSQLMigration{})
|
||||
|
||||
tables := []migrator.Table{}
|
||||
tables = append(tables, migrator.Table{
|
||||
Name: "resource",
|
||||
Columns: []*migrator.Column{
|
||||
// primary identifier
|
||||
{Name: "guid", Type: migrator.DB_NVarchar, Length: 36, Nullable: false, IsPrimaryKey: true},
|
||||
|
||||
{Name: "resource_version", Type: migrator.DB_BigInt, Nullable: true},
|
||||
|
||||
// K8s Identity group+(version)+namespace+resource+name
|
||||
{Name: "group", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "resource", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "namespace", Type: migrator.DB_NVarchar, Length: 63, Nullable: false},
|
||||
{Name: "name", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "value", Type: migrator.DB_LongText, Nullable: true},
|
||||
{Name: "action", Type: migrator.DB_Int, Nullable: false}, // 1: create, 2: update, 3: delete
|
||||
|
||||
// Hashed label set
|
||||
{Name: "label_set", Type: migrator.DB_NVarchar, Length: 64, Nullable: true}, // null is no labels
|
||||
},
|
||||
Indices: []*migrator.Index{
|
||||
{Cols: []string{"namespace", "group", "resource", "name"}, Type: migrator.UniqueIndex},
|
||||
},
|
||||
})
|
||||
|
||||
tables = append(tables, migrator.Table{
|
||||
Name: "resource_history",
|
||||
Columns: []*migrator.Column{
|
||||
// primary identifier
|
||||
{Name: "guid", Type: migrator.DB_NVarchar, Length: 36, Nullable: false, IsPrimaryKey: true},
|
||||
{Name: "resource_version", Type: migrator.DB_BigInt, Nullable: true},
|
||||
|
||||
// K8s Identity group+(version)+namespace+resource+name
|
||||
{Name: "group", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "resource", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "namespace", Type: migrator.DB_NVarchar, Length: 63, Nullable: false},
|
||||
{Name: "name", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "value", Type: migrator.DB_LongText, Nullable: true},
|
||||
{Name: "action", Type: migrator.DB_Int, Nullable: false}, // 1: create, 2: update, 3: delete
|
||||
|
||||
// Hashed label set
|
||||
{Name: "label_set", Type: migrator.DB_NVarchar, Length: 64, Nullable: true}, // null is no labels
|
||||
},
|
||||
Indices: []*migrator.Index{
|
||||
{
|
||||
Cols: []string{"namespace", "group", "resource", "name", "resource_version"},
|
||||
Type: migrator.UniqueIndex,
|
||||
Name: "UQE_resource_history_namespace_group_name_version",
|
||||
},
|
||||
// index to support watch poller
|
||||
{Cols: []string{"resource_version"}, Type: migrator.IndexType},
|
||||
},
|
||||
})
|
||||
|
||||
tables = append(tables, migrator.Table{
|
||||
Name: "resource_label_set",
|
||||
Columns: []*migrator.Column{
|
||||
{Name: "label_set", Type: migrator.DB_NVarchar, Length: 64, Nullable: false},
|
||||
{Name: "label", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "value", Type: migrator.DB_Text, Nullable: false},
|
||||
},
|
||||
Indices: []*migrator.Index{
|
||||
{Cols: []string{"label_set", "label"}, Type: migrator.UniqueIndex},
|
||||
},
|
||||
})
|
||||
|
||||
tables = append(tables, migrator.Table{
|
||||
Name: "resource_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},
|
||||
},
|
||||
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))
|
||||
mg.AddMigration("create table "+tables[t].Name, migrator.NewAddTableMigration(tables[t]))
|
||||
for i := range tables[t].Indices {
|
||||
mg.AddMigration(fmt.Sprintf("create table %s, index: %d", tables[t].Name, i), migrator.NewAddIndexMigration(tables[t], tables[t].Indices[i]))
|
||||
}
|
||||
}
|
||||
|
||||
return marker
|
||||
}
|
71
pkg/storage/unified/sql/db/service.go
Executable file
71
pkg/storage/unified/sql/db/service.go
Executable file
@ -0,0 +1,71 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
DriverPostgres = "postgres"
|
||||
DriverMySQL = "mysql"
|
||||
DriverSQLite = "sqlite"
|
||||
DriverSQLite3 = "sqlite3"
|
||||
)
|
||||
|
||||
// ResourceDBInterface provides access to a database capable of supporting the
|
||||
// Entity Server.
|
||||
type ResourceDBInterface interface {
|
||||
Init() error
|
||||
GetCfg() *setting.Cfg
|
||||
GetDB() (DB, error)
|
||||
|
||||
// TODO: deprecate.
|
||||
GetSession() (*session.SessionDB, error)
|
||||
GetEngine() (*xorm.Engine, error)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// TxFunc is a function that executes with access to a transaction. The context
|
||||
// it receives is the same context used to create the transaction, and is
|
||||
// provided so that a general prupose TxFunc is able to retrieve information
|
||||
// from that context, and derive other contexts that may be used to run database
|
||||
// operation methods accepting a context. A derived context can be used to
|
||||
// request a specific database operation to take no more than a specific
|
||||
// fraction of the remaining timeout of the transaction context, or to enrich
|
||||
// the downstream observability layer with relevant information regarding the
|
||||
// specific operation being carried out.
|
||||
type TxFunc = func(context.Context, Tx) error
|
||||
|
||||
// Tx is a thin abstraction on *sql.Tx to allow mocking to provide better unit
|
||||
// testing. We allow database operation methods that do not take a
|
||||
// context.Context here since a Tx can only be obtained with DB.BeginTx, which
|
||||
// already takes a context.Context.
|
||||
type Tx interface {
|
||||
ContextExecer
|
||||
Commit() error
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
// ContextExecer is a set of database operation methods that take
|
||||
// context.Context.
|
||||
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
|
||||
}
|
176
pkg/storage/unified/sql/queries.go
Normal file
176
pkg/storage/unified/sql/queries.go
Normal file
@ -0,0 +1,176 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||
)
|
||||
|
||||
// Templates setup.
|
||||
var (
|
||||
//go:embed data/*.sql
|
||||
sqlTemplatesFS embed.FS
|
||||
|
||||
// all templates
|
||||
helpers = template.FuncMap{
|
||||
"listSep": helperListSep,
|
||||
"join": helperJoin,
|
||||
}
|
||||
sqlTemplates = template.Must(template.New("sql").Funcs(helpers).ParseFS(sqlTemplatesFS, `data/*.sql`))
|
||||
)
|
||||
|
||||
func mustTemplate(filename string) *template.Template {
|
||||
if t := sqlTemplates.Lookup(filename); t != nil {
|
||||
return t
|
||||
}
|
||||
panic(fmt.Sprintf("template file not found: %s", filename))
|
||||
}
|
||||
|
||||
// Templates.
|
||||
var (
|
||||
sqlResourceDelete = mustTemplate("resource_delete.sql")
|
||||
sqlResourceInsert = mustTemplate("resource_insert.sql")
|
||||
sqlResourceUpdate = mustTemplate("resource_update.sql")
|
||||
sqlResourceRead = mustTemplate("resource_read.sql")
|
||||
sqlResourceUpdateRV = mustTemplate("resource_update_rv.sql")
|
||||
sqlResourceHistoryUpdateRV = mustTemplate("resource_history_update_rv.sql")
|
||||
sqlResourceHistoryInsert = mustTemplate("resource_history_insert.sql")
|
||||
|
||||
// sqlResourceLabelsInsert = mustTemplate("resource_labels_insert.sql")
|
||||
sqlResourceVersionGet = mustTemplate("resource_version_get.sql")
|
||||
sqlResourceVersionInc = mustTemplate("resource_version_inc.sql")
|
||||
sqlResourceVersionInsert = mustTemplate("resource_version_insert.sql")
|
||||
)
|
||||
|
||||
// TxOptions.
|
||||
var (
|
||||
ReadCommitted = &sql.TxOptions{
|
||||
Isolation: sql.LevelReadCommitted,
|
||||
}
|
||||
ReadCommittedRO = &sql.TxOptions{
|
||||
Isolation: sql.LevelReadCommitted,
|
||||
ReadOnly: true,
|
||||
}
|
||||
)
|
||||
|
||||
// SQLError is an error returned by the database, which includes additionally
|
||||
// debugging information about what was sent to the database.
|
||||
type SQLError struct {
|
||||
Err error
|
||||
CallType string // either Query, QueryRow or Exec
|
||||
TemplateName string
|
||||
Query string
|
||||
RawQuery string
|
||||
ScanDest []any
|
||||
|
||||
// potentially regulated information is not exported and only directly
|
||||
// available for local testing and local debugging purposes, making sure it
|
||||
// is never marshaled to JSON or any other serialization.
|
||||
|
||||
arguments []any
|
||||
}
|
||||
|
||||
func (e SQLError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
func (e SQLError) Error() string {
|
||||
return fmt.Sprintf("%s: %s with %d input arguments and %d output "+
|
||||
"destination arguments: %v", e.TemplateName, e.CallType,
|
||||
len(e.arguments), len(e.ScanDest), e.Err)
|
||||
}
|
||||
|
||||
type sqlResourceRequest struct {
|
||||
*sqltemplate.SQLTemplate
|
||||
GUID string
|
||||
WriteEvent resource.WriteEvent
|
||||
}
|
||||
|
||||
func (r sqlResourceRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
// sqlResourceReadRequest can be used to retrieve a row from either the "resource"
|
||||
// or the "resource_history" tables.
|
||||
|
||||
type readResponse struct {
|
||||
resource.ReadResponse
|
||||
}
|
||||
|
||||
func (r *readResponse) Results() (*readResponse, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
type sqlResourceReadRequest struct {
|
||||
*sqltemplate.SQLTemplate
|
||||
Request *resource.ReadRequest
|
||||
*readResponse
|
||||
}
|
||||
|
||||
func (r sqlResourceReadRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
// update RV
|
||||
|
||||
type sqlResourceUpdateRVRequest struct {
|
||||
*sqltemplate.SQLTemplate
|
||||
GUID string
|
||||
ResourceVersion int64
|
||||
}
|
||||
|
||||
func (r sqlResourceUpdateRVRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
// resource_version table requests.
|
||||
type resourceVersion struct {
|
||||
ResourceVersion int64
|
||||
}
|
||||
|
||||
func (r *resourceVersion) Results() (*resourceVersion, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
type sqlResourceVersionRequest struct {
|
||||
*sqltemplate.SQLTemplate
|
||||
Key *resource.ResourceKey
|
||||
*resourceVersion
|
||||
}
|
||||
|
||||
func (r sqlResourceVersionRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
// Template helpers.
|
||||
|
||||
// helperListSep is a helper that helps writing simpler loops in SQL templates.
|
||||
// Example usage:
|
||||
//
|
||||
// {{ $comma := listSep ", " }}
|
||||
// {{ range .Values }}
|
||||
// {{/* here we put "-" on each end to remove extra white space */}}
|
||||
// {{- call $comma -}}
|
||||
// {{ .Value }}
|
||||
// {{ end }}
|
||||
func helperListSep(sep string) func() string {
|
||||
var addSep bool
|
||||
|
||||
return func() string {
|
||||
if addSep {
|
||||
return sep
|
||||
}
|
||||
addSep = true
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func helperJoin(sep string, elems ...string) string {
|
||||
return strings.Join(elems, sep)
|
||||
}
|
584
pkg/storage/unified/sql/queries_test.go
Normal file
584
pkg/storage/unified/sql/queries_test.go
Normal file
@ -0,0 +1,584 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||
)
|
||||
|
||||
// debug is meant to provide greater debugging detail about certain errors. The
|
||||
// returned error will either provide more detailed information or be the same
|
||||
// original error, suitable only for local debugging. The details provided are
|
||||
// not meant to be logged, since they could include PII or otherwise
|
||||
// sensitive/confidential information. These information should only be used for
|
||||
// local debugging with fake or otherwise non-regulated information.
|
||||
func debug(err error) error {
|
||||
var d interface{ Debug() string }
|
||||
if errors.As(err, &d) {
|
||||
return errors.New(d.Debug())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ = debug // silence the `unused` linter
|
||||
|
||||
//go:embed testdata/*
|
||||
var testdataFS embed.FS
|
||||
|
||||
func testdata(t *testing.T, filename string) []byte {
|
||||
t.Helper()
|
||||
b, err := testdataFS.ReadFile(`testdata/` + filename)
|
||||
require.NoError(t, err)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func testdataJSON(t *testing.T, filename string, dest any) {
|
||||
t.Helper()
|
||||
b := testdata(t, filename)
|
||||
err := json.Unmarshal(b, dest)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestQueries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Each template has one or more test cases, each identified with a
|
||||
// descriptive name (e.g. "happy path", "error twiddling the frobb"). Each
|
||||
// of them will test that for the same input data they must produce a result
|
||||
// that will depend on the Dialect. Expected queries should be defined in
|
||||
// separate files in the testdata directory. This improves the testing
|
||||
// experience by separating test data from test code, since mixing both
|
||||
// tends to make it more difficult to reason about what is being done,
|
||||
// especially as we want testing code to scale and make it easy to add
|
||||
// tests.
|
||||
type (
|
||||
// type aliases to make code more semantic and self-documenting
|
||||
resultSQLFilename = string
|
||||
dialects = []sqltemplate.Dialect
|
||||
expected map[resultSQLFilename]dialects
|
||||
|
||||
testCase = struct {
|
||||
Name string
|
||||
|
||||
// Data should be the struct passed to the template.
|
||||
Data sqltemplate.SQLTemplateIface
|
||||
|
||||
// Expected maps the filename containing the expected result query
|
||||
// to the list of dialects that would produce it. For simple
|
||||
// queries, it is possible that more than one dialect produce the
|
||||
// same output. The filename is expected to be in the `testdata`
|
||||
// directory.
|
||||
Expected expected
|
||||
}
|
||||
)
|
||||
|
||||
// Define tests cases. Most templates are trivial and testing that they
|
||||
// generate correct code for a single Dialect is fine, since the one thing
|
||||
// that always changes is how SQL placeholder arguments are passed (most
|
||||
// Dialects use `?` while PostgreSQL uses `$1`, `$2`, etc.), and that is
|
||||
// something that should be tested in the Dialect implementation instead of
|
||||
// here. We will ask to have at least one test per SQL template, and we will
|
||||
// lean to test MySQL. Templates containing branching (conditionals, loops,
|
||||
// etc.) should be exercised at least once in each of their branches.
|
||||
//
|
||||
// NOTE: in the Data field, make sure to have pointers populated to simulate
|
||||
// data is set as it would be in a real request. The data being correctly
|
||||
// populated in each case should be tested in integration tests, where the
|
||||
// data will actually flow to and from a real database. In this tests we
|
||||
// only care about producing the correct SQL.
|
||||
testCases := map[*template.Template][]*testCase{
|
||||
sqlResourceDelete: {
|
||||
{
|
||||
Name: "single path",
|
||||
Data: &sqlResourceRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
WriteEvent: resource.WriteEvent{
|
||||
Key: &resource.ResourceKey{},
|
||||
},
|
||||
},
|
||||
Expected: expected{
|
||||
"resource_delete_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
"resource_delete_postgres.sql": dialects{
|
||||
sqltemplate.PostgreSQL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlResourceInsert: {
|
||||
{
|
||||
Name: "insert into resource",
|
||||
Data: &sqlResourceRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
WriteEvent: resource.WriteEvent{
|
||||
Key: &resource.ResourceKey{},
|
||||
},
|
||||
},
|
||||
Expected: expected{
|
||||
"resource_insert_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sqlResourceUpdate: {
|
||||
{
|
||||
Name: "single path",
|
||||
Data: &sqlResourceRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
WriteEvent: resource.WriteEvent{
|
||||
Key: &resource.ResourceKey{},
|
||||
},
|
||||
},
|
||||
Expected: expected{
|
||||
"resource_update_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlResourceRead: {
|
||||
{
|
||||
Name: "without resource version",
|
||||
Data: &sqlResourceReadRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
Request: &resource.ReadRequest{
|
||||
Key: &resource.ResourceKey{},
|
||||
},
|
||||
readResponse: new(readResponse),
|
||||
},
|
||||
Expected: expected{
|
||||
"resource_read_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sqlResourceUpdateRV: {
|
||||
{
|
||||
Name: "single path",
|
||||
Data: &sqlResourceUpdateRVRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
},
|
||||
Expected: expected{
|
||||
"resource_update_rv_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sqlResourceHistoryUpdateRV: {
|
||||
{
|
||||
Name: "single path",
|
||||
Data: &sqlResourceUpdateRVRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
},
|
||||
Expected: expected{
|
||||
"resource_history_update_rv_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sqlResourceHistoryInsert: {
|
||||
{
|
||||
Name: "insert into entity_history",
|
||||
Data: &sqlResourceRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
WriteEvent: resource.WriteEvent{
|
||||
Key: &resource.ResourceKey{},
|
||||
},
|
||||
},
|
||||
Expected: expected{
|
||||
"resource_history_insert_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlResourceVersionGet: {
|
||||
{
|
||||
Name: "single path",
|
||||
Data: &sqlResourceVersionRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
Key: &resource.ResourceKey{},
|
||||
resourceVersion: new(resourceVersion),
|
||||
},
|
||||
Expected: expected{
|
||||
"resource_version_get_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlResourceVersionInc: {
|
||||
{
|
||||
Name: "increment resource version",
|
||||
Data: &sqlResourceVersionRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
Key: &resource.ResourceKey{},
|
||||
},
|
||||
Expected: expected{
|
||||
"resource_version_inc_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlResourceVersionInsert: {
|
||||
{
|
||||
Name: "single path",
|
||||
Data: &sqlResourceVersionRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
Key: &resource.ResourceKey{},
|
||||
},
|
||||
Expected: expected{
|
||||
"resource_version_insert_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Execute test cases
|
||||
for tmpl, tcs := range testCases {
|
||||
t.Run(tmpl.Name(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for filename, ds := range tc.Expected {
|
||||
t.Run(filename, func(t *testing.T) {
|
||||
// not parallel because we're sharing tc.Data, not
|
||||
// worth it deep cloning
|
||||
|
||||
rawQuery := string(testdata(t, filename))
|
||||
expectedQuery := sqltemplate.FormatSQL(rawQuery)
|
||||
|
||||
for _, d := range ds {
|
||||
t.Run(d.Name(), func(t *testing.T) {
|
||||
// not parallel for the same reason
|
||||
|
||||
tc.Data.SetDialect(d)
|
||||
err := tc.Data.Validate()
|
||||
require.NoError(t, err)
|
||||
got, err := sqltemplate.Execute(tmpl, tc.Data)
|
||||
require.NoError(t, err)
|
||||
got = sqltemplate.FormatSQL(got)
|
||||
require.Equal(t, expectedQuery, got)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// func TestReadEntity(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
// // readonly, shared data for all subtests
|
||||
// expectedEntity := newEmptyEntity()
|
||||
// testdataJSON(t, `grpc-res-entity.json`, expectedEntity)
|
||||
// key, err := grafanaregistry.ParseKey(expectedEntity.Key)
|
||||
// require.NoErrorf(t, err, "provided key: %#v", expectedEntity)
|
||||
|
||||
// t.Run("happy path - entity table, optimistic locking", func(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
// ctx := testutil.NewDefaultTestContext(t)
|
||||
// db, mock := newMockDBMatchWords(t)
|
||||
// x := expectReadEntity(t, mock, cloneEntity(expectedEntity))
|
||||
// x(ctx, db)
|
||||
// })
|
||||
|
||||
// t.Run("happy path - entity table, no optimistic locking", func(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
// // test declarations
|
||||
// ctx := testutil.NewDefaultTestContext(t)
|
||||
// db, mock := newMockDBMatchWords(t)
|
||||
// readReq := sqlEntityReadRequest{ // used to generate mock results
|
||||
// SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
|
||||
// Key: new(grafanaregistry.Key),
|
||||
// returnsEntitySet: newReturnsEntitySet(),
|
||||
// }
|
||||
// readReq.Entity.Entity = cloneEntity(expectedEntity)
|
||||
// results := newMockResults(t, mock, sqlEntityRead, readReq)
|
||||
|
||||
// // setup expectations
|
||||
// results.AddCurrentData()
|
||||
// mock.ExpectQuery(`select from entity where !resource_version update`).
|
||||
// WillReturnRows(results.Rows())
|
||||
|
||||
// // execute and assert
|
||||
// e, err := readEntity(ctx, db, sqltemplate.MySQL, key, 0, false, true)
|
||||
// require.NoError(t, err)
|
||||
// require.Equal(t, expectedEntity, e.Entity)
|
||||
// })
|
||||
|
||||
// t.Run("happy path - entity_history table", func(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
// // test declarations
|
||||
// ctx := testutil.NewDefaultTestContext(t)
|
||||
// db, mock := newMockDBMatchWords(t)
|
||||
// readReq := sqlEntityReadRequest{ // used to generate mock results
|
||||
// SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
|
||||
// Key: new(grafanaregistry.Key),
|
||||
// returnsEntitySet: newReturnsEntitySet(),
|
||||
// }
|
||||
// readReq.Entity.Entity = cloneEntity(expectedEntity)
|
||||
// results := newMockResults(t, mock, sqlEntityRead, readReq)
|
||||
|
||||
// // setup expectations
|
||||
// results.AddCurrentData()
|
||||
// mock.ExpectQuery(`select from entity_history where resource_version !update`).
|
||||
// WillReturnRows(results.Rows())
|
||||
|
||||
// // execute and assert
|
||||
// e, err := readEntity(ctx, db, sqltemplate.MySQL, key,
|
||||
// expectedEntity.ResourceVersion, false, false)
|
||||
// require.NoError(t, err)
|
||||
// require.Equal(t, expectedEntity, e.Entity)
|
||||
// })
|
||||
|
||||
// t.Run("entity table, optimistic locking failed", func(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
// ctx := testutil.NewDefaultTestContext(t)
|
||||
// db, mock := newMockDBMatchWords(t)
|
||||
// x := expectReadEntity(t, mock, nil)
|
||||
// x(ctx, db)
|
||||
// })
|
||||
|
||||
// t.Run("entity_history table, entity not found", func(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
// // test declarations
|
||||
// ctx := testutil.NewDefaultTestContext(t)
|
||||
// db, mock := newMockDBMatchWords(t)
|
||||
// readReq := sqlEntityReadRequest{ // used to generate mock results
|
||||
// SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
|
||||
// Key: new(grafanaregistry.Key),
|
||||
// returnsEntitySet: newReturnsEntitySet(),
|
||||
// }
|
||||
// results := newMockResults(t, mock, sqlEntityRead, readReq)
|
||||
|
||||
// // setup expectations
|
||||
// mock.ExpectQuery(`select from entity_history where resource_version !update`).
|
||||
// WillReturnRows(results.Rows())
|
||||
|
||||
// // execute and assert
|
||||
// e, err := readEntity(ctx, db, sqltemplate.MySQL, key,
|
||||
// expectedEntity.ResourceVersion, false, false)
|
||||
// require.Nil(t, e)
|
||||
// require.Error(t, err)
|
||||
// require.ErrorIs(t, err, ErrNotFound)
|
||||
// })
|
||||
|
||||
// t.Run("entity_history table, entity was deleted = not found", func(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
// // test declarations
|
||||
// ctx := testutil.NewDefaultTestContext(t)
|
||||
// db, mock := newMockDBMatchWords(t)
|
||||
// readReq := sqlEntityReadRequest{ // used to generate mock results
|
||||
// SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
|
||||
// Key: new(grafanaregistry.Key),
|
||||
// returnsEntitySet: newReturnsEntitySet(),
|
||||
// }
|
||||
// readReq.Entity.Entity = cloneEntity(expectedEntity)
|
||||
// readReq.Entity.Entity.Action = entity.Entity_DELETED
|
||||
// results := newMockResults(t, mock, sqlEntityRead, readReq)
|
||||
|
||||
// // setup expectations
|
||||
// results.AddCurrentData()
|
||||
// mock.ExpectQuery(`select from entity_history where resource_version !update`).
|
||||
// WillReturnRows(results.Rows())
|
||||
|
||||
// // execute and assert
|
||||
// e, err := readEntity(ctx, db, sqltemplate.MySQL, key,
|
||||
// expectedEntity.ResourceVersion, false, false)
|
||||
// require.Nil(t, e)
|
||||
// require.Error(t, err)
|
||||
// require.ErrorIs(t, err, ErrNotFound)
|
||||
// })
|
||||
// }
|
||||
|
||||
// // expectReadEntity arranges test expectations so that it's easier to reuse
|
||||
// // across tests that need to call `readEntity`. If you provide a non-nil
|
||||
// // *entity.Entity, that will be returned by `readEntity`. If it's nil, then
|
||||
// // `readEntity` will return ErrOptimisticLockingFailed. It returns the function
|
||||
// // to execute the actual test and assert the expectations that were set.
|
||||
// func expectReadEntity(t *testing.T, mock sqlmock.Sqlmock, e *entity.Entity) func(ctx context.Context, db db.DB) {
|
||||
// t.Helper()
|
||||
|
||||
// // test declarations
|
||||
// readReq := sqlEntityReadRequest{ // used to generate mock results
|
||||
// SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
|
||||
// Key: new(grafanaregistry.Key),
|
||||
// returnsEntitySet: newReturnsEntitySet(),
|
||||
// }
|
||||
// results := newMockResults(t, mock, sqlEntityRead, readReq)
|
||||
// if e != nil {
|
||||
// readReq.Entity.Entity = cloneEntity(e)
|
||||
// }
|
||||
|
||||
// // setup expectations
|
||||
// results.AddCurrentData()
|
||||
// mock.ExpectQuery(`select from entity where !resource_version update`).
|
||||
// WillReturnRows(results.Rows())
|
||||
|
||||
// // execute and assert
|
||||
// if e != nil {
|
||||
// return func(ctx context.Context, db db.DB) {
|
||||
// ent, err := readEntity(ctx, db, sqltemplate.MySQL, readReq.Key,
|
||||
// e.ResourceVersion, true, true)
|
||||
// require.NoError(t, err)
|
||||
// require.Equal(t, e, ent.Entity)
|
||||
// }
|
||||
// }
|
||||
|
||||
// return func(ctx context.Context, db db.DB) {
|
||||
// ent, err := readEntity(ctx, db, sqltemplate.MySQL, readReq.Key, 1, true,
|
||||
// true)
|
||||
// require.Nil(t, ent)
|
||||
// require.Error(t, err)
|
||||
// require.ErrorIs(t, err, ErrOptimisticLockingFailed)
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestKindVersionAtomicInc(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
// t.Run("happy path - row locked", func(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
// // test declarations
|
||||
// const curVersion int64 = 1
|
||||
// ctx := testutil.NewDefaultTestContext(t)
|
||||
// db, mock := newMockDBMatchWords(t)
|
||||
|
||||
// // setup expectations
|
||||
// mock.ExpectQuery(`select resource_version from resource_version where group resource update`).
|
||||
// WillReturnRows(mock.NewRows([]string{"resource_version"}).AddRow(curVersion))
|
||||
// mock.ExpectExec("update resource_version set resource_version updated_at where group resource").
|
||||
// WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
// // execute and assert
|
||||
// gotVersion, err := kindVersionAtomicInc(ctx, db, sqltemplate.MySQL, "groupname", "resname")
|
||||
// require.NoError(t, err)
|
||||
// require.Equal(t, curVersion+1, gotVersion)
|
||||
// })
|
||||
|
||||
// t.Run("happy path - row created", func(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
// ctx := testutil.NewDefaultTestContext(t)
|
||||
// db, mock := newMockDBMatchWords(t)
|
||||
// x := expectKindVersionAtomicInc(t, mock, false)
|
||||
// x(ctx, db)
|
||||
// })
|
||||
|
||||
// t.Run("fail to create row", func(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
// ctx := testutil.NewDefaultTestContext(t)
|
||||
// db, mock := newMockDBMatchWords(t)
|
||||
// x := expectKindVersionAtomicInc(t, mock, true)
|
||||
// x(ctx, db)
|
||||
// })
|
||||
// }
|
||||
|
||||
// // expectKindVersionAtomicInc arranges test expectations so that it's easier to
|
||||
// // reuse across tests that need to call `kindVersionAtomicInc`. If you the test
|
||||
// // shuld fail, it will do so with `errTest`, and it will return resource version
|
||||
// // 1 otherwise. It returns the function to execute the actual test and assert
|
||||
// // the expectations that were set.
|
||||
// func expectKindVersionAtomicInc(t *testing.T, mock sqlmock.Sqlmock, shouldFail bool) func(ctx context.Context, db db.DB) {
|
||||
// t.Helper()
|
||||
|
||||
// // setup expectations
|
||||
// mock.ExpectQuery(`select resource_version from resource_version where group resource update`).
|
||||
// WillReturnRows(mock.NewRows([]string{"resource_version"}))
|
||||
// call := mock.ExpectExec("insert resource_version resource_version")
|
||||
|
||||
// // execute and assert
|
||||
// if shouldFail {
|
||||
// call.WillReturnError(errTest)
|
||||
|
||||
// return func(ctx context.Context, db db.DB) {
|
||||
// gotVersion, err := kindVersionAtomicInc(ctx, db, sqltemplate.MySQL, "groupname", "resname")
|
||||
// require.Zero(t, gotVersion)
|
||||
// require.Error(t, err)
|
||||
// require.ErrorIs(t, err, errTest)
|
||||
// }
|
||||
// }
|
||||
// call.WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
// return func(ctx context.Context, db db.DB) {
|
||||
// gotVersion, err := kindVersionAtomicInc(ctx, db, sqltemplate.MySQL, "groupname", "resname")
|
||||
// require.NoError(t, err)
|
||||
// require.Equal(t, int64(1), gotVersion)
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestMustTemplate(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
// require.Panics(t, func() {
|
||||
// mustTemplate("non existent file")
|
||||
// })
|
||||
// }
|
||||
|
||||
// // Debug provides greater detail about the SQL error. It is defined on the same
|
||||
// // struct but on a test file so that the intention that its results should not
|
||||
// // be used in runtime code is very clear. The results could include PII or
|
||||
// // otherwise regulated information, hence this method is only available in
|
||||
// // tests, so that it can be used in local debugging only. Note that the error
|
||||
// // information may still be available through other means, like using the
|
||||
// // "reflect" package, so care must be taken not to ever expose these information
|
||||
// // in production.
|
||||
// func (e SQLError) Debug() string {
|
||||
// scanDestStr := "(none)"
|
||||
// if len(e.ScanDest) > 0 {
|
||||
// format := "[%T" + strings.Repeat(", %T", len(e.ScanDest)-1) + "]"
|
||||
// scanDestStr = fmt.Sprintf(format, e.ScanDest...)
|
||||
// }
|
||||
|
||||
// return fmt.Sprintf("%s: %s: %v\n\tArguments (%d): %#v\n\tReturn Value "+
|
||||
// "Types (%d): %s\n\tExecuted Query: %s\n\tRaw SQL Template Output: %s",
|
||||
// e.TemplateName, e.CallType, e.Err, len(e.arguments), e.arguments,
|
||||
// len(e.ScanDest), scanDestStr, e.Query, e.RawQuery)
|
||||
// }
|
85
pkg/storage/unified/sql/sqltemplate/args.go
Normal file
85
pkg/storage/unified/sql/sqltemplate/args.go
Normal file
@ -0,0 +1,85 @@
|
||||
package sqltemplate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Args errors.
|
||||
var (
|
||||
ErrInvalidArgList = errors.New("invalid arglist")
|
||||
)
|
||||
|
||||
// Args keeps the data that needs to be passed to the engine for execution in
|
||||
// the right order. Add it to your data types passed to SQLTemplate, either by
|
||||
// embedding or with a named struct field if its Arg method would clash with
|
||||
// another struct field.
|
||||
type Args struct {
|
||||
d interface{ ArgPlaceholder(argNum int) string }
|
||||
values []any
|
||||
}
|
||||
|
||||
func NewArgs(d Dialect) *Args {
|
||||
return &Args{
|
||||
d: d,
|
||||
}
|
||||
}
|
||||
|
||||
// Arg can be called from within templates to pass arguments to the SQL driver
|
||||
// to use in the execution of the query.
|
||||
func (a *Args) Arg(x any) string {
|
||||
a.values = append(a.values, x)
|
||||
|
||||
return a.d.ArgPlaceholder(len(a.values))
|
||||
}
|
||||
|
||||
// ArgList returns a comma separated list of `?` placeholders for each element
|
||||
// in the provided slice argument, calling Arg for each of them.
|
||||
// Example struct:
|
||||
//
|
||||
// type sqlMyRequest struct {
|
||||
// *sqltemplate.SQLTemplate
|
||||
// IDs []int64
|
||||
// }
|
||||
//
|
||||
// Example usage in a SQL template:
|
||||
//
|
||||
// DELETE FROM {{ .Ident "mytab" }}
|
||||
// WHERE id IN ( {{ argList . .IDs }} )
|
||||
// ;
|
||||
func (a *Args) ArgList(slice reflect.Value) (string, error) {
|
||||
if !slice.IsValid() || slice.Kind() != reflect.Slice {
|
||||
return "", ErrInvalidArgList
|
||||
}
|
||||
sliceLen := slice.Len()
|
||||
if sliceLen == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.Grow(3*sliceLen - 2) // the list will be ?, ?, ?
|
||||
for i, l := 0, slice.Len(); i < l; i++ {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
b.WriteString(a.Arg(slice.Index(i).Interface()))
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func (a *Args) Reset() {
|
||||
a.values = nil
|
||||
}
|
||||
|
||||
func (a *Args) GetArgs() []any {
|
||||
return a.values
|
||||
}
|
||||
|
||||
type ArgsIface interface {
|
||||
Arg(x any) string
|
||||
ArgList(slice reflect.Value) (string, error)
|
||||
GetArgs() []any
|
||||
Reset()
|
||||
}
|
101
pkg/storage/unified/sql/sqltemplate/args_test.go
Normal file
101
pkg/storage/unified/sql/sqltemplate/args_test.go
Normal file
@ -0,0 +1,101 @@
|
||||
package sqltemplate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestArgs_Arg(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldBeQuestionMark := func(t *testing.T, s string) {
|
||||
t.Helper()
|
||||
if s != "?" {
|
||||
t.Fatalf("expecting question mark, got %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
a := NewArgs(MySQL)
|
||||
|
||||
shouldBeQuestionMark(t, a.Arg(0))
|
||||
shouldBeQuestionMark(t, a.Arg(1))
|
||||
shouldBeQuestionMark(t, a.Arg(2))
|
||||
shouldBeQuestionMark(t, a.Arg(3))
|
||||
shouldBeQuestionMark(t, a.Arg(4))
|
||||
|
||||
for i, arg := range a.GetArgs() {
|
||||
v, ok := arg.(int)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected value: %T(%v)", arg, arg)
|
||||
}
|
||||
if v != i {
|
||||
t.Fatalf("unexpected int value: %v", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArg_ArgList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
input reflect.Value
|
||||
added []any
|
||||
output string
|
||||
err error
|
||||
}{
|
||||
{err: ErrInvalidArgList},
|
||||
{input: reflect.ValueOf(1), err: ErrInvalidArgList},
|
||||
{input: reflect.ValueOf(nil), err: ErrInvalidArgList},
|
||||
{input: reflect.ValueOf(any(nil)), err: ErrInvalidArgList},
|
||||
{input: reflect.ValueOf("asd"), err: ErrInvalidArgList},
|
||||
{input: reflect.ValueOf([]any{})},
|
||||
|
||||
{
|
||||
input: reflect.ValueOf([]any{true}),
|
||||
added: []any{true},
|
||||
output: "?",
|
||||
},
|
||||
|
||||
{
|
||||
input: reflect.ValueOf([]any{1, true}),
|
||||
added: []any{1, true},
|
||||
output: "?, ?",
|
||||
},
|
||||
|
||||
{
|
||||
input: reflect.ValueOf([]any{1, "asd", true}),
|
||||
added: []any{1, "asd", true},
|
||||
output: "?, ?, ?",
|
||||
},
|
||||
}
|
||||
|
||||
var a Args
|
||||
a.d = argFmtSQL92
|
||||
for i, tc := range testCases {
|
||||
a.Reset()
|
||||
|
||||
gotOutput, gotErr := a.ArgList(tc.input)
|
||||
if !errors.Is(gotErr, tc.err) {
|
||||
t.Fatalf("[test #%d] Unexpected error. Expected: %v, actual: %v",
|
||||
i, gotErr, tc.err)
|
||||
}
|
||||
|
||||
if tc.output != gotOutput {
|
||||
t.Fatalf("[test #%d] Unexpected output. Expected: %v, actual: %v",
|
||||
i, gotOutput, tc.output)
|
||||
}
|
||||
|
||||
if len(tc.added) != len(a.values) {
|
||||
t.Fatalf("[test #%d] Unexpected added items.\n\tExpected: %#v\n\t"+
|
||||
"Actual: %#v", i, tc.added, a.values)
|
||||
}
|
||||
|
||||
for j := range tc.added {
|
||||
if !reflect.DeepEqual(tc.added[j], a.values[j]) {
|
||||
t.Fatalf("[test #%d] Unexpected %d-eth item.\n\tExpected:"+
|
||||
" %#v\n\tActual: %#v", i, j, tc.added[j], a.values[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
140
pkg/storage/unified/sql/sqltemplate/dialect.go
Normal file
140
pkg/storage/unified/sql/sqltemplate/dialect.go
Normal file
@ -0,0 +1,140 @@
|
||||
package sqltemplate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Dialect-agnostic errors.
|
||||
var (
|
||||
ErrEmptyIdent = errors.New("empty identifier")
|
||||
ErrInvalidRowLockingClause = errors.New("invalid row-locking clause")
|
||||
)
|
||||
|
||||
// Dialect should be added to the data types passed to SQL templates to
|
||||
// provide methods that deal with SQL implementation-specific traits. It can be
|
||||
// embedded for ease of use, or with a named struct field if any of its methods
|
||||
// would clash with other struct field names.
|
||||
type Dialect interface {
|
||||
// Name identifies the Dialect. Note that a Dialect may be common to more
|
||||
// than one DBMS (e.g. "postgres" is common to PostgreSQL and to
|
||||
// CockroachDB), while we can maintain different Dialects for the same DBMS
|
||||
// but different versions (e.g. "mysql5" and "mysql8").
|
||||
Name() string
|
||||
|
||||
// Ident returns the given string quoted in a way that is suitable to be
|
||||
// used as an identifier. Database names, schema names, table names, column
|
||||
// names are all examples of identifiers.
|
||||
Ident(string) (string, error)
|
||||
|
||||
// ArgPlaceholder returns a safe argument suitable to be used in a SQL
|
||||
// prepared statement for the argNum-eth argument passed in execution
|
||||
// (starting at 1). The SQL92 Standard specifies the question mark ('?')
|
||||
// should be used in all cases, but some implementations differ.
|
||||
ArgPlaceholder(argNum int) string
|
||||
|
||||
// SelectFor parses and returns the given row-locking clause for a SELECT
|
||||
// statement. If the clause is invalid it returns an error. Implementations
|
||||
// of this method should use ParseRowLockingClause.
|
||||
// Example:
|
||||
//
|
||||
// SELECT *
|
||||
// FROM mytab
|
||||
// WHERE id = ?
|
||||
// {{ .SelectFor "Update NoWait" }}; -- will be uppercased
|
||||
SelectFor(...string) (string, error)
|
||||
}
|
||||
|
||||
// RowLockingClause represents a row-locking clause in a SELECT statement.
|
||||
type RowLockingClause string
|
||||
|
||||
// Valid returns whether the given option is valid.
|
||||
func (o RowLockingClause) Valid() bool {
|
||||
switch o {
|
||||
case SelectForShare, SelectForShareNoWait, SelectForShareSkipLocked,
|
||||
SelectForUpdate, SelectForUpdateNoWait, SelectForUpdateSkipLocked:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ParseRowLockingClause parses a RowLockingClause from the given strings. This
|
||||
// should be used by implementations of Dialect to parse the input of the
|
||||
// SelectFor method.
|
||||
func ParseRowLockingClause(s ...string) (RowLockingClause, error) {
|
||||
opt := RowLockingClause(strings.ToUpper(strings.Join(s, " ")))
|
||||
if !opt.Valid() {
|
||||
return "", ErrInvalidRowLockingClause
|
||||
}
|
||||
|
||||
return opt, nil
|
||||
}
|
||||
|
||||
// Row-locking clause options.
|
||||
const (
|
||||
SelectForShare RowLockingClause = "SHARE"
|
||||
SelectForShareNoWait RowLockingClause = "SHARE NOWAIT"
|
||||
SelectForShareSkipLocked RowLockingClause = "SHARE SKIP LOCKED"
|
||||
SelectForUpdate RowLockingClause = "UPDATE"
|
||||
SelectForUpdateNoWait RowLockingClause = "UPDATE NOWAIT"
|
||||
SelectForUpdateSkipLocked RowLockingClause = "UPDATE SKIP LOCKED"
|
||||
)
|
||||
|
||||
type rowLockingClauseMap map[RowLockingClause]RowLockingClause
|
||||
|
||||
func (rlc rowLockingClauseMap) SelectFor(s ...string) (string, error) {
|
||||
// all implementations should err on invalid input, otherwise we would just
|
||||
// be hiding the error until we change the dialect
|
||||
o, err := ParseRowLockingClause(s...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var ret string
|
||||
if len(rlc) > 0 {
|
||||
ret = "FOR " + string(rlc[o])
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
var rowLockingClauseAll = rowLockingClauseMap{
|
||||
SelectForShare: SelectForShare,
|
||||
SelectForShareNoWait: SelectForShareNoWait,
|
||||
SelectForShareSkipLocked: SelectForShareSkipLocked,
|
||||
SelectForUpdate: SelectForUpdate,
|
||||
SelectForUpdateNoWait: SelectForUpdateNoWait,
|
||||
SelectForUpdateSkipLocked: SelectForUpdateSkipLocked,
|
||||
}
|
||||
|
||||
// standardIdent provides standard SQL escaping of identifiers.
|
||||
type standardIdent struct{}
|
||||
|
||||
func (standardIdent) Ident(s string) (string, error) {
|
||||
if s == "" {
|
||||
return "", ErrEmptyIdent
|
||||
}
|
||||
return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`, nil
|
||||
}
|
||||
|
||||
type argPlaceholderFunc func(int) string
|
||||
|
||||
func (f argPlaceholderFunc) ArgPlaceholder(argNum int) string {
|
||||
return f(argNum)
|
||||
}
|
||||
|
||||
var (
|
||||
argFmtSQL92 = argPlaceholderFunc(func(int) string {
|
||||
return "?"
|
||||
})
|
||||
argFmtPositional = argPlaceholderFunc(func(argNum int) string {
|
||||
return "$" + strconv.Itoa(argNum)
|
||||
})
|
||||
)
|
||||
|
||||
type name string
|
||||
|
||||
func (n name) Name() string {
|
||||
return string(n)
|
||||
}
|
21
pkg/storage/unified/sql/sqltemplate/dialect_mysql.go
Normal file
21
pkg/storage/unified/sql/sqltemplate/dialect_mysql.go
Normal file
@ -0,0 +1,21 @@
|
||||
package sqltemplate
|
||||
|
||||
// MySQL is the default implementation of Dialect for the MySQL DMBS, currently
|
||||
// supporting MySQL-8.x. It relies on having ANSI_QUOTES SQL Mode enabled. For
|
||||
// more information about ANSI_QUOTES and SQL Modes see:
|
||||
//
|
||||
// https://dev.mysql.com/doc/refman/8.4/en/sql-mode.html#sqlmode_ansi_quotes
|
||||
var MySQL = mysql{
|
||||
rowLockingClauseMap: rowLockingClauseAll,
|
||||
argPlaceholderFunc: argFmtSQL92,
|
||||
name: "mysql",
|
||||
}
|
||||
|
||||
var _ Dialect = MySQL
|
||||
|
||||
type mysql struct {
|
||||
standardIdent
|
||||
rowLockingClauseMap
|
||||
argPlaceholderFunc
|
||||
name
|
||||
}
|
37
pkg/storage/unified/sql/sqltemplate/dialect_postgresql.go
Normal file
37
pkg/storage/unified/sql/sqltemplate/dialect_postgresql.go
Normal file
@ -0,0 +1,37 @@
|
||||
package sqltemplate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PostgreSQL is an implementation of Dialect for the PostgreSQL DMBS.
|
||||
var PostgreSQL = postgresql{
|
||||
rowLockingClauseMap: rowLockingClauseAll,
|
||||
argPlaceholderFunc: argFmtPositional,
|
||||
name: "postgres",
|
||||
}
|
||||
|
||||
var _ Dialect = PostgreSQL
|
||||
|
||||
// PostgreSQL-specific errors.
|
||||
var (
|
||||
ErrPostgreSQLUnsupportedIdent = errors.New("identifiers in PostgreSQL cannot contain the character with code zero")
|
||||
)
|
||||
|
||||
type postgresql struct {
|
||||
standardIdent
|
||||
rowLockingClauseMap
|
||||
argPlaceholderFunc
|
||||
name
|
||||
}
|
||||
|
||||
func (p postgresql) Ident(s string) (string, error) {
|
||||
// See:
|
||||
// https://www.postgresql.org/docs/current/sql-syntax-lexical.html
|
||||
if strings.IndexByte(s, 0) != -1 {
|
||||
return "", ErrPostgreSQLUnsupportedIdent
|
||||
}
|
||||
|
||||
return p.standardIdent.Ident(s)
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package sqltemplate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPostgreSQL_Ident(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
input string
|
||||
output string
|
||||
err error
|
||||
}{
|
||||
{input: ``, err: ErrEmptyIdent},
|
||||
{input: `polite_example`, output: `"polite_example"`},
|
||||
{input: `Juan Carlos`, output: `"Juan Carlos"`},
|
||||
{
|
||||
input: `unpolite_` + string([]byte{0}) + `example`,
|
||||
err: ErrPostgreSQLUnsupportedIdent,
|
||||
},
|
||||
{
|
||||
input: `exaggerated " ' ` + "`" + ` example`,
|
||||
output: `"exaggerated "" ' ` + "`" + ` example"`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
gotOutput, gotErr := PostgreSQL.Ident(tc.input)
|
||||
if !errors.Is(gotErr, tc.err) {
|
||||
t.Fatalf("unexpected error %v in test case %d", gotErr, i)
|
||||
}
|
||||
if gotOutput != tc.output {
|
||||
t.Fatalf("unexpected error %v in test case %d", gotErr, i)
|
||||
}
|
||||
}
|
||||
}
|
18
pkg/storage/unified/sql/sqltemplate/dialect_sqlite.go
Normal file
18
pkg/storage/unified/sql/sqltemplate/dialect_sqlite.go
Normal file
@ -0,0 +1,18 @@
|
||||
package sqltemplate
|
||||
|
||||
// SQLite is an implementation of Dialect for the SQLite DMBS.
|
||||
var SQLite = sqlite{
|
||||
argPlaceholderFunc: argFmtSQL92,
|
||||
name: "sqlite",
|
||||
}
|
||||
|
||||
var _ Dialect = SQLite
|
||||
|
||||
type sqlite struct {
|
||||
// See:
|
||||
// https://www.sqlite.org/lang_keywords.html
|
||||
standardIdent
|
||||
rowLockingClauseMap
|
||||
argPlaceholderFunc
|
||||
name
|
||||
}
|
184
pkg/storage/unified/sql/sqltemplate/dialect_test.go
Normal file
184
pkg/storage/unified/sql/sqltemplate/dialect_test.go
Normal file
@ -0,0 +1,184 @@
|
||||
package sqltemplate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSelectForOption_Valid(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
input RowLockingClause
|
||||
expected bool
|
||||
}{
|
||||
{input: "", expected: false},
|
||||
{input: "share", expected: false},
|
||||
{input: SelectForShare, expected: true},
|
||||
{input: SelectForShareNoWait, expected: true},
|
||||
{input: SelectForShareSkipLocked, expected: true},
|
||||
{input: SelectForUpdate, expected: true},
|
||||
{input: SelectForUpdateNoWait, expected: true},
|
||||
{input: SelectForUpdateSkipLocked, expected: true},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
got := tc.input.Valid()
|
||||
if got != tc.expected {
|
||||
t.Fatalf("unexpected %v in test case %d", got, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRowLockingClause(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
splitSpace := func(s string) []string {
|
||||
return strings.Split(s, " ")
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
input []string
|
||||
output RowLockingClause
|
||||
err error
|
||||
}{
|
||||
{err: ErrInvalidRowLockingClause},
|
||||
{
|
||||
input: []string{" " + string(SelectForShare)},
|
||||
err: ErrInvalidRowLockingClause,
|
||||
},
|
||||
{
|
||||
input: splitSpace(string(SelectForShareNoWait)),
|
||||
output: SelectForShareNoWait,
|
||||
},
|
||||
{
|
||||
input: splitSpace(strings.ToLower(string(SelectForShareNoWait))),
|
||||
output: SelectForShareNoWait,
|
||||
},
|
||||
{
|
||||
input: splitSpace(strings.ToTitle(string(SelectForShareNoWait))),
|
||||
output: SelectForShareNoWait,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
gotOutput, gotErr := ParseRowLockingClause(tc.input...)
|
||||
if !errors.Is(gotErr, tc.err) {
|
||||
t.Fatalf("unexpected error %v in test case %d", gotErr, i)
|
||||
}
|
||||
if gotOutput != (tc.output) {
|
||||
t.Fatalf("unexpected output %q in test case %d", gotOutput, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRowLockingClauseMap_SelectFor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
splitSpace := func(s string) []string {
|
||||
return strings.Split(s, " ")
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
input []string
|
||||
output RowLockingClause
|
||||
err error
|
||||
}{
|
||||
{err: ErrInvalidRowLockingClause},
|
||||
{input: []string{"invalid"}, err: ErrInvalidRowLockingClause},
|
||||
{input: []string{" share"}, err: ErrInvalidRowLockingClause},
|
||||
|
||||
{
|
||||
input: splitSpace(string(SelectForShare)),
|
||||
output: "FOR " + SelectForShare,
|
||||
},
|
||||
}
|
||||
|
||||
var nilRLC rowLockingClauseMap
|
||||
for i, tc := range testCases {
|
||||
gotOutput, gotErr := nilRLC.SelectFor(tc.input...)
|
||||
if !errors.Is(gotErr, tc.err) {
|
||||
t.Fatalf("[nil] unexpected error %v in test case %d", gotErr, i)
|
||||
}
|
||||
if gotOutput != "" {
|
||||
t.Fatalf("[nil] unexpected output %v in test case %d", gotOutput, i)
|
||||
}
|
||||
|
||||
gotOutput, gotErr = rowLockingClauseAll.SelectFor(tc.input...)
|
||||
if !errors.Is(gotErr, tc.err) {
|
||||
t.Fatalf("[all] unexpected error %v in test case %d", gotErr, i)
|
||||
}
|
||||
if gotOutput != string(tc.output) {
|
||||
t.Fatalf("[all] unexpected output %v in test case %d", gotOutput, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStandardIdent_Ident(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
input string
|
||||
output string
|
||||
err error
|
||||
}{
|
||||
{input: ``, err: ErrEmptyIdent},
|
||||
{input: `polite_example`, output: `"polite_example"`},
|
||||
{input: `Juan Carlos`, output: `"Juan Carlos"`},
|
||||
{
|
||||
input: `exaggerated " ' ` + "`" + ` example`,
|
||||
output: `"exaggerated "" ' ` + "`" + ` example"`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
gotOutput, gotErr := standardIdent{}.Ident(tc.input)
|
||||
if !errors.Is(gotErr, tc.err) {
|
||||
t.Fatalf("unexpected error %v in test case %d", gotErr, i)
|
||||
}
|
||||
if gotOutput != tc.output {
|
||||
t.Fatalf("unexpected error %v in test case %d", gotErr, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgPlaceholderFunc(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
input int
|
||||
valuePositional string
|
||||
}{
|
||||
{
|
||||
input: 1,
|
||||
valuePositional: "$1",
|
||||
},
|
||||
{
|
||||
input: 16,
|
||||
valuePositional: "$16",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
got := argFmtSQL92(tc.input)
|
||||
if got != "?" {
|
||||
t.Fatalf("[argFmtSQL92] unexpected value %q in test case %d", got, i)
|
||||
}
|
||||
|
||||
got = argFmtPositional(tc.input)
|
||||
if got != tc.valuePositional {
|
||||
t.Fatalf("[argFmtPositional] unexpected value %q in test case %d", got, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestName_Name(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const v = "some dialect name"
|
||||
n := name(v)
|
||||
if n.Name() != v {
|
||||
t.Fatalf("unexpected dialect name %q", n.Name())
|
||||
}
|
||||
}
|
156
pkg/storage/unified/sql/sqltemplate/example_test.go
Normal file
156
pkg/storage/unified/sql/sqltemplate/example_test.go
Normal file
@ -0,0 +1,156 @@
|
||||
package sqltemplate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// This file contains runnable examples. They serve the purpose of providing
|
||||
// idiomatic usage of the package as well as showing how it actually works,
|
||||
// since the examples are actually run together with regular Go tests. Note that
|
||||
// the "Output" comment section at the end of each function starting with
|
||||
// "Example" is used by the standard Go test tool to check that the standard
|
||||
// output of the function matches the commented text until the end of the
|
||||
// function. If you change the function, you may need to adapt that comment
|
||||
// section as it's possible that the output changes, causing it to fail tests.
|
||||
// To learn more about Go's runnable tests, which are a core builtin feature of
|
||||
// Go's standard testing library, see:
|
||||
// https://pkg.go.dev/testing#hdr-Examples
|
||||
//
|
||||
// If you're unfamiliar with Go text templating language, please, consider
|
||||
// reading that library's documentation first.
|
||||
|
||||
// In this example we will use both Args and Dialect to dynamically and securely
|
||||
// build SQL queries, while also keeping track of the arguments that need to be
|
||||
// passed to the database methods to replace the placeholder "?" with the
|
||||
// correct values.
|
||||
|
||||
// We will start by assuming we receive a request to retrieve a user's
|
||||
// information and that we need to provide a certain response.
|
||||
|
||||
type GetUserRequest struct {
|
||||
ID int
|
||||
}
|
||||
|
||||
type GetUserResponse struct {
|
||||
ID int
|
||||
Type string
|
||||
Name string
|
||||
}
|
||||
|
||||
// Our template will take care for us of taking the request to build the query,
|
||||
// and then sort the arguments for execution as well as preparing the values
|
||||
// that need to be read for the response. We wil create a struct to pass the
|
||||
// request and an empty response, as well as a *SQLTemplate that will provide
|
||||
// the methods to achieve our purpose::
|
||||
|
||||
type GetUserQuery struct {
|
||||
*SQLTemplate
|
||||
Request *GetUserRequest
|
||||
Response *GetUserResponse
|
||||
}
|
||||
|
||||
// And finally we will define our template, that is free to use all the power of
|
||||
// the Go templating language, plus the methods we added with *SQLTemplate:
|
||||
var getUserTmpl = template.Must(template.New("example").Parse(`
|
||||
SELECT
|
||||
{{ .Ident "id" | .Into .Response.ID }},
|
||||
{{ .Ident "type" | .Into .Response.Type }},
|
||||
{{ .Ident "name" | .Into .Response.Name }}
|
||||
|
||||
FROM {{ .Ident "users" }}
|
||||
WHERE
|
||||
{{ .Ident "id" }} = {{ .Arg .Request.ID }};
|
||||
`))
|
||||
|
||||
// There are three interesting methods used in the above template:
|
||||
// 1. Ident: safely escape a SQL identifier. Even though here the only
|
||||
// identifier that may be problematic is "type" (because it is a reserved
|
||||
// word in many dialects), it is a good practice to escape all identifiers
|
||||
// just to make sure we're accounting for all variability in dialects, and
|
||||
// also for consistency.
|
||||
// 2. Into: this causes the selected field to be saved to the corresponding
|
||||
// field of GetUserQuery.
|
||||
// 3. Arg: this allows us to state that at this point will be a "?" that has to
|
||||
// be populated with the value of the given field of GetUserQuery.
|
||||
|
||||
func Example() {
|
||||
// Let's pretend this example function is the handler of the GetUser method
|
||||
// of our service to see how it all works together.
|
||||
|
||||
queryData := &GetUserQuery{
|
||||
// The dialect (in this case we chose MySQL) should be set in your
|
||||
// service at startup when you connect to your database
|
||||
SQLTemplate: New(MySQL),
|
||||
|
||||
// This is a synthetic request for our test
|
||||
Request: &GetUserRequest{
|
||||
ID: 1,
|
||||
},
|
||||
|
||||
// Create an empty response to be populated
|
||||
Response: new(GetUserResponse),
|
||||
}
|
||||
|
||||
// The next step is to execute the query template for our queryData, and
|
||||
// generate the arguments for the db.QueryRow and row.Scan methods later
|
||||
query, err := Execute(getUserTmpl, queryData)
|
||||
if err != nil {
|
||||
panic(err) // terminate the runnable example on error
|
||||
}
|
||||
|
||||
// Assuming that we have a *sql.DB object named "db", we could now make our
|
||||
// query with:
|
||||
// row := db.QueryRowContext(ctx, query, queryData.GetArgs()...)
|
||||
// // and check row.Err() here
|
||||
|
||||
// As we're not actually running a database in this example, let's verify
|
||||
// that we find our arguments populated as expected instead:
|
||||
if len(queryData.GetArgs()) != 1 {
|
||||
panic(fmt.Sprintf("unexpected number of args: %#v", queryData.Args))
|
||||
}
|
||||
id, ok := queryData.GetArgs()[0].(int)
|
||||
if !ok || id != queryData.Request.ID {
|
||||
panic(fmt.Sprintf("unexpected args: %#v", queryData.Args))
|
||||
}
|
||||
|
||||
// In your code you would now have "row" populated with the row data,
|
||||
// assuming that the operation succeeded, so you would now scan the row data
|
||||
// abd populate the values of our response:
|
||||
// err := row.Scan(queryData.GetScanDest()...)
|
||||
// // and check err here
|
||||
|
||||
// Again, as we're not actually running a database in this example, we will
|
||||
// instead run the code to assert that queryData.ScanDest was populated with
|
||||
// the expected data, which should be pointers to each of the fields of
|
||||
// Response so that the Scan method can write to them:
|
||||
if len(queryData.GetScanDest()) != 3 {
|
||||
panic(fmt.Sprintf("unexpected number of scan dest: %#v", queryData.ScanDest))
|
||||
}
|
||||
idPtr, ok := queryData.GetScanDest()[0].(*int)
|
||||
if !ok || idPtr != &queryData.Response.ID {
|
||||
panic(fmt.Sprintf("unexpected response 'id' pointer: %#v", queryData.ScanDest))
|
||||
}
|
||||
typePtr, ok := queryData.GetScanDest()[1].(*string)
|
||||
if !ok || typePtr != &queryData.Response.Type {
|
||||
panic(fmt.Sprintf("unexpected response 'type' pointer: %#v", queryData.ScanDest))
|
||||
}
|
||||
namePtr, ok := queryData.GetScanDest()[2].(*string)
|
||||
if !ok || namePtr != &queryData.Response.Name {
|
||||
panic(fmt.Sprintf("unexpected response 'name' pointer: %#v", queryData.ScanDest))
|
||||
}
|
||||
|
||||
// Remember the variable "query"? Well, we didn't check it. We will now make
|
||||
// use of Go's runnable examples and print its contents to standard output
|
||||
// so Go's tooling verify this example's output each time we run tests.
|
||||
// By the way, to make the result more stable, we will remove some
|
||||
// unnecessary white space from the query.
|
||||
whiteSpaceRE := regexp.MustCompile(`\s+`)
|
||||
query = strings.TrimSpace(whiteSpaceRE.ReplaceAllString(query, " "))
|
||||
fmt.Println(query)
|
||||
|
||||
// Output:
|
||||
// SELECT "id", "type", "name" FROM "users" WHERE "id" = ?;
|
||||
}
|
41
pkg/storage/unified/sql/sqltemplate/into.go
Normal file
41
pkg/storage/unified/sql/sqltemplate/into.go
Normal file
@ -0,0 +1,41 @@
|
||||
package sqltemplate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type ScanDest struct {
|
||||
values []any
|
||||
colNames []string
|
||||
}
|
||||
|
||||
func (i *ScanDest) Into(v reflect.Value, colName string) (string, error) {
|
||||
if !v.IsValid() || !v.CanAddr() || !v.Addr().CanInterface() {
|
||||
return "", fmt.Errorf("invalid or unaddressable value: %v", colName)
|
||||
}
|
||||
|
||||
i.values = append(i.values, v.Addr().Interface())
|
||||
i.colNames = append(i.colNames, colName)
|
||||
|
||||
return colName, nil
|
||||
}
|
||||
|
||||
func (i *ScanDest) Reset() {
|
||||
i.values = nil
|
||||
}
|
||||
|
||||
func (i *ScanDest) GetScanDest() []any {
|
||||
return i.values
|
||||
}
|
||||
|
||||
func (i *ScanDest) GetColNames() []string {
|
||||
return i.colNames
|
||||
}
|
||||
|
||||
type ScanDestIface interface {
|
||||
Into(v reflect.Value, colName string) (string, error)
|
||||
GetScanDest() []any
|
||||
GetColNames() []string
|
||||
Reset()
|
||||
}
|
51
pkg/storage/unified/sql/sqltemplate/into_test.go
Normal file
51
pkg/storage/unified/sql/sqltemplate/into_test.go
Normal file
@ -0,0 +1,51 @@
|
||||
package sqltemplate
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScanDest_Into(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var d ScanDest
|
||||
|
||||
colName, err := d.Into(reflect.Value{}, "some field")
|
||||
if colName != "" || err == nil || len(d.GetScanDest()) != 0 {
|
||||
t.Fatalf("unexpected outcome, got colname %q, err: %v, scan dest: %#v",
|
||||
colName, err, d)
|
||||
}
|
||||
|
||||
data := struct {
|
||||
X int
|
||||
Y byte
|
||||
}{}
|
||||
dataVal := reflect.ValueOf(&data).Elem()
|
||||
|
||||
expectedColNames := []string{"some int", "and a byte"}
|
||||
|
||||
colName, err = d.Into(dataVal.FieldByName("X"), expectedColNames[0])
|
||||
v := d.GetScanDest()
|
||||
if err != nil || colName != expectedColNames[0] || len(v) != 1 || v[0] != &data.X {
|
||||
t.Fatalf("unexpected outcome, got colname %q, err: %v, scan dest: %#v",
|
||||
colName, err, d)
|
||||
}
|
||||
|
||||
colName, err = d.Into(dataVal.FieldByName("Y"), expectedColNames[1])
|
||||
v = d.GetScanDest()
|
||||
if err != nil || colName != expectedColNames[1] || len(v) != 2 || v[1] != &data.Y {
|
||||
t.Fatalf("unexpected outcome, got colname %q, err: %v, scan dest: %#v",
|
||||
colName, err, d)
|
||||
}
|
||||
|
||||
if gotColNames := d.GetColNames(); !slices.Equal(expectedColNames, gotColNames) {
|
||||
t.Fatalf("unexpected column names: %v", gotColNames)
|
||||
}
|
||||
|
||||
d.Reset()
|
||||
v = d.GetScanDest()
|
||||
if len(v) != 0 {
|
||||
t.Fatalf("unexpected values after reset: %v", v)
|
||||
}
|
||||
}
|
664
pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplateIface.go
Normal file
664
pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplateIface.go
Normal file
@ -0,0 +1,664 @@
|
||||
// Code generated by mockery v2.43.1. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
sqltemplate "github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate"
|
||||
)
|
||||
|
||||
// SQLTemplateIface is an autogenerated mock type for the SQLTemplateIface type
|
||||
type SQLTemplateIface struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type SQLTemplateIface_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *SQLTemplateIface) EXPECT() *SQLTemplateIface_Expecter {
|
||||
return &SQLTemplateIface_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Arg provides a mock function with given fields: x
|
||||
func (_m *SQLTemplateIface) Arg(x interface{}) string {
|
||||
ret := _m.Called(x)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Arg")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(interface{}) string); ok {
|
||||
r0 = rf(x)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SQLTemplateIface_Arg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Arg'
|
||||
type SQLTemplateIface_Arg_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Arg is a helper method to define mock.On call
|
||||
// - x interface{}
|
||||
func (_e *SQLTemplateIface_Expecter) Arg(x interface{}) *SQLTemplateIface_Arg_Call {
|
||||
return &SQLTemplateIface_Arg_Call{Call: _e.mock.On("Arg", x)}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Arg_Call) Run(run func(x interface{})) *SQLTemplateIface_Arg_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(interface{}))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Arg_Call) Return(_a0 string) *SQLTemplateIface_Arg_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Arg_Call) RunAndReturn(run func(interface{}) string) *SQLTemplateIface_Arg_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ArgList provides a mock function with given fields: slice
|
||||
func (_m *SQLTemplateIface) ArgList(slice reflect.Value) (string, error) {
|
||||
ret := _m.Called(slice)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ArgList")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value) (string, error)); ok {
|
||||
return rf(slice)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value) string); ok {
|
||||
r0 = rf(slice)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(reflect.Value) error); ok {
|
||||
r1 = rf(slice)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SQLTemplateIface_ArgList_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ArgList'
|
||||
type SQLTemplateIface_ArgList_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ArgList is a helper method to define mock.On call
|
||||
// - slice reflect.Value
|
||||
func (_e *SQLTemplateIface_Expecter) ArgList(slice interface{}) *SQLTemplateIface_ArgList_Call {
|
||||
return &SQLTemplateIface_ArgList_Call{Call: _e.mock.On("ArgList", slice)}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_ArgList_Call) Run(run func(slice reflect.Value)) *SQLTemplateIface_ArgList_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(reflect.Value))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_ArgList_Call) Return(_a0 string, _a1 error) *SQLTemplateIface_ArgList_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_ArgList_Call) RunAndReturn(run func(reflect.Value) (string, error)) *SQLTemplateIface_ArgList_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ArgPlaceholder provides a mock function with given fields: argNum
|
||||
func (_m *SQLTemplateIface) ArgPlaceholder(argNum int) string {
|
||||
ret := _m.Called(argNum)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ArgPlaceholder")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(int) string); ok {
|
||||
r0 = rf(argNum)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SQLTemplateIface_ArgPlaceholder_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ArgPlaceholder'
|
||||
type SQLTemplateIface_ArgPlaceholder_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ArgPlaceholder is a helper method to define mock.On call
|
||||
// - argNum int
|
||||
func (_e *SQLTemplateIface_Expecter) ArgPlaceholder(argNum interface{}) *SQLTemplateIface_ArgPlaceholder_Call {
|
||||
return &SQLTemplateIface_ArgPlaceholder_Call{Call: _e.mock.On("ArgPlaceholder", argNum)}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_ArgPlaceholder_Call) Run(run func(argNum int)) *SQLTemplateIface_ArgPlaceholder_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(int))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_ArgPlaceholder_Call) Return(_a0 string) *SQLTemplateIface_ArgPlaceholder_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_ArgPlaceholder_Call) RunAndReturn(run func(int) string) *SQLTemplateIface_ArgPlaceholder_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetArgs provides a mock function with given fields:
|
||||
func (_m *SQLTemplateIface) GetArgs() []interface{} {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetArgs")
|
||||
}
|
||||
|
||||
var r0 []interface{}
|
||||
if rf, ok := ret.Get(0).(func() []interface{}); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SQLTemplateIface_GetArgs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetArgs'
|
||||
type SQLTemplateIface_GetArgs_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetArgs is a helper method to define mock.On call
|
||||
func (_e *SQLTemplateIface_Expecter) GetArgs() *SQLTemplateIface_GetArgs_Call {
|
||||
return &SQLTemplateIface_GetArgs_Call{Call: _e.mock.On("GetArgs")}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetArgs_Call) Run(run func()) *SQLTemplateIface_GetArgs_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetArgs_Call) Return(_a0 []interface{}) *SQLTemplateIface_GetArgs_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetArgs_Call) RunAndReturn(run func() []interface{}) *SQLTemplateIface_GetArgs_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetColNames provides a mock function with given fields:
|
||||
func (_m *SQLTemplateIface) GetColNames() []string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetColNames")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SQLTemplateIface_GetColNames_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetColNames'
|
||||
type SQLTemplateIface_GetColNames_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetColNames is a helper method to define mock.On call
|
||||
func (_e *SQLTemplateIface_Expecter) GetColNames() *SQLTemplateIface_GetColNames_Call {
|
||||
return &SQLTemplateIface_GetColNames_Call{Call: _e.mock.On("GetColNames")}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetColNames_Call) Run(run func()) *SQLTemplateIface_GetColNames_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetColNames_Call) Return(_a0 []string) *SQLTemplateIface_GetColNames_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetColNames_Call) RunAndReturn(run func() []string) *SQLTemplateIface_GetColNames_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetScanDest provides a mock function with given fields:
|
||||
func (_m *SQLTemplateIface) GetScanDest() []interface{} {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetScanDest")
|
||||
}
|
||||
|
||||
var r0 []interface{}
|
||||
if rf, ok := ret.Get(0).(func() []interface{}); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SQLTemplateIface_GetScanDest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetScanDest'
|
||||
type SQLTemplateIface_GetScanDest_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetScanDest is a helper method to define mock.On call
|
||||
func (_e *SQLTemplateIface_Expecter) GetScanDest() *SQLTemplateIface_GetScanDest_Call {
|
||||
return &SQLTemplateIface_GetScanDest_Call{Call: _e.mock.On("GetScanDest")}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetScanDest_Call) Run(run func()) *SQLTemplateIface_GetScanDest_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetScanDest_Call) Return(_a0 []interface{}) *SQLTemplateIface_GetScanDest_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetScanDest_Call) RunAndReturn(run func() []interface{}) *SQLTemplateIface_GetScanDest_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Ident provides a mock function with given fields: _a0
|
||||
func (_m *SQLTemplateIface) Ident(_a0 string) (string, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Ident")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
|
||||
return rf(_a0)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) string); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SQLTemplateIface_Ident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ident'
|
||||
type SQLTemplateIface_Ident_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Ident is a helper method to define mock.On call
|
||||
// - _a0 string
|
||||
func (_e *SQLTemplateIface_Expecter) Ident(_a0 interface{}) *SQLTemplateIface_Ident_Call {
|
||||
return &SQLTemplateIface_Ident_Call{Call: _e.mock.On("Ident", _a0)}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Ident_Call) Run(run func(_a0 string)) *SQLTemplateIface_Ident_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Ident_Call) Return(_a0 string, _a1 error) *SQLTemplateIface_Ident_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Ident_Call) RunAndReturn(run func(string) (string, error)) *SQLTemplateIface_Ident_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Into provides a mock function with given fields: v, colName
|
||||
func (_m *SQLTemplateIface) Into(v reflect.Value, colName string) (string, error) {
|
||||
ret := _m.Called(v, colName)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Into")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value, string) (string, error)); ok {
|
||||
return rf(v, colName)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value, string) string); ok {
|
||||
r0 = rf(v, colName)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(reflect.Value, string) error); ok {
|
||||
r1 = rf(v, colName)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SQLTemplateIface_Into_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Into'
|
||||
type SQLTemplateIface_Into_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Into is a helper method to define mock.On call
|
||||
// - v reflect.Value
|
||||
// - colName string
|
||||
func (_e *SQLTemplateIface_Expecter) Into(v interface{}, colName interface{}) *SQLTemplateIface_Into_Call {
|
||||
return &SQLTemplateIface_Into_Call{Call: _e.mock.On("Into", v, colName)}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Into_Call) Run(run func(v reflect.Value, colName string)) *SQLTemplateIface_Into_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(reflect.Value), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Into_Call) Return(_a0 string, _a1 error) *SQLTemplateIface_Into_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Into_Call) RunAndReturn(run func(reflect.Value, string) (string, error)) *SQLTemplateIface_Into_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Name provides a mock function with given fields:
|
||||
func (_m *SQLTemplateIface) Name() string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Name")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SQLTemplateIface_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
|
||||
type SQLTemplateIface_Name_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Name is a helper method to define mock.On call
|
||||
func (_e *SQLTemplateIface_Expecter) Name() *SQLTemplateIface_Name_Call {
|
||||
return &SQLTemplateIface_Name_Call{Call: _e.mock.On("Name")}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Name_Call) Run(run func()) *SQLTemplateIface_Name_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Name_Call) Return(_a0 string) *SQLTemplateIface_Name_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Name_Call) RunAndReturn(run func() string) *SQLTemplateIface_Name_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Reset provides a mock function with given fields:
|
||||
func (_m *SQLTemplateIface) Reset() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// SQLTemplateIface_Reset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Reset'
|
||||
type SQLTemplateIface_Reset_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Reset is a helper method to define mock.On call
|
||||
func (_e *SQLTemplateIface_Expecter) Reset() *SQLTemplateIface_Reset_Call {
|
||||
return &SQLTemplateIface_Reset_Call{Call: _e.mock.On("Reset")}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Reset_Call) Run(run func()) *SQLTemplateIface_Reset_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Reset_Call) Return() *SQLTemplateIface_Reset_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Reset_Call) RunAndReturn(run func()) *SQLTemplateIface_Reset_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SelectFor provides a mock function with given fields: _a0
|
||||
func (_m *SQLTemplateIface) SelectFor(_a0 ...string) (string, error) {
|
||||
_va := make([]interface{}, len(_a0))
|
||||
for _i := range _a0 {
|
||||
_va[_i] = _a0[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SelectFor")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(...string) (string, error)); ok {
|
||||
return rf(_a0...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(...string) string); ok {
|
||||
r0 = rf(_a0...)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(...string) error); ok {
|
||||
r1 = rf(_a0...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SQLTemplateIface_SelectFor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectFor'
|
||||
type SQLTemplateIface_SelectFor_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SelectFor is a helper method to define mock.On call
|
||||
// - _a0 ...string
|
||||
func (_e *SQLTemplateIface_Expecter) SelectFor(_a0 ...interface{}) *SQLTemplateIface_SelectFor_Call {
|
||||
return &SQLTemplateIface_SelectFor_Call{Call: _e.mock.On("SelectFor",
|
||||
append([]interface{}{}, _a0...)...)}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_SelectFor_Call) Run(run func(_a0 ...string)) *SQLTemplateIface_SelectFor_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]string, len(args)-0)
|
||||
for i, a := range args[0:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(string)
|
||||
}
|
||||
}
|
||||
run(variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_SelectFor_Call) Return(_a0 string, _a1 error) *SQLTemplateIface_SelectFor_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_SelectFor_Call) RunAndReturn(run func(...string) (string, error)) *SQLTemplateIface_SelectFor_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetDialect provides a mock function with given fields: _a0
|
||||
func (_m *SQLTemplateIface) SetDialect(_a0 sqltemplate.Dialect) {
|
||||
_m.Called(_a0)
|
||||
}
|
||||
|
||||
// SQLTemplateIface_SetDialect_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDialect'
|
||||
type SQLTemplateIface_SetDialect_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetDialect is a helper method to define mock.On call
|
||||
// - _a0 sqltemplate.Dialect
|
||||
func (_e *SQLTemplateIface_Expecter) SetDialect(_a0 interface{}) *SQLTemplateIface_SetDialect_Call {
|
||||
return &SQLTemplateIface_SetDialect_Call{Call: _e.mock.On("SetDialect", _a0)}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_SetDialect_Call) Run(run func(_a0 sqltemplate.Dialect)) *SQLTemplateIface_SetDialect_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(sqltemplate.Dialect))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_SetDialect_Call) Return() *SQLTemplateIface_SetDialect_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_SetDialect_Call) RunAndReturn(run func(sqltemplate.Dialect)) *SQLTemplateIface_SetDialect_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Validate provides a mock function with given fields:
|
||||
func (_m *SQLTemplateIface) Validate() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Validate")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SQLTemplateIface_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate'
|
||||
type SQLTemplateIface_Validate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Validate is a helper method to define mock.On call
|
||||
func (_e *SQLTemplateIface_Expecter) Validate() *SQLTemplateIface_Validate_Call {
|
||||
return &SQLTemplateIface_Validate_Call{Call: _e.mock.On("Validate")}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Validate_Call) Run(run func()) *SQLTemplateIface_Validate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Validate_Call) Return(_a0 error) *SQLTemplateIface_Validate_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Validate_Call) RunAndReturn(run func() error) *SQLTemplateIface_Validate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewSQLTemplateIface creates a new instance of SQLTemplateIface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewSQLTemplateIface(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *SQLTemplateIface {
|
||||
mock := &SQLTemplateIface{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
719
pkg/storage/unified/sql/sqltemplate/mocks/WithResults.go
Normal file
719
pkg/storage/unified/sql/sqltemplate/mocks/WithResults.go
Normal file
@ -0,0 +1,719 @@
|
||||
// Code generated by mockery v2.43.1. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
sqltemplate "github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate"
|
||||
)
|
||||
|
||||
// WithResults is an autogenerated mock type for the WithResults type
|
||||
type WithResults[T interface{}] struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type WithResults_Expecter[T interface{}] struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *WithResults[T]) EXPECT() *WithResults_Expecter[T] {
|
||||
return &WithResults_Expecter[T]{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Arg provides a mock function with given fields: x
|
||||
func (_m *WithResults[T]) Arg(x interface{}) string {
|
||||
ret := _m.Called(x)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Arg")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(interface{}) string); ok {
|
||||
r0 = rf(x)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WithResults_Arg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Arg'
|
||||
type WithResults_Arg_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Arg is a helper method to define mock.On call
|
||||
// - x interface{}
|
||||
func (_e *WithResults_Expecter[T]) Arg(x interface{}) *WithResults_Arg_Call[T] {
|
||||
return &WithResults_Arg_Call[T]{Call: _e.mock.On("Arg", x)}
|
||||
}
|
||||
|
||||
func (_c *WithResults_Arg_Call[T]) Run(run func(x interface{})) *WithResults_Arg_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(interface{}))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Arg_Call[T]) Return(_a0 string) *WithResults_Arg_Call[T] {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Arg_Call[T]) RunAndReturn(run func(interface{}) string) *WithResults_Arg_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ArgList provides a mock function with given fields: slice
|
||||
func (_m *WithResults[T]) ArgList(slice reflect.Value) (string, error) {
|
||||
ret := _m.Called(slice)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ArgList")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value) (string, error)); ok {
|
||||
return rf(slice)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value) string); ok {
|
||||
r0 = rf(slice)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(reflect.Value) error); ok {
|
||||
r1 = rf(slice)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// WithResults_ArgList_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ArgList'
|
||||
type WithResults_ArgList_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ArgList is a helper method to define mock.On call
|
||||
// - slice reflect.Value
|
||||
func (_e *WithResults_Expecter[T]) ArgList(slice interface{}) *WithResults_ArgList_Call[T] {
|
||||
return &WithResults_ArgList_Call[T]{Call: _e.mock.On("ArgList", slice)}
|
||||
}
|
||||
|
||||
func (_c *WithResults_ArgList_Call[T]) Run(run func(slice reflect.Value)) *WithResults_ArgList_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(reflect.Value))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_ArgList_Call[T]) Return(_a0 string, _a1 error) *WithResults_ArgList_Call[T] {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_ArgList_Call[T]) RunAndReturn(run func(reflect.Value) (string, error)) *WithResults_ArgList_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ArgPlaceholder provides a mock function with given fields: argNum
|
||||
func (_m *WithResults[T]) ArgPlaceholder(argNum int) string {
|
||||
ret := _m.Called(argNum)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ArgPlaceholder")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(int) string); ok {
|
||||
r0 = rf(argNum)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WithResults_ArgPlaceholder_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ArgPlaceholder'
|
||||
type WithResults_ArgPlaceholder_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ArgPlaceholder is a helper method to define mock.On call
|
||||
// - argNum int
|
||||
func (_e *WithResults_Expecter[T]) ArgPlaceholder(argNum interface{}) *WithResults_ArgPlaceholder_Call[T] {
|
||||
return &WithResults_ArgPlaceholder_Call[T]{Call: _e.mock.On("ArgPlaceholder", argNum)}
|
||||
}
|
||||
|
||||
func (_c *WithResults_ArgPlaceholder_Call[T]) Run(run func(argNum int)) *WithResults_ArgPlaceholder_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(int))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_ArgPlaceholder_Call[T]) Return(_a0 string) *WithResults_ArgPlaceholder_Call[T] {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_ArgPlaceholder_Call[T]) RunAndReturn(run func(int) string) *WithResults_ArgPlaceholder_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetArgs provides a mock function with given fields:
|
||||
func (_m *WithResults[T]) GetArgs() []interface{} {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetArgs")
|
||||
}
|
||||
|
||||
var r0 []interface{}
|
||||
if rf, ok := ret.Get(0).(func() []interface{}); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WithResults_GetArgs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetArgs'
|
||||
type WithResults_GetArgs_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetArgs is a helper method to define mock.On call
|
||||
func (_e *WithResults_Expecter[T]) GetArgs() *WithResults_GetArgs_Call[T] {
|
||||
return &WithResults_GetArgs_Call[T]{Call: _e.mock.On("GetArgs")}
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetArgs_Call[T]) Run(run func()) *WithResults_GetArgs_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetArgs_Call[T]) Return(_a0 []interface{}) *WithResults_GetArgs_Call[T] {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetArgs_Call[T]) RunAndReturn(run func() []interface{}) *WithResults_GetArgs_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetColNames provides a mock function with given fields:
|
||||
func (_m *WithResults[T]) GetColNames() []string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetColNames")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WithResults_GetColNames_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetColNames'
|
||||
type WithResults_GetColNames_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetColNames is a helper method to define mock.On call
|
||||
func (_e *WithResults_Expecter[T]) GetColNames() *WithResults_GetColNames_Call[T] {
|
||||
return &WithResults_GetColNames_Call[T]{Call: _e.mock.On("GetColNames")}
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetColNames_Call[T]) Run(run func()) *WithResults_GetColNames_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetColNames_Call[T]) Return(_a0 []string) *WithResults_GetColNames_Call[T] {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetColNames_Call[T]) RunAndReturn(run func() []string) *WithResults_GetColNames_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetScanDest provides a mock function with given fields:
|
||||
func (_m *WithResults[T]) GetScanDest() []interface{} {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetScanDest")
|
||||
}
|
||||
|
||||
var r0 []interface{}
|
||||
if rf, ok := ret.Get(0).(func() []interface{}); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WithResults_GetScanDest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetScanDest'
|
||||
type WithResults_GetScanDest_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetScanDest is a helper method to define mock.On call
|
||||
func (_e *WithResults_Expecter[T]) GetScanDest() *WithResults_GetScanDest_Call[T] {
|
||||
return &WithResults_GetScanDest_Call[T]{Call: _e.mock.On("GetScanDest")}
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetScanDest_Call[T]) Run(run func()) *WithResults_GetScanDest_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetScanDest_Call[T]) Return(_a0 []interface{}) *WithResults_GetScanDest_Call[T] {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetScanDest_Call[T]) RunAndReturn(run func() []interface{}) *WithResults_GetScanDest_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Ident provides a mock function with given fields: _a0
|
||||
func (_m *WithResults[T]) Ident(_a0 string) (string, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Ident")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
|
||||
return rf(_a0)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) string); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// WithResults_Ident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ident'
|
||||
type WithResults_Ident_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Ident is a helper method to define mock.On call
|
||||
// - _a0 string
|
||||
func (_e *WithResults_Expecter[T]) Ident(_a0 interface{}) *WithResults_Ident_Call[T] {
|
||||
return &WithResults_Ident_Call[T]{Call: _e.mock.On("Ident", _a0)}
|
||||
}
|
||||
|
||||
func (_c *WithResults_Ident_Call[T]) Run(run func(_a0 string)) *WithResults_Ident_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Ident_Call[T]) Return(_a0 string, _a1 error) *WithResults_Ident_Call[T] {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Ident_Call[T]) RunAndReturn(run func(string) (string, error)) *WithResults_Ident_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Into provides a mock function with given fields: v, colName
|
||||
func (_m *WithResults[T]) Into(v reflect.Value, colName string) (string, error) {
|
||||
ret := _m.Called(v, colName)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Into")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value, string) (string, error)); ok {
|
||||
return rf(v, colName)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value, string) string); ok {
|
||||
r0 = rf(v, colName)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(reflect.Value, string) error); ok {
|
||||
r1 = rf(v, colName)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// WithResults_Into_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Into'
|
||||
type WithResults_Into_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Into is a helper method to define mock.On call
|
||||
// - v reflect.Value
|
||||
// - colName string
|
||||
func (_e *WithResults_Expecter[T]) Into(v interface{}, colName interface{}) *WithResults_Into_Call[T] {
|
||||
return &WithResults_Into_Call[T]{Call: _e.mock.On("Into", v, colName)}
|
||||
}
|
||||
|
||||
func (_c *WithResults_Into_Call[T]) Run(run func(v reflect.Value, colName string)) *WithResults_Into_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(reflect.Value), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Into_Call[T]) Return(_a0 string, _a1 error) *WithResults_Into_Call[T] {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Into_Call[T]) RunAndReturn(run func(reflect.Value, string) (string, error)) *WithResults_Into_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Name provides a mock function with given fields:
|
||||
func (_m *WithResults[T]) Name() string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Name")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WithResults_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
|
||||
type WithResults_Name_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Name is a helper method to define mock.On call
|
||||
func (_e *WithResults_Expecter[T]) Name() *WithResults_Name_Call[T] {
|
||||
return &WithResults_Name_Call[T]{Call: _e.mock.On("Name")}
|
||||
}
|
||||
|
||||
func (_c *WithResults_Name_Call[T]) Run(run func()) *WithResults_Name_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Name_Call[T]) Return(_a0 string) *WithResults_Name_Call[T] {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Name_Call[T]) RunAndReturn(run func() string) *WithResults_Name_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Reset provides a mock function with given fields:
|
||||
func (_m *WithResults[T]) Reset() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// WithResults_Reset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Reset'
|
||||
type WithResults_Reset_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Reset is a helper method to define mock.On call
|
||||
func (_e *WithResults_Expecter[T]) Reset() *WithResults_Reset_Call[T] {
|
||||
return &WithResults_Reset_Call[T]{Call: _e.mock.On("Reset")}
|
||||
}
|
||||
|
||||
func (_c *WithResults_Reset_Call[T]) Run(run func()) *WithResults_Reset_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Reset_Call[T]) Return() *WithResults_Reset_Call[T] {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Reset_Call[T]) RunAndReturn(run func()) *WithResults_Reset_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Results provides a mock function with given fields:
|
||||
func (_m *WithResults[T]) Results() (T, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Results")
|
||||
}
|
||||
|
||||
var r0 T
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (T, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() T); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(T)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// WithResults_Results_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Results'
|
||||
type WithResults_Results_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Results is a helper method to define mock.On call
|
||||
func (_e *WithResults_Expecter[T]) Results() *WithResults_Results_Call[T] {
|
||||
return &WithResults_Results_Call[T]{Call: _e.mock.On("Results")}
|
||||
}
|
||||
|
||||
func (_c *WithResults_Results_Call[T]) Run(run func()) *WithResults_Results_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Results_Call[T]) Return(_a0 T, _a1 error) *WithResults_Results_Call[T] {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Results_Call[T]) RunAndReturn(run func() (T, error)) *WithResults_Results_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SelectFor provides a mock function with given fields: _a0
|
||||
func (_m *WithResults[T]) SelectFor(_a0 ...string) (string, error) {
|
||||
_va := make([]interface{}, len(_a0))
|
||||
for _i := range _a0 {
|
||||
_va[_i] = _a0[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SelectFor")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(...string) (string, error)); ok {
|
||||
return rf(_a0...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(...string) string); ok {
|
||||
r0 = rf(_a0...)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(...string) error); ok {
|
||||
r1 = rf(_a0...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// WithResults_SelectFor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectFor'
|
||||
type WithResults_SelectFor_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SelectFor is a helper method to define mock.On call
|
||||
// - _a0 ...string
|
||||
func (_e *WithResults_Expecter[T]) SelectFor(_a0 ...interface{}) *WithResults_SelectFor_Call[T] {
|
||||
return &WithResults_SelectFor_Call[T]{Call: _e.mock.On("SelectFor",
|
||||
append([]interface{}{}, _a0...)...)}
|
||||
}
|
||||
|
||||
func (_c *WithResults_SelectFor_Call[T]) Run(run func(_a0 ...string)) *WithResults_SelectFor_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]string, len(args)-0)
|
||||
for i, a := range args[0:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(string)
|
||||
}
|
||||
}
|
||||
run(variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_SelectFor_Call[T]) Return(_a0 string, _a1 error) *WithResults_SelectFor_Call[T] {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_SelectFor_Call[T]) RunAndReturn(run func(...string) (string, error)) *WithResults_SelectFor_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetDialect provides a mock function with given fields: _a0
|
||||
func (_m *WithResults[T]) SetDialect(_a0 sqltemplate.Dialect) {
|
||||
_m.Called(_a0)
|
||||
}
|
||||
|
||||
// WithResults_SetDialect_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDialect'
|
||||
type WithResults_SetDialect_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetDialect is a helper method to define mock.On call
|
||||
// - _a0 sqltemplate.Dialect
|
||||
func (_e *WithResults_Expecter[T]) SetDialect(_a0 interface{}) *WithResults_SetDialect_Call[T] {
|
||||
return &WithResults_SetDialect_Call[T]{Call: _e.mock.On("SetDialect", _a0)}
|
||||
}
|
||||
|
||||
func (_c *WithResults_SetDialect_Call[T]) Run(run func(_a0 sqltemplate.Dialect)) *WithResults_SetDialect_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(sqltemplate.Dialect))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_SetDialect_Call[T]) Return() *WithResults_SetDialect_Call[T] {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_SetDialect_Call[T]) RunAndReturn(run func(sqltemplate.Dialect)) *WithResults_SetDialect_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Validate provides a mock function with given fields:
|
||||
func (_m *WithResults[T]) Validate() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Validate")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WithResults_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate'
|
||||
type WithResults_Validate_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Validate is a helper method to define mock.On call
|
||||
func (_e *WithResults_Expecter[T]) Validate() *WithResults_Validate_Call[T] {
|
||||
return &WithResults_Validate_Call[T]{Call: _e.mock.On("Validate")}
|
||||
}
|
||||
|
||||
func (_c *WithResults_Validate_Call[T]) Run(run func()) *WithResults_Validate_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Validate_Call[T]) Return(_a0 error) *WithResults_Validate_Call[T] {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Validate_Call[T]) RunAndReturn(run func() error) *WithResults_Validate_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewWithResults creates a new instance of WithResults. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewWithResults[T interface{}](t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *WithResults[T] {
|
||||
mock := &WithResults[T]{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
141
pkg/storage/unified/sql/sqltemplate/sqltemplate.go
Normal file
141
pkg/storage/unified/sql/sqltemplate/sqltemplate.go
Normal file
@ -0,0 +1,141 @@
|
||||
package sqltemplate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// Package-level errors.
|
||||
var (
|
||||
ErrValidationNotImplemented = errors.New("validation not implemented")
|
||||
ErrSQLTemplateNoSerialize = errors.New("SQLTemplate should not be serialized")
|
||||
)
|
||||
|
||||
// SQLTemplate provides comprehensive support for SQL templating, handling
|
||||
// dialect traits, execution arguments and scanning arguments.
|
||||
type SQLTemplate struct {
|
||||
Dialect
|
||||
Args
|
||||
ScanDest
|
||||
}
|
||||
|
||||
// New returns a nee *SQLTemplate that will use the given dialect.
|
||||
func New(d Dialect) *SQLTemplate {
|
||||
ret := new(SQLTemplate)
|
||||
ret.SetDialect(d)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t *SQLTemplate) Reset() {
|
||||
t.Args.Reset()
|
||||
t.ScanDest.Reset()
|
||||
}
|
||||
|
||||
func (t *SQLTemplate) SetDialect(d Dialect) {
|
||||
t.Reset()
|
||||
t.Dialect = d
|
||||
t.Args.d = d
|
||||
}
|
||||
|
||||
func (t *SQLTemplate) Validate() error {
|
||||
return ErrValidationNotImplemented
|
||||
}
|
||||
|
||||
func (t *SQLTemplate) MarshalJSON() ([]byte, error) {
|
||||
return nil, ErrSQLTemplateNoSerialize
|
||||
}
|
||||
|
||||
func (t *SQLTemplate) UnmarshalJSON([]byte) error {
|
||||
return ErrSQLTemplateNoSerialize
|
||||
}
|
||||
|
||||
//go:generate mockery --with-expecter --name SQLTemplateIface
|
||||
|
||||
// SQLTemplateIface can be used as argument in general purpose utilities
|
||||
// expecting a struct embedding *SQLTemplate.
|
||||
type SQLTemplateIface interface {
|
||||
Dialect
|
||||
ArgsIface
|
||||
ScanDestIface
|
||||
// Reset calls the Reset method of ArgsIface and ScanDestIface.
|
||||
Reset()
|
||||
// SetDialect allows reusing the template components. It should first call
|
||||
// Reset.
|
||||
SetDialect(Dialect)
|
||||
// Validate should be implemented to validate a request before executing the
|
||||
// template.
|
||||
Validate() error
|
||||
}
|
||||
|
||||
//go:generate mockery --with-expecter --name WithResults
|
||||
|
||||
// WithResults has an additional method suited for structs embedding
|
||||
// *SQLTemplate and returning a set of rows.
|
||||
type WithResults[T any] interface {
|
||||
SQLTemplateIface
|
||||
|
||||
// Results returns the results of the query. If the query is expected to
|
||||
// return a set of rows, then it should be a deep copy of the internal
|
||||
// results, so that it can be called multiple times to get the different
|
||||
// values.
|
||||
Results() (T, error)
|
||||
}
|
||||
|
||||
// Execute is a trivial utility to execute and return the results of any
|
||||
// text/template as a string and an error.
|
||||
func Execute(t *template.Template, data any) (string, error) {
|
||||
var b strings.Builder
|
||||
if err := t.Execute(&b, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// FormatSQL is an opinionated formatter for SQL template output. It can be used
|
||||
// to reduce the final code length, for debugging, and testing. It is not a
|
||||
// propoer and full-fledged SQL parser, so it makes the following assumptions,
|
||||
// which are also good practices for writing your SQL templates:
|
||||
// 1. There are no SQL comments. Consider adding your comments as template
|
||||
// comments instead (i.e. "{{/* this is a template comment */}}").
|
||||
// 2. There are no multiline strings, and strings do not contain consecutive
|
||||
// spaces. Code looking like this is already a smell. Avoid string literals,
|
||||
// pass them as arguments so they can be appropriately escaped by the
|
||||
// corresponding driver. And identifiers with white space should be avoided
|
||||
// in all cases as well.
|
||||
func FormatSQL(q string) string {
|
||||
q = strings.TrimSpace(q)
|
||||
for _, f := range formatREs {
|
||||
q = f.re.ReplaceAllString(q, f.replacement)
|
||||
}
|
||||
q = strings.TrimSpace(q)
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
type reFormatting struct {
|
||||
re *regexp.Regexp
|
||||
replacement string
|
||||
}
|
||||
|
||||
var formatREs = []reFormatting{
|
||||
{re: regexp.MustCompile(`\s+`), replacement: " "},
|
||||
{re: regexp.MustCompile(` ?([+-/*=<>%!~]+) ?`), replacement: " $1 "},
|
||||
{re: regexp.MustCompile(`([([{]) `), replacement: "$1"},
|
||||
{re: regexp.MustCompile(` ([)\]}])`), replacement: "$1"},
|
||||
{re: regexp.MustCompile(` ?, ?`), replacement: ", "},
|
||||
{re: regexp.MustCompile(` ?([;.:]) ?`), replacement: "$1"},
|
||||
|
||||
// Add newlines and a bit of visual aid
|
||||
{
|
||||
re: regexp.MustCompile(`((UNION|INTERSECT|EXCEPT)( (ALL|DISTINCT))? )?SELECT `),
|
||||
replacement: "\n${1}SELECT ",
|
||||
},
|
||||
{
|
||||
re: regexp.MustCompile(` (FROM|WHERE|GROUP BY|HAVING|WINDOW|ORDER BY|LIMIT|OFFSET) `),
|
||||
replacement: "\n $1 ",
|
||||
},
|
||||
}
|
91
pkg/storage/unified/sql/sqltemplate/sqltemplate_test.go
Normal file
91
pkg/storage/unified/sql/sqltemplate/sqltemplate_test.go
Normal file
@ -0,0 +1,91 @@
|
||||
package sqltemplate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func TestSQLTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
field := reflect.ValueOf(new(struct {
|
||||
X int
|
||||
})).Elem().FieldByName("X")
|
||||
|
||||
tmpl := New(MySQL)
|
||||
tmpl.Arg(1)
|
||||
_, err := tmpl.Into(field, "colname")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tmpl.SetDialect(SQLite)
|
||||
a := tmpl.GetArgs()
|
||||
d := tmpl.GetScanDest()
|
||||
if len(a) != 0 || len(d) != 0 {
|
||||
t.Fatalf("unexpected values after SetDialect(). Args: %v, ScanDest: %v",
|
||||
a, d)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(tmpl)
|
||||
if b != nil || !errors.Is(err, ErrSQLTemplateNoSerialize) {
|
||||
t.Fatalf("should fail serialization with ErrSQLTemplateNoSerialize")
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(`{}`), &tmpl)
|
||||
if !errors.Is(err, ErrSQLTemplateNoSerialize) {
|
||||
t.Fatalf("should fail deserialization with ErrSQLTemplateNoSerialize")
|
||||
}
|
||||
|
||||
err = tmpl.Validate()
|
||||
if !errors.Is(err, ErrValidationNotImplemented) {
|
||||
t.Fatalf("should fail with ErrValidationNotImplemented")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpl := template.Must(template.New("test").Parse(`{{ .ID }}`))
|
||||
|
||||
data := struct {
|
||||
ID int
|
||||
}{
|
||||
ID: 1,
|
||||
}
|
||||
|
||||
txt, err := Execute(tmpl, data)
|
||||
if txt != "1" || err != nil {
|
||||
t.Fatalf("unexpected error, txt: %q, err: %v", txt, err)
|
||||
}
|
||||
|
||||
txt, err = Execute(tmpl, 1)
|
||||
if txt != "" || err == nil {
|
||||
t.Fatalf("unexpected result, txt: %q, err: %v", txt, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSQL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// TODO: improve testing
|
||||
|
||||
const (
|
||||
input = `
|
||||
SELECT *
|
||||
FROM "mytab" AS t
|
||||
WHERE "id">= 3 AND "str" = ? ;
|
||||
`
|
||||
expected = `SELECT *
|
||||
FROM "mytab" AS t
|
||||
WHERE "id" >= 3 AND "str" = ?;`
|
||||
)
|
||||
|
||||
got := FormatSQL(input)
|
||||
if expected != got {
|
||||
t.Fatalf("Unexpected output.\n\tExpected: %s\n\tActual: %s", expected,
|
||||
got)
|
||||
}
|
||||
}
|
3
pkg/storage/unified/sql/testdata/entity_folder_insert_1_mysql_sqlite.sql
vendored
Normal file
3
pkg/storage/unified/sql/testdata/entity_folder_insert_1_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
INSERT INTO "entity_folder"
|
||||
("guid", "namespace", "name", "slug_path", "tree", "depth", "lft", "rgt", "detached")
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
5
pkg/storage/unified/sql/testdata/entity_folder_insert_2_mysql_sqlite.sql
vendored
Normal file
5
pkg/storage/unified/sql/testdata/entity_folder_insert_2_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
INSERT INTO "entity_folder"
|
||||
("guid", "namespace", "name", "slug_path", "tree", "depth", "lft", "rgt", "detached")
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?),
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?);
|
5
pkg/storage/unified/sql/testdata/entity_history_read_full_mysql.sql
vendored
Normal file
5
pkg/storage/unified/sql/testdata/entity_history_read_full_mysql.sql
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
SELECT e."guid", e."resource_version", e."key", e."group", e."group_version", e."resource", e."namespace", e."name", e."folder", e."meta", e."body", e."status", e."size", e."etag", e."created_at", e."created_by", e."updated_at", e."updated_by", e."origin", e."origin_key", e."origin_ts", e."title", e."slug", e."description", e."message", e."labels", e."fields", e."errors", e."action"
|
||||
FROM "entity_history" AS e
|
||||
WHERE 1 = 1 AND "namespace" = ? AND "group" = ? AND "resource" = ? AND "name" = ? AND "resource_version" <= ?
|
||||
ORDER BY "resource_version" DESC
|
||||
LIMIT 1 FOR UPDATE NOWAIT;
|
1
pkg/storage/unified/sql/testdata/entity_labels_delete_1_mysql_sqlite.sql
vendored
Normal file
1
pkg/storage/unified/sql/testdata/entity_labels_delete_1_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1 @@
|
||||
DELETE FROM "entity_labels" WHERE 1 = 1 AND "guid" = ? AND "label" NOT IN (?);
|
1
pkg/storage/unified/sql/testdata/entity_labels_delete_2_mysql_sqlite.sql
vendored
Normal file
1
pkg/storage/unified/sql/testdata/entity_labels_delete_2_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1 @@
|
||||
DELETE FROM "entity_labels" WHERE 1 = 1 AND "guid" = ? AND "label" NOT IN (?, ?);
|
2
pkg/storage/unified/sql/testdata/entity_labels_insert_1_mysql_sqlite.sql
vendored
Normal file
2
pkg/storage/unified/sql/testdata/entity_labels_insert_1_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
INSERT INTO "entity_labels" ("guid", "label", "value")
|
||||
VALUES (?, ?, ?);
|
3
pkg/storage/unified/sql/testdata/entity_labels_insert_2_mysql_sqlite.sql
vendored
Normal file
3
pkg/storage/unified/sql/testdata/entity_labels_insert_2_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
INSERT INTO "entity_labels" ("guid", "label", "value") VALUES
|
||||
(?, ?, ?),
|
||||
(?, ?, ?);
|
1
pkg/storage/unified/sql/testdata/resource_delete_mysql_sqlite.sql
vendored
Normal file
1
pkg/storage/unified/sql/testdata/resource_delete_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1 @@
|
||||
DELETE FROM "resource" WHERE 1 = 1 AND "namespace" = ? AND "group" = ? AND "resource" = ? AND "name" = ?;
|
1
pkg/storage/unified/sql/testdata/resource_delete_postgres.sql
vendored
Normal file
1
pkg/storage/unified/sql/testdata/resource_delete_postgres.sql
vendored
Normal file
@ -0,0 +1 @@
|
||||
DELETE FROM "resource" WHERE 1 = 1 AND "namespace" = $1 AND "group" = $2 AND "resource" = $3 AND "name" = $4;
|
3
pkg/storage/unified/sql/testdata/resource_history_insert_mysql_sqlite.sql
vendored
Normal file
3
pkg/storage/unified/sql/testdata/resource_history_insert_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
INSERT INTO "resource_history"
|
||||
("guid", "group", "resource", "namespace", "name", "value", "action")
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);
|
3
pkg/storage/unified/sql/testdata/resource_insert_mysql_sqlite.sql
vendored
Normal file
3
pkg/storage/unified/sql/testdata/resource_insert_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
INSERT INTO "resource"
|
||||
("guid", "group", "resource", "namespace", "name", "value", "action")
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);
|
3
pkg/storage/unified/sql/testdata/resource_read_mysql_sqlite.sql
vendored
Normal file
3
pkg/storage/unified/sql/testdata/resource_read_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
SELECT "resource_version", "value"
|
||||
FROM "resource"
|
||||
WHERE 1 = 1 AND "namespace" = ? AND "group" = ? AND "resource" = ? AND "name" = ?;
|
2
pkg/storage/unified/sql/testdata/resource_update_mysql_sqlite.sql
vendored
Normal file
2
pkg/storage/unified/sql/testdata/resource_update_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
UPDATE "resource" SET "guid" = ?, "value" = ?, "action" = ?
|
||||
WHERE 1 =1 AND "group" = ? AND "resource" = ? AND "namespace" = ? AND "name" = ? ;
|
Loading…
Reference in New Issue
Block a user