Compare · 019

Types through CTEs, window functions and views: MariaDB and DuckDB

Every file of the MariaDB and DuckDB versions side by side, with the lines that differ marked. 3 parameter or column types differ in Go.

Configuration

sgl.yamldiffers
MariaDB
DuckDB
1version: 1
1version: 1
2
2
3databases:
3databases:
4 - name: colony
4 - name: colony
5 dialect: mysql
5 dialect: duckdb
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

Migrations

db/migrations/0001_gulls.up.sqldiffers
MariaDB
DuckDB
1CREATE SEQUENCE gull_ids;
2
3CREATE SEQUENCE theft_ids;
4
1CREATE TABLE gulls (
5CREATE TABLE gulls (
2 id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
6 id BIGINT PRIMARY KEY DEFAULT nextval('gull_ids'),
3 name VARCHAR(50) NOT NULL,
7 name VARCHAR NOT NULL,
4 parent_id BIGINT,
8 parent_id BIGINT REFERENCES gulls (id),
5 beach VARCHAR(50) NOT NULL,
9 beach VARCHAR NOT NULL
6 FOREIGN KEY (parent_id) REFERENCES gulls (id)
7);
10);
8
11
9CREATE TABLE thefts (
12CREATE TABLE thefts (
10 id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
13 id BIGINT PRIMARY KEY DEFAULT nextval('theft_ids'),
11 gull_id BIGINT NOT NULL,
14 gull_id BIGINT NOT NULL REFERENCES gulls (id),
12 snack VARCHAR(50) NOT NULL,
15 snack VARCHAR NOT NULL,
13 stolen_on DATE NOT NULL,
16 stolen_on VARCHAR NOT NULL
14 FOREIGN KEY (gull_id) REFERENCES gulls (id)
15);
17);
16
18
17CREATE VIEW beach_records AS
19CREATE VIEW beach_records AS
18SELECT
20SELECT
19 gulls.beach,
21 gulls.beach,
20 COUNT(DISTINCT gulls.id) AS gulls,
22 COUNT(DISTINCT gulls.id) AS gulls,
21 COUNT(thefts.id) AS thefts,
23 COUNT(thefts.id) AS thefts,
22 MAX(thefts.stolen_on) AS last_theft
24 MAX(thefts.stolen_on) AS last_theft
23FROM gulls
25FROM gulls
24LEFT JOIN thefts ON thefts.gull_id = gulls.id
26LEFT JOIN thefts ON thefts.gull_id = gulls.id
25GROUP BY gulls.beach;
27GROUP BY gulls.beach;
db/migrations/0001_gulls.down.sqldiffers
MariaDB
DuckDB
1DROP VIEW beach_records;
1DROP VIEW beach_records;
2DROP TABLE thefts;
2DROP TABLE thefts;
3DROP TABLE gulls;
3DROP TABLE gulls;
4DROP SEQUENCE theft_ids;
5DROP SEQUENCE gull_ids;

Queries

HatchGulldiffers
MariaDB · db/queries/gulls.sql
DuckDB · db/queries/gulls.sql
1-- sgl.query(HatchGull, exec)
1-- sgl.query(HatchGull, exec)
2INSERT INTO gulls (name, parent_id, beach) VALUES (?, ?, ?);
2INSERT INTO gulls (name, parent_id, beach) VALUES ($1, $2, $3);
RecordTheftdiffers
MariaDB · db/queries/gulls.sql
DuckDB · db/queries/gulls.sql
1-- sgl.query(RecordTheft, exec)
1-- sgl.query(RecordTheft, exec)
2INSERT INTO thefts (gull_id, snack, stolen_on) VALUES (?, ?, ?);
2INSERT INTO thefts (gull_id, snack, stolen_on) VALUES ($1, $2, $3);
FamilyTreediffers
MariaDB · db/queries/gulls.sql
DuckDB · db/queries/gulls.sql
1-- sgl.query(FamilyTree, many)
1-- sgl.query(FamilyTree, many)
2WITH RECURSIVE family AS (
2WITH RECURSIVE family AS (
3 SELECT id, name, parent_id, 0 AS generation
3 SELECT id, name, parent_id, 0 AS generation
4 FROM gulls
4 FROM gulls
5 WHERE id = ?
5 WHERE id = $1
6 UNION ALL
6 UNION ALL
7 SELECT gulls.id, gulls.name, gulls.parent_id, family.generation + 1
7 SELECT gulls.id, gulls.name, gulls.parent_id, family.generation + 1
8 FROM gulls
8 FROM gulls
9 JOIN family ON gulls.parent_id = family.id
9 JOIN family ON gulls.parent_id = family.id
10)
10)
11SELECT family.name, family.generation, parents.name AS parent
11SELECT family.name, family.generation, parents.name AS parent
12FROM family
12FROM family
13LEFT JOIN gulls AS parents ON parents.id = family.parent_id
13LEFT JOIN gulls AS parents ON parents.id = family.parent_id
14ORDER BY family.generation, family.id;
14ORDER BY family.generation, family.id;
TheftLeaguediffers
MariaDB · db/queries/gulls.sql
DuckDB · db/queries/gulls.sql
1-- sgl.query(TheftLeague, many)
1-- sgl.query(TheftLeague, many)
2WITH totals AS (
2WITH totals AS (
3 SELECT gulls.name, gulls.beach, COUNT(thefts.id) AS thefts
3 SELECT gulls.name, gulls.beach, COUNT(thefts.id) AS thefts
4 FROM gulls
4 FROM gulls
5 LEFT JOIN thefts ON thefts.gull_id = gulls.id
5 LEFT JOIN thefts ON thefts.gull_id = gulls.id
6 GROUP BY gulls.id
6 GROUP BY gulls.id, gulls.name, gulls.beach
7)
7)
8SELECT
8SELECT
9 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_thefts
13 SUM(thefts) OVER (PARTITION BY beach) AS beach_thefts
14FROM totals
14FROM totals
15ORDER BY beach, beach_rank, name;
15ORDER BY beach, beach_rank, name;
BeachRecordsidentical
MariaDB · db/queries/gulls.sql
DuckDB · db/queries/gulls.sql
1-- sgl.query(BeachRecords, many)
1-- sgl.query(BeachRecords, many)
2SELECT beach, gulls, thefts, last_theft FROM beach_records ORDER BY beach;
2SELECT beach, gulls, thefts, last_theft FROM beach_records ORDER BY beach;

What sgl generates

HatchGullidentical
MariaDB
DuckDB
1func (queries *Queries) HatchGull(ctx context.Context, params HatchGullParams) error
1func (queries *Queries) HatchGull(ctx context.Context, params HatchGullParams) error
2
2
3type HatchGullParams struct {
3type HatchGullParams struct {
4 Name string
4 Name string
5 ParentID *int64
5 ParentID *int64
6 Beach string
6 Beach string
7}
7}
RecordTheftdiffers
MariaDB
DuckDB
1func (queries *Queries) RecordTheft(ctx context.Context, params RecordTheftParams) error
1func (queries *Queries) RecordTheft(ctx context.Context, params RecordTheftParams) error
2
2
3type RecordTheftParams struct {
3type RecordTheftParams struct {
4 GullID int64
4 GullID int64
5 Snack string
5 Snack string
6 StolenOn time.Time
6 StolenOn string
7}
7}
FamilyTreeidentical
MariaDB
DuckDB
1func (queries *Queries) FamilyTree(ctx context.Context, id int64) ([]FamilyTreeRow, error)
1func (queries *Queries) FamilyTree(ctx context.Context, id int64) ([]FamilyTreeRow, error)
2
2
3type FamilyTreeRow struct {
3type FamilyTreeRow struct {
4 Name string `json:"name"`
4 Name string `json:"name"`
5 Generation int32 `json:"generation"`
5 Generation int32 `json:"generation"`
6 Parent *string `json:"parent"`
6 Parent *string `json:"parent"`
7}
7}
TheftLeaguediffers
MariaDB
DuckDB
1func (queries *Queries) TheftLeague(ctx context.Context) ([]TheftLeagueRow, error)
1func (queries *Queries) TheftLeague(ctx context.Context) ([]TheftLeagueRow, error)
2
2
3type TheftLeagueRow struct {
3type 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 *big.Int `json:"beach_thefts"`
9}
9}
BeachRecordsdiffers
MariaDB
DuckDB
1func (queries *Queries) BeachRecords(ctx context.Context) ([]BeachRecordsRow, error)
1func (queries *Queries) BeachRecords(ctx context.Context) ([]BeachRecordsRow, error)
2
2
3type BeachRecordsRow struct {
3type 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 *string `json:"last_theft"`
8}
8}

The program

example.godiffers
MariaDB
DuckDB
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 "time"
9
8
10 "seagull.sh/seagull/examples/mysql/019_ctes_and_windows/internal/store"
9 "seagull.sh/seagull/examples/duckdb/019_ctes_and_windows/internal/store"
11)
10)
12
11
13// run hatches a colony of gulls, records their snack thefts and reads them back three
12// run hatches a colony of gulls, records their snack thefts and reads them back three
14// ways. Seagull follows each column's type and nullability through the SQL:
13// ways. Seagull follows each column's type and nullability through the SQL:
15//
14//
16// - FamilyTree's recursive CTE starts from one gull and joins its chicks generation by
15// - FamilyTree's recursive CTE starts from one gull and joins its chicks generation by
17// generation; generation is an integer from 0 AS generation and generation + 1, and
16// generation; generation is an integer from 0 AS generation and generation + 1, and
18// parent is *string because the LEFT JOIN finds no parent for the first gull;
17// 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() and
18// - TheftLeague counts thefts in a CTE, then ranks gulls on each beach with RANK() and
20// totals the beach with SUM() OVER; RANK is never NULL, while SUM is nullable, as SQL
19// totals the beach with SUM() OVER; RANK is a BIGINT that is never NULL, so int64.
21// sums no rows to NULL, and it is a DECIMAL, as MySQL sums integers exactly into a
20// DuckDB widens the SUM of a BIGINT to a 128-bit HUGEINT, which arrives as *big.Int,
22// DECIMAL, so BeachThefts is *apd.Decimal;
21// and nil stands for NULL, as SQL sums no rows to NULL;
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.
25//
24//
26// Takes database (*sql.DB) which is a migrated database.
25// Takes database (*sql.DB) which is a migrated database.
27// Takes output (io.Writer) which receives the three listings.
26// Takes output (io.Writer) which receives the three listings.
28//
27//
29// Returns error when a query fails.
28// Returns error when a query fails.
30func run(ctx context.Context, database *sql.DB, output io.Writer) error {
29func run(ctx context.Context, database *sql.DB, output io.Writer) error {
31 queries := store.New(database)
30 queries := store.New(database)
32
31
33 gulls := []store.HatchGullParams{
32 gulls := []store.HatchGullParams{
34 {Name: "Steven", ParentID: nil, Beach: "Porthmeor"},
33 {Name: "Steven", ParentID: nil, Beach: "Porthmeor"},
35 {Name: "Gwen", ParentID: new(int64(1)), Beach: "Porthmeor"},
34 {Name: "Gwen", ParentID: new(int64(1)), Beach: "Porthmeor"},
36 {Name: "Jago", ParentID: new(int64(1)), Beach: "Porthminster"},
35 {Name: "Jago", ParentID: new(int64(1)), Beach: "Porthminster"},
37 {Name: "Kerensa", ParentID: new(int64(2)), Beach: "Porthmeor"},
36 {Name: "Kerensa", ParentID: new(int64(2)), Beach: "Porthmeor"},
38 {Name: "Tristan", ParentID: nil, Beach: "Porthminster"},
37 {Name: "Tristan", ParentID: nil, Beach: "Porthminster"},
39 {Name: "Morwenna", ParentID: nil, Beach: "Gwithian"},
38 {Name: "Morwenna", ParentID: nil, Beach: "Gwithian"},
40 }
39 }
41 for _, gull := range gulls {
40 for _, gull := range gulls {
42 if err := queries.HatchGull(ctx, gull); err != nil {
41 if err := queries.HatchGull(ctx, gull); err != nil {
43 return fmt.Errorf("hatching %s: %w", gull.Name, err)
42 return fmt.Errorf("hatching %s: %w", gull.Name, err)
44 }
43 }
45 }
44 }
46 thefts := []store.RecordTheftParams{
45 thefts := []store.RecordTheftParams{
47 {GullID: 1, Snack: "chips", StolenOn: day(1)},
46 {GullID: 1, Snack: "chips", StolenOn: "2026-07-01"},
48 {GullID: 1, Snack: "pasty", StolenOn: day(3)},
47 {GullID: 1, Snack: "pasty", StolenOn: "2026-07-03"},
49 {GullID: 1, Snack: "ice cream", StolenOn: day(4)},
48 {GullID: 1, Snack: "ice cream", StolenOn: "2026-07-04"},
50 {GullID: 2, Snack: "chips", StolenOn: day(2)},
49 {GullID: 2, Snack: "chips", StolenOn: "2026-07-02"},
51 {GullID: 3, Snack: "pasty", StolenOn: day(2)},
50 {GullID: 3, Snack: "pasty", StolenOn: "2026-07-02"},
52 {GullID: 3, Snack: "chips", StolenOn: day(5)},
51 {GullID: 3, Snack: "chips", StolenOn: "2026-07-05"},
53 {GullID: 5, Snack: "sandwich", StolenOn: day(3)},
52 {GullID: 5, Snack: "sandwich", StolenOn: "2026-07-03"},
54 }
53 }
55 for _, theft := range thefts {
54 for _, theft := range thefts {
56 if err := queries.RecordTheft(ctx, theft); err != nil {
55 if err := queries.RecordTheft(ctx, theft); err != nil {
57 return fmt.Errorf("recording a theft: %w", err)
56 return fmt.Errorf("recording a theft: %w", err)
58 }
57 }
59 }
58 }
60
59
61 family, err := queries.FamilyTree(ctx, 1)
60 family, err := queries.FamilyTree(ctx, 1)
62 if err != nil {
61 if err != nil {
63 return fmt.Errorf("tracing Steven's family: %w", err)
62 return fmt.Errorf("tracing Steven's family: %w", err)
64 }
63 }
65 fmt.Fprintln(output, "Steven's family:")
64 fmt.Fprintln(output, "Steven's family:")
66 for _, member := range family {
65 for _, member := range family {
67 fmt.Fprintf(output, " generation %d %-8s parent %s\n", member.Generation, member.Name, orNull(member.Parent))
66 fmt.Fprintf(output, " generation %d %-8s parent %s\n", member.Generation, member.Name, orNull(member.Parent))
68 }
67 }
69
68
70 league, err := queries.TheftLeague(ctx)
69 league, err := queries.TheftLeague(ctx)
71 if err != nil {
70 if err != nil {
72 return fmt.Errorf("ranking thieves: %w", err)
71 return fmt.Errorf("ranking thieves: %w", err)
73 }
72 }
74 fmt.Fprintln(output, "theft league:")
73 fmt.Fprintln(output, "theft league:")
75 for _, entry := range league {
74 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))
75 fmt.Fprintf(output, " %-13s #%d %-8s %d of %s\n", entry.Beach, entry.BeachRank, entry.Name, entry.Thefts, entry.BeachThefts)
77 }
76 }
78
77
79 records, err := queries.BeachRecords(ctx)
78 records, err := queries.BeachRecords(ctx)
80 if err != nil {
79 if err != nil {
81 return fmt.Errorf("reading beach records: %w", err)
80 return fmt.Errorf("reading beach records: %w", err)
82 }
81 }
83 fmt.Fprintln(output, "beach records:")
82 fmt.Fprintln(output, "beach records:")
84 for _, record := range records {
83 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))
84 fmt.Fprintf(output, " %-13s gulls %d, thefts %d, last theft %s\n", record.Beach, record.Gulls, record.Thefts, orNull(record.LastTheft))
86 }
85 }
87 return nil
86 return nil
88}
87}
89
88
90// day returns midnight UTC on a day in July 2026, the value a DATE column holds.
89// orNull formats a nullable field, or returns NULL when the field is nil.
91//
92// Takes date (int) which is the day of the month.
93//
94// Returns time.Time which is the start of that day.
95func day(date int) time.Time {
96 return time.Date(2026, time.July, date, 0, 0, 0, 0, time.UTC)
97}
98
99// orNull formats a nullable field, or returns NULL when the field is nil. A date prints
100// as YYYY-MM-DD, and a decimal through its String method, which apd.Decimal declares on
101// its pointer.
102//
90//
103// Takes value (*T) which is a nullable field.
91// Takes value (*T) which is a nullable field.
104//
92//
105// Returns string which is the text to print.
93// Returns string which is the text to print.
106func orNull[T any](value *T) string {
94func orNull[T any](value *T) string {
107 if value == nil {
95 if value == nil {
108 return "NULL"
96 return "NULL"
109 }
97 }
110 switch field := any(value).(type) {
98 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}
99}

Go type differences

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

QueryFieldMariaDBDuckDB
RecordTheftparameter stolen_ontime.Timestring
TheftLeaguecolumn beach_thefts*apd.Decimal*big.Int
BeachRecordscolumn last_theft*time.Time*string