From SQL types to Go types: ClickHouse and DuckDB
Every file of the ClickHouse and DuckDB versions side by side, with the lines that differ marked. 11 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
ClickHouse
DuckDB
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: landings4
- name: harbour5
dialect: clickhouse5
dialect: duckdb6
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
ClickHouse
DuckDB
1
CREATE SEQUENCE landing_ids;2
1
CREATE TABLE landings (3
CREATE TABLE landings (2
id UInt64,4
id BIGINT PRIMARY KEY DEFAULT nextval('landing_ids'),3
catch_id UUID,5
species VARCHAR NOT NULL,4
species String,6
quay UTINYINT NOT NULL,5
boxes UInt16,7
boxes SMALLINT NOT NULL,6
fish UInt32,8
fish INTEGER NOT NULL,7
weight_g Int64,9
weight_g BIGINT NOT NULL,8
length_cm Float32,10
length_cm DOUBLE NOT NULL,9
price Decimal(9, 2),11
water_temp REAL NOT NULL,10
on_ice Bool,12
on_ice BOOLEAN NOT NULL,11
landed_at DateTime('UTC'),13
landed_at TIMESTAMP NOT NULL,12
tag FixedString(4),14
landed_on DATE NOT NULL,13
notes Nullable(String)15
catch_id UUID NOT NULL,14
) ENGINE = MergeTree() ORDER BY id;16
auction_lot HUGEINT NOT NULL,17
tag BLOB NOT NULL,18
notes VARCHAR19
);db/migrations/0001_landings.down.sqldiffers
ClickHouse
DuckDB
1
DROP TABLE landings;1
DROP TABLE landings;2
DROP SEQUENCE landing_ids;Queries
RecordLandingdiffers
ClickHouse · db/queries/landings.sql
DuckDB · db/queries/landings.sql
1
-- sgl.query(RecordLanding, exec)1
-- sgl.query(RecordLanding, one)2
INSERT INTO landings (id, catch_id, species, boxes, fish, weight_g, length_cm, price, on_ice, landed_at, tag, notes)2
INSERT INTO landings (species, quay, boxes, fish, weight_g, length_cm, water_temp, on_ice, landed_at, landed_on, catch_id, auction_lot, tag, notes)3
VALUES (3
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)4
{id:UInt64},4
RETURNING id;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
ClickHouse · db/queries/landings.sql
DuckDB · db/queries/landings.sql
1
-- sgl.query(GetLanding, one)1
-- sgl.query(GetLanding, one)2
SELECT id, catch_id, species, boxes, fish, weight_g, length_cm, price, on_ice, landed_at, tag, notes2
SELECT id, species, quay, boxes, fish, weight_g, length_cm, water_temp, on_ice, landed_at, landed_on, catch_id, auction_lot, tag, notes3
FROM landings3
FROM landings4
WHERE id = {id:UInt64};4
WHERE id = $1;What sgl generates
RecordLandingdiffers
ClickHouse
DuckDB
1
func (queries *Queries) RecordLanding(ctx context.Context, params RecordLandingParams) error1
func (queries *Queries) RecordLanding(ctx context.Context, params RecordLandingParams) (RecordLandingRow, error)2
2
3
type RecordLandingParams struct {3
type RecordLandingParams struct {4
ID uint644
Species string5
CatchID string5
Quay uint86
Species string6
Boxes int167
Boxes uint167
Fish int328
Fish uint328
WeightG int649
WeightG int649
LengthCm float6410
LengthCm float3210
WaterTemp float3211
Price string11
OnIce bool12
OnIce bool12
LandedAt time.Time13
LandedAt time.Time13
LandedOn time.Time14
Tag string14
CatchID uuid.UUID15
Notes *string15
AuctionLot *big.Int16
Tag []byte17
Notes *string16
}18
}19
20
type RecordLandingRow struct {21
ID int64 `json:"id"`22
}GetLandingdiffers
ClickHouse
DuckDB
1
func (queries *Queries) GetLanding(ctx context.Context, id uint64) (GetLandingRow, error)1
func (queries *Queries) GetLanding(ctx context.Context, id int64) (GetLandingRow, error)2
2
3
type GetLandingRow struct {3
type GetLandingRow struct {4
ID uint64 `json:"id"`4
ID int64 `json:"id"`5
CatchID uuid.UUID `json:"catch_id"`5
Species string `json:"species"`6
Species string `json:"species"`6
Quay uint8 `json:"quay"`7
Boxes uint16 `json:"boxes"`7
Boxes int16 `json:"boxes"`8
Fish uint32 `json:"fish"`8
Fish int32 `json:"fish"`9
WeightG int64 `json:"weight_g"`9
WeightG int64 `json:"weight_g"`10
LengthCm float32 `json:"length_cm"`10
LengthCm float64 `json:"length_cm"`11
Price decimal.Decimal `json:"price"`11
WaterTemp float32 `json:"water_temp"`12
OnIce bool `json:"on_ice"`12
OnIce bool `json:"on_ice"`13
LandedAt time.Time `json:"landed_at"`13
LandedAt time.Time `json:"landed_at"`14
Tag string `json:"tag"`14
LandedOn time.Time `json:"landed_on"`15
Notes *string `json:"notes"`15
CatchID uuid.UUID `json:"catch_id"`16
AuctionLot *big.Int `json:"auction_lot"`17
Tag []byte `json:"tag"`18
Notes *string `json:"notes"`16
}19
}The program
example.godiffers
ClickHouse
DuckDB
1
package main1
package main2
2
3
import (3
import (4
"context"4
"context"5
"database/sql"5
"database/sql"6
"errors"6
"fmt"7
"fmt"7
"io"8
"io"9
"math/big"8
"time"10
"time"9
11
10
"seagull.sh/seagull/examples/clickhouse/005_type_mapping/internal/store"12
"github.com/google/uuid"13
14
"seagull.sh/seagull/examples/duckdb/005_type_mapping/internal/store"11
)15
)12
16
13
// run records a landing of fish and reads it back, printing the Go type sgl chose for17
// run records a landing of fish and reads it back, printing the Go type sgl chose for14
// each column. ClickHouse types are exact about width and sign, and Seagull follows them:18
// each column. DuckDB declares every column's type exactly, and Seagull maps each one:15
// UInt16, UInt32 and UInt64 give uint16, uint32 and uint64, Int64 gives int64, Float3216
// gives float32, and Bool gives bool. DateTime('UTC') gives time.Time; the generated code17
// sends a time as Unix seconds, so the server keeps its moment whatever the zone, and the18
// 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.20
//19
//21
// UUID gives uuid.UUID from github.com/google/uuid and Decimal(9, 2) gives20
// - SMALLINT, INTEGER and BIGINT give int16, int32 and int64, and the unsigned UTINYINT22
// decimal.Decimal from github.com/shopspring/decimal, the types clickhouse-go scans them21
// gives uint8;23
// into. A {price:Decimal(9, 2)} placeholder would make the parameter a decimal.Decimal22
// - DOUBLE gives float64, but REAL is DuckDB's 4-byte float, so it gives float32;24
// too; this example passes the price and catch id as text and converts them in SQL with23
// - TIMESTAMP and DATE give time.Time, and UUID gives uuid.UUID from25
// toDecimal32 and toUUID, so it needs nothing beyond the standard library.24
// github.com/google/uuid;25
// - HUGEINT is a 128-bit integer with no Go equivalent, so it gives *big.Int, the type26
// duckdb-go returns, even when the column is NOT NULL;27
// - BLOB gives []byte, and a nullable VARCHAR gives *string.26
//28
//27
// Takes database (*sql.DB) which is a migrated database.29
// Takes database (*sql.DB) which is a migrated database.28
// Takes output (io.Writer) which receives one line per column.30
// Takes output (io.Writer) which receives one line per column.29
//31
//30
// Returns error when a query fails.32
// Returns error when a query fails.31
func run(ctx context.Context, database *sql.DB, output io.Writer) error {33
func run(ctx context.Context, database *sql.DB, output io.Writer) error {32
queries := store.New(database)34
queries := store.New(database)33
35
34
if err := queries.RecordLanding(ctx, store.RecordLandingParams{36
// The auction lot number has 20 digits, too many for int64 or uint64.35
ID: 1,37
lot, ok := new(big.Int).SetString("20260501000000000001", 10)36
CatchID: "5f0c6c7e-8d0e-4b77-9a51-2c4a6f9e3b10",38
if !ok {37
Species: "mackerel",39
return errors.New("parsing the auction lot number")38
Boxes: 12,40
}39
Fish: 480,41
recorded, err := queries.RecordLanding(ctx, store.RecordLandingParams{40
WeightG: 163_200,42
Species: "mackerel",41
LengthCm: 31.5,43
Quay: 3,42
Price: "412.50",44
Boxes: 12,43
OnIce: true,45
Fish: 480,44
LandedAt: time.Date(2026, time.May, 1, 5, 30, 0, 0, time.UTC),46
WeightG: 163_200,45
Tag: string([]byte{0x04, 0xa2, 0x19, 0xf3}),47
LengthCm: 31.5,46
Notes: new("landed at Newlyn"),48
WaterTemp: 11.25,47
}); err != nil {49
OnIce: true,50
LandedAt: time.Date(2026, time.May, 1, 5, 30, 0, 0, time.UTC),51
LandedOn: time.Date(2026, time.May, 1, 0, 0, 0, 0, time.UTC),52
CatchID: uuid.MustParse("6f1c2a5e-4d3b-4c2a-9e8f-1a2b3c4d5e6f"),53
AuctionLot: lot,54
Tag: []byte{0x04, 0xa2, 0x19, 0xf3},55
Notes: new("landed at Newlyn"),56
})57
if err != nil {48
return fmt.Errorf("recording a landing: %w", err)58
return fmt.Errorf("recording a landing: %w", err)49
}59
}50
60
51
landing, err := queries.GetLanding(ctx, 1)61
landing, err := queries.GetLanding(ctx, recorded.ID)52
if err != nil {62
if err != nil {53
return fmt.Errorf("getting landing 1: %w", err)63
return fmt.Errorf("getting landing %d: %w", recorded.ID, err)54
}64
}55
fmt.Fprintf(output, "id %-16T %d\n", landing.ID, landing.ID)65
fmt.Fprintf(output, "id %-10T %d\n", landing.ID, landing.ID)56
fmt.Fprintf(output, "catch_id %-16T %s\n", landing.CatchID, landing.CatchID)66
fmt.Fprintf(output, "species %-10T %s\n", landing.Species, landing.Species)57
fmt.Fprintf(output, "species %-16T %s\n", landing.Species, landing.Species)67
fmt.Fprintf(output, "quay %-10T %d\n", landing.Quay, landing.Quay)58
fmt.Fprintf(output, "boxes %-16T %d\n", landing.Boxes, landing.Boxes)68
fmt.Fprintf(output, "boxes %-10T %d\n", landing.Boxes, landing.Boxes)59
fmt.Fprintf(output, "fish %-16T %d\n", landing.Fish, landing.Fish)69
fmt.Fprintf(output, "fish %-10T %d\n", landing.Fish, landing.Fish)60
fmt.Fprintf(output, "weight_g %-16T %d\n", landing.WeightG, landing.WeightG)70
fmt.Fprintf(output, "weight_g %-10T %d\n", landing.WeightG, landing.WeightG)61
fmt.Fprintf(output, "length_cm %-16T %.1f\n", landing.LengthCm, landing.LengthCm)71
fmt.Fprintf(output, "length_cm %-10T %.1f\n", landing.LengthCm, landing.LengthCm)62
fmt.Fprintf(output, "price %-16T %s\n", landing.Price, landing.Price.StringFixed(2))72
fmt.Fprintf(output, "water_temp %-10T %.2f\n", landing.WaterTemp, landing.WaterTemp)63
fmt.Fprintf(output, "on_ice %-16T %t\n", landing.OnIce, landing.OnIce)73
fmt.Fprintf(output, "on_ice %-10T %t\n", landing.OnIce, landing.OnIce)64
fmt.Fprintf(output, "landed_at %-16T %s\n", landing.LandedAt, landing.LandedAt.Format(time.RFC3339))74
fmt.Fprintf(output, "landed_at %-10T %s\n", landing.LandedAt, landing.LandedAt.Format(time.RFC3339))65
fmt.Fprintf(output, "tag %-16T % x\n", landing.Tag, landing.Tag)75
fmt.Fprintf(output, "landed_on %-10T %s\n", landing.LandedOn, landing.LandedOn.Format(time.DateOnly))66
fmt.Fprintf(output, "notes %-16T %s\n", landing.Notes, *landing.Notes)76
fmt.Fprintf(output, "catch_id %-10T %s\n", landing.CatchID, landing.CatchID)77
fmt.Fprintf(output, "auction_lot %-10T %s\n", landing.AuctionLot, landing.AuctionLot)78
// %T prints []byte as []uint8, since byte is an alias for uint8.79
fmt.Fprintf(output, "tag %-10T % x\n", landing.Tag, landing.Tag)80
fmt.Fprintf(output, "notes %-10T %s\n", landing.Notes, *landing.Notes)67
return nil81
return nil68
}82
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | ClickHouse | DuckDB |
|---|---|---|---|
| RecordLanding | parameter catch_id | string | uuid.UUID |
| RecordLanding | parameter boxes | uint16 | int16 |
| RecordLanding | parameter fish | uint32 | int32 |
| RecordLanding | parameter length_cm | float32 | float64 |
| RecordLanding | parameter tag | string | []byte |
| GetLanding | parameter id | uint64 | int64 |
| GetLanding | column id | uint64 | int64 |
| GetLanding | column boxes | uint16 | int16 |
| GetLanding | column fish | uint32 | int32 |
| GetLanding | column length_cm | float32 | float64 |
| GetLanding | column tag | string | []byte |