JSON columns: CockroachDB and SQLite
Every file of the CockroachDB and SQLite versions side by side, with the lines that differ marked.
Configuration
sgl.yamldiffers
CockroachDB
SQLite
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: orders4
- name: orders5
dialect: postgres5
dialect: sqlite6
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
CockroachDB
SQLite
1
CREATE TABLE orders (1
CREATE TABLE orders (2
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,2
id INTEGER PRIMARY KEY,3
customer TEXT NOT NULL,3
customer TEXT NOT NULL,4
basket JSONB NOT NULL,4
basket JSON NOT NULL,5
delivery JSONB5
delivery JSON6
);6
);db/migrations/0001_orders.down.sqlidentical
CockroachDB
SQLite
1
DROP TABLE orders;1
DROP TABLE orders;Queries
PlaceOrderdiffers
CockroachDB · db/queries/orders.sql
SQLite · db/queries/orders.sql
1
-- sgl.query(PlaceOrder, exec)1
-- sgl.query(PlaceOrder, exec)2
INSERT INTO orders (customer, basket, delivery) VALUES ($1, $2, $3);2
INSERT INTO orders (customer, basket, delivery) VALUES (?, ?, ?);GetOrderdiffers
CockroachDB · db/queries/orders.sql
SQLite · db/queries/orders.sql
1
-- sgl.query(GetOrder, one)1
-- sgl.query(GetOrder, one)2
SELECT id, customer, basket, delivery FROM orders WHERE id = $1;2
SELECT id, customer, basket, delivery FROM orders WHERE id = ?;OrdersWithFishdiffers
CockroachDB · db/queries/orders.sql
SQLite · 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 delivery_address7
CAST(delivery ->> '$.address' AS TEXT) AS address8
FROM orders8
FROM orders9
WHERE basket ->> 'fish' = $19
WHERE basket ->> '$.fish' = ?110
ORDER BY id;10
ORDER BY id;OrdersContainingonly CockroachDB
CockroachDB · db/queries/orders.sql
SQLite · not in this version
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
CockroachDB
SQLite
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
CockroachDB
SQLite
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,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
10
type JSON []byte10
type JSON []byteOrdersWithFishdiffers
CockroachDB
SQLite
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,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
10
type JSON []byte10
type JSON []byteOrdersContainingonly CockroachDB
CockroachDB
SQLite · not in this version
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
CockroachDB
SQLite
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"11
9
12
"seagull.sh/seagull/examples/postgres/006_json/internal/store"10
"seagull.sh/seagull/examples/sqlite/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.16
type basket struct {14
type 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.28
type delivery struct {26
type 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 the31
// run places three chip shop orders and reads them back. A JSON column becomes the34
// generated store.JSON type, which holds the raw document as bytes: encode a value into32
// generated store.JSON type, which holds the raw document as bytes: encode a value into35
// 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 own35
// OrdersWithFish selects with ->, which extracts a JSON fragment, so Extras is store.JSON38
// form: a space after each comma and colon, keys reordered and duplicate keys dropped, so36
// again. It filters with ->>, which extracts the SQL value the document holds: text, a39
// compare documents by decoding them, not by their bytes.37
// number or NULL, depending on the document, so Seagull types it as any. The query casts40
//38
// the address to TEXT, which makes Address a *string, nil when the delivery column or its41
// OrdersWithFish filters with ->>, which extracts a value as text, and selects with ->,39
// address is missing.42
// which extracts a JSONB fragment, so Extras is store.JSON again. DeliveryAddress is43
// *string because ->> gives NULL when the delivery column or its address is missing. The44
// ->> 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.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
//40
//55
// Takes database (*sql.DB) which is a migrated database.41
// Takes database (*sql.DB) which is a migrated database.56
// Takes output (io.Writer) which receives the decoded orders and the searches.42
// Takes output (io.Writer) which receives the decoded orders and the search.57
//43
//58
// Returns error when a query fails or a document does not encode or decode.44
// Returns error when a query fails or a document does not encode or decode.59
func run(ctx context.Context, database *sql.DB, output io.Writer) error {45
func run(ctx context.Context, database *sql.DB, output io.Writer) error {60
queries := store.New(database)46
queries := store.New(database)61
47
62
orders := []struct {48
orders := []struct {63
customer string49
customer string64
basket basket50
basket basket65
delivery *delivery51
delivery *delivery66
}{52
}{67
{customer: "Steven", basket: basket{Fish: "cod", Portions: 2, Extras: []string{"mushy peas", "curry sauce"}}},53
{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{}}},54
{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"}},55
{customer: "Morwenna", basket: basket{Fish: "cod", Portions: 1, Extras: []string{"pickled egg"}}, delivery: &delivery{Address: "4 Harbour Row"}},70
}56
}71
for _, order := range orders {57
for _, order := range orders {72
encodedBasket, err := json.Marshal(order.basket)58
encodedBasket, err := json.Marshal(order.basket)73
if err != nil {59
if err != nil {74
return fmt.Errorf("encoding %s's basket: %w", order.customer, err)60
return fmt.Errorf("encoding %s's basket: %w", order.customer, err)75
}61
}76
parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}62
parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}77
if order.delivery != nil {63
if order.delivery != nil {78
encodedDelivery, err := json.Marshal(order.delivery)64
encodedDelivery, err := json.Marshal(order.delivery)79
if err != nil {65
if err != nil {80
return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)66
return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)81
}67
}82
parameters.Delivery = encodedDelivery68
parameters.Delivery = encodedDelivery83
}69
}84
if err := queries.PlaceOrder(ctx, parameters); err != nil {70
if err := queries.PlaceOrder(ctx, parameters); err != nil {85
return fmt.Errorf("placing %s's order: %w", order.customer, err)71
return fmt.Errorf("placing %s's order: %w", order.customer, err)86
}72
}87
}73
}88
74
89
first, err := queries.GetOrder(ctx, 1)75
first, err := queries.GetOrder(ctx, 1)90
if err != nil {76
if err != nil {91
return fmt.Errorf("getting order 1: %w", err)77
return fmt.Errorf("getting order 1: %w", err)92
}78
}93
var decoded basket79
var decoded basket94
if err := json.Unmarshal(first.Basket, &decoded); err != nil {80
if err := json.Unmarshal(first.Basket, &decoded); err != nil {95
return fmt.Errorf("decoding order 1's basket: %w", err)81
return fmt.Errorf("decoding order 1's basket: %w", err)96
}82
}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)83
fmt.Fprintf(output, "order 1 stored %s\n", first.Basket)84
fmt.Fprintf(output, "decoded: %d x %s with %v, delivery is NULL: %t\n", decoded.Portions, decoded.Fish, decoded.Extras, first.Delivery == nil)98
85
99
cod, err := queries.OrdersWithFish(ctx, "cod")86
cod, err := queries.OrdersWithFish(ctx, "cod")100
if err != nil {87
if err != nil {101
return fmt.Errorf("finding cod orders: %w", err)88
return fmt.Errorf("finding cod orders: %w", err)102
}89
}103
fmt.Fprintln(output, "\ncod orders:")90
fmt.Fprintln(output, "\ncod orders:")104
for _, order := range cod {91
for _, order := range cod {105
address := "collecting"92
address := "collecting"106
if order.DeliveryAddress != nil {93
if order.Address != nil {107
address = "to " + *order.DeliveryAddress94
address = "to " + *order.Address108
}95
}109
fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)96
fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)110
}97
}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, ", "))135
return nil98
return nil136
}99
}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
}