Compare · 003

Placeholders, inferred names and Params structs: ClickHouse and DuckDB

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

Configuration

sgl.yamldiffers
ClickHouse
DuckDB
1version: 1
1version: 1
2
2
3databases:
3databases:
4 - name: ferries
4 - name: ferries
5 dialect: clickhouse
5 dialect: duckdb
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_sailings.up.sqldiffers
ClickHouse
DuckDB
1CREATE SEQUENCE sailing_ids;
2
1CREATE TABLE sailings (
3CREATE TABLE sailings (
2 id UInt64,
4 id BIGINT PRIMARY KEY DEFAULT nextval('sailing_ids'),
3 origin String,
5 origin VARCHAR NOT NULL,
4 destination String,
6 destination VARCHAR NOT NULL,
5 departs String,
7 departs VARCHAR NOT NULL,
6 seats_free Int32
8 seats_free INTEGER NOT NULL
7) ENGINE = MergeTree() ORDER BY id;
9);
db/migrations/0001_sailings.down.sqldiffers
ClickHouse
DuckDB
1DROP TABLE sailings;
1DROP TABLE sailings;
2DROP SEQUENCE sailing_ids;

Queries

AddSailingdiffers
ClickHouse · db/queries/sailings.sql
DuckDB · db/queries/sailings.sql
1-- sgl.query(AddSailing, exec)
1-- sgl.query(AddSailing, exec)
2INSERT INTO sailings (id, origin, destination, departs, seats_free)
2INSERT INTO sailings (origin, destination, departs, seats_free) VALUES ($1, $2, $3, $4);
3VALUES ({id:UInt64}, {origin:String}, {destination:String}, {departs:String}, {seats_free:Int32});
FindSailingsdiffers
ClickHouse · db/queries/sailings.sql
DuckDB · db/queries/sailings.sql
1-- sgl.query(FindSailings, many)
1-- sgl.query(FindSailings, many)
2SELECT id, origin, destination, departs, seats_free
2SELECT id, origin, destination, departs, seats_free
3FROM sailings
3FROM sailings
4WHERE origin = {origin:String} AND destination = {destination:String}
4WHERE origin = $1 AND destination = $2
5ORDER BY departs;
5ORDER BY departs;
SailingsDepartingdiffers
ClickHouse · db/queries/sailings.sql
DuckDB · db/queries/sailings.sql
1-- sgl.query(SailingsDeparting, many)
1-- sgl.query(SailingsDeparting, many)
2-- $1 as sgl.param(earliest)
3-- $2 as sgl.param(latest)
2SELECT id, origin, destination, departs, seats_free
4SELECT id, origin, destination, departs, seats_free
3FROM sailings
5FROM sailings
4WHERE departs >= {earliest:String} AND departs < {latest:String}
6WHERE departs >= $1 AND departs < $2
5ORDER BY departs;
7ORDER BY departs;
BookSeatsdiffers
ClickHouse · db/queries/sailings.sql
DuckDB · db/queries/sailings.sql
1-- sgl.query(BookSeats, asyncexec)
1-- sgl.query(BookSeats, execrows)
2ALTER TABLE sailings
2UPDATE sailings
3UPDATE seats_free = seats_free - {seats:Int32}
3SET seats_free = seats_free - :seats
4WHERE id = {sailing_id:UInt64} AND seats_free >= {seats:Int32};
4WHERE id = :sailing_id AND seats_free >= :seats;
SeatsFreeonly ClickHouse
ClickHouse · db/queries/sailings.sql
DuckDB · not in this version
1-- sgl.query(SeatsFree, one)
2SELECT seats_free FROM sailings WHERE id = {sailing_id:UInt64};

What sgl generates

AddSailingdiffers
ClickHouse
DuckDB
1func (queries *Queries) AddSailing(ctx context.Context, params AddSailingParams) error
1func (queries *Queries) AddSailing(ctx context.Context, params AddSailingParams) error
2
2
3type AddSailingParams struct {
3type AddSailingParams struct {
4 ID uint64
5 Origin string
4 Origin string
6 Destination string
5 Destination string
7 Departs string
6 Departs string
8 SeatsFree int32
7 SeatsFree int32
9}
8}
FindSailingsdiffers
ClickHouse
DuckDB
1func (queries *Queries) FindSailings(ctx context.Context, params FindSailingsParams) ([]FindSailingsRow, error)
1func (queries *Queries) FindSailings(ctx context.Context, params FindSailingsParams) ([]FindSailingsRow, error)
2
2
3type FindSailingsParams struct {
3type FindSailingsParams struct {
4 Origin string
4 Origin string
5 Destination string
5 Destination string
6}
6}
7
7
8type FindSailingsRow struct {
8type FindSailingsRow struct {
9 ID uint64 `json:"id"`
9 ID int64 `json:"id"`
10 Origin string `json:"origin"`
10 Origin string `json:"origin"`
11 Destination string `json:"destination"`
11 Destination string `json:"destination"`
12 Departs string `json:"departs"`
12 Departs string `json:"departs"`
13 SeatsFree int32 `json:"seats_free"`
13 SeatsFree int32 `json:"seats_free"`
14}
14}
SailingsDepartingdiffers
ClickHouse
DuckDB
1func (queries *Queries) SailingsDeparting(ctx context.Context, params SailingsDepartingParams) ([]SailingsDepartingRow, error)
1func (queries *Queries) SailingsDeparting(ctx context.Context, params SailingsDepartingParams) ([]SailingsDepartingRow, error)
2
2
3type SailingsDepartingParams struct {
3type SailingsDepartingParams struct {
4 Earliest string
4 Earliest string
5 Latest string
5 Latest string
6}
6}
7
7
8type SailingsDepartingRow struct {
8type SailingsDepartingRow struct {
9 ID uint64 `json:"id"`
9 ID int64 `json:"id"`
10 Origin string `json:"origin"`
10 Origin string `json:"origin"`
11 Destination string `json:"destination"`
11 Destination string `json:"destination"`
12 Departs string `json:"departs"`
12 Departs string `json:"departs"`
13 SeatsFree int32 `json:"seats_free"`
13 SeatsFree int32 `json:"seats_free"`
14}
14}
BookSeatsdiffers
ClickHouse
DuckDB
1func (queries *Queries) BookSeats(ctx context.Context, params BookSeatsParams) error
1func (queries *Queries) BookSeats(ctx context.Context, params BookSeatsParams) (int64, error)
2
2
3type BookSeatsParams struct {
3type BookSeatsParams struct {
4 Seats int32
4 Seats int32
5 SailingID uint64
5 SailingID int64
6}
6}
SeatsFreeonly ClickHouse
ClickHouse
DuckDB · not in this version
1func (queries *Queries) SeatsFree(ctx context.Context, sailingID uint64) (SeatsFreeRow, error)
2
3type SeatsFreeRow struct {
4 SeatsFree int32 `json:"seats_free"`
5}

The program

example.godiffers
ClickHouse
DuckDB
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/clickhouse/003_parameters/internal/store"
9 "seagull.sh/seagull/examples/duckdb/003_parameters/internal/store"
10)
10)
11
11
12// run fills a ferry timetable, searches it two ways and books seats. A ClickHouse
12// run fills a ferry timetable, searches it two ways and books seats. Every query has two
13// placeholder carries its own name and type, as in {origin:String}, so each Params field
13// or more parameters, so each method takes a Params struct, and the struct's field names
14// is named after the placeholder and typed from it: String gives string, Int32 int32 and
14// come from the SQL:
15// UInt64 uint64. A query with two or more parameters takes a Params struct:
16//
17// - AddSailing and FindSailings name their fields ID, Origin, Destination, Departs and
18// SeatsFree from {id:UInt64}, {origin:String} and the rest;
19// - SailingsDeparting compares departs twice, so it names the two bounds itself with
20// {earliest:String} and {latest:String}, and needs no binding line;
21// - BookSeats uses {seats:Int32} twice, but it is one field, bound once.
22//
23// Seagull sends each placeholder as p_<name>, as in {p_seats:Int32}, so a parameter may
24// be called limit or offset without the server reading it as a keyword.
25//
15//
26// ALTER TABLE ... UPDATE reports no changed rows, so BookSeats is asyncexec and the
16// - AddSailing and FindSailings name each $N after the column it is inserted into or
27// booking reads seats_free before and after to see whether the guard let it through.
17// compared with: Origin, Destination, Departs, SeatsFree;
18// - SailingsDeparting compares departs twice, which would give Departs and Departs2, so
19// its binding lines ($1 as sgl.param(earliest)) choose better names;
20// - BookSeats uses named placeholders; :seats appears twice but is one field. sgl
21// numbers each name in the generated SQL, so the driver binds two values, not three.
28//
22//
29// Takes database (*sql.DB) which is a migrated database.
23// Takes database (*sql.DB) which is a migrated database.
30// Takes output (io.Writer) which receives the search results and bookings.
24// Takes output (io.Writer) which receives the search results and bookings.
31//
25//
32// Returns error when a query fails.
26// Returns error when a query fails.
33func run(ctx context.Context, database *sql.DB, output io.Writer) error {
27func run(ctx context.Context, database *sql.DB, output io.Writer) error {
34 queries := store.New(database)
28 queries := store.New(database)
35
29
36 timetable := []store.AddSailingParams{
30 timetable := []store.AddSailingParams{
37 {ID: 1, Origin: "Penzance", Destination: "St Mary's", Departs: "09:15", SeatsFree: 40},
31 {Origin: "Penzance", Destination: "St Mary's", Departs: "09:15", SeatsFree: 40},
38 {ID: 2, Origin: "St Mary's", Destination: "Tresco", Departs: "11:00", SeatsFree: 12},
32 {Origin: "St Mary's", Destination: "Tresco", Departs: "11:00", SeatsFree: 12},
39 {ID: 3, Origin: "Penzance", Destination: "St Mary's", Departs: "14:30", SeatsFree: 4},
33 {Origin: "Penzance", Destination: "St Mary's", Departs: "14:30", SeatsFree: 4},
40 {ID: 4, Origin: "St Mary's", Destination: "Penzance", Departs: "16:30", SeatsFree: 60},
34 {Origin: "St Mary's", Destination: "Penzance", Departs: "16:30", SeatsFree: 60},
41 }
35 }
42 for _, sailing := range timetable {
36 for _, sailing := range timetable {
43 if err := queries.AddSailing(ctx, sailing); err != nil {
37 if err := queries.AddSailing(ctx, sailing); err != nil {
44 return fmt.Errorf("adding the %s sailing: %w", sailing.Departs, err)
38 return fmt.Errorf("adding the %s sailing: %w", sailing.Departs, err)
45 }
39 }
46 }
40 }
47
41
48 route, err := queries.FindSailings(ctx, store.FindSailingsParams{
42 route, err := queries.FindSailings(ctx, store.FindSailingsParams{
49 Origin: "Penzance",
43 Origin: "Penzance",
50 Destination: "St Mary's",
44 Destination: "St Mary's",
51 })
45 })
52 if err != nil {
46 if err != nil {
53 return fmt.Errorf("finding sailings: %w", err)
47 return fmt.Errorf("finding sailings: %w", err)
54 }
48 }
55 fmt.Fprintln(output, "Penzance to St Mary's:")
49 fmt.Fprintln(output, "Penzance to St Mary's:")
56 for _, sailing := range route {
50 for _, sailing := range route {
57 fmt.Fprintf(output, " sailing %d at %s, %d seats free\n", sailing.ID, sailing.Departs, sailing.SeatsFree)
51 fmt.Fprintf(output, " sailing %d at %s, %d seats free\n", sailing.ID, sailing.Departs, sailing.SeatsFree)
58 }
52 }
59
53
60 midday, err := queries.SailingsDeparting(ctx, store.SailingsDepartingParams{
54 midday, err := queries.SailingsDeparting(ctx, store.SailingsDepartingParams{
61 Earliest: "10:00",
55 Earliest: "10:00",
62 Latest: "15:00",
56 Latest: "15:00",
63 })
57 })
64 if err != nil {
58 if err != nil {
65 return fmt.Errorf("finding midday sailings: %w", err)
59 return fmt.Errorf("finding midday sailings: %w", err)
66 }
60 }
67 fmt.Fprintln(output, "Departing between 10:00 and 15:00:")
61 fmt.Fprintln(output, "Departing between 10:00 and 15:00:")
68 for _, sailing := range midday {
62 for _, sailing := range midday {
69 fmt.Fprintf(output, " sailing %d at %s, %s to %s\n", sailing.ID, sailing.Departs, sailing.Origin, sailing.Destination)
63 fmt.Fprintf(output, " sailing %d at %s, %s to %s\n", sailing.ID, sailing.Departs, sailing.Origin, sailing.Destination)
70 }
64 }
71
65
72 // The same booking twice: the first takes 3 of the 4 free seats, so the second changes
66 // The same booking twice: the first takes 3 of the 4 free seats, so the second changes
73 // nothing because seats_free >= {seats:Int32} is now false.
67 // no row because seats_free >= :seats is now false.
74 booking := store.BookSeatsParams{Seats: 3, SailingID: 3}
68 booking := store.BookSeatsParams{Seats: 3, SailingID: 3}
75 for range 2 {
69 for range 2 {
76 booked, err := bookSeats(ctx, queries, booking)
70 booked, err := queries.BookSeats(ctx, booking)
77 if err != nil {
71 if err != nil {
78 return err
72 return fmt.Errorf("booking sailing %d: %w", booking.SailingID, err)
79 }
73 }
80 if !booked {
74 if booked == 0 {
81 fmt.Fprintf(output, "sailing %d has fewer than %d seats free\n", booking.SailingID, booking.Seats)
75 fmt.Fprintf(output, "sailing %d has fewer than %d seats free\n", booking.SailingID, booking.Seats)
82 continue
76 continue
83 }
77 }
84 fmt.Fprintf(output, "booked %d seats on sailing %d\n", booking.Seats, booking.SailingID)
78 fmt.Fprintf(output, "booked %d seats on sailing %d\n", booking.Seats, booking.SailingID)
85 }
79 }
86 return nil
80 return nil
87}
81}
88
89// bookSeats runs BookSeats and reports whether it took the seats, by comparing seats_free
90// before and after. The connection sets mutations_sync=2, so the mutation has finished
91// when BookSeats returns.
92//
93// Takes queries (*store.Queries) which runs the queries.
94// Takes booking (store.BookSeatsParams) which is the sailing and the number of seats.
95//
96// Returns bool which is true when the seats were booked.
97// Returns error when a query fails.
98func bookSeats(ctx context.Context, queries *store.Queries, booking store.BookSeatsParams) (bool, error) {
99 before, err := queries.SeatsFree(ctx, booking.SailingID)
100 if err != nil {
101 return false, fmt.Errorf("reading seats on sailing %d: %w", booking.SailingID, err)
102 }
103 if err := queries.BookSeats(ctx, booking); err != nil {
104 return false, fmt.Errorf("booking sailing %d: %w", booking.SailingID, err)
105 }
106 after, err := queries.SeatsFree(ctx, booking.SailingID)
107 if err != nil {
108 return false, fmt.Errorf("reading seats on sailing %d: %w", booking.SailingID, err)
109 }
110 return after.SeatsFree != before.SeatsFree, nil
111}

Go type differences

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

QueryFieldClickHouseDuckDB
FindSailingscolumn iduint64int64
SailingsDepartingcolumn iduint64int64
BookSeatsparameter sailing_iduint64int64