Skip to content

Commit 5d06b89

Browse files
authored
Merge pull request #27 from kayrein/query_templates
Query templates
2 parents 6147875 + 138f85d commit 5d06b89

File tree

6 files changed

+601
-458
lines changed

6 files changed

+601
-458
lines changed

README.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,27 @@ dot2, err := dotsql.LoadFromFile("queries2.sql")
7474
dot := dotsql.Merge(dot1, dot2)
7575
```
7676

77+
Text Interpolation
78+
--
79+
[text/template](https://pkg.go.dev/text/template)-style text interpolation is supported.
80+
81+
To use, call `.WithData(any)` on your dotsql instance to
82+
create a new instance which passes those values into the templating library.
83+
84+
```sql
85+
-- name: count-users
86+
SELECT count(*) FROM users {{if .exclude_deleted}}WHERE deleted IS NULL{{end}}
87+
```
88+
89+
```go
90+
dotsql.WithData(map[string]any{"exclude_deleted": true}).Query(db, "count-users")
91+
```
92+
7793
Embeding
7894
--
7995
To avoid distributing `sql` files alongside the binary file, you will need to use tools like
8096
[gotic](https://github.com/qustavo/gotic) to embed / pack everything into one file.
8197

82-
TODO
83-
--
84-
- [ ] Enable text interpolation inside queries using `text/template`
85-
86-
8798
SQLX
8899
--
89100
For [sqlx](https://github.com/jmoiron/sqlx) support check [dotsqlx](https://github.com/swithek/dotsqlx)

compareCalls_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package dotsql
2+
3+
import (
4+
"context"
5+
"reflect"
6+
"testing"
7+
)
8+
9+
type PrepareCalls []struct {
10+
Query string
11+
}
12+
13+
func comparePrepareCalls(t *testing.T, ff PrepareCalls, template string) bool {
14+
t.Helper()
15+
if len(ff) != 1 {
16+
t.Errorf("prepare was expected to be called only once, but was called %d times", len(ff))
17+
return false
18+
} else if ff[0].Query != template {
19+
t.Errorf("prepare was expected to be called with %q query, got %q", template, ff[0].Query)
20+
return false
21+
}
22+
return true
23+
}
24+
25+
type PrepareContextCalls []struct {
26+
Ctx context.Context
27+
Query string
28+
}
29+
30+
func comparePrepareContextCalls(t *testing.T, ff PrepareContextCalls, ctx context.Context, template string) bool {
31+
t.Helper()
32+
if len(ff) != 1 {
33+
t.Errorf("prepare was expected to be called only once, but was called %d times", len(ff))
34+
return false
35+
} else if ff[0].Query != template {
36+
t.Errorf("prepare was expected to be called with %q query, got %q", template, ff[0].Query)
37+
return false
38+
} else if !reflect.DeepEqual(ff[0].Ctx, ctx) {
39+
t.Error("prepare context does not match")
40+
return false
41+
}
42+
return true
43+
}
44+
45+
type QueryCalls []struct {
46+
Query string
47+
Args []interface{}
48+
}
49+
50+
func compareCalls(t *testing.T, ff QueryCalls, command, template, testArg string) bool {
51+
t.Helper()
52+
if len(ff) != 1 {
53+
t.Errorf("%s was expected to be called only once, but was called %d times", command, len(ff))
54+
return false
55+
} else if ff[0].Query != template {
56+
t.Errorf("%s was expected to be called with %q query, got %q", command, template, ff[0].Query)
57+
return false
58+
} else if len(ff[0].Args) != 1 {
59+
t.Errorf("%s was expected to be called with 1 argument, got %d", command, len(ff[0].Args))
60+
return false
61+
} else if !reflect.DeepEqual(ff[0].Args[0], testArg) {
62+
t.Errorf("%s was expected to be called with %q argument, got %v", command, testArg, ff[0].Args[0])
63+
return false
64+
}
65+
return true
66+
}
67+
68+
type QueryContextCalls []struct {
69+
Ctx context.Context
70+
Query string
71+
Args []interface{}
72+
}
73+
74+
func compareContextCalls(t *testing.T, ff QueryContextCalls, ctx context.Context, command, template, testArg string) bool {
75+
t.Helper()
76+
if len(ff) != 1 {
77+
t.Errorf("%s was expected to be called only once, but was called %d times", command, len(ff))
78+
return false
79+
} else if ff[0].Query != template {
80+
t.Errorf("%s was expected to be called with %q query, got %q", command, template, ff[0].Query)
81+
return false
82+
} else if len(ff[0].Args) != 1 {
83+
t.Errorf("%s was expected to be called with 1 argument, got %d", command, len(ff[0].Args))
84+
return false
85+
} else if !reflect.DeepEqual(ff[0].Args[0], testArg) {
86+
t.Errorf("%s was expected to be called with %q argument, got %v", command, testArg, ff[0].Args[0])
87+
return false
88+
} else if !reflect.DeepEqual(ff[0].Ctx, ctx) {
89+
t.Errorf("%s context does not match", command)
90+
return false
91+
}
92+
return true
93+
}

dotsql.go

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"fmt"
1515
"io"
1616
"os"
17+
"text/template"
1718
)
1819

1920
// Preparer is an interface used by Prepare.
@@ -58,21 +59,34 @@ type ExecerContext interface {
5859

5960
// DotSql represents a dotSQL queries holder.
6061
type DotSql struct {
61-
queries map[string]string
62+
queries map[string]*template.Template
63+
data any
6264
}
6365

64-
func (d DotSql) lookupQuery(name string) (query string, err error) {
65-
query, ok := d.queries[name]
66+
func (d DotSql) WithData(data any) DotSql {
67+
return DotSql{queries: d.queries, data: data}
68+
}
69+
70+
func (d DotSql) lookupQuery(name string, data any) (string, error) {
71+
template, ok := d.queries[name]
6672
if !ok {
67-
err = fmt.Errorf("dotsql: '%s' could not be found", name)
73+
return "", fmt.Errorf("dotsql: '%s' could not be found", name)
74+
}
75+
if template == nil {
76+
return "", nil
77+
}
78+
buffer := bytes.NewBufferString("")
79+
err := template.Execute(buffer, data)
80+
if err != nil {
81+
return "", fmt.Errorf("error parsing template: %w", err)
6882
}
6983

70-
return
84+
return buffer.String(), nil
7185
}
7286

7387
// Prepare is a wrapper for database/sql's Prepare(), using dotsql named query.
7488
func (d DotSql) Prepare(db Preparer, name string) (*sql.Stmt, error) {
75-
query, err := d.lookupQuery(name)
89+
query, err := d.lookupQuery(name, d.data)
7690
if err != nil {
7791
return nil, err
7892
}
@@ -82,7 +96,7 @@ func (d DotSql) Prepare(db Preparer, name string) (*sql.Stmt, error) {
8296

8397
// PrepareContext is a wrapper for database/sql's PrepareContext(), using dotsql named query.
8498
func (d DotSql) PrepareContext(ctx context.Context, db PreparerContext, name string) (*sql.Stmt, error) {
85-
query, err := d.lookupQuery(name)
99+
query, err := d.lookupQuery(name, d.data)
86100
if err != nil {
87101
return nil, err
88102
}
@@ -92,7 +106,7 @@ func (d DotSql) PrepareContext(ctx context.Context, db PreparerContext, name str
92106

93107
// Query is a wrapper for database/sql's Query(), using dotsql named query.
94108
func (d DotSql) Query(db Queryer, name string, args ...interface{}) (*sql.Rows, error) {
95-
query, err := d.lookupQuery(name)
109+
query, err := d.lookupQuery(name, d.data)
96110
if err != nil {
97111
return nil, err
98112
}
@@ -102,7 +116,7 @@ func (d DotSql) Query(db Queryer, name string, args ...interface{}) (*sql.Rows,
102116

103117
// QueryContext is a wrapper for database/sql's QueryContext(), using dotsql named query.
104118
func (d DotSql) QueryContext(ctx context.Context, db QueryerContext, name string, args ...interface{}) (*sql.Rows, error) {
105-
query, err := d.lookupQuery(name)
119+
query, err := d.lookupQuery(name, d.data)
106120
if err != nil {
107121
return nil, err
108122
}
@@ -112,7 +126,7 @@ func (d DotSql) QueryContext(ctx context.Context, db QueryerContext, name string
112126

113127
// QueryRow is a wrapper for database/sql's QueryRow(), using dotsql named query.
114128
func (d DotSql) QueryRow(db QueryRower, name string, args ...interface{}) (*sql.Row, error) {
115-
query, err := d.lookupQuery(name)
129+
query, err := d.lookupQuery(name, d.data)
116130
if err != nil {
117131
return nil, err
118132
}
@@ -122,7 +136,7 @@ func (d DotSql) QueryRow(db QueryRower, name string, args ...interface{}) (*sql.
122136

123137
// QueryRowContext is a wrapper for database/sql's QueryRowContext(), using dotsql named query.
124138
func (d DotSql) QueryRowContext(ctx context.Context, db QueryRowerContext, name string, args ...interface{}) (*sql.Row, error) {
125-
query, err := d.lookupQuery(name)
139+
query, err := d.lookupQuery(name, d.data)
126140
if err != nil {
127141
return nil, err
128142
}
@@ -132,7 +146,7 @@ func (d DotSql) QueryRowContext(ctx context.Context, db QueryRowerContext, name
132146

133147
// Exec is a wrapper for database/sql's Exec(), using dotsql named query.
134148
func (d DotSql) Exec(db Execer, name string, args ...interface{}) (sql.Result, error) {
135-
query, err := d.lookupQuery(name)
149+
query, err := d.lookupQuery(name, d.data)
136150
if err != nil {
137151
return nil, err
138152
}
@@ -142,7 +156,7 @@ func (d DotSql) Exec(db Execer, name string, args ...interface{}) (sql.Result, e
142156

143157
// ExecContext is a wrapper for database/sql's ExecContext(), using dotsql named query.
144158
func (d DotSql) ExecContext(ctx context.Context, db ExecerContext, name string, args ...interface{}) (sql.Result, error) {
145-
query, err := d.lookupQuery(name)
159+
query, err := d.lookupQuery(name, d.data)
146160
if err != nil {
147161
return nil, err
148162
}
@@ -152,11 +166,11 @@ func (d DotSql) ExecContext(ctx context.Context, db ExecerContext, name string,
152166

153167
// Raw returns the query, everything after the --name tag
154168
func (d DotSql) Raw(name string) (string, error) {
155-
return d.lookupQuery(name)
169+
return d.lookupQuery(name, d.data)
156170
}
157171

158172
// QueryMap returns a map[string]string of loaded queries
159-
func (d DotSql) QueryMap() map[string]string {
173+
func (d DotSql) QueryMap() map[string]*template.Template {
160174
return d.queries
161175
}
162176

@@ -165,11 +179,18 @@ func Load(r io.Reader) (*DotSql, error) {
165179
scanner := &Scanner{}
166180
queries := scanner.Run(bufio.NewScanner(r))
167181

168-
dotsql := &DotSql{
169-
queries: queries,
182+
templates := make(map[string]*template.Template)
183+
for k, v := range queries {
184+
tmpl, err := template.New(k).Parse(v)
185+
if err != nil {
186+
return nil, err
187+
}
188+
templates[k] = tmpl
170189
}
171190

172-
return dotsql, nil
191+
return &DotSql{
192+
queries: templates,
193+
}, nil
173194
}
174195

175196
// LoadFromFile imports SQL queries from the file.
@@ -193,7 +214,7 @@ func LoadFromString(sql string) (*DotSql, error) {
193214
// It's in-order, so the last source will override queries with the same name
194215
// in the previous arguments if any.
195216
func Merge(dots ...*DotSql) *DotSql {
196-
queries := make(map[string]string)
217+
queries := make(map[string]*template.Template)
197218

198219
for _, dot := range dots {
199220
for k, v := range dot.QueryMap() {

0 commit comments

Comments
 (0)