Type overrides: CockroachDB and SQLite
Every file of the CockroachDB and SQLite versions side by side, with the lines that differ marked. 2 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
CockroachDB
SQLite
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: menu4
- name: menu5
dialect: postgres5
dialect: sqlite6
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/sql12
types:11
types:13
decimal: string14
overrides:12
overrides:15
- column: menu_items.status13
- column: menu_items.status16
go: seagull.sh/seagull/examples/postgres/009_overrides/internal/menu.Status14
go: seagull.sh/seagull/examples/sqlite/009_overrides/internal/menu.StatusMigrations
db/migrations/0001_menu.up.sqldiffers
CockroachDB
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")2
CREATE TABLE menu_items (2
CREATE 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 NULL8
nutrition TEXT NOT NULL9
);9
);db/migrations/0001_menu.down.sqlidentical
CockroachDB
SQLite
1
DROP TABLE menu_items;1
DROP TABLE menu_items;Queries
AddItemdiffers
CockroachDB · db/queries/menu.sql
SQLite · db/queries/menu.sql
1
-- sgl.query(AddItem, exec)1
-- sgl.query(AddItem, exec)2
INSERT INTO menu_items (name, section, status, price, nutrition) VALUES ($1, $2, $3, $4, $5);2
INSERT INTO menu_items (name, section, status, price_pence, nutrition) VALUES (?, ?, ?, ?, ?);ListMenudiffers
CockroachDB · 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)3
SELECT name, status, price, nutrition ->> 'kcal' AS kcal3
SELECT name, status, price_pence, json_extract(nutrition, '$.kcal') AS kcal4
FROM menu_items4
FROM menu_items5
ORDER BY section, name;5
ORDER BY section, name;CheapestPerSectiondiffers
CockroachDB · 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)3
SELECT section, MIN(price) AS cheapest3
SELECT section, MIN(price_pence) 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
CockroachDB
SQLite
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
Price menu.Price7
PricePence menu.Pence8
Nutrition JSON8
Nutrition string9
}9
}10
11
type JSON []byteListMenudiffers
CockroachDB
SQLite
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
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
CockroachDB
SQLite
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.Price `json:"cheapest"`5
Cheapest menu.Pence `json:"cheapest"`6
}6
}The program
example.godiffers
CockroachDB
SQLite
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/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 types13
// run fills a chip shop menu and reads it back. Four 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;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 type17
// - sgl.column(menu_items.price_pence, go_type: ...) in the migration gives price_pence21
// 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 by23
// '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 override25
// - in CheapestPerSection, MIN(price) is a new value, so the migration's override no22
// 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, so25
// Column overrides also reach models.go and the parameters bound to those columns, so30
// 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 the28
// sgl vet catches mistakes in overrides: Q036 for a query override naming a column the33
// 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.40
func run(ctx context.Context, database *sql.DB, output io.Writer) error {36
func 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
continue58
continue63
}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 nil70
return nil75
}71
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | CockroachDB | SQLite |
|---|---|---|---|
| AddItem | parameter nutrition | JSON | string |
| CheapestPerSection | column cheapest | menu.Price | menu.Pence |