Placeholders, inferred names and Params structs: PostgreSQL and ClickHouse
Every file of the PostgreSQL and ClickHouse versions side by side, with the lines that differ marked. 7 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
PostgreSQL
ClickHouse
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: ferries4
- name: ferries5
dialect: postgres5
dialect: clickhouse6
migrations: db/migrations6
migrations: db/migrations7
queries: db/queries7
queries: db/queries8
go:8
go:9
package: store9
package: store10
out: internal/store10
out: internal/store11
driver: database/sqlMigrations
db/migrations/0001_sailings.up.sqldiffers
PostgreSQL
ClickHouse
1
CREATE TABLE sailings (1
CREATE TABLE sailings (2
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,2
id UInt64,3
origin TEXT NOT NULL,3
origin String,4
destination TEXT NOT NULL,4
destination String,5
departs TEXT NOT NULL,5
departs String,6
seats_free BIGINT NOT NULL6
seats_free Int327
);7
) ENGINE = MergeTree() ORDER BY id;db/migrations/0001_sailings.down.sqlidentical
PostgreSQL
ClickHouse
1
DROP TABLE sailings;1
DROP TABLE sailings;Queries
AddSailingdiffers
PostgreSQL · db/queries/sailings.sql
ClickHouse · db/queries/sailings.sql
1
-- sgl.query(AddSailing, exec)1
-- sgl.query(AddSailing, exec)2
INSERT INTO sailings (origin, destination, departs, seats_free) VALUES ($1, $2, $3, $4);2
INSERT INTO sailings (id, origin, destination, departs, seats_free)3
VALUES ({id:UInt64}, {origin:String}, {destination:String}, {departs:String}, {seats_free:Int32});FindSailingsdiffers
PostgreSQL · db/queries/sailings.sql
ClickHouse · db/queries/sailings.sql
1
-- sgl.query(FindSailings, many)1
-- sgl.query(FindSailings, many)2
SELECT id, origin, destination, departs, seats_free2
SELECT id, origin, destination, departs, seats_free3
FROM sailings3
FROM sailings4
WHERE origin = $1 AND destination = $24
WHERE origin = {origin:String} AND destination = {destination:String}5
ORDER BY departs;5
ORDER BY departs;SailingsDepartingdiffers
PostgreSQL · db/queries/sailings.sql
ClickHouse · 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)4
SELECT id, origin, destination, departs, seats_free2
SELECT id, origin, destination, departs, seats_free5
FROM sailings3
FROM sailings6
WHERE departs >= $1 AND departs < $24
WHERE departs >= {earliest:String} AND departs < {latest:String}7
ORDER BY departs;5
ORDER BY departs;BookSeatsdiffers
PostgreSQL · db/queries/sailings.sql
ClickHouse · db/queries/sailings.sql
1
-- sgl.query(BookSeats, execrows)1
-- sgl.query(BookSeats, asyncexec)2
UPDATE sailings2
ALTER TABLE sailings3
SET seats_free = seats_free - :seats3
UPDATE seats_free = seats_free - {seats:Int32}4
WHERE id = :sailing_id AND seats_free >= :seats;4
WHERE id = {sailing_id:UInt64} AND seats_free >= {seats:Int32};SeatsFreeonly ClickHouse
PostgreSQL · not in this version
ClickHouse · db/queries/sailings.sql
1
-- sgl.query(SeatsFree, one)2
SELECT seats_free FROM sailings WHERE id = {sailing_id:UInt64};What sgl generates
AddSailingdiffers
PostgreSQL
ClickHouse
1
func (queries *Queries) AddSailing(ctx context.Context, params AddSailingParams) error1
func (queries *Queries) AddSailing(ctx context.Context, params AddSailingParams) error2
2
3
type AddSailingParams struct {3
type AddSailingParams struct {4
ID uint644
Origin string5
Origin string5
Destination string6
Destination string6
Departs string7
Departs string7
SeatsFree int648
SeatsFree int328
}9
}FindSailingsdiffers
PostgreSQL
ClickHouse
1
func (queries *Queries) FindSailings(ctx context.Context, params FindSailingsParams) ([]FindSailingsRow, error)1
func (queries *Queries) FindSailings(ctx context.Context, params FindSailingsParams) ([]FindSailingsRow, error)2
2
3
type FindSailingsParams struct {3
type FindSailingsParams struct {4
Origin string4
Origin string5
Destination string5
Destination string6
}6
}7
7
8
type FindSailingsRow struct {8
type FindSailingsRow struct {9
ID int64 `json:"id"`9
ID uint64 `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 int64 `json:"seats_free"`13
SeatsFree int32 `json:"seats_free"`14
}14
}SailingsDepartingdiffers
PostgreSQL
ClickHouse
1
func (queries *Queries) SailingsDeparting(ctx context.Context, params SailingsDepartingParams) ([]SailingsDepartingRow, error)1
func (queries *Queries) SailingsDeparting(ctx context.Context, params SailingsDepartingParams) ([]SailingsDepartingRow, error)2
2
3
type SailingsDepartingParams struct {3
type SailingsDepartingParams struct {4
Earliest string4
Earliest string5
Latest string5
Latest string6
}6
}7
7
8
type SailingsDepartingRow struct {8
type SailingsDepartingRow struct {9
ID int64 `json:"id"`9
ID uint64 `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 int64 `json:"seats_free"`13
SeatsFree int32 `json:"seats_free"`14
}14
}BookSeatsdiffers
PostgreSQL
ClickHouse
1
func (queries *Queries) BookSeats(ctx context.Context, params BookSeatsParams) (int64, error)1
func (queries *Queries) BookSeats(ctx context.Context, params BookSeatsParams) error2
2
3
type BookSeatsParams struct {3
type BookSeatsParams struct {4
Seats int644
Seats int325
SailingID int645
SailingID uint646
}6
}SeatsFreeonly ClickHouse
PostgreSQL · not in this version
ClickHouse
1
func (queries *Queries) SeatsFree(ctx context.Context, sailingID uint64) (SeatsFreeRow, error)2
3
type SeatsFreeRow struct {4
SeatsFree int32 `json:"seats_free"`5
}The program
example.godiffers
PostgreSQL
ClickHouse
1
package main1
package main2
2
3
import (3
import (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/postgres/003_parameters/internal/store"9
"seagull.sh/seagull/examples/clickhouse/003_parameters/internal/store"10
)10
)11
11
12
// run fills a ferry timetable, searches it two ways and books seats. Every query has two12
// run fills a ferry timetable, searches it two ways and books seats. A ClickHouse13
// or more parameters, so each method takes a Params struct, and the struct's field names13
// placeholder carries its own name and type, as in {origin:String}, so each Params field14
// come from the SQL:14
// is named after the placeholder and typed from it: String gives string, Int32 int32 and15
// UInt64 uint64. A query with two or more parameters takes a Params struct:15
//16
//16
// - AddSailing and FindSailings name each $N after the column it is inserted into or17
// - AddSailing and FindSailings name their fields ID, Origin, Destination, Departs and17
// compared with: Origin, Destination, Departs, SeatsFree;18
// SeatsFree from {id:UInt64}, {origin:String} and the rest;18
// - SailingsDeparting compares departs twice, which would give Departs and Departs2, so19
// - SailingsDeparting compares departs twice, so it names the two bounds itself with19
// its binding lines ($1 as sgl.param(earliest)) choose better names;20
// {earliest:String} and {latest:String}, and needs no binding line;20
// - BookSeats uses named placeholders; :seats appears twice but is one field. The21
// - BookSeats uses {seats:Int32} twice, but it is one field, bound once.21
// generated SQL numbers them, so :seats becomes $1 in both places, as PostgreSQL22
// binds a reused number once.23
//22
//23
// Seagull sends each placeholder as p_<name>, as in {p_seats:Int32}, so a parameter may24
// be called limit or offset without the server reading it as a keyword.25
//26
// ALTER TABLE ... UPDATE reports no changed rows, so BookSeats is asyncexec and the27
// booking reads seats_free before and after to see whether the guard let it through.28
//24
// Takes database (*sql.DB) which is a migrated database.29
// Takes database (*sql.DB) which is a migrated database.25
// Takes output (io.Writer) which receives the search results and bookings.30
// Takes output (io.Writer) which receives the search results and bookings.26
//31
//27
// Returns error when a query fails.32
// Returns error when a query fails.28
func run(ctx context.Context, database *sql.DB, output io.Writer) error {33
func run(ctx context.Context, database *sql.DB, output io.Writer) error {29
queries := store.New(database)34
queries := store.New(database)30
35
31
timetable := []store.AddSailingParams{36
timetable := []store.AddSailingParams{32
{Origin: "Penzance", Destination: "St Mary's", Departs: "09:15", SeatsFree: 40},37
{ID: 1, Origin: "Penzance", Destination: "St Mary's", Departs: "09:15", SeatsFree: 40},33
{Origin: "St Mary's", Destination: "Tresco", Departs: "11:00", SeatsFree: 12},38
{ID: 2, Origin: "St Mary's", Destination: "Tresco", Departs: "11:00", SeatsFree: 12},34
{Origin: "Penzance", Destination: "St Mary's", Departs: "14:30", SeatsFree: 4},39
{ID: 3, Origin: "Penzance", Destination: "St Mary's", Departs: "14:30", SeatsFree: 4},35
{Origin: "St Mary's", Destination: "Penzance", Departs: "16:30", SeatsFree: 60},40
{ID: 4, Origin: "St Mary's", Destination: "Penzance", Departs: "16:30", SeatsFree: 60},36
}41
}37
for _, sailing := range timetable {42
for _, sailing := range timetable {38
if err := queries.AddSailing(ctx, sailing); err != nil {43
if err := queries.AddSailing(ctx, sailing); err != nil {39
return fmt.Errorf("adding the %s sailing: %w", sailing.Departs, err)44
return fmt.Errorf("adding the %s sailing: %w", sailing.Departs, err)40
}45
}41
}46
}42
47
43
route, err := queries.FindSailings(ctx, store.FindSailingsParams{48
route, err := queries.FindSailings(ctx, store.FindSailingsParams{44
Origin: "Penzance",49
Origin: "Penzance",45
Destination: "St Mary's",50
Destination: "St Mary's",46
})51
})47
if err != nil {52
if err != nil {48
return fmt.Errorf("finding sailings: %w", err)53
return fmt.Errorf("finding sailings: %w", err)49
}54
}50
fmt.Fprintln(output, "Penzance to St Mary's:")55
fmt.Fprintln(output, "Penzance to St Mary's:")51
for _, sailing := range route {56
for _, sailing := range route {52
fmt.Fprintf(output, " sailing %d at %s, %d seats free\n", sailing.ID, sailing.Departs, sailing.SeatsFree)57
fmt.Fprintf(output, " sailing %d at %s, %d seats free\n", sailing.ID, sailing.Departs, sailing.SeatsFree)53
}58
}54
59
55
midday, err := queries.SailingsDeparting(ctx, store.SailingsDepartingParams{60
midday, err := queries.SailingsDeparting(ctx, store.SailingsDepartingParams{56
Earliest: "10:00",61
Earliest: "10:00",57
Latest: "15:00",62
Latest: "15:00",58
})63
})59
if err != nil {64
if err != nil {60
return fmt.Errorf("finding midday sailings: %w", err)65
return fmt.Errorf("finding midday sailings: %w", err)61
}66
}62
fmt.Fprintln(output, "Departing between 10:00 and 15:00:")67
fmt.Fprintln(output, "Departing between 10:00 and 15:00:")63
for _, sailing := range midday {68
for _, sailing := range midday {64
fmt.Fprintf(output, " sailing %d at %s, %s to %s\n", sailing.ID, sailing.Departs, sailing.Origin, sailing.Destination)69
fmt.Fprintf(output, " sailing %d at %s, %s to %s\n", sailing.ID, sailing.Departs, sailing.Origin, sailing.Destination)65
}70
}66
71
67
// The same booking twice: the first takes 3 of the 4 free seats, so the second changes72
// The same booking twice: the first takes 3 of the 4 free seats, so the second changes68
// no row because seats_free >= :seats is now false.73
// nothing because seats_free >= {seats:Int32} is now false.69
booking := store.BookSeatsParams{Seats: 3, SailingID: 3}74
booking := store.BookSeatsParams{Seats: 3, SailingID: 3}70
for range 2 {75
for range 2 {71
booked, err := queries.BookSeats(ctx, booking)76
booked, err := bookSeats(ctx, queries, booking)72
if err != nil {77
if err != nil {73
return fmt.Errorf("booking sailing %d: %w", booking.SailingID, err)78
return err74
}79
}75
if booked == 0 {80
if !booked {76
fmt.Fprintf(output, "sailing %d has fewer than %d seats free\n", booking.SailingID, booking.Seats)81
fmt.Fprintf(output, "sailing %d has fewer than %d seats free\n", booking.SailingID, booking.Seats)77
continue82
continue78
}83
}79
fmt.Fprintf(output, "booked %d seats on sailing %d\n", booking.Seats, booking.SailingID)84
fmt.Fprintf(output, "booked %d seats on sailing %d\n", booking.Seats, booking.SailingID)80
}85
}81
return nil86
return nil82
}87
}88
89
// bookSeats runs BookSeats and reports whether it took the seats, by comparing seats_free90
// before and after. The connection sets mutations_sync=2, so the mutation has finished91
// 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.98
func 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, nil111
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | PostgreSQL | ClickHouse |
|---|---|---|---|
| AddSailing | parameter seats_free | int64 | int32 |
| FindSailings | column id | int64 | uint64 |
| FindSailings | column seats_free | int64 | int32 |
| SailingsDeparting | column id | int64 | uint64 |
| SailingsDeparting | column seats_free | int64 | int32 |
| BookSeats | parameter seats | int64 | int32 |
| BookSeats | parameter sailing_id | int64 | uint64 |