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