Types through CTEs, window functions and views: TimescaleDB and PostgreSQL
TimescaleDB and PostgreSQL run the same files for this example: the SQL and the Go are identical.
Configuration
sgl.yamlidentical
TimescaleDB
PostgreSQL
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: colony4
- name: colony5
dialect: postgres5
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/sql11
driver: database/sql12
migrate:12
migrate:13
dsn: ${env:DATABASE_URL}13
dsn: ${env:DATABASE_URL}Migrations
db/migrations/0001_gulls.up.sqlidentical
TimescaleDB
PostgreSQL
1
CREATE TABLE gulls (1
CREATE TABLE gulls (2
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,2
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,3
name TEXT NOT NULL,3
name TEXT NOT NULL,4
parent_id BIGINT REFERENCES gulls (id),4
parent_id BIGINT REFERENCES gulls (id),5
beach TEXT NOT NULL5
beach TEXT NOT NULL6
);6
);7
7
8
CREATE TABLE thefts (8
CREATE TABLE thefts (9
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,9
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,10
gull_id BIGINT NOT NULL REFERENCES gulls (id),10
gull_id BIGINT NOT NULL REFERENCES gulls (id),11
snack TEXT NOT NULL,11
snack TEXT NOT NULL,12
stolen_on DATE NOT NULL12
stolen_on DATE NOT NULL13
);13
);14
14
15
CREATE VIEW beach_records AS15
CREATE VIEW beach_records AS16
SELECT16
SELECT17
gulls.beach,17
gulls.beach,18
COUNT(DISTINCT gulls.id) AS gulls,18
COUNT(DISTINCT gulls.id) AS gulls,19
COUNT(thefts.id) AS thefts,19
COUNT(thefts.id) AS thefts,20
MAX(thefts.stolen_on) AS last_theft20
MAX(thefts.stolen_on) AS last_theft21
FROM gulls21
FROM gulls22
LEFT JOIN thefts ON thefts.gull_id = gulls.id22
LEFT JOIN thefts ON thefts.gull_id = gulls.id23
GROUP BY gulls.beach;23
GROUP BY gulls.beach;db/migrations/0001_gulls.down.sqlidentical
TimescaleDB
PostgreSQL
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
HatchGullidentical
TimescaleDB · db/queries/gulls.sql
PostgreSQL · 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 ($1, $2, $3);RecordTheftidentical
TimescaleDB · db/queries/gulls.sql
PostgreSQL · 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 ($1, $2, $3);FamilyTreeidentical
TimescaleDB · db/queries/gulls.sql
PostgreSQL · db/queries/gulls.sql
1
-- sgl.query(FamilyTree, many)1
-- sgl.query(FamilyTree, many)2
WITH RECURSIVE descendants AS (2
WITH RECURSIVE descendants AS (3
SELECT id, name, parent_id, 0::BIGINT AS generation3
SELECT id, name, parent_id, 0::BIGINT AS generation4
FROM gulls4
FROM gulls5
WHERE id = $15
WHERE id = $16
UNION ALL6
UNION ALL7
SELECT gulls.id, gulls.name, gulls.parent_id, descendants.generation + 17
SELECT gulls.id, gulls.name, gulls.parent_id, descendants.generation + 18
FROM gulls8
FROM gulls9
JOIN descendants ON gulls.parent_id = descendants.id9
JOIN descendants ON gulls.parent_id = descendants.id10
)10
)11
SELECT descendants.name, descendants.generation, parents.name AS parent11
SELECT descendants.name, descendants.generation, parents.name AS parent12
FROM descendants12
FROM descendants13
LEFT JOIN gulls AS parents ON parents.id = descendants.parent_id13
LEFT JOIN gulls AS parents ON parents.id = descendants.parent_id14
ORDER BY descendants.generation, descendants.id;14
ORDER BY descendants.generation, descendants.id;TheftLeagueidentical
TimescaleDB · db/queries/gulls.sql
PostgreSQL · 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;BeachRecordsidentical
TimescaleDB · db/queries/gulls.sql
PostgreSQL · db/queries/gulls.sql
1
-- sgl.query(BeachRecords, many)1
-- sgl.query(BeachRecords, many)2
SELECT * FROM beach_records ORDER BY beach;2
SELECT * FROM beach_records ORDER BY beach;What sgl generates
HatchGullidentical
TimescaleDB
PostgreSQL
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
TimescaleDB
PostgreSQL
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
}FamilyTreeidentical
TimescaleDB
PostgreSQL
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 int64 `json:"generation"`5
Generation int64 `json:"generation"`6
Parent *string `json:"parent"`6
Parent *string `json:"parent"`7
}7
}TheftLeagueidentical
TimescaleDB
PostgreSQL
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
TimescaleDB
PostgreSQL
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.goidentical
TimescaleDB
PostgreSQL
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/postgres/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 int64 from 0::BIGINT AS generation and generation + 1,17
// generation; generation is an int64 from 0::BIGINT AS generation and generation + 1,18
// and 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; COUNT and RANK are bigint, so int64, while SUM of20
// totals the beach with SUM() OVER; COUNT and RANK are bigint, so int64, while SUM of21
// a bigint is numeric in PostgreSQL, so BeachThefts is a nullable *apd.Decimal;21
// a bigint is numeric in PostgreSQL, so BeachThefts is a nullable *apd.Decimal;22
// - 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,23
// and its columns keep the types the view's own SELECT gives them: MAX over a DATE23
// 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 as24
// 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 a25
// 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 already26
// 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 the27
// deployed, which scans exactly those four. go.keep_stars: true in sgl.yaml keeps the28
// star as written.28
// star as written.29
//29
//30
// Takes database (*sql.DB) which is a migrated database.30
// Takes database (*sql.DB) which is a migrated database.31
// Takes output (io.Writer) which receives the three listings.31
// Takes output (io.Writer) which receives the three listings.32
//32
//33
// Returns error when a query fails.33
// Returns error when a query fails.34
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 {35
queries := store.New(database)35
queries := store.New(database)36
36
37
gulls := []store.HatchGullParams{37
gulls := []store.HatchGullParams{38
{Name: "Steven", ParentID: nil, Beach: "Porthmeor"},38
{Name: "Steven", ParentID: nil, Beach: "Porthmeor"},39
{Name: "Gwen", ParentID: new(int64(1)), Beach: "Porthmeor"},39
{Name: "Gwen", ParentID: new(int64(1)), Beach: "Porthmeor"},40
{Name: "Jago", ParentID: new(int64(1)), Beach: "Porthminster"},40
{Name: "Jago", ParentID: new(int64(1)), Beach: "Porthminster"},41
{Name: "Kerensa", ParentID: new(int64(2)), Beach: "Porthmeor"},41
{Name: "Kerensa", ParentID: new(int64(2)), Beach: "Porthmeor"},42
{Name: "Tristan", ParentID: nil, Beach: "Porthminster"},42
{Name: "Tristan", ParentID: nil, Beach: "Porthminster"},43
{Name: "Morwenna", ParentID: nil, Beach: "Gwithian"},43
{Name: "Morwenna", ParentID: nil, Beach: "Gwithian"},44
}44
}45
for _, gull := range gulls {45
for _, gull := range gulls {46
if err := queries.HatchGull(ctx, gull); err != nil {46
if err := queries.HatchGull(ctx, gull); err != nil {47
return fmt.Errorf("hatching %s: %w", gull.Name, err)47
return fmt.Errorf("hatching %s: %w", gull.Name, err)48
}48
}49
}49
}50
thefts := []store.RecordTheftParams{50
thefts := []store.RecordTheftParams{51
{GullID: 1, Snack: "chips", StolenOn: july(1)},51
{GullID: 1, Snack: "chips", StolenOn: july(1)},52
{GullID: 1, Snack: "pasty", StolenOn: july(3)},52
{GullID: 1, Snack: "pasty", StolenOn: july(3)},53
{GullID: 1, Snack: "ice cream", StolenOn: july(4)},53
{GullID: 1, Snack: "ice cream", StolenOn: july(4)},54
{GullID: 2, Snack: "chips", StolenOn: july(2)},54
{GullID: 2, Snack: "chips", StolenOn: july(2)},55
{GullID: 3, Snack: "pasty", StolenOn: july(2)},55
{GullID: 3, Snack: "pasty", StolenOn: july(2)},56
{GullID: 3, Snack: "chips", StolenOn: july(5)},56
{GullID: 3, Snack: "chips", StolenOn: july(5)},57
{GullID: 5, Snack: "sandwich", StolenOn: july(3)},57
{GullID: 5, Snack: "sandwich", StolenOn: july(3)},58
}58
}59
for _, theft := range thefts {59
for _, theft := range thefts {60
if err := queries.RecordTheft(ctx, theft); err != nil {60
if err := queries.RecordTheft(ctx, theft); err != nil {61
return fmt.Errorf("recording a theft: %w", err)61
return fmt.Errorf("recording a theft: %w", err)62
}62
}63
}63
}64
64
65
family, err := queries.FamilyTree(ctx, 1)65
family, err := queries.FamilyTree(ctx, 1)66
if err != nil {66
if err != nil {67
return fmt.Errorf("tracing Steven's family: %w", err)67
return fmt.Errorf("tracing Steven's family: %w", err)68
}68
}69
fmt.Fprintln(output, "Steven's family:")69
fmt.Fprintln(output, "Steven's family:")70
for _, member := range family {70
for _, member := range family {71
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))72
}72
}73
73
74
league, err := queries.TheftLeague(ctx)74
league, err := queries.TheftLeague(ctx)75
if err != nil {75
if err != nil {76
return fmt.Errorf("ranking thieves: %w", err)76
return fmt.Errorf("ranking thieves: %w", err)77
}77
}78
fmt.Fprintln(output, "theft league:")78
fmt.Fprintln(output, "theft league:")79
for _, entry := range league {79
for _, entry := range league {80
beachThefts := "NULL"80
beachThefts := "NULL"81
if entry.BeachThefts != nil {81
if entry.BeachThefts != nil {82
beachThefts = entry.BeachThefts.String()82
beachThefts = entry.BeachThefts.String()83
}83
}84
fmt.Fprintf(output, " %-13s #%d %-8s %d of %s\n", entry.Beach, entry.BeachRank, entry.Name, entry.Thefts, beachThefts)84
fmt.Fprintf(output, " %-13s #%d %-8s %d of %s\n", entry.Beach, entry.BeachRank, entry.Name, entry.Thefts, beachThefts)85
}85
}86
86
87
records, err := queries.BeachRecords(ctx)87
records, err := queries.BeachRecords(ctx)88
if err != nil {88
if err != nil {89
return fmt.Errorf("reading beach records: %w", err)89
return fmt.Errorf("reading beach records: %w", err)90
}90
}91
fmt.Fprintln(output, "beach records:")91
fmt.Fprintln(output, "beach records:")92
for _, record := range records {92
for _, record := range records {93
lastTheft := "NULL"93
lastTheft := "NULL"94
if record.LastTheft != nil {94
if record.LastTheft != nil {95
lastTheft = record.LastTheft.Format(time.DateOnly)95
lastTheft = record.LastTheft.Format(time.DateOnly)96
}96
}97
fmt.Fprintf(output, " %-13s gulls %d, thefts %d, last theft %s\n", record.Beach, record.Gulls, record.Thefts, lastTheft)97
fmt.Fprintf(output, " %-13s gulls %d, thefts %d, last theft %s\n", record.Beach, record.Gulls, record.Thefts, lastTheft)98
}98
}99
return nil99
return nil100
}100
}101
101
102
// july returns a day in July 2026, the month the thefts happen in.102
// july returns a day in July 2026, the month the thefts happen in.103
//103
//104
// Takes day (int) which is the day of the month.104
// Takes day (int) which is the day of the month.105
//105
//106
// Returns time.Time which is midnight UTC on that day, as a DATE column holds it.106
// Returns time.Time which is midnight UTC on that day, as a DATE column holds it.107
func july(day int) time.Time {107
func july(day int) time.Time {108
return time.Date(2026, time.July, day, 0, 0, 0, 0, time.UTC)108
return time.Date(2026, time.July, day, 0, 0, 0, 0, time.UTC)109
}109
}110
110
111
// orNull formats a nullable field, or returns NULL when the field is nil.111
// orNull formats a nullable field, or returns NULL when the field is nil.112
//112
//113
// Takes value (*T) which is a nullable field.113
// Takes value (*T) which is a nullable field.114
//114
//115
// Returns string which is the text to print.115
// Returns string which is the text to print.116
func orNull[T any](value *T) string {116
func orNull[T any](value *T) string {117
if value == nil {117
if value == nil {118
return "NULL"118
return "NULL"119
}119
}120
return fmt.Sprint(*value)120
return fmt.Sprint(*value)121
}121
}