Compare · 003

Placeholders, inferred names and Params structs: PostgreSQL and TimescaleDB

PostgreSQL and TimescaleDB run the same files for this example: the SQL and the Go are identical.

Configuration

sgl.yamlidentical
PostgreSQL
TimescaleDB
1version: 1
1version: 1
2
2
3databases:
3databases:
4 - name: ferries
4 - name: ferries
5 dialect: postgres
5 dialect: postgres
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
11 driver: database/sql
11 driver: database/sql

Migrations

db/migrations/0001_sailings.up.sqlidentical
PostgreSQL
TimescaleDB
1CREATE TABLE sailings (
1CREATE TABLE sailings (
2 id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
2 id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
3 origin TEXT NOT NULL,
3 origin TEXT NOT NULL,
4 destination TEXT NOT NULL,
4 destination TEXT NOT NULL,
5 departs TEXT NOT NULL,
5 departs TEXT NOT NULL,
6 seats_free BIGINT NOT NULL
6 seats_free BIGINT NOT NULL
7);
7);
db/migrations/0001_sailings.down.sqlidentical
PostgreSQL
TimescaleDB
1DROP TABLE sailings;
1DROP TABLE sailings;

Queries

AddSailingidentical
PostgreSQL · db/queries/sailings.sql
TimescaleDB · db/queries/sailings.sql
1-- sgl.query(AddSailing, exec)
1-- sgl.query(AddSailing, exec)
2INSERT INTO sailings (origin, destination, departs, seats_free) VALUES ($1, $2, $3, $4);
2INSERT INTO sailings (origin, destination, departs, seats_free) VALUES ($1, $2, $3, $4);
FindSailingsidentical
PostgreSQL · db/queries/sailings.sql
TimescaleDB · 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 = $1 AND destination = $2
4WHERE origin = $1 AND destination = $2
5ORDER BY departs;
5ORDER BY departs;
SailingsDepartingidentical
PostgreSQL · db/queries/sailings.sql
TimescaleDB · db/queries/sailings.sql
1-- sgl.query(SailingsDeparting, many)
1-- sgl.query(SailingsDeparting, many)
2-- $1 as sgl.param(earliest)
2-- $1 as sgl.param(earliest)
3-- $2 as sgl.param(latest)
3-- $2 as sgl.param(latest)
4SELECT id, origin, destination, departs, seats_free
4SELECT id, origin, destination, departs, seats_free
5FROM sailings
5FROM sailings
6WHERE departs >= $1 AND departs < $2
6WHERE departs >= $1 AND departs < $2
7ORDER BY departs;
7ORDER BY departs;
BookSeatsidentical
PostgreSQL · db/queries/sailings.sql
TimescaleDB · db/queries/sailings.sql
1-- sgl.query(BookSeats, execrows)
1-- sgl.query(BookSeats, execrows)
2UPDATE sailings
2UPDATE sailings
3SET seats_free = seats_free - :seats
3SET seats_free = seats_free - :seats
4WHERE id = :sailing_id AND seats_free >= :seats;
4WHERE id = :sailing_id AND seats_free >= :seats;

What sgl generates

AddSailingidentical
PostgreSQL
TimescaleDB
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 Origin string
4 Origin string
5 Destination string
5 Destination string
6 Departs string
6 Departs string
7 SeatsFree int64
7 SeatsFree int64
8}
8}
FindSailingsidentical
PostgreSQL
TimescaleDB
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 int64 `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 int64 `json:"seats_free"`
13 SeatsFree int64 `json:"seats_free"`
14}
14}
SailingsDepartingidentical
PostgreSQL
TimescaleDB
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 int64 `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 int64 `json:"seats_free"`
13 SeatsFree int64 `json:"seats_free"`
14}
14}
BookSeatsidentical
PostgreSQL
TimescaleDB
1func (queries *Queries) BookSeats(ctx context.Context, params BookSeatsParams) (int64, error)
1func (queries *Queries) BookSeats(ctx context.Context, params BookSeatsParams) (int64, error)
2
2
3type BookSeatsParams struct {
3type BookSeatsParams struct {
4 Seats int64
4 Seats int64
5 SailingID int64
5 SailingID int64
6}
6}

The program

example.goidentical
PostgreSQL
TimescaleDB
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/postgres/003_parameters/internal/store"
9 "seagull.sh/seagull/examples/postgres/003_parameters/internal/store"
10)
10)
11
11
12// run fills a ferry timetable, searches it two ways and books seats. Every query has two
12// run fills a ferry timetable, searches it two ways and books seats. Every query has two
13// or more parameters, so each method takes a Params struct, and the struct's field names
13// or more parameters, so each method takes a Params struct, and the struct's field names
14// come from the SQL:
14// come from the SQL:
15//
15//
16// - AddSailing and FindSailings name each $N after the column it is inserted into or
16// - AddSailing and FindSailings name each $N after the column it is inserted into or
17// compared with: Origin, Destination, Departs, SeatsFree;
17// compared with: Origin, Destination, Departs, SeatsFree;
18// - SailingsDeparting compares departs twice, which would give Departs and Departs2, so
18// - SailingsDeparting compares departs twice, which would give Departs and Departs2, so
19// its binding lines ($1 as sgl.param(earliest)) choose better names;
19// its binding lines ($1 as sgl.param(earliest)) choose better names;
20// - BookSeats uses named placeholders; :seats appears twice but is one field. The
20// - BookSeats uses named placeholders; :seats appears twice but is one field. The
21// generated SQL numbers them, so :seats becomes $1 in both places, as PostgreSQL
21// generated SQL numbers them, so :seats becomes $1 in both places, as PostgreSQL
22// binds a reused number once.
22// binds a reused number once.
23//
23//
24// Takes database (*sql.DB) which is a migrated database.
24// Takes database (*sql.DB) which is a migrated database.
25// Takes output (io.Writer) which receives the search results and bookings.
25// Takes output (io.Writer) which receives the search results and bookings.
26//
26//
27// Returns error when a query fails.
27// Returns error when a query fails.
28func run(ctx context.Context, database *sql.DB, output io.Writer) error {
28func run(ctx context.Context, database *sql.DB, output io.Writer) error {
29 queries := store.New(database)
29 queries := store.New(database)
30
30
31 timetable := []store.AddSailingParams{
31 timetable := []store.AddSailingParams{
32 {Origin: "Penzance", Destination: "St Mary's", Departs: "09:15", SeatsFree: 40},
32 {Origin: "Penzance", Destination: "St Mary's", Departs: "09:15", SeatsFree: 40},
33 {Origin: "St Mary's", Destination: "Tresco", Departs: "11:00", SeatsFree: 12},
33 {Origin: "St Mary's", Destination: "Tresco", Departs: "11:00", SeatsFree: 12},
34 {Origin: "Penzance", Destination: "St Mary's", Departs: "14:30", SeatsFree: 4},
34 {Origin: "Penzance", Destination: "St Mary's", Departs: "14:30", SeatsFree: 4},
35 {Origin: "St Mary's", Destination: "Penzance", Departs: "16:30", SeatsFree: 60},
35 {Origin: "St Mary's", Destination: "Penzance", Departs: "16:30", SeatsFree: 60},
36 }
36 }
37 for _, sailing := range timetable {
37 for _, sailing := range timetable {
38 if err := queries.AddSailing(ctx, sailing); err != nil {
38 if err := queries.AddSailing(ctx, sailing); err != nil {
39 return fmt.Errorf("adding the %s sailing: %w", sailing.Departs, err)
39 return fmt.Errorf("adding the %s sailing: %w", sailing.Departs, err)
40 }
40 }
41 }
41 }
42
42
43 route, err := queries.FindSailings(ctx, store.FindSailingsParams{
43 route, err := queries.FindSailings(ctx, store.FindSailingsParams{
44 Origin: "Penzance",
44 Origin: "Penzance",
45 Destination: "St Mary's",
45 Destination: "St Mary's",
46 })
46 })
47 if err != nil {
47 if err != nil {
48 return fmt.Errorf("finding sailings: %w", err)
48 return fmt.Errorf("finding sailings: %w", err)
49 }
49 }
50 fmt.Fprintln(output, "Penzance to St Mary's:")
50 fmt.Fprintln(output, "Penzance to St Mary's:")
51 for _, sailing := range route {
51 for _, sailing := range route {
52 fmt.Fprintf(output, " sailing %d at %s, %d seats free\n", sailing.ID, sailing.Departs, sailing.SeatsFree)
52 fmt.Fprintf(output, " sailing %d at %s, %d seats free\n", sailing.ID, sailing.Departs, sailing.SeatsFree)
53 }
53 }
54
54
55 midday, err := queries.SailingsDeparting(ctx, store.SailingsDepartingParams{
55 midday, err := queries.SailingsDeparting(ctx, store.SailingsDepartingParams{
56 Earliest: "10:00",
56 Earliest: "10:00",
57 Latest: "15:00",
57 Latest: "15:00",
58 })
58 })
59 if err != nil {
59 if err != nil {
60 return fmt.Errorf("finding midday sailings: %w", err)
60 return fmt.Errorf("finding midday sailings: %w", err)
61 }
61 }
62 fmt.Fprintln(output, "Departing between 10:00 and 15:00:")
62 fmt.Fprintln(output, "Departing between 10:00 and 15:00:")
63 for _, sailing := range midday {
63 for _, sailing := range midday {
64 fmt.Fprintf(output, " sailing %d at %s, %s to %s\n", sailing.ID, sailing.Departs, sailing.Origin, sailing.Destination)
64 fmt.Fprintf(output, " sailing %d at %s, %s to %s\n", sailing.ID, sailing.Departs, sailing.Origin, sailing.Destination)
65 }
65 }
66
66
67 // The same booking twice: the first takes 3 of the 4 free seats, so the second changes
67 // The same booking twice: the first takes 3 of the 4 free seats, so the second changes
68 // no row because seats_free >= :seats is now false.
68 // no row because seats_free >= :seats is now false.
69 booking := store.BookSeatsParams{Seats: 3, SailingID: 3}
69 booking := store.BookSeatsParams{Seats: 3, SailingID: 3}
70 for range 2 {
70 for range 2 {
71 booked, err := queries.BookSeats(ctx, booking)
71 booked, err := queries.BookSeats(ctx, booking)
72 if err != nil {
72 if err != nil {
73 return fmt.Errorf("booking sailing %d: %w", booking.SailingID, err)
73 return fmt.Errorf("booking sailing %d: %w", booking.SailingID, err)
74 }
74 }
75 if booked == 0 {
75 if booked == 0 {
76 fmt.Fprintf(output, "sailing %d has fewer than %d seats free\n", booking.SailingID, booking.Seats)
76 fmt.Fprintf(output, "sailing %d has fewer than %d seats free\n", booking.SailingID, booking.Seats)
77 continue
77 continue
78 }
78 }
79 fmt.Fprintf(output, "booked %d seats on sailing %d\n", booking.Seats, booking.SailingID)
79 fmt.Fprintf(output, "booked %d seats on sailing %d\n", booking.Seats, booking.SailingID)
80 }
80 }
81 return nil
81 return nil
82}
82}