Arrays and lists as typed slices: TimescaleDB and PostgreSQL
TimescaleDB and PostgreSQL run the same files for this example: the SQL and the Go are identical.
Configuration
sgl.yamlidentical
TimescaleDB
PostgreSQL
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: voyages4
- name: voyages5
dialect: postgres5
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/sql11
driver: database/sqlMigrations
db/migrations/0001_voyages.up.sqlidentical
TimescaleDB
PostgreSQL
1
CREATE TABLE voyages (1
CREATE TABLE voyages (2
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,2
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,3
boat TEXT NOT NULL,3
boat TEXT NOT NULL,4
ports TEXT[] NOT NULL,4
ports TEXT[] NOT NULL,5
catch_kg BIGINT[] NOT NULL,5
catch_kg BIGINT[] NOT NULL,6
sightings TEXT[]6
sightings TEXT[]7
);7
);8
-- sgl.column(voyages.ports, non_null_elements: true)8
-- sgl.column(voyages.ports, non_null_elements: true)db/migrations/0001_voyages.down.sqlidentical
TimescaleDB
PostgreSQL
1
DROP TABLE voyages;1
DROP TABLE voyages;Queries
LogVoyageidentical
TimescaleDB · db/queries/voyages.sql
PostgreSQL · db/queries/voyages.sql
1
-- sgl.query(LogVoyage, exec)1
-- sgl.query(LogVoyage, exec)2
INSERT INTO voyages (boat, ports, catch_kg, sightings) VALUES ($1, $2, $3, $4);2
INSERT INTO voyages (boat, ports, catch_kg, sightings) VALUES ($1, $2, $3, $4);ListVoyagesidentical
TimescaleDB · db/queries/voyages.sql
PostgreSQL · db/queries/voyages.sql
1
-- sgl.query(ListVoyages, many)1
-- sgl.query(ListVoyages, many)2
SELECT id, boat, ports, catch_kg, sightings FROM voyages ORDER BY id;2
SELECT id, boat, ports, catch_kg, sightings FROM voyages ORDER BY id;VoyagesCallingAtidentical
TimescaleDB · db/queries/voyages.sql
PostgreSQL · db/queries/voyages.sql
1
-- sgl.query(VoyagesCallingAt, many)1
-- sgl.query(VoyagesCallingAt, many)2
-- $1 as sgl.param(port)2
-- $1 as sgl.param(port)3
SELECT boat, ports[1] AS first_port, cardinality(ports) AS stops3
SELECT boat, ports[1] AS first_port, cardinality(ports) AS stops4
FROM voyages4
FROM voyages5
WHERE $1::text = ANY(ports)5
WHERE $1::text = ANY(ports)6
ORDER BY id;6
ORDER BY id;VoyagesPerBoatidentical
TimescaleDB · db/queries/voyages.sql
PostgreSQL · db/queries/voyages.sql
1
-- sgl.query(VoyagesPerBoat, many)1
-- sgl.query(VoyagesPerBoat, many)2
SELECT boat, array_agg(id ORDER BY id) AS voyages2
SELECT boat, array_agg(id ORDER BY id) AS voyages3
FROM voyages3
FROM voyages4
GROUP BY boat4
GROUP BY boat5
ORDER BY boat;5
ORDER BY boat;What sgl generates
LogVoyageidentical
TimescaleDB
PostgreSQL
1
func (queries *Queries) LogVoyage(ctx context.Context, params LogVoyageParams) error1
func (queries *Queries) LogVoyage(ctx context.Context, params LogVoyageParams) error2
2
3
type LogVoyageParams struct {3
type LogVoyageParams struct {4
Boat string4
Boat string5
Ports []string5
Ports []string6
CatchKg []*int646
CatchKg []*int647
Sightings []*string7
Sightings []*string8
}8
}ListVoyagesidentical
TimescaleDB
PostgreSQL
1
func (queries *Queries) ListVoyages(ctx context.Context) ([]ListVoyagesRow, error)1
func (queries *Queries) ListVoyages(ctx context.Context) ([]ListVoyagesRow, error)2
2
3
type ListVoyagesRow struct {3
type ListVoyagesRow struct {4
ID int64 `json:"id"`4
ID int64 `json:"id"`5
Boat string `json:"boat"`5
Boat string `json:"boat"`6
Ports []string `json:"ports"`6
Ports []string `json:"ports"`7
CatchKg []*int64 `json:"catch_kg"`7
CatchKg []*int64 `json:"catch_kg"`8
Sightings []*string `json:"sightings"`8
Sightings []*string `json:"sightings"`9
}9
}VoyagesCallingAtidentical
TimescaleDB
PostgreSQL
1
func (queries *Queries) VoyagesCallingAt(ctx context.Context, port string) ([]VoyagesCallingAtRow, error)1
func (queries *Queries) VoyagesCallingAt(ctx context.Context, port string) ([]VoyagesCallingAtRow, error)2
2
3
type VoyagesCallingAtRow struct {3
type VoyagesCallingAtRow struct {4
Boat string `json:"boat"`4
Boat string `json:"boat"`5
FirstPort *string `json:"first_port"`5
FirstPort *string `json:"first_port"`6
Stops int32 `json:"stops"`6
Stops int32 `json:"stops"`7
}7
}VoyagesPerBoatidentical
TimescaleDB
PostgreSQL
1
func (queries *Queries) VoyagesPerBoat(ctx context.Context) ([]VoyagesPerBoatRow, error)1
func (queries *Queries) VoyagesPerBoat(ctx context.Context) ([]VoyagesPerBoatRow, error)2
2
3
type VoyagesPerBoatRow struct {3
type VoyagesPerBoatRow struct {4
Boat string `json:"boat"`4
Boat string `json:"boat"`5
Voyages []int64 `json:"voyages"`5
Voyages []int64 `json:"voyages"`6
}6
}The program
example.goidentical
TimescaleDB
PostgreSQL
1
package main1
package main2
2
3
import (3
import (4
"context"4
"context"5
"database/sql"5
"database/sql"6
"fmt"6
"fmt"7
"io"7
"io"8
8
9
"seagull.sh/seagull/examples/postgres/008_arrays/internal/store"9
"seagull.sh/seagull/examples/postgres/008_arrays/internal/store"10
)10
)11
11
12
// run logs three fishing voyages and reads them back. An array column becomes a typed Go12
// run logs three fishing voyages and reads them back. An array column becomes a typed Go13
// slice: TEXT[] gives []*string and BIGINT[] gives []*int64, and array_agg gives a slice13
// slice: TEXT[] gives []*string and BIGINT[] gives []*int64, and array_agg gives a slice14
// of its argument's type, here []int64: the id column it collects is NOT NULL, so the14
// of its argument's type, here []int64: the id column it collects is NOT NULL, so the15
// array can hold no NULL and its elements are plain values. A slice parameter is sent as15
// array can hold no NULL and its elements are plain values. A slice parameter is sent as16
// a PostgreSQL array, and a nil slice is sent as NULL.16
// a PostgreSQL array, and a nil slice is sent as NULL.17
//17
//18
// The elements are pointers because PostgreSQL can hold NULL in any array, whatever the18
// The elements are pointers because PostgreSQL can hold NULL in any array, whatever the19
// column's type says, so a NULL element reads as nil, which elements prints as NULL.19
// column's type says, so a NULL element reads as nil, which elements prints as NULL.20
// Parameters take the same pointers, so LogVoyage takes []*int64 for catch_kg and20
// Parameters take the same pointers, so LogVoyage takes []*int64 for catch_kg and21
// []*string for sightings, and a nil element writes NULL; values builds them from plain21
// []*string for sightings, and a nil element writes NULL; values builds them from plain22
// values. When the data never holds a NULL element, non_null_elements: true opts out, in22
// values. When the data never holds a NULL element, non_null_elements: true opts out, in23
// an sgl.column line of a query or migration or as go.types.non_null_elements in23
// an sgl.column line of a query or migration or as go.types.non_null_elements in24
// sgl.yaml: the elements then read and bind as plain values, and a NULL one fails the24
// sgl.yaml: the elements then read and bind as plain values, and a NULL one fails the25
// read. The migration declares it for ports, as a voyage never calls at a NULL port, so25
// read. The migration declares it for ports, as a voyage never calls at a NULL port, so26
// Ports is a plain []string in the model, the row and the parameters alike.26
// Ports is a plain []string in the model, the row and the parameters alike.27
//27
//28
// database/sql cannot scan a PostgreSQL array, so the generated SQL wraps each array28
// database/sql cannot scan a PostgreSQL array, so the generated SQL wraps each array29
// column in to_json() and decodes the JSON into the slice; open internal/store to see it.29
// column in to_json() and decodes the JSON into the slice; open internal/store to see it.30
// A NULL array therefore scans to a nil slice, and an empty array to an empty one.30
// A NULL array therefore scans to a nil slice, and an empty array to an empty one.31
//31
//32
// VoyagesCallingAt searches inside an array with $1::text = ANY(ports). The cast gives32
// VoyagesCallingAt searches inside an array with $1::text = ANY(ports). The cast gives33
// the parameter its type, for Seagull, which would otherwise leave it as any, and for33
// the parameter its type, for Seagull, which would otherwise leave it as any, and for34
// CockroachDB, which cannot work it out from ANY. ports[1] reads one element, and is34
// CockroachDB, which cannot work it out from ANY. ports[1] reads one element, and is35
// *string because a missing element is NULL.35
// *string because a missing element is NULL.36
//36
//37
// Takes database (*sql.DB) which is a migrated database.37
// Takes database (*sql.DB) which is a migrated database.38
// Takes output (io.Writer) which receives the voyages and the searches.38
// Takes output (io.Writer) which receives the voyages and the searches.39
//39
//40
// Returns error when a query fails.40
// Returns error when a query fails.41
func run(ctx context.Context, database *sql.DB, output io.Writer) error {41
func run(ctx context.Context, database *sql.DB, output io.Writer) error {42
queries := store.New(database)42
queries := store.New(database)43
43
44
voyages := []store.LogVoyageParams{44
voyages := []store.LogVoyageParams{45
{Boat: "Kittiwake", Ports: []string{"Newlyn", "St Ives", "Mousehole"}, CatchKg: values[int64](120, 85), Sightings: values("dolphin", "basking shark")},45
{Boat: "Kittiwake", Ports: []string{"Newlyn", "St Ives", "Mousehole"}, CatchKg: values[int64](120, 85), Sightings: values("dolphin", "basking shark")},46
{Boat: "Salty Chip", Ports: []string{"Penzance", "Mousehole"}, CatchKg: values[int64](40), Sightings: nil},46
{Boat: "Salty Chip", Ports: []string{"Penzance", "Mousehole"}, CatchKg: values[int64](40), Sightings: nil},47
{Boat: "Kittiwake", Ports: []string{"Newlyn", "Lamorna"}, CatchKg: values[int64](60, 30, 15), Sightings: []*string{}},47
{Boat: "Kittiwake", Ports: []string{"Newlyn", "Lamorna"}, CatchKg: values[int64](60, 30, 15), Sightings: []*string{}},48
}48
}49
for _, voyage := range voyages {49
for _, voyage := range voyages {50
if err := queries.LogVoyage(ctx, voyage); err != nil {50
if err := queries.LogVoyage(ctx, voyage); err != nil {51
return fmt.Errorf("logging %s's voyage: %w", voyage.Boat, err)51
return fmt.Errorf("logging %s's voyage: %w", voyage.Boat, err)52
}52
}53
}53
}54
54
55
logged, err := queries.ListVoyages(ctx)55
logged, err := queries.ListVoyages(ctx)56
if err != nil {56
if err != nil {57
return fmt.Errorf("listing voyages: %w", err)57
return fmt.Errorf("listing voyages: %w", err)58
}58
}59
for _, voyage := range logged {59
for _, voyage := range logged {60
sightings := elements(voyage.Sightings)60
sightings := elements(voyage.Sightings)61
if voyage.Sightings == nil {61
if voyage.Sightings == nil {62
sightings = "NULL"62
sightings = "NULL"63
}63
}64
fmt.Fprintf(output, "voyage %d %-10s ports %s, catch %s kg, sightings %s\n", voyage.ID, voyage.Boat, voyage.Ports, elements(voyage.CatchKg), sightings)64
fmt.Fprintf(output, "voyage %d %-10s ports %s, catch %s kg, sightings %s\n", voyage.ID, voyage.Boat, voyage.Ports, elements(voyage.CatchKg), sightings)65
}65
}66
66
67
calling, err := queries.VoyagesCallingAt(ctx, "Mousehole")67
calling, err := queries.VoyagesCallingAt(ctx, "Mousehole")68
if err != nil {68
if err != nil {69
return fmt.Errorf("finding voyages calling at Mousehole: %w", err)69
return fmt.Errorf("finding voyages calling at Mousehole: %w", err)70
}70
}71
fmt.Fprintln(output, "\ncalling at Mousehole:")71
fmt.Fprintln(output, "\ncalling at Mousehole:")72
for _, voyage := range calling {72
for _, voyage := range calling {73
fmt.Fprintf(output, " %-10s from %s, %d stops\n", voyage.Boat, *voyage.FirstPort, voyage.Stops)73
fmt.Fprintf(output, " %-10s from %s, %d stops\n", voyage.Boat, *voyage.FirstPort, voyage.Stops)74
}74
}75
75
76
perBoat, err := queries.VoyagesPerBoat(ctx)76
perBoat, err := queries.VoyagesPerBoat(ctx)77
if err != nil {77
if err != nil {78
return fmt.Errorf("grouping voyages by boat: %w", err)78
return fmt.Errorf("grouping voyages by boat: %w", err)79
}79
}80
fmt.Fprintln(output, "\nvoyages per boat:")80
fmt.Fprintln(output, "\nvoyages per boat:")81
for _, boat := range perBoat {81
for _, boat := range perBoat {82
fmt.Fprintf(output, " %-10s %v\n", boat.Boat, boat.Voyages)82
fmt.Fprintf(output, " %-10s %v\n", boat.Boat, boat.Voyages)83
}83
}84
return nil84
return nil85
}85
}86
86
87
// elements returns an array's elements as fmt prints a slice, with NULL for each element87
// elements returns an array's elements as fmt prints a slice, with NULL for each element88
// the database holds NULL in.88
// the database holds NULL in.89
//89
//90
// Takes array ([]*T) which holds the elements, nil for a NULL one.90
// Takes array ([]*T) which holds the elements, nil for a NULL one.91
//91
//92
// Returns string which is the elements in brackets, separated by spaces.92
// Returns string which is the elements in brackets, separated by spaces.93
func elements[T any](array []*T) string {93
func elements[T any](array []*T) string {94
texts := make([]string, 0, len(array))94
texts := make([]string, 0, len(array))95
for _, element := range array {95
for _, element := range array {96
if element == nil {96
if element == nil {97
texts = append(texts, "NULL")97
texts = append(texts, "NULL")98
continue98
continue99
}99
}100
texts = append(texts, fmt.Sprint(*element))100
texts = append(texts, fmt.Sprint(*element))101
}101
}102
return fmt.Sprint(texts)102
return fmt.Sprint(texts)103
}103
}104
104
105
// values returns pointers to copies of plain values, as an array parameter whose elements105
// values returns pointers to copies of plain values, as an array parameter whose elements106
// may be NULL takes them.106
// may be NULL takes them.107
//107
//108
// Takes items (...T) which are the elements.108
// Takes items (...T) which are the elements.109
//109
//110
// Returns []*T which points at the copies, none of them nil.110
// Returns []*T which points at the copies, none of them nil.111
func values[T any](items ...T) []*T {111
func values[T any](items ...T) []*T {112
pointed := make([]*T, len(items))112
pointed := make([]*T, len(items))113
for index := range items {113
for index := range items {114
pointed[index] = &items[index]114
pointed[index] = &items[index]115
}115
}116
return pointed116
return pointed117
}117
}