Pagination with defaults and limits: ClickHouse and SQLite
Every file of the ClickHouse and SQLite versions side by side, with the lines that differ marked. 5 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
ClickHouse
SQLite
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: rockpools4
- name: rockpools5
dialect: clickhouse5
dialect: sqlite6
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/storeMigrations
db/migrations/0001_finds.up.sqldiffers
ClickHouse
SQLite
1
CREATE TABLE finds (1
CREATE TABLE finds (2
id UInt64,2
id INTEGER PRIMARY KEY,3
creature String,3
creature TEXT NOT NULL,4
pool String,4
pool TEXT NOT NULL,5
size_mm UInt325
size_mm INTEGER NOT NULL6
) ENGINE = MergeTree() ORDER BY id;6
);db/migrations/0001_finds.down.sqlidentical
ClickHouse
SQLite
1
DROP TABLE finds;1
DROP TABLE finds;Queries
LogFinddiffers
ClickHouse · db/queries/finds.sql
SQLite · 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 (?, ?, ?);3
VALUES ({id:UInt64}, {creature:String}, {pool:String}, {size_mm:UInt32});ListFindsdiffers
ClickHouse · db/queries/finds.sql
SQLite · 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
SQLite · 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 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
SQLite
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
SQLite
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
SQLite
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
SQLite
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/sqlite/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 | SQLite |
|---|---|---|---|
| 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 |