Compare · 030

Functions in migrations: MySQL and MariaDB

MySQL and MariaDB run the same files for this example: the SQL and the Go are identical.

Configuration

sgl.yamlidentical
MySQL
MariaDB
1version: 1
1version: 1
2
2
3databases:
3databases:
4 - name: market
4 - name: market
5 dialect: mysql
5 dialect: mysql
6 migrations: db/migrations
6 migrations: db/migrations
7 queries: db/queries
7 queries: db/queries
8 go:
8 go:
9 package: store
9 package: store
10 out: internal/store
10 out: internal/store

Migrations

db/migrations/0001_catches.up.sqlidentical
MySQL
MariaDB
1CREATE TABLE catches (
1CREATE 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 NULL
5 weight_grams INT NOT NULL
6);
6);
7
7
8CREATE TABLE prices (
8CREATE TABLE prices (
9 species VARCHAR(50) NOT NULL PRIMARY KEY,
9 species VARCHAR(50) NOT NULL PRIMARY KEY,
10 pence_per_kilo INT NOT NULL
10 pence_per_kilo INT NOT NULL
11);
11);
db/migrations/0001_catches.down.sqlidentical
MySQL
MariaDB
1DROP TABLE prices;
1DROP TABLE prices;
2DROP TABLE catches;
2DROP TABLE catches;
db/migrations/0002_market_functions.up.sqlidentical
MySQL
MariaDB
1CREATE FUNCTION price_pence(weight_grams INT, pence_per_kilo INT) RETURNS INT
1CREATE FUNCTION price_pence(weight_grams INT, pence_per_kilo INT) RETURNS INT
2DETERMINISTIC NO SQL
2DETERMINISTIC NO SQL
3RETURN weight_grams * pence_per_kilo / 1000;
3RETURN weight_grams * pence_per_kilo / 1000;
4
4
5CREATE FUNCTION land_catch(boat_name VARCHAR(50), caught VARCHAR(50), grams INT) RETURNS BIGINT
5CREATE FUNCTION land_catch(boat_name VARCHAR(50), caught VARCHAR(50), grams INT) RETURNS BIGINT
6DETERMINISTIC MODIFIES SQL DATA
6DETERMINISTIC MODIFIES SQL DATA
7BEGIN
7BEGIN
8 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();
10END;
10END;
11
11
12-- sgl.migration(readonly: true)
12-- sgl.migration(readonly: true)
13CREATE FUNCTION species_price(wanted VARCHAR(50)) RETURNS INT
13CREATE FUNCTION species_price(wanted VARCHAR(50)) RETURNS INT
14DETERMINISTIC MODIFIES SQL DATA
14DETERMINISTIC MODIFIES SQL DATA
15RETURN (SELECT pence_per_kilo FROM prices WHERE species = wanted);
15RETURN (SELECT pence_per_kilo FROM prices WHERE species = wanted);
db/migrations/0002_market_functions.down.sqlidentical
MySQL
MariaDB
1DROP FUNCTION species_price;
1DROP FUNCTION species_price;
2DROP FUNCTION land_catch;
2DROP FUNCTION land_catch;
3DROP FUNCTION price_pence;
3DROP FUNCTION price_pence;

Queries

SetPriceidentical
MySQL · db/queries/market.sql
MariaDB · db/queries/market.sql
1-- sgl.query(SetPrice, exec)
1-- sgl.query(SetPrice, exec)
2INSERT INTO prices (species, pence_per_kilo) VALUES (?, ?);
2INSERT INTO prices (species, pence_per_kilo) VALUES (?, ?);
LandCatchidentical
MySQL · db/queries/market.sql
MariaDB · 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)
5SELECT land_catch(?1, ?2, ?3) AS id;
5SELECT land_catch(?1, ?2, ?3) AS id;
SpeciesPriceidentical
MySQL · db/queries/market.sql
MariaDB · 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)
3SELECT species_price(?1) AS pence_per_kilo;
3SELECT species_price(?1) AS pence_per_kilo;
BoatHaulidentical
MySQL · db/queries/market.sql
MariaDB · db/queries/market.sql
1-- sgl.query(BoatHaul, many)
1-- sgl.query(BoatHaul, many)
2SELECT catches.species, catches.weight_grams, price_pence(catches.weight_grams, prices.pence_per_kilo) AS value_pence
2SELECT catches.species, catches.weight_grams, price_pence(catches.weight_grams, prices.pence_per_kilo) AS value_pence
3FROM catches
3FROM catches
4JOIN prices ON prices.species = catches.species
4JOIN prices ON prices.species = catches.species
5WHERE catches.boat = ?
5WHERE catches.boat = ?
6ORDER BY catches.species;
6ORDER BY catches.species;

What sgl generates

SetPriceidentical
MySQL
MariaDB
1func (queries *Queries) SetPrice(ctx context.Context, params SetPriceParams) error
1func (queries *Queries) SetPrice(ctx context.Context, params SetPriceParams) error
2
2
3type SetPriceParams struct {
3type SetPriceParams struct {
4 Species string
4 Species string
5 PencePerKilo int32
5 PencePerKilo int32
6}
6}
LandCatchidentical
MySQL
MariaDB
1func (queries *Queries) LandCatch(ctx context.Context, params LandCatchParams) (LandCatchRow, error)
1func (queries *Queries) LandCatch(ctx context.Context, params LandCatchParams) (LandCatchRow, error)
2
2
3type LandCatchParams struct {
3type LandCatchParams struct {
4 Boat string
4 Boat string
5 Species string
5 Species string
6 Grams int32
6 Grams int32
7}
7}
8
8
9type LandCatchRow struct {
9type LandCatchRow struct {
10 ID *int64 `json:"id"`
10 ID *int64 `json:"id"`
11}
11}
SpeciesPriceidentical
MySQL
MariaDB
1func (queries *Queries) SpeciesPrice(ctx context.Context, species string) (SpeciesPriceRow, error)
1func (queries *Queries) SpeciesPrice(ctx context.Context, species string) (SpeciesPriceRow, error)
2
2
3type SpeciesPriceRow struct {
3type SpeciesPriceRow struct {
4 PencePerKilo *int32 `json:"pence_per_kilo"`
4 PencePerKilo *int32 `json:"pence_per_kilo"`
5}
5}
BoatHaulidentical
MySQL
MariaDB
1func (queries *Queries) BoatHaul(ctx context.Context, boat string) ([]BoatHaulRow, error)
1func (queries *Queries) BoatHaul(ctx context.Context, boat string) ([]BoatHaulRow, error)
2
2
3type BoatHaulRow struct {
3type 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
MySQL
MariaDB
1package main
1package main
2
2
3import (
3import (
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 example
12// connection is a store.DBTX that notes its name before each statement, so the example
13// can say whether Seagull sent a query to the writer or the reader. Both connections wrap
13// can say whether Seagull sent a query to the writer or the reader. Both connections wrap
14// 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.
15type connection struct {
15type connection struct {
16 *sql.DB
16 *sql.DB
17
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 *string
19 last *string
20
20
21 // name labels the connection.
21 // name labels the connection.
22 name string
22 name string
23}
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.
32func (c connection) ExecContext(ctx context.Context, query string, arguments ...any) (sql.Result, error) {
32func (c connection) ExecContext(ctx context.Context, query string, arguments ...any) (sql.Result, error) {
33 *c.last = c.name
33 *c.last = c.name
34 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.
44func (c connection) QueryContext(ctx context.Context, query string, arguments ...any) (*sql.Rows, error) {
44func (c connection) QueryContext(ctx context.Context, query string, arguments ...any) (*sql.Rows, error) {
45 *c.last = c.name
45 *c.last = c.name
46 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.
55func (c connection) QueryRowContext(ctx context.Context, query string, arguments ...any) *sql.Row {
55func (c connection) QueryRowContext(ctx context.Context, query string, arguments ...any) *sql.Row {
56 *c.last = c.name
56 *c.last = c.name
57 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 SQL
60// run prices the fish, lands three catches and reads the market back, all through the SQL
61// functions that migration 0002 creates. Seagull reads each CREATE FUNCTION when it
61// functions that migration 0002 creates. Seagull reads each CREATE FUNCTION when it
62// builds the schema: the argument and return types type the queries that call it, and
62// builds the schema: the argument and return types type the queries that call it, and
63// what it says about data access, along with what its body does, decides whether those
63// what it says about data access, along with what its body does, decides whether those
64// 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 to
66// - land_catch says MODIFIES SQL DATA and its body inserts a row, so LandCatch goes to
67// the writer. Its body is a BEGIN ... END block, which a migration can hold without
67// the writer. Its body is a BEGIN ... END block, which a migration can hold without
68// 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, but
70// - species_price says MODIFIES SQL DATA, which Seagull takes to mean it writes, but
71// its body only reads. MySQL never checks these characteristics against the body. The
71// its body only reads. MySQL never checks these characteristics against the body. The
72// sgl.migration(readonly: true) line above it in the migration says so, and
72// sgl.migration(readonly: true) line above it in the migration says so, and
73// 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 not
75// With binary logging on, as it is by default, MySQL refuses a function that is not
76// DETERMINISTIC, NO SQL or READS SQL DATA, so the two MODIFIES SQL DATA functions are
76// DETERMINISTIC, NO SQL or READS SQL DATA, so the two MODIFIES SQL DATA functions are
77// 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 no
79// A function's result can be NULL, so the columns are pointers: species_price finds no
80// 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.
86func run(ctx context.Context, database *sql.DB, output io.Writer) error {
86func run(ctx context.Context, database *sql.DB, output io.Writer) error {
87 var last string
87 var last string
88 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 continue
124 continue
125 }
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 nil
137 return nil
138}
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.
145func orNull[T any](value *T) string {
145func 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}