JSON columns: MySQL and SQLite
Every file of the MySQL and SQLite versions side by side, with the lines that differ marked.
Configuration
sgl.yamldiffers
MySQL
SQLite
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: orders4
- name: orders5
dialect: mysql5
dialect: sqlite6
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_orders.up.sqldiffers
MySQL
SQLite
1
CREATE TABLE orders (1
CREATE TABLE orders (2
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,2
id INTEGER PRIMARY KEY,3
customer VARCHAR(50) NOT NULL,3
customer TEXT NOT NULL,4
basket JSON NOT NULL,4
basket JSON NOT NULL,5
delivery JSON5
delivery JSON6
);6
);db/migrations/0001_orders.down.sqlidentical
MySQL
SQLite
1
DROP TABLE orders;1
DROP TABLE orders;Queries
PlaceOrderidentical
MySQL · db/queries/orders.sql
SQLite · db/queries/orders.sql
1
-- sgl.query(PlaceOrder, exec)1
-- sgl.query(PlaceOrder, exec)2
INSERT INTO orders (customer, basket, delivery) VALUES (?, ?, ?);2
INSERT INTO orders (customer, basket, delivery) VALUES (?, ?, ?);GetOrderidentical
MySQL · db/queries/orders.sql
SQLite · db/queries/orders.sql
1
-- sgl.query(GetOrder, one)1
-- sgl.query(GetOrder, one)2
SELECT id, customer, basket, delivery FROM orders WHERE id = ?;2
SELECT id, customer, basket, delivery FROM orders WHERE id = ?;OrdersWithFishdiffers
MySQL · db/queries/orders.sql
SQLite · db/queries/orders.sql
1
-- sgl.query(OrdersWithFish, many)1
-- sgl.query(OrdersWithFish, many)2
-- ?1 as sgl.param(fish, type: text)2
-- ?1 as sgl.param(fish, type: text)3
SELECT3
SELECT4
id,4
id,5
customer,5
customer,6
basket -> '$.extras' AS extras,6
basket -> '$.extras' AS extras,7
delivery ->> '$.address' AS address7
CAST(delivery ->> '$.address' AS TEXT) AS address8
FROM orders8
FROM orders9
WHERE basket ->> '$.fish' = ?19
WHERE basket ->> '$.fish' = ?110
ORDER BY id;10
ORDER BY id;What sgl generates
PlaceOrderidentical
MySQL
SQLite
1
func (queries *Queries) PlaceOrder(ctx context.Context, params PlaceOrderParams) error1
func (queries *Queries) PlaceOrder(ctx context.Context, params PlaceOrderParams) error2
2
3
type PlaceOrderParams struct {3
type PlaceOrderParams struct {4
Customer string4
Customer string5
Basket JSON5
Basket JSON6
Delivery JSON6
Delivery JSON7
}7
}8
8
9
type JSON []byte9
type JSON []byteGetOrderidentical
MySQL
SQLite
1
func (queries *Queries) GetOrder(ctx context.Context, id int64) (GetOrderRow, error)1
func (queries *Queries) GetOrder(ctx context.Context, id int64) (GetOrderRow, error)2
2
3
type GetOrderRow struct {3
type GetOrderRow struct {4
ID int64 `json:"id"`4
ID int64 `json:"id"`5
Customer string `json:"customer"`5
Customer string `json:"customer"`6
Basket JSON `json:"basket"`6
Basket JSON `json:"basket"`7
Delivery JSON `json:"delivery"`7
Delivery JSON `json:"delivery"`8
}8
}9
9
10
type JSON []byte10
type JSON []byteOrdersWithFishidentical
MySQL
SQLite
1
func (queries *Queries) OrdersWithFish(ctx context.Context, fish string) ([]OrdersWithFishRow, error)1
func (queries *Queries) OrdersWithFish(ctx context.Context, fish string) ([]OrdersWithFishRow, error)2
2
3
type OrdersWithFishRow struct {3
type OrdersWithFishRow struct {4
ID int64 `json:"id"`4
ID int64 `json:"id"`5
Customer string `json:"customer"`5
Customer string `json:"customer"`6
Extras JSON `json:"extras"`6
Extras JSON `json:"extras"`7
Address *string `json:"address"`7
Address *string `json:"address"`8
}8
}9
9
10
type JSON []byte10
type JSON []byteThe program
example.godiffers
MySQL
SQLite
1
package main1
package main2
2
3
import (3
import (4
"context"4
"context"5
"database/sql"5
"database/sql"6
"encoding/json"6
"encoding/json"7
"fmt"7
"fmt"8
"io"8
"io"9
9
10
"seagull.sh/seagull/examples/mysql/006_json/internal/store"10
"seagull.sh/seagull/examples/sqlite/006_json/internal/store"11
)11
)12
12
13
// basket is the document an order's basket column holds.13
// basket is the document an order's basket column holds.14
type basket struct {14
type basket struct {15
// Fish is the fish the order is for.15
// Fish is the fish the order is for.16
Fish string `json:"fish"`16
Fish string `json:"fish"`17
17
18
// Portions is how many portions of fish and chips.18
// Portions is how many portions of fish and chips.19
Portions int `json:"portions"`19
Portions int `json:"portions"`20
20
21
// Extras lists the sides.21
// Extras lists the sides.22
Extras []string `json:"extras"`22
Extras []string `json:"extras"`23
}23
}24
24
25
// delivery is the document an order's delivery column holds, when the order is delivered.25
// delivery is the document an order's delivery column holds, when the order is delivered.26
type delivery struct {26
type delivery struct {27
// Address is where the order goes.27
// Address is where the order goes.28
Address string `json:"address"`28
Address string `json:"address"`29
}29
}30
30
31
// run places three chip shop orders and reads them back. A JSON column becomes the31
// run places three chip shop orders and reads them back. A JSON column becomes the32
// generated store.JSON type, which holds the raw document as bytes: encode a value into32
// generated store.JSON type, which holds the raw document as bytes: encode a value into33
// it with json.Marshal and decode it with json.Unmarshal. A nil store.JSON is SQL NULL.33
// it with json.Marshal and decode it with json.Unmarshal. A nil store.JSON is SQL NULL.34
//34
//35
// MySQL checks each document as it is stored and keeps it in a binary form, so what comes35
// OrdersWithFish selects with ->, which extracts a JSON fragment, so Extras is store.JSON36
// back is MySQL's own text for it: keys reordered, with a space after each colon and36
// again. It filters with ->>, which extracts the SQL value the document holds: text, a37
// comma. Decoding gives the same values either way.37
// number or NULL, depending on the document, so Seagull types it as any. The query casts38
//38
// the address to TEXT, which makes Address a *string, nil when the delivery column or its39
// OrdersWithFish filters with ->>, which extracts a value as SQL text, and selects with39
// address is missing.40
// ->, which extracts a JSON fragment, so Extras is store.JSON again. Address is *string41
// because ->> gives NULL when the delivery column or its address is missing. They are42
// MySQL's shorthand for JSON_EXTRACT and JSON_UNQUOTE(JSON_EXTRACT(...)).43
//40
//44
// Takes database (*sql.DB) which is a migrated database.41
// Takes database (*sql.DB) which is a migrated database.45
// Takes output (io.Writer) which receives the decoded orders and the search.42
// Takes output (io.Writer) which receives the decoded orders and the search.46
//43
//47
// Returns error when a query fails or a document does not encode or decode.44
// Returns error when a query fails or a document does not encode or decode.48
func run(ctx context.Context, database *sql.DB, output io.Writer) error {45
func run(ctx context.Context, database *sql.DB, output io.Writer) error {49
queries := store.New(database)46
queries := store.New(database)50
47
51
orders := []struct {48
orders := []struct {52
customer string49
customer string53
basket basket50
basket basket54
delivery *delivery51
delivery *delivery55
}{52
}{56
{customer: "Steven", basket: basket{Fish: "cod", Portions: 2, Extras: []string{"mushy peas", "curry sauce"}}},53
{customer: "Steven", basket: basket{Fish: "cod", Portions: 2, Extras: []string{"mushy peas", "curry sauce"}}},57
{customer: "Gwen", basket: basket{Fish: "haddock", Portions: 1, Extras: []string{}}},54
{customer: "Gwen", basket: basket{Fish: "haddock", Portions: 1, Extras: []string{}}},58
{customer: "Morwenna", basket: basket{Fish: "cod", Portions: 1, Extras: []string{"pickled egg"}}, delivery: &delivery{Address: "4 Harbour Row"}},55
{customer: "Morwenna", basket: basket{Fish: "cod", Portions: 1, Extras: []string{"pickled egg"}}, delivery: &delivery{Address: "4 Harbour Row"}},59
}56
}60
for _, order := range orders {57
for _, order := range orders {61
encodedBasket, err := json.Marshal(order.basket)58
encodedBasket, err := json.Marshal(order.basket)62
if err != nil {59
if err != nil {63
return fmt.Errorf("encoding %s's basket: %w", order.customer, err)60
return fmt.Errorf("encoding %s's basket: %w", order.customer, err)64
}61
}65
parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}62
parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}66
if order.delivery != nil {63
if order.delivery != nil {67
encodedDelivery, err := json.Marshal(order.delivery)64
encodedDelivery, err := json.Marshal(order.delivery)68
if err != nil {65
if err != nil {69
return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)66
return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)70
}67
}71
parameters.Delivery = encodedDelivery68
parameters.Delivery = encodedDelivery72
}69
}73
if err := queries.PlaceOrder(ctx, parameters); err != nil {70
if err := queries.PlaceOrder(ctx, parameters); err != nil {74
return fmt.Errorf("placing %s's order: %w", order.customer, err)71
return fmt.Errorf("placing %s's order: %w", order.customer, err)75
}72
}76
}73
}77
74
78
first, err := queries.GetOrder(ctx, 1)75
first, err := queries.GetOrder(ctx, 1)79
if err != nil {76
if err != nil {80
return fmt.Errorf("getting order 1: %w", err)77
return fmt.Errorf("getting order 1: %w", err)81
}78
}82
var decoded basket79
var decoded basket83
if err := json.Unmarshal(first.Basket, &decoded); err != nil {80
if err := json.Unmarshal(first.Basket, &decoded); err != nil {84
return fmt.Errorf("decoding order 1's basket: %w", err)81
return fmt.Errorf("decoding order 1's basket: %w", err)85
}82
}86
fmt.Fprintf(output, "order 1 stored %s\n", first.Basket)83
fmt.Fprintf(output, "order 1 stored %s\n", first.Basket)87
fmt.Fprintf(output, "decoded: %d x %s with %v, delivery is NULL: %t\n", decoded.Portions, decoded.Fish, decoded.Extras, first.Delivery == nil)84
fmt.Fprintf(output, "decoded: %d x %s with %v, delivery is NULL: %t\n", decoded.Portions, decoded.Fish, decoded.Extras, first.Delivery == nil)88
85
89
cod, err := queries.OrdersWithFish(ctx, "cod")86
cod, err := queries.OrdersWithFish(ctx, "cod")90
if err != nil {87
if err != nil {91
return fmt.Errorf("finding cod orders: %w", err)88
return fmt.Errorf("finding cod orders: %w", err)92
}89
}93
fmt.Fprintln(output, "\ncod orders:")90
fmt.Fprintln(output, "\ncod orders:")94
for _, order := range cod {91
for _, order := range cod {95
address := "collecting"92
address := "collecting"96
if order.Address != nil {93
if order.Address != nil {97
address = "to " + *order.Address94
address = "to " + *order.Address98
}95
}99
fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)96
fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)100
}97
}101
return nil98
return nil102
}99
}