Functions in migrations: MariaDB and MySQL
MariaDB and MySQL run the same files for this example: the SQL and the Go are identical.
Configuration
sgl.yamlidentical
MariaDB
MySQL
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: market4
- name: market5
dialect: mysql5
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/storeMigrations
db/migrations/0001_catches.up.sqlidentical
MariaDB
MySQL
1
CREATE TABLE catches (1
CREATE TABLE catches (2
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,2
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,3
boat VARCHAR(50) NOT NULL,3
boat VARCHAR(50) NOT NULL,4
species VARCHAR(50) NOT NULL,4
species VARCHAR(50) NOT NULL,5
weight_grams INT NOT NULL5
weight_grams INT NOT NULL6
);6
);7
7
8
CREATE TABLE prices (8
CREATE TABLE prices (9
species VARCHAR(50) NOT NULL PRIMARY KEY,9
species VARCHAR(50) NOT NULL PRIMARY KEY,10
pence_per_kilo INT NOT NULL10
pence_per_kilo INT NOT NULL11
);11
);db/migrations/0001_catches.down.sqlidentical
MariaDB
MySQL
1
DROP TABLE prices;1
DROP TABLE prices;2
DROP TABLE catches;2
DROP TABLE catches;db/migrations/0002_market_functions.up.sqlidentical
MariaDB
MySQL
1
CREATE FUNCTION price_pence(weight_grams INT, pence_per_kilo INT) RETURNS INT1
CREATE FUNCTION price_pence(weight_grams INT, pence_per_kilo INT) RETURNS INT2
DETERMINISTIC NO SQL2
DETERMINISTIC NO SQL3
RETURN weight_grams * pence_per_kilo / 1000;3
RETURN weight_grams * pence_per_kilo / 1000;4
4
5
CREATE FUNCTION land_catch(boat_name VARCHAR(50), caught VARCHAR(50), grams INT) RETURNS BIGINT5
CREATE FUNCTION land_catch(boat_name VARCHAR(50), caught VARCHAR(50), grams INT) RETURNS BIGINT6
DETERMINISTIC MODIFIES SQL DATA6
DETERMINISTIC MODIFIES SQL DATA7
BEGIN7
BEGIN8
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);9
RETURN LAST_INSERT_ID();9
RETURN LAST_INSERT_ID();10
END;10
END;11
11
12
-- sgl.migration(readonly: true)12
-- sgl.migration(readonly: true)13
CREATE FUNCTION species_price(wanted VARCHAR(50)) RETURNS INT13
CREATE FUNCTION species_price(wanted VARCHAR(50)) RETURNS INT14
DETERMINISTIC MODIFIES SQL DATA14
DETERMINISTIC MODIFIES SQL DATA15
RETURN (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.sqlidentical
MariaDB
MySQL
1
DROP FUNCTION species_price;1
DROP FUNCTION species_price;2
DROP FUNCTION land_catch;2
DROP FUNCTION land_catch;3
DROP FUNCTION price_pence;3
DROP FUNCTION price_pence;Queries
SetPriceidentical
MariaDB · db/queries/market.sql
MySQL · db/queries/market.sql
1
-- sgl.query(SetPrice, exec)1
-- sgl.query(SetPrice, exec)2
INSERT INTO prices (species, pence_per_kilo) VALUES (?, ?);2
INSERT INTO prices (species, pence_per_kilo) VALUES (?, ?);LandCatchidentical
MariaDB · db/queries/market.sql
MySQL · db/queries/market.sql
1
-- sgl.query(LandCatch, one)1
-- sgl.query(LandCatch, one)2
-- ?1 as sgl.param(boat)2
-- ?1 as sgl.param(boat)3
-- ?2 as sgl.param(species)3
-- ?2 as sgl.param(species)4
-- ?3 as sgl.param(grams)4
-- ?3 as sgl.param(grams)5
SELECT land_catch(?1, ?2, ?3) AS id;5
SELECT land_catch(?1, ?2, ?3) AS id;SpeciesPriceidentical
MariaDB · db/queries/market.sql
MySQL · db/queries/market.sql
1
-- sgl.query(SpeciesPrice, one)1
-- sgl.query(SpeciesPrice, one)2
-- ?1 as sgl.param(species)2
-- ?1 as sgl.param(species)3
SELECT species_price(?1) AS pence_per_kilo;3
SELECT species_price(?1) AS pence_per_kilo;BoatHaulidentical
MariaDB · db/queries/market.sql
MySQL · db/queries/market.sql
1
-- sgl.query(BoatHaul, many)1
-- sgl.query(BoatHaul, many)2
SELECT catches.species, catches.weight_grams, price_pence(catches.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 catches3
FROM catches4
JOIN prices ON prices.species = catches.species4
JOIN prices ON prices.species = catches.species5
WHERE catches.boat = ?5
WHERE catches.boat = ?6
ORDER BY catches.species;6
ORDER BY catches.species;What sgl generates
SetPriceidentical
MariaDB
MySQL
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
MariaDB
MySQL
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
MariaDB
MySQL
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
}BoatHaulidentical
MariaDB
MySQL
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.goidentical
MariaDB
MySQL
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/mysql/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
// what it says about data access, along with what its body does, decides whether those63
// what it says about data access, along with what its body does, decides whether those64
// queries are read-only, which picks the writer or the reader.64
// queries are read-only, which picks the writer or the reader.65
//65
//66
// - land_catch says MODIFIES SQL DATA and its body inserts a row, so LandCatch goes to66
// - land_catch says MODIFIES SQL DATA and its body inserts a row, so LandCatch goes to67
// the writer. Its body is a BEGIN ... END block, which a migration can hold without67
// the writer. Its body is a BEGIN ... END block, which a migration can hold without68
// DELIMITER, as Seagull splits the file into statements itself;68
// DELIMITER, as Seagull splits the file into statements itself;69
// - price_pence says NO SQL, so BoatHaul, which calls it, goes to the reader;69
// - price_pence says NO SQL, so BoatHaul, which calls it, goes to the reader;70
// - species_price says MODIFIES SQL DATA, which Seagull takes to mean it writes, but70
// - species_price says MODIFIES SQL DATA, which Seagull takes to mean it writes, but71
// its body only reads. MySQL never checks these characteristics against the body. The71
// its body only reads. MySQL never checks these characteristics against the body. The72
// sgl.migration(readonly: true) line above it in the migration says so, and72
// sgl.migration(readonly: true) line above it in the migration says so, and73
// SpeciesPrice goes to the reader.73
// SpeciesPrice goes to the reader.74
//74
//75
// With binary logging on, as it is by default, MySQL refuses a function that is not75
// 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 are76
// DETERMINISTIC, NO SQL or READS SQL DATA, so the two MODIFIES SQL DATA functions are77
// declared DETERMINISTIC too.77
// declared DETERMINISTIC too.78
//78
//79
// 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 no80
// row for a fish with no price.80
// row for a fish with no price.81
//81
//82
// Takes database (*sql.DB) which is a migrated database.82
// Takes database (*sql.DB) which is a migrated database.83
// 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.84
//84
//85
// Returns error when a query fails.85
// Returns error when a query fails.86
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 {87
var last string87
var last string88
queries := store.NewWithReplica(88
queries := store.NewWithReplica(89
connection{DB: database, last: &last, name: "writer"},89
connection{DB: database, last: &last, name: "writer"},90
connection{DB: database, last: &last, name: "reader"},90
connection{DB: database, last: &last, name: "reader"},91
)91
)92
92
93
prices := []store.SetPriceParams{93
prices := []store.SetPriceParams{94
{Species: "crab", PencePerKilo: 1200},94
{Species: "crab", PencePerKilo: 1200},95
{Species: "mackerel", PencePerKilo: 450},95
{Species: "mackerel", PencePerKilo: 450},96
{Species: "pollock", PencePerKilo: 800},96
{Species: "pollock", PencePerKilo: 800},97
}97
}98
for _, price := range prices {98
for _, price := range prices {99
if err := queries.SetPrice(ctx, price); err != nil {99
if err := queries.SetPrice(ctx, price); err != nil {100
return fmt.Errorf("pricing %s: %w", price.Species, err)100
return fmt.Errorf("pricing %s: %w", price.Species, err)101
}101
}102
}102
}103
103
104
catches := []store.LandCatchParams{104
catches := []store.LandCatchParams{105
{Boat: "Kittiwake", Species: "mackerel", Grams: 12000},105
{Boat: "Kittiwake", Species: "mackerel", Grams: 12000},106
{Boat: "Kittiwake", Species: "crab", Grams: 3500},106
{Boat: "Kittiwake", Species: "crab", Grams: 3500},107
{Boat: "Puffin", Species: "pollock", Grams: 8000},107
{Boat: "Puffin", Species: "pollock", Grams: 8000},108
}108
}109
for _, catch := range catches {109
for _, catch := range catches {110
landed, err := queries.LandCatch(ctx, catch)110
landed, err := queries.LandCatch(ctx, catch)111
if err != nil {111
if err != nil {112
return fmt.Errorf("landing %s: %w", catch.Species, err)112
return fmt.Errorf("landing %s: %w", catch.Species, err)113
}113
}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)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)115
}115
}116
116
117
for _, species := range []string{"crab", "halibut"} {117
for _, species := range []string{"crab", "halibut"} {118
price, err := queries.SpeciesPrice(ctx, species)118
price, err := queries.SpeciesPrice(ctx, species)119
if err != nil {119
if err != nil {120
return fmt.Errorf("pricing %s: %w", species, err)120
return fmt.Errorf("pricing %s: %w", species, err)121
}121
}122
if price.PencePerKilo == nil {122
if price.PencePerKilo == nil {123
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)124
continue124
continue125
}125
}126
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)127
}127
}128
128
129
haul, err := queries.BoatHaul(ctx, "Kittiwake")129
haul, err := queries.BoatHaul(ctx, "Kittiwake")130
if err != nil {130
if err != nil {131
return fmt.Errorf("reading the Kittiwake's haul: %w", err)131
return fmt.Errorf("reading the Kittiwake's haul: %w", err)132
}132
}133
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)134
for _, row := range haul {134
for _, row := range haul {135
fmt.Fprintf(output, " %-9s %6dg %5sp\n", row.Species, row.WeightGrams, orNull(row.ValuePence))135
fmt.Fprintf(output, " %-9s %6dg %5sp\n", row.Species, row.WeightGrams, orNull(row.ValuePence))136
}136
}137
return nil137
return nil138
}138
}139
139
140
// 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.141
//141
//142
// Takes value (*T) which is a nullable field.142
// Takes value (*T) which is a nullable field.143
//143
//144
// Returns string which is the text to print.144
// Returns string which is the text to print.145
func orNull[T any](value *T) string {145
func orNull[T any](value *T) string {146
if value == nil {146
if value == nil {147
return "NULL"147
return "NULL"148
}148
}149
return fmt.Sprint(*value)149
return fmt.Sprint(*value)150
}150
}