Type overrides
On this page
The same on CockroachDB. CockroachDB runs this example unchanged: the SQL and the Go that sgl generates are identical to the PostgreSQL version. Only
dialect:in sgl.yaml changes, and the program migrates withmigrate_backend_cockroachdb.New().
The type Seagull infers for a column is right most of the time, but not always the type you want. A price might be better as your own money type with a String method, a status as a named type with constants, and an expression whose type depends on the data, such as a value pulled out of a JSON document, needs a type Seagull cannot work out alone. An override tells Seagull which Go type, SQL type or nullability to use instead.
Overrides can live in three places, and each reaches a different distance:
- in
sgl.yaml, for a column or a whole SQL type, in every query; - in a migration, above the
CREATE TABLE, for one column in every query that returns it as it is; - in a query's header, for one output column of that query.
This example reads a chip shop menu back as the shop's own types, and uses all three.
The shop's own types
The program's package internal/menu declares two types. Status is a string type with the constants Available and SoldOut, for a TEXT column with a CHECK constraint; an enum column takes an override the same way. Price is a string type that holds the exact text CockroachDB writes for a NUMERIC(5, 2), such as 3.50, and its String method prints it as £3.50.
In sgl.yaml
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: menu
dialect: postgres
migrations: db/migrations
queries: db/queries
go:
package: store
out: internal/store
driver: database/sql
types:
decimal: string
overrides:
- column: menu_items.status
go: seagull.sh/seagull/examples/postgres/009_overrides/internal/menu.Status
types.overrides names the column menu_items.status, so every query that returns the column as it is gets a menu.Status. An entry with sql: in place of column: would change every column of that SQL type.
types.decimal: string is a type slot, which replaces a whole category. Every NUMERIC becomes a string holding the server's exact text, in place of the default apd.Decimal, so the module needs no decimal package. Unlike a column override, a slot changes models.go and parameters as well as returned columns.
In a migration
db/migrations/0001_menu.up.sql
-- sgl.column(menu_items.price, go_type: "seagull.sh/seagull/examples/postgres/009_overrides/internal/menu.Price")
CREATE TABLE menu_items (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
section TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('available', 'sold_out')),
price NUMERIC(5, 2) NOT NULL,
nutrition JSONB NOT NULL
);
The sgl.column(menu_items.price, go_type: ...) line above the CREATE TABLE gives price the type menu.Price in every query that returns the column as it is. The value names the type by its full import path, and the generated code imports the package.
In a query
db/queries/menu.sql
-- sgl.query(ListMenu, many)
-- sgl.column(kcal, type: bigint, nullable: false)
SELECT name, status, price, nutrition ->> 'kcal' AS kcal
FROM menu_items
ORDER BY section, name;
nutrition ->> 'kcal' extracts text that is NULL when the key is missing, so Seagull types it *string. sgl.column(kcal, type: bigint, nullable: false) overrides its SQL type and its nullability: type: says what the value is in SQL, and the usual mapping turns that into int64. go_type: names the Go type directly, and the two cannot be combined. A cast, (nutrition ->> 'kcal')::bigint, would type the column too, and make the server check it.
The row shows all three overrides at once: Status from sgl.yaml, the price from the migration, and Kcal from the query.
internal/store
type ListMenuRow struct {
Name string `json:"name"`
Status menu.Status `json:"status"`
Price menu.Price `json:"price"`
Kcal int64 `json:"kcal"`
}
func (queries *Queries) ListMenu(ctx context.Context) ([]ListMenuRow, error) {
rows, err := queries.reader.QueryContext(ctx, listMenuSQL)
if err != nil {
return nil, err
}
defer rows.Close()
var results []ListMenuRow
for rows.Next() {
var row ListMenuRow
if err := rows.Scan(&row.Name, sglScanOverride(&row.Status), &row.Price, &row.Kcal); err != nil {
return nil, err
}
results = append(results, row)
}
if err := rows.Err(); err != nil {
return nil, err
}
return results, nil
}
The migration's override reaches only a column returned as it is. In CheapestPerSection, the cheapest price is computed by MIN, which is a new value, so the override does not apply, and an aggregate could be NULL. Without a directive, Cheapest would be a pointer to the inferred type.
The directive names menu.Price again, and nullable: false drops the pointer, since every group has at least one row:
db/queries/menu.sql
-- sgl.query(CheapestPerSection, many)
-- sgl.column(cheapest, go_type: "seagull.sh/seagull/examples/postgres/009_overrides/internal/menu.Price", nullable: false)
SELECT section, MIN(price) AS cheapest
FROM menu_items
GROUP BY section
ORDER BY section;
internal/store
type CheapestPerSectionRow struct {
Section string `json:"section"`
Cheapest menu.Price `json:"cheapest"`
}
func (queries *Queries) CheapestPerSection(ctx context.Context) ([]CheapestPerSectionRow, error) {
rows, err := queries.reader.QueryContext(ctx, cheapestPerSectionSQL)
if err != nil {
return nil, err
}
defer rows.Close()
var results []CheapestPerSectionRow
for rows.Next() {
var row CheapestPerSectionRow
if err := rows.Scan(&row.Section, &row.Cheapest); err != nil {
return nil, err
}
results = append(results, row)
}
if err := rows.Err(); err != nil {
return nil, err
}
return results, nil
}
When two overrides cover the same column, the narrower one wins: a query's directive over a migration's, and a directive over sgl.yaml. The type mapping reference gives the full order.
Parameters keep the inferred type
A column override changes what a query returns, not what it takes, and models.go keeps the inferred types too.
AddItemParams still has a string status, and a string price from the slot, so the program converts on the way in with string(menu.Available) and passes prices as text.
Using it
example.go
package main
import (
"context"
"database/sql"
"fmt"
"io"
"seagull.sh/seagull/examples/postgres/009_overrides/internal/menu"
"seagull.sh/seagull/examples/postgres/009_overrides/internal/store"
)
// run fills a chip shop menu and reads it back. Five overrides replace the Go types
// Seagull would infer:
//
// - sgl.yaml's types.decimal slot maps every NUMERIC to string, the server's exact
// text, in place of apd.Decimal, so the module needs no decimal package; a slot
// changes models.go and parameters as well as returned columns;
// - sgl.yaml's types.overrides gives menu_items.status the type menu.Status;
// - sgl.column(menu_items.price, go_type: ...) in the migration gives price the type
// menu.Price in every query that returns the column as it is;
// - sgl.column(kcal, type: bigint, nullable: false) in ListMenu types nutrition ->>
// 'kcal', which is nullable text, as the number every document holds; a cast,
// (nutrition ->> 'kcal')::bigint, would type it too, and make the server check it;
// - in CheapestPerSection, MIN(price) is a new value, so the migration's override no
// longer applies and the aggregate could be NULL; sgl.column(cheapest, go_type: ...,
// nullable: false) makes it menu.Price again, as every group has a row.
//
// Column overrides also reach models.go and the parameters bound to those columns, so
// AddItemParams takes a menu.Status and a menu.Price.
//
// sgl vet catches mistakes in overrides: Q036 for a query override naming a column the
// query does not return, Q037 for an override naming a table column that does not exist,
// Q038 for type: and go_type: together, and Q049 for a go_type the generator cannot use.
//
// Takes database (*sql.DB) which is a migrated database.
// Takes output (io.Writer) which receives the menu and the cheapest item per section.
//
// Returns error when a query fails.
func run(ctx context.Context, database *sql.DB, output io.Writer) error {
queries := store.New(database)
items := []store.AddItemParams{
{Name: "Cod and chips", Section: "fish", Status: menu.Available, Price: "9.50", Nutrition: store.JSON(`{"kcal": 1050}`)},
{Name: "Haddock and chips", Section: "fish", Status: menu.SoldOut, Price: "10.50", Nutrition: store.JSON(`{"kcal": 980}`)},
{Name: "Chips", Section: "sides", Status: menu.Available, Price: "3.50", Nutrition: store.JSON(`{"kcal": 560}`)},
{Name: "Mushy peas", Section: "sides", Status: menu.Available, Price: "1.75", Nutrition: store.JSON(`{"kcal": 120}`)},
}
for _, item := range items {
if err := queries.AddItem(ctx, item); err != nil {
return fmt.Errorf("adding %s: %w", item.Name, err)
}
}
listed, err := queries.ListMenu(ctx)
if err != nil {
return fmt.Errorf("listing the menu: %w", err)
}
for _, item := range listed {
if item.Status == menu.SoldOut {
fmt.Fprintf(output, "%-18s sold out\n", item.Name)
continue
}
fmt.Fprintf(output, "%-18s %6s %4d kcal\n", item.Name, item.Price, item.Kcal)
}
cheapest, err := queries.CheapestPerSection(ctx)
if err != nil {
return fmt.Errorf("finding the cheapest items: %w", err)
}
for _, section := range cheapest {
fmt.Fprintf(output, "cheapest %-6s from %s\n", section.Section, section.Cheapest)
}
return nil
}
What it prints
Cod and chips £9.50 1050 kcal
Haddock and chips sold out
Chips £3.50 560 kcal
Mushy peas £1.75 120 kcal
cheapest fish from £9.50
cheapest sides from £1.75
When an override is wrong
sgl vet checks every override against the schema and the query, and reports a mistake as an error. sgl generate reports the same errors and writes no code until they are fixed:
- Q036: a query's
sgl.columnnames a column the query does not return; - Q037: a migration's
sgl.columnnames a table column that does not exist, with a suggestion for the closest one; - Q038: one directive sets both
type:andgo_type:; - Q049: a
go_typenames a type the generator cannot write.
Try it
Rename kcal to calories in ListMenu's directive and run sgl vet to see Q036. Or delete the directive from CheapestPerSection and run sgl generate: Cheapest goes back to a pointer to the inferred type.
Across engines
Only these Go types of CheapestPerSection differ between engines; the rest are the same everywhere.
| PostgreSQL | SQLite | |
|---|---|---|
cheapest column | menu.Price | menu.Pence |
CockroachDB and TimescaleDB run the PostgreSQL version unchanged.
Compare the whole example side by side with SQLite.
Run it locally
From the directory you cloned the Seagull repository into:
cd seagull/examples/postgres && go run ./009_overrides
Its test runs it too, and checks what it prints against output.txt:
cd seagull/examples/postgres && SGL_EXAMPLE_ENGINE=cockroachdb SGL_EXAMPLE_DSN="$DATABASE_URL" go test ./009_overrides
Needs a running CockroachDB server: set DATABASE_URL, or pass -dsn, to the address of an empty database.
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: menu
dialect: postgres
migrations: db/migrations
queries: db/queries
go:
package: store
out: internal/store
driver: database/sql
types:
decimal: string
overrides:
- column: menu_items.status
go: seagull.sh/seagull/examples/postgres/009_overrides/internal/menu.Status
db/migrations/0001_menu.up.sql
-- sgl.column(menu_items.price, go_type: "seagull.sh/seagull/examples/postgres/009_overrides/internal/menu.Price")
CREATE TABLE menu_items (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
section TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('available', 'sold_out')),
price NUMERIC(5, 2) NOT NULL,
nutrition JSONB NOT NULL
);
db/migrations/0001_menu.down.sql
DROP TABLE menu_items;
db/queries/menu.sql
-- sgl.query(AddItem, exec)
INSERT INTO menu_items (name, section, status, price, nutrition) VALUES ($1, $2, $3, $4, $5);
-- sgl.query(ListMenu, many)
-- sgl.column(kcal, type: bigint, nullable: false)
SELECT name, status, price, nutrition ->> 'kcal' AS kcal
FROM menu_items
ORDER BY section, name;
-- sgl.query(CheapestPerSection, many)
-- sgl.column(cheapest, go_type: "seagull.sh/seagull/examples/postgres/009_overrides/internal/menu.Price", nullable: false)
SELECT section, MIN(price) AS cheapest
FROM menu_items
GROUP BY section
ORDER BY section;
example.go
package main
import (
"context"
"database/sql"
"fmt"
"io"
"seagull.sh/seagull/examples/postgres/009_overrides/internal/menu"
"seagull.sh/seagull/examples/postgres/009_overrides/internal/store"
)
// run fills a chip shop menu and reads it back. Five overrides replace the Go types
// Seagull would infer:
//
// - sgl.yaml's types.decimal slot maps every NUMERIC to string, the server's exact
// text, in place of apd.Decimal, so the module needs no decimal package; a slot
// changes models.go and parameters as well as returned columns;
// - sgl.yaml's types.overrides gives menu_items.status the type menu.Status;
// - sgl.column(menu_items.price, go_type: ...) in the migration gives price the type
// menu.Price in every query that returns the column as it is;
// - sgl.column(kcal, type: bigint, nullable: false) in ListMenu types nutrition ->>
// 'kcal', which is nullable text, as the number every document holds; a cast,
// (nutrition ->> 'kcal')::bigint, would type it too, and make the server check it;
// - in CheapestPerSection, MIN(price) is a new value, so the migration's override no
// longer applies and the aggregate could be NULL; sgl.column(cheapest, go_type: ...,
// nullable: false) makes it menu.Price again, as every group has a row.
//
// Column overrides also reach models.go and the parameters bound to those columns, so
// AddItemParams takes a menu.Status and a menu.Price.
//
// sgl vet catches mistakes in overrides: Q036 for a query override naming a column the
// query does not return, Q037 for an override naming a table column that does not exist,
// Q038 for type: and go_type: together, and Q049 for a go_type the generator cannot use.
//
// Takes database (*sql.DB) which is a migrated database.
// Takes output (io.Writer) which receives the menu and the cheapest item per section.
//
// Returns error when a query fails.
func run(ctx context.Context, database *sql.DB, output io.Writer) error {
queries := store.New(database)
items := []store.AddItemParams{
{Name: "Cod and chips", Section: "fish", Status: menu.Available, Price: "9.50", Nutrition: store.JSON(`{"kcal": 1050}`)},
{Name: "Haddock and chips", Section: "fish", Status: menu.SoldOut, Price: "10.50", Nutrition: store.JSON(`{"kcal": 980}`)},
{Name: "Chips", Section: "sides", Status: menu.Available, Price: "3.50", Nutrition: store.JSON(`{"kcal": 560}`)},
{Name: "Mushy peas", Section: "sides", Status: menu.Available, Price: "1.75", Nutrition: store.JSON(`{"kcal": 120}`)},
}
for _, item := range items {
if err := queries.AddItem(ctx, item); err != nil {
return fmt.Errorf("adding %s: %w", item.Name, err)
}
}
listed, err := queries.ListMenu(ctx)
if err != nil {
return fmt.Errorf("listing the menu: %w", err)
}
for _, item := range listed {
if item.Status == menu.SoldOut {
fmt.Fprintf(output, "%-18s sold out\n", item.Name)
continue
}
fmt.Fprintf(output, "%-18s %6s %4d kcal\n", item.Name, item.Price, item.Kcal)
}
cheapest, err := queries.CheapestPerSection(ctx)
if err != nil {
return fmt.Errorf("finding the cheapest items: %w", err)
}
for _, section := range cheapest {
fmt.Fprintf(output, "cheapest %-6s from %s\n", section.Section, section.Cheapest)
}
return nil
}
internal/menu/menu.go
// Package menu holds the chip shop's own types. The generated store returns them in place
// of the inferred strings, through the overrides in sgl.yaml, the migration and a query.
package menu
// Status is whether a menu item can be ordered. The menu_items table's CHECK constraint
// allows the same values.
type Status string
const (
// Available marks an item that can be ordered.
Available Status = "available"
// SoldOut marks an item that has run out for the day.
SoldOut Status = "sold_out"
)
// Price is an amount of sterling as PostgreSQL's NUMERIC(5, 2) writes it, such as 3.50.
// Keeping the server's exact text, rather than a float, keeps prices exact.
type Price string
// String formats the amount in pounds, such as £3.50.
//
// Returns string which is the formatted amount.
func (p Price) String() string {
return "£" + string(p)
}
main.go
// Command 009_overrides applies its migrations to a PostgreSQL database, then runs the
// example in example.go: a chip shop menu is filled and read back as its own Status and
// Price types.
//
// Run it with `go run . -dsn postgres://user:password@localhost:5432/example`, or set
// DATABASE_URL instead of passing -dsn. Give it an empty database of its own, as the
// migrations create its tables and the example adds rows each time it runs.
package main
import (
"context"
"database/sql"
"embed"
"errors"
"flag"
"fmt"
"log"
"os"
_ "github.com/jackc/pgx/v5/stdlib"
"seagull.sh/seagull/sdk/migrate"
"seagull.sh/seagull/sdk/migrate/migrate_backend_postgres"
)
// migrations holds the migration files, compiled into the binary.
//
//go:embed db/migrations
var migrations embed.FS
func main() {
dsn := flag.String("dsn", os.Getenv("DATABASE_URL"), "PostgreSQL connection string; defaults to $DATABASE_URL")
flag.Parse()
if err := execute(context.Background(), *dsn); err != nil {
log.Fatal(err)
}
}
// execute opens and migrates the database at dsn, and runs the example against it.
//
// Takes dsn (string) which is the PostgreSQL connection string.
//
// Returns error when the database cannot be prepared or the example fails.
func execute(ctx context.Context, dsn string) error {
if dsn == "" {
return errors.New("set -dsn or DATABASE_URL to a PostgreSQL connection string")
}
database, err := sql.Open("pgx", dsn)
if err != nil {
return err
}
defer func() { _ = database.Close() }()
migrator, err := migrate.NewMigrator(database, migrate_backend_postgres.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/driver_composites.go
// Code generated by sgl. DO NOT EDIT.
package store
import (
"database/sql"
"database/sql/driver"
"encoding"
"encoding/json"
"fmt"
"reflect"
"strings"
)
func sglScanOverride[T any](dest *T) sql.Scanner {
return sglOverrideScanner[T]{dest: dest}
}
type sglOverrideScanner[T any] struct {
dest *T
}
func (scanner sglOverrideScanner[T]) Scan(source any) error {
target := reflect.ValueOf(scanner.dest).Elem()
if source == nil {
return sglReadDriverNull(target)
}
var read sql.Null[T]
err := read.Scan(source)
if err == nil {
*scanner.dest = read.V
return nil
}
if sglReadOverride(target, source) != nil {
return err
}
return nil
}
func sglReadOverride(target reflect.Value, source any) error {
if target.Kind() == reflect.Pointer {
element := reflect.New(target.Type().Elem())
if err := sglReadOverride(element.Elem(), source); err != nil {
return err
}
target.Set(element)
return nil
}
if read, err := sglReadOverrideText(target, source); read {
return err
}
if target.Kind() == reflect.Bool {
converted, err := driver.Bool.ConvertValue(source)
if err != nil {
return err
}
isTrue, isBool := converted.(bool)
target.SetBool(isBool && isTrue)
return nil
}
return sglReadDriverValue(target, source)
}
func sglReadOverrideText(target reflect.Value, source any) (bool, error) {
var text []byte
switch typed := source.(type) {
case encoding.TextMarshaler:
marshalled, err := typed.MarshalText()
if err != nil {
return false, nil
}
text = marshalled
case string:
text = []byte(typed)
case []byte:
text = typed
default:
return false, nil
}
if unmarshaler, unmarshals := reflect.TypeAssert[encoding.TextUnmarshaler](target.Addr()); unmarshals {
return true, unmarshaler.UnmarshalText(text)
}
switch {
case target.Kind() == reflect.String:
target.SetString(string(text))
return true, nil
case target.Kind() == reflect.Slice && target.Type().Elem().Kind() == reflect.Uint8:
target.SetBytes(append([]byte(nil), text...))
return true, nil
default:
return false, nil
}
}
func sglReadDriverValue(target reflect.Value, source any) error {
if source == nil {
return sglReadDriverNull(target)
}
value := reflect.ValueOf(source)
if value.Type().AssignableTo(target.Type()) {
target.Set(value)
return nil
}
if target.Kind() == reflect.Pointer {
element := reflect.New(target.Type().Elem())
if err := sglReadDriverValue(element.Elem(), source); err != nil {
return err
}
target.Set(element)
return nil
}
if read, err := sglReadDriverLeaf(target, source); read {
return err
}
return sglReadDriverShape(target, value)
}
func sglReadDriverNull(target reflect.Value) error {
switch target.Kind() {
case reflect.Pointer, reflect.Slice, reflect.Map, reflect.Interface:
target.SetZero()
return nil
}
if scanner, scans := reflect.TypeAssert[sql.Scanner](target.Addr()); scans {
return scanner.Scan(nil)
}
return fmt.Errorf("converting NULL to %s is unsupported", target.Type())
}
func sglReadDriverShape(target reflect.Value, value reflect.Value) error {
switch target.Kind() {
case reflect.Slice:
return sglReadList(target, value, sglReadDriverValue)
case reflect.Map:
return sglReadMap(target, value, sglReadDriverValue)
case reflect.Struct:
return sglReadDriverStruct(target, value)
default:
return sglReadScalar(target, value)
}
}
func sglReadDriverLeaf(target reflect.Value, source any) (bool, error) {
address := target.Addr().Interface()
if unmarshaler, ok := address.(json.Unmarshaler); ok {
encoded, err := json.Marshal(source)
if err != nil {
return true, err
}
return true, unmarshaler.UnmarshalJSON(encoded)
}
if scanner, ok := address.(sql.Scanner); ok {
return true, scanner.Scan(source)
}
return false, nil
}
func sglReadDriverStruct(target reflect.Value, value reflect.Value) error {
source := value.Interface()
fields, ok := source.(map[string]any)
if !ok {
return sglUnreadable(value.Type(), target.Type())
}
target.SetZero()
structType := target.Type()
for index := range structType.NumField() {
name, readable := sglDriverFieldName(structType.Field(index))
if !readable {
continue
}
element, found := sglDriverField(fields, name)
if !found {
continue
}
if err := sglReadDriverValue(target.Field(index), element); err != nil {
return fmt.Errorf("field %s: %w", name, err)
}
}
return nil
}
func sglDriverFieldName(field reflect.StructField) (string, bool) {
if !field.IsExported() {
return "", false
}
name, _, _ := strings.Cut(field.Tag.Get("json"), ",")
switch name {
case "-":
return "", false
case "":
return field.Name, true
default:
return name, true
}
}
func sglDriverField(fields map[string]any, name string) (any, bool) {
if element, found := fields[name]; found {
return element, true
}
for key, element := range fields {
if strings.EqualFold(key, name) {
return element, true
}
}
return nil, false
}
internal/store/menu.sql.go
// Code generated by sgl. DO NOT EDIT.
package store
import (
"context"
"seagull.sh/seagull/examples/postgres/009_overrides/internal/menu"
)
const addItemSQL = `INSERT INTO menu_items (name, section, status, price, nutrition) VALUES ($1, $2, $3, $4, $5);`
type AddItemParams struct {
Name string
Section string
Status menu.Status
Price menu.Price
Nutrition JSON
}
func (queries *Queries) AddItem(ctx context.Context, params AddItemParams) error {
_, err := queries.writer.ExecContext(ctx, addItemSQL, params.Name, params.Section, params.Status, params.Price, params.Nutrition)
return err
}
const cheapestPerSectionSQL = `SELECT section, MIN(price) AS cheapest
FROM menu_items
GROUP BY section
ORDER BY section;`
type CheapestPerSectionRow struct {
Section string `json:"section"`
Cheapest menu.Price `json:"cheapest"`
}
func (queries *Queries) CheapestPerSection(ctx context.Context) ([]CheapestPerSectionRow, error) {
rows, err := queries.reader.QueryContext(ctx, cheapestPerSectionSQL)
if err != nil {
return nil, err
}
defer rows.Close()
var results []CheapestPerSectionRow
for rows.Next() {
var row CheapestPerSectionRow
if err := rows.Scan(&row.Section, &row.Cheapest); err != nil {
return nil, err
}
results = append(results, row)
}
if err := rows.Err(); err != nil {
return nil, err
}
return results, nil
}
const listMenuSQL = `SELECT name, status, price, nutrition ->> 'kcal' AS kcal
FROM menu_items
ORDER BY section, name;`
type ListMenuRow struct {
Name string `json:"name"`
Status menu.Status `json:"status"`
Price menu.Price `json:"price"`
Kcal int64 `json:"kcal"`
}
func (queries *Queries) ListMenu(ctx context.Context) ([]ListMenuRow, error) {
rows, err := queries.reader.QueryContext(ctx, listMenuSQL)
if err != nil {
return nil, err
}
defer rows.Close()
var results []ListMenuRow
for rows.Next() {
var row ListMenuRow
if err := rows.Scan(&row.Name, sglScanOverride(&row.Status), &row.Price, &row.Kcal); err != nil {
return nil, err
}
results = append(results, row)
}
if err := rows.Err(); err != nil {
return nil, err
}
return results, nil
}
internal/store/models.go
// Code generated by sgl. DO NOT EDIT.
package store
import "seagull.sh/seagull/examples/postgres/009_overrides/internal/menu"
type MenuItems struct {
ID int64 `json:"id"`
Name string `json:"name"`
Section string `json:"section"`
Status menu.Status `json:"status"`
Price menu.Price `json:"price"`
Nutrition JSON `json:"nutrition"`
}
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()
}
internal/store/sgl_json.go
// Code generated by sgl. DO NOT EDIT.
package store
import (
"database/sql/driver"
"encoding/json"
"fmt"
)
type JSON []byte
func (j *JSON) Scan(source any) error {
switch value := source.(type) {
case nil:
*j = nil
return nil
case []byte:
clone := make(JSON, len(value))
copy(clone, value)
*j = clone
return nil
case string:
*j = JSON(value)
return nil
default:
encoded, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("sgl: encode %T to JSON: %w", source, err)
}
*j = encoded
return nil
}
}
func (j JSON) Value() (driver.Value, error) {
if j == nil {
return nil, nil
}
return string(j), nil
}
func (j JSON) MarshalJSON() ([]byte, error) {
if j == nil {
return []byte("null"), nil
}
return j, nil
}
func (j *JSON) UnmarshalJSON(data []byte) error {
clone := make(JSON, len(data))
copy(clone, data)
*j = clone
return nil
}
internal/store/value_reading.go
// Code generated by sgl. DO NOT EDIT.
package store
import (
"fmt"
"reflect"
)
type sglOrderedMap interface {
Keys() []any
Values() []any
}
func sglReadList(target reflect.Value, value reflect.Value, read func(reflect.Value, any) error) error {
if value.Kind() != reflect.Slice && value.Kind() != reflect.Array {
return sglUnreadable(value.Type(), target.Type())
}
if value.Kind() == reflect.Slice && value.IsNil() {
target.SetZero()
return nil
}
list := reflect.MakeSlice(target.Type(), value.Len(), value.Len())
for index := range value.Len() {
if err := read(list.Index(index), value.Index(index).Interface()); err != nil {
return fmt.Errorf("element %d: %w", index, err)
}
}
target.Set(list)
return nil
}
func sglReadMap(target reflect.Value, value reflect.Value, read func(reflect.Value, any) error) error {
if value.Kind() == reflect.Map && value.IsNil() {
target.SetZero()
return nil
}
keys, values, ok := sglMapEntries(value)
if !ok {
return sglUnreadable(value.Type(), target.Type())
}
converted := reflect.MakeMapWithSize(target.Type(), len(keys))
for index, key := range keys {
convertedKey := reflect.New(target.Type().Key()).Elem()
if err := read(convertedKey, key); err != nil {
return fmt.Errorf("key %v: %w", key, err)
}
element := reflect.New(target.Type().Elem()).Elem()
if err := read(element, values[index]); err != nil {
return fmt.Errorf("value of key %v: %w", key, err)
}
converted.SetMapIndex(convertedKey, element)
}
target.Set(converted)
return nil
}
func sglMapEntries(value reflect.Value) (keys []any, values []any, ok bool) {
if value.Kind() == reflect.Map {
keys = make([]any, 0, value.Len())
values = make([]any, 0, value.Len())
entries := value.MapRange()
for entries.Next() {
keys = append(keys, entries.Key().Interface())
values = append(values, entries.Value().Interface())
}
return keys, values, true
}
pointer := reflect.New(value.Type())
pointer.Elem().Set(value)
ordered, ordersEntries := reflect.TypeAssert[sglOrderedMap](pointer)
if !ordersEntries {
return nil, nil, false
}
keys, values = ordered.Keys(), ordered.Values()
return keys, values, len(keys) == len(values)
}
func sglReadScalar(target reflect.Value, value reflect.Value) error {
kind := sglScalarKind(value.Kind())
if kind == "" || kind != sglScalarKind(target.Kind()) || !value.CanConvert(target.Type()) {
return sglUnreadable(value.Type(), target.Type())
}
converted := value.Convert(target.Type())
if !sglScalarHolds(converted, value) {
return fmt.Errorf("%v overflows %s", value.Interface(), target.Type())
}
target.Set(converted)
return nil
}
func sglScalarHolds(converted reflect.Value, value reflect.Value) bool {
switch {
case !value.CanInt() && !value.CanUint():
return true
case !converted.Convert(value.Type()).Equal(value):
return false
case value.CanInt() && converted.CanUint():
return value.Int() >= 0
case value.CanUint() && converted.CanInt():
return converted.Int() >= 0
default:
return true
}
}
func sglScalarKind(kind reflect.Kind) string {
switch kind {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return "integer"
case reflect.Float32, reflect.Float64:
return "float"
case reflect.String:
return "string"
case reflect.Bool:
return "boolean"
default:
return ""
}
}
func sglUnreadable(source reflect.Type, target reflect.Type) error {
return fmt.Errorf("cannot read %s into %s", source, target)
}
output.txt
Cod and chips £9.50 1050 kcal
Haddock and chips sold out
Chips £3.50 560 kcal
Mushy peas £1.75 120 kcal
cheapest fish from £9.50
cheapest sides from £1.75