Compare · 009

Type overrides: PostgreSQL and SQLite

Every file of the PostgreSQL and SQLite versions side by side, with the lines that differ marked. 2 parameter or column types differ in Go.

Compare PostgreSQL withSQLiteCockroachDBTimescaleDB

Configuration

sgl.yamldiffers
PostgreSQL
SQLite
1version: 1
1version: 1
2
2
3databases:
3databases:
4 - name: menu
4 - name: menu
5 dialect: postgres
5 dialect: sqlite
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
12 types:
11 types:
13 decimal: string
14 overrides:
12 overrides:
15 - column: menu_items.status
13 - column: menu_items.status
16 go: seagull.sh/seagull/examples/postgres/009_overrides/internal/menu.Status
14 go: seagull.sh/seagull/examples/sqlite/009_overrides/internal/menu.Status

Migrations

db/migrations/0001_menu.up.sqldiffers
PostgreSQL
SQLite
1-- sgl.column(menu_items.price, go_type: "seagull.sh/seagull/examples/postgres/009_overrides/internal/menu.Price")
1-- sgl.column(menu_items.price_pence, go_type: "seagull.sh/seagull/examples/sqlite/009_overrides/internal/menu.Pence")
2CREATE TABLE menu_items (
2CREATE TABLE menu_items (
3 id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
3 id INTEGER 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 NUMERIC(5, 2) NOT NULL,
7 price_pence INTEGER NOT NULL,
8 nutrition JSONB NOT NULL
8 nutrition TEXT NOT NULL
9);
9);
db/migrations/0001_menu.down.sqlidentical
PostgreSQL
SQLite
1DROP TABLE menu_items;
1DROP TABLE menu_items;

Queries

AddItemdiffers
PostgreSQL · db/queries/menu.sql
SQLite · db/queries/menu.sql
1-- sgl.query(AddItem, exec)
1-- sgl.query(AddItem, exec)
2INSERT INTO menu_items (name, section, status, price, nutrition) VALUES ($1, $2, $3, $4, $5);
2INSERT INTO menu_items (name, section, status, price_pence, nutrition) VALUES (?, ?, ?, ?, ?);
ListMenudiffers
PostgreSQL · db/queries/menu.sql
SQLite · db/queries/menu.sql
1-- sgl.query(ListMenu, many)
1-- sgl.query(ListMenu, many)
2-- sgl.column(kcal, type: bigint, nullable: false)
2-- sgl.column(kcal, type: integer)
3SELECT name, status, price, nutrition ->> 'kcal' AS kcal
3SELECT name, status, price_pence, json_extract(nutrition, '$.kcal') AS kcal
4FROM menu_items
4FROM menu_items
5ORDER BY section, name;
5ORDER BY section, name;
CheapestPerSectiondiffers
PostgreSQL · db/queries/menu.sql
SQLite · db/queries/menu.sql
1-- sgl.query(CheapestPerSection, many)
1-- sgl.query(CheapestPerSection, many)
2-- sgl.column(cheapest, go_type: "seagull.sh/seagull/examples/postgres/009_overrides/internal/menu.Price", nullable: false)
2-- sgl.column(cheapest, go_type: "seagull.sh/seagull/examples/sqlite/009_overrides/internal/menu.Pence", nullable: false)
3SELECT section, MIN(price) AS cheapest
3SELECT section, MIN(price_pence) AS cheapest
4FROM menu_items
4FROM menu_items
5GROUP BY section
5GROUP BY section
6ORDER BY section;
6ORDER BY section;

What sgl generates

AddItemdiffers
PostgreSQL
SQLite
1func (queries *Queries) AddItem(ctx context.Context, params AddItemParams) error
1func (queries *Queries) AddItem(ctx context.Context, params AddItemParams) error
2
2
3type AddItemParams struct {
3type AddItemParams struct {
4 Name string
4 Name string
5 Section string
5 Section string
6 Status menu.Status
6 Status menu.Status
7 Price menu.Price
7 PricePence menu.Pence
8 Nutrition JSON
8 Nutrition string
9}
9}
10
11type JSON []byte
ListMenudiffers
PostgreSQL
SQLite
1func (queries *Queries) ListMenu(ctx context.Context) ([]ListMenuRow, error)
1func (queries *Queries) ListMenu(ctx context.Context) ([]ListMenuRow, error)
2
2
3type ListMenuRow struct {
3type 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 Price menu.Price `json:"price"`
6 PricePence menu.Pence `json:"price_pence"`
7 Kcal int64 `json:"kcal"`
7 Kcal int64 `json:"kcal"`
8}
8}
CheapestPerSectiondiffers
PostgreSQL
SQLite
1func (queries *Queries) CheapestPerSection(ctx context.Context) ([]CheapestPerSectionRow, error)
1func (queries *Queries) CheapestPerSection(ctx context.Context) ([]CheapestPerSectionRow, error)
2
2
3type CheapestPerSectionRow struct {
3type CheapestPerSectionRow struct {
4 Section string `json:"section"`
4 Section string `json:"section"`
5 Cheapest menu.Price `json:"cheapest"`
5 Cheapest menu.Pence `json:"cheapest"`
6}
6}

The program

example.godiffers
PostgreSQL
SQLite
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/009_overrides/internal/menu"
9 "seagull.sh/seagull/examples/sqlite/009_overrides/internal/menu"
10 "seagull.sh/seagull/examples/postgres/009_overrides/internal/store"
10 "seagull.sh/seagull/examples/sqlite/009_overrides/internal/store"
11)
11)
12
12
13// run fills a chip shop menu and reads it back. Five overrides replace the Go types
13// run fills a chip shop menu and reads it back. Four overrides replace the Go types
14// Seagull would infer:
14// Seagull would infer:
15//
15//
16// - sgl.yaml's types.decimal slot maps every NUMERIC to string, the server's exact
17// text, in place of apd.Decimal, so the module needs no decimal package; a slot
18// changes models.go and parameters as well as returned columns;
19// - sgl.yaml's types.overrides gives menu_items.status the type menu.Status;
16// - sgl.yaml's types.overrides gives menu_items.status the type menu.Status;
20// - sgl.column(menu_items.price, go_type: ...) in the migration gives price the type
17// - sgl.column(menu_items.price_pence, go_type: ...) in the migration gives price_pence
21// menu.Price in every query that returns the column as it is;
18// the type menu.Pence in every query that returns the column as it is;
22// - sgl.column(kcal, type: bigint, nullable: false) in ListMenu types nutrition ->>
19// - sgl.column(kcal, type: integer) in ListMenu types json_extract, which is any by
23// 'kcal', which is nullable text, as the number every document holds; a cast,
20// default because its type depends on the document;
24// (nutrition ->> 'kcal')::bigint, would type it too, and make the server check it;
21// - in CheapestPerSection, MIN(price_pence) is a new value, so the migration's override
25// - in CheapestPerSection, MIN(price) is a new value, so the migration's override no
22// no longer applies and the aggregate could be NULL; sgl.column(cheapest, go_type:
26// longer applies and the aggregate could be NULL; sgl.column(cheapest, go_type: ...,
23// ..., nullable: false) makes it menu.Pence again, as every group has a row.
27// nullable: false) makes it menu.Price again, as every group has a row.
28//
24//
29// Column overrides also reach models.go and the parameters bound to those columns, so
25// Column overrides also reach models.go and the parameters bound to those columns, so
30// AddItemParams takes a menu.Status and a menu.Price.
26// AddItemParams takes a menu.Status and a menu.Pence.
31//
27//
32// sgl vet catches mistakes in overrides: Q036 for a query override naming a column the
28// sgl vet catches mistakes in overrides: Q036 for a query override naming a column the
33// query does not return, Q037 for an override naming a table column that does not exist,
29// query does not return, Q037 for an override naming a table column that does not exist,
34// Q038 for type: and go_type: together, and Q049 for a go_type the generator cannot use.
30// Q038 for type: and go_type: together, and Q049 for a go_type the generator cannot use.
35//
31//
36// Takes database (*sql.DB) which is a migrated database.
32// Takes database (*sql.DB) which is a migrated database.
37// Takes output (io.Writer) which receives the menu and the cheapest item per section.
33// Takes output (io.Writer) which receives the menu and the cheapest item per section.
38//
34//
39// Returns error when a query fails.
35// Returns error when a query fails.
40func run(ctx context.Context, database *sql.DB, output io.Writer) error {
36func run(ctx context.Context, database *sql.DB, output io.Writer) error {
41 queries := store.New(database)
37 queries := store.New(database)
42
38
43 items := []store.AddItemParams{
39 items := []store.AddItemParams{
44 {Name: "Cod and chips", Section: "fish", Status: menu.Available, Price: "9.50", Nutrition: store.JSON(`{"kcal": 1050}`)},
40 {Name: "Cod and chips", Section: "fish", Status: menu.Available, PricePence: 950, Nutrition: `{"kcal": 1050}`},
45 {Name: "Haddock and chips", Section: "fish", Status: menu.SoldOut, Price: "10.50", Nutrition: store.JSON(`{"kcal": 980}`)},
41 {Name: "Haddock and chips", Section: "fish", Status: menu.SoldOut, PricePence: 1050, Nutrition: `{"kcal": 980}`},
46 {Name: "Chips", Section: "sides", Status: menu.Available, Price: "3.50", Nutrition: store.JSON(`{"kcal": 560}`)},
42 {Name: "Chips", Section: "sides", Status: menu.Available, PricePence: 350, Nutrition: `{"kcal": 560}`},
47 {Name: "Mushy peas", Section: "sides", Status: menu.Available, Price: "1.75", Nutrition: store.JSON(`{"kcal": 120}`)},
43 {Name: "Mushy peas", Section: "sides", Status: menu.Available, PricePence: 175, Nutrition: `{"kcal": 120}`},
48 }
44 }
49 for _, item := range items {
45 for _, item := range items {
50 if err := queries.AddItem(ctx, item); err != nil {
46 if err := queries.AddItem(ctx, item); err != nil {
51 return fmt.Errorf("adding %s: %w", item.Name, err)
47 return fmt.Errorf("adding %s: %w", item.Name, err)
52 }
48 }
53 }
49 }
54
50
55 listed, err := queries.ListMenu(ctx)
51 listed, err := queries.ListMenu(ctx)
56 if err != nil {
52 if err != nil {
57 return fmt.Errorf("listing the menu: %w", err)
53 return fmt.Errorf("listing the menu: %w", err)
58 }
54 }
59 for _, item := range listed {
55 for _, item := range listed {
60 if item.Status == menu.SoldOut {
56 if item.Status == menu.SoldOut {
61 fmt.Fprintf(output, "%-18s sold out\n", item.Name)
57 fmt.Fprintf(output, "%-18s sold out\n", item.Name)
62 continue
58 continue
63 }
59 }
64 fmt.Fprintf(output, "%-18s %6s %4d kcal\n", item.Name, item.Price, item.Kcal)
60 fmt.Fprintf(output, "%-18s %6s %4d kcal\n", item.Name, item.PricePence, item.Kcal)
65 }
61 }
66
62
67 cheapest, err := queries.CheapestPerSection(ctx)
63 cheapest, err := queries.CheapestPerSection(ctx)
68 if err != nil {
64 if err != nil {
69 return fmt.Errorf("finding the cheapest items: %w", err)
65 return fmt.Errorf("finding the cheapest items: %w", err)
70 }
66 }
71 for _, section := range cheapest {
67 for _, section := range cheapest {
72 fmt.Fprintf(output, "cheapest %-6s from %s\n", section.Section, section.Cheapest)
68 fmt.Fprintf(output, "cheapest %-6s from %s\n", section.Section, section.Cheapest)
73 }
69 }
74 return nil
70 return nil
75}
71}

Go type differences

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

QueryFieldPostgreSQLSQLite
AddItemparameter nutritionJSONstring
CheapestPerSectioncolumn cheapestmenu.Pricemenu.Pence