Skip to content

Commit f234820

Browse files
Merge pull request #7 from OVYA/refactoring
Refactoring + concurrent unit tests + benchmark
2 parents 3fddaeb + 846fb75 commit f234820

File tree

9 files changed

+296
-185
lines changed

9 files changed

+296
-185
lines changed

README.md

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ A simple template engine for writing dynamic SQL queries.
88
[![GoDoc](https://godoc.org/github.com/NicklasWallgren/sqlTemplate?status.svg)](https://godoc.org/github.com/NicklasWallgren/sqlTemplate)
99
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/cabd5fbbcde543ec959fb4a3581600ed)](https://app.codacy.com/gh/NicklasWallgren/sqlTemplate?utm_source=github.com&utm_medium=referral&utm_content=NicklasWallgren/sqlTemplate&utm_campaign=Badge_Grade)
1010

11-
Sometimes it can be hard to write comprehensible SQL queries with tools like SQL builders ([squirrel](https://github.com/Masterminds/squirrel)
12-
or [dbr](https://github.com/gocraft/dbr)), specially dynamic queries with optional statements and joins.
13-
It can be hard to see the overall cohesive structure of the queries, and the primary goal.
11+
Sometimes it can be hard to write comprehensible SQL queries with
12+
tools like SQL builders ([squirrel](https://github.com/Masterminds/squirrel)
13+
or [dbr](https://github.com/gocraft/dbr)), specially dynamic queries
14+
with optional statements and joins.
15+
It can be hard to see the overall cohesive structure of the queries,
16+
and the primary goal.
1417

15-
The main motivation of this library is to separate the SQL queries from the Go code, and to improve the readability of complex dynamic queries.
18+
The main motivation of this library is to separate the SQL queries
19+
from the Go code, and to improve the readability of complex dynamic
20+
queries.
1621

1722
Check out the API Documentation http://godoc.org/github.com/NicklasWallgren/sqlTemplate
1823

@@ -77,17 +82,29 @@ fmt.Printf("query parameters %v\n", tmpl.GetParams())
7782
```
7883

7984
## Unit tests
85+
86+
```bash
87+
go test -v -race ./pkg
88+
```
89+
90+
For benchmark :
91+
8092
```bash
81-
go test -v -race $(go list ./... | grep -v vendor)
93+
go test ./pkg -bench=.
8294
```
8395

8496
### Code Guide
8597

86-
We use GitHub Actions to make sure the codebase is consistent (`golangci-lint run`) and continuously tested (`go test -v -race ./pkg`). We try to keep comments at a maximum of 120 characters of length and code at 120.
98+
We use GitHub Actions to make sure the codebase is consistent
99+
(`golangci-lint run`) and continuously tested (`go test -v -race
100+
./pkg`). We try to keep comments at a maximum of 120 characters of
101+
length and code at 120.
87102

88103
## Contributing
89104

90-
If you find any problems or have suggestions about this library, please submit an issue. Moreover, any pull request, code review and feedback are welcome.
105+
If you find any problems or have suggestions about this library,
106+
please submit an issue. Moreover, any pull request, code review and
107+
feedback are welcome.
91108

92109
## Contributors
93110
- [Nicklas Wallgren](https://github.com/NicklasWallgren)
@@ -97,4 +114,4 @@ If you find any problems or have suggestions about this library, please submit a
97114

98115
## License
99116

100-
[MIT](./LICENSE)
117+
[MIT](./LICENSE)

examples/embedded_files/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
var fs embed.FS
1212

1313
func main() {
14-
sqlT := sqlTemplate.NewQueryTemplateEngine()
14+
sqlT := sqlTemplate.NewTemplateEngine()
1515
if err := sqlT.Register("users", fs, ".tsql"); err != nil {
1616
panic(err)
1717
}

examples/map_as_param/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ func main() {
1111
wd, _ := os.Getwd()
1212
fs := os.DirFS(wd + "/examples/map_as_param/queries/users")
1313

14-
sqlT := sqlTemplate.NewQueryTemplateEngine()
14+
sqlT := sqlTemplate.NewTemplateEngine()
1515
if err := sqlT.Register("users", fs, ".tsql"); err != nil {
1616
panic(err)
1717
}

examples/postgresql_placeholder/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ func main() {
1313

1414
pgPlaceholder := func(_ any, index int) string { return fmt.Sprintf("$%d", index+1) }
1515

16-
sqlT := sqlTemplate.NewQueryTemplateEngine(sqlTemplate.WithPlaceholderFunc(pgPlaceholder))
16+
sqlT := sqlTemplate.NewTemplateEngine(sqlTemplate.WithPlaceholderFunc(pgPlaceholder))
1717
if err := sqlT.Register("users", fs, ".tsql"); err != nil {
1818
panic(err)
1919
}

examples/struct_as_param/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func main() {
1717
wd, _ := os.Getwd()
1818
fs := os.DirFS(wd + "/examples/struct_as_param/queries/users")
1919

20-
sqlT := sqlTemplate.NewQueryTemplateEngine()
20+
sqlT := sqlTemplate.NewTemplateEngine()
2121
if err := sqlT.Register("users", fs, ".tsql"); err != nil {
2222
panic(err)
2323
}

pkg/template_binding.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package pkg
2+
3+
type placeholderFunc func(value any, index int) string
4+
5+
func defaultPlaceholderFunc(_ any, _ int) string { return "?" }
6+
7+
type bindingEngine interface {
8+
// StoreValue stores the given `value` and return the index order of
9+
// the stored value.
10+
storeValue(any) int
11+
// GetValues get the stored values.
12+
getValues() []any
13+
// new returns an new instance.
14+
new() bindingEngine
15+
// SetPlaceholderFunc allows to set custom placeholderFunc.
16+
SetPlaceholderFunc(placeholderFunc)
17+
// getPlaceholderFunc returns the placeholder function.
18+
getPlaceholderFunc() placeholderFunc
19+
}
20+
21+
// DefaultBindingEngine is a base engine to handle bindings (placeholder "?" for MySQL-like dbe).
22+
// It can be oveload to provide other type if bindings (placeholder "$i" for PostgreSQL-like dbe).
23+
type DefaultBindingEngine struct {
24+
values []any
25+
index int
26+
placeholderFunc placeholderFunc
27+
}
28+
29+
func (b *DefaultBindingEngine) new() bindingEngine {
30+
newBE := NewBindingEngine()
31+
newBE.SetPlaceholderFunc(b.placeholderFunc)
32+
33+
return newBE
34+
}
35+
36+
func (b *DefaultBindingEngine) getPlaceholderFunc() placeholderFunc {
37+
if b.placeholderFunc == nil {
38+
return defaultPlaceholderFunc
39+
}
40+
41+
return b.placeholderFunc
42+
}
43+
44+
// SetPlaceholderFunc allows to set custom placeholder function.
45+
func (b *DefaultBindingEngine) SetPlaceholderFunc(placeholderfunc placeholderFunc) {
46+
b.placeholderFunc = placeholderfunc
47+
}
48+
49+
func (b *DefaultBindingEngine) storeValue(value any) int {
50+
b.values = append(b.values, value)
51+
b.index++
52+
53+
return b.index - 1
54+
}
55+
56+
func (b *DefaultBindingEngine) getValues() []any {
57+
return b.values
58+
}
59+
60+
// NewBindingEngine build a binding engine based on the default binding engine.
61+
func NewBindingEngine() bindingEngine {
62+
return &DefaultBindingEngine{values: []any{}, index: 0, placeholderFunc: defaultPlaceholderFunc}
63+
}
64+
65+
// bind is a dummy function which is never used while executing a template.
66+
func bind(_ interface{}) string {
67+
panic("dummy function which should never be used.")
68+
}

pkg/template_engine.go

Lines changed: 86 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,95 @@
1+
// Package pkg is the public lib of SQLTemplate.
2+
// SQLTemplate is a simple template engine for writing
3+
// dynamic SQL queries.
14
package pkg
25

36
import (
7+
"bytes"
48
"fmt"
59
"io/fs"
6-
"text/template"
10+
txtTemplate "text/template"
711
)
812

9-
// QueryTemplateEngine is the interface implemented by types that can parse sql templates.
10-
type QueryTemplateEngine interface {
11-
// Parse parses a sql template and returns the 'QueryTemplate'
12-
Parse(namespace string, templateName string) (QueryTemplate, error)
13-
// ParseWithValuesFromMap parses a sql template with values from a map and returns the 'QueryTemplate'
14-
ParseWithValuesFromMap(namespace string, templateName string, parameters map[string]interface{}) (QueryTemplate, error)
15-
// ParseWithValuesFromStruct parses a sql template with values from a struct and returns the 'QueryTemplate'
16-
ParseWithValuesFromStruct(namespace string, templateName string, parameters interface{}) (QueryTemplate, error)
17-
// Register registers a new namespace by template filesystem and extension
13+
// TemplateEngine is the interface implemented by types that can parse sql templates.
14+
type TemplateEngine interface {
15+
// Parse parses a sql template and returns the 'Template'.
16+
Parse(namespace string, templateName string) (Template, error)
17+
// ParseWithValuesFromMap parses a sql template with values from a map and returns the 'Template'.
18+
ParseWithValuesFromMap(namespace string, templateName string, parameters map[string]interface{}) (Template, error)
19+
// ParseWithValuesFromStruct parses a sql template with values from a struct and returns the 'Template'.
20+
ParseWithValuesFromStruct(namespace string, templateName string, parameters interface{}) (Template, error)
21+
// Register registers a new namespace by template filesystem and extension.
1822
Register(namespace string, filesystem fs.FS, extensions string) error
1923
}
2024

21-
// QueryTemplate is the interface implemented by types that holds the parsed template sql query context.
22-
type QueryTemplate interface {
25+
// QueryTemplateEngine is for backwards compatibility.
26+
// Deprecated: use TemplateEngine.
27+
type QueryTemplateEngine = TemplateEngine
28+
29+
// Template is the interface implemented by types that holds the parsed template sql context.
30+
type Template interface {
2331
// GetQuery returns the query containing named values.
2432
GetQuery() string
2533
// GetParams returns the values in order.
2634
GetParams() []interface{}
2735
}
2836

37+
// QueryTemplate is for backwards compatibility.
38+
// Deprecated: use Template.
39+
type QueryTemplate = Template
40+
2941
// Option definition.
30-
type Option func(*queryTemplateEngine)
42+
type Option func(*templateEngine)
3143

32-
type queryTemplateEngine struct {
44+
type templateEngine struct {
3345
repository *repository
3446
bindingEngine bindingEngine
3547
}
3648

37-
type queryTemplate struct {
49+
type template struct {
3850
template string
3951
params []interface{}
4052
}
4153

42-
func (t queryTemplate) GetQuery() string {
54+
func (t template) GetQuery() string {
4355
return t.template
4456
}
4557

46-
func (t queryTemplate) GetParams() []interface{} {
58+
func (t template) GetParams() []interface{} {
4759
return t.params
4860
}
4961

5062
// WithTemplateFunctions creates an Option func to set template functions.
5163
// nolint:deadcode
52-
func WithTemplateFunctions(funcMap template.FuncMap) Option {
53-
return func(queryTypeEngine *queryTemplateEngine) {
54-
queryTypeEngine.repository.addFunctions(funcMap)
64+
func WithTemplateFunctions(funcMap txtTemplate.FuncMap) Option {
65+
return func(templateTypeEngine *templateEngine) {
66+
templateTypeEngine.repository.addFunctions(funcMap)
5567
}
5668
}
5769

5870
// WithBindingEngine creates an Option func to set custom binding engine.
5971
// nolint:deadcode
6072
func WithBindingEngine(bEngine bindingEngine) Option {
61-
return func(queryTypeEngine *queryTemplateEngine) {
62-
queryTypeEngine.bindingEngine = bEngine
73+
return func(templateTypeEngine *templateEngine) {
74+
templateTypeEngine.bindingEngine = bEngine
6375
}
6476
}
6577

6678
// WithPlaceholderFunc creates an Option func to set custom placeholder function.
6779
// nolint:deadcode
6880
func WithPlaceholderFunc(placeholderfunc placeholderFunc) Option {
69-
return func(queryTypeEngine *queryTemplateEngine) {
70-
if queryTypeEngine.bindingEngine == nil {
71-
queryTypeEngine.bindingEngine = NewBindingEngine()
81+
return func(templateTypeEngine *templateEngine) {
82+
if templateTypeEngine.bindingEngine == nil {
83+
templateTypeEngine.bindingEngine = NewBindingEngine()
7284
}
7385

74-
queryTypeEngine.bindingEngine.SetPlaceholderFunc(placeholderfunc)
86+
templateTypeEngine.bindingEngine.SetPlaceholderFunc(placeholderfunc)
7587
}
7688
}
7789

78-
// NewQueryTemplateEngine returns a new instance of 'QueryTemplateEngine'.
79-
func NewQueryTemplateEngine(options ...Option) QueryTemplateEngine {
80-
templateEngine := &queryTemplateEngine{repository: newRepository(), bindingEngine: nil}
90+
// NewTemplateEngine returns a new instance of 'TemplateEngine'.
91+
func NewTemplateEngine(options ...Option) TemplateEngine {
92+
templateEngine := &templateEngine{repository: newRepository(), bindingEngine: nil}
8193

8294
// Apply options if there are any, can overwrite default
8395
for _, option := range options {
@@ -87,7 +99,12 @@ func NewQueryTemplateEngine(options ...Option) QueryTemplateEngine {
8799
return templateEngine
88100
}
89101

90-
func (q queryTemplateEngine) Register(namespace string, filesystem fs.FS, ext string) error {
102+
// Deprecated: use NewTemplateEngine.
103+
func NewQueryTemplateEngine(options ...Option) TemplateEngine {
104+
return NewTemplateEngine(options...)
105+
}
106+
107+
func (q templateEngine) Register(namespace string, filesystem fs.FS, ext string) error {
91108
err := q.repository.add(namespace, filesystem, ext)
92109
if err != nil {
93110
return fmt.Errorf("could not register the namespace %s %w", namespace, err)
@@ -96,29 +113,59 @@ func (q queryTemplateEngine) Register(namespace string, filesystem fs.FS, ext st
96113
return nil
97114
}
98115

99-
func (q queryTemplateEngine) Parse(namespace string, templateName string) (QueryTemplate, error) {
100-
sqlQuery, bindings, err := q.repository.parse(namespace, templateName, nil, q.bindingEngine)
116+
func (q templateEngine) Parse(namespace string, templateName string) (Template, error) {
117+
sqlQuery, bindings, err := q.parse(namespace, templateName, nil)
101118
if err != nil {
102119
return nil, fmt.Errorf("unable to parse %s for namespace %s %w", templateName, namespace, err)
103120
}
104121

105-
return &queryTemplate{sqlQuery, bindings}, nil
122+
return &template{sqlQuery, bindings}, nil
106123
}
107124

108-
func (q queryTemplateEngine) ParseWithValuesFromMap(namespace string, templateName string, parameters map[string]interface{}) (QueryTemplate, error) {
109-
sqlQuery, bindings, err := q.repository.parse(namespace, templateName, parameters, q.bindingEngine)
125+
func (q templateEngine) ParseWithValuesFromMap(namespace string, templateName string, parameters map[string]interface{}) (Template, error) {
126+
sqlQuery, bindings, err := q.parse(namespace, templateName, parameters)
110127
if err != nil {
111128
return nil, fmt.Errorf("unable to parse %s for namespace %s %w", templateName, namespace, err)
112129
}
113130

114-
return &queryTemplate{sqlQuery, bindings}, nil
131+
return &template{sqlQuery, bindings}, nil
115132
}
116133

117-
func (q queryTemplateEngine) ParseWithValuesFromStruct(namespace string, templateName string, parameters interface{}) (QueryTemplate, error) {
118-
sqlQuery, bindings, err := q.repository.parse(namespace, templateName, parameters, q.bindingEngine)
134+
func (q templateEngine) ParseWithValuesFromStruct(namespace string, templateName string, parameters interface{}) (Template, error) {
135+
sqlQuery, bindings, err := q.parse(namespace, templateName, parameters)
119136
if err != nil {
120137
return nil, fmt.Errorf("unable to parse %s for namespace %s %w", templateName, namespace, err)
121138
}
122139

123-
return &queryTemplate{sqlQuery, bindings}, nil
140+
return &template{sqlQuery, bindings}, nil
141+
}
142+
143+
// parse executes the template and returns the resulting SQL or an error.
144+
func (q templateEngine) parse(namespace string, name string, data interface{}) (string, []interface{}, error) {
145+
tmpl, err := q.repository.getTemplate(namespace)
146+
if err != nil {
147+
return "", nil, err
148+
}
149+
150+
var bEngine bindingEngine
151+
152+
// Apply the bind function which stores the values for any placeholder parameters
153+
if q.bindingEngine == nil {
154+
bEngine = &DefaultBindingEngine{values: []any{}, index: 0, placeholderFunc: defaultPlaceholderFunc}
155+
} else {
156+
bEngine = q.bindingEngine.new()
157+
}
158+
159+
tmpl.Funcs(txtTemplate.FuncMap{"bind": func(value any) string {
160+
index := bEngine.storeValue(value)
161+
162+
return bEngine.getPlaceholderFunc()(value, index)
163+
}})
164+
165+
var b bytes.Buffer
166+
if err := tmpl.ExecuteTemplate(&b, name, data); err != nil {
167+
return "", nil, fmt.Errorf("unable to execute template %w", err)
168+
}
169+
170+
return b.String(), bEngine.getValues(), nil
124171
}

0 commit comments

Comments
 (0)