From SQL types to Go types
On this page
On DuckDB:
- Every column has an exact type, so the mapping follows the declared type directly
- REAL is a 4-byte float and maps to float32; DOUBLE maps to float64
- Unsigned types map to Go unsigned integers, e.g. UTINYINT to uint8
- HUGEINT maps to *big.Int, the type duckdb-go returns, even when the column is NOT NULL
- DATE and TIMESTAMP map to time.Time, and UUID to uuid.UUID
- DECIMAL and INTERVAL are left out: their default Go types, apd.Decimal and string, do not round-trip through duckdb-go yet, which returns an INTERVAL as its own duckdb.Interval
Seagull reads each column's declared type from your migrations and picks the Go type that holds it without loss. Integers keep the width and sign the engine stores, floats their precision, exact decimals become a decimal type rather than a float, dates and times become time.Time, and a column that can be NULL becomes a pointer. You never choose a Go type for a column by hand, and you never scan a NUMERIC into a float64 by accident.
This example records one landing of fish, with a column of each common kind, reads it back, and prints the Go type every column became.
The schema
db/migrations/0001_landings.up.sql
CREATE SEQUENCE landing_ids;
CREATE TABLE landings (
id BIGINT PRIMARY KEY DEFAULT nextval('landing_ids'),
species VARCHAR NOT NULL,
quay UTINYINT NOT NULL,
boxes SMALLINT NOT NULL,
fish INTEGER NOT NULL,
weight_g BIGINT NOT NULL,
length_cm DOUBLE NOT NULL,
water_temp REAL NOT NULL,
on_ice BOOLEAN NOT NULL,
landed_at TIMESTAMP NOT NULL,
landed_on DATE NOT NULL,
catch_id UUID NOT NULL,
auction_lot HUGEINT NOT NULL,
tag BLOB NOT NULL,
notes VARCHAR
);
What sgl generates
The row type for GetLanding is the mapping, field by field:
internal/store
type GetLandingRow struct {
ID int64 `json:"id"`
Species string `json:"species"`
Quay uint8 `json:"quay"`
Boxes int16 `json:"boxes"`
Fish int32 `json:"fish"`
WeightG int64 `json:"weight_g"`
LengthCm float64 `json:"length_cm"`
WaterTemp float32 `json:"water_temp"`
OnIce bool `json:"on_ice"`
LandedAt time.Time `json:"landed_at"`
LandedOn time.Time `json:"landed_on"`
CatchID uuid.UUID `json:"catch_id"`
AuctionLot *big.Int `json:"auction_lot"`
Tag []byte `json:"tag"`
Notes *string `json:"notes"`
}
func (queries *Queries) GetLanding(ctx context.Context, id int64) (GetLandingRow, error) {
var row GetLandingRow
err := queries.reader.QueryRowContext(ctx, getLandingSQL, id).Scan(&row.ID, &row.Species, &row.Quay, &row.Boxes, &row.Fish, &row.WeightG, &row.LengthCm, &row.WaterTemp, &row.OnIce, &row.LandedAt, &row.LandedOn, &row.CatchID, &row.AuctionLot, &row.Tag, &row.Notes)
if err != nil {
return GetLandingRow{}, err
}
return row, nil
}
SMALLINT,INTEGERandBIGINTgiveint16,int32andint64, and the unsignedUTINYINTgivesuint8.HUGEINTis a 128-bit integer with no Go equivalent, so it gives*big.Int, the type the DuckDB driver returns. It is a pointer even though the column isNOT NULL.REALis DuckDB's 4-byte float, so it givesfloat32;DOUBLEgivesfloat64.TIMESTAMPandDATEgivetime.Time, andUUIDgivesuuid.UUIDfromgithub.com/google/uuid.BLOBgives[]byte, and the nullablenotesgives*string.
Parameters follow the same rules, so RecordLanding takes the same Go types it hands back.
Using it
The program records a landing and reads it back, then prints each column's name, the Go type of its field (with %T) and its value.
%T prints []byte as []uint8, since byte is an alias for uint8.
example.go
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"math/big"
"time"
"github.com/google/uuid"
"seagull.sh/seagull/examples/duckdb/005_type_mapping/internal/store"
)
// run records a landing of fish and reads it back, printing the Go type sgl chose for
// each column. DuckDB declares every column's type exactly, and Seagull maps each one:
//
// - SMALLINT, INTEGER and BIGINT give int16, int32 and int64, and the unsigned UTINYINT
// gives uint8;
// - DOUBLE gives float64, but REAL is DuckDB's 4-byte float, so it gives float32;
// - TIMESTAMP and DATE give time.Time, and UUID gives uuid.UUID from
// github.com/google/uuid;
// - HUGEINT is a 128-bit integer with no Go equivalent, so it gives *big.Int, the type
// duckdb-go returns, even when the column is NOT NULL;
// - BLOB gives []byte, and a nullable VARCHAR gives *string.
//
// Takes database (*sql.DB) which is a migrated database.
// Takes output (io.Writer) which receives one line per column.
//
// Returns error when a query fails.
func run(ctx context.Context, database *sql.DB, output io.Writer) error {
queries := store.New(database)
// The auction lot number has 20 digits, too many for int64 or uint64.
lot, ok := new(big.Int).SetString("20260501000000000001", 10)
if !ok {
return errors.New("parsing the auction lot number")
}
recorded, err := queries.RecordLanding(ctx, store.RecordLandingParams{
Species: "mackerel",
Quay: 3,
Boxes: 12,
Fish: 480,
WeightG: 163_200,
LengthCm: 31.5,
WaterTemp: 11.25,
OnIce: true,
LandedAt: time.Date(2026, time.May, 1, 5, 30, 0, 0, time.UTC),
LandedOn: time.Date(2026, time.May, 1, 0, 0, 0, 0, time.UTC),
CatchID: uuid.MustParse("6f1c2a5e-4d3b-4c2a-9e8f-1a2b3c4d5e6f"),
AuctionLot: lot,
Tag: []byte{0x04, 0xa2, 0x19, 0xf3},
Notes: new("landed at Newlyn"),
})
if err != nil {
return fmt.Errorf("recording a landing: %w", err)
}
landing, err := queries.GetLanding(ctx, recorded.ID)
if err != nil {
return fmt.Errorf("getting landing %d: %w", recorded.ID, err)
}
fmt.Fprintf(output, "id %-10T %d\n", landing.ID, landing.ID)
fmt.Fprintf(output, "species %-10T %s\n", landing.Species, landing.Species)
fmt.Fprintf(output, "quay %-10T %d\n", landing.Quay, landing.Quay)
fmt.Fprintf(output, "boxes %-10T %d\n", landing.Boxes, landing.Boxes)
fmt.Fprintf(output, "fish %-10T %d\n", landing.Fish, landing.Fish)
fmt.Fprintf(output, "weight_g %-10T %d\n", landing.WeightG, landing.WeightG)
fmt.Fprintf(output, "length_cm %-10T %.1f\n", landing.LengthCm, landing.LengthCm)
fmt.Fprintf(output, "water_temp %-10T %.2f\n", landing.WaterTemp, landing.WaterTemp)
fmt.Fprintf(output, "on_ice %-10T %t\n", landing.OnIce, landing.OnIce)
fmt.Fprintf(output, "landed_at %-10T %s\n", landing.LandedAt, landing.LandedAt.Format(time.RFC3339))
fmt.Fprintf(output, "landed_on %-10T %s\n", landing.LandedOn, landing.LandedOn.Format(time.DateOnly))
fmt.Fprintf(output, "catch_id %-10T %s\n", landing.CatchID, landing.CatchID)
fmt.Fprintf(output, "auction_lot %-10T %s\n", landing.AuctionLot, landing.AuctionLot)
// %T prints []byte as []uint8, since byte is an alias for uint8.
fmt.Fprintf(output, "tag %-10T % x\n", landing.Tag, landing.Tag)
fmt.Fprintf(output, "notes %-10T %s\n", landing.Notes, *landing.Notes)
return nil
}
What it prints
id int64 1
species string mackerel
quay uint8 3
boxes int16 12
fish int32 480
weight_g int64 163200
length_cm float64 31.5
water_temp float32 11.25
on_ice bool true
landed_at time.Time 2026-05-01T05:30:00Z
landed_on time.Time 2026-05-01
catch_id uuid.UUID 6f1c2a5e-4d3b-4c2a-9e8f-1a2b3c4d5e6f
auction_lot *big.Int 20260501000000000001
tag []uint8 04 a2 19 f3
notes *string landed at Newlyn
Changing a type
The defaults are a starting point. A go_type directive on a column, or an override in sgl.yaml, swaps in a type of your own, such as a named Pence type for money; Type overrides shows how. The type mapping reference lists every default, and the order in which overrides apply.
Across engines
Only these Go types of GetLanding differ between engines; the rest are the same everywhere.
| DuckDB | SQLite | PostgreSQL | MySQL | ClickHouse | |
|---|---|---|---|---|---|
id parameter | int64 | int64 | int64 | int64 | uint64 |
id column | int64 | int64 | int64 | int64 | uint64 |
boxes column | int16 | int64 | int16 | int16 | uint16 |
fish column | int32 | int64 | int32 | int32 | uint32 |
length_cm column | float64 | float64 | float32 | float64 | float32 |
tag column | []byte | []byte | []byte | []byte | string |
catch_id column | uuid.UUID | · | uuid.UUID | · | uuid.UUID |
sea_temp column | · | · | float64 | · | · |
price column | · | · | apd.Decimal | · | decimal.Decimal |
landed_on column | time.Time | · | time.Time | · | · |
harbour_ip column | · | · | any | · | · |
berth column | · | · | · | uint8 | · |
price_per_kg column | · | · | · | apd.Decimal | · |
quay column | uint8 | · | · | · | · |
water_temp column | float32 | · | · | · | · |
auction_lot column | *big.Int | · | · | · | · |
A · marks a parameter or column that version of the example does not have.
TimescaleDB runs the PostgreSQL version unchanged.
MariaDB runs the MySQL version unchanged.
Compare the whole example side by side with SQLite, PostgreSQL, MySQL and ClickHouse.
Run it locally
From the directory you cloned the Seagull repository into:
cd seagull/examples/duckdb && go run ./005_type_mapping
Its test runs it too, and checks what it prints against output.txt:
cd seagull/examples/duckdb && go test ./005_type_mapping
Needs cgo and a C compiler (the DuckDB driver).
The files
Every file of the example, as it is in the Seagull repository.
sgl.yaml
# Copyright 2026 PolitePixels Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This project stands against fascism, authoritarianism, and all forms of
# oppression. We built this to empower people, not to enable those who would
# strip others of their rights and dignity.
version: 1
databases:
- name: harbour
dialect: duckdb
migrations: db/migrations
queries: db/queries
go:
package: store
out: internal/store
db/migrations/0001_landings.up.sql
CREATE SEQUENCE landing_ids;
CREATE TABLE landings (
id BIGINT PRIMARY KEY DEFAULT nextval('landing_ids'),
species VARCHAR NOT NULL,
quay UTINYINT NOT NULL,
boxes SMALLINT NOT NULL,
fish INTEGER NOT NULL,
weight_g BIGINT NOT NULL,
length_cm DOUBLE NOT NULL,
water_temp REAL NOT NULL,
on_ice BOOLEAN NOT NULL,
landed_at TIMESTAMP NOT NULL,
landed_on DATE NOT NULL,
catch_id UUID NOT NULL,
auction_lot HUGEINT NOT NULL,
tag BLOB NOT NULL,
notes VARCHAR
);
db/migrations/0001_landings.down.sql
DROP TABLE landings;
DROP SEQUENCE landing_ids;
db/queries/landings.sql
-- sgl.query(RecordLanding, one)
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)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
RETURNING id;
-- sgl.query(GetLanding, one)
SELECT id, species, quay, boxes, fish, weight_g, length_cm, water_temp, on_ice, landed_at, landed_on, catch_id, auction_lot, tag, notes
FROM landings
WHERE id = $1;
example.go
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"math/big"
"time"
"github.com/google/uuid"
"seagull.sh/seagull/examples/duckdb/005_type_mapping/internal/store"
)
// run records a landing of fish and reads it back, printing the Go type sgl chose for
// each column. DuckDB declares every column's type exactly, and Seagull maps each one:
//
// - SMALLINT, INTEGER and BIGINT give int16, int32 and int64, and the unsigned UTINYINT
// gives uint8;
// - DOUBLE gives float64, but REAL is DuckDB's 4-byte float, so it gives float32;
// - TIMESTAMP and DATE give time.Time, and UUID gives uuid.UUID from
// github.com/google/uuid;
// - HUGEINT is a 128-bit integer with no Go equivalent, so it gives *big.Int, the type
// duckdb-go returns, even when the column is NOT NULL;
// - BLOB gives []byte, and a nullable VARCHAR gives *string.
//
// Takes database (*sql.DB) which is a migrated database.
// Takes output (io.Writer) which receives one line per column.
//
// Returns error when a query fails.
func run(ctx context.Context, database *sql.DB, output io.Writer) error {
queries := store.New(database)
// The auction lot number has 20 digits, too many for int64 or uint64.
lot, ok := new(big.Int).SetString("20260501000000000001", 10)
if !ok {
return errors.New("parsing the auction lot number")
}
recorded, err := queries.RecordLanding(ctx, store.RecordLandingParams{
Species: "mackerel",
Quay: 3,
Boxes: 12,
Fish: 480,
WeightG: 163_200,
LengthCm: 31.5,
WaterTemp: 11.25,
OnIce: true,
LandedAt: time.Date(2026, time.May, 1, 5, 30, 0, 0, time.UTC),
LandedOn: time.Date(2026, time.May, 1, 0, 0, 0, 0, time.UTC),
CatchID: uuid.MustParse("6f1c2a5e-4d3b-4c2a-9e8f-1a2b3c4d5e6f"),
AuctionLot: lot,
Tag: []byte{0x04, 0xa2, 0x19, 0xf3},
Notes: new("landed at Newlyn"),
})
if err != nil {
return fmt.Errorf("recording a landing: %w", err)
}
landing, err := queries.GetLanding(ctx, recorded.ID)
if err != nil {
return fmt.Errorf("getting landing %d: %w", recorded.ID, err)
}
fmt.Fprintf(output, "id %-10T %d\n", landing.ID, landing.ID)
fmt.Fprintf(output, "species %-10T %s\n", landing.Species, landing.Species)
fmt.Fprintf(output, "quay %-10T %d\n", landing.Quay, landing.Quay)
fmt.Fprintf(output, "boxes %-10T %d\n", landing.Boxes, landing.Boxes)
fmt.Fprintf(output, "fish %-10T %d\n", landing.Fish, landing.Fish)
fmt.Fprintf(output, "weight_g %-10T %d\n", landing.WeightG, landing.WeightG)
fmt.Fprintf(output, "length_cm %-10T %.1f\n", landing.LengthCm, landing.LengthCm)
fmt.Fprintf(output, "water_temp %-10T %.2f\n", landing.WaterTemp, landing.WaterTemp)
fmt.Fprintf(output, "on_ice %-10T %t\n", landing.OnIce, landing.OnIce)
fmt.Fprintf(output, "landed_at %-10T %s\n", landing.LandedAt, landing.LandedAt.Format(time.RFC3339))
fmt.Fprintf(output, "landed_on %-10T %s\n", landing.LandedOn, landing.LandedOn.Format(time.DateOnly))
fmt.Fprintf(output, "catch_id %-10T %s\n", landing.CatchID, landing.CatchID)
fmt.Fprintf(output, "auction_lot %-10T %s\n", landing.AuctionLot, landing.AuctionLot)
// %T prints []byte as []uint8, since byte is an alias for uint8.
fmt.Fprintf(output, "tag %-10T % x\n", landing.Tag, landing.Tag)
fmt.Fprintf(output, "notes %-10T %s\n", landing.Notes, *landing.Notes)
return nil
}
main.go
// Command 005_type_mapping applies its migrations to an in-memory DuckDB database, then
// runs the example in example.go: one landing of fish is recorded and read back, printing
// the Go type sgl chose for each column.
//
// Run it with `go run .`, or `go run . -db example.duckdb` to keep the database in a
// file.
package main
import (
"context"
"database/sql"
"embed"
"flag"
"fmt"
"log"
"os"
_ "github.com/duckdb/duckdb-go/v2"
"seagull.sh/seagull/sdk/migrate"
"seagull.sh/seagull/sdk/migrate/migrate_backend_duckdb"
)
// migrations holds the migration files, compiled into the binary.
//
//go:embed db/migrations
var migrations embed.FS
func main() {
path := flag.String("db", "", "DuckDB database file; an in-memory database when empty")
flag.Parse()
if err := execute(context.Background(), *path); err != nil {
log.Fatal(err)
}
}
// execute opens and migrates the DuckDB database at path, or an in-memory one when path
// is empty, and runs the example against it.
//
// Takes path (string) which is the database file, or empty.
//
// Returns error when the database cannot be prepared or the example fails.
func execute(ctx context.Context, path string) error {
database, err := sql.Open("duckdb", path)
if err != nil {
return err
}
defer func() { _ = database.Close() }()
migrator, err := migrate.NewMigrator(database, migrate_backend_duckdb.New(), migrations, "db/migrations")
if err != nil {
return err
}
if _, err := migrator.Up(ctx); err != nil {
return fmt.Errorf("migrating: %w", err)
}
return run(ctx, database, os.Stdout)
}
internal/store/landings.sql.go
// Code generated by sgl. DO NOT EDIT.
package store
import (
"context"
"math/big"
"time"
"github.com/google/uuid"
)
const getLandingSQL = `SELECT id, species, quay, boxes, fish, weight_g, length_cm, water_temp, on_ice, landed_at, landed_on, catch_id, auction_lot, tag, notes
FROM landings
WHERE id = $1;`
type GetLandingRow struct {
ID int64 `json:"id"`
Species string `json:"species"`
Quay uint8 `json:"quay"`
Boxes int16 `json:"boxes"`
Fish int32 `json:"fish"`
WeightG int64 `json:"weight_g"`
LengthCm float64 `json:"length_cm"`
WaterTemp float32 `json:"water_temp"`
OnIce bool `json:"on_ice"`
LandedAt time.Time `json:"landed_at"`
LandedOn time.Time `json:"landed_on"`
CatchID uuid.UUID `json:"catch_id"`
AuctionLot *big.Int `json:"auction_lot"`
Tag []byte `json:"tag"`
Notes *string `json:"notes"`
}
func (queries *Queries) GetLanding(ctx context.Context, id int64) (GetLandingRow, error) {
var row GetLandingRow
err := queries.reader.QueryRowContext(ctx, getLandingSQL, id).Scan(&row.ID, &row.Species, &row.Quay, &row.Boxes, &row.Fish, &row.WeightG, &row.LengthCm, &row.WaterTemp, &row.OnIce, &row.LandedAt, &row.LandedOn, &row.CatchID, &row.AuctionLot, &row.Tag, &row.Notes)
if err != nil {
return GetLandingRow{}, err
}
return row, nil
}
const recordLandingSQL = `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)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
RETURNING id;`
type RecordLandingParams struct {
Species string
Quay uint8
Boxes int16
Fish int32
WeightG int64
LengthCm float64
WaterTemp float32
OnIce bool
LandedAt time.Time
LandedOn time.Time
CatchID uuid.UUID
AuctionLot *big.Int
Tag []byte
Notes *string
}
type RecordLandingRow struct {
ID int64 `json:"id"`
}
func (queries *Queries) RecordLanding(ctx context.Context, params RecordLandingParams) (RecordLandingRow, error) {
var row RecordLandingRow
err := queries.writer.QueryRowContext(ctx, recordLandingSQL, params.Species, params.Quay, params.Boxes, params.Fish, params.WeightG, params.LengthCm, params.WaterTemp, params.OnIce, params.LandedAt, params.LandedOn, params.CatchID, params.AuctionLot, params.Tag, params.Notes).Scan(&row.ID)
if err != nil {
return RecordLandingRow{}, err
}
return row, nil
}
internal/store/models.go
// Code generated by sgl. DO NOT EDIT.
package store
import (
"math/big"
"time"
"github.com/google/uuid"
)
type Landings struct {
ID int64 `json:"id"`
Species string `json:"species"`
Quay uint8 `json:"quay"`
Boxes int16 `json:"boxes"`
Fish int32 `json:"fish"`
WeightG int64 `json:"weight_g"`
LengthCm float64 `json:"length_cm"`
WaterTemp float32 `json:"water_temp"`
OnIce bool `json:"on_ice"`
LandedAt time.Time `json:"landed_at"`
LandedOn time.Time `json:"landed_on"`
CatchID uuid.UUID `json:"catch_id"`
AuctionLot *big.Int `json:"auction_lot"`
Tag []byte `json:"tag"`
Notes *string `json:"notes"`
}
internal/store/querier.go
// Code generated by sgl. DO NOT EDIT.
package store
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
type dbtxWrapper interface {
WrapDBTX(inner any) any
}
type Queries struct {
reader DBTX
writer DBTX
wrapper dbtxWrapper
}
func New(db DBTX) *Queries {
wrapper, _ := db.(dbtxWrapper)
return &Queries{reader: db, writer: db, wrapper: wrapper}
}
func NewWithReplica(writer DBTX, reader DBTX) *Queries {
wrapper, _ := writer.(dbtxWrapper)
return &Queries{reader: reader, writer: writer, wrapper: wrapper}
}
func (queries *Queries) WithTx(transaction *sql.Tx) *Queries {
if queries == nil {
return &Queries{reader: transaction, writer: transaction, wrapper: nil}
}
db := queries.wrapDBTX(transaction)
return &Queries{reader: db, writer: db, wrapper: queries.wrapper}
}
func (queries *Queries) wrapDBTX(db DBTX) DBTX {
if queries.wrapper == nil {
return db
}
wrapped, ok := queries.wrapper.WrapDBTX(db).(DBTX)
if !ok {
return db
}
return wrapped
}
func (queries *Queries) RunInTx(ctx context.Context, db *sql.DB, fn func(*Queries) error) error {
transaction, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer transaction.Rollback()
if err = fn(queries.WithTx(transaction)); err != nil {
return err
}
return transaction.Commit()
}
output.txt
id int64 1
species string mackerel
quay uint8 3
boxes int16 12
fish int32 480
weight_g int64 163200
length_cm float64 31.5
water_temp float32 11.25
on_ice bool true
landed_at time.Time 2026-05-01T05:30:00Z
landed_on time.Time 2026-05-01
catch_id uuid.UUID 6f1c2a5e-4d3b-4c2a-9e8f-1a2b3c4d5e6f
auction_lot *big.Int 20260501000000000001
tag []uint8 04 a2 19 f3
notes *string landed at Newlyn