Type overrides

On this page

The same on TimescaleDB. TimescaleDB runs this example unchanged: the SQL and the Go that sgl generates are identical to the PostgreSQL version. Only dialect: in sgl.yaml changes.

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 TimescaleDB 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.column names a column the query does not return;
  • Q037: a migration's sgl.column names a table column that does not exist, with a suggestion for the closest one;
  • Q038: one directive sets both type: and go_type:;
  • Q049: a go_type names 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.

PostgreSQLSQLite
cheapest columnmenu.Pricemenu.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=timescaledb SGL_EXAMPLE_DSN="$DATABASE_URL" go test ./009_overrides

Needs a running TimescaleDB 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

Also on other engines