Compare · 006

JSON columns: PostgreSQL and MariaDB

Every file of the PostgreSQL and MariaDB versions side by side, with the lines that differ marked.

Configuration

sgl.yamldiffers
PostgreSQL
MariaDB
1version: 1
1version: 1
2
2
3databases:
3databases:
4 - name: orders
4 - name: orders
5 dialect: postgres
5 dialect: mariadb
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
11 driver: database/sql
12 json_tags:
13 case: camel
14 omitempty: true
15 struct_tags:
16 db: "{name}"
17 empty_slices: true

Migrations

db/migrations/0001_orders.up.sqldiffers
PostgreSQL
MariaDB
1CREATE TABLE orders (
1CREATE TABLE orders (
2 id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
2 id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
3 customer TEXT NOT NULL,
3 customer VARCHAR(50) NOT NULL,
4 basket JSONB NOT NULL,
4 basket JSON NOT NULL,
5 delivery JSONB
5 delivery JSON
6);
6);
db/migrations/0001_orders.down.sqlidentical
PostgreSQL
MariaDB
1DROP TABLE orders;
1DROP TABLE orders;

Queries

PlaceOrderdiffers
PostgreSQL · db/queries/orders.sql
MariaDB · db/queries/orders.sql
1-- sgl.query(PlaceOrder, exec)
1-- sgl.query(PlaceOrder, exec)
2INSERT INTO orders (customer, basket, delivery) VALUES ($1, $2, $3);
2INSERT INTO orders (customer, basket, delivery) VALUES (?, ?, ?);
GetOrderdiffers
PostgreSQL · db/queries/orders.sql
MariaDB · db/queries/orders.sql
1-- sgl.query(GetOrder, one)
1-- sgl.query(GetOrder, one)
2SELECT id, customer, basket, delivery FROM orders WHERE id = $1;
2SELECT id, customer, basket, delivery FROM orders WHERE id = ?;
OrdersWithFishdiffers
PostgreSQL · db/queries/orders.sql
MariaDB · 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)
3SELECT
3SELECT
4 id,
4 id,
5 customer,
5 customer,
6 basket -> 'extras' AS extras,
6 JSON_EXTRACT(basket, '$.extras') AS extras,
7 delivery ->> 'address' AS delivery_address
7 JSON_UNQUOTE(JSON_EXTRACT(delivery, '$.address')) AS address
8FROM orders
8FROM orders
9WHERE basket ->> 'fish' = $1
9WHERE JSON_UNQUOTE(JSON_EXTRACT(basket, '$.fish')) = ?1
10ORDER BY id;
10ORDER BY id;
OrdersContainingonly PostgreSQL
PostgreSQL · db/queries/orders.sql
MariaDB · not in this version
1-- sgl.query(OrdersContaining, many)
2-- $1 as sgl.param(subset)
3SELECT id, customer
4FROM orders
5WHERE basket @> $1::jsonb
6ORDER BY id;

What sgl generates

PlaceOrderidentical
PostgreSQL
MariaDB
1func (queries *Queries) PlaceOrder(ctx context.Context, params PlaceOrderParams) error
1func (queries *Queries) PlaceOrder(ctx context.Context, params PlaceOrderParams) error
2
2
3type PlaceOrderParams struct {
3type PlaceOrderParams struct {
4 Customer string
4 Customer string
5 Basket JSON
5 Basket JSON
6 Delivery JSON
6 Delivery JSON
7}
7}
8
8
9type JSON []byte
9type JSON []byte
GetOrderdiffers
PostgreSQL
MariaDB
1func (queries *Queries) GetOrder(ctx context.Context, id int64) (GetOrderRow, error)
1func (queries *Queries) GetOrder(ctx context.Context, id int64) (GetOrderRow, error)
2
2
3type GetOrderRow struct {
3type GetOrderRow struct {
4 ID int64 `json:"id,omitempty" db:"id"`
4 ID int64 `json:"id"`
5 Customer string `json:"customer,omitempty" db:"customer"`
5 Customer string `json:"customer"`
6 Basket JSON `json:"basket,omitempty" db:"basket"`
6 Basket JSON `json:"basket"`
7 Delivery JSON `json:"delivery,omitempty" db:"delivery"`
7 Delivery JSON `json:"delivery"`
8}
8}
9
9
10type JSON []byte
10type JSON []byte
OrdersWithFishdiffers
PostgreSQL
MariaDB
1func (queries *Queries) OrdersWithFish(ctx context.Context, fish string) ([]OrdersWithFishRow, error)
1func (queries *Queries) OrdersWithFish(ctx context.Context, fish string) ([]OrdersWithFishRow, error)
2
2
3type OrdersWithFishRow struct {
3type OrdersWithFishRow struct {
4 ID int64 `json:"id,omitempty" db:"id"`
4 ID int64 `json:"id"`
5 Customer string `json:"customer,omitempty" db:"customer"`
5 Customer string `json:"customer"`
6 Extras JSON `json:"extras,omitempty" db:"extras"`
6 Extras JSON `json:"extras"`
7 DeliveryAddress *string `json:"deliveryAddress,omitempty" db:"delivery_address"`
7 Address *string `json:"address"`
8}
8}
9
9
10type JSON []byte
10type JSON []byte
OrdersContainingonly PostgreSQL
PostgreSQL
MariaDB · not in this version
1func (queries *Queries) OrdersContaining(ctx context.Context, subset JSON) ([]OrdersContainingRow, error)
2
3type JSON []byte
4
5type OrdersContainingRow struct {
6 ID int64 `json:"id,omitempty" db:"id"`
7 Customer string `json:"customer,omitempty" db:"customer"`
8}

The program

example.godiffers
PostgreSQL
MariaDB
1package main
1package main
2
2
3import (
3import (
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"
11
9
12 "seagull.sh/seagull/examples/postgres/006_json/internal/store"
10 "seagull.sh/seagull/examples/mariadb/006_json/internal/store"
13)
11)
14
12
15// basket is the document an order's basket column holds.
13// basket is the document an order's basket column holds.
16type basket struct {
14type basket struct {
17 // Fish is the fish the order is for.
15 // Fish is the fish the order is for.
18 Fish string `json:"fish"`
16 Fish string `json:"fish"`
19
17
20 // Portions is how many portions of fish and chips.
18 // Portions is how many portions of fish and chips.
21 Portions int `json:"portions"`
19 Portions int `json:"portions"`
22
20
23 // Extras lists the sides.
21 // Extras lists the sides.
24 Extras []string `json:"extras"`
22 Extras []string `json:"extras"`
25}
23}
26
24
27// 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.
28type delivery struct {
26type delivery struct {
29 // Address is where the order goes.
27 // Address is where the order goes.
30 Address string `json:"address"`
28 Address string `json:"address"`
31}
29}
32
30
33// run places three chip shop orders and reads them back. A JSONB column becomes the
31// run places three chip shop orders and reads them back. A JSON column becomes the
34// generated store.JSON type, which holds the raw document as bytes: encode a value into
32// generated store.JSON type, which holds the raw document as bytes: encode a value into
35// 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.
36//
34//
37// JSONB stores a parsed document, not the text it was sent, and writes it back in its own
35// In MariaDB, JSON is another name for LONGTEXT with a JSON_VALID check. An invalid
38// form: a space after each comma and colon, keys reordered and duplicate keys dropped, so
36// document is refused, and a valid one is kept as the text it was written as, so it comes
39// compare documents by decoding them, not by their bytes.
37// back byte for byte: here, in the field order and compact form json.Marshal gave it.
40//
41// OrdersWithFish filters with ->>, which extracts a value as text, and selects with ->,
42// which extracts a JSONB fragment, so Extras is store.JSON again. DeliveryAddress is
43// *string because ->> gives NULL when the delivery column or its address is missing. The
44// ->> comparison's parameter is pinned with sgl.param(fish, type: text), as inference
45// leaves it as any. OrdersContaining uses @>, which matches a document containing the one
46// bound to $1::jsonb.
47//
38//
48// The rows go straight out as JSON, as an HTTP handler would send them, so sgl.yaml
39// MariaDB has no -> or ->> operators, and sgl vet reports Q010 for them, naming the
49// shapes the generated structs for that: go.json_tags writes each field's json tag in
40// function to use instead. OrdersWithFish calls those functions. JSON_EXTRACT returns a
50// camel case with omitempty, so delivery_address is "deliveryAddress" and a nil one is
41// JSON fragment, so Extras is store.JSON again, written in MariaDB's own style with a
51// left out; go.struct_tags adds a db tag holding the column's name, {name}, for libraries
42// space after each comma. JSON_UNQUOTE(JSON_EXTRACT(...)) returns SQL text, so Address is
52// that map columns by tag; and go.empty_slices makes a many method return an empty slice
43// *string, NULL when the delivery column or its address is missing. The fish parameter is
53// when no row matches, which encodes as [] rather than null.
44// compared with that text, so it is inferred as a string, and its binding line only gives
45// it a name in place of p1. JSON_VALUE(basket, '$.fish') is a shorter spelling for a
46// single value.
54//
47//
55// Takes database (*sql.DB) which is a migrated database.
48// Takes database (*sql.DB) which is a migrated database.
56// Takes output (io.Writer) which receives the decoded orders and the searches.
49// Takes output (io.Writer) which receives the decoded orders and the search.
57//
50//
58// Returns error when a query fails or a document does not encode or decode.
51// Returns error when a query fails or a document does not encode or decode.
59func run(ctx context.Context, database *sql.DB, output io.Writer) error {
52func run(ctx context.Context, database *sql.DB, output io.Writer) error {
60 queries := store.New(database)
53 queries := store.New(database)
61
54
62 orders := []struct {
55 orders := []struct {
63 customer string
56 customer string
64 basket basket
57 basket basket
65 delivery *delivery
58 delivery *delivery
66 }{
59 }{
67 {customer: "Steven", basket: basket{Fish: "cod", Portions: 2, Extras: []string{"mushy peas", "curry sauce"}}},
60 {customer: "Steven", basket: basket{Fish: "cod", Portions: 2, Extras: []string{"mushy peas", "curry sauce"}}},
68 {customer: "Gwen", basket: basket{Fish: "haddock", Portions: 1, Extras: []string{}}},
61 {customer: "Gwen", basket: basket{Fish: "haddock", Portions: 1, Extras: []string{}}},
69 {customer: "Morwenna", basket: basket{Fish: "cod", Portions: 1, Extras: []string{"pickled egg"}}, delivery: &delivery{Address: "4 Harbour Row"}},
62 {customer: "Morwenna", basket: basket{Fish: "cod", Portions: 1, Extras: []string{"pickled egg"}}, delivery: &delivery{Address: "4 Harbour Row"}},
70 }
63 }
71 for _, order := range orders {
64 for _, order := range orders {
72 encodedBasket, err := json.Marshal(order.basket)
65 encodedBasket, err := json.Marshal(order.basket)
73 if err != nil {
66 if err != nil {
74 return fmt.Errorf("encoding %s's basket: %w", order.customer, err)
67 return fmt.Errorf("encoding %s's basket: %w", order.customer, err)
75 }
68 }
76 parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}
69 parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}
77 if order.delivery != nil {
70 if order.delivery != nil {
78 encodedDelivery, err := json.Marshal(order.delivery)
71 encodedDelivery, err := json.Marshal(order.delivery)
79 if err != nil {
72 if err != nil {
80 return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)
73 return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)
81 }
74 }
82 parameters.Delivery = encodedDelivery
75 parameters.Delivery = encodedDelivery
83 }
76 }
84 if err := queries.PlaceOrder(ctx, parameters); err != nil {
77 if err := queries.PlaceOrder(ctx, parameters); err != nil {
85 return fmt.Errorf("placing %s's order: %w", order.customer, err)
78 return fmt.Errorf("placing %s's order: %w", order.customer, err)
86 }
79 }
87 }
80 }
88
81
89 first, err := queries.GetOrder(ctx, 1)
82 first, err := queries.GetOrder(ctx, 1)
90 if err != nil {
83 if err != nil {
91 return fmt.Errorf("getting order 1: %w", err)
84 return fmt.Errorf("getting order 1: %w", err)
92 }
85 }
93 var decoded basket
86 var decoded basket
94 if err := json.Unmarshal(first.Basket, &decoded); err != nil {
87 if err := json.Unmarshal(first.Basket, &decoded); err != nil {
95 return fmt.Errorf("decoding order 1's basket: %w", err)
88 return fmt.Errorf("decoding order 1's basket: %w", err)
96 }
89 }
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)
90 fmt.Fprintf(output, "order 1 stored %s\n", first.Basket)
91 fmt.Fprintf(output, "decoded: %d x %s with %v, delivery is NULL: %t\n", decoded.Portions, decoded.Fish, decoded.Extras, first.Delivery == nil)
98
92
99 cod, err := queries.OrdersWithFish(ctx, "cod")
93 cod, err := queries.OrdersWithFish(ctx, "cod")
100 if err != nil {
94 if err != nil {
101 return fmt.Errorf("finding cod orders: %w", err)
95 return fmt.Errorf("finding cod orders: %w", err)
102 }
96 }
103 fmt.Fprintln(output, "\ncod orders:")
97 fmt.Fprintln(output, "\ncod orders:")
104 for _, order := range cod {
98 for _, order := range cod {
105 address := "collecting"
99 address := "collecting"
106 if order.DeliveryAddress != nil {
100 if order.Address != nil {
107 address = "to " + *order.DeliveryAddress
101 address = "to " + *order.Address
108 }
102 }
109 fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)
103 fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)
110 }
104 }
111 if err := writeJSON(output, "cod orders as JSON", cod); err != nil {
112 return err
113 }
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 err
121 }
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, ", "))
135 return nil
105 return nil
136}
106}
137
138// writeJSON writes rows encoded as JSON, as an HTTP handler would send them, after a
139// 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.
146func 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 nil
153}