SQL: Add sql template test helper (#91953)
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
reflect "reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
sqltemplate "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func NewTestingSQLTemplate() sqltemplate.SQLTemplateIface {
|
||||
standard := sqltemplate.New(sqltemplate.MySQL) // dialect gets replaced at each iteration
|
||||
return &testingSQLTemplate{standard}
|
||||
}
|
||||
|
||||
type testingSQLTemplate struct {
|
||||
*sqltemplate.SQLTemplate
|
||||
}
|
||||
|
||||
func (t *testingSQLTemplate) Arg(x any) string {
|
||||
_ = t.SQLTemplate.Arg(x) // discard the output
|
||||
|
||||
switch v := reflect.ValueOf(x); {
|
||||
case v.Kind() == reflect.Bool:
|
||||
if v.Bool() {
|
||||
return "TRUE"
|
||||
}
|
||||
return "FALSE"
|
||||
|
||||
case v.CanInt(), v.CanUint(), v.CanFloat():
|
||||
_, ok := x.(fmt.Stringer)
|
||||
if !ok {
|
||||
return fmt.Sprintf("%v", x)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("'%v'", x) // single quotes
|
||||
}
|
||||
|
||||
func (t *testingSQLTemplate) ArgList(slice reflect.Value) (string, error) {
|
||||
// Copied from upstream Arg
|
||||
if !slice.IsValid() || slice.Kind() != reflect.Slice {
|
||||
return "", sqltemplate.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(t.Arg(slice.Index(i).Interface()))
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
type TemplateTestCase struct {
|
||||
Name string
|
||||
|
||||
// Data should be the struct passed to the template.
|
||||
Data sqltemplate.SQLTemplateIface
|
||||
}
|
||||
|
||||
type TemplateTestSetup struct {
|
||||
// Where the snapshots can be found
|
||||
RootDir string
|
||||
|
||||
// The template will be run through each dialect
|
||||
Dialects []sqltemplate.Dialect
|
||||
|
||||
// Check a set of templates against example inputs
|
||||
Templates map[*template.Template][]TemplateTestCase
|
||||
}
|
||||
|
||||
func CheckQuerySnapshots(t *testing.T, setup TemplateTestSetup) {
|
||||
t.Helper()
|
||||
t.Parallel()
|
||||
|
||||
if len(setup.Dialects) < 1 {
|
||||
setup.Dialects = []sqltemplate.Dialect{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
sqltemplate.PostgreSQL,
|
||||
}
|
||||
}
|
||||
|
||||
for tmpl, cases := range setup.Templates {
|
||||
t.Run(tmpl.Name(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tname := strings.TrimSuffix(tmpl.Name(), ".sql")
|
||||
for _, input := range cases {
|
||||
t.Run(input.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
for _, dialect := range setup.Dialects {
|
||||
t.Run(dialect.DialectName(), func(t *testing.T) {
|
||||
// not parallel because we're sharing tc.Data,
|
||||
// but also not worth deep cloning
|
||||
input.Data.SetDialect(dialect)
|
||||
err := input.Data.Validate()
|
||||
|
||||
require.NoError(t, err)
|
||||
got, err := sqltemplate.Execute(tmpl, input.Data)
|
||||
require.NoError(t, err)
|
||||
|
||||
clean := sqltemplate.RemoveEmptyLines(got)
|
||||
|
||||
update := false
|
||||
fname := fmt.Sprintf("%s--%s-%s.sql", dialect.DialectName(), tname, input.Name)
|
||||
fpath := filepath.Join(setup.RootDir, fname)
|
||||
|
||||
// We can ignore the gosec G304 because this is only for tests
|
||||
// nolint:gosec
|
||||
expect, err := os.ReadFile(fpath)
|
||||
if err != nil || len(expect) < 1 {
|
||||
update = true
|
||||
t.Errorf("missing " + fpath)
|
||||
} else {
|
||||
if diff := cmp.Diff(string(expect), clean); diff != "" {
|
||||
t.Errorf("%s: %s", fname, diff)
|
||||
update = true
|
||||
}
|
||||
}
|
||||
if update {
|
||||
_ = os.WriteFile(fpath, []byte(clean), 0777)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,21 +11,25 @@ import (
|
||||
var (
|
||||
ErrValidationNotImplemented = errors.New("validation not implemented")
|
||||
ErrSQLTemplateNoSerialize = errors.New("SQLTemplate should not be serialized")
|
||||
|
||||
// Make sure SQLTemplate implements the interface
|
||||
_ SQLTemplateIface = (*SQLTemplate)(nil)
|
||||
)
|
||||
|
||||
// SQLTemplate provides comprehensive support for SQL templating, handling
|
||||
// dialect traits, execution arguments and scanning arguments.
|
||||
type SQLTemplate struct {
|
||||
Dialect
|
||||
Args
|
||||
ScanDest
|
||||
*Args
|
||||
*ScanDest
|
||||
}
|
||||
|
||||
// New returns a nee *SQLTemplate that will use the given dialect.
|
||||
func New(d Dialect) *SQLTemplate {
|
||||
ret := new(SQLTemplate)
|
||||
ret.ScanDest = new(ScanDest)
|
||||
ret.Args = NewArgs(d)
|
||||
ret.SetDialect(d)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user