JSON columns: SQLite and PostgreSQL
Every file of the SQLite and PostgreSQL versions side by side, with the lines that differ marked.
Configuration
sgl.yamldiffers
SQLite
PostgreSQL
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: orders4
- name: orders5
dialect: sqlite5
dialect: postgres6
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
SQLite
PostgreSQL
1
CREATE TABLE orders (1
CREATE TABLE orders (2
id INTEGER PRIMARY KEY,2
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,3
customer TEXT NOT NULL,3
customer TEXT NOT NULL,4
basket JSON NOT NULL,4
basket JSONB NOT NULL,5
delivery JSON5
delivery JSONB6
);6
);db/migrations/0001_orders.down.sqlidentical
SQLite
PostgreSQL
1
DROP TABLE orders;1
DROP TABLE orders;Queries
PlaceOrderdiffers
SQLite · db/queries/orders.sql
PostgreSQL · 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 (customer, basket, delivery) VALUES ($1, $2, $3);GetOrderdiffers
SQLite · db/queries/orders.sql
PostgreSQL · 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 = $1;OrdersWithFishdiffers
SQLite · db/queries/orders.sql
PostgreSQL · 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
CAST(delivery ->> '$.address' AS TEXT) AS address7
delivery ->> 'address' AS delivery_address8
FROM orders8
FROM orders9
WHERE basket ->> '$.fish' = ?19
WHERE basket ->> 'fish' = $110
ORDER BY id;10
ORDER BY id;OrdersContainingonly PostgreSQL
SQLite · not in this version
PostgreSQL · db/queries/orders.sql
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
SQLite
PostgreSQL
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
SQLite
PostgreSQL
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"`4
ID int64 `json:"id,omitempty" db:"id"`5
Customer string `json:"customer"`5
Customer string `json:"customer,omitempty" db:"customer"`6
Basket JSON `json:"basket"`6
Basket JSON `json:"basket,omitempty" db:"basket"`7
Delivery JSON `json:"delivery"`7
Delivery JSON `json:"delivery,omitempty" db:"delivery"`8
}8
}9
9
10
type JSON []byte10
type JSON []byteOrdersWithFishdiffers
SQLite
PostgreSQL
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 int64 `json:"id,omitempty" db:"id"`5
Customer string `json:"customer"`5
Customer string `json:"customer,omitempty" db:"customer"`6
Extras JSON `json:"extras"`6
Extras JSON `json:"extras,omitempty" db:"extras"`7
Address *string `json:"address"`7
DeliveryAddress *string `json:"deliveryAddress,omitempty" db:"delivery_address"`8
}8
}9
9
10
type JSON []byte10
type JSON []byteOrdersContainingonly PostgreSQL
SQLite · not in this version
PostgreSQL
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
SQLite
PostgreSQL
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"9
11
10
"seagull.sh/seagull/examples/sqlite/006_json/internal/store"12
"seagull.sh/seagull/examples/postgres/006_json/internal/store"11
)13
)12
14
13
// basket is the document an order's basket column holds.15
// basket is the document an order's basket column holds.14
type basket struct {16
type basket struct {15
// Fish is the fish the order is for.17
// Fish is the fish the order is for.16
Fish string `json:"fish"`18
Fish string `json:"fish"`17
19
18
// Portions is how many portions of fish and chips.20
// Portions is how many portions of fish and chips.19
Portions int `json:"portions"`21
Portions int `json:"portions"`20
22
21
// Extras lists the sides.23
// Extras lists the sides.22
Extras []string `json:"extras"`24
Extras []string `json:"extras"`23
}25
}24
26
25
// delivery is the document an order's delivery column holds, when the order is delivered.27
// delivery is the document an order's delivery column holds, when the order is delivered.26
type delivery struct {28
type delivery struct {27
// Address is where the order goes.29
// Address is where the order goes.28
Address string `json:"address"`30
Address string `json:"address"`29
}31
}30
32
31
// run places three chip shop orders and reads them back. A JSON column becomes the33
// run places three chip shop orders and reads them back. A JSONB column becomes the32
// generated store.JSON type, which holds the raw document as bytes: encode a value into34
// generated store.JSON type, which holds the raw document as bytes: encode a value into33
// it with json.Marshal and decode it with json.Unmarshal. A nil store.JSON is SQL NULL.35
// it with json.Marshal and decode it with json.Unmarshal. A nil store.JSON is SQL NULL.34
//36
//35
// OrdersWithFish selects with ->, which extracts a JSON fragment, so Extras is store.JSON37
// JSONB stores a parsed document, not the text it was sent, and writes it back in its own36
// again. It filters with ->>, which extracts the SQL value the document holds: text, a38
// form: a space after each comma and colon, keys reordered and duplicate keys dropped, so37
// number or NULL, depending on the document, so Seagull types it as any. The query casts39
// compare documents by decoding them, not by their bytes.38
// the address to TEXT, which makes Address a *string, nil when the delivery column or its39
// address is missing.40
//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 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
//41
// Takes database (*sql.DB) which is a migrated database.55
// Takes database (*sql.DB) which is a migrated database.42
// Takes output (io.Writer) which receives the decoded orders and the search.56
// Takes output (io.Writer) which receives the decoded orders and the searches.43
//57
//44
// Returns error when a query fails or a document does not encode or decode.58
// Returns error when a query fails or a document does not encode or decode.45
func run(ctx context.Context, database *sql.DB, output io.Writer) error {59
func run(ctx context.Context, database *sql.DB, output io.Writer) error {46
queries := store.New(database)60
queries := store.New(database)47
61
48
orders := []struct {62
orders := []struct {49
customer string63
customer string50
basket basket64
basket basket51
delivery *delivery65
delivery *delivery52
}{66
}{53
{customer: "Steven", basket: basket{Fish: "cod", Portions: 2, Extras: []string{"mushy peas", "curry sauce"}}},67
{customer: "Steven", basket: basket{Fish: "cod", Portions: 2, Extras: []string{"mushy peas", "curry sauce"}}},54
{customer: "Gwen", basket: basket{Fish: "haddock", Portions: 1, Extras: []string{}}},68
{customer: "Gwen", basket: basket{Fish: "haddock", Portions: 1, Extras: []string{}}},55
{customer: "Morwenna", basket: basket{Fish: "cod", Portions: 1, Extras: []string{"pickled egg"}}, delivery: &delivery{Address: "4 Harbour Row"}},69
{customer: "Morwenna", basket: basket{Fish: "cod", Portions: 1, Extras: []string{"pickled egg"}}, delivery: &delivery{Address: "4 Harbour Row"}},56
}70
}57
for _, order := range orders {71
for _, order := range orders {58
encodedBasket, err := json.Marshal(order.basket)72
encodedBasket, err := json.Marshal(order.basket)59
if err != nil {73
if err != nil {60
return fmt.Errorf("encoding %s's basket: %w", order.customer, err)74
return fmt.Errorf("encoding %s's basket: %w", order.customer, err)61
}75
}62
parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}76
parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}63
if order.delivery != nil {77
if order.delivery != nil {64
encodedDelivery, err := json.Marshal(order.delivery)78
encodedDelivery, err := json.Marshal(order.delivery)65
if err != nil {79
if err != nil {66
return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)80
return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)67
}81
}68
parameters.Delivery = encodedDelivery82
parameters.Delivery = encodedDelivery69
}83
}70
if err := queries.PlaceOrder(ctx, parameters); err != nil {84
if err := queries.PlaceOrder(ctx, parameters); err != nil {71
return fmt.Errorf("placing %s's order: %w", order.customer, err)85
return fmt.Errorf("placing %s's order: %w", order.customer, err)72
}86
}73
}87
}74
88
75
first, err := queries.GetOrder(ctx, 1)89
first, err := queries.GetOrder(ctx, 1)76
if err != nil {90
if err != nil {77
return fmt.Errorf("getting order 1: %w", err)91
return fmt.Errorf("getting order 1: %w", err)78
}92
}79
var decoded basket93
var decoded basket80
if err := json.Unmarshal(first.Basket, &decoded); err != nil {94
if err := json.Unmarshal(first.Basket, &decoded); err != nil {81
return fmt.Errorf("decoding order 1's basket: %w", err)95
return fmt.Errorf("decoding order 1's basket: %w", err)82
}96
}83
fmt.Fprintf(output, "order 1 stored %s\n", first.Basket)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)84
fmt.Fprintf(output, "decoded: %d x %s with %v, delivery is NULL: %t\n", decoded.Portions, decoded.Fish, decoded.Extras, first.Delivery == nil)85
98
86
cod, err := queries.OrdersWithFish(ctx, "cod")99
cod, err := queries.OrdersWithFish(ctx, "cod")87
if err != nil {100
if err != nil {88
return fmt.Errorf("finding cod orders: %w", err)101
return fmt.Errorf("finding cod orders: %w", err)89
}102
}90
fmt.Fprintln(output, "\ncod orders:")103
fmt.Fprintln(output, "\ncod orders:")91
for _, order := range cod {104
for _, order := range cod {92
address := "collecting"105
address := "collecting"93
if order.Address != nil {106
if order.DeliveryAddress != nil {94
address = "to " + *order.Address107
address = "to " + *order.DeliveryAddress95
}108
}96
fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)109
fmt.Fprintf(output, " order %d %-9s extras %-30s %s\n", order.ID, order.Customer, order.Extras, address)97
}110
}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, ", "))98
return nil135
return nil99
}136
}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
}