JSON columns: PostgreSQL and DuckDB
Every file of the PostgreSQL and DuckDB versions side by side, with the lines that differ marked.
Configuration
sgl.yamldiffers
PostgreSQL
DuckDB
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: orders4
- name: chippy5
dialect: postgres5
dialect: duckdb6
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
PostgreSQL
DuckDB
1
CREATE SEQUENCE order_ids;2
1
CREATE TABLE orders (3
CREATE TABLE orders (2
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,4
id BIGINT PRIMARY KEY DEFAULT nextval('order_ids'),3
customer TEXT NOT NULL,5
customer VARCHAR NOT NULL,4
basket JSONB NOT NULL,6
basket JSON NOT NULL,5
delivery JSONB7
delivery JSON6
);8
);db/migrations/0001_orders.down.sqldiffers
PostgreSQL
DuckDB
1
DROP TABLE orders;1
DROP TABLE orders;2
DROP SEQUENCE order_ids;Queries
PlaceOrderidentical
PostgreSQL · db/queries/orders.sql
DuckDB · 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 ($1, $2, $3);GetOrderidentical
PostgreSQL · db/queries/orders.sql
DuckDB · 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 = $1;OrdersWithFishdiffers
PostgreSQL · 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, type: text)2
-- $1 as sgl.param(fish, type: varchar)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
delivery ->> '$.address' AS address8
FROM orders8
FROM orders9
WHERE basket ->> 'fish' = $19
WHERE basket ->> '$.fish' = $110
ORDER BY id;10
ORDER BY id;OrdersContainingonly PostgreSQL
PostgreSQL · db/queries/orders.sql
DuckDB · 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
PostgreSQL
DuckDB
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
PostgreSQL
DuckDB
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
PostgreSQL
DuckDB
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 PostgreSQL
PostgreSQL
DuckDB · 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
PostgreSQL
DuckDB
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/duckdb/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 read by a query34
// generated store.JSON type, which holds the raw document as bytes: encode a value into32
// becomes the generated store.JSON type, which holds the raw document as bytes: decode it35
// 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.36
//34
//37
// JSONB stores a parsed document, not the text it was sent, and writes it back in its own35
// The generated SQL casts each JSON column it reads to VARCHAR, so store.JSON holds the38
// form: a space after each comma and colon, keys reordered and duplicate keys dropped, so36
// document as DuckDB stored it, the keys in the order they were written.39
// compare documents by decoding them, not by their bytes.40
//37
//41
// OrdersWithFish filters with ->>, which extracts a value as text, and selects with ->,38
// PlaceOrder takes its documents as store.JSON too, and the generated code hands42
// which extracts a JSONB fragment, so Extras is store.JSON again. DeliveryAddress is39
// duckdb-go their text; DuckDB's JSON type is VARCHAR underneath and checks the text as43
// *string because ->> gives NULL when the delivery column or its address is missing. The40
// it is stored.44
// ->> 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
//41
//48
// The rows go straight out as JSON, as an HTTP handler would send them, so sgl.yaml42
// OrdersWithFish filters with ->>, which extracts a value as text, and selects with ->,49
// shapes the generated structs for that: go.json_tags writes each field's json tag in43
// which extracts a JSON fragment, so Extras is store.JSON again. Address is *string50
// camel case with omitempty, so delivery_address is "deliveryAddress" and a nil one is44
// because ->> gives NULL when the delivery column or its address is missing.51
// 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
//45
//55
// Takes database (*sql.DB) which is a migrated database.46
// Takes database (*sql.DB) which is a migrated database.56
// Takes output (io.Writer) which receives the decoded orders and the searches.47
// Takes output (io.Writer) which receives the decoded orders and the search.57
//48
//58
// 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.59
func run(ctx context.Context, database *sql.DB, output io.Writer) error {50
func run(ctx context.Context, database *sql.DB, output io.Writer) error {60
queries := store.New(database)51
queries := store.New(database)61
52
62
orders := []struct {53
orders := []struct {63
customer string54
customer string64
basket basket55
basket basket65
delivery *delivery56
delivery *delivery66
}{57
}{67
{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"}}},68
{customer: "Gwen", basket: basket{Fish: "haddock", Portions: 1, Extras: []string{}}},59
{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"}},60
{customer: "Morwenna", basket: basket{Fish: "cod", Portions: 1, Extras: []string{"pickled egg"}}, delivery: &delivery{Address: "4 Harbour Row"}},70
}61
}71
for _, order := range orders {62
for _, order := range orders {72
encodedBasket, err := json.Marshal(order.basket)63
encodedBasket, err := json.Marshal(order.basket)73
if err != nil {64
if err != nil {74
return fmt.Errorf("encoding %s's basket: %w", order.customer, err)65
return fmt.Errorf("encoding %s's basket: %w", order.customer, err)75
}66
}76
parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}67
parameters := store.PlaceOrderParams{Customer: order.customer, Basket: encodedBasket, Delivery: nil}77
if order.delivery != nil {68
if order.delivery != nil {78
encodedDelivery, err := json.Marshal(order.delivery)69
encodedDelivery, err := json.Marshal(order.delivery)79
if err != nil {70
if err != nil {80
return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)71
return fmt.Errorf("encoding %s's delivery: %w", order.customer, err)81
}72
}82
parameters.Delivery = encodedDelivery73
parameters.Delivery = encodedDelivery83
}74
}84
if err := queries.PlaceOrder(ctx, parameters); err != nil {75
if err := queries.PlaceOrder(ctx, parameters); err != nil {85
return fmt.Errorf("placing %s's order: %w", order.customer, err)76
return fmt.Errorf("placing %s's order: %w", order.customer, err)86
}77
}87
}78
}88
79
89
first, err := queries.GetOrder(ctx, 1)80
first, err := queries.GetOrder(ctx, 1)90
if err != nil {81
if err != nil {91
return fmt.Errorf("getting order 1: %w", err)82
return fmt.Errorf("getting order 1: %w", err)92
}83
}93
var decoded basket84
var decoded basket94
if err := json.Unmarshal(first.Basket, &decoded); err != nil {85
if err := json.Unmarshal(first.Basket, &decoded); err != nil {95
return fmt.Errorf("decoding order 1's basket: %w", err)86
return fmt.Errorf("decoding order 1's basket: %w", err)96
}87
}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)88
fmt.Fprintf(output, "order 1 stored %s\n", first.Basket)89
fmt.Fprintf(output, "decoded: %d x %s with %v, delivery is NULL: %t\n", decoded.Portions, decoded.Fish, decoded.Extras, first.Delivery == nil)98
90
99
cod, err := queries.OrdersWithFish(ctx, "cod")91
cod, err := queries.OrdersWithFish(ctx, "cod")100
if err != nil {92
if err != nil {101
return fmt.Errorf("finding cod orders: %w", err)93
return fmt.Errorf("finding cod orders: %w", err)102
}94
}103
fmt.Fprintln(output, "\ncod orders:")95
fmt.Fprintln(output, "\ncod orders:")104
for _, order := range cod {96
for _, order := range cod {105
address := "collecting"97
address := "collecting"106
if order.DeliveryAddress != nil {98
if order.Address != nil {107
address = "to " + *order.DeliveryAddress99
address = "to " + *order.Address108
}100
}109
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)110
}102
}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 nil103
return nil136
}104
}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
}