Type overrides

On this page

On SQLite:

  • SQLite has no enum type, so status is TEXT with a CHECK constraint and a go_type override
  • Money is INTEGER pence with a go_type, as SQLite has no exact decimal type
  • json_extract is typed any until sgl.column(kcal, type: integer) pins it
  • The example also imports its own internal/menu package

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; SQLite has no enum type, so the column is TEXT with a CHECK constraint, and the Go type carries the allowed values. Pence is an int64 type whose String method prints an amount such as £3.50, which keeps money exact in an INTEGER column.

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: sqlite
    migrations: db/migrations
    queries: db/queries
    go:
      package: store
      out: internal/store
      types:
        overrides:
          - column: menu_items.status
            go: seagull.sh/seagull/examples/sqlite/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.

In a migration

db/migrations/0001_menu.up.sql

-- sgl.column(menu_items.price_pence, go_type: "seagull.sh/seagull/examples/sqlite/009_overrides/internal/menu.Pence")
CREATE TABLE menu_items (
    id          INTEGER PRIMARY KEY,
    name        TEXT    NOT NULL,
    section     TEXT    NOT NULL,
    status      TEXT    NOT NULL CHECK (status IN ('available', 'sold_out')),
    price_pence INTEGER NOT NULL,
    nutrition   TEXT    NOT NULL
);

The sgl.column(menu_items.price_pence, go_type: ...) line above the CREATE TABLE gives price_pence the type menu.Pence 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: integer)
SELECT name, status, price_pence, json_extract(nutrition, '$.kcal') AS kcal
FROM menu_items
ORDER BY section, name;

json_extract returns whatever the document holds at that path, so Seagull types it any. sgl.column(kcal, type: integer) overrides its SQL type instead of its Go type: 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.

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"`
	PricePence menu.Pence  `json:"price_pence"`
	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.PricePence, &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.Pence 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/sqlite/009_overrides/internal/menu.Pence", nullable: false)
SELECT section, MIN(price_pence) AS cheapest
FROM menu_items
GROUP BY section
ORDER BY section;

internal/store

type CheapestPerSectionRow struct {
	Section  string     `json:"section"`
	Cheapest menu.Pence `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 an int64 price, so the program converts on the way in with string(menu.Available).

Using it

example.go

package main

import (
	"context"
	"database/sql"
	"fmt"
	"io"

	"seagull.sh/seagull/examples/sqlite/009_overrides/internal/menu"
	"seagull.sh/seagull/examples/sqlite/009_overrides/internal/store"
)

// run fills a chip shop menu and reads it back. Four overrides replace the Go types
// Seagull would infer:
//
//   - sgl.yaml's types.overrides gives menu_items.status the type menu.Status;
//   - sgl.column(menu_items.price_pence, go_type: ...) in the migration gives price_pence
//     the type menu.Pence in every query that returns the column as it is;
//   - sgl.column(kcal, type: integer) in ListMenu types json_extract, which is any by
//     default because its type depends on the document;
//   - in CheapestPerSection, MIN(price_pence) 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.Pence 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.Pence.
//
// 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, PricePence: 950, Nutrition: `{"kcal": 1050}`},
		{Name: "Haddock and chips", Section: "fish", Status: menu.SoldOut, PricePence: 1050, Nutrition: `{"kcal": 980}`},
		{Name: "Chips", Section: "sides", Status: menu.Available, PricePence: 350, Nutrition: `{"kcal": 560}`},
		{Name: "Mushy peas", Section: "sides", Status: menu.Available, PricePence: 175, Nutrition: `{"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.PricePence, 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.

SQLitePostgreSQL
cheapest columnmenu.Pencemenu.Price

CockroachDB and TimescaleDB run the PostgreSQL version unchanged.

Compare the whole example side by side with PostgreSQL.

Run it locally

From the directory you cloned the Seagull repository into:

cd seagull/examples/sqlite && go run ./009_overrides

Its test runs it too, and checks what it prints against output.txt:

cd seagull/examples/sqlite && go test ./009_overrides

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: sqlite
    migrations: db/migrations
    queries: db/queries
    go:
      package: store
      out: internal/store
      types:
        overrides:
          - column: menu_items.status
            go: seagull.sh/seagull/examples/sqlite/009_overrides/internal/menu.Status

db/migrations/0001_menu.up.sql

-- sgl.column(menu_items.price_pence, go_type: "seagull.sh/seagull/examples/sqlite/009_overrides/internal/menu.Pence")
CREATE TABLE menu_items (
    id          INTEGER PRIMARY KEY,
    name        TEXT    NOT NULL,
    section     TEXT    NOT NULL,
    status      TEXT    NOT NULL CHECK (status IN ('available', 'sold_out')),
    price_pence INTEGER NOT NULL,
    nutrition   TEXT    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_pence, nutrition) VALUES (?, ?, ?, ?, ?);

-- sgl.query(ListMenu, many)
-- sgl.column(kcal, type: integer)
SELECT name, status, price_pence, json_extract(nutrition, '$.kcal') AS kcal
FROM menu_items
ORDER BY section, name;

-- sgl.query(CheapestPerSection, many)
-- sgl.column(cheapest, go_type: "seagull.sh/seagull/examples/sqlite/009_overrides/internal/menu.Pence", nullable: false)
SELECT section, MIN(price_pence) 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/sqlite/009_overrides/internal/menu"
	"seagull.sh/seagull/examples/sqlite/009_overrides/internal/store"
)

// run fills a chip shop menu and reads it back. Four overrides replace the Go types
// Seagull would infer:
//
//   - sgl.yaml's types.overrides gives menu_items.status the type menu.Status;
//   - sgl.column(menu_items.price_pence, go_type: ...) in the migration gives price_pence
//     the type menu.Pence in every query that returns the column as it is;
//   - sgl.column(kcal, type: integer) in ListMenu types json_extract, which is any by
//     default because its type depends on the document;
//   - in CheapestPerSection, MIN(price_pence) 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.Pence 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.Pence.
//
// 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, PricePence: 950, Nutrition: `{"kcal": 1050}`},
		{Name: "Haddock and chips", Section: "fish", Status: menu.SoldOut, PricePence: 1050, Nutrition: `{"kcal": 980}`},
		{Name: "Chips", Section: "sides", Status: menu.Available, PricePence: 350, Nutrition: `{"kcal": 560}`},
		{Name: "Mushy peas", Section: "sides", Status: menu.Available, PricePence: 175, Nutrition: `{"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.PricePence, 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 string and int64, through the overrides in sgl.yaml and the migration.
package menu

import "fmt"

// Status is whether a menu item can be ordered. The menu_items table's CHECK constraint
// allows the same values, since SQLite has no enum type.
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"
)

// Pence is an amount of sterling in pence, which keeps prices exact in an INTEGER column.
type Pence int64

// String formats the amount in pounds, such as £3.50.
//
// Returns string which is the formatted amount.
func (p Pence) String() string {
	return fmt.Sprintf("£%d.%02d", p/100, p%100)
}

main.go

// Command 009_overrides applies its migrations to a SQLite database, then runs the
// example in example.go: a chip shop menu is read back with its own Status and Pence
// types, chosen by a column override in sgl.yaml and sgl.column directives.
//
// Run it with `go run .`, or `go run . -db menu.db` to keep the database.
package main

import (
	"context"
	"database/sql"
	"embed"
	"flag"
	"fmt"
	"log"
	"os"
	"path/filepath"

	_ "modernc.org/sqlite"

	"seagull.sh/seagull/sdk/migrate"
	"seagull.sh/seagull/sdk/migrate/migrate_backend_sqlite"
)

// migrations holds the migration files, compiled into the binary.
//
//go:embed db/migrations
var migrations embed.FS

func main() {
	path := flag.String("db", "", "SQLite database file; a temporary one when empty")
	flag.Parse()
	if err := execute(context.Background(), *path); err != nil {
		log.Fatal(err)
	}
}

// execute opens and migrates the database at path, or a temporary 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 {
	if path == "" {
		directory, err := os.MkdirTemp("", "seagull-example-")
		if err != nil {
			return err
		}
		defer func() { _ = os.RemoveAll(directory) }()
		path = filepath.Join(directory, "example.db")
	}
	database, err := sql.Open("sqlite", "file:"+path)
	if err != nil {
		return err
	}
	defer func() { _ = database.Close() }()

	migrator, err := migrate.NewMigrator(database, migrate_backend_sqlite.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/driver_values.go

// Code generated by sgl. DO NOT EDIT.

package store

import "database/sql/driver"

type sglDriverText struct {
	value any
}

func (wrapped sglDriverText) Value() (driver.Value, error) {
	converted, err := driver.DefaultParameterConverter.ConvertValue(wrapped.value)
	if text, isBytes := converted.([]byte); isBytes && err == nil {
		return string(text), nil
	}
	return converted, err
}

internal/store/menu.sql.go

// Code generated by sgl. DO NOT EDIT.

package store

import (
	"context"

	"seagull.sh/seagull/examples/sqlite/009_overrides/internal/menu"
)

const addItemSQL = `INSERT INTO menu_items (name, section, status, price_pence, nutrition) VALUES (?1, ?2, ?3, ?4, ?5);`

type AddItemParams struct {
	Name       string
	Section    string
	Status     menu.Status
	PricePence menu.Pence
	Nutrition  string
}

func (queries *Queries) AddItem(ctx context.Context, params AddItemParams) error {
	_, err := queries.writer.ExecContext(ctx, addItemSQL, params.Name, params.Section, sglDriverText{value: params.Status}, params.PricePence, params.Nutrition)
	return err
}

const cheapestPerSectionSQL = `SELECT section, MIN(price_pence) AS cheapest
FROM menu_items
GROUP BY section
ORDER BY section;`

type CheapestPerSectionRow struct {
	Section  string     `json:"section"`
	Cheapest menu.Pence `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_pence, json_extract(nutrition, '$.kcal') AS kcal
FROM menu_items
ORDER BY section, name;`

type ListMenuRow struct {
	Name       string      `json:"name"`
	Status     menu.Status `json:"status"`
	PricePence menu.Pence  `json:"price_pence"`
	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.PricePence, &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/sqlite/009_overrides/internal/menu"

type MenuItems struct {
	ID         int64       `json:"id"`
	Name       string      `json:"name"`
	Section    string      `json:"section"`
	Status     menu.Status `json:"status"`
	PricePence menu.Pence  `json:"price_pence"`
	Nutrition  string      `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/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