diff --git a/README.md b/README.md index 7fbbb7e..ab8d215 100644 --- a/README.md +++ b/README.md @@ -125,4 +125,63 @@ if err != nil { } else { fmt.Printf("%#v", args) // prints: []interface {}{"some name", "unknown", time.Date{...}} } -``` \ No newline at end of file +``` + +### Tokens +Sometimes you may have a common lexicon of table names, columns and/or arg names defined as consts. +These can be used in templates using token replace notation (`{{token}}`) in the template string and transposed by providing a `sqlnt.TokenOption` implementation... +```go +package main + +import ( + "database/sql" + "fmt" + "github.com/go-andiamo/sqlnt" + "time" +) + +func main() { + insertQueue := sqlnt.MustCreateNamedTemplate(InsertStatement, &Lexicon{TableNameQueues}). + DefaultValue(ParamNameCreatedAt, nowFunc).DefaultValue(ParamNameStatus, "unknown") + insertTopic := sqlnt.MustCreateNamedTemplate(InsertStatement, &Lexicon{TableNameTopics}). + DefaultValue(ParamNameCreatedAt, nowFunc).DefaultValue(ParamNameStatus, "unknown") + + statement, args := insertQueue.MustStatementAndArgs(sql.Named(ParamNameName, "foo")) + fmt.Printf("statement: %s\n args: %#v\n", statement, args) + + statement, args = insertTopic.MustStatementAndArgs(sql.Named(ParamNameName, "foo")) + fmt.Printf("statement: %s\n args: %#v\n", statement, args) +} + +const ( + InsertStatement = "INSERT INTO {{table}} ({{baseCols}}) VALUES ({{insertArgs}})" + TableNameQueues = "queues" + TableNameTopics = "topics" + BaseCols = "name,status,created_at" + ParamNameName = "name" + ParamNameStatus = "status" + ParamNameCreatedAt = "createdAt" +) + +var nowFunc = func(name string) any { + return time.Now() +} + +var commonLexiconMap = map[string]string{ + "baseCols": BaseCols, + "insertArgs": ":" + ParamNameName + ",:" + ParamNameStatus + ",:" + ParamNameCreatedAt, +} + +type Lexicon struct { + TableName string +} + +// Replace implements sqlnt.TokenOption.Replace +func (l *Lexicon) Replace(token string) (string, bool) { + if token == "table" { + return l.TableName, true + } + r, ok := commonLexiconMap[token] + return r, ok +} +``` diff --git a/build_args.go b/build_args.go index c0d563f..9ce679e 100644 --- a/build_args.go +++ b/build_args.go @@ -2,18 +2,22 @@ package sqlnt import ( "fmt" + "regexp" "strconv" "strings" ) func (n *namedTemplate) buildArgs() error { + if err := n.replaceTokens(); err != nil { + return err + } var builder strings.Builder n.argsCount = 0 lastPos := 0 runes := []rune(n.originalStatement) rlen := len(runes) purge := func(pos int) { - if lastPos != -1 && pos > lastPos { + if pos > lastPos { builder.WriteString(string(runes[lastPos:pos])) } } @@ -59,8 +63,30 @@ func (n *namedTemplate) buildArgs() error { return nil } +var tokenRegexp = regexp.MustCompile(`\{\{([^}]*)}}`) + +func (n *namedTemplate) replaceTokens() error { + errs := make([]string, 0) + n.originalStatement = tokenRegexp.ReplaceAllStringFunc(n.originalStatement, func(s string) string { + token := s[2 : len(s)-2] + for _, tr := range n.tokenOptions { + if r, ok := tr.Replace(token); ok { + return r + } + } + errs = append(errs, token) + return "" + }) + if len(errs) == 1 { + return fmt.Errorf("unknown token: %s", errs[0]) + } else if len(errs) > 0 { + return fmt.Errorf("unknown tokens: %s", strings.Join(errs, ", ")) + } + return nil +} + func isNameRune(r rune) bool { - return r == '_' || r == '-' || (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') + return r == '_' || r == '-' || r == '.' || (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') } func (n *namedTemplate) addNamedArg(name string, omissible bool) string { diff --git a/doc.go b/doc.go index 8f7b437..ed2fcb5 100644 --- a/doc.go +++ b/doc.go @@ -11,5 +11,14 @@ Example: "b": "b value", } _, _ = db.Exec(tmp.Statement(), tmp.MustArgs(args)...) + +Or directly using template: + var tmp = sqlnt.MustCreateNamedTemplate(`INSERT INTO table (col_a, col_b) VALUES(:a, :b)`, nil) + + args := map[string]any{ + "a": "a value", + "b": "b value", + } + _, _ = tmp.Exec(db, args) */ package sqlnt diff --git a/named_template.go b/named_template.go index 1379c88..f9e8e02 100644 --- a/named_template.go +++ b/named_template.go @@ -90,39 +90,65 @@ type namedTemplate struct { argsCount int usePositionalTags bool argTag string + tokenOptions []TokenOption } // NewNamedTemplate creates a new NamedTemplate // -// Returns an error if the supplied template cannot be parsed for arg names -func NewNamedTemplate(statement string, option Option) (NamedTemplate, error) { - if option == nil { - option = DefaultsOption +// # Returns an error if the supplied template cannot be parsed for arg names +// +// Multiple options can be specified - each must be either a sqlnt.Option or sqlnt.TokenOption +func NewNamedTemplate(statement string, options ...any) (NamedTemplate, error) { + opt, tokenOptions, err := getOptions(options...) + if err != nil { + return nil, err } - result := newNamedTemplate(statement, option.UsePositionalTags(), option.ArgTag()) - if err := result.buildArgs(); err != nil { + result := newNamedTemplate(statement, opt.UsePositionalTags(), opt.ArgTag(), tokenOptions) + if err = result.buildArgs(); err != nil { return nil, err } return result, nil } +func getOptions(options ...any) (Option, []TokenOption, error) { + opt := DefaultsOption + tokenOptions := make([]TokenOption, 0) + for _, o := range options { + if o != nil { + o1, ok1 := o.(Option) + o2, ok2 := o.(TokenOption) + if !ok1 && !ok2 { + return nil, nil, errors.New("invalid option") + } + if ok1 { + opt = o1 + } + if ok2 { + tokenOptions = append(tokenOptions, o2) + } + } + } + return opt, tokenOptions, nil +} + // MustCreateNamedTemplate creates a new NamedTemplate // // is the same as NewNamedTemplate, except panics in case of error -func MustCreateNamedTemplate(statement string, option Option) NamedTemplate { - nt, err := NewNamedTemplate(statement, option) +func MustCreateNamedTemplate(statement string, options ...any) NamedTemplate { + nt, err := NewNamedTemplate(statement, options...) if err != nil { panic(err) } return nt } -func newNamedTemplate(statement string, usePositionalTags bool, argTag string) *namedTemplate { +func newNamedTemplate(statement string, usePositionalTags bool, argTag string, tokenOptions []TokenOption) *namedTemplate { return &namedTemplate{ originalStatement: statement, args: map[string]*namedArg{}, usePositionalTags: usePositionalTags, argTag: argTag, + tokenOptions: tokenOptions, } } @@ -323,7 +349,7 @@ func (n *namedTemplate) Clone(option Option) NamedTemplate { // no material change, just copy everything... return n.copy() } else { - r := newNamedTemplate(n.originalStatement, option.UsePositionalTags(), option.ArgTag()) + r := newNamedTemplate(n.originalStatement, option.UsePositionalTags(), option.ArgTag(), n.tokenOptions) _ = r.buildArgs() for name, arg := range n.args { if rarg, ok := r.args[name]; ok { @@ -339,7 +365,7 @@ func (n *namedTemplate) Clone(option Option) NamedTemplate { // // Returns an error if the supplied statement portion cannot be parsed for arg names func (n *namedTemplate) Append(portion string) (NamedTemplate, error) { - result := newNamedTemplate(n.originalStatement+portion, n.usePositionalTags, n.argTag) + result := newNamedTemplate(n.originalStatement+portion, n.usePositionalTags, n.argTag, n.tokenOptions) if err := result.buildArgs(); err != nil { return nil, err } @@ -362,7 +388,7 @@ func (n *namedTemplate) MustAppend(portion string) NamedTemplate { } func (n *namedTemplate) copy() *namedTemplate { - r := newNamedTemplate(n.originalStatement, n.usePositionalTags, n.argTag) + r := newNamedTemplate(n.originalStatement, n.usePositionalTags, n.argTag, n.tokenOptions) r.statement = n.statement r.argsCount = n.argsCount r.usePositionalTags = n.usePositionalTags diff --git a/named_template_test.go b/named_template_test.go index 2e34bab..c026163 100644 --- a/named_template_test.go +++ b/named_template_test.go @@ -16,11 +16,13 @@ func TestNamedTemplate(t *testing.T) { testCases := []struct { statement string expectError bool + expectErrorMessage string expectStatement string + expectOriginal string expectArgsCount int expectArgNamesCount int expectArgNames []string - option Option + options []any omissibleArgs []string inArgs []any expectArgsError bool @@ -44,7 +46,7 @@ func TestNamedTemplate(t *testing.T) { { statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:a, :bb, :ccc)`, expectStatement: `INSERT INTO table (col_a, col_b, col_c) VALUES(?, ?, ?)`, - option: MySqlOption, + options: []any{MySqlOption}, expectArgsCount: 3, expectArgNamesCount: 3, expectArgNames: []string{"a", "bb", "ccc"}, @@ -60,7 +62,7 @@ func TestNamedTemplate(t *testing.T) { { statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:a, :bb, :ccc)`, expectStatement: `INSERT INTO table (col_a, col_b, col_c) VALUES(?, ?, ?)`, - option: MySqlOption, + options: []any{MySqlOption}, expectArgsCount: 3, expectArgNamesCount: 3, expectArgNames: []string{"a", "bb", "ccc"}, @@ -82,7 +84,7 @@ func TestNamedTemplate(t *testing.T) { { statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:a, :bb, :ccc)`, expectStatement: `INSERT INTO table (col_a, col_b, col_c) VALUES(?, ?, ?)`, - option: MySqlOption, + options: []any{MySqlOption}, expectArgsCount: 3, expectArgNamesCount: 3, expectArgNames: []string{"a", "bb", "ccc"}, @@ -102,7 +104,7 @@ func TestNamedTemplate(t *testing.T) { { statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:a, :bb, :ccc)`, expectStatement: `INSERT INTO table (col_a, col_b, col_c) VALUES(?, ?, ?)`, - option: MySqlOption, + options: []any{MySqlOption}, expectArgsCount: 3, expectArgNamesCount: 3, expectArgNames: []string{"a", "bb", "ccc"}, @@ -118,7 +120,7 @@ func TestNamedTemplate(t *testing.T) { { statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:a, :bb, :ccc)`, expectStatement: `INSERT INTO table (col_a, col_b, col_c) VALUES(?, ?, ?)`, - option: MySqlOption, + options: []any{MySqlOption}, expectArgsCount: 3, expectArgNamesCount: 3, expectArgNames: []string{"a", "bb", "ccc"}, @@ -134,7 +136,7 @@ func TestNamedTemplate(t *testing.T) { { statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:a, :bb, :ccc)`, expectStatement: `INSERT INTO table (col_a, col_b, col_c) VALUES(?, ?, ?)`, - option: MySqlOption, + options: []any{MySqlOption}, expectArgsCount: 3, expectArgNamesCount: 3, expectArgNames: []string{"a", "bb", "ccc"}, @@ -149,7 +151,7 @@ func TestNamedTemplate(t *testing.T) { { statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:a, :bb, :ccc)`, expectStatement: `INSERT INTO table (col_a, col_b, col_c) VALUES(?, ?, ?)`, - option: MySqlOption, + options: []any{MySqlOption}, expectArgsCount: 3, expectArgNamesCount: 3, expectArgNames: []string{"a", "bb", "ccc"}, @@ -159,7 +161,7 @@ func TestNamedTemplate(t *testing.T) { { statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:a, :bb, :ccc)`, expectStatement: `INSERT INTO table (col_a, col_b, col_c) VALUES(?, ?, ?)`, - option: MySqlOption, + options: []any{MySqlOption}, expectArgsCount: 3, expectArgNamesCount: 3, expectArgNames: []string{"a", "bb", "ccc"}, @@ -169,7 +171,7 @@ func TestNamedTemplate(t *testing.T) { { statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:a, :bb, :ccc)`, expectStatement: `INSERT INTO table (col_a, col_b, col_c) VALUES($1, $2, $3)`, - option: PostgresOption, + options: []any{PostgresOption}, expectArgsCount: 3, expectArgNamesCount: 3, expectArgNames: []string{"a", "bb", "ccc"}, @@ -212,7 +214,7 @@ func TestNamedTemplate(t *testing.T) { { statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:a, :b, :a)`, expectStatement: `INSERT INTO table (col_a, col_b, col_c) VALUES($1, $2, $1)`, - option: PostgresOption, + options: []any{PostgresOption}, expectArgsCount: 2, expectArgNamesCount: 2, expectArgNames: []string{"a", "b"}, @@ -227,7 +229,7 @@ func TestNamedTemplate(t *testing.T) { { statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:a, :b, :a)`, expectStatement: `INSERT INTO table (col_a, col_b, col_c) VALUES($1, $2, $1)`, - option: PostgresOption, + options: []any{PostgresOption}, expectArgsCount: 2, expectArgNamesCount: 2, expectArgNames: []string{"a", "b"}, @@ -241,7 +243,7 @@ func TestNamedTemplate(t *testing.T) { { statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:a, :b, :a)`, expectStatement: `INSERT INTO table (col_a, col_b, col_c) VALUES($1, $2, $1)`, - option: PostgresOption, + options: []any{PostgresOption}, expectArgsCount: 2, expectArgNamesCount: 2, expectArgNames: []string{"a", "b"}, @@ -257,7 +259,7 @@ func TestNamedTemplate(t *testing.T) { { statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:a, :b, :a)`, expectStatement: `INSERT INTO table (col_a, col_b, col_c) VALUES($1, $2, $1)`, - option: PostgresOption, + options: []any{PostgresOption}, expectArgsCount: 2, expectArgNamesCount: 2, expectArgNames: []string{"a", "b"}, @@ -282,7 +284,7 @@ func TestNamedTemplate(t *testing.T) { { statement: `UPDATE table SET col_a = :a`, expectStatement: `UPDATE table SET col_a = $1`, - option: PostgresOption, + options: []any{PostgresOption}, expectArgsCount: 1, expectArgNamesCount: 1, expectArgNames: []string{"a"}, @@ -305,7 +307,7 @@ func TestNamedTemplate(t *testing.T) { { statement: `UPDATE table SET col_a = :a?`, expectStatement: `UPDATE table SET col_a = $1`, - option: PostgresOption, + options: []any{PostgresOption}, expectArgsCount: 1, expectArgNamesCount: 1, expectArgNames: []string{"a"}, @@ -313,9 +315,10 @@ func TestNamedTemplate(t *testing.T) { expectOutArgs: []any{nil}, }, { - statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:, :b, :c)`, - option: PostgresOption, - expectError: true, + statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:, :b, :c)`, + options: []any{PostgresOption}, + expectError: true, + expectErrorMessage: "named marker ':' without name (at position 47)", }, { statement: `INSERT INTO table (col_a, col_b, col_c) VALUES(:a, '::bb', '::ccc')`, @@ -343,22 +346,71 @@ func TestNamedTemplate(t *testing.T) { expectArgsCount: 0, expectArgNamesCount: 0, }, + { + statement: `UPDATE table SET col_a = :a`, + options: []any{true}, // not a valid option + expectError: true, + expectErrorMessage: "invalid option", + }, + { + statement: `UPDATE table SET col_a = :a`, + expectStatement: `UPDATE table SET col_a = ?`, + options: []any{nil}, + expectArgsCount: 1, + expectArgNamesCount: 1, + expectArgNames: []string{"a"}, + expectError: false, + }, + { + statement: `INSERT INTO {{tableName}} ({{cols}}) VALUES(:{{argA}},:{{argB}},:{{argC}})`, + expectStatement: `INSERT INTO foo (col_a,col_b,col_c) VALUES($1,$2,$3)`, + expectOriginal: `INSERT INTO foo (col_a,col_b,col_c) VALUES(:a,:b,:c)`, + options: []any{PostgresOption, &testTokenOption{}}, + expectArgsCount: 3, + expectArgNamesCount: 3, + expectArgNames: []string{"a", "b", "c"}, + inArgs: []any{ + map[string]any{ + "a": "a value", + "b": "b value", + "c": "c value", + }, + }, + expectOutArgs: []any{"a value", "b value", "c value"}, + }, + { + statement: `INSERT INTO {{unknownToken}} ({{cols}}) VALUES({{argA}},{{argB}},{{argC}})`, + options: []any{PostgresOption, &testTokenOption{}}, + expectError: true, + expectErrorMessage: "unknown token: unknownToken", + }, + { + statement: `INSERT INTO {{unknown token}} ({{another unknown}}) VALUES({{argA}},{{argB}},{{argC}})`, + options: []any{PostgresOption, &testTokenOption{}}, + expectError: true, + expectErrorMessage: "unknown tokens: unknown token, another unknown", + }, } for i, tc := range testCases { t.Run(fmt.Sprintf("[%d]%s", i+1, tc.statement), func(t *testing.T) { - nt, err := NewNamedTemplate(tc.statement, tc.option) + nt, err := NewNamedTemplate(tc.statement, tc.options...) if tc.expectError { assert.Nil(t, nt) assert.Error(t, err) + assert.Equal(t, tc.expectErrorMessage, err.Error()) assert.Panics(t, func() { - _ = MustCreateNamedTemplate(tc.statement, tc.option) + _ = MustCreateNamedTemplate(tc.statement, tc.options...) }) } else { assert.NoError(t, err) assert.NotPanics(t, func() { - _ = MustCreateNamedTemplate(tc.statement, tc.option) + _ = MustCreateNamedTemplate(tc.statement, tc.options...) }) - assert.Equal(t, tc.statement, nt.OriginalStatement()) + if tc.expectOriginal == "" { + assert.Equal(t, tc.statement, nt.OriginalStatement()) + } else { + assert.Equal(t, tc.expectOriginal, nt.OriginalStatement()) + } assert.Equal(t, tc.expectStatement, nt.Statement()) assert.Equal(t, tc.expectArgsCount, nt.ArgsCount()) args := nt.GetArgNames() @@ -402,6 +454,19 @@ func TestNamedTemplate(t *testing.T) { } } +type testTokenOption struct{} + +func (t *testTokenOption) Replace(token string) (string, bool) { + ok, r := (map[string]string{ + "tableName": "foo", + "cols": "col_a,col_b,col_c", + "argA": "a", + "argB": "b", + "argC": "c", + })[token] + return ok, r +} + type unmarshalable struct{} func (u *unmarshalable) MarshalJSON() ([]byte, error) { @@ -409,7 +474,7 @@ func (u *unmarshalable) MarshalJSON() ([]byte, error) { } func TestNamedTemplate_OmissibleInTemplate(t *testing.T) { - nt, err := NewNamedTemplate(`INSERT INTO table (col_a, col_b, col_c) VALUES (:a, :b, :a?)`, nil) + nt, err := NewNamedTemplate(`INSERT INTO table (col_a, col_b, col_c) VALUES (:a, :b, :a?)`) assert.NoError(t, err) assert.Equal(t, `INSERT INTO table (col_a, col_b, col_c) VALUES (?, ?, ?)`, nt.Statement()) args := nt.GetArgNames() @@ -417,20 +482,20 @@ func TestNamedTemplate_OmissibleInTemplate(t *testing.T) { assert.False(t, args["b"]) // can only be set omissible once... - nt, err = NewNamedTemplate(`INSERT INTO table (col_a, col_b, col_c) VALUES (:a?, :b, :a)`, nil) + nt, err = NewNamedTemplate(`INSERT INTO table (col_a, col_b, col_c) VALUES (:a?, :b, :a)`) assert.NoError(t, err) assert.Equal(t, `INSERT INTO table (col_a, col_b, col_c) VALUES (?, ?, ?)`, nt.Statement()) args = nt.GetArgNames() assert.True(t, args["a"]) assert.False(t, args["b"]) - nt, _ = NewNamedTemplate(`SELECT * FROM table WHERE col_a = :a?`, nil) + nt, _ = NewNamedTemplate(`SELECT * FROM table WHERE col_a = :a?`) assert.Equal(t, `SELECT * FROM table WHERE col_a = ?`, nt.Statement()) } func TestNamedTemplate_DefaultValue(t *testing.T) { now := time.Now() - nt := MustCreateNamedTemplate(`INSERT INTO table (col_a, created_at) VALUES (:a, :crat)`, nil). + nt := MustCreateNamedTemplate(`INSERT INTO table (col_a, created_at) VALUES (:a, :crat)`). DefaultValue("crat", now) assert.Equal(t, `INSERT INTO table (col_a, created_at) VALUES (?, ?)`, nt.Statement()) args, err := nt.Args(map[string]any{ @@ -455,7 +520,7 @@ func TestNamedTemplate_DefaultValue(t *testing.T) { } func TestNamedTemplate_Clone(t *testing.T) { - nt := MustCreateNamedTemplate(`INSERT INTO table (col_a, col_b, col_c) VALUES (:a, :b, :a)`, nil). + nt := MustCreateNamedTemplate(`INSERT INTO table (col_a, col_b, col_c) VALUES (:a, :b, :a)`). DefaultValue("a", "a default"). OmissibleArgs("b") assert.Equal(t, `INSERT INTO table (col_a, col_b, col_c) VALUES (?, ?, ?)`, nt.Statement()) @@ -471,7 +536,7 @@ func TestNamedTemplate_Clone(t *testing.T) { } func TestNamedTemplate_Append(t *testing.T) { - nt := MustCreateNamedTemplate(`SELECT * FROM table WHERE col_a = :a`, nil). + nt := MustCreateNamedTemplate(`SELECT * FROM table WHERE col_a = :a`). OmissibleArgs("a") assert.Equal(t, `SELECT * FROM table WHERE col_a = ?`, nt.Statement()) @@ -498,7 +563,7 @@ func TestNamedTemplate_Append(t *testing.T) { } func TestNamedTemplate_Exec(t *testing.T) { - nt, err := NewNamedTemplate(`INSERT INTO table (col_a, col_b, col_c) VALUES (:a, :b, :a)`, nil) + nt, err := NewNamedTemplate(`INSERT INTO table (col_a, col_b, col_c) VALUES (:a, :b, :a)`) require.NoError(t, err) db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) require.NoError(t, err) @@ -520,7 +585,7 @@ func TestNamedTemplate_Exec(t *testing.T) { } func TestNamedTemplate_ExecContext(t *testing.T) { - nt, err := NewNamedTemplate(`INSERT INTO table (col_a, col_b, col_c) VALUES (:a, :b, :a)`, nil) + nt, err := NewNamedTemplate(`INSERT INTO table (col_a, col_b, col_c) VALUES (:a, :b, :a)`) require.NoError(t, err) db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) require.NoError(t, err) @@ -542,7 +607,7 @@ func TestNamedTemplate_ExecContext(t *testing.T) { } func TestNamedTemplate_Query(t *testing.T) { - nt, err := NewNamedTemplate(`SELECT * FROM table WHERE col_a = :a OR col_a = :b AND col_c = :a`, nil) + nt, err := NewNamedTemplate(`SELECT * FROM table WHERE col_a = :a OR col_a = :b AND col_c = :a`) require.NoError(t, err) db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) require.NoError(t, err) @@ -564,7 +629,7 @@ func TestNamedTemplate_Query(t *testing.T) { } func TestNamedTemplate_QueryContext(t *testing.T) { - nt, err := NewNamedTemplate(`SELECT * FROM table WHERE col_a = :a OR col_a = :b AND col_c = :a`, nil) + nt, err := NewNamedTemplate(`SELECT * FROM table WHERE col_a = :a OR col_a = :b AND col_c = :a`) require.NoError(t, err) db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) require.NoError(t, err) diff --git a/options.go b/options.go index 38d1485..fce1d96 100644 --- a/options.go +++ b/options.go @@ -18,10 +18,20 @@ type Option interface { ArgTag() string } +// TokenOption is an interface that can be provided to NewNamedTemplate or MustCreateNamedTemplate +// to replace tokens in the statement (tokens are denoted by `{{token}}`) +// +// If tokens are found but none of the provided TokenOption implementations provides a replacement +// then NewNamedTemplate will error +type TokenOption interface { + // Replace receives the token and returns the replacement and a bool indicating whether to use the replacement + Replace(token string) (string, bool) +} + var ( - MySqlOption Option = _MySqlOption - PostgresOption Option = _PostgresOption - DefaultsOption Option = _DefaultsOption + MySqlOption Option = _MySqlOption // option to produce final args like ?, ?, ? (e.g. for https://github.com/go-sql-driver/mysql) + PostgresOption Option = _PostgresOption // option to produce final args like $1, $2, $3 (e.g. for https://github.com/lib/pq or https://github.com/jackc/pgx) + DefaultsOption Option = _DefaultsOption // option to produce final args determined by DefaultUsePositionalTags and DefaultArgTag ) var (