JSON columns: MySQL and CockroachDB
Every file of the MySQL and CockroachDB versions side by side, with the lines that differ marked.
Configuration
sgl.yamldiffers
MySQL
CockroachDB
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: orders4
- name: orders5
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
json_tags:13
case: camel14
omitempty: true15
struct_tags:16
db: "{name}"17
empty_slices: trueMigrations
db/migrations/0001_orders.up.sqldiffers
MySQL
CockroachDB
1
CREATE TABLE orders (1
CREATE TABLE orders (2
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,2
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,3
customer VARCHAR(50) NOT NULL,3
customer TEXT NOT NULL,4
basket JSON NOT NULL,4
basket JSONB NOT NULL,5
delivery JSON5
delivery JSONB6
);6
);db/migrations/0001_orders.down.sqlidentical
MySQL
CockroachDB
1
DROP TABLE orders;1
DROP TABLE orders;Queries
PlaceOrderdiffers
MySQL · db/queries/orders.sql
CockroachDB · 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 ($1, $2, $3);GetOrderdiffers
MySQL · db/queries/orders.sql
CockroachDB · 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 = $1;OrdersWithFishdiffers
MySQL · db/queries/orders.sql
CockroachDB · 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
delivery ->> 'address' AS delivery_address8
FROM orders8
FROM orders9
WHERE basket ->> '$.fish' = ?19
WHERE basket ->> 'fish' = $110
ORDER BY id;10
ORDER BY id;OrdersContainingonly CockroachDB
MySQL · not in this version
CockroachDB · db/queries/orders.sql
1
-- sgl.query(OrdersContaining, many)2
-- $1 as sgl.param(subset)3
SELECT id, customer4
FROM orders5
WHERE basket @> $1::jsonb6
ORDER BY id;What sgl generates
PlaceOrderidentical
MySQL
CockroachDB
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 []byteGetOrderdiffers
MySQL
CockroachDB
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,omitempty" db:"id"`5
Customer string `json:"customer"`5
Customer string `json:"customer,omitempty" db:"customer"`6
Basket JSON `json:"basket"`6
Basket JSON `json:"basket,omitempty" db:"basket"`7
Delivery JSON `json:"delivery"`7
Delivery JSON `json:"delivery,omitempty" db:"delivery"`8
}8
}9
9
10
type JSON []byte10
type JSON []byteOrdersWithFishdiffers
MySQL
CockroachDB
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,omitempty" db:"id"`5
Customer string `json:"customer"`5
Customer string `json:"customer,omitempty" db:"customer"`6
Extras JSON `json:"extras"`6
Extras JSON `json:"extras,omitempty" db:"extras"`7
Address *string `json:"address"`7
DeliveryAddress *string `json:"deliveryAddress,omitempty" db:"delivery_address"`8
}8
}9
9
10
type JSON []byte10
type JSON []byteOrdersContainingonly CockroachDB
MySQL · not in this version
CockroachDB
1
func (queries *Queries) OrdersContaining(ctx context.Context, subset JSON) ([]OrdersContainingRow, error)2
3
type JSON []byte4
5
type OrdersContainingRow struct {6
ID int64 `json:"id,omitempty" db:"id"`7
Customer string `json:"customer,omitempty" db:"customer"`8
}The program
example.godiffers
MySQL
CockroachDB
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
"reflect"10
"strings"9
11
10
"seagull.sh/seagull/examples/mysql/006_json/internal/store"12
"seagull.sh/seagull/examples/postgres/006_json/internal/store"11
)13
)12
14
13
// basket is the document an order's basket column holds.15
// basket is the document an order's basket column holds.14
type basket struct {16
type basket struct {15
// Fish is the fish the order is for.17
// Fish is the fish the order is for.16
Fish string `json:"fish"`18
Fish string `json:"fish"`17
19
18
// Portions is how many portions of fish and chips.20
// Portions is how many portions of fish and chips.19
Portions int `json:"portions"`21
Portions int `json:"portions"`20
22
21
// Extras lists the sides.23
// Extras lists the sides.22
Extras []string `json:"extras"`24
Extras []string `json:"extras"`23
}25
}24
26
25
// delivery is the document an order's delivery column holds, when the order is delivered.27
// delivery is the document an order's delivery column holds, when the order is delivered.26
type delivery struct {28
type delivery struct {27
// Address is where the order goes.29
// Address is where the order goes.28
Address string `json:"address"`30
Address string `json:"address"`29
}31
}30
32
31
// run places three chip shop orders and reads them back. A JSON column becomes the33
// run places three chip shop orders and reads them back. A JSONB column becomes the32
// generated store.JSON type, which holds the raw document as bytes: encode a value into34
// 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.35
// it with json.Marshal and decode it with json.Unmarshal. A nil store.JSON is SQL NULL.34
//36
//35
// MySQL checks each document as it is stored and keeps it in a binary form, so what comes37
// JSONB stores a parsed document, not the text it was sent, and writes it back in its own36
// back is MySQL's own text for it: keys reordered, with a space after each colon and38
// form: a space after each comma and colon, keys reordered and duplicate keys dropped, so37
// comma. Decoding gives the same values either way.39
// compare documents by decoding them, not by their bytes.38
//40
//39
// OrdersWithFish filters with ->>, which extracts a value as SQL text, and selects with41
// OrdersWithFish filters with ->>, which extracts a value as text, and selects with ->,40
// ->, which extracts a JSON fragment, so Extras is store.JSON again. Address is *string42
// which extracts a JSONB fragment, so Extras is store.JSON again. DeliveryAddress is41
// because ->> gives NULL when the delivery column or its address is missing. They are43
// *string because ->> gives NULL when the delivery column or its address is missing. The42
// MySQL's shorthand for JSON_EXTRACT and JSON_UNQUOTE(JSON_EXTRACT(...)).44
// ->> comparison's parameter is pinned with sgl.param(fish, type: text), as inference45
// leaves it as any. OrdersContaining uses @>, which matches a document containing the one46
// bound to $1::jsonb.43
//47
//48
// The rows go straight out as JSON, as an HTTP handler would send them, so sgl.yaml49
// shapes the generated structs for that: go.json_tags writes each field's json tag in50
// camel case with omitempty, so delivery_address is "deliveryAddress" and a nil one is51
// left out; go.struct_tags adds a db tag holding the column's name, {name}, for libraries52
// that map columns by tag; and go.empty_slices makes a many method return an empty slice53
// when no row matches, which encodes as [] rather than null.54
//44
// Takes database (*sql.DB) which is a migrated database.55
// Takes database (*sql.DB) which is a migrated database.45
// Takes output (io.Writer) which receives the decoded orders and the search.56
// Takes output (io.Writer) which receives the decoded orders and the searches.46
//57
//47
// Returns error when a query fails or a document does not encode or decode.58
// 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 {59
func run(ctx context.Context, database *sql.DB, output io.Writer) error {49
queries := store.New(database)60
queries := store.New(database)50
61
51
orders := []struct {62
orders := []struct {52
customer string63
customer string53
basket basket64
basket basket54
delivery *delivery65
delivery *delivery55
}{66
}{56
{customer: "Steven", basket: basket{Fish: "cod", Portions: 2, Extras: []string{"mushy peas", "curry sauce"}}},67
{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{}}},68
{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"}},69
{customer: "Morwenna", basket: basket{Fish: "cod", Portions: 1, Extras: []string{"pickled egg"}}, delivery: &delivery{Address: "4 Harbour Row"}},59
}70
}60
for _, order := range orders {71
for _, order := range orders {61
encodedBasket, err := json.Marshal(order.basket)72
encodedBasket, err := json.Marshal(order.basket)62
if err != nil {73
if err != nil {63
return fmt.Errorf("encoding %s's basket: %w", order.customer, err)74
return fmt.Errorf("encoding %s's basket: %w", order.customer, err)64
}75
}65
parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}76
parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}66
if order.delivery != nil {77
if order.delivery != nil {67
encodedDelivery, err := json.Marshal(order.delivery)78
encodedDelivery, err := json.Marshal(order.delivery)68
if err != nil {79
if err != nil {69
return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)80
return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)70
}81
}71
parameters.Delivery = encodedDelivery82
parameters.Delivery = encodedDelivery72
}83
}73
if err := queries.PlaceOrder(ctx, parameters); err != nil {84
if err := queries.PlaceOrder(ctx, parameters); err != nil {74
return fmt.Errorf("placing %s's order: %w", order.customer, err)85
return fmt.Errorf("placing %s's order: %w", order.customer, err)75
}86
}76
}87
}77
88
78
first, err := queries.GetOrder(ctx, 1)89
first, err := queries.GetOrder(ctx, 1)79
if err != nil {90
if err != nil {80
return fmt.Errorf("getting order 1: %w", err)91
return fmt.Errorf("getting order 1: %w", err)81
}92
}82
var decoded basket93
var decoded basket83
if err := json.Unmarshal(first.Basket, &decoded); err != nil {94
if err := json.Unmarshal(first.Basket, &decoded); err != nil {84
return fmt.Errorf("decoding order 1's basket: %w", err)95
return fmt.Errorf("decoding order 1's basket: %w", err)85
}96
}86
fmt.Fprintf(output, "order 1 stored %s\n", first.Basket)97
fmt.Fprintf(output, "order 1 decoded: %d x %s with %v, delivery is NULL: %t\n", decoded.Portions, decoded.Fish, decoded.Extras, first.Delivery == nil)87
fmt.Fprintf(output, "decoded: %d x %s with %v, delivery is NULL: %t\n", decoded.Portions, decoded.Fish, decoded.Extras, first.Delivery == nil)88
98
89
cod, err := queries.OrdersWithFish(ctx, "cod")99
cod, err := queries.OrdersWithFish(ctx, "cod")90
if err != nil {100
if err != nil {91
return fmt.Errorf("finding cod orders: %w", err)101
return fmt.Errorf("finding cod orders: %w", err)92
}102
}93
fmt.Fprintln(output, "\ncod orders:")103
fmt.Fprintln(output, "\ncod orders:")94
for _, order := range cod {104
for _, order := range cod {95
address := "collecting"105
address := "collecting"96
if order.Address != nil {106
if order.DeliveryAddress != nil {97
address = "to " + *order.Address107
address = "to " + *order.DeliveryAddress98
}108
}99
fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)109
fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)100
}110
}111
if err := writeJSON(output, "cod orders as JSON", cod); err != nil {112
return err113
}114
115
plaice, err := queries.OrdersWithFish(ctx, "plaice")116
if err != nil {117
return fmt.Errorf("finding plaice orders: %w", err)118
}119
if err := writeJSON(output, "plaice orders as JSON", plaice); err != nil {120
return err121
}122
field, _ := reflect.TypeFor[store.OrdersWithFishRow]().FieldByName("DeliveryAddress")123
fmt.Fprintf(output, "DeliveryAddress is tagged %s\n", field.Tag)124
125
subset := store.JSON(`{"extras": ["pickled egg"]}`)126
containing, err := queries.OrdersContaining(ctx, subset)127
if err != nil {128
return fmt.Errorf("finding orders containing %s: %w", subset, err)129
}130
customers := make([]string, 0, len(containing))131
for _, order := range containing {132
customers = append(customers, order.Customer)133
}134
fmt.Fprintf(output, "\norders containing %s: %s\n", subset, strings.Join(customers, ", "))101
return nil135
return nil102
}136
}137
138
// writeJSON writes rows encoded as JSON, as an HTTP handler would send them, after a139
// label.140
//141
// Takes output (io.Writer) which receives the label and the document.142
// Takes label (string) which says what the rows are.143
// Takes rows (any) which are the rows to encode.144
//145
// Returns error when the rows do not encode.146
func writeJSON(output io.Writer, label string, rows any) error {147
encoded, err := json.Marshal(rows)148
if err != nil {149
return fmt.Errorf("encoding %s: %w", label, err)150
}151
fmt.Fprintf(output, "%s: %s\n", label, encoded)152
return nil153
}