diff --git a/README.md b/README.md index b1d26eb..13d1fdc 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Latest Version](https://img.shields.io/github/v/tag/go-andiamo/sqlnt.svg?sort=semver&style=flat&label=version&color=blue)](https://github.com/go-andiamo/sqlnt/releases) [![Go Report Card](https://goreportcard.com/badge/github.com/go-andiamo/sqlnt)](https://goreportcard.com/report/github.com/go-andiamo/sqlnt) +## Overview Go package for named SQL templates... tired of battling `driver does not support the use of Named Parameters` or wish you could reliably use named parameters instead of incomprehensible `(?, ?, ?, ?, ?, ?)` @@ -53,4 +54,72 @@ func insertExample(db *sql.DB, aVal string, bVal string, cVal string) error { sql.NamedArg{Name: "c", Value: cVal}) return err } +``` + +## Installation +To install Sqlnt, use go get: + + go get github.com/go-andiamo/sqlnt + +To update Sqlnt to the latest version, run: + + go get -u github.com/go-andiamo/sqlnt + +## Enhanced Features + +### Omissible args +By default, named templates check that all named args have been supplied... +```go +template := sqlnt.MustCreateNamedTemplate(`INSERT INTO table (col_a,col_b) VALUES (:a, :b)`, nil) +_, err := template.Args(map[string]any{"a": "a value"}) +if err != nil { + panic(err) // will panic because named arg "b" is missing +} +``` +However, named args can be set as omissible... +```go +template := sqlnt.MustCreateNamedTemplate(`INSERT INTO table (col_a,col_b) VALUES (:a, :b)`, nil) +template.OmissibleArgs("b") +args, err := template.Args(map[string]any{"a": "a value"}) +if err != nil { + panic(err) // will not panic here because named arg "b" is missing but omissible +} else { + fmt.Printf("%#v", args) // prints: []interface {}{"a value", interface {}(nil)} +} +``` +Named args can also be set as omissible in the original template by suffixing the name with `?`... +```go +template := sqlnt.MustCreateNamedTemplate(`INSERT INTO table (col_a,col_b) VALUES (:a, :b?)`, nil) +args, err := template.Args(map[string]any{"a": "a value"}) +if err != nil { + panic(err) // will not panic here because named arg "b" is missing but omissible +} else { + fmt.Printf("%#v", args) // prints: []interface {}{"a value", interface {}(nil)} +} +``` +### Default values +Named templates also provides for default - where if a named arg is not supplied a default value is used... +```go +template := sqlnt.MustCreateNamedTemplate(`INSERT INTO table (name,status) VALUES (:name, :status)`, nil) +template.DefaultValue("status", "unknown") +args, err := template.Args(map[string]any{"name": "some name"}) +if err != nil { + panic(err) // will not panic here because named arg "status" is missing but defaulted +} else { + fmt.Printf("%#v", args) // prints: []interface {}{"some name", "unknown"} +} +``` +Default values can also be supplied as a function... +```go +template := sqlnt.MustCreateNamedTemplate(`INSERT INTO table (name,status,created_at) VALUES (:name, :status, :createdAt)`, nil) +template.DefaultValue("status", "unknown") +template.DefaultValue("createdAt", func(name string) any { + return time.Now() +}) +args, err := template.Args(map[string]any{"name": "some name"}) +if err != nil { + panic(err) // will not panic here because named args "status" and "createdAt" are missing but defaulted +} else { + fmt.Printf("%#v", args) // prints: []interface {}{"some name", "unknown", time.Date{...}} +} ``` \ No newline at end of file diff --git a/named_template.go b/named_template.go index b1e2836..5c0d44b 100644 --- a/named_template.go +++ b/named_template.go @@ -17,6 +17,13 @@ import ( type NamedTemplate interface { // Statement returns the sql statement to use (with named args transposed) Statement() string + // StatementAndArgs returns the sql statement to use (with named args transposed) and + // the input named args converted to positional args + // + // Essentially the same as calling Statement and then Args + StatementAndArgs(args ...any) (string, []any, error) + // MustStatementAndArgs is the same as StatementAndArgs, except no error is returned (and panics on error) + MustStatementAndArgs(args ...any) (string, []any) // OriginalStatement returns the original named template statement OriginalStatement() string // Args converts the input named args to positional args (for use in db.Exec, db.Query etc.) @@ -42,7 +49,11 @@ type NamedTemplate interface { ArgsCount() int // OmissibleArgs specifies the names of args that can be omitted // - // Calling this without any names makes are args omissible + // Calling this without any names makes all args omissible + // + // Note: Named args can also be set as omissible in the template - example: + // tmp := sqlnt.MustCreateNamedTemplate(`INSERT INTO table (col_a,col_b) VALUES (:a, :b?)`) + // makes the named arg "b" omissible (denoted by the '?' after name) OmissibleArgs(names ...string) NamedTemplate // DefaultValue specifies a value to be used for a given arg name when the arg // is not supplied in the map for Args or MustArgs @@ -71,14 +82,33 @@ type NamedTemplate interface { type namedTemplate struct { originalStatement string statement string - argPositions map[string][]int + args map[string]*namedArg argsCount int - omissibleArgs map[string]bool - defaultValues map[string]DefaultValueFunc usePositionalTags bool argTag string } +type namedArg struct { + positions []int + omissible bool + defValue DefaultValueFunc +} + +func (a *namedArg) clone() *namedArg { + return &namedArg{ + positions: a.positions, + omissible: a.omissible, + defValue: a.defValue, + } +} + +func (a *namedArg) setOmissible(omissible bool) { + if !a.omissible { + // can only be set when not yet omissible + a.omissible = omissible + } +} + // NewNamedTemplate creates a new NamedTemplate // // Returns an error if the supplied template cannot be parsed for arg names @@ -107,9 +137,7 @@ func MustCreateNamedTemplate(statement string, option Option) NamedTemplate { func newNamedTemplate(statement string, usePositionalTags bool, argTag string) *namedTemplate { return &namedTemplate{ originalStatement: statement, - argPositions: map[string][]int{}, - omissibleArgs: map[string]bool{}, - defaultValues: map[string]DefaultValueFunc{}, + args: map[string]*namedArg{}, usePositionalTags: usePositionalTags, argTag: argTag, } @@ -120,24 +148,30 @@ func (n *namedTemplate) buildArgs() error { n.argsCount = 0 lastPos := 0 runes := []rune(n.originalStatement) + l := len(runes) purge := func(pos int) { if lastPos != -1 && pos > lastPos { builder.WriteString(string(runes[lastPos:pos])) } } - getNamed := func(pos int) (string, int, error) { + getNamed := func(pos int) (string, int, bool, error) { i := pos + 1 skip := 0 - for ; i < len(runes); i++ { + for ; i < l; i++ { if !isNameRune(runes[i]) { break } skip++ } if skip == 0 { - return "", 0, fmt.Errorf("named marker ':' without name (at position %d)", pos) + return "", 0, false, fmt.Errorf("named marker ':' without name (at position %d)", pos) + } + omissible := false + if i+1 < l && runes[i] == '?' { + omissible = true + skip++ } - return string(runes[pos+1 : i]), skip, nil + return string(runes[pos+1 : i]), skip, omissible, nil } for pos := 0; pos < len(runes); pos++ { if runes[pos] == ':' { @@ -147,13 +181,13 @@ func (n *namedTemplate) buildArgs() error { pos++ lastPos = pos } else { - name, skip, err := getNamed(pos) + name, skip, omissible, err := getNamed(pos) if err != nil { return err } pos += skip lastPos = pos + 1 - builder.WriteString(n.addNamedArg(name)) + builder.WriteString(n.addNamedArg(name, omissible)) } } } @@ -166,27 +200,65 @@ func isNameRune(r rune) bool { return r == '_' || r == '-' || (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') } -func (n *namedTemplate) addNamedArg(name string) string { +func (n *namedTemplate) addNamedArg(name string, omissible bool) string { if n.usePositionalTags { - if posns, ok := n.argPositions[name]; ok { - return n.argTag + strconv.Itoa(posns[0]+1) - } else { - n.argPositions[name] = []int{n.argsCount} - n.argsCount++ - return n.argTag + strconv.Itoa(n.argsCount) - } + return n.addNamedArgPositional(name, omissible) + } else { + return n.addNamedArgNonPositional(name, omissible) + } +} + +func (n *namedTemplate) addNamedArgPositional(name string, omissible bool) string { + if arg, ok := n.args[name]; ok { + arg.setOmissible(omissible) + return n.argTag + strconv.Itoa(arg.positions[0]+1) } else { - n.argPositions[name] = append(n.argPositions[name], n.argsCount) + n.args[name] = &namedArg{ + positions: []int{n.argsCount}, + omissible: omissible, + } n.argsCount++ - return n.argTag + return n.argTag + strconv.Itoa(n.argsCount) } } +func (n *namedTemplate) addNamedArgNonPositional(name string, omissible bool) string { + if arg, ok := n.args[name]; ok { + arg.setOmissible(omissible) + arg.positions = append(arg.positions, n.argsCount) + } else { + n.args[name] = &namedArg{ + positions: []int{n.argsCount}, + omissible: omissible, + } + } + n.argsCount++ + return n.argTag +} + // Statement returns the sql statement to use (with named args transposed) func (n *namedTemplate) Statement() string { return n.statement } +// StatementAndArgs returns the sql statement to use (with named args transposed) and +// the input named args converted to positional args +// +// Essentially the same as calling Statement and then Args +func (n *namedTemplate) StatementAndArgs(args ...any) (string, []any, error) { + rargs, err := n.Args(args...) + return n.statement, rargs, err +} + +// MustStatementAndArgs is the same as StatementAndArgs, except no error is returned (and panics on error) +func (n *namedTemplate) MustStatementAndArgs(args ...any) (string, []any) { + rargs, err := n.Args(args...) + if err != nil { + panic(err) + } + return n.statement, rargs +} + // OriginalStatement returns the original named template statement func (n *namedTemplate) OriginalStatement() string { return n.originalStatement @@ -210,20 +282,20 @@ func (n *namedTemplate) OriginalStatement() string { // have been set with a default value (see NamedTemplate.DefaultValue) func (n *namedTemplate) Args(args ...any) ([]any, error) { out := make([]any, n.argsCount) - in, err := inArgs(args...) + mapped, err := mappedArgs(args...) if err != nil { return nil, err } - for name, posns := range n.argPositions { - if v, ok := in[name]; ok { - for _, posn := range posns { + for name, arg := range n.args { + if v, ok := mapped[name]; ok { + for _, posn := range arg.positions { out[posn] = v } - } else if !n.omissibleArgs[name] { + } else if !arg.omissible { return nil, fmt.Errorf("named arg '%s' missing", name) - } else if dvf, ok := n.defaultValues[name]; ok { - v = dvf(name) - for _, posn := range posns { + } else if arg.defValue != nil { + v = arg.defValue(name) + for _, posn := range arg.positions { out[posn] = v } } @@ -231,7 +303,7 @@ func (n *namedTemplate) Args(args ...any) ([]any, error) { return out, nil } -func inArgs(args ...any) (map[string]any, error) { +func mappedArgs(args ...any) (map[string]any, error) { result := map[string]any{} for _, arg := range args { if arg != nil { @@ -292,15 +364,23 @@ func (n *namedTemplate) ArgsCount() int { // OmissibleArgs specifies the names of args that can be omitted // -// Calling this without any names makes are args omissible +// # Calling this without any names makes all args omissible +// +// Note: Named args can also be set as omissible in the template - example: +// +// tmp := sqlnt.MustCreateNamedTemplate(`INSERT INTO table (col_a,col_b) VALUES (:a, :b?)`) +// +// makes the named arg "b" omissible (denoted by the '?' after name) func (n *namedTemplate) OmissibleArgs(names ...string) NamedTemplate { if len(names) == 0 { - for name := range n.argPositions { - n.omissibleArgs[name] = true + for _, arg := range n.args { + arg.omissible = true } } else { for _, name := range names { - n.omissibleArgs[name] = true + if arg, ok := n.args[name]; ok { + arg.omissible = true + } } } return n @@ -321,12 +401,14 @@ type DefaultValueFunc func(name string) any // // then that func is called to obtain the default value func (n *namedTemplate) DefaultValue(name string, v interface{}) NamedTemplate { - n.omissibleArgs[name] = true - if dvf, ok := v.(func(name string) any); ok { - n.defaultValues[name] = dvf - } else { - n.defaultValues[name] = func(name string) any { - return v + if arg, ok := n.args[name]; ok { + arg.omissible = true + if dvf, ok := v.(func(name string) any); ok { + arg.defValue = dvf + } else { + arg.defValue = func(name string) any { + return v + } } } return n @@ -335,9 +417,9 @@ func (n *namedTemplate) DefaultValue(name string, v interface{}) NamedTemplate { // GetArgNames returns a map of the arg names (where the map value is a bool indicating whether // the arg is omissible func (n *namedTemplate) GetArgNames() map[string]bool { - result := make(map[string]bool, len(n.argPositions)) - for name := range n.argPositions { - result[name] = n.omissibleArgs[name] + result := make(map[string]bool, len(n.args)) + for name, arg := range n.args { + result[name] = arg.omissible } return result } @@ -353,7 +435,12 @@ func (n *namedTemplate) Clone(option Option) NamedTemplate { } else { r := newNamedTemplate(n.originalStatement, option.UsePositionalTags(), option.ArgTag()) _ = r.buildArgs() - n.copyAdditionsTo(r) + for name, arg := range n.args { + if rarg, ok := r.args[name]; ok { + rarg.omissible = arg.omissible + rarg.defValue = arg.defValue + } + } return r } } @@ -364,22 +451,12 @@ func (n *namedTemplate) copy() *namedTemplate { r.argsCount = n.argsCount r.usePositionalTags = n.usePositionalTags r.argTag = n.argTag - for name, v := range n.argPositions { - r.argPositions[name] = v + for name, arg := range n.args { + r.args[name] = arg.clone() } - n.copyAdditionsTo(r) return r } -func (n *namedTemplate) copyAdditionsTo(r *namedTemplate) { - for name, v := range n.omissibleArgs { - r.omissibleArgs[name] = v - } - for name, v := range n.defaultValues { - r.defaultValues[name] = v - } -} - // Exec performs sql.DB.Exec on the supplied db with the supplied named args func (n *namedTemplate) Exec(db *sql.DB, args ...any) (sql.Result, error) { if qargs, err := n.Args(args...); err == nil { diff --git a/named_template_test.go b/named_template_test.go index b4f5dc4..8e7a1bd 100644 --- a/named_template_test.go +++ b/named_template_test.go @@ -321,12 +321,22 @@ func TestNamedTemplate(t *testing.T) { assert.Panics(t, func() { _ = nt.MustArgs(tc.inArgs...) }) + assert.Panics(t, func() { + _, _ = nt.MustStatementAndArgs(tc.inArgs...) + }) } else { assert.NoError(t, err) assert.Equal(t, tc.expectOutArgs, outArgs) assert.NotPanics(t, func() { _ = nt.MustArgs(tc.inArgs...) }) + stmt, outArgs, err := nt.StatementAndArgs(tc.inArgs...) + assert.NoError(t, err) + assert.Equal(t, tc.expectOutArgs, outArgs) + assert.Equal(t, tc.expectStatement, stmt) + assert.NotPanics(t, func() { + _, _ = nt.MustStatementAndArgs(tc.inArgs...) + }) } }) } @@ -341,6 +351,23 @@ func (u *unmarshalable) MarshalJSON() ([]byte, error) { return nil, errors.New("fooey") } +func TestNamedTemplate_OmissibleInTemplate(t *testing.T) { + nt, err := NewNamedTemplate(`INSERT INTO table (col_a, col_b, col_c) VALUES (:a, :b, :a?)`, nil) + 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"]) + + // can only be set omissible once... + nt, err = NewNamedTemplate(`INSERT INTO table (col_a, col_b, col_c) VALUES (:a?, :b, :a)`, nil) + 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"]) +} + func TestNamedTemplate_DefaultValue(t *testing.T) { now := time.Now() nt := MustCreateNamedTemplate(`INSERT INTO table (col_a, created_at) VALUES (:a, :crat)`, nil). @@ -354,7 +381,7 @@ func TestNamedTemplate_DefaultValue(t *testing.T) { assert.Equal(t, "a value", args[0]) assert.Equal(t, now, args[1]) - time.Sleep(time.Second) + time.Sleep(50 * time.Millisecond) // wait for time to change! nt.DefaultValue("crat", func(name string) any { return time.Now() })