Skip to content

Commit

Permalink
Merge pull request #1 from sanggonlee/add-execute-method
Browse files Browse the repository at this point in the history
Add Execute function for alternative text/template based syntax
  • Loading branch information
sanggonlee authored Sep 18, 2021
2 parents 995a06b + 96db476 commit 4cc9234
Show file tree
Hide file tree
Showing 3 changed files with 320 additions and 1 deletion.
26 changes: 25 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ q, err := gosq.Compile(`
})
```

Or if you prefer the syntax from [text/template](https://pkg.go.dev/text/template) package:

```go
q, err := gosq.Execute(`
SELECT
products.*
{{if .IncludeReviews}} ,json_agg(reviews) AS reviews {{end}}
FROM products
{{if .IncludeReviews}} LEFT JOIN reviews ON reviews.product_id = products.id {{end}}
WHERE category = $1
OFFSET 100
LIMIT 10
`, map[string]interface{}{
"IncludeReviews": true,
})
```

## Installation

```
Expand Down Expand Up @@ -165,4 +182,11 @@ func getProducts(includeReviews bool) {
And here we are, `gosq` is born.
Note, this still doesn't address the problem with the preceeding comma. I can't think of a good way to address it in this solution - any suggestion for improvement is welcome.
Note, this still doesn't address the problem with the preceeding comma. I can't think of a good way to address it in this solution - any suggestion for improvement is welcome.
## Benchmarks
```
BenchmarkExecute-8 57698 19530 ns/op
BenchmarkCompile-8 260319 4570 ns/op
```
23 changes: 23 additions & 0 deletions gosq.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package gosq

import (
"bytes"
"fmt"
"reflect"
"text/template"

"github.com/pkg/errors"
"github.com/sanggonlee/gosq/ast"
Expand Down Expand Up @@ -85,3 +87,24 @@ func convertStructToMap(args interface{}) map[string]interface{} {
}
return m
}

// Execute is similar to Compile, but instead uses the syntax from the
// text/template package.
// Indeed it simply uses text/template package internally, and supports all
// syntax provided by it, so use at your own risk/advantage.
// The if-else-then expression equivalent to the Compile function would be:
//
// {{if predicate}} clause {{else}} clause {{end}}
func Execute(str string, args interface{}) (string, error) {
tmpl, err := template.New("gosq").Parse(str)
if err != nil {
return "", errors.Wrap(err, "parsing template")
}

var buf bytes.Buffer
if err = tmpl.Execute(&buf, args); err != nil {
return "", errors.Wrap(err, "executing template")
}

return buf.String(), nil
}
272 changes: 272 additions & 0 deletions gosq_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,279 @@ func TestCompile(t *testing.T) {
}
}

func TestExecute(t *testing.T) {
cases := []struct {
desc string
inputTemplate string
inputArgs interface{}
expected string
expectedError error
}{
{
desc: "No args",
inputTemplate: `SELECT * FROM products`,
inputArgs: nil,
expected: `SELECT * FROM products`,
},
{
desc: "Simple case of falsey substitute from map",
inputTemplate: `
SELECT
products.*
{{if .IncludeReviews}} ,json_agg(reviews) AS reviews {{end}}
FROM products
{{if .IncludeReviews}} LEFT JOIN reviews ON reviews.product_id = products.id {{end}}
WHERE category = $1
OFFSET 100
LIMIT 10
`,
inputArgs: map[string]interface{}{
"IncludeReviews": false,
},
expected: `
SELECT
products.*
FROM products
WHERE category = $1
OFFSET 100
LIMIT 10
`,
},
{
desc: "Simple case of truthy substitute from map",
inputTemplate: `
SELECT
products.*
{{if .IncludeReviews}} ,json_agg(reviews) AS reviews {{end}}
FROM products
{{if .IncludeReviews}} LEFT JOIN reviews ON reviews.product_id = products.id {{end}}
WHERE category = $1
OFFSET 100
LIMIT 10
`,
inputArgs: map[string]interface{}{
"IncludeReviews": true,
},
expected: `
SELECT
products.*
,json_agg(reviews) AS reviews
FROM products
LEFT JOIN reviews ON reviews.product_id = products.id
WHERE category = $1
OFFSET 100
LIMIT 10
`,
},
{
desc: "Recursive truthy expression",
inputTemplate: `
SELECT
products.*
{{if .IncludeReviews}}
,json_agg(reviews) AS reviews
{{if .IncludeCount}} ,count(reviews) AS num_reviews {{end}}
{{end}}
FROM products
{{if .IncludeReviews}} LEFT JOIN reviews ON reviews.product_id = products.id {{end}}
WHERE category = $1
OFFSET 100
LIMIT 10
`,
inputArgs: map[string]interface{}{
"IncludeReviews": true,
"IncludeCount": true,
},
expected: `
SELECT
products.*
,json_agg(reviews) AS reviews
,count(reviews) AS num_reviews
FROM products
LEFT JOIN reviews ON reviews.product_id = products.id
WHERE category = $1
OFFSET 100
LIMIT 10
`,
},
{
desc: "Recursive falsey expression",
inputTemplate: `
SELECT
products.*
{{if .IncludeReviews}} ,json_agg(reviews) AS reviews
{{if .IncludeCount}} ,count(reviews) AS num_reviews {{end}}
{{end}}
FROM products
{{if .IncludeReviews}} LEFT JOIN reviews ON reviews.product_id = products.id {{end}}
WHERE category = $1
OFFSET 100
LIMIT 10
`,
inputArgs: map[string]interface{}{
"IncludeReviews": true,
"IncludeCount": false,
},
expected: `
SELECT
products.*
,json_agg(reviews) AS reviews
FROM products
LEFT JOIN reviews ON reviews.product_id = products.id
WHERE category = $1
OFFSET 100
LIMIT 10
`,
},
{
desc: "Simple truthy if-then-else clause",
inputTemplate: `
SELECT
products.*
FROM products
WHERE category = $1
OFFSET 100
LIMIT {{if .GetMany}} 100 {{else}} 10 {{end}}
`,
inputArgs: map[string]interface{}{
"GetMany": true,
},
expected: `
SELECT
products.*
FROM products
WHERE category = $1
OFFSET 100
LIMIT 100
`,
},
{
desc: "Simple falsey if-then-else clause",
inputTemplate: `
SELECT
products.*
FROM products
WHERE category = $1
OFFSET 100
LIMIT {{if .GetMany}} 100 {{else}} 10 {{end}}
`,
inputArgs: map[string]interface{}{
"GetMany": false,
},
expected: `
SELECT
products.*
FROM products
WHERE category = $1
OFFSET 100
LIMIT 10
`,
},
{
desc: "Simple case of truthy substitute from struct",
inputTemplate: `
SELECT
products.*
{{if .IncludeReviews}} ,json_agg(reviews) AS reviews {{end}}
FROM products
{{if .IncludeReviews}} LEFT JOIN reviews ON reviews.product_id = products.id {{end}}
WHERE category = $1
OFFSET 100
LIMIT 10
`,
inputArgs: struct {
IncludeReviews bool
}{
IncludeReviews: true,
},
expected: `
SELECT
products.*
,json_agg(reviews) AS reviews
FROM products
LEFT JOIN reviews ON reviews.product_id = products.id
WHERE category = $1
OFFSET 100
LIMIT 10
`,
},
{
desc: "Recursive falsey expression from struct",
inputTemplate: `
SELECT
products.*
{{if .IncludeReviews}} ,json_agg(reviews) AS reviews
{{if .IncludeCount}} ,count(reviews) AS num_reviews {{end}}
{{end}}
FROM products
{{if .IncludeReviews}} LEFT JOIN reviews ON reviews.product_id = products.id {{end}}
WHERE category = $1
OFFSET 100
LIMIT 10
`,
inputArgs: struct {
IncludeReviews bool
IncludeCount bool
}{
IncludeReviews: true,
IncludeCount: false,
},
expected: `
SELECT
products.*
,json_agg(reviews) AS reviews
FROM products
LEFT JOIN reviews ON reviews.product_id = products.id
WHERE category = $1
OFFSET 100
LIMIT 10
`,
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
result, err := gosq.Execute(c.inputTemplate, c.inputArgs)
if whitespaceNormalized(result) != whitespaceNormalized(c.expected) {
t.Errorf("Expected %s, got %s", c.expected, result)
}
if err != c.expectedError {
t.Errorf("Expected error %s, got %s", c.expectedError, err)
}
})
}
}

func whitespaceNormalized(s string) string {
whitespaceRegex := regexp.MustCompile(`\s+`)
return whitespaceRegex.ReplaceAllString(strings.TrimSpace(s), " ")
}

var benchmarkInputTmpl = `
SELECT
products.*
{{if .IncludeReviews}}
,json_agg(reviews) AS reviews
{{if .IncludeCount}} ,count(reviews) AS num_reviews {{end}}
{{end}}
FROM products
{{if .IncludeReviews}} LEFT JOIN reviews ON reviews.product_id = products.id {{end}}
WHERE category = $1
OFFSET 100
LIMIT 10
`
var benchmarkInputArgs = map[string]interface{}{
"IncludeReviews": true,
"IncludeCount": true,
}

func BenchmarkExecute(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = gosq.Execute(benchmarkInputTmpl, benchmarkInputArgs)
}
}

func BenchmarkCompile(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = gosq.Compile(benchmarkInputTmpl, benchmarkInputArgs)
}
}

0 comments on commit 4cc9234

Please sign in to comment.