Missing rows without errors: DuckDB and TimescaleDB
Every file of the DuckDB and TimescaleDB versions side by side, with the lines that differ marked.
Configuration
sgl.yamldiffers
DuckDB
TimescaleDB
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: colony4
- name: ringing5
dialect: duckdb5
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
migrate:13
dsn: ${env:DATABASE_URL}Migrations
db/migrations/0001_gulls.up.sqldiffers
DuckDB
TimescaleDB
1
CREATE SEQUENCE gull_ids;2
3
CREATE TABLE gulls (1
CREATE TABLE gulls (4
id BIGINT PRIMARY KEY DEFAULT nextval('gull_ids'),2
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,5
ring VARCHAR NOT NULL UNIQUE,3
ring TEXT NOT NULL UNIQUE,6
species VARCHAR NOT NULL4
species TEXT NOT NULL7
);5
);db/migrations/0001_gulls.down.sqldiffers
DuckDB
TimescaleDB
1
DROP TABLE gulls;1
DROP TABLE gulls;2
DROP SEQUENCE gull_ids;Queries
RingGullidentical
DuckDB · db/queries/gulls.sql
TimescaleDB · 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
DuckDB · db/queries/gulls.sql
TimescaleDB · 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
DuckDB
TimescaleDB
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
DuckDB
TimescaleDB
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.godiffers
DuckDB
TimescaleDB
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/duckdb/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 for22
// 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.24
//21
// Takes database (*sql.DB) which is a migrated database.25
// Takes database (*sql.DB) which is a migrated database.22
// Takes output (io.Writer) which receives each ringing and lookup.26
// Takes output (io.Writer) which receives each ringing and lookup.23
//27
//24
// Returns error when a query fails.28
// Returns error when a query fails.25
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 {26
queries := store.New(database)30
queries := store.New(database)27
31
28
attempts := []store.RingGullParams{32
attempts := []store.RingGullParams{29
{Ring: "GB1001", Species: "herring gull"},33
{Ring: "GB1001", Species: "herring gull"},30
{Ring: "GB1002", Species: "kittiwake"},34
{Ring: "GB1002", Species: "kittiwake"},31
{Ring: "GB1001", Species: "black-headed gull"},35
{Ring: "GB1001", Species: "black-headed gull"},36
{Ring: "GB1003", Species: "black-headed gull"},32
}37
}33
for _, attempt := range attempts {38
for _, attempt := range attempts {34
ringed, ok, err := queries.RingGull(ctx, attempt)39
ringed, ok, err := queries.RingGull(ctx, attempt)35
if err != nil {40
if err != nil {36
return fmt.Errorf("ringing %s: %w", attempt.Ring, err)41
return fmt.Errorf("ringing %s: %w", attempt.Ring, err)37
}42
}38
if !ok {43
if !ok {39
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)40
continue45
continue41
}46
}42
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)43
}48
}44
49
45
for _, ring := range []string{"GB1002", "GB9999"} {50
for _, ring := range []string{"GB1002", "GB9999"} {46
gull, ok, err := queries.FindGull(ctx, ring)51
gull, ok, err := queries.FindGull(ctx, ring)47
if err != nil {52
if err != nil {48
return fmt.Errorf("finding %s: %w", ring, err)53
return fmt.Errorf("finding %s: %w", ring, err)49
}54
}50
if !ok {55
if !ok {51
fmt.Fprintf(output, "no gull wears ring %s\n", ring)56
fmt.Fprintf(output, "no gull wears ring %s\n", ring)52
continue57
continue53
}58
}54
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)55
}60
}56
return nil61
return nil57
}62
}