Functions in migrations: TimescaleDB and MariaDB
Every file of the TimescaleDB and MariaDB versions side by side, with the lines that differ marked. 2 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
TimescaleDB
MariaDB
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: market4
- name: market5
dialect: postgres5
dialect: mysql6
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_catches.up.sqldiffers
TimescaleDB
MariaDB
1
CREATE TABLE catches (1
CREATE TABLE catches (2
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,2
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,3
boat TEXT NOT NULL,3
boat VARCHAR(50) NOT NULL,4
species TEXT NOT NULL,4
species VARCHAR(50) NOT NULL,5
weight_grams INTEGER NOT NULL5
weight_grams INT NOT NULL6
);6
);7
7
8
CREATE TABLE prices (8
CREATE TABLE prices (9
species TEXT PRIMARY KEY,9
species VARCHAR(50) NOT NULL PRIMARY KEY,10
pence_per_kilo INTEGER NOT NULL10
pence_per_kilo INT NOT NULL11
);11
);db/migrations/0001_catches.down.sqlidentical
TimescaleDB
MariaDB
1
DROP TABLE prices;1
DROP TABLE prices;2
DROP TABLE catches;2
DROP TABLE catches;db/migrations/0002_market_functions.up.sqldiffers
TimescaleDB
MariaDB
1
CREATE FUNCTION price_pence(weight_grams INTEGER, pence_per_kilo INTEGER) RETURNS INTEGER1
CREATE FUNCTION price_pence(weight_grams INT, pence_per_kilo INT) RETURNS INT2
LANGUAGE sql IMMUTABLE STRICT2
DETERMINISTIC NO SQL3
AS $$ SELECT weight_grams * pence_per_kilo / 1000 $$;3
RETURN weight_grams * pence_per_kilo / 1000;4
5
CREATE FUNCTION boat_haul(boat_name TEXT)6
RETURNS TABLE (species TEXT, weight_grams INTEGER)7
LANGUAGE sql STABLE8
AS $$ SELECT species, weight_grams FROM catches WHERE boat = boat_name $$;9
4
10
CREATE FUNCTION land_catch(boat_name TEXT, caught TEXT, grams INTEGER) RETURNS BIGINT5
CREATE FUNCTION land_catch(boat_name VARCHAR(50), caught VARCHAR(50), grams INT) RETURNS BIGINT11
LANGUAGE plpgsql VOLATILE6
DETERMINISTIC MODIFIES SQL DATA12
AS $$13
DECLARE14
landed BIGINT;15
BEGIN7
BEGIN16
INSERT INTO catches (boat, species, weight_grams) VALUES (boat_name, caught, grams)8
INSERT INTO catches (boat, species, weight_grams) VALUES (boat_name, caught, grams);17
RETURNING id INTO landed;9
RETURN LAST_INSERT_ID();18
RETURN landed;19
END;10
END;20
$$;21
11
22
-- sgl.migration(readonly: true)12
-- sgl.migration(readonly: true)23
CREATE FUNCTION species_price(wanted TEXT) RETURNS INTEGER13
CREATE FUNCTION species_price(wanted VARCHAR(50)) RETURNS INT24
LANGUAGE sql VOLATILE14
DETERMINISTIC MODIFIES SQL DATA25
AS $$ SELECT pence_per_kilo FROM prices WHERE species = wanted $$;15
RETURN (SELECT pence_per_kilo FROM prices WHERE species = wanted);db/migrations/0002_market_functions.down.sqldiffers
TimescaleDB
MariaDB
1
DROP FUNCTION species_price(TEXT);1
DROP FUNCTION species_price;2
DROP FUNCTION land_catch(TEXT, TEXT, INTEGER);2
DROP FUNCTION land_catch;3
DROP FUNCTION boat_haul(TEXT);3
DROP FUNCTION price_pence;4
DROP FUNCTION price_pence(INTEGER, INTEGER);Queries
SetPricediffers
TimescaleDB · db/queries/market.sql
MariaDB · db/queries/market.sql
1
-- sgl.query(SetPrice, exec)1
-- sgl.query(SetPrice, exec)2
INSERT INTO prices (species, pence_per_kilo) VALUES ($1, $2);2
INSERT INTO prices (species, pence_per_kilo) VALUES (?, ?);LandCatchdiffers
TimescaleDB · db/queries/market.sql
MariaDB · db/queries/market.sql
1
-- sgl.query(LandCatch, one)1
-- sgl.query(LandCatch, one)2
SELECT land_catch(:boat, :species, :grams) AS id;2
-- ?1 as sgl.param(boat)3
-- ?2 as sgl.param(species)4
-- ?3 as sgl.param(grams)5
SELECT land_catch(?1, ?2, ?3) AS id;SpeciesPricediffers
TimescaleDB · db/queries/market.sql
MariaDB · db/queries/market.sql
1
-- sgl.query(SpeciesPrice, one)1
-- sgl.query(SpeciesPrice, one)2
SELECT species_price(:species) AS pence_per_kilo;2
-- ?1 as sgl.param(species)3
SELECT species_price(?1) AS pence_per_kilo;BoatHauldiffers
TimescaleDB · db/queries/market.sql
MariaDB · db/queries/market.sql
1
-- sgl.query(BoatHaul, many)1
-- sgl.query(BoatHaul, many)2
SELECT haul.species, haul.weight_grams, price_pence(haul.weight_grams, prices.pence_per_kilo) AS value_pence2
SELECT catches.species, catches.weight_grams, price_pence(catches.weight_grams, prices.pence_per_kilo) AS value_pence3
FROM boat_haul(:boat) AS haul3
FROM catches4
JOIN prices ON prices.species = haul.species4
JOIN prices ON prices.species = catches.species5
ORDER BY haul.species;5
WHERE catches.boat = ?6
ORDER BY catches.species;What sgl generates
SetPriceidentical
TimescaleDB
MariaDB
1
func (queries *Queries) SetPrice(ctx context.Context, params SetPriceParams) error1
func (queries *Queries) SetPrice(ctx context.Context, params SetPriceParams) error2
2
3
type SetPriceParams struct {3
type SetPriceParams struct {4
Species string4
Species string5
PencePerKilo int325
PencePerKilo int326
}6
}LandCatchidentical
TimescaleDB
MariaDB
1
func (queries *Queries) LandCatch(ctx context.Context, params LandCatchParams) (LandCatchRow, error)1
func (queries *Queries) LandCatch(ctx context.Context, params LandCatchParams) (LandCatchRow, error)2
2
3
type LandCatchParams struct {3
type LandCatchParams struct {4
Boat string4
Boat string5
Species string5
Species string6
Grams int326
Grams int327
}7
}8
8
9
type LandCatchRow struct {9
type LandCatchRow struct {10
ID *int64 `json:"id"`10
ID *int64 `json:"id"`11
}11
}SpeciesPriceidentical
TimescaleDB
MariaDB
1
func (queries *Queries) SpeciesPrice(ctx context.Context, species string) (SpeciesPriceRow, error)1
func (queries *Queries) SpeciesPrice(ctx context.Context, species string) (SpeciesPriceRow, error)2
2
3
type SpeciesPriceRow struct {3
type SpeciesPriceRow struct {4
PencePerKilo *int32 `json:"pence_per_kilo"`4
PencePerKilo *int32 `json:"pence_per_kilo"`5
}5
}BoatHauldiffers
TimescaleDB
MariaDB
1
func (queries *Queries) BoatHaul(ctx context.Context, boat string) ([]BoatHaulRow, error)1
func (queries *Queries) BoatHaul(ctx context.Context, boat string) ([]BoatHaulRow, error)2
2
3
type BoatHaulRow struct {3
type BoatHaulRow struct {4
Species *string `json:"species"`4
Species string `json:"species"`5
WeightGrams *int32 `json:"weight_grams"`5
WeightGrams int32 `json:"weight_grams"`6
ValuePence *int32 `json:"value_pence"`6
ValuePence *int32 `json:"value_pence"`7
}7
}The program
example.godiffers
TimescaleDB
MariaDB
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/030_functions_in_migrations/internal/store"9
"seagull.sh/seagull/examples/mysql/030_functions_in_migrations/internal/store"10
)10
)11
11
12
// connection is a store.DBTX that notes its name before each statement, so the example12
// connection is a store.DBTX that notes its name before each statement, so the example13
// can say whether Seagull sent a query to the writer or the reader. Both connections wrap13
// can say whether Seagull sent a query to the writer or the reader. Both connections wrap14
// the same database here; concept 24 puts the reader on a replica.14
// the same database here; concept 24 puts the reader on a replica.15
type connection struct {15
type connection struct {16
*sql.DB16
*sql.DB17
17
18
// last receives the name of the connection that ran the latest statement.18
// last receives the name of the connection that ran the latest statement.19
last *string19
last *string20
20
21
// name labels the connection.21
// name labels the connection.22
name string22
name string23
}23
}24
24
25
// ExecContext notes the connection, then runs the statement on the database.25
// ExecContext notes the connection, then runs the statement on the database.26
//26
//27
// Takes query (string) which is the SQL to run.27
// Takes query (string) which is the SQL to run.28
// Takes arguments (...any) which are its bind values.28
// Takes arguments (...any) which are its bind values.29
//29
//30
// Returns sql.Result which is the database's result.30
// Returns sql.Result which is the database's result.31
// Returns error when the statement fails.31
// Returns error when the statement fails.32
func (c connection) ExecContext(ctx context.Context, query string, arguments ...any) (sql.Result, error) {32
func (c connection) ExecContext(ctx context.Context, query string, arguments ...any) (sql.Result, error) {33
*c.last = c.name33
*c.last = c.name34
return c.DB.ExecContext(ctx, query, arguments...)34
return c.DB.ExecContext(ctx, query, arguments...)35
}35
}36
36
37
// QueryContext notes the connection, then runs the query on the database.37
// QueryContext notes the connection, then runs the query on the database.38
//38
//39
// Takes query (string) which is the SQL to run.39
// Takes query (string) which is the SQL to run.40
// Takes arguments (...any) which are its bind values.40
// Takes arguments (...any) which are its bind values.41
//41
//42
// Returns *sql.Rows which holds the result rows.42
// Returns *sql.Rows which holds the result rows.43
// Returns error when the query fails.43
// Returns error when the query fails.44
func (c connection) QueryContext(ctx context.Context, query string, arguments ...any) (*sql.Rows, error) {44
func (c connection) QueryContext(ctx context.Context, query string, arguments ...any) (*sql.Rows, error) {45
*c.last = c.name45
*c.last = c.name46
return c.DB.QueryContext(ctx, query, arguments...)46
return c.DB.QueryContext(ctx, query, arguments...)47
}47
}48
48
49
// QueryRowContext notes the connection, then runs the query on the database.49
// QueryRowContext notes the connection, then runs the query on the database.50
//50
//51
// Takes query (string) which is the SQL to run.51
// Takes query (string) which is the SQL to run.52
// Takes arguments (...any) which are its bind values.52
// Takes arguments (...any) which are its bind values.53
//53
//54
// Returns *sql.Row which holds the result row or the error.54
// Returns *sql.Row which holds the result row or the error.55
func (c connection) QueryRowContext(ctx context.Context, query string, arguments ...any) *sql.Row {55
func (c connection) QueryRowContext(ctx context.Context, query string, arguments ...any) *sql.Row {56
*c.last = c.name56
*c.last = c.name57
return c.DB.QueryRowContext(ctx, query, arguments...)57
return c.DB.QueryRowContext(ctx, query, arguments...)58
}58
}59
59
60
// run prices the fish, lands three catches and reads the market back, all through the SQL60
// run prices the fish, lands three catches and reads the market back, all through the SQL61
// functions that migration 0002 creates. Seagull reads each CREATE FUNCTION when it61
// functions that migration 0002 creates. Seagull reads each CREATE FUNCTION when it62
// builds the schema: the argument and return types type the queries that call it, and62
// builds the schema: the argument and return types type the queries that call it, and63
// whether the function may change data decides whether those queries are read-only, which63
// what it says about data access, along with what its body does, decides whether those64
// picks the writer or the reader. Volatility does not decide it.64
// queries are read-only, which picks the writer or the reader.65
//65
//66
// - land_catch inserts a row, so LandCatch goes to the writer;66
// - land_catch says MODIFIES SQL DATA and its body inserts a row, so LandCatch goes to67
// - price_pence and boat_haul only read data, so BoatHaul, which calls both, goes to67
// the writer. Its body is a BEGIN ... END block, which a migration can hold without68
// the reader. boat_haul RETURNS TABLE, so it is a relation with typed columns, and68
// DELIMITER, as Seagull splits the file into statements itself;69
// its $1 is typed from the function's TEXT argument;69
// - price_pence says NO SQL, so BoatHaul, which calls it, goes to the reader;70
// - species_price is declared VOLATILE, but its body only reads, so SpeciesPrice goes70
// - species_price says MODIFIES SQL DATA, which Seagull takes to mean it writes, but71
// to the reader. The sgl.migration(readonly: true) line above it in the migration71
// its body only reads. MySQL never checks these characteristics against the body. The72
// states that outright, as a function whose body Seagull cannot read needs.72
// sgl.migration(readonly: true) line above it in the migration says so, and73
// SpeciesPrice goes to the reader.73
//74
//75
// With binary logging on, as it is by default, MySQL refuses a function that is not76
// DETERMINISTIC, NO SQL or READS SQL DATA, so the two MODIFIES SQL DATA functions are77
// declared DETERMINISTIC too.78
//74
// A function's result can be NULL, so the columns are pointers: species_price finds no79
// A function's result can be NULL, so the columns are pointers: species_price finds no75
// row for a fish with no price. price_pence is STRICT, so a NULL argument gives a NULL80
// row for a fish with no price.76
// result; here its arguments come from boat_haul, which may return NULLs.77
//81
//78
// Takes database (*sql.DB) which is a migrated database.82
// Takes database (*sql.DB) which is a migrated database.79
// Takes output (io.Writer) which receives each call's connection and result.83
// Takes output (io.Writer) which receives each call's connection and result.80
//84
//81
// Returns error when a query fails.85
// Returns error when a query fails.82
func run(ctx context.Context, database *sql.DB, output io.Writer) error {86
func run(ctx context.Context, database *sql.DB, output io.Writer) error {83
var last string87
var last string84
queries := store.NewWithReplica(88
queries := store.NewWithReplica(85
connection{DB: database, last: &last, name: "writer"},89
connection{DB: database, last: &last, name: "writer"},86
connection{DB: database, last: &last, name: "reader"},90
connection{DB: database, last: &last, name: "reader"},87
)91
)88
92
89
prices := []store.SetPriceParams{93
prices := []store.SetPriceParams{90
{Species: "crab", PencePerKilo: 1200},94
{Species: "crab", PencePerKilo: 1200},91
{Species: "mackerel", PencePerKilo: 450},95
{Species: "mackerel", PencePerKilo: 450},92
{Species: "pollock", PencePerKilo: 800},96
{Species: "pollock", PencePerKilo: 800},93
}97
}94
for _, price := range prices {98
for _, price := range prices {95
if err := queries.SetPrice(ctx, price); err != nil {99
if err := queries.SetPrice(ctx, price); err != nil {96
return fmt.Errorf("pricing %s: %w", price.Species, err)100
return fmt.Errorf("pricing %s: %w", price.Species, err)97
}101
}98
}102
}99
103
100
catches := []store.LandCatchParams{104
catches := []store.LandCatchParams{101
{Boat: "Kittiwake", Species: "mackerel", Grams: 12000},105
{Boat: "Kittiwake", Species: "mackerel", Grams: 12000},102
{Boat: "Kittiwake", Species: "crab", Grams: 3500},106
{Boat: "Kittiwake", Species: "crab", Grams: 3500},103
{Boat: "Puffin", Species: "pollock", Grams: 8000},107
{Boat: "Puffin", Species: "pollock", Grams: 8000},104
}108
}105
for _, catch := range catches {109
for _, catch := range catches {106
landed, err := queries.LandCatch(ctx, catch)110
landed, err := queries.LandCatch(ctx, catch)107
if err != nil {111
if err != nil {108
return fmt.Errorf("landing %s: %w", catch.Species, err)112
return fmt.Errorf("landing %s: %w", catch.Species, err)109
}113
}110
fmt.Fprintf(output, "LandCatch on the %s: catch %s, %dg of %s from the %s\n", last, orNull(landed.ID), catch.Grams, catch.Species, catch.Boat)114
fmt.Fprintf(output, "LandCatch on the %s: catch %s, %dg of %s from the %s\n", last, orNull(landed.ID), catch.Grams, catch.Species, catch.Boat)111
}115
}112
116
113
for _, species := range []string{"crab", "halibut"} {117
for _, species := range []string{"crab", "halibut"} {114
price, err := queries.SpeciesPrice(ctx, species)118
price, err := queries.SpeciesPrice(ctx, species)115
if err != nil {119
if err != nil {116
return fmt.Errorf("pricing %s: %w", species, err)120
return fmt.Errorf("pricing %s: %w", species, err)117
}121
}118
if price.PencePerKilo == nil {122
if price.PencePerKilo == nil {119
fmt.Fprintf(output, "SpeciesPrice on the %s: %s has no price\n", last, species)123
fmt.Fprintf(output, "SpeciesPrice on the %s: %s has no price\n", last, species)120
continue124
continue121
}125
}122
fmt.Fprintf(output, "SpeciesPrice on the %s: %s fetches %dp a kilo\n", last, species, *price.PencePerKilo)126
fmt.Fprintf(output, "SpeciesPrice on the %s: %s fetches %dp a kilo\n", last, species, *price.PencePerKilo)123
}127
}124
128
125
haul, err := queries.BoatHaul(ctx, "Kittiwake")129
haul, err := queries.BoatHaul(ctx, "Kittiwake")126
if err != nil {130
if err != nil {127
return fmt.Errorf("reading the Kittiwake's haul: %w", err)131
return fmt.Errorf("reading the Kittiwake's haul: %w", err)128
}132
}129
fmt.Fprintf(output, "BoatHaul on the %s: the Kittiwake's haul\n", last)133
fmt.Fprintf(output, "BoatHaul on the %s: the Kittiwake's haul\n", last)130
for _, row := range haul {134
for _, row := range haul {131
fmt.Fprintf(output, " %-9s %6sg %5sp\n", orNull(row.Species), orNull(row.WeightGrams), orNull(row.ValuePence))135
fmt.Fprintf(output, " %-9s %6dg %5sp\n", row.Species, row.WeightGrams, orNull(row.ValuePence))132
}136
}133
return nil137
return nil134
}138
}135
139
136
// orNull formats a nullable field, or returns NULL when the field is nil.140
// orNull formats a nullable field, or returns NULL when the field is nil.137
//141
//138
// Takes value (*T) which is a nullable field.142
// Takes value (*T) which is a nullable field.139
//143
//140
// Returns string which is the text to print.144
// Returns string which is the text to print.141
func orNull[T any](value *T) string {145
func orNull[T any](value *T) string {142
if value == nil {146
if value == nil {143
return "NULL"147
return "NULL"144
}148
}145
return fmt.Sprint(*value)149
return fmt.Sprint(*value)146
}150
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | TimescaleDB | MariaDB |
|---|---|---|---|
| BoatHaul | column species | *string | string |
| BoatHaul | column weight_grams | *int32 | int32 |