Seeds, namespaces and hooks
On this page
On PostgreSQL:
- CREATE INDEX CONCURRENTLY cannot run inside a transaction; Seagull detects CONCURRENTLY by itself, and the migration also carries sgl.migration(no_transaction: true) to say so
- IF NOT EXISTS keeps the no-transaction migration safe to retry if it ran but was not recorded
- WithNamespace renames the advisory lock keys as well as the history tables
- WithLogger routes Seagull's own warnings, such as an edited seed file, to your slog.Logger
- the seed file runs whole, since pgx's stdlib driver accepts several statements in one call
- sgl.yaml sets migrate.namespace and migrate.dsn, so sgl migrate and sgl seed use the same tables
Beyond applying migrations, the migrator has a few features for real deployments. A namespace keeps one program's migration history apart from another's in the same database. Hooks run your own code before a run and after each migration. Some statements cannot run inside a transaction, and a directive tells the migrator so. A seeder applies data files once each, so reference data can ship with the program. The example sets up the menu of a chip shop with all four.
Namespaces
The migrator records what it has applied in a history table, and the seeder in a seed table. Both are named from a namespace, sgl by default. migrate.WithNamespace("chippy") renames them to chippy_migrations and chippy_seeds, and renames the advisory lock keys as well, so several programs, or several modules of one program, can each keep their own history in one database without seeing each other's.
sgl.yaml sets migrate.namespace to match, so sgl migrate and sgl seed on the command line use the same tables as the program:
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: chippy
dialect: postgres
migrations: db/migrations
queries: db/queries
seeds: db/seeds
go:
package: store
out: internal/store
driver: database/sql
migrate:
dsn: ${env:DATABASE_URL}
namespace: chippy
To show the histories are separate, the program also reads the status of the same files against the default namespace's sgl_migrations, and finds nothing applied there.
Hooks
Hooks are options to NewMigrator. migrate.WithBeforeRun is called once before a run with the pending versions, and migrate.WithAfterMigration after each migration it applies. The example only prints from them, but a hook could record metrics, notify someone, or check a precondition: returning an error from a hook stops the run.
migrate.WithLogger sends Seagull's own log records, such as its warnings, to a *slog.Logger of your choosing instead of slog.Default(). The example's logger writes to the output without the time, so the output is the same on every run.
A migration outside a transaction
Each migration normally runs in a transaction, so it applies whole or not at all. Some statements refuse to run inside one.
PostgreSQL will not build an index CONCURRENTLY inside a transaction. Seagull spots CONCURRENTLY by itself, and the migration carries sgl.migration(no_transaction: true) as well, to make it plain to the reader:
db/migrations/0002_menu_price_index.up.sql
-- sgl.migration(no_transaction: true)
CREATE INDEX CONCURRENTLY IF NOT EXISTS menu_price_idx ON menu (price_pence);
Without a transaction, a crash could leave the index built but the migration unrecorded. IF NOT EXISTS lets the retry pass in that case.
Keep a statement like this in a migration of its own. If it shares a file with other statements, the whole file has to run without a transaction and is no longer atomic, and sgl vet warns with Q043. The migrations guide has more.
Seeds
Seeds are .sql files of data, in the directory seeds: names in sgl.yaml. migrate.NewSeeder is built like a migrator, and Apply runs each seed file that has not been applied, records it in the seed table, and returns how many it ran. Calling it at every start-up is safe: the second call finds nothing to do.
db/seeds/0001_menu.sql
INSERT INTO menu (dish, price_pence) VALUES
('cod and chips', 950),
('haddock and chips', 1020),
('mushy peas', 250)
ON CONFLICT (dish) DO NOTHING;
The file runs whole, since the driver accepts several statements in one call. It also says ON CONFLICT (dish) DO NOTHING, which keeps it harmless if the seed history is ever cleared and it runs again.
A seed runs once, so editing it later does not apply the change. The program shows this with a copy of the seed that has gained a pickled egg: Apply runs nothing, and the seeder logs a warning through the logger, naming both checksums. Where a changed migration stops the migrator, as in the migration lifecycle, a changed seed only warns. Put new data in a new seed file.
Using it
example.go
package main
import (
"context"
"database/sql"
"fmt"
"io"
"io/fs"
"log/slog"
"testing/fstest"
"seagull.sh/seagull/examples/postgres/029_seeds_namespaces_hooks/internal/store"
"seagull.sh/seagull/sdk/migrate"
"seagull.sh/seagull/sdk/migrate/migrate_backend_postgres"
)
const (
// namespace prefixes the tables that record this program's migrations and seeds.
namespace = "chippy"
// seedsDirectory is where the seed files sit inside the file system.
seedsDirectory = "db/seeds"
)
// run migrates the database under its own namespace with hooks that report progress,
// applies the seed files twice to show they run once, prints the seeded menu, and shows
// the warning Seagull logs when a seed file changes after it was applied.
//
// Takes database (*sql.DB) which is an empty database.
// Takes files (fs.FS) which holds db/migrations and db/seeds, the embed.FS in main.go.
// Takes output (io.Writer) which receives each step's result.
//
// Returns error when migrating, seeding or a query fails.
func run(ctx context.Context, database *sql.DB, files fs.FS, output io.Writer) error {
// WithNamespace renames every table and lock the migrator and seeder keep, so several
// programs or modules can each keep their own history in one database. sgl.yaml sets
// migrate.namespace to match, so `sgl migrate` uses the same tables.
backend := migrate_backend_postgres.New(migrate.WithNamespace(namespace))
names := backend.Names()
fmt.Fprintf(output, "history tables: %s and %s\n", names.History, names.Seeds)
// WithLogger sends Seagull's own log records, such as its warnings, to a logger of your
// choosing instead of slog.Default(). This one writes to the output and leaves out the
// time so the output stays the same from run to run.
logger := slog.New(slog.NewTextHandler(output, &slog.HandlerOptions{ReplaceAttr: withoutTime}))
// Hooks run around the migrations: before the run with the pending versions, and after
// each migration. Returning an error from a hook stops the run.
migrator, err := migrate.NewMigrator(database, backend, files, "db/migrations",
migrate.WithLogger(logger),
migrate.WithBeforeRun(func(_ context.Context, pending migrate.MigrationRunHookContext) error {
fmt.Fprintf(output, "migrating: %d pending, versions %v\n", pending.PendingCount, pending.PendingVersions)
return nil
}),
migrate.WithAfterMigration(func(_ context.Context, migration migrate.MigrationHookContext) error {
fmt.Fprintf(output, " applied %04d %s\n", migration.Version, migration.Name)
return nil
}),
)
if err != nil {
return err
}
if _, err := migrator.Up(ctx); err != nil {
return fmt.Errorf("migrating: %w", err)
}
// Migration 0002 built its index outside a transaction, as CONCURRENTLY requires.
var valid bool
if err := database.QueryRowContext(ctx, "SELECT indisvalid FROM pg_index WHERE indexrelid = 'menu_price_idx'::regclass").Scan(&valid); err != nil {
return fmt.Errorf("checking the index: %w", err)
}
fmt.Fprintf(output, "menu_price_idx built concurrently, valid=%t\n", valid)
// The default namespace is a separate history in the same database, and it has recorded
// nothing.
if err := printOtherHistory(ctx, database, files, output); err != nil {
return err
}
// A Seeder applies each seed file once and records it in the seed table, so calling
// Apply at every start-up is safe. The seed file also says ON CONFLICT DO NOTHING, which
// keeps it harmless if the seed history is ever cleared.
if err := seed(ctx, database, backend, files, logger, output); err != nil {
return err
}
menu, err := store.New(database).ListMenu(ctx)
if err != nil {
return fmt.Errorf("listing the menu: %w", err)
}
for _, dish := range menu {
fmt.Fprintf(output, " %-17s %dp\n", dish.Dish, dish.PricePence)
}
return seedEdited(ctx, database, backend, files, logger, output)
}
// seed applies the seed files twice; the second time finds nothing to do.
//
// Takes database (*sql.DB) which is the migrated database.
// Takes backend (migrate.Backend) which names the seed table.
// Takes files (fs.FS) which holds the seed files.
// Takes logger (*slog.Logger) which receives Seagull's log records.
// Takes output (io.Writer) which receives the counts.
//
// Returns error when a seed fails.
func seed(ctx context.Context, database *sql.DB, backend migrate.Backend, files fs.FS, logger *slog.Logger, output io.Writer) error {
seeder, err := migrate.NewSeeder(database, backend, files, seedsDirectory, migrate.WithLogger(logger))
if err != nil {
return err
}
for range 2 {
seeded, err := seeder.Apply(ctx)
if err != nil {
return fmt.Errorf("seeding: %w", err)
}
fmt.Fprintf(output, "seeds applied: %d\n", seeded)
}
return nil
}
// seedEdited applies a copy of the seed files in which the applied seed has gained a
// dish. A seed runs once, so the change is not applied; the Seeder logs a warning through
// the logger instead, naming both checksums.
//
// Takes database (*sql.DB) which is the seeded database.
// Takes backend (migrate.Backend) which names the seed table.
// Takes files (fs.FS) which holds the seed files.
// Takes logger (*slog.Logger) which receives the warning.
// Takes output (io.Writer) which receives the count.
//
// Returns error when the copy cannot be made or the seeder fails.
func seedEdited(ctx context.Context, database *sql.DB, backend migrate.Backend, files fs.FS, logger *slog.Logger, output io.Writer) error {
original, err := fs.ReadFile(files, seedsDirectory+"/0001_menu.sql")
if err != nil {
return err
}
edited := fstest.MapFS{
seedsDirectory + "/0001_menu.sql": {Data: append(original, "INSERT INTO menu (dish, price_pence) VALUES ('pickled egg', 120);\n"...)},
}
seeder, err := migrate.NewSeeder(database, backend, edited, seedsDirectory, migrate.WithLogger(logger))
if err != nil {
return err
}
seeded, err := seeder.Apply(ctx)
if err != nil {
return fmt.Errorf("seeding the edited file: %w", err)
}
fmt.Fprintf(output, "seeds applied after editing 0001_menu.sql: %d\n", seeded)
return nil
}
// printOtherHistory reads the same migration files against the default namespace's
// history, which this program never wrote to.
//
// Takes database (*sql.DB) which is the migrated database.
// Takes files (fs.FS) which holds the migration files.
// Takes output (io.Writer) which receives the count.
//
// Returns error when the status cannot be read.
func printOtherHistory(ctx context.Context, database *sql.DB, files fs.FS, output io.Writer) error {
backend := migrate_backend_postgres.New()
migrator, err := migrate.NewMigrator(database, backend, files, "db/migrations")
if err != nil {
return err
}
statuses, err := migrator.Status(ctx)
if err != nil {
return fmt.Errorf("reading the default history: %w", err)
}
applied := 0
for _, status := range statuses {
if status.Applied {
applied++
}
}
fmt.Fprintf(output, "%s (default namespace): %d of %d migrations applied\n", backend.Names().History, applied, len(statuses))
return nil
}
// withoutTime drops the time from each log record, so the output is the same on every
// run.
//
// Takes groups ([]string) which name the groups the attribute is in.
// Takes attribute (slog.Attr) which is the attribute to keep or drop.
//
// Returns slog.Attr which is the attribute, or an empty one for the time.
func withoutTime(groups []string, attribute slog.Attr) slog.Attr {
if len(groups) == 0 && attribute.Key == slog.TimeKey {
return slog.Attr{}
}
return attribute
}
What it prints
The hooks report the two pending migrations and each one as it is applied, and the index is valid. The default namespace has recorded nothing, the second Apply runs no seeds, and the edited seed produces a warning instead of a change.
history tables: chippy_migrations and chippy_seeds
migrating: 2 pending, versions [1 2]
applied 0001 menu
applied 0002 menu_price_index
menu_price_idx built concurrently, valid=true
sgl_migrations (default namespace): 0 of 2 migrations applied
seeds applied: 1
seeds applied: 0
cod and chips 950p
haddock and chips 1020p
mushy peas 250p
level=WARN msg="seed file checksum changed since last application" version=1 name=menu applied_checksum=0fe49fb6264686ee639a2b35cf19026da57e5a5f0e2bcdcecf4035b8d98ba635 file_checksum=41505f6d893d13901c29bd1badc3a62675bedbd0daabc516763951970d7b798b
seeds applied after editing 0001_menu.sql: 0
Try it
Add a CREATE TABLE to the end of migration 0002 and run sgl vet. It reports Q043: the new table would be created outside a transaction too, and a failure between the two statements would leave the migration half-applied.
Run it locally
From the directory you cloned the Seagull repository into:
cd seagull/examples/postgres && go run ./029_seeds_namespaces_hooks
Its test runs it too, and checks what it prints against output.txt:
cd seagull/examples/postgres && SGL_EXAMPLE_DSN="$DATABASE_URL" go test ./029_seeds_namespaces_hooks
Needs a running PostgreSQL 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: chippy
dialect: postgres
migrations: db/migrations
queries: db/queries
seeds: db/seeds
go:
package: store
out: internal/store
driver: database/sql
migrate:
dsn: ${env:DATABASE_URL}
namespace: chippy
db/migrations/0001_menu.up.sql
CREATE TABLE menu (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
dish TEXT NOT NULL UNIQUE,
price_pence INTEGER NOT NULL
);
db/migrations/0002_menu_price_index.up.sql
-- sgl.migration(no_transaction: true)
CREATE INDEX CONCURRENTLY IF NOT EXISTS menu_price_idx ON menu (price_pence);
db/migrations/0001_menu.down.sql
DROP TABLE menu;
db/migrations/0002_menu_price_index.down.sql
DROP INDEX CONCURRENTLY IF EXISTS menu_price_idx;
db/queries/menu.sql
-- sgl.query(ListMenu, many)
SELECT dish, price_pence FROM menu ORDER BY dish;
db/seeds/0001_menu.sql
INSERT INTO menu (dish, price_pence) VALUES
('cod and chips', 950),
('haddock and chips', 1020),
('mushy peas', 250)
ON CONFLICT (dish) DO NOTHING;
example.go
package main
import (
"context"
"database/sql"
"fmt"
"io"
"io/fs"
"log/slog"
"testing/fstest"
"seagull.sh/seagull/examples/postgres/029_seeds_namespaces_hooks/internal/store"
"seagull.sh/seagull/sdk/migrate"
"seagull.sh/seagull/sdk/migrate/migrate_backend_postgres"
)
const (
// namespace prefixes the tables that record this program's migrations and seeds.
namespace = "chippy"
// seedsDirectory is where the seed files sit inside the file system.
seedsDirectory = "db/seeds"
)
// run migrates the database under its own namespace with hooks that report progress,
// applies the seed files twice to show they run once, prints the seeded menu, and shows
// the warning Seagull logs when a seed file changes after it was applied.
//
// Takes database (*sql.DB) which is an empty database.
// Takes files (fs.FS) which holds db/migrations and db/seeds, the embed.FS in main.go.
// Takes output (io.Writer) which receives each step's result.
//
// Returns error when migrating, seeding or a query fails.
func run(ctx context.Context, database *sql.DB, files fs.FS, output io.Writer) error {
// WithNamespace renames every table and lock the migrator and seeder keep, so several
// programs or modules can each keep their own history in one database. sgl.yaml sets
// migrate.namespace to match, so `sgl migrate` uses the same tables.
backend := migrate_backend_postgres.New(migrate.WithNamespace(namespace))
names := backend.Names()
fmt.Fprintf(output, "history tables: %s and %s\n", names.History, names.Seeds)
// WithLogger sends Seagull's own log records, such as its warnings, to a logger of your
// choosing instead of slog.Default(). This one writes to the output and leaves out the
// time so the output stays the same from run to run.
logger := slog.New(slog.NewTextHandler(output, &slog.HandlerOptions{ReplaceAttr: withoutTime}))
// Hooks run around the migrations: before the run with the pending versions, and after
// each migration. Returning an error from a hook stops the run.
migrator, err := migrate.NewMigrator(database, backend, files, "db/migrations",
migrate.WithLogger(logger),
migrate.WithBeforeRun(func(_ context.Context, pending migrate.MigrationRunHookContext) error {
fmt.Fprintf(output, "migrating: %d pending, versions %v\n", pending.PendingCount, pending.PendingVersions)
return nil
}),
migrate.WithAfterMigration(func(_ context.Context, migration migrate.MigrationHookContext) error {
fmt.Fprintf(output, " applied %04d %s\n", migration.Version, migration.Name)
return nil
}),
)
if err != nil {
return err
}
if _, err := migrator.Up(ctx); err != nil {
return fmt.Errorf("migrating: %w", err)
}
// Migration 0002 built its index outside a transaction, as CONCURRENTLY requires.
var valid bool
if err := database.QueryRowContext(ctx, "SELECT indisvalid FROM pg_index WHERE indexrelid = 'menu_price_idx'::regclass").Scan(&valid); err != nil {
return fmt.Errorf("checking the index: %w", err)
}
fmt.Fprintf(output, "menu_price_idx built concurrently, valid=%t\n", valid)
// The default namespace is a separate history in the same database, and it has recorded
// nothing.
if err := printOtherHistory(ctx, database, files, output); err != nil {
return err
}
// A Seeder applies each seed file once and records it in the seed table, so calling
// Apply at every start-up is safe. The seed file also says ON CONFLICT DO NOTHING, which
// keeps it harmless if the seed history is ever cleared.
if err := seed(ctx, database, backend, files, logger, output); err != nil {
return err
}
menu, err := store.New(database).ListMenu(ctx)
if err != nil {
return fmt.Errorf("listing the menu: %w", err)
}
for _, dish := range menu {
fmt.Fprintf(output, " %-17s %dp\n", dish.Dish, dish.PricePence)
}
return seedEdited(ctx, database, backend, files, logger, output)
}
// seed applies the seed files twice; the second time finds nothing to do.
//
// Takes database (*sql.DB) which is the migrated database.
// Takes backend (migrate.Backend) which names the seed table.
// Takes files (fs.FS) which holds the seed files.
// Takes logger (*slog.Logger) which receives Seagull's log records.
// Takes output (io.Writer) which receives the counts.
//
// Returns error when a seed fails.
func seed(ctx context.Context, database *sql.DB, backend migrate.Backend, files fs.FS, logger *slog.Logger, output io.Writer) error {
seeder, err := migrate.NewSeeder(database, backend, files, seedsDirectory, migrate.WithLogger(logger))
if err != nil {
return err
}
for range 2 {
seeded, err := seeder.Apply(ctx)
if err != nil {
return fmt.Errorf("seeding: %w", err)
}
fmt.Fprintf(output, "seeds applied: %d\n", seeded)
}
return nil
}
// seedEdited applies a copy of the seed files in which the applied seed has gained a
// dish. A seed runs once, so the change is not applied; the Seeder logs a warning through
// the logger instead, naming both checksums.
//
// Takes database (*sql.DB) which is the seeded database.
// Takes backend (migrate.Backend) which names the seed table.
// Takes files (fs.FS) which holds the seed files.
// Takes logger (*slog.Logger) which receives the warning.
// Takes output (io.Writer) which receives the count.
//
// Returns error when the copy cannot be made or the seeder fails.
func seedEdited(ctx context.Context, database *sql.DB, backend migrate.Backend, files fs.FS, logger *slog.Logger, output io.Writer) error {
original, err := fs.ReadFile(files, seedsDirectory+"/0001_menu.sql")
if err != nil {
return err
}
edited := fstest.MapFS{
seedsDirectory + "/0001_menu.sql": {Data: append(original, "INSERT INTO menu (dish, price_pence) VALUES ('pickled egg', 120);\n"...)},
}
seeder, err := migrate.NewSeeder(database, backend, edited, seedsDirectory, migrate.WithLogger(logger))
if err != nil {
return err
}
seeded, err := seeder.Apply(ctx)
if err != nil {
return fmt.Errorf("seeding the edited file: %w", err)
}
fmt.Fprintf(output, "seeds applied after editing 0001_menu.sql: %d\n", seeded)
return nil
}
// printOtherHistory reads the same migration files against the default namespace's
// history, which this program never wrote to.
//
// Takes database (*sql.DB) which is the migrated database.
// Takes files (fs.FS) which holds the migration files.
// Takes output (io.Writer) which receives the count.
//
// Returns error when the status cannot be read.
func printOtherHistory(ctx context.Context, database *sql.DB, files fs.FS, output io.Writer) error {
backend := migrate_backend_postgres.New()
migrator, err := migrate.NewMigrator(database, backend, files, "db/migrations")
if err != nil {
return err
}
statuses, err := migrator.Status(ctx)
if err != nil {
return fmt.Errorf("reading the default history: %w", err)
}
applied := 0
for _, status := range statuses {
if status.Applied {
applied++
}
}
fmt.Fprintf(output, "%s (default namespace): %d of %d migrations applied\n", backend.Names().History, applied, len(statuses))
return nil
}
// withoutTime drops the time from each log record, so the output is the same on every
// run.
//
// Takes groups ([]string) which name the groups the attribute is in.
// Takes attribute (slog.Attr) which is the attribute to keep or drop.
//
// Returns slog.Attr which is the attribute, or an empty one for the time.
func withoutTime(groups []string, attribute slog.Attr) slog.Attr {
if len(groups) == 0 && attribute.Key == slog.TimeKey {
return slog.Attr{}
}
return attribute
}
main.go
// Command 029_seeds_namespaces_hooks connects to a PostgreSQL database and runs the
// example in example.go, which migrates it under its own namespace with progress hooks,
// including an index built CONCURRENTLY outside a transaction, and then applies the seed
// files.
//
// 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; a second
// run against the same database applies no migrations and no seeds.
package main
import (
"context"
"database/sql"
"embed"
"errors"
"flag"
"log"
"os"
_ "github.com/jackc/pgx/v5/stdlib"
)
// files holds the migration and seed files, compiled into the binary.
//
//go:embed db/migrations db/seeds
var files 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 connects to the database at dsn and runs the example against it. The example
// migrates and seeds the database itself.
//
// Takes dsn (string) which is the PostgreSQL connection string.
//
// Returns error when the database cannot be opened 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() }()
return run(ctx, database, files, os.Stdout)
}
internal/store/menu.sql.go
// Code generated by sgl. DO NOT EDIT.
package store
import "context"
const listMenuSQL = `SELECT dish, price_pence FROM menu ORDER BY dish;`
type ListMenuRow struct {
Dish string `json:"dish"`
PricePence int32 `json:"price_pence"`
}
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
if rows.Next() {
var row ListMenuRow
scanArgs := []any{&row.Dish, &row.PricePence}
for more := true; more; more = rows.Next() {
row = ListMenuRow{}
if err := rows.Scan(scanArgs...); 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
type Menu struct {
ID int64 `json:"id"`
Dish string `json:"dish"`
PricePence int32 `json:"price_pence"`
}
internal/store/querier.go
// Code generated by sgl. DO NOT EDIT.
package store
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
type dbtxWrapper interface {
WrapDBTX(inner any) any
}
type Queries struct {
reader DBTX
writer DBTX
wrapper dbtxWrapper
}
func New(db DBTX) *Queries {
wrapper, _ := db.(dbtxWrapper)
return &Queries{reader: db, writer: db, wrapper: wrapper}
}
func NewWithReplica(writer DBTX, reader DBTX) *Queries {
wrapper, _ := writer.(dbtxWrapper)
return &Queries{reader: reader, writer: writer, wrapper: wrapper}
}
func (queries *Queries) WithTx(transaction *sql.Tx) *Queries {
if queries == nil {
return &Queries{reader: transaction, writer: transaction, wrapper: nil}
}
db := queries.wrapDBTX(transaction)
return &Queries{reader: db, writer: db, wrapper: queries.wrapper}
}
func (queries *Queries) wrapDBTX(db DBTX) DBTX {
if queries.wrapper == nil {
return db
}
wrapped, ok := queries.wrapper.WrapDBTX(db).(DBTX)
if !ok {
return db
}
return wrapped
}
func (queries *Queries) RunInTx(ctx context.Context, db *sql.DB, fn func(*Queries) error) error {
transaction, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer transaction.Rollback()
if err = fn(queries.WithTx(transaction)); err != nil {
return err
}
return transaction.Commit()
}
output.txt
history tables: chippy_migrations and chippy_seeds
migrating: 2 pending, versions [1 2]
applied 0001 menu
applied 0002 menu_price_index
menu_price_idx built concurrently, valid=true
sgl_migrations (default namespace): 0 of 2 migrations applied
seeds applied: 1
seeds applied: 0
cod and chips 950p
haddock and chips 1020p
mushy peas 250p
level=WARN msg="seed file checksum changed since last application" version=1 name=menu applied_checksum=0fe49fb6264686ee639a2b35cf19026da57e5a5f0e2bcdcecf4035b8d98ba635 file_checksum=41505f6d893d13901c29bd1badc3a62675bedbd0daabc516763951970d7b798b
seeds applied after editing 0001_menu.sql: 0