Compare · 006

JSON columns: ClickHouse and MySQL

Every file of the ClickHouse and MySQL versions side by side, with the lines that differ marked. 8 parameter or column types differ in Go.

Configuration

sgl.yamldiffers
ClickHouse
MySQL
1version: 1
1version: 1
2
2
3databases:
3databases:
4 - name: orders
4 - name: orders
5 dialect: clickhouse
5 dialect: mysql
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
ClickHouse
MySQL
1CREATE TABLE orders (
1CREATE TABLE orders (
2 id UInt64,
2 id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
3 customer String,
3 customer VARCHAR(50) NOT NULL,
4 basket String,
4 basket JSON NOT NULL,
5 delivery Nullable(String)
5 delivery JSON
6) ENGINE = MergeTree() ORDER BY id;
6);
db/migrations/0001_orders.down.sqlidentical
ClickHouse
MySQL
1DROP TABLE orders;
1DROP TABLE orders;

Queries

PlaceOrderdiffers
ClickHouse · db/queries/orders.sql
MySQL · db/queries/orders.sql
1-- sgl.query(PlaceOrder, exec)
1-- sgl.query(PlaceOrder, exec)
2INSERT INTO orders (id, customer, basket, delivery)
2INSERT INTO orders (customer, basket, delivery) VALUES (?, ?, ?);
3VALUES ({id:UInt64}, {customer:String}, {basket:String}, {delivery:Nullable(String)});
GetOrderdiffers
ClickHouse · db/queries/orders.sql
MySQL · db/queries/orders.sql
1-- sgl.query(GetOrder, one)
1-- sgl.query(GetOrder, one)
2SELECT id, customer, basket, delivery FROM orders WHERE id = {id:UInt64};
2SELECT id, customer, basket, delivery FROM orders WHERE id = ?;
OrdersWithFishdiffers
ClickHouse · db/queries/orders.sql
MySQL · db/queries/orders.sql
1-- sgl.query(OrdersWithFish, many)
1-- sgl.query(OrdersWithFish, many)
2-- ?1 as sgl.param(fish, type: text)
2SELECT
3SELECT
3 id,
4 id,
4 customer,
5 customer,
5 JSONExtractRaw(basket, 'extras') AS extras,
6 basket -> '$.extras' AS extras,
6 JSONExtractString(delivery, 'address') AS address
7 delivery ->> '$.address' AS address
7FROM orders
8FROM orders
8WHERE JSONExtractString(basket, 'fish') = {fish:String}
9WHERE basket ->> '$.fish' = ?1
9ORDER BY id;
10ORDER BY id;

What sgl generates

PlaceOrderdiffers
ClickHouse
MySQL
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 ID uint64
5 Customer string
4 Customer string
6 Basket string
5 Basket JSON
7 Delivery *string
6 Delivery JSON
8}
7}
8
9type JSON []byte
GetOrderdiffers
ClickHouse
MySQL
1func (queries *Queries) GetOrder(ctx context.Context, id uint64) (GetOrderRow, error)
1func (queries *Queries) GetOrder(ctx context.Context, id int64) (GetOrderRow, error)
2
2
3type GetOrderRow struct {
3type GetOrderRow struct {
4 ID uint64 `json:"id"`
4 ID int64 `json:"id"`
5 Customer string `json:"customer"`
5 Customer string `json:"customer"`
6 Basket string `json:"basket"`
6 Basket JSON `json:"basket"`
7 Delivery *string `json:"delivery"`
7 Delivery JSON `json:"delivery"`
8}
8}
9
10type JSON []byte
OrdersWithFishdiffers
ClickHouse
MySQL
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 uint64 `json:"id"`
4 ID int64 `json:"id"`
5 Customer string `json:"customer"`
5 Customer string `json:"customer"`
6 Extras string `json:"extras"`
6 Extras JSON `json:"extras"`
7 Address *string `json:"address"`
7 Address *string `json:"address"`
8}
8}
9
10type JSON []byte

The program

example.godiffers
ClickHouse
MySQL
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/clickhouse/006_json/internal/store"
10 "seagull.sh/seagull/examples/mysql/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. The documents live in String
31// run places three chip shop orders and reads them back. A JSON column becomes the
32// columns, the usual home for JSON text in ClickHouse, so Basket is a string and
32// generated store.JSON type, which holds the raw document as bytes: encode a value into
33// Delivery, a Nullable(String), is *string: encode a value into it with json.Marshal and
33// it with json.Marshal and decode it with json.Unmarshal. A nil store.JSON is SQL NULL.
34// decode it with json.Unmarshal. A nil Delivery is SQL NULL.
35//
34//
36// OrdersWithFish reads inside the documents with ClickHouse's JSON functions instead of
35// MySQL checks each document as it is stored and keeps it in a binary form, so what comes
37// operators: JSONExtractString(basket, 'fish') extracts a value as text, and
36// back is MySQL's own text for it: keys reordered, with a space after each colon and
38// JSONExtractRaw(basket, 'extras') extracts the fragment's JSON text. Both return a
37// comma. Decoding gives the same values either way.
39// String, which is empty for a missing key rather than NULL, so Extras is a string.
40// Address is *string because the delivery column is Nullable(String), and a JSON function
41// given NULL returns NULL.
42//
38//
39// OrdersWithFish filters with ->>, which extracts a value as SQL text, and selects with
40// ->, which extracts a JSON fragment, so Extras is store.JSON again. Address is *string
41// because ->> gives NULL when the delivery column or its address is missing. They are
42// MySQL's shorthand for JSON_EXTRACT and JSON_UNQUOTE(JSON_EXTRACT(...)).
43//
43// Takes database (*sql.DB) which is a migrated database.
44// Takes database (*sql.DB) which is a migrated database.
44// Takes output (io.Writer) which receives the decoded orders and the search.
45// Takes output (io.Writer) which receives the decoded orders and the search.
45//
46//
46// Returns error when a query fails or a document does not encode or decode.
47// Returns error when a query fails or a document does not encode or decode.
47func run(ctx context.Context, database *sql.DB, output io.Writer) error {
48func run(ctx context.Context, database *sql.DB, output io.Writer) error {
48 queries := store.New(database)
49 queries := store.New(database)
49
50
50 orders := []struct {
51 orders := []struct {
51 customer string
52 customer string
52 basket basket
53 basket basket
53 delivery *delivery
54 delivery *delivery
54 }{
55 }{
55 {customer: "Steven", basket: basket{Fish: "cod", Portions: 2, Extras: []string{"mushy peas", "curry sauce"}}},
56 {customer: "Steven", basket: basket{Fish: "cod", Portions: 2, Extras: []string{"mushy peas", "curry sauce"}}},
56 {customer: "Gwen", basket: basket{Fish: "haddock", Portions: 1, Extras: []string{}}},
57 {customer: "Gwen", basket: basket{Fish: "haddock", Portions: 1, Extras: []string{}}},
57 {customer: "Morwenna", basket: basket{Fish: "cod", Portions: 1, Extras: []string{"pickled egg"}}, delivery: &delivery{Address: "4 Harbour Row"}},
58 {customer: "Morwenna", basket: basket{Fish: "cod", Portions: 1, Extras: []string{"pickled egg"}}, delivery: &delivery{Address: "4 Harbour Row"}},
58 }
59 }
59 for index, order := range orders {
60 for _, order := range orders {
60 encodedBasket, err := json.Marshal(order.basket)
61 encodedBasket, err := json.Marshal(order.basket)
61 if err != nil {
62 if err != nil {
62 return fmt.Errorf("encoding %s's basket: %w", order.customer, err)
63 return fmt.Errorf("encoding %s's basket: %w", order.customer, err)
63 }
64 }
64 parameters := store.PlaceOrderParams{ID: uint64(index + 1), Customer: order.customer, Basket: string(encodedBasket), Delivery: nil}
65 parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}
65 if order.delivery != nil {
66 if order.delivery != nil {
66 encodedDelivery, err := json.Marshal(order.delivery)
67 encodedDelivery, err := json.Marshal(order.delivery)
67 if err != nil {
68 if err != nil {
68 return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)
69 return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)
69 }
70 }
70 parameters.Delivery = new(string(encodedDelivery))
71 parameters.Delivery = encodedDelivery
71 }
72 }
72 if err := queries.PlaceOrder(ctx, parameters); err != nil {
73 if err := queries.PlaceOrder(ctx, parameters); err != nil {
73 return fmt.Errorf("placing %s's order: %w", order.customer, err)
74 return fmt.Errorf("placing %s's order: %w", order.customer, err)
74 }
75 }
75 }
76 }
76
77
77 first, err := queries.GetOrder(ctx, 1)
78 first, err := queries.GetOrder(ctx, 1)
78 if err != nil {
79 if err != nil {
79 return fmt.Errorf("getting order 1: %w", err)
80 return fmt.Errorf("getting order 1: %w", err)
80 }
81 }
81 var decoded basket
82 var decoded basket
82 if err := json.Unmarshal([]byte(first.Basket), &decoded); err != nil {
83 if err := json.Unmarshal(first.Basket, &decoded); err != nil {
83 return fmt.Errorf("decoding order 1's basket: %w", err)
84 return fmt.Errorf("decoding order 1's basket: %w", err)
84 }
85 }
85 fmt.Fprintf(output, "order 1 stored %s\n", first.Basket)
86 fmt.Fprintf(output, "order 1 stored %s\n", first.Basket)
86 fmt.Fprintf(output, "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)
87
88
88 cod, err := queries.OrdersWithFish(ctx, "cod")
89 cod, err := queries.OrdersWithFish(ctx, "cod")
89 if err != nil {
90 if err != nil {
90 return fmt.Errorf("finding cod orders: %w", err)
91 return fmt.Errorf("finding cod orders: %w", err)
91 }
92 }
92 fmt.Fprintln(output, "\ncod orders:")
93 fmt.Fprintln(output, "\ncod orders:")
93 for _, order := range cod {
94 for _, order := range cod {
94 address := "collecting"
95 address := "collecting"
95 if order.Address != nil {
96 if order.Address != nil {
96 address = "to " + *order.Address
97 address = "to " + *order.Address
97 }
98 }
98 fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)
99 fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)
99 }
100 }
100 return nil
101 return nil
101}
102}

Go type differences

The same SQL types map to different Go types, because each database reports them differently.

QueryFieldClickHouseMySQL
PlaceOrderparameter basketstringJSON
PlaceOrderparameter delivery*stringJSON
GetOrderparameter iduint64int64
GetOrdercolumn iduint64int64
GetOrdercolumn basketstringJSON
GetOrdercolumn delivery*stringJSON
OrdersWithFishcolumn iduint64int64
OrdersWithFishcolumn extrasstringJSON