Missing rows without errors: TimescaleDB and CockroachDB
TimescaleDB and CockroachDB run the same files for this example: the SQL and the Go are identical.
Configuration
sgl.yamlidentical
TimescaleDB
CockroachDB
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: ringing4
- name: ringing5
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/sql12
migrate:12
migrate:13
dsn: ${env:DATABASE_URL}13
dsn: ${env:DATABASE_URL}Migrations
db/migrations/0001_gulls.up.sqlidentical
TimescaleDB
CockroachDB
1
CREATE TABLE gulls (1
CREATE TABLE gulls (2
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,2
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,3
ring TEXT NOT NULL UNIQUE,3
ring TEXT NOT NULL UNIQUE,4
species TEXT NOT NULL4
species TEXT NOT NULL5
);5
);db/migrations/0001_gulls.down.sqlidentical
TimescaleDB
CockroachDB
1
DROP TABLE gulls;1
DROP TABLE gulls;Queries
RingGullidentical
TimescaleDB · db/queries/gulls.sql
CockroachDB · db/queries/gulls.sql
1
-- sgl.query(RingGull, one, optional: true)1
-- sgl.query(RingGull, one, optional: true)2
INSERT INTO gulls (ring, species) VALUES ($1, $2)2
INSERT INTO gulls (ring, species) VALUES ($1, $2)3
ON CONFLICT (ring) DO NOTHING3
ON CONFLICT (ring) DO NOTHING4
RETURNING id, ring;4
RETURNING id, ring;FindGullidentical
TimescaleDB · db/queries/gulls.sql
CockroachDB · db/queries/gulls.sql
1
-- sgl.query(FindGull, one, optional: true)1
-- sgl.query(FindGull, one, optional: true)2
SELECT id, ring, species FROM gulls WHERE ring = $1;2
SELECT id, ring, species FROM gulls WHERE ring = $1;What sgl generates
RingGullidentical
TimescaleDB
CockroachDB
1
func (queries *Queries) RingGull(ctx context.Context, params RingGullParams) (RingGullRow, bool, error)1
func (queries *Queries) RingGull(ctx context.Context, params RingGullParams) (RingGullRow, bool, error)2
2
3
type RingGullParams struct {3
type RingGullParams struct {4
Ring string4
Ring string5
Species string5
Species string6
}6
}7
7
8
type RingGullRow struct {8
type RingGullRow struct {9
ID int64 `json:"id"`9
ID int64 `json:"id"`10
Ring string `json:"ring"`10
Ring string `json:"ring"`11
}11
}FindGullidentical
TimescaleDB
CockroachDB
1
func (queries *Queries) FindGull(ctx context.Context, ring string) (FindGullRow, bool, error)1
func (queries *Queries) FindGull(ctx context.Context, ring string) (FindGullRow, bool, error)2
2
3
type FindGullRow struct {3
type FindGullRow struct {4
ID int64 `json:"id"`4
ID int64 `json:"id"`5
Ring string `json:"ring"`5
Ring string `json:"ring"`6
Species string `json:"species"`6
Species string `json:"species"`7
}7
}The program
example.goidentical
TimescaleDB
CockroachDB
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/018_optional_one/internal/store"9
"seagull.sh/seagull/examples/postgres/018_optional_one/internal/store"10
)10
)11
11
12
// run rings gulls and looks them up. Both queries are one queries with optional: true, so12
// run rings gulls and looks them up. Both queries are one queries with optional: true, so13
// they return (row, ok, error): a missing row gives ok false and a nil error instead of13
// they return (row, ok, error): a missing row gives ok false and a nil error instead of14
// sql.ErrNoRows, and error is left for real failures.14
// sql.ErrNoRows, and error is left for real failures.15
//15
//16
// RingGull inserts with ON CONFLICT (ring) DO NOTHING RETURNING. A ring that is already16
// RingGull inserts with ON CONFLICT (ring) DO NOTHING RETURNING. A ring that is already17
// taken skips the insert, so RETURNING has no row and ok is false. Without optional: true17
// taken skips the insert, so RETURNING has no row and ok is false. Without optional: true18
// sgl vet warns about this (Q046), as the call would fail with sql.ErrNoRows. optional18
// sgl vet warns about this (Q046), as the call would fail with sql.ErrNoRows. optional19
// applies only to a one query; on any other command sgl vet refuses it (Q047).19
// applies only to a one query; on any other command sgl vet refuses it (Q047).20
//20
//21
// PostgreSQL takes the next id from the identity column's sequence before it checks for21
// PostgreSQL takes the next id from the identity column's sequence before it checks for22
// the conflict, and a skipped insert does not hand it back. The black-headed gull that is22
// the conflict, and a skipped insert does not hand it back. The black-headed gull that is23
// turned away and then ringed GB1003 becomes gull 4, not gull 3.23
// turned away and then ringed GB1003 becomes gull 4, not gull 3.24
//24
//25
// Takes database (*sql.DB) which is a migrated database.25
// Takes database (*sql.DB) which is a migrated database.26
// Takes output (io.Writer) which receives each ringing and lookup.26
// Takes output (io.Writer) which receives each ringing and lookup.27
//27
//28
// Returns error when a query fails.28
// Returns error when a query fails.29
func run(ctx context.Context, database *sql.DB, output io.Writer) error {29
func run(ctx context.Context, database *sql.DB, output io.Writer) error {30
queries := store.New(database)30
queries := store.New(database)31
31
32
attempts := []store.RingGullParams{32
attempts := []store.RingGullParams{33
{Ring: "GB1001", Species: "herring gull"},33
{Ring: "GB1001", Species: "herring gull"},34
{Ring: "GB1002", Species: "kittiwake"},34
{Ring: "GB1002", Species: "kittiwake"},35
{Ring: "GB1001", Species: "black-headed gull"},35
{Ring: "GB1001", Species: "black-headed gull"},36
{Ring: "GB1003", Species: "black-headed gull"},36
{Ring: "GB1003", Species: "black-headed gull"},37
}37
}38
for _, attempt := range attempts {38
for _, attempt := range attempts {39
ringed, ok, err := queries.RingGull(ctx, attempt)39
ringed, ok, err := queries.RingGull(ctx, attempt)40
if err != nil {40
if err != nil {41
return fmt.Errorf("ringing %s: %w", attempt.Ring, err)41
return fmt.Errorf("ringing %s: %w", attempt.Ring, err)42
}42
}43
if !ok {43
if !ok {44
fmt.Fprintf(output, "ring %s is already taken, so the %s was not ringed\n", attempt.Ring, attempt.Species)44
fmt.Fprintf(output, "ring %s is already taken, so the %s was not ringed\n", attempt.Ring, attempt.Species)45
continue45
continue46
}46
}47
fmt.Fprintf(output, "ringed a %s as %s, gull %d\n", attempt.Species, ringed.Ring, ringed.ID)47
fmt.Fprintf(output, "ringed a %s as %s, gull %d\n", attempt.Species, ringed.Ring, ringed.ID)48
}48
}49
49
50
for _, ring := range []string{"GB1002", "GB9999"} {50
for _, ring := range []string{"GB1002", "GB9999"} {51
gull, ok, err := queries.FindGull(ctx, ring)51
gull, ok, err := queries.FindGull(ctx, ring)52
if err != nil {52
if err != nil {53
return fmt.Errorf("finding %s: %w", ring, err)53
return fmt.Errorf("finding %s: %w", ring, err)54
}54
}55
if !ok {55
if !ok {56
fmt.Fprintf(output, "no gull wears ring %s\n", ring)56
fmt.Fprintf(output, "no gull wears ring %s\n", ring)57
continue57
continue58
}58
}59
fmt.Fprintf(output, "ring %s is on gull %d, a %s\n", ring, gull.ID, gull.Species)59
fmt.Fprintf(output, "ring %s is on gull %d, a %s\n", ring, gull.ID, gull.Species)60
}60
}61
return nil61
return nil62
}62
}