Skip to content

Commit edaa689

Browse files
feat: add mysql support and fix some sqlite bugs
1 parent ac98981 commit edaa689

10 files changed

Lines changed: 714 additions & 6 deletions

File tree

examples/dynamicquery/mysql/db.go

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
//go:build examples
2+
3+
package dynamicquerymysql
4+
5+
import (
6+
"context"
7+
"database/sql"
8+
"testing"
9+
10+
_ "github.com/go-sql-driver/mysql"
11+
12+
"github.com/sqlc-dev/sqlc/internal/sqltest/local"
13+
)
14+
15+
func seedContacts(t *testing.T, ctx context.Context, q *Queries) {
16+
t.Helper()
17+
seed := []CreateRecordParams{
18+
{TenantID: 1, Name: "alice", Age: 30, Status: "active"},
19+
{TenantID: 1, Name: "bob", Age: 20, Status: "inactive"},
20+
{TenantID: 1, Name: "carol", Age: 40, Status: "active"},
21+
{TenantID: 2, Name: "dave", Age: 99, Status: "active"},
22+
}
23+
for _, s := range seed {
24+
if err := q.CreateRecord(ctx, s); err != nil {
25+
t.Fatal(err)
26+
}
27+
}
28+
}
29+
30+
func newDB(t *testing.T) *sql.DB {
31+
t.Helper()
32+
uri := local.MySQL(t, []string{"schema.sql"})
33+
sdb, err := sql.Open("mysql", uri)
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
t.Cleanup(func() { sdb.Close() })
38+
return sdb
39+
}
40+
41+
func TestListRecordsMySQL(t *testing.T) {
42+
ctx := context.Background()
43+
sdb := newDB(t)
44+
q := New(sdb)
45+
seedContacts(t, ctx, q)
46+
47+
t.Run("no_filters_returns_all_tenant_rows", func(t *testing.T) {
48+
got, err := q.ListRecords(ctx, 1, ListRecordsOpts{})
49+
if err != nil {
50+
t.Fatal(err)
51+
}
52+
if len(got) != 3 {
53+
t.Fatalf("want 3 rows, got %d", len(got))
54+
}
55+
})
56+
57+
t.Run("age_gt_uses_question_mark_placeholder", func(t *testing.T) {
58+
// alice(30), carol(40); bob(20) excluded.
59+
got, err := q.ListRecords(ctx, 1, ListRecordsOpts{}.Age(25))
60+
if err != nil {
61+
t.Fatal(err)
62+
}
63+
if len(got) != 2 {
64+
t.Fatalf("want 2 rows, got %d", len(got))
65+
}
66+
})
67+
68+
t.Run("combined_predicates", func(t *testing.T) {
69+
got, err := q.ListRecords(ctx, 1, ListRecordsOpts{}.Name("carol").Age(25))
70+
if err != nil {
71+
t.Fatal(err)
72+
}
73+
if len(got) != 1 || got[0].Name != "carol" {
74+
t.Fatalf("want [carol], got %+v", got)
75+
}
76+
})
77+
78+
t.Run("order_by_age_desc", func(t *testing.T) {
79+
got, err := q.ListRecords(ctx, 1, ListRecordsOpts{}.OrderBy(ListRecordsOrderByAge, true))
80+
if err != nil {
81+
t.Fatal(err)
82+
}
83+
if len(got) != 3 || got[0].Name != "carol" {
84+
t.Fatalf("want carol first (desc), got %+v", got)
85+
}
86+
})
87+
}
88+
89+
func TestSearchContactsMySQL(t *testing.T) {
90+
ctx := context.Background()
91+
sdb := newDB(t)
92+
q := New(sdb)
93+
seedContacts(t, ctx, q)
94+
95+
assertNames := func(t *testing.T, rows []SearchContactsRow, want ...string) {
96+
t.Helper()
97+
got := map[string]bool{}
98+
for _, r := range rows {
99+
got[r.Name] = true
100+
}
101+
if len(got) != len(want) {
102+
t.Fatalf("got %d rows %v, want %v", len(rows), got, want)
103+
}
104+
for _, w := range want {
105+
if !got[w] {
106+
t.Fatalf("missing %q in %v", w, got)
107+
}
108+
}
109+
}
110+
111+
t.Run("no_filters_returns_all_tenant_rows", func(t *testing.T) {
112+
got, err := q.SearchContacts(ctx, 1, SearchContactsOpts{})
113+
if err != nil {
114+
t.Fatal(err)
115+
}
116+
assertNames(t, got, "alice", "bob", "carol")
117+
})
118+
119+
t.Run("both_set_is_a_disjunction", func(t *testing.T) {
120+
// (name = alice OR status = inactive): alice by name, bob by status.
121+
// An AND would return zero rows, so two rows proves the OR grouping.
122+
got, err := q.SearchContacts(ctx, 1, SearchContactsOpts{}.Name("alice").Status("inactive"))
123+
if err != nil {
124+
t.Fatal(err)
125+
}
126+
assertNames(t, got, "alice", "bob")
127+
})
128+
}
129+
130+
func TestExcludeContactsMySQL(t *testing.T) {
131+
ctx := context.Background()
132+
sdb := newDB(t)
133+
q := New(sdb)
134+
seedContacts(t, ctx, q)
135+
136+
assertNames := func(t *testing.T, rows []ExcludeContactsRow, want ...string) {
137+
t.Helper()
138+
got := map[string]bool{}
139+
for _, r := range rows {
140+
got[r.Name] = true
141+
}
142+
if len(got) != len(want) {
143+
t.Fatalf("got %d rows %v, want %v", len(rows), got, want)
144+
}
145+
for _, w := range want {
146+
if !got[w] {
147+
t.Fatalf("missing %q in %v", w, got)
148+
}
149+
}
150+
}
151+
152+
t.Run("no_filters_omits_the_negated_group", func(t *testing.T) {
153+
got, err := q.ExcludeContacts(ctx, 1, ExcludeContactsOpts{})
154+
if err != nil {
155+
t.Fatal(err)
156+
}
157+
assertNames(t, got, "alice", "bob", "carol")
158+
})
159+
160+
t.Run("negated_disjunction_is_de_morgan", func(t *testing.T) {
161+
// NOT (name = alice OR status = active) => name != alice AND status != active => bob.
162+
got, err := q.ExcludeContacts(ctx, 1, ExcludeContactsOpts{}.Name("alice").Status("active"))
163+
if err != nil {
164+
t.Fatal(err)
165+
}
166+
assertNames(t, got, "bob")
167+
})
168+
}
169+
170+
func TestFilterRecordsMySQL(t *testing.T) {
171+
ctx := context.Background()
172+
sdb := newDB(t)
173+
q := New(sdb)
174+
seedContacts(t, ctx, q)
175+
176+
idByName := func(name string) int64 {
177+
var id int64
178+
if err := sdb.QueryRowContext(ctx,
179+
"SELECT id FROM records WHERE name = ? AND tenant_id = 1", name).Scan(&id); err != nil {
180+
t.Fatal(err)
181+
}
182+
return id
183+
}
184+
185+
t.Run("in_with_two_ids", func(t *testing.T) {
186+
got, err := q.FilterRecords(ctx, 1, FilterRecordsOpts{}.Ids([]int64{idByName("alice"), idByName("carol")}))
187+
if err != nil {
188+
t.Fatal(err)
189+
}
190+
if len(got) != 2 {
191+
t.Fatalf("want 2 rows, got %d", len(got))
192+
}
193+
})
194+
195+
t.Run("empty_slice_applies_no_in_filter", func(t *testing.T) {
196+
got, err := q.FilterRecords(ctx, 1, FilterRecordsOpts{})
197+
if err != nil {
198+
t.Fatal(err)
199+
}
200+
if len(got) != 3 {
201+
t.Fatalf("want all 3 rows, got %d", len(got))
202+
}
203+
})
204+
}

examples/dynamicquery/mysql/models.go

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
-- name: ListRecords :dynamicmany
2+
-- @dynamic name
3+
-- @dynamic age
4+
-- @dynamic-sort name, age, created_at
5+
SELECT id, name, age, created_at FROM records
6+
WHERE tenant_id = sqlc.arg(tenant_id)
7+
AND name = sqlc.arg(name)
8+
AND age > sqlc.arg(age);
9+
10+
-- name: SearchContacts :dynamicmany
11+
-- @dynamic name
12+
-- @dynamic status
13+
SELECT id, name, age, status, created_at FROM records
14+
WHERE tenant_id = sqlc.arg(tenant_id)
15+
AND (name = sqlc.arg(name) OR status = sqlc.arg(status));
16+
17+
-- name: ExcludeContacts :dynamicmany
18+
-- @dynamic name
19+
-- @dynamic status
20+
SELECT id, name, age, status, created_at FROM records
21+
WHERE tenant_id = sqlc.arg(tenant_id)
22+
AND NOT (name = sqlc.arg(name) OR status = sqlc.arg(status));
23+
24+
-- name: FilterRecords :dynamicmany
25+
-- @dynamic ids
26+
SELECT id, name, age, created_at FROM records
27+
WHERE tenant_id = sqlc.arg(tenant_id)
28+
AND id IN (sqlc.slice(ids));
29+
30+
-- name: CreateRecord :exec
31+
INSERT INTO records (tenant_id, name, age, status)
32+
VALUES (sqlc.arg(tenant_id), sqlc.arg(name), sqlc.arg(age), sqlc.arg(status));

0 commit comments

Comments
 (0)