From SQL types to Go types: MySQL and TimescaleDB
Every file of the MySQL and TimescaleDB versions side by side, with the lines that differ marked. 2 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
MySQL
TimescaleDB
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: landings4
- name: landings5
dialect: mysql5
dialect: postgres6
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: database/sqlMigrations
db/migrations/0001_landings.up.sqldiffers
MySQL
TimescaleDB
1
CREATE TABLE landings (1
CREATE TABLE landings (2
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,2
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,3
species VARCHAR(50) NOT NULL,3
catch_id UUID NOT NULL,4
boxes SMALLINT NOT NULL,4
species TEXT NOT NULL,5
fish INT NOT NULL,5
boxes SMALLINT NOT NULL,6
weight_g BIGINT NOT NULL,6
fish INTEGER NOT NULL,7
length_cm DOUBLE NOT NULL,7
weight_g BIGINT NOT NULL,8
on_ice BOOLEAN NOT NULL,8
length_cm REAL NOT NULL,9
landed_at DATETIME NOT NULL,9
sea_temp DOUBLE PRECISION NOT NULL,10
tag VARBINARY(16) NOT NULL,10
price NUMERIC(8, 2) NOT NULL,11
notes TEXT,11
on_ice BOOLEAN NOT NULL,12
berth TINYINT UNSIGNED NOT NULL,12
landed_on DATE NOT NULL,13
price_per_kg DECIMAL(6, 2) NOT NULL13
landed_at TIMESTAMPTZ NOT NULL,14
harbour_ip INET NOT NULL,15
tag BYTEA NOT NULL,16
notes TEXT14
);17
);db/migrations/0001_landings.down.sqlidentical
MySQL
TimescaleDB
1
DROP TABLE landings;1
DROP TABLE landings;Queries
RecordLandingdiffers
MySQL · db/queries/landings.sql
TimescaleDB · db/queries/landings.sql
1
-- sgl.query(RecordLanding, execresult)1
-- sgl.query(RecordLanding, one)2
-- ?11 as sgl.param(price_per_kg, type: text)2
INSERT INTO landings (catch_id, species, boxes, fish, weight_g, length_cm, sea_temp, price, on_ice, landed_on, landed_at, harbour_ip, 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 ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)4
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);4
RETURNING id;GetLandingdiffers
MySQL · db/queries/landings.sql
TimescaleDB · 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, sea_temp, price, on_ice, landed_on, landed_at, harbour_ip, tag, notes3
FROM landings3
FROM landings4
WHERE id = ?;4
WHERE id = $1;What sgl generates
RecordLandingdiffers
MySQL
TimescaleDB
1
func (queries *Queries) RecordLanding(ctx context.Context, params RecordLandingParams) (sql.Result, error)1
func (queries *Queries) RecordLanding(ctx context.Context, params RecordLandingParams) (RecordLandingRow, error)2
2
3
type RecordLandingParams struct {3
type RecordLandingParams struct {4
Species string4
CatchID uuid.UUID5
Boxes int165
Species string6
Fish int326
Boxes int167
WeightG int647
Fish int328
LengthCm float648
WeightG int649
OnIce bool9
LengthCm float3210
LandedAt time.Time10
SeaTemp float6411
Tag []byte11
Price apd.Decimal12
Notes *string12
OnIce bool13
Berth uint813
LandedOn time.Time14
PricePerKg string14
LandedAt time.Time15
HarbourIP any16
Tag []byte17
Notes *string15
}18
}19
20
type RecordLandingRow struct {21
ID int64 `json:"id"`22
}GetLandingdiffers
MySQL
TimescaleDB
1
func (queries *Queries) GetLanding(ctx context.Context, id int64) (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 int64 `json:"id"`4
ID int64 `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 int16 `json:"boxes"`8
WeightG int64 `json:"weight_g"`8
Fish int32 `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
SeaTemp float64 `json:"sea_temp"`12
Tag []byte `json:"tag"`12
Price apd.Decimal `json:"price"`13
Notes *string `json:"notes"`13
OnIce bool `json:"on_ice"`14
Berth uint8 `json:"berth"`14
LandedOn time.Time `json:"landed_on"`15
PricePerKg apd.Decimal `json:"price_per_kg"`15
LandedAt time.Time `json:"landed_at"`16
HarbourIP any `json:"harbour_ip"`17
Tag []byte `json:"tag"`18
Notes *string `json:"notes"`16
}19
}The program
example.godiffers
MySQL
TimescaleDB
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
"github.com/cockroachdb/apd/v3"11
"github.com/google/uuid"12
13
"seagull.sh/seagull/examples/postgres/005_type_mapping/internal/store"11
)14
)12
15
13
// run records a landing of fish and reads it back, printing the Go type sgl chose for16
// 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, INT17
// each column of db/migrations. PostgreSQL's types are exact, so each maps by name:15
// and BIGINT give int16, int32 and int64, and an UNSIGNED integer gives the unsigned Go16
// type of the same width, so TINYINT UNSIGNED is uint8. DOUBLE gives float64, BOOLEAN17
// (MySQL's TINYINT(1)) gives bool, DATETIME gives time.Time when the DSN sets18
// parseTime=true, and VARBINARY gives []byte.19
//18
//20
// DECIMAL gives apd.Decimal from github.com/cockroachdb/apd/v3, which holds the exact19
// - SMALLINT, INTEGER and BIGINT give int16, int32 and int64;21
// value. The insert binds the price as text instead, through sgl.param(price_per_kg,20
// - REAL is single precision and gives float32, while DOUBLE PRECISION gives float64;22
// type: text), and MySQL converts "4.20" to DECIMAL(6, 2) without rounding.21
// - NUMERIC gives apd.Decimal from github.com/cockroachdb/apd/v3, which keeps money22
// exact, and UUID gives uuid.UUID from github.com/google/uuid;23
// - DATE and TIMESTAMPTZ give time.Time, and BYTEA gives []byte;24
// - a type Seagull has no mapping for, such as INET, gives any, and the driver decides25
// what it holds: pgx's database/sql driver hands back the address as a string. Give26
// such a column a go_type override to pin it down.23
//27
//24
// Takes database (*sql.DB) which is a migrated database.28
// Takes database (*sql.DB) which is a migrated database.25
// Takes output (io.Writer) which receives one line per column.29
// Takes output (io.Writer) which receives one line per column.26
//30
//27
// Returns error when a query fails.31
// Returns error when a query fails.28
func run(ctx context.Context, database *sql.DB, output io.Writer) error {32
func run(ctx context.Context, database *sql.DB, output io.Writer) error {29
queries := store.New(database)33
queries := store.New(database)30
34
31
result, err := queries.RecordLanding(ctx, store.RecordLandingParams{35
recorded, err := queries.RecordLanding(ctx, store.RecordLandingParams{32
Species: "mackerel",36
CatchID: uuid.MustParse("6f1c0b8e-4b2a-4c55-9d7e-2a3f5e8c1d40"),33
Boxes: 12,37
Species: "mackerel",34
Fish: 480,38
Boxes: 12,35
WeightG: 163_200,39
Fish: 480,36
LengthCm: 31.5,40
WeightG: 163_200,37
OnIce: true,41
LengthCm: 31.5,38
LandedAt: time.Date(2026, time.May, 1, 5, 30, 0, 0, time.UTC),42
SeaTemp: 12.25,39
Tag: []byte{0x04, 0xa2, 0x19, 0xf3},43
Price: *apd.New(41_280, -2),40
Notes: new("landed at Newlyn"),44
OnIce: true,41
Berth: 7,45
LandedOn: time.Date(2026, time.May, 1, 0, 0, 0, 0, time.UTC),42
PricePerKg: "4.20",46
LandedAt: time.Date(2026, time.May, 1, 5, 30, 0, 0, time.UTC),47
HarbourIP: "192.0.2.17",48
Tag: []byte{0x04, 0xa2, 0x19, 0xf3},49
Notes: new("landed at Newlyn"),43
})50
})44
if err != nil {51
if err != nil {45
return fmt.Errorf("recording a landing: %w", err)52
return fmt.Errorf("recording a landing: %w", err)46
}53
}47
id, err := result.LastInsertId()48
if err != nil {49
return fmt.Errorf("reading the landing's id: %w", err)50
}51
54
52
landing, err := queries.GetLanding(ctx, id)55
landing, err := queries.GetLanding(ctx, recorded.ID)53
if err != nil {56
if err != nil {54
return fmt.Errorf("getting landing %d: %w", id, err)57
return fmt.Errorf("getting landing %d: %w", recorded.ID, err)55
}58
}56
fmt.Fprintf(output, "id %-12T %d\n", landing.ID, landing.ID)59
fmt.Fprintf(output, "id %-12T %d\n", landing.ID, landing.ID)57
fmt.Fprintf(output, "species %-12T %s\n", landing.Species, landing.Species)60
fmt.Fprintf(output, "catch_id %-12T %s\n", landing.CatchID, landing.CatchID)58
fmt.Fprintf(output, "boxes %-12T %d\n", landing.Boxes, landing.Boxes)61
fmt.Fprintf(output, "species %-12T %s\n", landing.Species, landing.Species)59
fmt.Fprintf(output, "fish %-12T %d\n", landing.Fish, landing.Fish)62
fmt.Fprintf(output, "boxes %-12T %d\n", landing.Boxes, landing.Boxes)60
fmt.Fprintf(output, "weight_g %-12T %d\n", landing.WeightG, landing.WeightG)63
fmt.Fprintf(output, "fish %-12T %d\n", landing.Fish, landing.Fish)61
fmt.Fprintf(output, "length_cm %-12T %.1f\n", landing.LengthCm, landing.LengthCm)64
fmt.Fprintf(output, "weight_g %-12T %d\n", landing.WeightG, landing.WeightG)62
fmt.Fprintf(output, "on_ice %-12T %t\n", landing.OnIce, landing.OnIce)65
fmt.Fprintf(output, "length_cm %-12T %.1f\n", landing.LengthCm, landing.LengthCm)63
fmt.Fprintf(output, "landed_at %-12T %s\n", landing.LandedAt, landing.LandedAt.Format(time.RFC3339))66
fmt.Fprintf(output, "sea_temp %-12T %.2f\n", landing.SeaTemp, landing.SeaTemp)67
fmt.Fprintf(output, "price %-12T %s\n", landing.Price, landing.Price.String())68
fmt.Fprintf(output, "on_ice %-12T %t\n", landing.OnIce, landing.OnIce)69
fmt.Fprintf(output, "landed_on %-12T %s\n", landing.LandedOn, landing.LandedOn.Format(time.DateOnly))70
// TIMESTAMPTZ arrives in the connection's time zone, so print it in UTC.71
fmt.Fprintf(output, "landed_at %-12T %s\n", landing.LandedAt, landing.LandedAt.UTC().Format(time.RFC3339))72
fmt.Fprintf(output, "harbour_ip %-12T %v\n", landing.HarbourIP, landing.HarbourIP)64
// %T prints []byte as []uint8, since byte is an alias for uint8.73
// %T prints []byte as []uint8, since byte is an alias for uint8.65
fmt.Fprintf(output, "tag %-12T % x\n", landing.Tag, landing.Tag)74
fmt.Fprintf(output, "tag %-12T % x\n", landing.Tag, landing.Tag)66
fmt.Fprintf(output, "notes %-12T %s\n", landing.Notes, *landing.Notes)75
fmt.Fprintf(output, "notes %-12T %s\n", landing.Notes, *landing.Notes)67
fmt.Fprintf(output, "berth %-12T %d\n", landing.Berth, landing.Berth)68
fmt.Fprintf(output, "price_per_kg %-12T %s\n", landing.PricePerKg, landing.PricePerKg.String())69
return nil76
return nil70
}77
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | MySQL | TimescaleDB |
|---|---|---|---|
| RecordLanding | parameter length_cm | float64 | float32 |
| GetLanding | column length_cm | float64 | float32 |