Nested structs with sgl.embed: SQLite and TimescaleDB
Every file of the SQLite and TimescaleDB versions side by side, with the lines that differ marked. 2 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
SQLite
TimescaleDB
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: fleet4
- name: fleet5
dialect: sqlite5
dialect: postgres6
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_fleet.up.sqldiffers
SQLite
TimescaleDB
1
CREATE TABLE harbours (1
CREATE TABLE harbours (2
id INTEGER PRIMARY KEY,2
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,3
name TEXT NOT NULL,3
name TEXT NOT NULL,4
county TEXT NOT NULL4
county TEXT NOT NULL5
);5
);6
6
7
CREATE TABLE skippers (7
CREATE TABLE skippers (8
id INTEGER PRIMARY KEY,8
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,9
name TEXT NOT NULL,9
name TEXT NOT NULL,10
licence TEXT NOT NULL10
licence TEXT NOT NULL11
);11
);12
12
13
CREATE TABLE boats (13
CREATE TABLE boats (14
id INTEGER PRIMARY KEY,14
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,15
name TEXT NOT NULL,15
name TEXT NOT NULL,16
harbour_id INTEGER NOT NULL REFERENCES harbours (id),16
harbour_id BIGINT NOT NULL REFERENCES harbours (id),17
skipper_id INTEGER REFERENCES skippers (id)17
skipper_id BIGINT REFERENCES skippers (id)18
);18
);db/migrations/0001_fleet.down.sqlidentical
SQLite
TimescaleDB
1
DROP TABLE boats;1
DROP TABLE boats;2
DROP TABLE skippers;2
DROP TABLE skippers;3
DROP TABLE harbours;3
DROP TABLE harbours;Queries
AddHarbourdiffers
SQLite · db/queries/boats.sql
TimescaleDB · db/queries/boats.sql
1
-- sgl.query(AddHarbour, exec)1
-- sgl.query(AddHarbour, exec)2
INSERT INTO harbours (name, county) VALUES (?, ?);2
INSERT INTO harbours (name, county) VALUES ($1, $2);AddSkipperdiffers
SQLite · db/queries/boats.sql
TimescaleDB · db/queries/boats.sql
1
-- sgl.query(AddSkipper, exec)1
-- sgl.query(AddSkipper, exec)2
INSERT INTO skippers (name, licence) VALUES (?, ?);2
INSERT INTO skippers (name, licence) VALUES ($1, $2);AddBoatdiffers
SQLite · db/queries/boats.sql
TimescaleDB · db/queries/boats.sql
1
-- sgl.query(AddBoat, exec)1
-- sgl.query(AddBoat, exec)2
INSERT INTO boats (name, harbour_id, skipper_id) VALUES (?, ?, ?);2
INSERT INTO boats (name, harbour_id, skipper_id) VALUES ($1, $2, $3);GetBoatdiffers
SQLite · db/queries/boats.sql
TimescaleDB · db/queries/boats.sql
1
-- sgl.query(GetBoat, one)1
-- sgl.query(GetBoat, one)2
-- sgl.embed(harbours)2
-- sgl.embed(harbours)3
SELECT boats.id, boats.name, harbours.id, harbours.name, harbours.county3
SELECT boats.id, boats.name, harbours.id, harbours.name, harbours.county4
FROM boats4
FROM boats5
INNER JOIN harbours ON harbours.id = boats.harbour_id5
INNER JOIN harbours ON harbours.id = boats.harbour_id6
WHERE boats.id = ?;6
WHERE boats.id = $1;ListCrewsidentical
SQLite · db/queries/boats.sql
TimescaleDB · db/queries/boats.sql
1
-- sgl.query(ListCrews, many)1
-- sgl.query(ListCrews, many)2
-- sgl.embed(skippers)2
-- sgl.embed(skippers)3
SELECT boats.id, boats.name, skippers.id, skippers.name, skippers.licence3
SELECT boats.id, boats.name, skippers.id, skippers.name, skippers.licence4
FROM boats4
FROM boats5
LEFT JOIN skippers ON skippers.id = boats.skipper_id5
LEFT JOIN skippers ON skippers.id = boats.skipper_id6
ORDER BY boats.id;6
ORDER BY boats.id;What sgl generates
AddHarbouridentical
SQLite
TimescaleDB
1
func (queries *Queries) AddHarbour(ctx context.Context, params AddHarbourParams) error1
func (queries *Queries) AddHarbour(ctx context.Context, params AddHarbourParams) error2
2
3
type AddHarbourParams struct {3
type AddHarbourParams struct {4
Name string4
Name string5
County string5
County string6
}6
}AddSkipperidentical
SQLite
TimescaleDB
1
func (queries *Queries) AddSkipper(ctx context.Context, params AddSkipperParams) error1
func (queries *Queries) AddSkipper(ctx context.Context, params AddSkipperParams) error2
2
3
type AddSkipperParams struct {3
type AddSkipperParams struct {4
Name string4
Name string5
Licence string5
Licence string6
}6
}AddBoatidentical
SQLite
TimescaleDB
1
func (queries *Queries) AddBoat(ctx context.Context, params AddBoatParams) error1
func (queries *Queries) AddBoat(ctx context.Context, params AddBoatParams) error2
2
3
type AddBoatParams struct {3
type AddBoatParams struct {4
Name string4
Name string5
HarbourID int645
HarbourID int646
SkipperID *int646
SkipperID *int647
}7
}GetBoatidentical
SQLite
TimescaleDB
1
func (queries *Queries) GetBoat(ctx context.Context, id int64) (GetBoatRow, error)1
func (queries *Queries) GetBoat(ctx context.Context, id int64) (GetBoatRow, error)2
2
3
type GetBoatRow struct {3
type GetBoatRow struct {4
ID int64 `json:"id"`4
ID int64 `json:"id"`5
Name string `json:"name"`5
Name string `json:"name"`6
Harbours GetBoatHarbours6
Harbours GetBoatHarbours7
}7
}8
8
9
type GetBoatHarbours struct {9
type GetBoatHarbours struct {10
ID int64 `json:"id"`10
ID int64 `json:"id"`11
Name string `json:"name"`11
Name string `json:"name"`12
County string `json:"county"`12
County string `json:"county"`13
}13
}ListCrewsidentical
SQLite
TimescaleDB
1
func (queries *Queries) ListCrews(ctx context.Context) ([]ListCrewsRow, error)1
func (queries *Queries) ListCrews(ctx context.Context) ([]ListCrewsRow, error)2
2
3
type ListCrewsRow struct {3
type ListCrewsRow struct {4
ID int64 `json:"id"`4
ID int64 `json:"id"`5
Name string `json:"name"`5
Name string `json:"name"`6
Skippers *ListCrewsSkippers6
Skippers *ListCrewsSkippers7
}7
}8
8
9
type ListCrewsSkippers struct {9
type ListCrewsSkippers struct {10
ID *int64 `json:"id"`10
ID *int64 `json:"id"`11
Name *string `json:"name"`11
Name *string `json:"name"`12
Licence *string `json:"licence"`12
Licence *string `json:"licence"`13
}13
}The program
example.godiffers
SQLite
TimescaleDB
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/sqlite/015_embed/internal/store"9
"seagull.sh/seagull/examples/postgres/015_embed/internal/store"10
)10
)11
11
12
// run adds a small fleet and reads it back with joined tables as nested structs.12
// run adds a small fleet and reads it back with joined tables as nested structs.13
// sgl.embed(table) gathers the columns the query selects from that table into a field13
// sgl.embed(table) gathers the columns the query selects from that table into a field14
// named after the table, here Harbours and Skippers, with its own struct type named after14
// named after the table, here Harbours and Skippers, with its own struct type named after15
// the query and the table, such as GetBoatHarbours.15
// the query and the table, such as GetBoatHarbours.16
//16
//17
// GetBoat uses an INNER JOIN, so every boat has a harbour and Harbours is a value.17
// GetBoat uses an INNER JOIN, so every boat has a harbour and Harbours is a value.18
// ListCrews uses a LEFT JOIN, so a boat may have no skipper: Skippers is a pointer that18
// ListCrews uses a LEFT JOIN, so a boat may have no skipper: Skippers is a pointer that19
// is nil when the joined row is missing, and its fields are pointers too, as each column19
// is nil when the joined row is missing, and its fields are pointers too, as each column20
// of a LEFT JOIN can be NULL.20
// of a LEFT JOIN can be NULL.21
//21
//22
// Takes database (*sql.DB) which is a migrated database.22
// Takes database (*sql.DB) which is a migrated database.23
// Takes output (io.Writer) which receives the boat and the crew list.23
// Takes output (io.Writer) which receives the boat and the crew list.24
//24
//25
// Returns error when a query fails.25
// Returns error when a query fails.26
func run(ctx context.Context, database *sql.DB, output io.Writer) error {26
func run(ctx context.Context, database *sql.DB, output io.Writer) error {27
queries := store.New(database)27
queries := store.New(database)28
28
29
if err := queries.AddHarbour(ctx, store.AddHarbourParams{Name: "Newlyn", County: "Cornwall"}); err != nil {29
if err := queries.AddHarbour(ctx, store.AddHarbourParams{Name: "Newlyn", County: "Cornwall"}); err != nil {30
return fmt.Errorf("adding a harbour: %w", err)30
return fmt.Errorf("adding a harbour: %w", err)31
}31
}32
if err := queries.AddSkipper(ctx, store.AddSkipperParams{Name: "Morwenna Penrose", Licence: "MCA-1182"}); err != nil {32
if err := queries.AddSkipper(ctx, store.AddSkipperParams{Name: "Morwenna Penrose", Licence: "MCA-1182"}); err != nil {33
return fmt.Errorf("adding a skipper: %w", err)33
return fmt.Errorf("adding a skipper: %w", err)34
}34
}35
boats := []store.AddBoatParams{35
boats := []store.AddBoatParams{36
{Name: "Kittiwake", HarbourID: 1, SkipperID: new(int64(1))},36
{Name: "Kittiwake", HarbourID: 1, SkipperID: new(int64(1))},37
{Name: "Salty Chip", HarbourID: 1, SkipperID: nil},37
{Name: "Salty Chip", HarbourID: 1, SkipperID: nil},38
}38
}39
for _, boat := range boats {39
for _, boat := range boats {40
if err := queries.AddBoat(ctx, boat); err != nil {40
if err := queries.AddBoat(ctx, boat); err != nil {41
return fmt.Errorf("adding %s: %w", boat.Name, err)41
return fmt.Errorf("adding %s: %w", boat.Name, err)42
}42
}43
}43
}44
44
45
boat, err := queries.GetBoat(ctx, 2)45
boat, err := queries.GetBoat(ctx, 2)46
if err != nil {46
if err != nil {47
return fmt.Errorf("getting boat 2: %w", err)47
return fmt.Errorf("getting boat 2: %w", err)48
}48
}49
fmt.Fprintf(output, "%s is moored at %s, %s\n", boat.Name, boat.Harbours.Name, boat.Harbours.County)49
fmt.Fprintf(output, "%s is moored at %s, %s\n", boat.Name, boat.Harbours.Name, boat.Harbours.County)50
50
51
crews, err := queries.ListCrews(ctx)51
crews, err := queries.ListCrews(ctx)52
if err != nil {52
if err != nil {53
return fmt.Errorf("listing crews: %w", err)53
return fmt.Errorf("listing crews: %w", err)54
}54
}55
for _, crew := range crews {55
for _, crew := range crews {56
if crew.Skippers == nil {56
if crew.Skippers == nil {57
fmt.Fprintf(output, "%s has no skipper\n", crew.Name)57
fmt.Fprintf(output, "%s has no skipper\n", crew.Name)58
continue58
continue59
}59
}60
fmt.Fprintf(output, "%s is skippered by %s, licence %s\n", crew.Name, *crew.Skippers.Name, *crew.Skippers.Licence)60
fmt.Fprintf(output, "%s is skippered by %s, licence %s\n", crew.Name, *crew.Skippers.Name, *crew.Skippers.Licence)61
}61
}62
return nil62
return nil63
}63
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | SQLite | TimescaleDB |
|---|---|---|---|
| ListCrews | column id | *int64 | int64 |
| ListCrews | column name | *string | string |