From SQL types to Go types: MySQL and ClickHouse
Every file of the MySQL and ClickHouse versions side by side, with the lines that differ marked. 10 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
MySQL
ClickHouse
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: landings4
- name: landings5
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_landings.up.sqldiffers
MySQL
ClickHouse
1
CREATE TABLE landings (1
CREATE TABLE landings (2
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,2
id UInt64,3
species VARCHAR(50) NOT NULL,3
catch_id UUID,4
boxes SMALLINT NOT NULL,4
species String,5
fish INT NOT NULL,5
boxes UInt16,6
weight_g BIGINT NOT NULL,6
fish UInt32,7
length_cm DOUBLE NOT NULL,7
weight_g Int64,8
on_ice BOOLEAN NOT NULL,8
length_cm Float32,9
landed_at DATETIME NOT NULL,9
price Decimal(9, 2),10
tag VARBINARY(16) NOT NULL,10
on_ice Bool,11
notes TEXT,11
landed_at DateTime('UTC'),12
berth TINYINT UNSIGNED NOT NULL,12
tag FixedString(4),13
price_per_kg DECIMAL(6, 2) NOT NULL13
notes Nullable(String)14
);14
) ENGINE = MergeTree() ORDER BY id;db/migrations/0001_landings.down.sqlidentical
MySQL
ClickHouse
1
DROP TABLE landings;1
DROP TABLE landings;Queries
RecordLandingdiffers
MySQL · db/queries/landings.sql
ClickHouse · db/queries/landings.sql
1
-- sgl.query(RecordLanding, execresult)1
-- sgl.query(RecordLanding, exec)2
-- ?11 as sgl.param(price_per_kg, type: text)2
INSERT INTO landings (id, catch_id, species, boxes, fish, weight_g, length_cm, price, on_ice, landed_at, tag, notes)3
INSERT INTO landings (species, boxes, fish, weight_g, length_cm, on_ice, landed_at, tag, notes, berth, price_per_kg)3
VALUES (4
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);4
{id:UInt64},5
toUUID({catch_id:String}),6
{species:String},7
{boxes:UInt16},8
{fish:UInt32},9
{weight_g:Int64},10
{length_cm:Float32},11
toDecimal32({price:String}, 2),12
{on_ice:Bool},13
{landed_at:DateTime('UTC')},14
{tag:FixedString(4)},15
{notes:Nullable(String)}16
);GetLandingdiffers
MySQL · db/queries/landings.sql
ClickHouse · db/queries/landings.sql
1
-- sgl.query(GetLanding, one)1
-- sgl.query(GetLanding, one)2
SELECT id, species, boxes, fish, weight_g, length_cm, on_ice, landed_at, tag, notes, berth, price_per_kg2
SELECT id, catch_id, species, boxes, fish, weight_g, length_cm, price, on_ice, landed_at, tag, notes3
FROM landings3
FROM landings4
WHERE id = ?;4
WHERE id = {id:UInt64};What sgl generates
RecordLandingdiffers
MySQL
ClickHouse
1
func (queries *Queries) RecordLanding(ctx context.Context, params RecordLandingParams) (sql.Result, error)1
func (queries *Queries) RecordLanding(ctx context.Context, params RecordLandingParams) error2
2
3
type RecordLandingParams struct {3
type RecordLandingParams struct {4
Species string4
ID uint645
Boxes int165
CatchID string6
Fish int326
Species string7
WeightG int647
Boxes uint168
LengthCm float648
Fish uint329
OnIce bool9
WeightG int6410
LandedAt time.Time10
LengthCm float3211
Tag []byte11
Price string12
Notes *string12
OnIce bool13
Berth uint813
LandedAt time.Time14
PricePerKg string14
Tag string15
Notes *string15
}16
}GetLandingdiffers
MySQL
ClickHouse
1
func (queries *Queries) GetLanding(ctx context.Context, id int64) (GetLandingRow, error)1
func (queries *Queries) GetLanding(ctx context.Context, id uint64) (GetLandingRow, error)2
2
3
type GetLandingRow struct {3
type GetLandingRow struct {4
ID int64 `json:"id"`4
ID uint64 `json:"id"`5
Species string `json:"species"`5
CatchID uuid.UUID `json:"catch_id"`6
Boxes int16 `json:"boxes"`6
Species string `json:"species"`7
Fish int32 `json:"fish"`7
Boxes uint16 `json:"boxes"`8
WeightG int64 `json:"weight_g"`8
Fish uint32 `json:"fish"`9
LengthCm float64 `json:"length_cm"`9
WeightG int64 `json:"weight_g"`10
OnIce bool `json:"on_ice"`10
LengthCm float32 `json:"length_cm"`11
LandedAt time.Time `json:"landed_at"`11
Price decimal.Decimal `json:"price"`12
Tag []byte `json:"tag"`12
OnIce bool `json:"on_ice"`13
Notes *string `json:"notes"`13
LandedAt time.Time `json:"landed_at"`14
Berth uint8 `json:"berth"`14
Tag string `json:"tag"`15
PricePerKg apd.Decimal `json:"price_per_kg"`15
Notes *string `json:"notes"`16
}16
}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
"time"8
"time"9
9
10
"seagull.sh/seagull/examples/mysql/005_type_mapping/internal/store"10
"seagull.sh/seagull/examples/clickhouse/005_type_mapping/internal/store"11
)11
)12
12
13
// run records a landing of fish and reads it back, printing the Go type sgl chose for13
// run records a landing of fish and reads it back, printing the Go type sgl chose for14
// each column. Seagull maps the type each column declares in db/migrations: SMALLINT, INT14
// each column. ClickHouse types are exact about width and sign, and Seagull follows them:15
// and BIGINT give int16, int32 and int64, and an UNSIGNED integer gives the unsigned Go15
// UInt16, UInt32 and UInt64 give uint16, uint32 and uint64, Int64 gives int64, Float3216
// type of the same width, so TINYINT UNSIGNED is uint8. DOUBLE gives float64, BOOLEAN16
// gives float32, and Bool gives bool. DateTime('UTC') gives time.Time; the generated code17
// (MySQL's TINYINT(1)) gives bool, DATETIME gives time.Time when the DSN sets17
// sends a time as Unix seconds, so the server keeps its moment whatever the zone, and the18
// parseTime=true, and VARBINARY gives []byte.18
// column's time zone makes it read back in UTC. FixedString(4) and String both give19
// string, which in ClickHouse holds any bytes, and Nullable(String) gives *string.19
//20
//20
// DECIMAL gives apd.Decimal from github.com/cockroachdb/apd/v3, which holds the exact21
// UUID gives uuid.UUID from github.com/google/uuid and Decimal(9, 2) gives21
// value. The insert binds the price as text instead, through sgl.param(price_per_kg,22
// decimal.Decimal from github.com/shopspring/decimal, the types clickhouse-go scans them22
// type: text), and MySQL converts "4.20" to DECIMAL(6, 2) without rounding.23
// into. A {price:Decimal(9, 2)} placeholder would make the parameter a decimal.Decimal24
// too; this example passes the price and catch id as text and converts them in SQL with25
// toDecimal32 and toUUID, so it needs nothing beyond the standard library.23
//26
//24
// Takes database (*sql.DB) which is a migrated database.27
// Takes database (*sql.DB) which is a migrated database.25
// Takes output (io.Writer) which receives one line per column.28
// Takes output (io.Writer) which receives one line per column.26
//29
//27
// Returns error when a query fails.30
// Returns error when a query fails.28
func run(ctx context.Context, database *sql.DB, output io.Writer) error {31
func run(ctx context.Context, database *sql.DB, output io.Writer) error {29
queries := store.New(database)32
queries := store.New(database)30
33
31
result, err := queries.RecordLanding(ctx, store.RecordLandingParams{34
if err := queries.RecordLanding(ctx, store.RecordLandingParams{32
Species: "mackerel",35
ID: 1,33
Boxes: 12,36
CatchID: "5f0c6c7e-8d0e-4b77-9a51-2c4a6f9e3b10",34
Fish: 480,37
Species: "mackerel",35
WeightG: 163_200,38
Boxes: 12,36
LengthCm: 31.5,39
Fish: 480,37
OnIce: true,40
WeightG: 163_200,38
LandedAt: time.Date(2026, time.May, 1, 5, 30, 0, 0, time.UTC),41
LengthCm: 31.5,39
Tag: []byte{0x04, 0xa2, 0x19, 0xf3},42
Price: "412.50",40
Notes: new("landed at Newlyn"),43
OnIce: true,41
Berth: 7,44
LandedAt: time.Date(2026, time.May, 1, 5, 30, 0, 0, time.UTC),42
PricePerKg: "4.20",45
Tag: string([]byte{0x04, 0xa2, 0x19, 0xf3}),43
})46
Notes: new("landed at Newlyn"),44
if err != nil {47
}); err != nil {45
return fmt.Errorf("recording a landing: %w", err)48
return fmt.Errorf("recording a landing: %w", err)46
}49
}47
id, err := result.LastInsertId()48
if err != nil {49
return fmt.Errorf("reading the landing's id: %w", err)50
}51
50
52
landing, err := queries.GetLanding(ctx, id)51
landing, err := queries.GetLanding(ctx, 1)53
if err != nil {52
if err != nil {54
return fmt.Errorf("getting landing %d: %w", id, err)53
return fmt.Errorf("getting landing 1: %w", err)55
}54
}56
fmt.Fprintf(output, "id %-12T %d\n", landing.ID, landing.ID)55
fmt.Fprintf(output, "id %-16T %d\n", landing.ID, landing.ID)57
fmt.Fprintf(output, "species %-12T %s\n", landing.Species, landing.Species)56
fmt.Fprintf(output, "catch_id %-16T %s\n", landing.CatchID, landing.CatchID)58
fmt.Fprintf(output, "boxes %-12T %d\n", landing.Boxes, landing.Boxes)57
fmt.Fprintf(output, "species %-16T %s\n", landing.Species, landing.Species)59
fmt.Fprintf(output, "fish %-12T %d\n", landing.Fish, landing.Fish)58
fmt.Fprintf(output, "boxes %-16T %d\n", landing.Boxes, landing.Boxes)60
fmt.Fprintf(output, "weight_g %-12T %d\n", landing.WeightG, landing.WeightG)59
fmt.Fprintf(output, "fish %-16T %d\n", landing.Fish, landing.Fish)61
fmt.Fprintf(output, "length_cm %-12T %.1f\n", landing.LengthCm, landing.LengthCm)60
fmt.Fprintf(output, "weight_g %-16T %d\n", landing.WeightG, landing.WeightG)62
fmt.Fprintf(output, "on_ice %-12T %t\n", landing.OnIce, landing.OnIce)61
fmt.Fprintf(output, "length_cm %-16T %.1f\n", landing.LengthCm, landing.LengthCm)63
fmt.Fprintf(output, "landed_at %-12T %s\n", landing.LandedAt, landing.LandedAt.Format(time.RFC3339))62
fmt.Fprintf(output, "price %-16T %s\n", landing.Price, landing.Price.StringFixed(2))64
// %T prints []byte as []uint8, since byte is an alias for uint8.63
fmt.Fprintf(output, "on_ice %-16T %t\n", landing.OnIce, landing.OnIce)65
fmt.Fprintf(output, "tag %-12T % x\n", landing.Tag, landing.Tag)64
fmt.Fprintf(output, "landed_at %-16T %s\n", landing.LandedAt, landing.LandedAt.Format(time.RFC3339))66
fmt.Fprintf(output, "notes %-12T %s\n", landing.Notes, *landing.Notes)65
fmt.Fprintf(output, "tag %-16T % x\n", landing.Tag, landing.Tag)67
fmt.Fprintf(output, "berth %-12T %d\n", landing.Berth, landing.Berth)66
fmt.Fprintf(output, "notes %-16T %s\n", landing.Notes, *landing.Notes)68
fmt.Fprintf(output, "price_per_kg %-12T %s\n", landing.PricePerKg, landing.PricePerKg.String())69
return nil67
return nil70
}68
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | MySQL | ClickHouse |
|---|---|---|---|
| RecordLanding | parameter boxes | int16 | uint16 |
| RecordLanding | parameter fish | int32 | uint32 |
| RecordLanding | parameter length_cm | float64 | float32 |
| RecordLanding | parameter tag | []byte | string |
| GetLanding | parameter id | int64 | uint64 |
| GetLanding | column id | int64 | uint64 |
| GetLanding | column boxes | int16 | uint16 |
| GetLanding | column fish | int32 | uint32 |
| GetLanding | column length_cm | float64 | float32 |
| GetLanding | column tag | []byte | string |