Batch and copyfrom: SQLite and MySQL
Every file of the SQLite and MySQL versions side by side, with the lines that differ marked. 2 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
SQLite
MySQL
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: tides4
- name: tides5
dialect: sqlite5
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/storeMigrations
db/migrations/0001_readings.up.sqldiffers
SQLite
MySQL
1
CREATE TABLE readings (1
CREATE TABLE readings (2
id INTEGER PRIMARY KEY,2
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,3
station TEXT NOT NULL,3
station VARCHAR(20) NOT NULL,4
minute INTEGER NOT NULL,4
minute INT NOT NULL,5
height_cm INTEGER NOT NULL,5
height_cm INT NOT NULL,6
note TEXT6
note VARCHAR(50)7
);7
);db/migrations/0001_readings.down.sqlidentical
SQLite
MySQL
1
DROP TABLE readings;1
DROP TABLE readings;Queries
RecordReadingsidentical
SQLite · db/queries/readings.sql
MySQL · db/queries/readings.sql
1
-- sgl.query(RecordReadings, batch)1
-- sgl.query(RecordReadings, batch)2
INSERT INTO readings (station, minute, height_cm, note) VALUES (?, ?, ?, ?);2
INSERT INTO readings (station, minute, height_cm, note) VALUES (?, ?, ?, ?);SummariseReadingsidentical
SQLite · db/queries/readings.sql
MySQL · 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;What sgl generates
RecordReadingsdiffers
SQLite
MySQL
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 int645
Minute int326
HeightCm int646
HeightCm int327
Note *string7
Note *string8
}8
}SummariseReadingsidentical
SQLite
MySQL
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 int64 `json:"readings"`5
Readings int64 `json:"readings"`6
Noted int64 `json:"noted"`6
Noted int64 `json:"noted"`7
}7
}The program
example.godiffers
SQLite
MySQL
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/sqlite/022_bulk_writes/internal/store"9
"seagull.sh/seagull/examples/mysql/022_bulk_writes/internal/store"10
)10
)11
11
12
const (12
const (13
// 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.14
minutesPerDay = 24 * 6014
minutesPerWeek = 7 * 24 * 6015
)15
)16
16
17
// statementCounter is a store.DBTX that counts the statements sent through ExecContext.17
// statementCounter is a store.DBTX that counts the statements sent through ExecContext.18
// QueryContext and QueryRowContext come from the embedded *sql.DB.18
// QueryContext and QueryRowContext come from the embedded *sql.DB.19
type statementCounter struct {19
type statementCounter struct {20
*sql.DB20
*sql.DB21
21
22
// statements is how many times ExecContext has been called.22
// statements is how many times ExecContext has been called.23
statements int23
statements int24
}24
}25
25
26
// ExecContext counts the statement, then runs it on the database.26
// ExecContext counts the statement, then runs it on the database.27
//27
//28
// Takes query (string) which is the SQL to run.28
// Takes query (string) which is the SQL to run.29
// Takes arguments (...any) which are its bind values.29
// Takes arguments (...any) which are its bind values.30
//30
//31
// Returns sql.Result which is the database's result.31
// Returns sql.Result which is the database's result.32
// Returns error when the statement fails.32
// Returns error when the statement fails.33
func (counter *statementCounter) ExecContext(ctx context.Context, query string, arguments ...any) (sql.Result, error) {33
func (counter *statementCounter) ExecContext(ctx context.Context, query string, arguments ...any) (sql.Result, error) {34
counter.statements++34
counter.statements++35
return counter.DB.ExecContext(ctx, query, arguments...)35
return counter.DB.ExecContext(ctx, query, arguments...)36
}36
}37
37
38
// run records a day of readings from two tide gauges in one batch call, shows how many38
// 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.39
// INSERT statements that took, and summarises what landed in the table.40
//40
//41
// Takes database (*sql.DB) which is a migrated database.41
// Takes database (*sql.DB) which is a migrated database.42
// Takes output (io.Writer) which receives the statement counts and the summary.42
// Takes output (io.Writer) which receives the statement counts and the summary.43
//43
//44
// Returns error when a query fails.44
// Returns error when a query fails.45
func run(ctx context.Context, database *sql.DB, output io.Writer) error {45
func run(ctx context.Context, database *sql.DB, output io.Writer) error {46
counter := &statementCounter{DB: database}46
counter := &statementCounter{DB: database}47
queries := store.New(counter)47
queries := store.New(counter)48
48
49
// An empty batch is a no-op: nothing is sent to the database.49
// An empty batch is a no-op: nothing is sent to the database.50
if err := queries.RecordReadings(ctx, nil); err != nil {50
if err := queries.RecordReadings(ctx, nil); err != nil {51
return fmt.Errorf("recording no readings: %w", err)51
return fmt.Errorf("recording no readings: %w", err)52
}52
}53
fmt.Fprintf(output, "0 readings sent in %d statements\n", counter.statements)53
fmt.Fprintf(output, "0 readings sent in %d statements\n", counter.statements)54
54
55
readings := tideReadings("harbour", "pier")55
readings := tideReadings("harbour", "pier")56
56
57
// A batch query takes a slice of Params, one per row. On database/sql the generated57
// 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 than58
// method writes them as multi-row INSERTs, chunked so that no statement binds more than59
// 999 variables, the default limit of SQLite before 3.32: four columns a row allows 24959
// 65,535 variables, the most a MySQL prepared statement can hold: four columns a row60
// rows a statement. copyfrom generates the same code on database/sql.60
// allows 16,383 rows a statement. copyfrom generates the same code on database/sql.61
//61
//62
// Each chunk is its own statement, so run the call inside RunInTx (concept 23) when62
// Each chunk is its own statement, so run the call inside RunInTx (concept 23) when63
// every row must land or none.63
// every row must land or none.64
if err := queries.RecordReadings(ctx, readings); err != nil {64
if err := queries.RecordReadings(ctx, readings); err != nil {65
return fmt.Errorf("recording readings: %w", err)65
return fmt.Errorf("recording readings: %w", err)66
}66
}67
fmt.Fprintf(output, "%d readings sent in %d statements\n", len(readings), counter.statements)67
fmt.Fprintf(output, "%d readings sent in %d statements\n", len(readings), counter.statements)68
68
69
summary, err := queries.SummariseReadings(ctx)69
summary, err := queries.SummariseReadings(ctx)70
if err != nil {70
if err != nil {71
return fmt.Errorf("summarising readings: %w", err)71
return fmt.Errorf("summarising readings: %w", err)72
}72
}73
for _, row := range summary {73
for _, row := range summary {74
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)75
}75
}76
return nil76
return nil77
}77
}78
78
79
// tideReadings makes one reading a minute for a day at each station. The height rises and79
// tideReadings makes one reading a minute for a week at each station. The height rises80
// falls in a simple saw-tooth, and the reading on the hour carries a note.80
// and 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 int64(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 | SQLite | MySQL |
|---|---|---|---|
| RecordReadings | parameter minute | int64 | int32 |
| RecordReadings | parameter height_cm | int64 | int32 |