Types through CTEs, window functions and views: MySQL and CockroachDB
Every file of the MySQL and CockroachDB versions side by side, with the lines that differ marked. 1 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
MySQL
CockroachDB
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: colony4
- name: colony5
dialect: mysql5
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/sql12
migrate:13
dsn: ${env:DATABASE_URL}Migrations
db/migrations/0001_gulls.up.sqldiffers
MySQL
CockroachDB
1
CREATE TABLE gulls (1
CREATE TABLE gulls (2
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,2
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,3
name VARCHAR(50) NOT NULL,3
name TEXT NOT NULL,4
parent_id BIGINT,4
parent_id BIGINT REFERENCES gulls (id),5
beach VARCHAR(50) NOT NULL,5
beach TEXT NOT NULL6
FOREIGN KEY (parent_id) REFERENCES gulls (id)7
);6
);8
7
9
CREATE TABLE thefts (8
CREATE TABLE thefts (10
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,9
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,11
gull_id BIGINT NOT NULL,10
gull_id BIGINT NOT NULL REFERENCES gulls (id),12
snack VARCHAR(50) NOT NULL,11
snack TEXT NOT NULL,13
stolen_on DATE NOT NULL,12
stolen_on DATE NOT NULL14
FOREIGN KEY (gull_id) REFERENCES gulls (id)15
);13
);16
14
17
CREATE VIEW beach_records AS15
CREATE VIEW beach_records AS18
SELECT16
SELECT19
gulls.beach,17
gulls.beach,20
COUNT(DISTINCT gulls.id) AS gulls,18
COUNT(DISTINCT gulls.id) AS gulls,21
COUNT(thefts.id) AS thefts,19
COUNT(thefts.id) AS thefts,22
MAX(thefts.stolen_on) AS last_theft20
MAX(thefts.stolen_on) AS last_theft23
FROM gulls21
FROM gulls24
LEFT JOIN thefts ON thefts.gull_id = gulls.id22
LEFT JOIN thefts ON thefts.gull_id = gulls.id25
GROUP BY gulls.beach;23
GROUP BY gulls.beach;db/migrations/0001_gulls.down.sqlidentical
MySQL
CockroachDB
1
DROP VIEW beach_records;1
DROP VIEW beach_records;2
DROP TABLE thefts;2
DROP TABLE thefts;3
DROP TABLE gulls;3
DROP TABLE gulls;Queries
HatchGulldiffers
MySQL · db/queries/gulls.sql
CockroachDB · db/queries/gulls.sql
1
-- sgl.query(HatchGull, exec)1
-- sgl.query(HatchGull, exec)2
INSERT INTO gulls (name, parent_id, beach) VALUES (?, ?, ?);2
INSERT INTO gulls (name, parent_id, beach) VALUES ($1, $2, $3);RecordTheftdiffers
MySQL · db/queries/gulls.sql
CockroachDB · db/queries/gulls.sql
1
-- sgl.query(RecordTheft, exec)1
-- sgl.query(RecordTheft, exec)2
INSERT INTO thefts (gull_id, snack, stolen_on) VALUES (?, ?, ?);2
INSERT INTO thefts (gull_id, snack, stolen_on) VALUES ($1, $2, $3);FamilyTreediffers
MySQL · db/queries/gulls.sql
CockroachDB · db/queries/gulls.sql
1
-- sgl.query(FamilyTree, many)1
-- sgl.query(FamilyTree, many)2
WITH RECURSIVE family AS (2
WITH RECURSIVE descendants AS (3
SELECT id, name, parent_id, 0 AS generation3
SELECT id, name, parent_id, 0::BIGINT AS generation4
FROM gulls4
FROM gulls5
WHERE id = ?5
WHERE id = $16
UNION ALL6
UNION ALL7
SELECT gulls.id, gulls.name, gulls.parent_id, family.generation + 17
SELECT gulls.id, gulls.name, gulls.parent_id, descendants.generation + 18
FROM gulls8
FROM gulls9
JOIN family ON gulls.parent_id = family.id9
JOIN descendants ON gulls.parent_id = descendants.id10
)10
)11
SELECT family.name, family.generation, parents.name AS parent11
SELECT descendants.name, descendants.generation, parents.name AS parent12
FROM family12
FROM descendants13
LEFT JOIN gulls AS parents ON parents.id = family.parent_id13
LEFT JOIN gulls AS parents ON parents.id = descendants.parent_id14
ORDER BY family.generation, family.id;14
ORDER BY descendants.generation, descendants.id;TheftLeagueidentical
MySQL · db/queries/gulls.sql
CockroachDB · db/queries/gulls.sql
1
-- sgl.query(TheftLeague, many)1
-- sgl.query(TheftLeague, many)2
WITH totals AS (2
WITH totals AS (3
SELECT gulls.name, gulls.beach, COUNT(thefts.id) AS thefts3
SELECT gulls.name, gulls.beach, COUNT(thefts.id) AS thefts4
FROM gulls4
FROM gulls5
LEFT JOIN thefts ON thefts.gull_id = gulls.id5
LEFT JOIN thefts ON thefts.gull_id = gulls.id6
GROUP BY gulls.id6
GROUP BY gulls.id7
)7
)8
SELECT8
SELECT9
name,9
name,10
beach,10
beach,11
thefts,11
thefts,12
RANK() OVER (PARTITION BY beach ORDER BY thefts DESC) AS beach_rank,12
RANK() OVER (PARTITION BY beach ORDER BY thefts DESC) AS beach_rank,13
SUM(thefts) OVER (PARTITION BY beach) AS beach_thefts13
SUM(thefts) OVER (PARTITION BY beach) AS beach_thefts14
FROM totals14
FROM totals15
ORDER BY beach, beach_rank, name;15
ORDER BY beach, beach_rank, name;BeachRecordsdiffers
MySQL · db/queries/gulls.sql
CockroachDB · db/queries/gulls.sql
1
-- sgl.query(BeachRecords, many)1
-- sgl.query(BeachRecords, many)2
SELECT beach, gulls, thefts, last_theft FROM beach_records ORDER BY beach;2
SELECT * FROM beach_records ORDER BY beach;What sgl generates
HatchGullidentical
MySQL
CockroachDB
1
func (queries *Queries) HatchGull(ctx context.Context, params HatchGullParams) error1
func (queries *Queries) HatchGull(ctx context.Context, params HatchGullParams) error2
2
3
type HatchGullParams struct {3
type HatchGullParams struct {4
Name string4
Name string5
ParentID *int645
ParentID *int646
Beach string6
Beach string7
}7
}RecordTheftidentical
MySQL
CockroachDB
1
func (queries *Queries) RecordTheft(ctx context.Context, params RecordTheftParams) error1
func (queries *Queries) RecordTheft(ctx context.Context, params RecordTheftParams) error2
2
3
type RecordTheftParams struct {3
type RecordTheftParams struct {4
GullID int644
GullID int645
Snack string5
Snack string6
StolenOn time.Time6
StolenOn time.Time7
}7
}FamilyTreediffers
MySQL
CockroachDB
1
func (queries *Queries) FamilyTree(ctx context.Context, id int64) ([]FamilyTreeRow, error)1
func (queries *Queries) FamilyTree(ctx context.Context, id int64) ([]FamilyTreeRow, error)2
2
3
type FamilyTreeRow struct {3
type FamilyTreeRow struct {4
Name string `json:"name"`4
Name string `json:"name"`5
Generation int32 `json:"generation"`5
Generation int64 `json:"generation"`6
Parent *string `json:"parent"`6
Parent *string `json:"parent"`7
}7
}TheftLeagueidentical
MySQL
CockroachDB
1
func (queries *Queries) TheftLeague(ctx context.Context) ([]TheftLeagueRow, error)1
func (queries *Queries) TheftLeague(ctx context.Context) ([]TheftLeagueRow, error)2
2
3
type TheftLeagueRow struct {3
type TheftLeagueRow struct {4
Name string `json:"name"`4
Name string `json:"name"`5
Beach string `json:"beach"`5
Beach string `json:"beach"`6
Thefts int64 `json:"thefts"`6
Thefts int64 `json:"thefts"`7
BeachRank int64 `json:"beach_rank"`7
BeachRank int64 `json:"beach_rank"`8
BeachThefts *apd.Decimal `json:"beach_thefts"`8
BeachThefts *apd.Decimal `json:"beach_thefts"`9
}9
}BeachRecordsidentical
MySQL
CockroachDB
1
func (queries *Queries) BeachRecords(ctx context.Context) ([]BeachRecordsRow, error)1
func (queries *Queries) BeachRecords(ctx context.Context) ([]BeachRecordsRow, error)2
2
3
type BeachRecordsRow struct {3
type BeachRecordsRow struct {4
Beach string `json:"beach"`4
Beach string `json:"beach"`5
Gulls int64 `json:"gulls"`5
Gulls int64 `json:"gulls"`6
Thefts int64 `json:"thefts"`6
Thefts int64 `json:"thefts"`7
LastTheft *time.Time `json:"last_theft"`7
LastTheft *time.Time `json:"last_theft"`8
}8
}The program
example.godiffers
MySQL
CockroachDB
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
"time"8
"time"9
9
10
"seagull.sh/seagull/examples/mysql/019_ctes_and_windows/internal/store"10
"seagull.sh/seagull/examples/postgres/019_ctes_and_windows/internal/store"11
)11
)12
12
13
// run hatches a colony of gulls, records their snack thefts and reads them back three13
// run hatches a colony of gulls, records their snack thefts and reads them back three14
// ways. Seagull follows each column's type and nullability through the SQL:14
// ways. Seagull follows each column's type and nullability through the SQL:15
//15
//16
// - FamilyTree's recursive CTE starts from one gull and joins its chicks generation by16
// - FamilyTree's recursive CTE starts from one gull and joins its chicks generation by17
// generation; generation is an integer from 0 AS generation and generation + 1, and17
// generation; generation is an int64 from 0::BIGINT AS generation and generation + 1,18
// parent is *string because the LEFT JOIN finds no parent for the first gull;18
// and parent is *string because the LEFT JOIN finds no parent for the first gull;19
// - TheftLeague counts thefts in a CTE, then ranks gulls on each beach with RANK() and19
// - TheftLeague counts thefts in a CTE, then ranks gulls on each beach with RANK() and20
// totals the beach with SUM() OVER; RANK is never NULL, while SUM is nullable, as SQL20
// totals the beach with SUM() OVER; COUNT and RANK are bigint, so int64, while SUM of21
// sums no rows to NULL, and it is a DECIMAL, as MySQL sums integers exactly into a21
// a bigint is numeric in PostgreSQL, so BeachThefts is a nullable *apd.Decimal;22
// DECIMAL, so BeachThefts is *apd.Decimal;23
// - BeachRecords reads the beach_records view from the migration as if it were a table,22
// - BeachRecords reads the beach_records view from the migration as if it were a table,24
// and its columns keep the types the view's own SELECT gives them.23
// and its columns keep the types the view's own SELECT gives them: MAX over a DATE24
// column is a *time.Time, nil for a beach with no thefts. The query is written as25
// SELECT *, and the generated SQL names the view's four columns instead, so a26
// migration that later adds a column to the view does not break the code already27
// deployed, which scans exactly those four. go.keep_stars: true in sgl.yaml keeps the28
// star as written.25
//29
//26
// Takes database (*sql.DB) which is a migrated database.30
// Takes database (*sql.DB) which is a migrated database.27
// Takes output (io.Writer) which receives the three listings.31
// Takes output (io.Writer) which receives the three listings.28
//32
//29
// Returns error when a query fails.33
// Returns error when a query fails.30
func run(ctx context.Context, database *sql.DB, output io.Writer) error {34
func run(ctx context.Context, database *sql.DB, output io.Writer) error {31
queries := store.New(database)35
queries := store.New(database)32
36
33
gulls := []store.HatchGullParams{37
gulls := []store.HatchGullParams{34
{Name: "Steven", ParentID: nil, Beach: "Porthmeor"},38
{Name: "Steven", ParentID: nil, Beach: "Porthmeor"},35
{Name: "Gwen", ParentID: new(int64(1)), Beach: "Porthmeor"},39
{Name: "Gwen", ParentID: new(int64(1)), Beach: "Porthmeor"},36
{Name: "Jago", ParentID: new(int64(1)), Beach: "Porthminster"},40
{Name: "Jago", ParentID: new(int64(1)), Beach: "Porthminster"},37
{Name: "Kerensa", ParentID: new(int64(2)), Beach: "Porthmeor"},41
{Name: "Kerensa", ParentID: new(int64(2)), Beach: "Porthmeor"},38
{Name: "Tristan", ParentID: nil, Beach: "Porthminster"},42
{Name: "Tristan", ParentID: nil, Beach: "Porthminster"},39
{Name: "Morwenna", ParentID: nil, Beach: "Gwithian"},43
{Name: "Morwenna", ParentID: nil, Beach: "Gwithian"},40
}44
}41
for _, gull := range gulls {45
for _, gull := range gulls {42
if err := queries.HatchGull(ctx, gull); err != nil {46
if err := queries.HatchGull(ctx, gull); err != nil {43
return fmt.Errorf("hatching %s: %w", gull.Name, err)47
return fmt.Errorf("hatching %s: %w", gull.Name, err)44
}48
}45
}49
}46
thefts := []store.RecordTheftParams{50
thefts := []store.RecordTheftParams{47
{GullID: 1, Snack: "chips", StolenOn: day(1)},51
{GullID: 1, Snack: "chips", StolenOn: july(1)},48
{GullID: 1, Snack: "pasty", StolenOn: day(3)},52
{GullID: 1, Snack: "pasty", StolenOn: july(3)},49
{GullID: 1, Snack: "ice cream", StolenOn: day(4)},53
{GullID: 1, Snack: "ice cream", StolenOn: july(4)},50
{GullID: 2, Snack: "chips", StolenOn: day(2)},54
{GullID: 2, Snack: "chips", StolenOn: july(2)},51
{GullID: 3, Snack: "pasty", StolenOn: day(2)},55
{GullID: 3, Snack: "pasty", StolenOn: july(2)},52
{GullID: 3, Snack: "chips", StolenOn: day(5)},56
{GullID: 3, Snack: "chips", StolenOn: july(5)},53
{GullID: 5, Snack: "sandwich", StolenOn: day(3)},57
{GullID: 5, Snack: "sandwich", StolenOn: july(3)},54
}58
}55
for _, theft := range thefts {59
for _, theft := range thefts {56
if err := queries.RecordTheft(ctx, theft); err != nil {60
if err := queries.RecordTheft(ctx, theft); err != nil {57
return fmt.Errorf("recording a theft: %w", err)61
return fmt.Errorf("recording a theft: %w", err)58
}62
}59
}63
}60
64
61
family, err := queries.FamilyTree(ctx, 1)65
family, err := queries.FamilyTree(ctx, 1)62
if err != nil {66
if err != nil {63
return fmt.Errorf("tracing Steven's family: %w", err)67
return fmt.Errorf("tracing Steven's family: %w", err)64
}68
}65
fmt.Fprintln(output, "Steven's family:")69
fmt.Fprintln(output, "Steven's family:")66
for _, member := range family {70
for _, member := range family {67
fmt.Fprintf(output, " generation %d %-8s parent %s\n", member.Generation, member.Name, orNull(member.Parent))71
fmt.Fprintf(output, " generation %d %-8s parent %s\n", member.Generation, member.Name, orNull(member.Parent))68
}72
}69
73
70
league, err := queries.TheftLeague(ctx)74
league, err := queries.TheftLeague(ctx)71
if err != nil {75
if err != nil {72
return fmt.Errorf("ranking thieves: %w", err)76
return fmt.Errorf("ranking thieves: %w", err)73
}77
}74
fmt.Fprintln(output, "theft league:")78
fmt.Fprintln(output, "theft league:")75
for _, entry := range league {79
for _, entry := range league {76
fmt.Fprintf(output, " %-13s #%d %-8s %d of %s\n", entry.Beach, entry.BeachRank, entry.Name, entry.Thefts, orNull(entry.BeachThefts))80
beachThefts := "NULL"81
if entry.BeachThefts != nil {82
beachThefts = entry.BeachThefts.String()83
}84
fmt.Fprintf(output, " %-13s #%d %-8s %d of %s\n", entry.Beach, entry.BeachRank, entry.Name, entry.Thefts, beachThefts)77
}85
}78
86
79
records, err := queries.BeachRecords(ctx)87
records, err := queries.BeachRecords(ctx)80
if err != nil {88
if err != nil {81
return fmt.Errorf("reading beach records: %w", err)89
return fmt.Errorf("reading beach records: %w", err)82
}90
}83
fmt.Fprintln(output, "beach records:")91
fmt.Fprintln(output, "beach records:")84
for _, record := range records {92
for _, record := range records {85
fmt.Fprintf(output, " %-13s gulls %d, thefts %d, last theft %s\n", record.Beach, record.Gulls, record.Thefts, orNull(record.LastTheft))93
lastTheft := "NULL"94
if record.LastTheft != nil {95
lastTheft = record.LastTheft.Format(time.DateOnly)96
}97
fmt.Fprintf(output, " %-13s gulls %d, thefts %d, last theft %s\n", record.Beach, record.Gulls, record.Thefts, lastTheft)86
}98
}87
return nil99
return nil88
}100
}89
101
90
// day returns midnight UTC on a day in July 2026, the value a DATE column holds.102
// july returns a day in July 2026, the month the thefts happen in.91
//103
//92
// Takes date (int) which is the day of the month.104
// Takes day (int) which is the day of the month.93
//105
//94
// Returns time.Time which is the start of that day.106
// Returns time.Time which is midnight UTC on that day, as a DATE column holds it.95
func day(date int) time.Time {107
func july(day int) time.Time {96
return time.Date(2026, time.July, date, 0, 0, 0, 0, time.UTC)108
return time.Date(2026, time.July, day, 0, 0, 0, 0, time.UTC)97
}109
}98
110
99
// orNull formats a nullable field, or returns NULL when the field is nil. A date prints111
// orNull formats a nullable field, or returns NULL when the field is nil.100
// as YYYY-MM-DD, and a decimal through its String method, which apd.Decimal declares on101
// its pointer.102
//112
//103
// Takes value (*T) which is a nullable field.113
// Takes value (*T) which is a nullable field.104
//114
//105
// Returns string which is the text to print.115
// Returns string which is the text to print.106
func orNull[T any](value *T) string {116
func orNull[T any](value *T) string {107
if value == nil {117
if value == nil {108
return "NULL"118
return "NULL"109
}119
}110
switch field := any(value).(type) {120
return fmt.Sprint(*value)111
case *time.Time:112
return field.Format(time.DateOnly)113
case fmt.Stringer:114
return field.String()115
default:116
return fmt.Sprint(*value)117
}118
}121
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | MySQL | CockroachDB |
|---|---|---|---|
| FamilyTree | column generation | int32 | int64 |