Skip to content

Commit 3fddaeb

Browse files
Merge pull request #6 from OVYA/master
Support to set custom binding placeholder and custom binding engine
2 parents 28a921d + a4a8faf commit 3fddaeb

6 files changed

Lines changed: 229 additions & 19 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
sqlTemplate "github.com/NicklasWallgren/sqlTemplate/pkg"
8+
)
9+
10+
func main() {
11+
wd, _ := os.Getwd()
12+
fs := os.DirFS(wd + "/examples/postgresql_placeholder/queries/users")
13+
14+
pgPlaceholder := func(_ any, index int) string { return fmt.Sprintf("$%d", index+1) }
15+
16+
sqlT := sqlTemplate.NewQueryTemplateEngine(sqlTemplate.WithPlaceholderFunc(pgPlaceholder))
17+
if err := sqlT.Register("users", fs, ".tsql"); err != nil {
18+
panic(err)
19+
}
20+
21+
criteria := map[string]interface{}{"a": "a", "b": "b", "c": "c"}
22+
23+
tmpl, err := sqlT.ParseWithValuesFromMap("users", "multipleBinds", criteria)
24+
if err != nil {
25+
panic(err)
26+
}
27+
28+
// nolint:forbidigo
29+
fmt.Printf("query %v\n", tmpl.GetQuery())
30+
// nolint:forbidigo
31+
fmt.Printf("query parameters %v\n", tmpl.GetParams())
32+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{{define "multipleBinds"}}
2+
SELECT *
3+
FROM users
4+
WHERE TRUE
5+
AND a={{bind .a}}
6+
AND b={{bind .b}}
7+
AND c={{bind .c}}
8+
{{end}}

pkg/template_engine.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ type QueryTemplate interface {
3030
type Option func(*queryTemplateEngine)
3131

3232
type queryTemplateEngine struct {
33-
repository *repository
33+
repository *repository
34+
bindingEngine bindingEngine
3435
}
3536

3637
type queryTemplate struct {
@@ -54,9 +55,29 @@ func WithTemplateFunctions(funcMap template.FuncMap) Option {
5455
}
5556
}
5657

58+
// WithBindingEngine creates an Option func to set custom binding engine.
59+
// nolint:deadcode
60+
func WithBindingEngine(bEngine bindingEngine) Option {
61+
return func(queryTypeEngine *queryTemplateEngine) {
62+
queryTypeEngine.bindingEngine = bEngine
63+
}
64+
}
65+
66+
// WithPlaceholderFunc creates an Option func to set custom placeholder function.
67+
// nolint:deadcode
68+
func WithPlaceholderFunc(placeholderfunc placeholderFunc) Option {
69+
return func(queryTypeEngine *queryTemplateEngine) {
70+
if queryTypeEngine.bindingEngine == nil {
71+
queryTypeEngine.bindingEngine = NewBindingEngine()
72+
}
73+
74+
queryTypeEngine.bindingEngine.SetPlaceholderFunc(placeholderfunc)
75+
}
76+
}
77+
5778
// NewQueryTemplateEngine returns a new instance of 'QueryTemplateEngine'.
5879
func NewQueryTemplateEngine(options ...Option) QueryTemplateEngine {
59-
templateEngine := &queryTemplateEngine{repository: newRepository()}
80+
templateEngine := &queryTemplateEngine{repository: newRepository(), bindingEngine: nil}
6081

6182
// Apply options if there are any, can overwrite default
6283
for _, option := range options {
@@ -76,7 +97,7 @@ func (q queryTemplateEngine) Register(namespace string, filesystem fs.FS, ext st
7697
}
7798

7899
func (q queryTemplateEngine) Parse(namespace string, templateName string) (QueryTemplate, error) {
79-
sqlQuery, bindings, err := q.repository.parse(namespace, templateName, nil)
100+
sqlQuery, bindings, err := q.repository.parse(namespace, templateName, nil, q.bindingEngine)
80101
if err != nil {
81102
return nil, fmt.Errorf("unable to parse %s for namespace %s %w", templateName, namespace, err)
82103
}
@@ -85,7 +106,7 @@ func (q queryTemplateEngine) Parse(namespace string, templateName string) (Query
85106
}
86107

87108
func (q queryTemplateEngine) ParseWithValuesFromMap(namespace string, templateName string, parameters map[string]interface{}) (QueryTemplate, error) {
88-
sqlQuery, bindings, err := q.repository.parse(namespace, templateName, parameters)
109+
sqlQuery, bindings, err := q.repository.parse(namespace, templateName, parameters, q.bindingEngine)
89110
if err != nil {
90111
return nil, fmt.Errorf("unable to parse %s for namespace %s %w", templateName, namespace, err)
91112
}
@@ -94,7 +115,7 @@ func (q queryTemplateEngine) ParseWithValuesFromMap(namespace string, templateNa
94115
}
95116

96117
func (q queryTemplateEngine) ParseWithValuesFromStruct(namespace string, templateName string, parameters interface{}) (QueryTemplate, error) {
97-
sqlQuery, bindings, err := q.repository.parse(namespace, templateName, parameters)
118+
sqlQuery, bindings, err := q.repository.parse(namespace, templateName, parameters, q.bindingEngine)
98119
if err != nil {
99120
return nil, fmt.Errorf("unable to parse %s for namespace %s %w", templateName, namespace, err)
100121
}

pkg/template_engine_test.go

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@ package pkg_test
22

33
import (
44
"embed"
5+
"fmt"
56
"testing"
67
"text/template"
78

89
"github.com/NicklasWallgren/sqlTemplate/pkg"
910

1011
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
1113
)
1214

1315
//go:embed testdata/*.tsql
1416
var fs embed.FS // nolint: varnamelen
1517

16-
func TestNewQueryTemplateEngine(t *testing.T) {
18+
func TestNewQueryTemplateEngine(_ *testing.T) {
1719
pkg.NewQueryTemplateEngine()
1820
}
1921

@@ -74,7 +76,88 @@ func TestQueryTemplateEngine_ParseWithValuesFromStruct(t *testing.T) {
7476
criteria := searchCriteria{ID: 1, Order: "id"}
7577

7678
template, err := sqlT.ParseWithValuesFromStruct("users", "findById", criteria)
77-
assert.Nil(t, err)
79+
require.Nil(t, err)
7880
assert.Equal(t, "\n SELECT *\n FROM users\n WHERE id=?\n ORDER BY id\n", template.GetQuery())
7981
assert.Equal(t, []any{1}, template.GetParams())
8082
}
83+
84+
func getExtendedCriteria() map[string]any {
85+
return map[string]interface{}{"a": "a", "b": "b", "c": "c"}
86+
}
87+
88+
func getExpectedExtendedQuery(isPgBind bool) string {
89+
expected := `
90+
SELECT *
91+
FROM users
92+
WHERE TRUE
93+
`
94+
if isPgBind {
95+
expected += ` AND a=$1
96+
AND b=$2
97+
AND c=$3
98+
`
99+
} else {
100+
expected += ` AND a=?
101+
AND b=?
102+
AND c=?
103+
`
104+
}
105+
106+
return expected
107+
}
108+
109+
func testExtendedParams(t *testing.T, params []any) {
110+
t.Helper()
111+
assert.Equal(t, 3, len(params), "wrong parameters length")
112+
assert.Equal(t, "a", params[0], "parameter does not match")
113+
assert.Equal(t, "b", params[1], "parameter does not match")
114+
assert.Equal(t, "c", params[2], "parameter does not match")
115+
}
116+
117+
func TestQueryTemplateEngine_ParseWithValuesFromMap2(t *testing.T) {
118+
sqlT := pkg.NewQueryTemplateEngine(pkg.WithTemplateFunctions(template.FuncMap{}))
119+
120+
if err := sqlT.Register("users", fs, ".tsql"); err != nil {
121+
t.Fatal()
122+
}
123+
124+
template, err := sqlT.ParseWithValuesFromMap("users", "multipleBinds", getExtendedCriteria())
125+
assert.Nil(t, err)
126+
127+
assert.Equal(t, getExpectedExtendedQuery(false), template.GetQuery())
128+
129+
testExtendedParams(t, template.GetParams())
130+
}
131+
132+
func TestQueryTemplateEngine_ParseWithValuesFromMapCustomPlaceholder(t *testing.T) {
133+
pgPlaceholder := func(_ any, index int) string { return fmt.Sprintf("$%d", index+1) }
134+
135+
sqlT := pkg.NewQueryTemplateEngine(pkg.WithPlaceholderFunc(pgPlaceholder))
136+
137+
if err := sqlT.Register("users", fs, ".tsql"); err != nil {
138+
t.Fatal()
139+
}
140+
141+
template, err := sqlT.ParseWithValuesFromMap("users", "multipleBinds", getExtendedCriteria())
142+
require.Nil(t, err)
143+
144+
assert.Equal(t, getExpectedExtendedQuery(true), template.GetQuery(), "expected query failed")
145+
testExtendedParams(t, template.GetParams())
146+
}
147+
148+
func TestQueryTemplateEngine_ParseWithValuesFromMapCustomBindingEngine(t *testing.T) {
149+
pgPlaceholder := func(_ any, index int) string { return fmt.Sprintf("$%d", index+1) }
150+
bindingEninge := pkg.NewBindingEngine()
151+
bindingEninge.SetPlaceholderFunc(pgPlaceholder)
152+
sqlT := pkg.NewQueryTemplateEngine(pkg.WithBindingEngine(bindingEninge))
153+
154+
if err := sqlT.Register("users", fs, ".tsql"); err != nil {
155+
t.Fatal()
156+
}
157+
158+
template, err := sqlT.ParseWithValuesFromMap("users", "multipleBinds", getExtendedCriteria())
159+
require.Nil(t, err)
160+
161+
assert.Equal(t, getExpectedExtendedQuery(true), template.GetQuery(), "expected query failed")
162+
testExtendedParams(t, template.GetParams())
163+
}

pkg/template_repository.go

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,65 @@ type templateEntry struct {
1414
fullPath string
1515
}
1616

17-
// bindings stores the values of any placeholder parameter in the query.
18-
type bindings struct {
19-
values []interface{}
17+
type placeholderFunc func(value any, index int) string
18+
19+
func defaultPlaceholderFunc(_ any, _ int) string { return "?" }
20+
21+
type bindingEngine interface {
22+
// StoreValue stores the given `value` and return the index order of
23+
// the stored value.
24+
storeValue(any) int
25+
// GetValues get the stored values.
26+
getValues() []any
27+
// new returns an new instance.
28+
new() bindingEngine
29+
// SetPlaceholderFunc allows to set custom placeholderFunc.
30+
SetPlaceholderFunc(placeholderFunc)
31+
// getPlaceholderFunc returns the placeholder function.
32+
getPlaceholderFunc() placeholderFunc
2033
}
2134

22-
// bind stores the given `value` and returns a placeholder parameter.
23-
func (b *bindings) bind(value interface{}) string {
35+
// DefaultBindingEngine is a base engine to handle bindings (placeholder "?" for MySQL-like dbe).
36+
// It can be oveload to provide other type if bindings (placeholder "$i" for PostgreSQL-like dbe).
37+
type DefaultBindingEngine struct {
38+
values []any
39+
index int
40+
placeholderFunc placeholderFunc
41+
}
42+
43+
func (b *DefaultBindingEngine) new() bindingEngine {
44+
newBE := NewBindingEngine()
45+
newBE.SetPlaceholderFunc(b.placeholderFunc)
46+
47+
return newBE
48+
}
49+
50+
func (b *DefaultBindingEngine) getPlaceholderFunc() placeholderFunc {
51+
if b.placeholderFunc == nil {
52+
return defaultPlaceholderFunc
53+
}
54+
55+
return b.placeholderFunc
56+
}
57+
58+
// SetPlaceholderFunc allows to set custom placeholder function.
59+
func (b *DefaultBindingEngine) SetPlaceholderFunc(placeholderfunc placeholderFunc) {
60+
b.placeholderFunc = placeholderfunc
61+
}
62+
63+
func (b *DefaultBindingEngine) storeValue(value any) int {
2464
b.values = append(b.values, value)
65+
b.index++
2566

26-
return "?"
67+
return b.index - 1
68+
}
69+
70+
func (b *DefaultBindingEngine) getValues() []any {
71+
return b.values
72+
}
73+
74+
func NewBindingEngine() bindingEngine {
75+
return &DefaultBindingEngine{values: []any{}, index: 0, placeholderFunc: defaultPlaceholderFunc}
2776
}
2877

2978
// repository stores SQL templates.
@@ -68,34 +117,42 @@ func (r *repository) add(namespace string, filesystem fs.FS, extension string) e
68117
}
69118

70119
// parse executes the template and returns the resulting SQL or an error.
71-
func (r *repository) parse(namespace string, name string, data interface{}) (string, []interface{}, error) {
120+
func (r *repository) parse(namespace string, name string, data interface{}, bEngine bindingEngine) (string, []interface{}, error) {
72121
entry, ok := r.templates[namespace]
73122
if !ok {
74123
return "", nil, errors.New("unable to locate namespace " + namespace)
75124
}
76125

77126
// We clone the template to prevent simultaneous mutation of the template.FuncMap
78-
// otherwise the bind function might be replaced during execution of a template
127+
// otherwise the binds functions might be replaced during execution of a template
79128
clonedTmpl, err := entry.template.Clone()
80129
if err != nil {
81130
return "", nil, fmt.Errorf("unable to parse template %w", err)
82131
}
83132

84133
// Apply the bind function which stores the values for any placeholder parameters
85-
values := &bindings{values: []interface{}{}}
134+
if bEngine == nil {
135+
bEngine = &DefaultBindingEngine{values: []any{}, index: 0, placeholderFunc: defaultPlaceholderFunc}
136+
} else {
137+
bEngine = bEngine.new()
138+
}
139+
140+
clonedTmpl.Funcs(template.FuncMap{"bind": func(value any) string {
141+
index := bEngine.storeValue(value)
86142

87-
clonedTmpl.Funcs(template.FuncMap{"bind": values.bind})
143+
return bEngine.getPlaceholderFunc()(value, index)
144+
}})
88145

89146
var b bytes.Buffer
90147
if err := clonedTmpl.ExecuteTemplate(&b, name, data); err != nil {
91148
return "", nil, fmt.Errorf("unable to execute template %w", err)
92149
}
93150

94-
return b.String(), values.values, nil
151+
return b.String(), bEngine.getValues(), nil
95152
}
96153

97154
// bind is a dummy function which is never used while executing a template.
98-
func bind(param interface{}) string {
155+
func bind(_ interface{}) string {
99156
return "?"
100157
}
101158

pkg/testdata/users.tsql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
{{if .Order}}ORDER BY {{.Order}}{{end}}
66
{{end}}
77

8+
{{define "multipleBinds"}}
9+
SELECT *
10+
FROM users
11+
WHERE TRUE
12+
AND a={{bind .a}}
13+
AND b={{bind .b}}
14+
AND c={{bind .c}}
15+
{{end}}
16+
817
{{define "findUsers"}}
918
SELECT *
1019
FROM users

0 commit comments

Comments
 (0)