Enums as generated Go types, and your own types with go_type: PostgreSQL and ClickHouse
Every file of the PostgreSQL and ClickHouse versions side by side, with the lines that differ marked. 2 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
PostgreSQL
ClickHouse
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: harbour4
- name: harbour5
dialect: postgres5
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/store11
driver: database/sqlMigrations
db/migrations/0001_boats.up.sqldiffers
PostgreSQL
ClickHouse
1
CREATE TYPE boat_status AS ENUM ('moored', 'at_sea', 'dry_dock');2
3
CREATE TABLE boats (1
CREATE TABLE boats (4
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,2
id UInt64,5
name TEXT NOT NULL,3
name String,6
status boat_status NOT NULL DEFAULT 'moored'4
status Enum8('moored' = 1, 'fishing' = 2, 'in_dock' = 3)7
);5
) ENGINE = MergeTree() ORDER BY id;db/migrations/0001_boats.down.sqldiffers
PostgreSQL
ClickHouse
1
DROP TABLE boats;1
DROP TABLE boats;2
DROP TYPE boat_status;Queries
AddBoatdiffers
PostgreSQL · db/queries/boats.sql
ClickHouse · db/queries/boats.sql
1
-- sgl.query(AddBoat, exec)1
-- sgl.query(AddBoat, exec)2
INSERT INTO boats (name, status) VALUES ($1, $2);2
INSERT INTO boats (id, name, status) VALUES ({id:UInt64}, {name:String}, {status:String});SetStatusonly PostgreSQL
PostgreSQL · db/queries/boats.sql
ClickHouse · not in this version
1
-- sgl.query(SetStatus, execrows)2
UPDATE boats SET status = $1 WHERE name = $2;ListBoatsdiffers
PostgreSQL · db/queries/boats.sql
ClickHouse · db/queries/boats.sql
1
-- sgl.query(ListBoats, many)1
-- sgl.query(ListBoats, many)2
SELECT name, status FROM boats ORDER BY id;2
SELECT id, name, status FROM boats ORDER BY id;BoatsWithStatusonly PostgreSQL
PostgreSQL · db/queries/boats.sql
ClickHouse · not in this version
1
-- sgl.query(BoatsWithStatus, many)2
-- sgl.column(status, go_type: "seagull.sh/seagull/examples/postgres/007_enums/internal/harbour.Status")3
SELECT name, status FROM boats WHERE status = $1 ORDER BY id;BoatsNotInonly ClickHouse
PostgreSQL · not in this version
ClickHouse · db/queries/boats.sql
1
-- sgl.query(BoatsNotIn, many)2
-- sgl.column(status, go_type: "seagull.sh/seagull/examples/clickhouse/007_enums/internal/domain.Status")3
SELECT id, name, status FROM boats WHERE status != {status:String} ORDER BY id;What sgl generates
AddBoatdiffers
PostgreSQL
ClickHouse
1
func (queries *Queries) AddBoat(ctx context.Context, params AddBoatParams) error1
func (queries *Queries) AddBoat(ctx context.Context, params AddBoatParams) error2
2
3
type AddBoatParams struct {3
type AddBoatParams struct {4
ID uint644
Name string5
Name string5
Status BoatStatus6
Status string6
}7
}7
8
type BoatStatus stringSetStatusonly PostgreSQL
PostgreSQL
ClickHouse · not in this version
1
func (queries *Queries) SetStatus(ctx context.Context, params SetStatusParams) (int64, error)2
3
type SetStatusParams struct {4
Status BoatStatus5
Name string6
}7
8
type BoatStatus stringListBoatsdiffers
PostgreSQL
ClickHouse
1
func (queries *Queries) ListBoats(ctx context.Context) ([]ListBoatsRow, error)1
func (queries *Queries) ListBoats(ctx context.Context) ([]ListBoatsRow, error)2
2
3
type ListBoatsRow struct {3
type ListBoatsRow struct {4
Name string `json:"name"`4
ID uint64 `json:"id"`5
Status BoatStatus `json:"status"`5
Name string `json:"name"`6
Status BoatsStatus `json:"status"`6
}7
}7
8
8
type BoatStatus string9
type BoatsStatus stringBoatsWithStatusonly PostgreSQL
PostgreSQL
ClickHouse · not in this version
1
func (queries *Queries) BoatsWithStatus(ctx context.Context, status BoatStatus) ([]BoatsWithStatusRow, error)2
3
type BoatStatus string4
5
type BoatsWithStatusRow struct {6
Name string `json:"name"`7
Status harbour.Status `json:"status"`8
}BoatsNotInonly ClickHouse
PostgreSQL · not in this version
ClickHouse
1
func (queries *Queries) BoatsNotIn(ctx context.Context, status string) ([]BoatsNotInRow, error)2
3
type BoatsNotInRow struct {4
ID uint64 `json:"id"`5
Name string `json:"name"`6
Status domain.Status `json:"status"`7
}The program
example.godiffers
PostgreSQL
ClickHouse
1
package main1
package main2
2
3
import (3
import (4
"context"4
"context"5
"database/sql"5
"database/sql"6
"errors"7
"fmt"6
"fmt"8
"io"7
"io"8
"strings"9
9
10
"seagull.sh/seagull/examples/postgres/007_enums/internal/store"10
"seagull.sh/seagull/examples/clickhouse/007_enums/internal/domain"11
"seagull.sh/seagull/examples/clickhouse/007_enums/internal/store"11
)12
)12
13
13
// run gives the harbour's boats a status from the boat_status enum and lists them twice.14
// run berths four boats and lists them twice. boats.status is an Enum8, and Seagull14
// The enum becomes a named Go type in the generated package: models.go declares15
// declares a named Go type for it in models.go, named after the table and column that15
// store.BoatStatus, named after the enum type, with a constant for each label, such as16
// hold it: store.BoatsStatus, with a constant for each label, such as16
// store.BoatStatusMoored, and a Valid method. Every column and parameter of the enum17
// store.BoatsStatusMoored, and a Valid method. ListBoats returns Status as a17
// takes it, in models.go, in Params structs and in Row structs, as ListBoats shows. Valid18
// store.BoatsStatus. The status parameters are written {status:String}, so they are plain18
// reports whether a value is one of the labels, and the server still enforces them, so a19
// strings, and the server converts the label to the Enum8.19
// status outside the enum is refused when the statement runs.20
//20
//21
// For a type of your own, here harbour.Status with a method that describes each label,21
// BoatsNotIn adds sgl.column(status, go_type: "…/internal/domain.Status"), so its rows22
// name it with a go_type override. BoatsWithStatus does that for one query with22
// carry the harbour's own domain.Status type, with its constants and methods. The23
// sgl.column(status, go_type: ...); a types.overrides entry in sgl.yaml, naming the enum23
// override changes only that query's result rows: parameters stay string, so the example24
// type boat_status or the column boats.status, would do it for every query, and for the24
// converts with string(domain.StatusInDock) on the way in. A types.overrides entry in25
// parameters bound to the column too. A query's sgl.column override changes only that25
// sgl.yaml would change every query's rows the same way.26
// query's returned column, so AddBoat, SetStatus and BoatsWithStatus's own parameter27
// still take the generated store.BoatStatus.28
//26
//27
// ClickHouse checks every label against the Enum8, so a status that is not one of its28
// labels is refused by the server, not stored.29
//29
// Takes database (*sql.DB) which is a migrated database.30
// Takes database (*sql.DB) which is a migrated database.30
// Takes output (io.Writer) which receives both listings.31
// Takes output (io.Writer) which receives both listings and the refusal.31
//32
//32
// Returns error when a query fails or a status outside the enum is accepted.33
// Returns error when a query fails or a bad status is accepted.33
func run(ctx context.Context, database *sql.DB, output io.Writer) error {34
func run(ctx context.Context, database *sql.DB, output io.Writer) error {34
queries := store.New(database)35
queries := store.New(database)35
36
36
boats := []store.AddBoatParams{37
boats := []store.AddBoatParams{37
{Name: "Kittiwake", Status: store.BoatStatusMoored},38
{ID: 1, Name: "Kittiwake", Status: string(domain.StatusMoored)},38
{Name: "Salty Chip", Status: store.BoatStatusAtSea},39
{ID: 2, Name: "Salty Chip", Status: string(domain.StatusFishing)},39
{Name: "Gull Wing", Status: store.BoatStatusDryDock},40
{ID: 3, Name: "Gull Wing", Status: string(domain.StatusInDock)},40
{Name: "Driftwood", Status: store.BoatStatusMoored},41
{ID: 4, Name: "Driftwood", Status: string(domain.StatusFishing)},41
}42
}42
for _, boat := range boats {43
for _, boat := range boats {43
if err := queries.AddBoat(ctx, boat); err != nil {44
if err := queries.AddBoat(ctx, boat); err != nil {44
return fmt.Errorf("adding %s: %w", boat.Name, err)45
return fmt.Errorf("adding %s: %w", boat.Name, err)45
}46
}46
}47
}47
48
48
sunk := store.BoatStatus("sunk")49
if err := queries.AddBoat(ctx, store.AddBoatParams{Name: "Wreck", Status: sunk}); err == nil {50
return errors.New("adding a boat with status sunk: got no error, want the enum to refuse it")51
}52
fmt.Fprintf(output, "status %q: Valid() is %t, and the server refuses it, as it is not a boat_status label\n", sunk, sunk.Valid())53
54
if _, err := queries.SetStatus(ctx, store.SetStatusParams{Status: store.BoatStatusAtSea, Name: "Kittiwake"}); err != nil {55
return fmt.Errorf("sending Kittiwake to sea: %w", err)56
}57
58
listed, err := queries.ListBoats(ctx)49
listed, err := queries.ListBoats(ctx)59
if err != nil {50
if err != nil {60
return fmt.Errorf("listing boats: %w", err)51
return fmt.Errorf("listing boats: %w", err)61
}52
}62
fmt.Fprintln(output, "\nListBoats, where status is the generated store.BoatStatus:")53
fmt.Fprintln(output, "all boats, status as store.BoatsStatus:")63
for _, boat := range listed {54
for _, boat := range listed {64
fmt.Fprintf(output, " %-10s %-8s %T\n", boat.Name, boat.Status, boat.Status)55
fmt.Fprintf(output, " %d %-11s %s\n", boat.ID, boat.Name, boat.Status)65
}56
}66
57
67
atSea, err := queries.BoatsWithStatus(ctx, store.BoatStatusAtSea)58
afloat, err := queries.BoatsNotIn(ctx, string(domain.StatusInDock))68
if err != nil {59
if err != nil {69
return fmt.Errorf("listing boats at sea: %w", err)60
return fmt.Errorf("listing boats not in dock: %w", err)70
}61
}71
fmt.Fprintln(output, "\nBoatsWithStatus, where a go_type override makes status a harbour.Status:")62
fmt.Fprintln(output, "not in dock, status as domain.Status:")72
for _, boat := range atSea {63
for _, boat := range afloat {73
fmt.Fprintf(output, " %-10s %-14s %T\n", boat.Name, boat.Status.Describe(), boat.Status)64
fmt.Fprintf(output, " %d %-11s %-8s at sea: %t\n", boat.ID, boat.Name, boat.Status, boat.Status.AtSea())74
}65
}66
67
sunk := store.AddBoatParams{ID: 5, Name: "Old Barnacle", Status: "sunk"}68
err = queries.AddBoat(ctx, sunk)69
if err == nil {70
return fmt.Errorf("adding status %q: got no error, want the server's refusal", sunk.Status)71
}72
if !strings.Contains(err.Error(), "Unknown element 'sunk' for enum") {73
return fmt.Errorf("adding status %q: got %w, want the server's refusal", sunk.Status, err)74
}75
fmt.Fprintf(output, "status %q refused: not one of the Enum8 labels\n", sunk.Status)75
return nil76
return nil76
}77
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | PostgreSQL | ClickHouse |
|---|---|---|---|
| AddBoat | parameter status | BoatStatus | string |
| ListBoats | column status | BoatStatus | BoatsStatus |