Compare · 006

JSON columns: MariaDB and DuckDB

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

Configuration

sgl.yamldiffers
MariaDB
DuckDB
1version: 1
1version: 1
2
2
3databases:
3databases:
4 - name: orders
4 - name: chippy
5 dialect: mariadb
5 dialect: duckdb
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_orders.up.sqldiffers
MariaDB
DuckDB
1CREATE SEQUENCE order_ids;
2
1CREATE TABLE orders (
3CREATE TABLE orders (
2 id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
4 id BIGINT PRIMARY KEY DEFAULT nextval('order_ids'),
3 customer VARCHAR(50) NOT NULL,
5 customer VARCHAR NOT NULL,
4 basket JSON NOT NULL,
6 basket JSON NOT NULL,
5 delivery JSON
7 delivery JSON
6);
8);
db/migrations/0001_orders.down.sqldiffers
MariaDB
DuckDB
1DROP TABLE orders;
1DROP TABLE orders;
2DROP SEQUENCE order_ids;

Queries

PlaceOrderdiffers
MariaDB · db/queries/orders.sql
DuckDB · db/queries/orders.sql
1-- sgl.query(PlaceOrder, exec)
1-- sgl.query(PlaceOrder, exec)
2INSERT INTO orders (customer, basket, delivery) VALUES (?, ?, ?);
2INSERT INTO orders (customer, basket, delivery) VALUES ($1, $2, $3);
GetOrderdiffers
MariaDB · db/queries/orders.sql
DuckDB · db/queries/orders.sql
1-- sgl.query(GetOrder, one)
1-- sgl.query(GetOrder, one)
2SELECT id, customer, basket, delivery FROM orders WHERE id = ?;
2SELECT id, customer, basket, delivery FROM orders WHERE id = $1;
OrdersWithFishdiffers
MariaDB · db/queries/orders.sql
DuckDB · db/queries/orders.sql
1-- sgl.query(OrdersWithFish, many)
1-- sgl.query(OrdersWithFish, many)
2-- ?1 as sgl.param(fish)
2-- $1 as sgl.param(fish, type: varchar)
3SELECT
3SELECT
4 id,
4 id,
5 customer,
5 customer,
6 JSON_EXTRACT(basket, '$.extras') AS extras,
6 basket -> '$.extras' AS extras,
7 JSON_UNQUOTE(JSON_EXTRACT(delivery, '$.address')) AS address
7 delivery ->> '$.address' AS address
8FROM orders
8FROM orders
9WHERE JSON_UNQUOTE(JSON_EXTRACT(basket, '$.fish')) = ?1
9WHERE basket ->> '$.fish' = $1
10ORDER BY id;
10ORDER BY id;

What sgl generates

PlaceOrderidentical
MariaDB
DuckDB
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
GetOrderidentical
MariaDB
DuckDB
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"`
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
10type JSON []byte
10type JSON []byte
OrdersWithFishidentical
MariaDB
DuckDB
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"`
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
10type JSON []byte
10type JSON []byte

The program

example.godiffers
MariaDB
DuckDB
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
9
10 "seagull.sh/seagull/examples/mariadb/006_json/internal/store"
10 "seagull.sh/seagull/examples/duckdb/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.
14type basket struct {
14type 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.
26type delivery struct {
26type 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 the
31// run places three chip shop orders and reads them back. A JSON column read by a query
32// generated store.JSON type, which holds the raw document as bytes: encode a value into
32// becomes the generated store.JSON type, which holds the raw document as bytes: decode it
33// it with json.Marshal and decode it with json.Unmarshal. A nil store.JSON is SQL NULL.
33// with json.Unmarshal. A nil store.JSON is SQL NULL.
34//
34//
35// In MariaDB, JSON is another name for LONGTEXT with a JSON_VALID check. An invalid
35// The generated SQL casts each JSON column it reads to VARCHAR, so store.JSON holds the
36// document is refused, and a valid one is kept as the text it was written as, so it comes
36// document as DuckDB stored it, the keys in the order they were written.
37// back byte for byte: here, in the field order and compact form json.Marshal gave it.
38//
37//
39// MariaDB has no -> or ->> operators, and sgl vet reports Q010 for them, naming the
38// PlaceOrder takes its documents as store.JSON too, and the generated code hands
40// function to use instead. OrdersWithFish calls those functions. JSON_EXTRACT returns a
39// duckdb-go their text; DuckDB's JSON type is VARCHAR underneath and checks the text as
41// JSON fragment, so Extras is store.JSON again, written in MariaDB's own style with a
40// it is stored.
42// space after each comma. JSON_UNQUOTE(JSON_EXTRACT(...)) returns SQL text, so Address is
43// *string, NULL when the delivery column or its address is missing. The fish parameter is
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.
47//
41//
42// OrdersWithFish filters with ->>, which extracts a value as text, and selects with ->,
43// which extracts a JSON fragment, so Extras is store.JSON again. Address is *string
44// because ->> gives NULL when the delivery column or its address is missing.
45//
48// Takes database (*sql.DB) which is a migrated database.
46// Takes database (*sql.DB) which is a migrated database.
49// Takes output (io.Writer) which receives the decoded orders and the search.
47// Takes output (io.Writer) which receives the decoded orders and the search.
50//
48//
51// Returns error when a query fails or a document does not encode or decode.
49// Returns error when a query fails or a document does not encode or decode.
52func run(ctx context.Context, database *sql.DB, output io.Writer) error {
50func run(ctx context.Context, database *sql.DB, output io.Writer) error {
53 queries := store.New(database)
51 queries := store.New(database)
54
52
55 orders := []struct {
53 orders := []struct {
56 customer string
54 customer string
57 basket basket
55 basket basket
58 delivery *delivery
56 delivery *delivery
59 }{
57 }{
60 {customer: "Steven", basket: basket{Fish: "cod", Portions: 2, Extras: []string{"mushy peas", "curry sauce"}}},
58 {customer: "Steven", basket: basket{Fish: "cod", Portions: 2, Extras: []string{"mushy peas", "curry sauce"}}},
61 {customer: "Gwen", basket: basket{Fish: "haddock", Portions: 1, Extras: []string{}}},
59 {customer: "Gwen", basket: basket{Fish: "haddock", Portions: 1, Extras: []string{}}},
62 {customer: "Morwenna", basket: basket{Fish: "cod", Portions: 1, Extras: []string{"pickled egg"}}, delivery: &delivery{Address: "4 Harbour Row"}},
60 {customer: "Morwenna", basket: basket{Fish: "cod", Portions: 1, Extras: []string{"pickled egg"}}, delivery: &delivery{Address: "4 Harbour Row"}},
63 }
61 }
64 for _, order := range orders {
62 for _, order := range orders {
65 encodedBasket, err := json.Marshal(order.basket)
63 encodedBasket, err := json.Marshal(order.basket)
66 if err != nil {
64 if err != nil {
67 return fmt.Errorf("encoding %s's basket: %w", order.customer, err)
65 return fmt.Errorf("encoding %s's basket: %w", order.customer, err)
68 }
66 }
69 parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}
67 parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}
70 if order.delivery != nil {
68 if order.delivery != nil {
71 encodedDelivery, err := json.Marshal(order.delivery)
69 encodedDelivery, err := json.Marshal(order.delivery)
72 if err != nil {
70 if err != nil {
73 return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)
71 return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)
74 }
72 }
75 parameters.Delivery = encodedDelivery
73 parameters.Delivery = encodedDelivery
76 }
74 }
77 if err := queries.PlaceOrder(ctx, parameters); err != nil {
75 if err := queries.PlaceOrder(ctx, parameters); err != nil {
78 return fmt.Errorf("placing %s's order: %w", order.customer, err)
76 return fmt.Errorf("placing %s's order: %w", order.customer, err)
79 }
77 }
80 }
78 }
81
79
82 first, err := queries.GetOrder(ctx, 1)
80 first, err := queries.GetOrder(ctx, 1)
83 if err != nil {
81 if err != nil {
84 return fmt.Errorf("getting order 1: %w", err)
82 return fmt.Errorf("getting order 1: %w", err)
85 }
83 }
86 var decoded basket
84 var decoded basket
87 if err := json.Unmarshal(first.Basket, &decoded); err != nil {
85 if err := json.Unmarshal(first.Basket, &decoded); err != nil {
88 return fmt.Errorf("decoding order 1's basket: %w", err)
86 return fmt.Errorf("decoding order 1's basket: %w", err)
89 }
87 }
90 fmt.Fprintf(output, "order 1 stored %s\n", first.Basket)
88 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)
89 fmt.Fprintf(output, "decoded: %d x %s with %v, delivery is NULL: %t\n", decoded.Portions, decoded.Fish, decoded.Extras, first.Delivery == nil)
92
90
93 cod, err := queries.OrdersWithFish(ctx, "cod")
91 cod, err := queries.OrdersWithFish(ctx, "cod")
94 if err != nil {
92 if err != nil {
95 return fmt.Errorf("finding cod orders: %w", err)
93 return fmt.Errorf("finding cod orders: %w", err)
96 }
94 }
97 fmt.Fprintln(output, "\ncod orders:")
95 fmt.Fprintln(output, "\ncod orders:")
98 for _, order := range cod {
96 for _, order := range cod {
99 address := "collecting"
97 address := "collecting"
100 if order.Address != nil {
98 if order.Address != nil {
101 address = "to " + *order.Address
99 address = "to " + *order.Address
102 }
100 }
103 fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)
101 fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)
104 }
102 }
105 return nil
103 return nil
106}
104}