Pagination with defaults and limits: ClickHouse and TimescaleDB
Every file of the ClickHouse and TimescaleDB versions side by side, with the lines that differ marked. 5 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
ClickHouse
TimescaleDB
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: rockpools4
- name: rockpools5
dialect: clickhouse5
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/sqlMigrations
db/migrations/0001_finds.up.sqldiffers
ClickHouse
TimescaleDB
1
CREATE TABLE finds (1
CREATE TABLE finds (2
id UInt64,2
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,3
creature String,3
creature TEXT NOT NULL,4
pool String,4
pool TEXT NOT NULL,5
size_mm UInt325
size_mm BIGINT NOT NULL6
) ENGINE = MergeTree() ORDER BY id;6
);db/migrations/0001_finds.down.sqlidentical
ClickHouse
TimescaleDB
1
DROP TABLE finds;1
DROP TABLE finds;Queries
LogFinddiffers
ClickHouse · db/queries/finds.sql
TimescaleDB · db/queries/finds.sql
1
-- sgl.query(LogFind, exec)1
-- sgl.query(LogFind, exec)2
INSERT INTO finds (id, creature, pool, size_mm)2
INSERT INTO finds (creature, pool, size_mm) VALUES ($1, $2, $3);3
VALUES ({id:UInt64}, {creature:String}, {pool:String}, {size_mm:UInt32});ListFindsdiffers
ClickHouse · db/queries/finds.sql
TimescaleDB · db/queries/finds.sql
1
-- sgl.query(ListFinds, many)1
-- sgl.query(ListFinds, many)2
-- {page_size:UInt32} as sgl.param(page_size, default: 3, max: 5)2
-- $1 as sgl.param(page_size, default: 3, max: 5)3
-- $2 as sgl.param(page_offset)3
SELECT id, creature, pool, size_mm4
SELECT id, creature, pool, size_mm4
FROM finds5
FROM finds5
ORDER BY id6
ORDER BY id6
LIMIT {page_size:UInt32} OFFSET {page_offset:UInt32};7
LIMIT $1 OFFSET $2;SearchFindsdiffers
ClickHouse · db/queries/finds.sql
TimescaleDB · db/queries/finds.sql
1
-- sgl.query(SearchFinds, many)1
-- sgl.query(SearchFinds, many)2
-- {creature:Nullable(String)} as sgl.param(creature, optional: true)2
-- $1 as sgl.param(creature, optional: true)3
-- {page_size:UInt32} as sgl.param(page_size, default: 3, max: 5)3
-- $2 as sgl.param(page_size, default: 3, max: 5)4
-- $3 as sgl.param(page_offset)4
SELECT id, creature, pool, size_mm5
SELECT id, creature, pool, size_mm5
FROM finds6
FROM finds6
WHERE ({creature:Nullable(String)} IS NULL OR creature = {creature:Nullable(String)})7
WHERE ($1::text IS NULL OR creature = $1)7
ORDER BY size_mm DESC, id8
ORDER BY size_mm DESC, id8
LIMIT {page_size:UInt32} OFFSET {page_offset:UInt32};9
LIMIT $2 OFFSET $3;What sgl generates
LogFinddiffers
ClickHouse
TimescaleDB
1
func (queries *Queries) LogFind(ctx context.Context, params LogFindParams) error1
func (queries *Queries) LogFind(ctx context.Context, params LogFindParams) error2
2
3
type LogFindParams struct {3
type LogFindParams struct {4
ID uint645
Creature string4
Creature string6
Pool string5
Pool string7
SizeMm uint326
SizeMm int648
}7
}ListFindsdiffers
ClickHouse
TimescaleDB
1
func (queries *Queries) ListFinds(ctx context.Context, params ListFindsParams) ([]ListFindsRow, error)1
func (queries *Queries) ListFinds(ctx context.Context, params ListFindsParams) ([]ListFindsRow, error)2
2
3
type ListFindsParams struct {3
type ListFindsParams struct {4
PageSize int4
PageSize int5
PageOffset int5
PageOffset int6
}6
}7
7
8
type ListFindsRow struct {8
type ListFindsRow struct {9
ID uint64 `json:"id"`9
ID int64 `json:"id"`10
Creature string `json:"creature"`10
Creature string `json:"creature"`11
Pool string `json:"pool"`11
Pool string `json:"pool"`12
SizeMm uint32 `json:"size_mm"`12
SizeMm int64 `json:"size_mm"`13
}13
}SearchFindsdiffers
ClickHouse
TimescaleDB
1
func (queries *Queries) SearchFinds(ctx context.Context, params SearchFindsParams) ([]SearchFindsRow, error)1
func (queries *Queries) SearchFinds(ctx context.Context, params SearchFindsParams) ([]SearchFindsRow, error)2
2
3
type SearchFindsParams struct {3
type SearchFindsParams struct {4
Creature *string4
Creature *string5
PageSize int5
PageSize int6
PageOffset int6
PageOffset int7
}7
}8
8
9
type SearchFindsRow struct {9
type SearchFindsRow struct {10
ID uint64 `json:"id"`10
ID int64 `json:"id"`11
Creature string `json:"creature"`11
Creature string `json:"creature"`12
Pool string `json:"pool"`12
Pool string `json:"pool"`13
SizeMm uint32 `json:"size_mm"`13
SizeMm int64 `json:"size_mm"`14
}14
}The program
example.godiffers
ClickHouse
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/clickhouse/013_pagination/internal/store"9
"seagull.sh/seagull/examples/postgres/013_pagination/internal/store"10
)10
)11
11
12
// run logs seven rock pool finds and pages through them. A placeholder that is the whole12
// run logs seven rock pool finds and pages through them. A parameter in a LIMIT or OFFSET13
// LIMIT or OFFSET count becomes a plain int, whatever ClickHouse type it names, so13
// clause becomes a plain int. On page_size, default: 3 replaces a zero value and max: 514
// {page_size:UInt32} gives an int field. On page_size, default: 3 replaces a zero value14
// caps a larger one, before the query runs, so a caller cannot ask for every row at once;15
// and max: 5 caps a larger one, before the query runs, so a caller cannot ask for every15
// a negative size or offset becomes 0.16
// row at once; a negative size or offset becomes 0.17
//18
// ClickHouse has no auto-increment, so each find's id is a parameter.19
//16
//20
// SearchFinds is the shape of a search endpoint: an optional filter, a fixed order and a17
// SearchFinds is the shape of a search endpoint: an optional filter, a fixed order and a21
// page, all in one static query.18
// page, all in one static query.22
//19
//23
// Takes database (*sql.DB) which is a migrated database.20
// Takes database (*sql.DB) which is a migrated database.24
// Takes output (io.Writer) which receives each page.21
// Takes output (io.Writer) which receives each page.25
//22
//26
// Returns error when a query fails.23
// Returns error when a query fails.27
func run(ctx context.Context, database *sql.DB, output io.Writer) error {24
func run(ctx context.Context, database *sql.DB, output io.Writer) error {28
queries := store.New(database)25
queries := store.New(database)29
26
30
finds := []store.LogFindParams{27
finds := []store.LogFindParams{31
{ID: 1, Creature: "crab", Pool: "north", SizeMm: 45},28
{Creature: "crab", Pool: "north", SizeMm: 45},32
{ID: 2, Creature: "starfish", Pool: "east", SizeMm: 80},29
{Creature: "starfish", Pool: "east", SizeMm: 80},33
{ID: 3, Creature: "crab", Pool: "east", SizeMm: 60},30
{Creature: "crab", Pool: "east", SizeMm: 60},34
{ID: 4, Creature: "blenny", Pool: "north", SizeMm: 70},31
{Creature: "blenny", Pool: "north", SizeMm: 70},35
{ID: 5, Creature: "crab", Pool: "south", SizeMm: 30},32
{Creature: "crab", Pool: "south", SizeMm: 30},36
{ID: 6, Creature: "prawn", Pool: "north", SizeMm: 25},33
{Creature: "prawn", Pool: "north", SizeMm: 25},37
{ID: 7, Creature: "crab", Pool: "north", SizeMm: 55},34
{Creature: "crab", Pool: "north", SizeMm: 55},38
}35
}39
for _, find := range finds {36
for _, find := range finds {40
if err := queries.LogFind(ctx, find); err != nil {37
if err := queries.LogFind(ctx, find); err != nil {41
return fmt.Errorf("logging a %s: %w", find.Creature, err)38
return fmt.Errorf("logging a %s: %w", find.Creature, err)42
}39
}43
}40
}44
41
45
pages := []struct {42
pages := []struct {46
label string43
label string47
parameters store.ListFindsParams44
parameters store.ListFindsParams48
}{45
}{49
{label: "size 0 uses the default of 3", parameters: store.ListFindsParams{PageSize: 0, PageOffset: 0}},46
{label: "size 0 uses the default of 3", parameters: store.ListFindsParams{PageSize: 0, PageOffset: 0}},50
{label: "the next page, offset 3", parameters: store.ListFindsParams{PageSize: 3, PageOffset: 3}},47
{label: "the next page, offset 3", parameters: store.ListFindsParams{PageSize: 3, PageOffset: 3}},51
{label: "size 50 is capped at 5", parameters: store.ListFindsParams{PageSize: 50, PageOffset: 0}},48
{label: "size 50 is capped at 5", parameters: store.ListFindsParams{PageSize: 50, PageOffset: 0}},52
}49
}53
for _, page := range pages {50
for _, page := range pages {54
listed, err := queries.ListFinds(ctx, page.parameters)51
listed, err := queries.ListFinds(ctx, page.parameters)55
if err != nil {52
if err != nil {56
return fmt.Errorf("listing finds, %s: %w", page.label, err)53
return fmt.Errorf("listing finds, %s: %w", page.label, err)57
}54
}58
fmt.Fprintf(output, "%s:\n", page.label)55
fmt.Fprintf(output, "%s:\n", page.label)59
for _, find := range listed {56
for _, find := range listed {60
fmt.Fprintf(output, " #%d %s in the %s pool\n", find.ID, find.Creature, find.Pool)57
fmt.Fprintf(output, " #%d %s in the %s pool\n", find.ID, find.Creature, find.Pool)61
}58
}62
}59
}63
60
64
for _, offset := range []int{0, 2} {61
for _, offset := range []int{0, 2} {65
crabs, err := queries.SearchFinds(ctx, store.SearchFindsParams{62
crabs, err := queries.SearchFinds(ctx, store.SearchFindsParams{66
Creature: new("crab"),63
Creature: new("crab"),67
PageSize: 2,64
PageSize: 2,68
PageOffset: offset,65
PageOffset: offset,69
})66
})70
if err != nil {67
if err != nil {71
return fmt.Errorf("searching crabs from %d: %w", offset, err)68
return fmt.Errorf("searching crabs from %d: %w", offset, err)72
}69
}73
fmt.Fprintf(output, "biggest crabs, offset %d:\n", offset)70
fmt.Fprintf(output, "biggest crabs, offset %d:\n", offset)74
for _, crab := range crabs {71
for _, crab := range crabs {75
fmt.Fprintf(output, " #%d %dmm\n", crab.ID, crab.SizeMm)72
fmt.Fprintf(output, " #%d %dmm\n", crab.ID, crab.SizeMm)76
}73
}77
}74
}78
return nil75
return nil79
}76
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | ClickHouse | TimescaleDB |
|---|---|---|---|
| LogFind | parameter size_mm | uint32 | int64 |
| ListFinds | column id | uint64 | int64 |
| ListFinds | column size_mm | uint32 | int64 |
| SearchFinds | column id | uint64 | int64 |
| SearchFinds | column size_mm | uint32 | int64 |