Skip to content

Commit 3746ef0

Browse files
committed
feat: implement prototype
0 parents  commit 3746ef0

File tree

10 files changed

+854
-0
lines changed

10 files changed

+854
-0
lines changed

LICENSE

Lines changed: 373 additions & 0 deletions
Large diffs are not rendered by default.

Makefile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
.POSIX:
2+
.SUFFIXES:
3+
4+
all: test lint
5+
6+
test:
7+
go test -race -shuffle=on -cover ./...
8+
9+
test/cover:
10+
go test -race -shuffle=on -coverprofile=coverage.out ./...
11+
go tool cover -html=coverage.out
12+
13+
lint:
14+
golangci-lint run
15+
16+
tidy:
17+
go mod tidy
18+
19+
generate:
20+
go generate ./...
21+
22+
# run `make pre-commit` once to install the hook.
23+
pre-commit: .git/hooks/pre-commit test lint tidy generate
24+
git diff --exit-code
25+
26+
.git/hooks/pre-commit:
27+
echo "make pre-commit" > .git/hooks/pre-commit
28+
chmod +x .git/hooks/pre-commit

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# queries
2+
3+
[WIP] Convenience helpers for working with SQL queries.
4+
5+
## Features
6+
7+
- `Builder`: an `fmt`-based query builder with an API similar to `strings.Builder`.
8+
- `Scanner`: a query-to-struct scanner, a lightweight version of `sqlx` with a smaller and stricter API.
9+
10+
## Usage
11+
12+
See [examples](example_test.go).

assert/EF/alias.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assert/assert.go

Lines changed: 94 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

builder.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package queries
2+
3+
import (
4+
"fmt"
5+
"slices"
6+
"strings"
7+
)
8+
9+
type Builder struct {
10+
query strings.Builder
11+
Args []any
12+
counter int
13+
placeholders []rune
14+
}
15+
16+
func (b *Builder) Appendf(format string, args ...any) {
17+
a := make([]any, len(args))
18+
for i, arg := range args {
19+
a[i] = argument{value: arg, builder: b}
20+
}
21+
fmt.Fprintf(&b.query, format, a...)
22+
}
23+
24+
func (b *Builder) String() string {
25+
slices.Sort(b.placeholders)
26+
if len(slices.Compact(b.placeholders)) > 1 {
27+
panic(fmt.Sprintf("queries.Builder: bad query: %s placeholders used", string(b.placeholders)))
28+
}
29+
30+
s := b.query.String()
31+
if strings.Contains(s, "%!") {
32+
// fmt silently recovers panics and writes them to the output.
33+
// we want panics to be loud, so we find and rethrow them.
34+
// see also https://github.com/golang/go/issues/28150.
35+
panic(fmt.Sprintf("queries.Builder: bad query: %s", s))
36+
}
37+
38+
return s
39+
}
40+
41+
type argument struct {
42+
value any
43+
builder *Builder
44+
}
45+
46+
// Format implements the [fmt.Formatter] interface.
47+
func (a argument) Format(s fmt.State, verb rune) {
48+
switch verb {
49+
case '?', '$', '@':
50+
a.builder.Args = append(a.builder.Args, a.value)
51+
a.builder.placeholders = append(a.builder.placeholders, verb)
52+
}
53+
54+
switch verb {
55+
case '?': // MySQL, SQLite
56+
fmt.Fprint(s, "?")
57+
case '$': // PostgreSQL
58+
a.builder.counter++
59+
fmt.Fprintf(s, "$%d", a.builder.counter)
60+
case '@': // MSSQL
61+
a.builder.counter++
62+
fmt.Fprintf(s, "@p%d", a.builder.counter)
63+
default:
64+
format := fmt.FormatString(s, verb)
65+
fmt.Fprintf(s, format, a.value)
66+
}
67+
}

builder_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package queries_test
2+
3+
import (
4+
"testing"
5+
6+
"go-simpler.org/queries"
7+
"go-simpler.org/queries/assert"
8+
. "go-simpler.org/queries/assert/EF"
9+
)
10+
11+
//go:generate go run -tags=cp go-simpler.org/assert/cmd/cp@v0.9.0
12+
13+
func TestBuilder(t *testing.T) {
14+
var qb queries.Builder
15+
qb.Appendf("select %s from tbl where 1=1", "*")
16+
qb.Appendf(" and foo = %$", 1)
17+
qb.Appendf(" and bar = %$", 2)
18+
qb.Appendf(" and baz = %$", 3)
19+
20+
assert.Equal[E](t, qb.String(), "select * from tbl where 1=1 and foo = $1 and bar = $2 and baz = $3")
21+
assert.Equal[E](t, qb.Args, []any{1, 2, 3})
22+
}
23+
24+
func TestBuilder_placeholders(t *testing.T) {
25+
tests := map[string]struct {
26+
format string
27+
query string
28+
}{
29+
"?": {
30+
format: "select * from tbl where foo = %? and bar = %? and baz = %?",
31+
query: "select * from tbl where foo = ? and bar = ? and baz = ?",
32+
},
33+
"$": {
34+
format: "select * from tbl where foo = %$ and bar = %$ and baz = %$",
35+
query: "select * from tbl where foo = $1 and bar = $2 and baz = $3",
36+
},
37+
"@": {
38+
format: "select * from tbl where foo = %@ and bar = %@ and baz = %@",
39+
query: "select * from tbl where foo = @p1 and bar = @p2 and baz = @p3",
40+
},
41+
}
42+
43+
for name, tt := range tests {
44+
t.Run(name, func(t *testing.T) {
45+
var qb queries.Builder
46+
qb.Appendf(tt.format, 1, 2, 3)
47+
assert.Equal[E](t, qb.String(), tt.query)
48+
assert.Equal[E](t, qb.Args, []any{1, 2, 3})
49+
})
50+
}
51+
}
52+
53+
func TestBuilder_badQuery(t *testing.T) {
54+
tests := map[string]struct {
55+
appends func(*queries.Builder)
56+
panicMsg string
57+
}{
58+
"bad verb": {
59+
appends: func(qb *queries.Builder) {
60+
qb.Appendf("select %d from tbl", "foo")
61+
},
62+
panicMsg: "queries.Builder: bad query: select %!d(string=foo) from tbl",
63+
},
64+
"too few arguments": {
65+
appends: func(qb *queries.Builder) {
66+
qb.Appendf("select %s from tbl")
67+
},
68+
panicMsg: "queries.Builder: bad query: select %!s(MISSING) from tbl",
69+
},
70+
"too many arguments": {
71+
appends: func(qb *queries.Builder) {
72+
qb.Appendf("select %s from tbl", "foo", "bar")
73+
},
74+
panicMsg: "queries.Builder: bad query: select foo from tbl%!(EXTRA queries.argument=bar)",
75+
},
76+
"different placeholders": {
77+
appends: func(qb *queries.Builder) {
78+
qb.Appendf("select * from tbl where foo = %? and bar = %$ and baz = %@", 1, 2, 3)
79+
},
80+
panicMsg: "queries.Builder: bad query: $?@ placeholders used",
81+
},
82+
}
83+
84+
for name, tt := range tests {
85+
t.Run(name, func(t *testing.T) {
86+
var qb queries.Builder
87+
tt.appends(&qb)
88+
assert.Panics[E](t, func() { _ = qb.String() }, tt.panicMsg)
89+
})
90+
}
91+
}

example_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// nolint (WIP)
2+
package queries_test
3+
4+
import (
5+
"context"
6+
"database/sql"
7+
"fmt"
8+
"os"
9+
"os/signal"
10+
"strings"
11+
"time"
12+
13+
"go-simpler.org/queries"
14+
// <database driver of your choice>
15+
)
16+
17+
func main() {
18+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
19+
defer cancel()
20+
21+
if err := run(ctx); err != nil {
22+
fmt.Println(err)
23+
os.Exit(1)
24+
}
25+
}
26+
27+
func run(ctx context.Context) error {
28+
db, err := sql.Open("<driver name>", "<connection string>")
29+
if err != nil {
30+
return err
31+
}
32+
33+
columns := []string{"first_name", "last_name"}
34+
if true {
35+
columns = append(columns, "created_at")
36+
}
37+
38+
var qb queries.Builder
39+
qb.Appendf("select %s from users", strings.Join(columns, ", "))
40+
if true {
41+
qb.Appendf(" where created_at >= %$", time.Date(2024, time.January, 1, 0, 0, 0, 0, time.Local))
42+
}
43+
44+
// select first_name, last_name, created_at from users where created_at >= $1
45+
rows, err := db.QueryContext(ctx, qb.String(), qb.Args...)
46+
if err != nil {
47+
return err
48+
}
49+
defer rows.Close()
50+
51+
var users []struct {
52+
FirstName string `sql:"first_name"`
53+
LastName string `sql:"last_name"`
54+
CreatedAt time.Time `sql:"created_at"`
55+
}
56+
if err := queries.ScanAll(&users, rows); err != nil {
57+
return err
58+
}
59+
60+
fmt.Println(users)
61+
return nil
62+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module go-simpler.org/queries
2+
3+
go 1.18

0 commit comments

Comments
 (0)