Insert or update: 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: stock4
- name: stock5
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_stock.up.sqlidentical
MariaDB
MySQL
1
CREATE TABLE stock (1
CREATE TABLE stock (2
item VARCHAR(50) NOT NULL PRIMARY KEY,2
item VARCHAR(50) NOT NULL PRIMARY KEY,3
quantity INT NOT NULL,3
quantity INT NOT NULL,4
deliveries INT NOT NULL DEFAULT 04
deliveries INT NOT NULL DEFAULT 05
);5
);db/migrations/0001_stock.down.sqlidentical
MariaDB
MySQL
1
DROP TABLE stock;1
DROP TABLE stock;Queries
ReceiveDeliveryidentical
MariaDB · db/queries/stock.sql
MySQL · db/queries/stock.sql
1
-- sgl.query(ReceiveDelivery, execrows)1
-- sgl.query(ReceiveDelivery, execrows)2
INSERT INTO stock (item, quantity, deliveries) VALUES (?, ?, 1)2
INSERT INTO stock (item, quantity, deliveries) VALUES (?, ?, 1)3
ON DUPLICATE KEY UPDATE3
ON DUPLICATE KEY UPDATE4
quantity = quantity + VALUES(quantity),4
quantity = quantity + VALUES(quantity),5
deliveries = deliveries + 1;5
deliveries = deliveries + 1;GetStockidentical
MariaDB · db/queries/stock.sql
MySQL · db/queries/stock.sql
1
-- sgl.query(GetStock, one)1
-- sgl.query(GetStock, one)2
SELECT item, quantity, deliveries FROM stock WHERE item = ?;2
SELECT item, quantity, deliveries FROM stock WHERE item = ?;AddItemidentical
MariaDB · db/queries/stock.sql
MySQL · db/queries/stock.sql
1
-- sgl.query(AddItem, execrows)1
-- sgl.query(AddItem, execrows)2
INSERT INTO stock (item, quantity) VALUES (?, 0)2
INSERT INTO stock (item, quantity) VALUES (?, 0)3
ON DUPLICATE KEY UPDATE item = item;3
ON DUPLICATE KEY UPDATE item = item;ListStockidentical
MariaDB · db/queries/stock.sql
MySQL · db/queries/stock.sql
1
-- sgl.query(ListStock, many)1
-- sgl.query(ListStock, many)2
SELECT item, quantity, deliveries FROM stock ORDER BY item;2
SELECT item, quantity, deliveries FROM stock ORDER BY item;What sgl generates
ReceiveDeliveryidentical
MariaDB
MySQL
1
func (queries *Queries) ReceiveDelivery(ctx context.Context, params ReceiveDeliveryParams) (int64, error)1
func (queries *Queries) ReceiveDelivery(ctx context.Context, params ReceiveDeliveryParams) (int64, error)2
2
3
type ReceiveDeliveryParams struct {3
type ReceiveDeliveryParams struct {4
Item string4
Item string5
Quantity int325
Quantity int326
}6
}GetStockidentical
MariaDB
MySQL
1
func (queries *Queries) GetStock(ctx context.Context, item string) (GetStockRow, error)1
func (queries *Queries) GetStock(ctx context.Context, item string) (GetStockRow, error)2
2
3
type GetStockRow struct {3
type GetStockRow struct {4
Item string `json:"item"`4
Item string `json:"item"`5
Quantity int32 `json:"quantity"`5
Quantity int32 `json:"quantity"`6
Deliveries int32 `json:"deliveries"`6
Deliveries int32 `json:"deliveries"`7
}7
}AddItemidentical
MariaDB
MySQL
1
func (queries *Queries) AddItem(ctx context.Context, item string) (int64, error)1
func (queries *Queries) AddItem(ctx context.Context, item string) (int64, error)ListStockidentical
MariaDB
MySQL
1
func (queries *Queries) ListStock(ctx context.Context) ([]ListStockRow, error)1
func (queries *Queries) ListStock(ctx context.Context) ([]ListStockRow, error)2
2
3
type ListStockRow struct {3
type ListStockRow struct {4
Item string `json:"item"`4
Item string `json:"item"`5
Quantity int32 `json:"quantity"`5
Quantity int32 `json:"quantity"`6
Deliveries int32 `json:"deliveries"`6
Deliveries int32 `json:"deliveries"`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/021_upserts/internal/store"9
"seagull.sh/seagull/examples/mysql/021_upserts/internal/store"10
)10
)11
11
12
// run books three deliveries into the stock table, two of them for the same item, then12
// run books three deliveries into the stock table, two of them for the same item, then13
// adds two items only if they are new, and prints the stock that results.13
// adds two items only if they are new, and prints the stock that results.14
//14
//15
// Takes database (*sql.DB) which is a migrated database.15
// Takes database (*sql.DB) which is a migrated database.16
// Takes output (io.Writer) which receives each upsert's outcome and the final stock.16
// Takes output (io.Writer) which receives each upsert's outcome and the final stock.17
//17
//18
// Returns error when a query fails.18
// Returns error when a query fails.19
func run(ctx context.Context, database *sql.DB, output io.Writer) error {19
func run(ctx context.Context, database *sql.DB, output io.Writer) error {20
queries := store.New(database)20
queries := store.New(database)21
21
22
// ReceiveDelivery inserts a new item, or adds to the quantity of one already in stock.22
// ReceiveDelivery inserts a new item, or adds to the quantity of one already in stock.23
// VALUES(quantity) is the value the INSERT tried to write, so the parameters stay plain23
// VALUES(quantity) is the value the INSERT tried to write, so the parameters stay plain24
// Item and Quantity fields. MySQL 8.0.19 and later also accept a row alias, VALUES (?,24
// Item and Quantity fields. MySQL 8.0.19 and later also accept a row alias, VALUES (?,25
// ?, 1) AS new with new.quantity, and deprecate VALUES(); MariaDB accepts only VALUES(),25
// ?, 1) AS new with new.quantity, and deprecate VALUES(); MariaDB accepts only VALUES(),26
// so the example keeps to it.26
// so the example keeps to it.27
//27
//28
// MySQL has no RETURNING, so the method is execrows, and MySQL's count says what28
// MySQL has no RETURNING, so the method is execrows, and MySQL's count says what29
// happened: 1 for a row inserted, 2 for a row updated. GetStock then reads the row as it29
// happened: 1 for a row inserted, 2 for a row updated. GetStock then reads the row as it30
// ended up.30
// ended up.31
deliveries := []store.ReceiveDeliveryParams{31
deliveries := []store.ReceiveDeliveryParams{32
{Item: "cod", Quantity: 10},32
{Item: "cod", Quantity: 10},33
{Item: "potatoes", Quantity: 40},33
{Item: "potatoes", Quantity: 40},34
{Item: "cod", Quantity: 5},34
{Item: "cod", Quantity: 5},35
}35
}36
for _, delivery := range deliveries {36
for _, delivery := range deliveries {37
affected, err := queries.ReceiveDelivery(ctx, delivery)37
affected, err := queries.ReceiveDelivery(ctx, delivery)38
if err != nil {38
if err != nil {39
return fmt.Errorf("receiving %s: %w", delivery.Item, err)39
return fmt.Errorf("receiving %s: %w", delivery.Item, err)40
}40
}41
row, err := queries.GetStock(ctx, delivery.Item)41
row, err := queries.GetStock(ctx, delivery.Item)42
if err != nil {42
if err != nil {43
return fmt.Errorf("reading %s: %w", delivery.Item, err)43
return fmt.Errorf("reading %s: %w", delivery.Item, err)44
}44
}45
outcome := "inserted"45
outcome := "inserted"46
if affected == 2 {46
if affected == 2 {47
outcome = "updated"47
outcome = "updated"48
}48
}49
fmt.Fprintf(output, "%-8s delivery %d: +%d, now %d in stock (%s)\n",49
fmt.Fprintf(output, "%-8s delivery %d: +%d, now %d in stock (%s)\n",50
row.Item, row.Deliveries, delivery.Quantity, row.Quantity, outcome)50
row.Item, row.Deliveries, delivery.Quantity, row.Quantity, outcome)51
}51
}52
52
53
// AddItem sets item = item on a duplicate, which changes nothing, so MySQL counts 053
// AddItem sets item = item on a duplicate, which changes nothing, so MySQL counts 054
// rows: execrows reports 1 for a new item and 0 when the item was already there. It is54
// rows: execrows reports 1 for a new item and 0 when the item was already there. It is55
// MySQL's form of ON CONFLICT DO NOTHING; INSERT IGNORE would also hide errors such as a55
// MySQL's form of ON CONFLICT DO NOTHING; INSERT IGNORE would also hide errors such as a56
// value too long for its column.56
// value too long for its column.57
for _, item := range []string{"peas", "cod"} {57
for _, item := range []string{"peas", "cod"} {58
added, err := queries.AddItem(ctx, item)58
added, err := queries.AddItem(ctx, item)59
if err != nil {59
if err != nil {60
return fmt.Errorf("adding %s: %w", item, err)60
return fmt.Errorf("adding %s: %w", item, err)61
}61
}62
if added == 0 {62
if added == 0 {63
fmt.Fprintf(output, "%s is already stocked, nothing changed\n", item)63
fmt.Fprintf(output, "%s is already stocked, nothing changed\n", item)64
continue64
continue65
}65
}66
fmt.Fprintf(output, "added %s with no stock yet\n", item)66
fmt.Fprintf(output, "added %s with no stock yet\n", item)67
}67
}68
68
69
stock, err := queries.ListStock(ctx)69
stock, err := queries.ListStock(ctx)70
if err != nil {70
if err != nil {71
return fmt.Errorf("listing stock: %w", err)71
return fmt.Errorf("listing stock: %w", err)72
}72
}73
for _, row := range stock {73
for _, row := range stock {74
fmt.Fprintf(output, "%-8s %2d\n", row.Item, row.Quantity)74
fmt.Fprintf(output, "%-8s %2d\n", row.Item, row.Quantity)75
}75
}76
return nil76
return nil77
}77
}