Functions in migrations: MySQL and PostgreSQL
Every file of the MySQL and PostgreSQL versions side by side, with the lines that differ marked. 2 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
MySQL
PostgreSQL
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: market4
- name: market5
dialect: mysql5
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/sql12
migrate:13
dsn: ${env:DATABASE_URL}Migrations
db/migrations/0001_catches.up.sqldiffers
MySQL
PostgreSQL
1
CREATE TABLE catches (1
CREATE TABLE catches (2
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,2
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,3
boat VARCHAR(50) NOT NULL,3
boat TEXT NOT NULL,4
species VARCHAR(50) NOT NULL,4
species TEXT NOT NULL,5
weight_grams INT NOT NULL5
weight_grams INTEGER NOT NULL6
);6
);7
7
8
CREATE TABLE prices (8
CREATE TABLE prices (9
species VARCHAR(50) NOT NULL PRIMARY KEY,9
species TEXT PRIMARY KEY,10
pence_per_kilo INT NOT NULL10
pence_per_kilo INTEGER NOT NULL11
);11
);db/migrations/0001_catches.down.sqlidentical
MySQL
PostgreSQL
1
DROP TABLE prices;1
DROP TABLE prices;2
DROP TABLE catches;2
DROP TABLE catches;db/migrations/0002_market_functions.up.sqldiffers
MySQL
PostgreSQL
1
CREATE FUNCTION price_pence(weight_grams INT, pence_per_kilo INT) RETURNS INT1
CREATE FUNCTION price_pence(weight_grams INTEGER, pence_per_kilo INTEGER) RETURNS INTEGER2
DETERMINISTIC NO SQL2
LANGUAGE sql IMMUTABLE STRICT3
RETURN weight_grams * pence_per_kilo / 1000;3
AS $$ SELECT 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 boat_haul(boat_name TEXT)6
DETERMINISTIC MODIFIES SQL DATA6
RETURNS TABLE (species TEXT, weight_grams INTEGER)7
LANGUAGE sql STABLE8
AS $$ SELECT species, weight_grams FROM catches WHERE boat = boat_name $$;9
10
CREATE FUNCTION land_catch(boat_name TEXT, caught TEXT, grams INTEGER) RETURNS BIGINT11
LANGUAGE plpgsql VOLATILE12
AS $$13
DECLARE14
landed BIGINT;7
BEGIN15
BEGIN8
INSERT INTO catches (boat, species, weight_grams) VALUES (boat_name, caught, grams);16
INSERT INTO catches (boat, species, weight_grams) VALUES (boat_name, caught, grams)9
RETURN LAST_INSERT_ID();17
RETURNING id INTO landed;18
RETURN landed;10
END;19
END;20
$$;11
21
12
-- sgl.migration(readonly: true)22
-- sgl.migration(readonly: true)13
CREATE FUNCTION species_price(wanted VARCHAR(50)) RETURNS INT23
CREATE FUNCTION species_price(wanted TEXT) RETURNS INTEGER14
DETERMINISTIC MODIFIES SQL DATA24
LANGUAGE sql VOLATILE15
RETURN (SELECT pence_per_kilo FROM prices WHERE species = wanted);25
AS $$ SELECT pence_per_kilo FROM prices WHERE species = wanted $$;db/migrations/0002_market_functions.down.sqldiffers
MySQL
PostgreSQL
1
DROP FUNCTION species_price;1
DROP FUNCTION species_price(TEXT);2
DROP FUNCTION land_catch;2
DROP FUNCTION land_catch(TEXT, TEXT, INTEGER);3
DROP FUNCTION price_pence;3
DROP FUNCTION boat_haul(TEXT);4
DROP FUNCTION price_pence(INTEGER, INTEGER);Queries
SetPricediffers
MySQL · db/queries/market.sql
PostgreSQL · 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 ($1, $2);LandCatchdiffers
MySQL · db/queries/market.sql
PostgreSQL · db/queries/market.sql
1
-- sgl.query(LandCatch, one)1
-- sgl.query(LandCatch, one)2
-- ?1 as sgl.param(boat)2
SELECT land_catch(:boat, :species, :grams) AS id;3
-- ?2 as sgl.param(species)4
-- ?3 as sgl.param(grams)5
SELECT land_catch(?1, ?2, ?3) AS id;SpeciesPricediffers
MySQL · db/queries/market.sql
PostgreSQL · db/queries/market.sql
1
-- sgl.query(SpeciesPrice, one)1
-- sgl.query(SpeciesPrice, one)2
-- ?1 as sgl.param(species)2
SELECT species_price(:species) AS pence_per_kilo;3
SELECT species_price(?1) AS pence_per_kilo;BoatHauldiffers
MySQL · db/queries/market.sql
PostgreSQL · 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 haul.species, haul.weight_grams, price_pence(haul.weight_grams, prices.pence_per_kilo) AS value_pence3
FROM catches3
FROM boat_haul(:boat) AS haul4
JOIN prices ON prices.species = catches.species4
JOIN prices ON prices.species = haul.species5
WHERE catches.boat = ?5
ORDER BY haul.species;6
ORDER BY catches.species;What sgl generates
SetPriceidentical
MySQL
PostgreSQL
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
MySQL
PostgreSQL
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
MySQL
PostgreSQL
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
MySQL
PostgreSQL
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
MySQL
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
8
9
"seagull.sh/seagull/examples/mysql/030_functions_in_migrations/internal/store"9
"seagull.sh/seagull/examples/postgres/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
// whether the function may change data decides whether those queries are read-only, which64
// queries are read-only, which picks the writer or the reader.64
// picks the writer or the reader. Volatility does not decide it.65
//66
// - 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 without68
// DELIMITER, as Seagull splits the file into statements itself;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, but71
// 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, and73
// SpeciesPrice goes to the reader.74
//65
//75
// With binary logging on, as it is by default, MySQL refuses a function that is not66
// - land_catch inserts a row, so LandCatch goes to the writer;76
// DETERMINISTIC, NO SQL or READS SQL DATA, so the two MODIFIES SQL DATA functions are67
// - price_pence and boat_haul only read data, so BoatHaul, which calls both, goes to77
// declared DETERMINISTIC too.68
// the reader. boat_haul RETURNS TABLE, so it is a relation with typed columns, and69
// its $1 is typed from the function's TEXT argument;70
// - species_price is declared VOLATILE, but its body only reads, so SpeciesPrice goes71
// to the reader. The sgl.migration(readonly: true) line above it in the migration72
// states that outright, as a function whose body Seagull cannot read needs.78
//73
//79
// A function's result can be NULL, so the columns are pointers: species_price finds no74
// A function's result can be NULL, so the columns are pointers: species_price finds no80
// row for a fish with no price.75
// row for a fish with no price. price_pence is STRICT, so a NULL argument gives a NULL76
// result; here its arguments come from boat_haul, which may return NULLs.81
//77
//82
// Takes database (*sql.DB) which is a migrated database.78
// Takes database (*sql.DB) which is a migrated database.83
// Takes output (io.Writer) which receives each call's connection and result.79
// Takes output (io.Writer) which receives each call's connection and result.84
//80
//85
// Returns error when a query fails.81
// Returns error when a query fails.86
func run(ctx context.Context, database *sql.DB, output io.Writer) error {82
func run(ctx context.Context, database *sql.DB, output io.Writer) error {87
var last string83
var last string88
queries := store.NewWithReplica(84
queries := store.NewWithReplica(89
connection{DB: database, last: &last, name: "writer"},85
connection{DB: database, last: &last, name: "writer"},90
connection{DB: database, last: &last, name: "reader"},86
connection{DB: database, last: &last, name: "reader"},91
)87
)92
88
93
prices := []store.SetPriceParams{89
prices := []store.SetPriceParams{94
{Species: "crab", PencePerKilo: 1200},90
{Species: "crab", PencePerKilo: 1200},95
{Species: "mackerel", PencePerKilo: 450},91
{Species: "mackerel", PencePerKilo: 450},96
{Species: "pollock", PencePerKilo: 800},92
{Species: "pollock", PencePerKilo: 800},97
}93
}98
for _, price := range prices {94
for _, price := range prices {99
if err := queries.SetPrice(ctx, price); err != nil {95
if err := queries.SetPrice(ctx, price); err != nil {100
return fmt.Errorf("pricing %s: %w", price.Species, err)96
return fmt.Errorf("pricing %s: %w", price.Species, err)101
}97
}102
}98
}103
99
104
catches := []store.LandCatchParams{100
catches := []store.LandCatchParams{105
{Boat: "Kittiwake", Species: "mackerel", Grams: 12000},101
{Boat: "Kittiwake", Species: "mackerel", Grams: 12000},106
{Boat: "Kittiwake", Species: "crab", Grams: 3500},102
{Boat: "Kittiwake", Species: "crab", Grams: 3500},107
{Boat: "Puffin", Species: "pollock", Grams: 8000},103
{Boat: "Puffin", Species: "pollock", Grams: 8000},108
}104
}109
for _, catch := range catches {105
for _, catch := range catches {110
landed, err := queries.LandCatch(ctx, catch)106
landed, err := queries.LandCatch(ctx, catch)111
if err != nil {107
if err != nil {112
return fmt.Errorf("landing %s: %w", catch.Species, err)108
return fmt.Errorf("landing %s: %w", catch.Species, err)113
}109
}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)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)115
}111
}116
112
117
for _, species := range []string{"crab", "halibut"} {113
for _, species := range []string{"crab", "halibut"} {118
price, err := queries.SpeciesPrice(ctx, species)114
price, err := queries.SpeciesPrice(ctx, species)119
if err != nil {115
if err != nil {120
return fmt.Errorf("pricing %s: %w", species, err)116
return fmt.Errorf("pricing %s: %w", species, err)121
}117
}122
if price.PencePerKilo == nil {118
if price.PencePerKilo == nil {123
fmt.Fprintf(output, "SpeciesPrice on the %s: %s has no price\n", last, species)119
fmt.Fprintf(output, "SpeciesPrice on the %s: %s has no price\n", last, species)124
continue120
continue125
}121
}126
fmt.Fprintf(output, "SpeciesPrice on the %s: %s fetches %dp a kilo\n", last, species, *price.PencePerKilo)122
fmt.Fprintf(output, "SpeciesPrice on the %s: %s fetches %dp a kilo\n", last, species, *price.PencePerKilo)127
}123
}128
124
129
haul, err := queries.BoatHaul(ctx, "Kittiwake")125
haul, err := queries.BoatHaul(ctx, "Kittiwake")130
if err != nil {126
if err != nil {131
return fmt.Errorf("reading the Kittiwake's haul: %w", err)127
return fmt.Errorf("reading the Kittiwake's haul: %w", err)132
}128
}133
fmt.Fprintf(output, "BoatHaul on the %s: the Kittiwake's haul\n", last)129
fmt.Fprintf(output, "BoatHaul on the %s: the Kittiwake's haul\n", last)134
for _, row := range haul {130
for _, row := range haul {135
fmt.Fprintf(output, " %-9s %6dg %5sp\n", row.Species, row.WeightGrams, orNull(row.ValuePence))131
fmt.Fprintf(output, " %-9s %6sg %5sp\n", orNull(row.Species), orNull(row.WeightGrams), orNull(row.ValuePence))136
}132
}137
return nil133
return nil138
}134
}139
135
140
// orNull formats a nullable field, or returns NULL when the field is nil.136
// orNull formats a nullable field, or returns NULL when the field is nil.141
//137
//142
// Takes value (*T) which is a nullable field.138
// Takes value (*T) which is a nullable field.143
//139
//144
// Returns string which is the text to print.140
// Returns string which is the text to print.145
func orNull[T any](value *T) string {141
func orNull[T any](value *T) string {146
if value == nil {142
if value == nil {147
return "NULL"143
return "NULL"148
}144
}149
return fmt.Sprint(*value)145
return fmt.Sprint(*value)150
}146
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | MySQL | PostgreSQL |
|---|---|---|---|
| BoatHaul | column species | string | *string |
| BoatHaul | column weight_grams | int32 | *int32 |