Types through CTEs, window functions and views: DuckDB and SQLite
Every file of the DuckDB and SQLite versions side by side, with the lines that differ marked. 2 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
DuckDB
SQLite
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: colony4
- name: colony5
dialect: duckdb5
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/storeMigrations
db/migrations/0001_gulls.up.sqldiffers
DuckDB
SQLite
1
CREATE SEQUENCE gull_ids;2
3
CREATE SEQUENCE theft_ids;4
5
CREATE TABLE gulls (1
CREATE TABLE gulls (6
id BIGINT PRIMARY KEY DEFAULT nextval('gull_ids'),2
id INTEGER PRIMARY KEY,7
name VARCHAR NOT NULL,3
name TEXT NOT NULL,8
parent_id BIGINT REFERENCES gulls (id),4
parent_id INTEGER REFERENCES gulls (id),9
beach VARCHAR NOT NULL5
beach TEXT NOT NULL10
);6
);11
7
12
CREATE TABLE thefts (8
CREATE TABLE thefts (13
id BIGINT PRIMARY KEY DEFAULT nextval('theft_ids'),9
id INTEGER PRIMARY KEY,14
gull_id BIGINT NOT NULL REFERENCES gulls (id),10
gull_id INTEGER NOT NULL REFERENCES gulls (id),15
snack VARCHAR NOT NULL,11
snack TEXT NOT NULL,16
stolen_on VARCHAR NOT NULL12
stolen_on TEXT NOT NULL17
);13
);18
14
19
CREATE VIEW beach_records AS15
CREATE VIEW beach_records AS20
SELECT16
SELECT21
gulls.beach,17
gulls.beach,22
COUNT(DISTINCT gulls.id) AS gulls,18
COUNT(DISTINCT gulls.id) AS gulls,23
COUNT(thefts.id) AS thefts,19
COUNT(thefts.id) AS thefts,24
MAX(thefts.stolen_on) AS last_theft20
MAX(thefts.stolen_on) AS last_theft25
FROM gulls21
FROM gulls26
LEFT JOIN thefts ON thefts.gull_id = gulls.id22
LEFT JOIN thefts ON thefts.gull_id = gulls.id27
GROUP BY gulls.beach;23
GROUP BY gulls.beach;db/migrations/0001_gulls.down.sqldiffers
DuckDB
SQLite
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;4
DROP SEQUENCE theft_ids;5
DROP SEQUENCE gull_ids;Queries
HatchGulldiffers
DuckDB · db/queries/gulls.sql
SQLite · db/queries/gulls.sql
1
-- sgl.query(HatchGull, exec)1
-- sgl.query(HatchGull, exec)2
INSERT INTO gulls (name, parent_id, beach) VALUES ($1, $2, $3);2
INSERT INTO gulls (name, parent_id, beach) VALUES (?, ?, ?);RecordTheftdiffers
DuckDB · db/queries/gulls.sql
SQLite · db/queries/gulls.sql
1
-- sgl.query(RecordTheft, exec)1
-- sgl.query(RecordTheft, exec)2
INSERT INTO thefts (gull_id, snack, stolen_on) VALUES ($1, $2, $3);2
INSERT INTO thefts (gull_id, snack, stolen_on) VALUES (?, ?, ?);FamilyTreediffers
DuckDB · db/queries/gulls.sql
SQLite · db/queries/gulls.sql
1
-- sgl.query(FamilyTree, many)1
-- sgl.query(FamilyTree, many)2
WITH RECURSIVE family AS (2
WITH RECURSIVE family AS (3
SELECT id, name, parent_id, 0 AS generation3
SELECT id, name, parent_id, 0 AS generation4
FROM gulls4
FROM gulls5
WHERE id = $15
WHERE id = ?6
UNION ALL6
UNION ALL7
SELECT gulls.id, gulls.name, gulls.parent_id, family.generation + 17
SELECT gulls.id, gulls.name, gulls.parent_id, family.generation + 18
FROM gulls8
FROM gulls9
JOIN family ON gulls.parent_id = family.id9
JOIN family ON gulls.parent_id = family.id10
)10
)11
SELECT family.name, family.generation, parents.name AS parent11
SELECT family.name, family.generation, parents.name AS parent12
FROM family12
FROM family13
LEFT JOIN gulls AS parents ON parents.id = family.parent_id13
LEFT JOIN gulls AS parents ON parents.id = family.parent_id14
ORDER BY family.generation, family.id;14
ORDER BY family.generation, family.id;TheftLeaguediffers
DuckDB · db/queries/gulls.sql
SQLite · 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.id, gulls.name, gulls.beach6
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;BeachRecordsidentical
DuckDB · db/queries/gulls.sql
SQLite · 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 beach, gulls, thefts, last_theft FROM beach_records ORDER BY beach;What sgl generates
HatchGullidentical
DuckDB
SQLite
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
DuckDB
SQLite
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 string6
StolenOn string7
}7
}FamilyTreediffers
DuckDB
SQLite
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
}TheftLeaguediffers
DuckDB
SQLite
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 *big.Int `json:"beach_thefts"`8
BeachThefts *int64 `json:"beach_thefts"`9
}9
}BeachRecordsidentical
DuckDB
SQLite
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 *string `json:"last_theft"`7
LastTheft *string `json:"last_theft"`8
}8
}The program
example.godiffers
DuckDB
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/duckdb/019_ctes_and_windows/internal/store"9
"seagull.sh/seagull/examples/sqlite/019_ctes_and_windows/internal/store"10
)10
)11
11
12
// run hatches a colony of gulls, records their snack thefts and reads them back three12
// run hatches a colony of gulls, records their snack thefts and reads them back three13
// 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:14
//14
//15
// - FamilyTree's recursive CTE starts from one gull and joins its chicks generation by15
// - FamilyTree's recursive CTE starts from one gull and joins its chicks generation by16
// generation; generation is an integer from 0 AS generation and generation + 1, and16
// generation; generation is an integer from 0 AS generation and generation + 1, and17
// 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;18
// - TheftLeague counts thefts in a CTE, then ranks gulls on each beach with RANK() and18
// - TheftLeague counts thefts in a CTE, then ranks gulls on each beach with RANK() and19
// totals the beach with SUM() OVER; RANK is a BIGINT that is never NULL, so int64.19
// totals the beach with SUM() OVER; RANK is never NULL, while SUM is nullable, as SQL20
// DuckDB widens the SUM of a BIGINT to a 128-bit HUGEINT, which arrives as *big.Int,20
// sums no rows to NULL;21
// and nil stands for NULL, as SQL sums no rows to NULL;22
// - BeachRecords reads the beach_records view from the migration as if it were a table,21
// - BeachRecords reads the beach_records view from the migration as if it were a table,23
// and its columns keep the types the view's own SELECT gives them.22
// and its columns keep the types the view's own SELECT gives them.24
//23
//25
// Takes database (*sql.DB) which is a migrated database.24
// Takes database (*sql.DB) which is a migrated database.26
// Takes output (io.Writer) which receives the three listings.25
// Takes output (io.Writer) which receives the three listings.27
//26
//28
// Returns error when a query fails.27
// Returns error when a query fails.29
func run(ctx context.Context, database *sql.DB, output io.Writer) error {28
func run(ctx context.Context, database *sql.DB, output io.Writer) error {30
queries := store.New(database)29
queries := store.New(database)31
30
32
gulls := []store.HatchGullParams{31
gulls := []store.HatchGullParams{33
{Name: "Steven", ParentID: nil, Beach: "Porthmeor"},32
{Name: "Steven", ParentID: nil, Beach: "Porthmeor"},34
{Name: "Gwen", ParentID: new(int64(1)), Beach: "Porthmeor"},33
{Name: "Gwen", ParentID: new(int64(1)), Beach: "Porthmeor"},35
{Name: "Jago", ParentID: new(int64(1)), Beach: "Porthminster"},34
{Name: "Jago", ParentID: new(int64(1)), Beach: "Porthminster"},36
{Name: "Kerensa", ParentID: new(int64(2)), Beach: "Porthmeor"},35
{Name: "Kerensa", ParentID: new(int64(2)), Beach: "Porthmeor"},37
{Name: "Tristan", ParentID: nil, Beach: "Porthminster"},36
{Name: "Tristan", ParentID: nil, Beach: "Porthminster"},38
{Name: "Morwenna", ParentID: nil, Beach: "Gwithian"},37
{Name: "Morwenna", ParentID: nil, Beach: "Gwithian"},39
}38
}40
for _, gull := range gulls {39
for _, gull := range gulls {41
if err := queries.HatchGull(ctx, gull); err != nil {40
if err := queries.HatchGull(ctx, gull); err != nil {42
return fmt.Errorf("hatching %s: %w", gull.Name, err)41
return fmt.Errorf("hatching %s: %w", gull.Name, err)43
}42
}44
}43
}45
thefts := []store.RecordTheftParams{44
thefts := []store.RecordTheftParams{46
{GullID: 1, Snack: "chips", StolenOn: "2026-07-01"},45
{GullID: 1, Snack: "chips", StolenOn: "2026-07-01"},47
{GullID: 1, Snack: "pasty", StolenOn: "2026-07-03"},46
{GullID: 1, Snack: "pasty", StolenOn: "2026-07-03"},48
{GullID: 1, Snack: "ice cream", StolenOn: "2026-07-04"},47
{GullID: 1, Snack: "ice cream", StolenOn: "2026-07-04"},49
{GullID: 2, Snack: "chips", StolenOn: "2026-07-02"},48
{GullID: 2, Snack: "chips", StolenOn: "2026-07-02"},50
{GullID: 3, Snack: "pasty", StolenOn: "2026-07-02"},49
{GullID: 3, Snack: "pasty", StolenOn: "2026-07-02"},51
{GullID: 3, Snack: "chips", StolenOn: "2026-07-05"},50
{GullID: 3, Snack: "chips", StolenOn: "2026-07-05"},52
{GullID: 5, Snack: "sandwich", StolenOn: "2026-07-03"},51
{GullID: 5, Snack: "sandwich", StolenOn: "2026-07-03"},53
}52
}54
for _, theft := range thefts {53
for _, theft := range thefts {55
if err := queries.RecordTheft(ctx, theft); err != nil {54
if err := queries.RecordTheft(ctx, theft); err != nil {56
return fmt.Errorf("recording a theft: %w", err)55
return fmt.Errorf("recording a theft: %w", err)57
}56
}58
}57
}59
58
60
family, err := queries.FamilyTree(ctx, 1)59
family, err := queries.FamilyTree(ctx, 1)61
if err != nil {60
if err != nil {62
return fmt.Errorf("tracing Steven's family: %w", err)61
return fmt.Errorf("tracing Steven's family: %w", err)63
}62
}64
fmt.Fprintln(output, "Steven's family:")63
fmt.Fprintln(output, "Steven's family:")65
for _, member := range family {64
for _, member := range family {66
fmt.Fprintf(output, " generation %d %-8s parent %s\n", member.Generation, member.Name, orNull(member.Parent))65
fmt.Fprintf(output, " generation %d %-8s parent %s\n", member.Generation, member.Name, orNull(member.Parent))67
}66
}68
67
69
league, err := queries.TheftLeague(ctx)68
league, err := queries.TheftLeague(ctx)70
if err != nil {69
if err != nil {71
return fmt.Errorf("ranking thieves: %w", err)70
return fmt.Errorf("ranking thieves: %w", err)72
}71
}73
fmt.Fprintln(output, "theft league:")72
fmt.Fprintln(output, "theft league:")74
for _, entry := range league {73
for _, entry := range league {75
fmt.Fprintf(output, " %-13s #%d %-8s %d of %s\n", entry.Beach, entry.BeachRank, entry.Name, entry.Thefts, entry.BeachThefts)74
fmt.Fprintf(output, " %-13s #%d %-8s %d of %s\n", entry.Beach, entry.BeachRank, entry.Name, entry.Thefts, orNull(entry.BeachThefts))76
}75
}77
76
78
records, err := queries.BeachRecords(ctx)77
records, err := queries.BeachRecords(ctx)79
if err != nil {78
if err != nil {80
return fmt.Errorf("reading beach records: %w", err)79
return fmt.Errorf("reading beach records: %w", err)81
}80
}82
fmt.Fprintln(output, "beach records:")81
fmt.Fprintln(output, "beach records:")83
for _, record := range records {82
for _, record := range records {84
fmt.Fprintf(output, " %-13s gulls %d, thefts %d, last theft %s\n", record.Beach, record.Gulls, record.Thefts, orNull(record.LastTheft))83
fmt.Fprintf(output, " %-13s gulls %d, thefts %d, last theft %s\n", record.Beach, record.Gulls, record.Thefts, orNull(record.LastTheft))85
}84
}86
return nil85
return nil87
}86
}88
87
89
// orNull formats a nullable field, or returns NULL when the field is nil.88
// orNull formats a nullable field, or returns NULL when the field is nil.90
//89
//91
// Takes value (*T) which is a nullable field.90
// Takes value (*T) which is a nullable field.92
//91
//93
// Returns string which is the text to print.92
// Returns string which is the text to print.94
func orNull[T any](value *T) string {93
func orNull[T any](value *T) string {95
if value == nil {94
if value == nil {96
return "NULL"95
return "NULL"97
}96
}98
return fmt.Sprint(*value)97
return fmt.Sprint(*value)99
}98
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | DuckDB | SQLite |
|---|---|---|---|
| FamilyTree | column generation | int32 | int64 |
| TheftLeague | column beach_thefts | *big.Int | *int64 |