Batch and copyfrom: ClickHouse and MariaDB
Every file of the ClickHouse and MariaDB versions side by side, with the lines that differ marked. 4 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
ClickHouse
MariaDB
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: tides4
- name: tides5
dialect: clickhouse5
dialect: mysql6
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: clickhouseMigrations
db/migrations/0001_readings.up.sqldiffers
ClickHouse
MariaDB
1
CREATE TABLE readings (1
CREATE TABLE readings (2
station String,2
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,3
minute UInt16,3
station VARCHAR(20) NOT NULL,4
height_cm UInt16,4
minute INT NOT NULL,5
note Nullable(String)5
height_cm INT NOT NULL,6
) ENGINE = MergeTree() ORDER BY (station, minute);6
note VARCHAR(50)7
);db/migrations/0001_readings.down.sqlidentical
ClickHouse
MariaDB
1
DROP TABLE readings;1
DROP TABLE readings;Queries
RecordReadingsdiffers
ClickHouse · db/queries/readings.sql
MariaDB · db/queries/readings.sql
1
-- sgl.query(RecordReadings, batch)1
-- sgl.query(RecordReadings, batch)2
INSERT INTO readings (station, minute, height_cm, note)2
INSERT INTO readings (station, minute, height_cm, note) VALUES (?, ?, ?, ?);3
VALUES ({station:String}, {minute:UInt16}, {height_cm:UInt16}, {note:Nullable(String)});SummariseReadingsdiffers
ClickHouse · db/queries/readings.sql
MariaDB · db/queries/readings.sql
1
-- sgl.query(SummariseReadings, many)1
-- sgl.query(SummariseReadings, many)2
SELECT station, count() AS readings, count(note) AS noted2
SELECT station, COUNT(*) AS readings, COUNT(note) AS noted3
FROM readings3
FROM readings4
GROUP BY station4
GROUP BY station5
ORDER BY station;5
ORDER BY station;CountPartsonly ClickHouse
ClickHouse · db/queries/readings.sql
MariaDB · not in this version
1
-- sgl.query(CountParts, one)2
SELECT uniqExact(_part) AS parts FROM readings;What sgl generates
RecordReadingsdiffers
ClickHouse
MariaDB
1
func (queries *Queries) RecordReadings(ctx context.Context, params []RecordReadingsParams) error1
func (queries *Queries) RecordReadings(ctx context.Context, params []RecordReadingsParams) error2
2
3
type RecordReadingsParams struct {3
type RecordReadingsParams struct {4
Station string4
Station string5
Minute uint165
Minute int326
HeightCm uint166
HeightCm int327
Note *string7
Note *string8
}8
}SummariseReadingsdiffers
ClickHouse
MariaDB
1
func (queries *Queries) SummariseReadings(ctx context.Context) ([]SummariseReadingsRow, error)1
func (queries *Queries) SummariseReadings(ctx context.Context) ([]SummariseReadingsRow, error)2
2
3
type SummariseReadingsRow struct {3
type SummariseReadingsRow struct {4
Station string `json:"station"`4
Station string `json:"station"`5
Readings uint64 `json:"readings"`5
Readings int64 `json:"readings"`6
Noted uint64 `json:"noted"`6
Noted int64 `json:"noted"`7
}7
}CountPartsonly ClickHouse
ClickHouse
MariaDB · not in this version
1
func (queries *Queries) CountParts(ctx context.Context) (CountPartsRow, error)2
3
type CountPartsRow struct {4
Parts uint64 `json:"parts"`5
}The program
example.godiffers
ClickHouse
MariaDB
1
package main1
package main2
2
3
import (3
import (4
"context"4
"context"5
"database/sql"5
"fmt"6
"fmt"6
"io"7
"io"7
8
8
"seagull.sh/seagull/examples/clickhouse/022_bulk_writes_native/internal/store"9
"seagull.sh/seagull/examples/mysql/022_bulk_writes/internal/store"9
)10
)10
11
11
const (12
const (12
// minutesPerDay is how many readings each tide gauge takes in a day.13
// minutesPerWeek is how many readings each tide gauge takes in a week.13
minutesPerDay = 24 * 6014
minutesPerWeek = 7 * 24 * 6014
)15
)15
16
16
// run records a day of readings from two tide gauges in one batch call, shows how many17
// statementCounter is a store.DBTX that counts the statements sent through ExecContext.17
// parts that wrote, and summarises what landed in the table.18
// QueryContext and QueryRowContext come from the embedded *sql.DB.19
type statementCounter struct {20
*sql.DB21
22
// statements is how many times ExecContext has been called.23
statements int24
}25
26
// ExecContext counts the statement, then runs it on the database.18
//27
//19
// A batch query takes a slice of Params, one per row. On the native driver the generated28
// Takes query (string) which is the SQL to run.20
// method calls PrepareBatch, appends every row with batch.Append and sends them with29
// Takes arguments (...any) which are its bind values.21
// batch.Send: one INSERT, its rows sent column by column in blocks, with no bind22
// variables and so no chunking. The {name:Type} placeholders in the VALUES list only name23
// and type the columns; the driver takes the values from Append. copyfrom generates the24
// same code.25
//30
//26
// Each INSERT writes a new part, which ClickHouse merges in the background, so writing31
// Returns sql.Result which is the database's result.27
// rows one at a time leaves many small parts to merge. CountParts reads the _part virtual32
// Returns error when the statement fails.28
// column to show that the whole day landed as one part.33
func (counter *statementCounter) ExecContext(ctx context.Context, query string, arguments ...any) (sql.Result, error) {34
counter.statements++35
return counter.DB.ExecContext(ctx, query, arguments...)36
}37
38
// run records a week of readings from two tide gauges in one batch call, shows how many39
// INSERT statements that took, and summarises what landed in the table.29
//40
//30
// Takes connection (store.DBTX) which is a native connection to a migrated database.41
// Takes database (*sql.DB) which is a migrated database.31
// Takes output (io.Writer) which receives the part counts and the summary.42
// Takes output (io.Writer) which receives the statement counts and the summary.32
//43
//33
// Returns error when a query fails.44
// Returns error when a query fails.34
func run(ctx context.Context, connection store.DBTX, output io.Writer) error {45
func run(ctx context.Context, database *sql.DB, output io.Writer) error {35
queries := store.New(connection)46
counter := &statementCounter{DB: database}47
queries := store.New(counter)36
48
37
// An empty batch returns at once, without preparing an INSERT, so it writes no part.49
// An empty batch is a no-op: nothing is sent to the database.38
if err := queries.RecordReadings(ctx, nil); err != nil {50
if err := queries.RecordReadings(ctx, nil); err != nil {39
return fmt.Errorf("recording no readings: %w", err)51
return fmt.Errorf("recording no readings: %w", err)40
}52
}41
if err := printParts(ctx, queries, 0, output); err != nil {53
fmt.Fprintf(output, "0 readings sent in %d statements\n", counter.statements)42
return err43
}44
54
45
readings := tideReadings("harbour", "pier")55
readings := tideReadings("harbour", "pier")56
57
// A batch query takes a slice of Params, one per row. On database/sql the generated58
// method writes them as multi-row INSERTs, chunked so that no statement binds more than59
// 65,535 variables, the most a MySQL prepared statement can hold: four columns a row60
// allows 16,383 rows a statement. copyfrom generates the same code on database/sql.61
//62
// Each chunk is its own statement, so run the call inside RunInTx (concept 23) when63
// every row must land or none.46
if err := queries.RecordReadings(ctx, readings); err != nil {64
if err := queries.RecordReadings(ctx, readings); err != nil {47
return fmt.Errorf("recording readings: %w", err)65
return fmt.Errorf("recording readings: %w", err)48
}66
}49
if err := printParts(ctx, queries, len(readings), output); err != nil {67
fmt.Fprintf(output, "%d readings sent in %d statements\n", len(readings), counter.statements)50
return err51
}52
68
53
summary, err := queries.SummariseReadings(ctx)69
summary, err := queries.SummariseReadings(ctx)54
if err != nil {70
if err != nil {55
return fmt.Errorf("summarising readings: %w", err)71
return fmt.Errorf("summarising readings: %w", err)56
}72
}57
for _, row := range summary {73
for _, row := range summary {58
fmt.Fprintf(output, "%-8s %d readings, %d with a note\n", row.Station, row.Readings, row.Noted)74
fmt.Fprintf(output, "%-8s %d readings, %d with a note\n", row.Station, row.Readings, row.Noted)59
}75
}60
return nil76
return nil61
}77
}62
78
63
// printParts prints how many parts hold the table's rows.79
// tideReadings makes one reading a minute for a week at each station. The height rises64
//80
// and falls in a simple saw-tooth, and the reading on the hour carries a note.65
// Takes queries (*store.Queries) which runs the query.66
// Takes sent (int) which is how many readings were sent.67
// Takes output (io.Writer) which receives the line.68
//69
// Returns error when the query fails.70
func printParts(ctx context.Context, queries *store.Queries, sent int, output io.Writer) error {71
parts, err := queries.CountParts(ctx)72
if err != nil {73
return fmt.Errorf("counting parts: %w", err)74
}75
fmt.Fprintf(output, "%d readings sent, parts holding rows: %d\n", sent, parts.Parts)76
return nil77
}78
79
// tideReadings makes one reading a minute for a day at each station. The height rises and80
// falls in a simple saw-tooth, and the reading on the hour carries a note.81
//81
//82
// Takes stations (...string) which name the gauges.82
// Takes stations (...string) which name the gauges.83
//83
//84
// Returns []store.RecordReadingsParams which holds every reading, ready for the batch.84
// Returns []store.RecordReadingsParams which holds every reading, ready for the batch.85
func tideReadings(stations ...string) []store.RecordReadingsParams {85
func tideReadings(stations ...string) []store.RecordReadingsParams {86
readings := make([]store.RecordReadingsParams, 0, len(stations)*minutesPerDay)86
readings := make([]store.RecordReadingsParams, 0, len(stations)*minutesPerWeek)87
for _, station := range stations {87
for _, station := range stations {88
for minute := range uint16(minutesPerDay) {88
for minute := range int32(minutesPerWeek) {89
var note *string89
var note *string90
if minute%60 == 0 {90
if minute%60 == 0 {91
note = new(fmt.Sprintf("hour %d check", minute/60))91
note = new(fmt.Sprintf("hour %d check", minute/60))92
}92
}93
readings = append(readings, store.RecordReadingsParams{93
readings = append(readings, store.RecordReadingsParams{94
Station: station,94
Station: station,95
Minute: minute,95
Minute: minute,96
HeightCm: 200 + minute%360,96
HeightCm: 200 + minute%360,97
Note: note,97
Note: note,98
})98
})99
}99
}100
}100
}101
return readings101
return readings102
}102
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | ClickHouse | MariaDB |
|---|---|---|---|
| RecordReadings | parameter minute | uint16 | int32 |
| RecordReadings | parameter height_cm | uint16 | int32 |
| SummariseReadings | column readings | uint64 | int64 |
| SummariseReadings | column noted | uint64 | int64 |