JSON columns: MariaDB and ClickHouse
Every file of the MariaDB and ClickHouse versions side by side, with the lines that differ marked. 8 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
MariaDB
ClickHouse
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: orders4
- name: orders5
dialect: mariadb5
dialect: clickhouse6
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
MariaDB
ClickHouse
1
CREATE TABLE orders (1
CREATE TABLE orders (2
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,2
id UInt64,3
customer VARCHAR(50) NOT NULL,3
customer String,4
basket JSON NOT NULL,4
basket String,5
delivery JSON5
delivery Nullable(String)6
);6
) ENGINE = MergeTree() ORDER BY id;db/migrations/0001_orders.down.sqlidentical
MariaDB
ClickHouse
1
DROP TABLE orders;1
DROP TABLE orders;Queries
PlaceOrderdiffers
MariaDB · db/queries/orders.sql
ClickHouse · 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 (id, customer, basket, delivery)3
VALUES ({id:UInt64}, {customer:String}, {basket:String}, {delivery:Nullable(String)});GetOrderdiffers
MariaDB · db/queries/orders.sql
ClickHouse · 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 = {id:UInt64};OrdersWithFishdiffers
MariaDB · db/queries/orders.sql
ClickHouse · db/queries/orders.sql
1
-- sgl.query(OrdersWithFish, many)1
-- sgl.query(OrdersWithFish, many)2
-- ?1 as sgl.param(fish)3
SELECT2
SELECT4
id,3
id,5
customer,4
customer,6
JSON_EXTRACT(basket, '$.extras') AS extras,5
JSONExtractRaw(basket, 'extras') AS extras,7
JSON_UNQUOTE(JSON_EXTRACT(delivery, '$.address')) AS address6
JSONExtractString(delivery, 'address') AS address8
FROM orders7
FROM orders9
WHERE JSON_UNQUOTE(JSON_EXTRACT(basket, '$.fish')) = ?18
WHERE JSONExtractString(basket, 'fish') = {fish:String}10
ORDER BY id;9
ORDER BY id;What sgl generates
PlaceOrderdiffers
MariaDB
ClickHouse
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
ID uint644
Customer string5
Customer string5
Basket JSON6
Basket string6
Delivery JSON7
Delivery *string7
}8
}8
9
type JSON []byteGetOrderdiffers
MariaDB
ClickHouse
1
func (queries *Queries) GetOrder(ctx context.Context, id int64) (GetOrderRow, error)1
func (queries *Queries) GetOrder(ctx context.Context, id uint64) (GetOrderRow, error)2
2
3
type GetOrderRow struct {3
type GetOrderRow struct {4
ID int64 `json:"id"`4
ID uint64 `json:"id"`5
Customer string `json:"customer"`5
Customer string `json:"customer"`6
Basket JSON `json:"basket"`6
Basket string `json:"basket"`7
Delivery JSON `json:"delivery"`7
Delivery *string `json:"delivery"`8
}8
}9
10
type JSON []byteOrdersWithFishdiffers
MariaDB
ClickHouse
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 uint64 `json:"id"`5
Customer string `json:"customer"`5
Customer string `json:"customer"`6
Extras JSON `json:"extras"`6
Extras string `json:"extras"`7
Address *string `json:"address"`7
Address *string `json:"address"`8
}8
}9
10
type JSON []byteThe program
example.godiffers
MariaDB
ClickHouse
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/mariadb/006_json/internal/store"10
"seagull.sh/seagull/examples/clickhouse/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. The documents live in String32
// generated store.JSON type, which holds the raw document as bytes: encode a value into32
// columns, the usual home for JSON text in ClickHouse, so Basket is a string and33
// it with json.Marshal and decode it with json.Unmarshal. A nil store.JSON is SQL NULL.33
// Delivery, a Nullable(String), is *string: encode a value into it with json.Marshal and34
//34
// decode it with json.Unmarshal. A nil Delivery is SQL NULL.35
// In MariaDB, JSON is another name for LONGTEXT with a JSON_VALID check. An invalid36
// document is refused, and a valid one is kept as the text it was written as, so it comes37
// back byte for byte: here, in the field order and compact form json.Marshal gave it.38
//35
//39
// MariaDB has no -> or ->> operators, and sgl vet reports Q010 for them, naming the36
// OrdersWithFish reads inside the documents with ClickHouse's JSON functions instead of40
// function to use instead. OrdersWithFish calls those functions. JSON_EXTRACT returns a37
// operators: JSONExtractString(basket, 'fish') extracts a value as text, and41
// JSON fragment, so Extras is store.JSON again, written in MariaDB's own style with a38
// JSONExtractRaw(basket, 'extras') extracts the fragment's JSON text. Both return a42
// space after each comma. JSON_UNQUOTE(JSON_EXTRACT(...)) returns SQL text, so Address is39
// String, which is empty for a missing key rather than NULL, so Extras is a string.43
// *string, NULL when the delivery column or its address is missing. The fish parameter is40
// Address is *string because the delivery column is Nullable(String), and a JSON function44
// compared with that text, so it is inferred as a string, and its binding line only gives41
// given NULL returns NULL.45
// it a name in place of p1. JSON_VALUE(basket, '$.fish') is a shorter spelling for a46
// single value.47
//42
//48
// Takes database (*sql.DB) which is a migrated database.43
// Takes database (*sql.DB) which is a migrated database.49
// Takes output (io.Writer) which receives the decoded orders and the search.44
// Takes output (io.Writer) which receives the decoded orders and the search.50
//45
//51
// Returns error when a query fails or a document does not encode or decode.46
// Returns error when a query fails or a document does not encode or decode.52
func run(ctx context.Context, database *sql.DB, output io.Writer) error {47
func run(ctx context.Context, database *sql.DB, output io.Writer) error {53
queries := store.New(database)48
queries := store.New(database)54
49
55
orders := []struct {50
orders := []struct {56
customer string51
customer string57
basket basket52
basket basket58
delivery *delivery53
delivery *delivery59
}{54
}{60
{customer: "Steven", basket: basket{Fish: "cod", Portions: 2, Extras: []string{"mushy peas", "curry sauce"}}},55
{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{}}},56
{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"}},57
{customer: "Morwenna", basket: basket{Fish: "cod", Portions: 1, Extras: []string{"pickled egg"}}, delivery: &delivery{Address: "4 Harbour Row"}},63
}58
}64
for _, order := range orders {59
for index, order := range orders {65
encodedBasket, err := json.Marshal(order.basket)60
encodedBasket, err := json.Marshal(order.basket)66
if err != nil {61
if err != nil {67
return fmt.Errorf("encoding %s's basket: %w", order.customer, err)62
return fmt.Errorf("encoding %s's basket: %w", order.customer, err)68
}63
}69
parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}64
parameters := store.PlaceOrderParams{ID: uint64(index + 1), Customer: order.customer, Basket: string(encodedBasket), Delivery: nil}70
if order.delivery != nil {65
if order.delivery != nil {71
encodedDelivery, err := json.Marshal(order.delivery)66
encodedDelivery, err := json.Marshal(order.delivery)72
if err != nil {67
if err != nil {73
return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)68
return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)74
}69
}75
parameters.Delivery = encodedDelivery70
parameters.Delivery = new(string(encodedDelivery))76
}71
}77
if err := queries.PlaceOrder(ctx, parameters); err != nil {72
if err := queries.PlaceOrder(ctx, parameters); err != nil {78
return fmt.Errorf("placing %s's order: %w", order.customer, err)73
return fmt.Errorf("placing %s's order: %w", order.customer, err)79
}74
}80
}75
}81
76
82
first, err := queries.GetOrder(ctx, 1)77
first, err := queries.GetOrder(ctx, 1)83
if err != nil {78
if err != nil {84
return fmt.Errorf("getting order 1: %w", err)79
return fmt.Errorf("getting order 1: %w", err)85
}80
}86
var decoded basket81
var decoded basket87
if err := json.Unmarshal(first.Basket, &decoded); err != nil {82
if err := json.Unmarshal([]byte(first.Basket), &decoded); err != nil {88
return fmt.Errorf("decoding order 1's basket: %w", err)83
return fmt.Errorf("decoding order 1's basket: %w", err)89
}84
}90
fmt.Fprintf(output, "order 1 stored %s\n", first.Basket)85
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)86
fmt.Fprintf(output, "decoded: %d x %s with %v, delivery is NULL: %t\n", decoded.Portions, decoded.Fish, decoded.Extras, first.Delivery == nil)92
87
93
cod, err := queries.OrdersWithFish(ctx, "cod")88
cod, err := queries.OrdersWithFish(ctx, "cod")94
if err != nil {89
if err != nil {95
return fmt.Errorf("finding cod orders: %w", err)90
return fmt.Errorf("finding cod orders: %w", err)96
}91
}97
fmt.Fprintln(output, "\ncod orders:")92
fmt.Fprintln(output, "\ncod orders:")98
for _, order := range cod {93
for _, order := range cod {99
address := "collecting"94
address := "collecting"100
if order.Address != nil {95
if order.Address != nil {101
address = "to " + *order.Address96
address = "to " + *order.Address102
}97
}103
fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)98
fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)104
}99
}105
return nil100
return nil106
}101
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | MariaDB | ClickHouse |
|---|---|---|---|
| PlaceOrder | parameter basket | JSON | string |
| PlaceOrder | parameter delivery | JSON | *string |
| GetOrder | parameter id | int64 | uint64 |
| GetOrder | column id | int64 | uint64 |
| GetOrder | column basket | JSON | string |
| GetOrder | column delivery | JSON | *string |
| OrdersWithFish | column id | int64 | uint64 |
| OrdersWithFish | column extras | JSON | string |