Pagination with defaults and limits: MySQL and ClickHouse
Every file of the MySQL and ClickHouse versions side by side, with the lines that differ marked. 5 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
MySQL
ClickHouse
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: rockpools4
- name: rockpools5
dialect: mysql5
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/storeMigrations
db/migrations/0001_finds.up.sqldiffers
MySQL
ClickHouse
1
CREATE TABLE finds (1
CREATE TABLE finds (2
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,2
id UInt64,3
creature VARCHAR(50) NOT NULL,3
creature String,4
pool VARCHAR(20) NOT NULL,4
pool String,5
size_mm INT NOT NULL5
size_mm UInt326
);6
) ENGINE = MergeTree() ORDER BY id;db/migrations/0001_finds.down.sqlidentical
MySQL
ClickHouse
1
DROP TABLE finds;1
DROP TABLE finds;Queries
LogFinddiffers
MySQL · 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 (?, ?, ?);2
INSERT INTO finds (id, creature, pool, size_mm)3
VALUES ({id:UInt64}, {creature:String}, {pool:String}, {size_mm:UInt32});ListFindsdiffers
MySQL · 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
MySQL · 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 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
MySQL
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 int327
SizeMm uint327
}8
}ListFindsdiffers
MySQL
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 int32 `json:"size_mm"`12
SizeMm uint32 `json:"size_mm"`13
}13
}SearchFindsdiffers
MySQL
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 int32 `json:"size_mm"`13
SizeMm uint32 `json:"size_mm"`14
}14
}The program
example.godiffers
MySQL
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/mysql/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 | MySQL | ClickHouse |
|---|---|---|---|
| LogFind | parameter size_mm | int32 | uint32 |
| ListFinds | column id | int64 | uint64 |
| ListFinds | column size_mm | int32 | uint32 |
| SearchFinds | column id | int64 | uint64 |
| SearchFinds | column size_mm | int32 | uint32 |