Type overrides: SQLite and PostgreSQL
Every file of the SQLite and PostgreSQL versions side by side, with the lines that differ marked. 2 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
SQLite
PostgreSQL
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: menu4
- name: menu5
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/sql11
types:12
types:13
decimal: string12
overrides:14
overrides:13
- column: menu_items.status15
- column: menu_items.status14
go: seagull.sh/seagull/examples/sqlite/009_overrides/internal/menu.Status16
go: seagull.sh/seagull/examples/postgres/009_overrides/internal/menu.StatusMigrations
db/migrations/0001_menu.up.sqldiffers
SQLite
PostgreSQL
1
-- sgl.column(menu_items.price_pence, go_type: "seagull.sh/seagull/examples/sqlite/009_overrides/internal/menu.Pence")1
-- sgl.column(menu_items.price, go_type: "seagull.sh/seagull/examples/postgres/009_overrides/internal/menu.Price")2
CREATE TABLE menu_items (2
CREATE TABLE menu_items (3
id INTEGER PRIMARY KEY,3
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,4
name TEXT NOT NULL,4
name TEXT NOT NULL,5
section TEXT NOT NULL,5
section TEXT NOT NULL,6
status TEXT NOT NULL CHECK (status IN ('available', 'sold_out')),6
status TEXT NOT NULL CHECK (status IN ('available', 'sold_out')),7
price_pence INTEGER NOT NULL,7
price NUMERIC(5, 2) NOT NULL,8
nutrition TEXT NOT NULL8
nutrition JSONB NOT NULL9
);9
);db/migrations/0001_menu.down.sqlidentical
SQLite
PostgreSQL
1
DROP TABLE menu_items;1
DROP TABLE menu_items;Queries
AddItemdiffers
SQLite · db/queries/menu.sql
PostgreSQL · db/queries/menu.sql
1
-- sgl.query(AddItem, exec)1
-- sgl.query(AddItem, exec)2
INSERT INTO menu_items (name, section, status, price_pence, nutrition) VALUES (?, ?, ?, ?, ?);2
INSERT INTO menu_items (name, section, status, price, nutrition) VALUES ($1, $2, $3, $4, $5);ListMenudiffers
SQLite · db/queries/menu.sql
PostgreSQL · db/queries/menu.sql
1
-- sgl.query(ListMenu, many)1
-- sgl.query(ListMenu, many)2
-- sgl.column(kcal, type: integer)2
-- sgl.column(kcal, type: bigint, nullable: false)3
SELECT name, status, price_pence, json_extract(nutrition, '$.kcal') AS kcal3
SELECT name, status, price, nutrition ->> 'kcal' AS kcal4
FROM menu_items4
FROM menu_items5
ORDER BY section, name;5
ORDER BY section, name;CheapestPerSectiondiffers
SQLite · db/queries/menu.sql
PostgreSQL · db/queries/menu.sql
1
-- sgl.query(CheapestPerSection, many)1
-- sgl.query(CheapestPerSection, many)2
-- sgl.column(cheapest, go_type: "seagull.sh/seagull/examples/sqlite/009_overrides/internal/menu.Pence", nullable: false)2
-- sgl.column(cheapest, go_type: "seagull.sh/seagull/examples/postgres/009_overrides/internal/menu.Price", nullable: false)3
SELECT section, MIN(price_pence) AS cheapest3
SELECT section, MIN(price) AS cheapest4
FROM menu_items4
FROM menu_items5
GROUP BY section5
GROUP BY section6
ORDER BY section;6
ORDER BY section;What sgl generates
AddItemdiffers
SQLite
PostgreSQL
1
func (queries *Queries) AddItem(ctx context.Context, params AddItemParams) error1
func (queries *Queries) AddItem(ctx context.Context, params AddItemParams) error2
2
3
type AddItemParams struct {3
type AddItemParams struct {4
Name string4
Name string5
Section string5
Section string6
Status menu.Status6
Status menu.Status7
PricePence menu.Pence7
Price menu.Price8
Nutrition string8
Nutrition JSON9
}9
}10
11
type JSON []byteListMenudiffers
SQLite
PostgreSQL
1
func (queries *Queries) ListMenu(ctx context.Context) ([]ListMenuRow, error)1
func (queries *Queries) ListMenu(ctx context.Context) ([]ListMenuRow, error)2
2
3
type ListMenuRow struct {3
type ListMenuRow struct {4
Name string `json:"name"`4
Name string `json:"name"`5
Status menu.Status `json:"status"`5
Status menu.Status `json:"status"`6
PricePence menu.Pence `json:"price_pence"`6
Price menu.Price `json:"price"`7
Kcal int64 `json:"kcal"`7
Kcal int64 `json:"kcal"`8
}8
}CheapestPerSectiondiffers
SQLite
PostgreSQL
1
func (queries *Queries) CheapestPerSection(ctx context.Context) ([]CheapestPerSectionRow, error)1
func (queries *Queries) CheapestPerSection(ctx context.Context) ([]CheapestPerSectionRow, error)2
2
3
type CheapestPerSectionRow struct {3
type CheapestPerSectionRow struct {4
Section string `json:"section"`4
Section string `json:"section"`5
Cheapest menu.Pence `json:"cheapest"`5
Cheapest menu.Price `json:"cheapest"`6
}6
}The program
example.godiffers
SQLite
PostgreSQL
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/009_overrides/internal/menu"9
"seagull.sh/seagull/examples/postgres/009_overrides/internal/menu"10
"seagull.sh/seagull/examples/sqlite/009_overrides/internal/store"10
"seagull.sh/seagull/examples/postgres/009_overrides/internal/store"11
)11
)12
12
13
// run fills a chip shop menu and reads it back. Four overrides replace the Go types13
// run fills a chip shop menu and reads it back. Five overrides replace the Go types14
// Seagull would infer:14
// Seagull would infer:15
//15
//16
// - sgl.yaml's types.decimal slot maps every NUMERIC to string, the server's exact17
// text, in place of apd.Decimal, so the module needs no decimal package; a slot18
// changes models.go and parameters as well as returned columns;16
// - sgl.yaml's types.overrides gives menu_items.status the type menu.Status;19
// - sgl.yaml's types.overrides gives menu_items.status the type menu.Status;17
// - sgl.column(menu_items.price_pence, go_type: ...) in the migration gives price_pence20
// - sgl.column(menu_items.price, go_type: ...) in the migration gives price the type18
// the type menu.Pence in every query that returns the column as it is;21
// menu.Price in every query that returns the column as it is;19
// - sgl.column(kcal, type: integer) in ListMenu types json_extract, which is any by22
// - sgl.column(kcal, type: bigint, nullable: false) in ListMenu types nutrition ->>20
// default because its type depends on the document;23
// 'kcal', which is nullable text, as the number every document holds; a cast,21
// - in CheapestPerSection, MIN(price_pence) is a new value, so the migration's override24
// (nutrition ->> 'kcal')::bigint, would type it too, and make the server check it;22
// no longer applies and the aggregate could be NULL; sgl.column(cheapest, go_type:25
// - in CheapestPerSection, MIN(price) is a new value, so the migration's override no23
// ..., nullable: false) makes it menu.Pence again, as every group has a row.26
// longer applies and the aggregate could be NULL; sgl.column(cheapest, go_type: ...,27
// nullable: false) makes it menu.Price again, as every group has a row.24
//28
//25
// Column overrides also reach models.go and the parameters bound to those columns, so29
// Column overrides also reach models.go and the parameters bound to those columns, so26
// AddItemParams takes a menu.Status and a menu.Pence.30
// AddItemParams takes a menu.Status and a menu.Price.27
//31
//28
// sgl vet catches mistakes in overrides: Q036 for a query override naming a column the32
// sgl vet catches mistakes in overrides: Q036 for a query override naming a column the29
// query does not return, Q037 for an override naming a table column that does not exist,33
// query does not return, Q037 for an override naming a table column that does not exist,30
// Q038 for type: and go_type: together, and Q049 for a go_type the generator cannot use.34
// Q038 for type: and go_type: together, and Q049 for a go_type the generator cannot use.31
//35
//32
// Takes database (*sql.DB) which is a migrated database.36
// Takes database (*sql.DB) which is a migrated database.33
// Takes output (io.Writer) which receives the menu and the cheapest item per section.37
// Takes output (io.Writer) which receives the menu and the cheapest item per section.34
//38
//35
// Returns error when a query fails.39
// Returns error when a query fails.36
func run(ctx context.Context, database *sql.DB, output io.Writer) error {40
func run(ctx context.Context, database *sql.DB, output io.Writer) error {37
queries := store.New(database)41
queries := store.New(database)38
42
39
items := []store.AddItemParams{43
items := []store.AddItemParams{40
{Name: "Cod and chips", Section: "fish", Status: menu.Available, PricePence: 950, Nutrition: `{"kcal": 1050}`},44
{Name: "Cod and chips", Section: "fish", Status: menu.Available, Price: "9.50", Nutrition: store.JSON(`{"kcal": 1050}`)},41
{Name: "Haddock and chips", Section: "fish", Status: menu.SoldOut, PricePence: 1050, Nutrition: `{"kcal": 980}`},45
{Name: "Haddock and chips", Section: "fish", Status: menu.SoldOut, Price: "10.50", Nutrition: store.JSON(`{"kcal": 980}`)},42
{Name: "Chips", Section: "sides", Status: menu.Available, PricePence: 350, Nutrition: `{"kcal": 560}`},46
{Name: "Chips", Section: "sides", Status: menu.Available, Price: "3.50", Nutrition: store.JSON(`{"kcal": 560}`)},43
{Name: "Mushy peas", Section: "sides", Status: menu.Available, PricePence: 175, Nutrition: `{"kcal": 120}`},47
{Name: "Mushy peas", Section: "sides", Status: menu.Available, Price: "1.75", Nutrition: store.JSON(`{"kcal": 120}`)},44
}48
}45
for _, item := range items {49
for _, item := range items {46
if err := queries.AddItem(ctx, item); err != nil {50
if err := queries.AddItem(ctx, item); err != nil {47
return fmt.Errorf("adding %s: %w", item.Name, err)51
return fmt.Errorf("adding %s: %w", item.Name, err)48
}52
}49
}53
}50
54
51
listed, err := queries.ListMenu(ctx)55
listed, err := queries.ListMenu(ctx)52
if err != nil {56
if err != nil {53
return fmt.Errorf("listing the menu: %w", err)57
return fmt.Errorf("listing the menu: %w", err)54
}58
}55
for _, item := range listed {59
for _, item := range listed {56
if item.Status == menu.SoldOut {60
if item.Status == menu.SoldOut {57
fmt.Fprintf(output, "%-18s sold out\n", item.Name)61
fmt.Fprintf(output, "%-18s sold out\n", item.Name)58
continue62
continue59
}63
}60
fmt.Fprintf(output, "%-18s %6s %4d kcal\n", item.Name, item.PricePence, item.Kcal)64
fmt.Fprintf(output, "%-18s %6s %4d kcal\n", item.Name, item.Price, item.Kcal)61
}65
}62
66
63
cheapest, err := queries.CheapestPerSection(ctx)67
cheapest, err := queries.CheapestPerSection(ctx)64
if err != nil {68
if err != nil {65
return fmt.Errorf("finding the cheapest items: %w", err)69
return fmt.Errorf("finding the cheapest items: %w", err)66
}70
}67
for _, section := range cheapest {71
for _, section := range cheapest {68
fmt.Fprintf(output, "cheapest %-6s from %s\n", section.Section, section.Cheapest)72
fmt.Fprintf(output, "cheapest %-6s from %s\n", section.Section, section.Cheapest)69
}73
}70
return nil74
return nil71
}75
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | SQLite | PostgreSQL |
|---|---|---|---|
| AddItem | parameter nutrition | string | JSON |
| CheapestPerSection | column cheapest | menu.Pence | menu.Price |