Compare · 010

IN lists from Go slices: SQLite and ClickHouse

Every file of the SQLite and ClickHouse versions side by side, with the lines that differ marked. 4 parameter or column types differ in Go.

Configuration

sgl.yamldiffers
SQLite
ClickHouse
1version: 1
1version: 1
2
2
3databases:
3databases:
4 - name: huts
4 - name: huts
5 dialect: sqlite
5 dialect: clickhouse
6 migrations: db/migrations
6 migrations: db/migrations
7 queries: db/queries
7 queries: db/queries
8 go:
8 go:
9 package: store
9 package: store
10 out: internal/store
10 out: internal/store

Migrations

db/migrations/0001_huts.up.sqldiffers
SQLite
ClickHouse
1CREATE TABLE huts (
1CREATE TABLE huts (
2 number INTEGER PRIMARY KEY,
2 number UInt64,
3 beach TEXT NOT NULL,
3 beach String,
4 colour TEXT NOT NULL,
4 colour String,
5 booked BOOLEAN NOT NULL DEFAULT 0
5 booked Bool DEFAULT false
6);
6) ENGINE = MergeTree() ORDER BY number;
db/migrations/0001_huts.down.sqlidentical
SQLite
ClickHouse
1DROP TABLE huts;
1DROP TABLE huts;

Queries

AddHutdiffers
SQLite · db/queries/huts.sql
ClickHouse · db/queries/huts.sql
1-- sgl.query(AddHut, exec)
1-- sgl.query(AddHut, exec)
2INSERT INTO huts (number, beach, colour) VALUES (?, ?, ?);
2INSERT INTO huts (number, beach, colour) VALUES ({number:UInt64}, {beach:String}, {colour:String});
HutsOnBeachesdiffers
SQLite · db/queries/huts.sql
ClickHouse · db/queries/huts.sql
1-- sgl.query(HutsOnBeaches, many)
1-- sgl.query(HutsOnBeaches, many)
2-- ?1 as sgl.param(beaches, kind: slice)
3SELECT number, beach, colour, booked
2SELECT number, beach, colour, booked
4FROM huts
3FROM huts
5WHERE beach IN (?1)
4WHERE beach IN {beaches:Array(String)}
6ORDER BY number;
5ORDER BY number;
BookHutsdiffers
SQLite · db/queries/huts.sql
ClickHouse · db/queries/huts.sql
1-- sgl.query(BookHuts, execrows)
1-- sgl.query(BookHuts, asyncexec)
2-- ?1 as sgl.param(numbers, kind: slice)
2ALTER TABLE huts UPDATE booked = true WHERE number IN {numbers:Array(UInt64)} AND NOT booked;
3UPDATE huts SET booked = 1 WHERE number IN (?1) AND booked = 0;
FreeHutsInColoursdiffers
SQLite · db/queries/huts.sql
ClickHouse · db/queries/huts.sql
1-- sgl.query(FreeHutsInColours, many)
1-- sgl.query(FreeHutsInColours, many)
2-- ?1 as sgl.param(beach)
3-- ?2 as sgl.param(colours, kind: slice)
4SELECT number, colour
2SELECT number, colour
5FROM huts
3FROM huts
6WHERE beach = ?1 AND colour IN (?2) AND booked = 0
4WHERE beach = {beach:String} AND colour IN {colours:Array(String)} AND NOT booked
7ORDER BY number;
5ORDER BY number;
CountBookedonly ClickHouse
SQLite · not in this version
ClickHouse · db/queries/huts.sql
1-- sgl.query(CountBooked, one)
2SELECT count() AS huts FROM huts WHERE booked;

What sgl generates

AddHutdiffers
SQLite
ClickHouse
1func (queries *Queries) AddHut(ctx context.Context, params AddHutParams) error
1func (queries *Queries) AddHut(ctx context.Context, params AddHutParams) error
2
2
3type AddHutParams struct {
3type AddHutParams struct {
4 Number int64
4 Number uint64
5 Beach string
5 Beach string
6 Colour string
6 Colour string
7}
7}
HutsOnBeachesdiffers
SQLite
ClickHouse
1func (queries *Queries) HutsOnBeaches(ctx context.Context, params HutsOnBeachesParams) ([]HutsOnBeachesRow, error)
1func (queries *Queries) HutsOnBeaches(ctx context.Context, beaches []string) ([]HutsOnBeachesRow, error)
2
3type HutsOnBeachesParams struct {
4 Beaches []string
5}
6
2
7type HutsOnBeachesRow struct {
3type HutsOnBeachesRow struct {
8 Number int64 `json:"number"`
4 Number uint64 `json:"number"`
9 Beach string `json:"beach"`
5 Beach string `json:"beach"`
10 Colour string `json:"colour"`
6 Colour string `json:"colour"`
11 Booked bool `json:"booked"`
7 Booked bool `json:"booked"`
12}
8}
BookHutsdiffers
SQLite
ClickHouse
1func (queries *Queries) BookHuts(ctx context.Context, params BookHutsParams) (int64, error)
1func (queries *Queries) BookHuts(ctx context.Context, numbers []uint64) error
2
3type BookHutsParams struct {
4 Numbers []int64
5}
FreeHutsInColoursdiffers
SQLite
ClickHouse
1func (queries *Queries) FreeHutsInColours(ctx context.Context, params FreeHutsInColoursParams) ([]FreeHutsInColoursRow, error)
1func (queries *Queries) FreeHutsInColours(ctx context.Context, params FreeHutsInColoursParams) ([]FreeHutsInColoursRow, error)
2
2
3type FreeHutsInColoursParams struct {
3type FreeHutsInColoursParams struct {
4 Beach string
4 Beach string
5 Colours []string
5 Colours []string
6}
6}
7
7
8type FreeHutsInColoursRow struct {
8type FreeHutsInColoursRow struct {
9 Number int64 `json:"number"`
9 Number uint64 `json:"number"`
10 Colour string `json:"colour"`
10 Colour string `json:"colour"`
11}
11}
CountBookedonly ClickHouse
SQLite · not in this version
ClickHouse
1func (queries *Queries) CountBooked(ctx context.Context) (CountBookedRow, error)
2
3type CountBookedRow struct {
4 Huts uint64 `json:"huts"`
5}

The program

example.godiffers
SQLite
ClickHouse
1package main
1package main
2
2
3import (
3import (
4 "context"
4 "context"
5 "database/sql"
5 "database/sql"
6 "fmt"
6 "fmt"
7 "io"
7 "io"
8
8
9 "seagull.sh/seagull/examples/sqlite/010_slices/internal/store"
9 "seagull.sh/seagull/examples/clickhouse/010_slices/internal/store"
10)
10)
11
11
12// run lets out beach huts by passing Go slices where the SQL has an IN list. A binding
12// run lets out beach huts by passing Go slices where the SQL has an IN list. ClickHouse
13// such as ?1 as sgl.param(beaches, kind: slice) makes the parameter a slice, and the
13// takes a whole array as one parameter, so the query says IN {beaches:Array(String)} and
14// generated method rewrites IN (?1) to one placeholder per element at call time. A slice
14// the method takes a []string; no kind: slice binding is needed and the SQL is not
15// is numbered after every other parameter, so no other placeholder moves. An empty slice
15// rewritten at call time. The slice is sent as one array value however long it is, and an
16// turns IN (?1) into IN (SELECT NULL WHERE 0), which matches nothing.
16// empty slice is an empty array, which matches nothing.
17//
17//
18// The rewrite happens before the statement runs, so sgl vet refuses a slice parameter on
18// Other engines expand IN (?1) into one placeholder per element, which is why sgl vet
19// a batch or copyfrom query (Q017) or a dynamic: runtime query (Q018).
19// refuses a kind: slice parameter on a batch query (Q017) or a dynamic: runtime query
20// (Q018); an Array parameter is a single value, so neither applies here.
20//
21//
22// ALTER TABLE ... UPDATE reports no changed rows, so BookHuts is asyncexec and the
23// example counts the booked huts before and after each booking.
24//
21// Takes database (*sql.DB) which is a migrated database.
25// Takes database (*sql.DB) which is a migrated database.
22// Takes output (io.Writer) which receives each search and booking.
26// Takes output (io.Writer) which receives each search and booking.
23//
27//
24// Returns error when a query fails.
28// Returns error when a query fails.
25func run(ctx context.Context, database *sql.DB, output io.Writer) error {
29func run(ctx context.Context, database *sql.DB, output io.Writer) error {
26 queries := store.New(database)
30 queries := store.New(database)
27
31
28 huts := []store.AddHutParams{
32 huts := []store.AddHutParams{
29 {Number: 1, Beach: "Polzeath", Colour: "blue"},
33 {Number: 1, Beach: "Polzeath", Colour: "blue"},
30 {Number: 2, Beach: "Polzeath", Colour: "yellow"},
34 {Number: 2, Beach: "Polzeath", Colour: "yellow"},
31 {Number: 3, Beach: "Polzeath", Colour: "red"},
35 {Number: 3, Beach: "Polzeath", Colour: "red"},
32 {Number: 4, Beach: "Watergate", Colour: "blue"},
36 {Number: 4, Beach: "Watergate", Colour: "blue"},
33 {Number: 5, Beach: "Watergate", Colour: "red"},
37 {Number: 5, Beach: "Watergate", Colour: "red"},
34 {Number: 6, Beach: "Fistral", Colour: "yellow"},
38 {Number: 6, Beach: "Fistral", Colour: "yellow"},
35 }
39 }
36 for _, hut := range huts {
40 for _, hut := range huts {
37 if err := queries.AddHut(ctx, hut); err != nil {
41 if err := queries.AddHut(ctx, hut); err != nil {
38 return fmt.Errorf("adding hut %d: %w", hut.Number, err)
42 return fmt.Errorf("adding hut %d: %w", hut.Number, err)
39 }
43 }
40 }
44 }
41
45
42 for _, beaches := range [][]string{{"Polzeath", "Fistral"}, {"Watergate"}, {}} {
46 for _, beaches := range [][]string{{"Polzeath", "Fistral"}, {"Watergate"}, {}} {
43 found, err := queries.HutsOnBeaches(ctx, store.HutsOnBeachesParams{Beaches: beaches})
47 found, err := queries.HutsOnBeaches(ctx, beaches)
44 if err != nil {
48 if err != nil {
45 return fmt.Errorf("finding huts on %v: %w", beaches, err)
49 return fmt.Errorf("finding huts on %v: %w", beaches, err)
46 }
50 }
47 numbers := make([]int64, 0, len(found))
51 numbers := make([]uint64, 0, len(found))
48 for _, hut := range found {
52 for _, hut := range found {
49 numbers = append(numbers, hut.Number)
53 numbers = append(numbers, hut.Number)
50 }
54 }
51 fmt.Fprintf(output, "huts on %v: %v\n", beaches, numbers)
55 fmt.Fprintf(output, "huts on %v: %v\n", beaches, numbers)
52 }
56 }
53
57
54 // Hut 3 is taken by the first booking, so the second books only hut 5.
58 // Hut 3 is taken by the first booking, so the second books only hut 5.
55 for _, numbers := range [][]int64{{1, 3, 4}, {3, 5}} {
59 for _, numbers := range [][]uint64{{1, 3, 4}, {3, 5}} {
56 booked, err := queries.BookHuts(ctx, store.BookHutsParams{Numbers: numbers})
60 booked, err := bookHuts(ctx, queries, numbers)
57 if err != nil {
61 if err != nil {
58 return fmt.Errorf("booking huts %v: %w", numbers, err)
62 return err
59 }
63 }
60 fmt.Fprintf(output, "booking huts %v booked %d\n", numbers, booked)
64 fmt.Fprintf(output, "booking huts %v booked %d\n", numbers, booked)
61 }
65 }
62
66
63 free, err := queries.FreeHutsInColours(ctx, store.FreeHutsInColoursParams{
67 free, err := queries.FreeHutsInColours(ctx, store.FreeHutsInColoursParams{
64 Beach: "Polzeath",
68 Beach: "Polzeath",
65 Colours: []string{"blue", "yellow"},
69 Colours: []string{"blue", "yellow"},
66 })
70 })
67 if err != nil {
71 if err != nil {
68 return fmt.Errorf("finding free huts: %w", err)
72 return fmt.Errorf("finding free huts: %w", err)
69 }
73 }
70 for _, hut := range free {
74 for _, hut := range free {
71 fmt.Fprintf(output, "hut %d at Polzeath is %s and free\n", hut.Number, hut.Colour)
75 fmt.Fprintf(output, "hut %d at Polzeath is %s and free\n", hut.Number, hut.Colour)
72 }
76 }
73 return nil
77 return nil
74}
78}
79
80// bookHuts books the huts that are free among numbers and returns how many it booked, by
81// counting the booked huts before and after. The connection sets mutations_sync=2, so the
82// mutation has finished when BookHuts returns.
83//
84// Takes queries (*store.Queries) which runs the queries.
85// Takes numbers ([]uint64) which are the huts to book.
86//
87// Returns uint64 which is how many huts were booked.
88// Returns error when a query fails.
89func bookHuts(ctx context.Context, queries *store.Queries, numbers []uint64) (uint64, error) {
90 before, err := queries.CountBooked(ctx)
91 if err != nil {
92 return 0, fmt.Errorf("counting booked huts: %w", err)
93 }
94 if err := queries.BookHuts(ctx, numbers); err != nil {
95 return 0, fmt.Errorf("booking huts %v: %w", numbers, err)
96 }
97 after, err := queries.CountBooked(ctx)
98 if err != nil {
99 return 0, fmt.Errorf("counting booked huts: %w", err)
100 }
101 return after.Huts - before.Huts, nil
102}

Go type differences

The same SQL types map to different Go types, because each database reports them differently.

QueryFieldSQLiteClickHouse
AddHutparameter numberint64uint64
HutsOnBeachescolumn numberint64uint64
BookHutsparameter numbers[]int64[]uint64
FreeHutsInColourscolumn numberint64uint64