Migrations at start-up
On this page
On CockroachDB:
- migrate_backend_cockroachdb.New() locks with a row in sgl_migration_lock held by SELECT ... FOR UPDATE, since CockroachDB has no advisory locks; asking it for LockAdvisory returns ErrUnsupportedLock
- the lock transaction pins one connection and the migrations run on a second, because CockroachDB rejects a BEGIN nested in the lock transaction
- migrations do not run in a transaction, because CockroachDB cannot roll a schema change back with the rest of a transaction; the backend records each finished statement, so a failed file is marked dirty and resumes; each file holds one schema change
- the backend turns on enable_durable_locking_for_serializable for its lock transaction, so a range split during a schema change cannot drop the lock; that needs CockroachDB 23.2 or later
- unique_rowid() keys change on every run, so the harbour is listed by name without IDs
A service that owns its database can bring the schema up to date itself, every time it starts. The migration files are compiled into the binary with go:embed, and a migrator applies whatever the database does not have yet before the program does anything else. There is no separate migration step in the deploy and no files to ship beside the binary, and the schema the code runs against is the one it was generated from: these are the same files sgl generate reads. sqlc leaves applying migrations to another tool; Seagull's migrator is a Go package, seagull.sh/seagull/sdk/migrate, that reads the same files.
The migrations
Two migrations: the first creates a table, the second adds a column to it. They are plain SQL files, numbered in the order they apply, each with a .down.sql beside it to undo it.
db/migrations/0001_boats.up.sql
CREATE TABLE boats (
id INT8 NOT NULL DEFAULT unique_rowid() PRIMARY KEY,
name STRING NOT NULL
);
db/migrations/0002_boat_home_port.up.sql
ALTER TABLE boats ADD COLUMN home_port STRING NOT NULL DEFAULT 'Padstow';
Embedding the files
main.go embeds the migrations directory:
//go:embed db/migrations
var migrations embed.FS
The program needs nothing beside it on disk to bring a database up to date, and the files it applies are always the ones it was built with.
Migrating at start-up
Before it touches the schema, the program builds a migrator and calls Up. migrate.NewMigrator takes the database, a backend for the engine, the embedded files and the directory inside them. Up applies every migration not yet recorded in the history table, in version order, and returns how many it applied. On a database that is already up to date it applies none, which is what makes it safe to call on every start.
migrate_backend_cockroachdb.New() is the PostgreSQL backend with one change, its lock. CockroachDB has no advisory locks, so the backend keeps a lock table, sgl_migration_lock, and holds its one row with SELECT ... FOR UPDATE in an open transaction while it migrates. The migrations themselves run on a second connection, because CockroachDB will not begin a transaction nested inside the lock's.
From version 25.1, CockroachDB commits before each schema change (its autocommit_before_ddl setting), so the statements of one migration do not share a transaction. A file with two schema changes could fail between them and leave the first applied, so each file here holds one.
Several replicas at once
An orchestrator rolling out a new version often starts several replicas at the same moment, and every one of them runs Up. The program does the same: three replicas start together, each migrating before it adds its boat. Up takes the lock only when something is pending, and reads the history again once it holds it. So one replica applies both migrations while the others wait on the lock row, and when they get it, they find the work done. A fourth replica then restarts and applies nothing.
The replicas here share the program's connection pool, where real ones would be separate processes. The lock lives in the database, so it orders them in the same way.
A configuration copied from PostgreSQL might ask for an advisory lock with migrate.WithLock(migrate.LockAdvisory). The CockroachDB backend refuses it with migrate.ErrUnsupportedLock when the migrator is built, before it touches the database, and the program shows that refusal too.
example.go
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"io/fs"
"sync"
"seagull.sh/seagull/examples/cockroachdb/027_migrations_at_startup/internal/store"
"seagull.sh/seagull/sdk/migrate"
"seagull.sh/seagull/sdk/migrate/migrate_backend_cockroachdb"
)
// run deploys the program against an empty database the way an orchestrator does,
// starting several replicas at the same moment, each of which migrates before it works.
// Then one replica restarts and finds nothing to migrate.
//
// Takes database (*sql.DB) which is the database to migrate, empty or not.
// Takes migrations (fs.FS) which holds db/migrations, the embed.FS in main.go.
// Takes output (io.Writer) which receives what each start did.
//
// Returns error when migrating or a query fails.
func run(ctx context.Context, database *sql.DB, migrations fs.FS, output io.Writer) error {
replicas := []string{"Kittiwake", "Puffin", "Gannet"}
applied, err := deploy(ctx, database, migrations, replicas)
if err != nil {
return err
}
fmt.Fprintf(output, "%d replicas started at once and applied %d migrations between them\n", len(replicas), applied)
restarted, err := start(ctx, database, migrations, "Cormorant")
if err != nil {
return err
}
fmt.Fprintf(output, "a replica restarted and applied %d migrations\n", restarted)
if err := printBoats(ctx, database, output); err != nil {
return err
}
if err := refuseAdvisoryLock(database, migrations, output); err != nil {
return err
}
return printHistory(ctx, database, migrations, output)
}
// deploy starts one replica per boat at the same moment and waits for them all.
//
// The replicas here share this process's connection pool, where real replicas are
// separate processes with pools of their own. The lock lives in the database, so it
// serialises them the same way.
//
// Takes database (*sql.DB) which is the database.
// Takes migrations (fs.FS) which holds the migration files.
// Takes boats ([]string) which names the boat each replica adds.
//
// Returns int which is how many migrations the replicas applied between them.
// Returns error which joins every replica's failure.
func deploy(ctx context.Context, database *sql.DB, migrations fs.FS, boats []string) (int, error) {
applied := make([]int, len(boats))
failures := make([]error, len(boats))
var replicas sync.WaitGroup
for index, boat := range boats {
replicas.Go(func() {
applied[index], failures[index] = start(ctx, database, migrations, boat)
})
}
replicas.Wait()
total := 0
for _, count := range applied {
total += count
}
return total, errors.Join(failures...)
}
// start is what the program does each time it starts: migrate first, then work. Here the
// work is adding a boat.
//
// Takes database (*sql.DB) which is the database.
// Takes migrations (fs.FS) which holds the migration files.
// Takes boat (string) which names the boat to add.
//
// Returns int which is how many migrations this start applied.
// Returns error when migrating or the query fails.
func start(ctx context.Context, database *sql.DB, migrations fs.FS, boat string) (int, error) {
// migrate_backend_cockroachdb.New() is the PostgreSQL backend with a lock table in place
// of an advisory lock, which CockroachDB does not have. Up creates sgl_migration_lock if
// it is missing and holds its one row with SELECT ... FOR UPDATE in an open transaction,
// so a second replica waits on that row until the first commits.
//
// By default CockroachDB keeps a SERIALIZABLE transaction's FOR UPDATE locks in memory
// and drops them when a range splits or moves, which a schema change such as 0002's ADD
// COLUMN ... DEFAULT can cause. The backend turns on
// enable_durable_locking_for_serializable for its lock transaction, so the lock holds
// until the migrations are done; that needs CockroachDB 23.2 or later.
//
// The migrations run on a second connection, one statement at a time and not in a
// transaction: CockroachDB cannot roll a schema change back as part of a larger
// transaction, so the backend records each finished statement instead. A file that fails
// partway is marked dirty and resumes from the statement that failed once the cause is
// fixed. Keeping one schema change per file keeps each step its own record.
migrator, err := migrate.NewMigrator(database, migrate_backend_cockroachdb.New(), migrations, "db/migrations")
if err != nil {
return 0, err
}
// Up applies every migration not yet in the history and returns how many it applied. It
// takes the lock only when something is pending, and reads the history again once it
// holds the lock, so a replica that waited finds the work done.
applied, err := migrator.Up(ctx)
if err != nil {
return 0, fmt.Errorf("migrating: %w", err)
}
queries := store.New(database)
if err := queries.AddBoat(ctx, store.AddBoatParams{Name: boat, HomePort: "Padstow"}); err != nil {
return 0, fmt.Errorf("adding %s: %w", boat, err)
}
return applied, nil
}
// printBoats lists the harbour, which every replica wrote to once the schema existed.
//
// Takes database (*sql.DB) which is the migrated database.
// Takes output (io.Writer) which receives the list.
//
// Returns error when the query fails.
func printBoats(ctx context.Context, database *sql.DB, output io.Writer) error {
boats, err := store.New(database).ListBoats(ctx)
if err != nil {
return fmt.Errorf("listing boats: %w", err)
}
for _, row := range boats {
fmt.Fprintf(output, " %-9s of %s\n", row.Name, row.HomePort)
}
return nil
}
// refuseAdvisoryLock shows that the CockroachDB backend rejects an advisory lock, as a
// configuration copied from PostgreSQL might ask for, before it touches the database.
//
// Takes database (*sql.DB) which is the database.
// Takes migrations (fs.FS) which holds the migration files.
// Takes output (io.Writer) which receives the refusal.
//
// Returns error when the backend accepts the advisory lock.
func refuseAdvisoryLock(database *sql.DB, migrations fs.FS, output io.Writer) error {
backend := migrate_backend_cockroachdb.New(migrate.WithLock(migrate.LockAdvisory))
_, err := migrate.NewMigrator(database, backend, migrations, "db/migrations")
if err == nil {
return errors.New("an advisory lock on CockroachDB gave no error, want migrate.ErrUnsupportedLock")
}
if !errors.Is(err, migrate.ErrUnsupportedLock) {
return fmt.Errorf("an advisory lock on CockroachDB gave %w, want migrate.ErrUnsupportedLock", err)
}
fmt.Fprintf(output, "advisory lock refused: %v\n", err)
return nil
}
// printHistory prints the migration history the migrator keeps in the database, and the
// table its lock uses.
//
// Takes database (*sql.DB) which is the migrated database.
// Takes migrations (fs.FS) which holds the migration files.
// Takes output (io.Writer) which receives the history.
//
// Returns error when the history cannot be read.
func printHistory(ctx context.Context, database *sql.DB, migrations fs.FS, output io.Writer) error {
backend := migrate_backend_cockroachdb.New()
migrator, err := migrate.NewMigrator(database, backend, migrations, "db/migrations")
if err != nil {
return err
}
statuses, err := migrator.Status(ctx)
if err != nil {
return fmt.Errorf("reading migration status: %w", err)
}
names := backend.Names()
fmt.Fprintf(output, "history in %s, locked through %s:\n", names.History, names.MigrationLock)
for _, status := range statuses {
fmt.Fprintf(output, " %04d %-15s applied=%t\n", status.Version, status.Name, status.Applied)
}
return nil
}
What it prints
Three replicas applied two migrations between them, not six, and the restart applied none. Every replica's boat is in the harbour, because each one added it only after the schema existed. CockroachDB's unique_rowid() ids change on every run, so the boats are listed by name.
3 replicas started at once and applied 2 migrations between them
a replica restarted and applied 0 migrations
Cormorant of Padstow
Gannet of Padstow
Kittiwake of Padstow
Puffin of Padstow
advisory lock refused: lock mode not supported by this backend: invalid configuration: advisory on CockroachDB
history in sgl_migrations, locked through sgl_migration_lock:
0001 boats applied=true
0002 boat_home_port applied=true
Try it
Add a third migration, such as 0003_boat_length.up.sql adding a column, and run sgl generate. Then run the program against the database from an earlier run: only the new migration is applied, and the rest of the history is left alone.
The migrations guide covers the migrator further, and the migration lifecycle goes on to rolling back, status and checksums.
Run it locally
From the directory you cloned the Seagull repository into:
cd seagull/examples/cockroachdb && go run ./027_migrations_at_startup
Its test runs it too, and checks what it prints against output.txt:
cd seagull/examples/cockroachdb && SGL_EXAMPLE_DSN="$DATABASE_URL" go test ./027_migrations_at_startup
Needs a running CockroachDB server: set DATABASE_URL, or pass -dsn, to the address of an empty database.
The files
Every file of the example, as it is in the Seagull repository.
sgl.yaml
# Copyright 2026 PolitePixels Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This project stands against fascism, authoritarianism, and all forms of
# oppression. We built this to empower people, not to enable those who would
# strip others of their rights and dignity.
version: 1
databases:
- name: harbour
dialect: cockroachdb
migrations: db/migrations
queries: db/queries
go:
package: store
out: internal/store
db/migrations/0001_boats.up.sql
CREATE TABLE boats (
id INT8 NOT NULL DEFAULT unique_rowid() PRIMARY KEY,
name STRING NOT NULL
);
db/migrations/0002_boat_home_port.up.sql
ALTER TABLE boats ADD COLUMN home_port STRING NOT NULL DEFAULT 'Padstow';
db/migrations/0001_boats.down.sql
DROP TABLE boats;
db/migrations/0002_boat_home_port.down.sql
ALTER TABLE boats DROP COLUMN home_port;
db/queries/boats.sql
-- sgl.query(AddBoat, exec)
INSERT INTO boats (name, home_port) VALUES ($1, $2);
-- sgl.query(ListBoats, many)
SELECT name, home_port FROM boats ORDER BY name;
example.go
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"io/fs"
"sync"
"seagull.sh/seagull/examples/cockroachdb/027_migrations_at_startup/internal/store"
"seagull.sh/seagull/sdk/migrate"
"seagull.sh/seagull/sdk/migrate/migrate_backend_cockroachdb"
)
// run deploys the program against an empty database the way an orchestrator does,
// starting several replicas at the same moment, each of which migrates before it works.
// Then one replica restarts and finds nothing to migrate.
//
// Takes database (*sql.DB) which is the database to migrate, empty or not.
// Takes migrations (fs.FS) which holds db/migrations, the embed.FS in main.go.
// Takes output (io.Writer) which receives what each start did.
//
// Returns error when migrating or a query fails.
func run(ctx context.Context, database *sql.DB, migrations fs.FS, output io.Writer) error {
replicas := []string{"Kittiwake", "Puffin", "Gannet"}
applied, err := deploy(ctx, database, migrations, replicas)
if err != nil {
return err
}
fmt.Fprintf(output, "%d replicas started at once and applied %d migrations between them\n", len(replicas), applied)
restarted, err := start(ctx, database, migrations, "Cormorant")
if err != nil {
return err
}
fmt.Fprintf(output, "a replica restarted and applied %d migrations\n", restarted)
if err := printBoats(ctx, database, output); err != nil {
return err
}
if err := refuseAdvisoryLock(database, migrations, output); err != nil {
return err
}
return printHistory(ctx, database, migrations, output)
}
// deploy starts one replica per boat at the same moment and waits for them all.
//
// The replicas here share this process's connection pool, where real replicas are
// separate processes with pools of their own. The lock lives in the database, so it
// serialises them the same way.
//
// Takes database (*sql.DB) which is the database.
// Takes migrations (fs.FS) which holds the migration files.
// Takes boats ([]string) which names the boat each replica adds.
//
// Returns int which is how many migrations the replicas applied between them.
// Returns error which joins every replica's failure.
func deploy(ctx context.Context, database *sql.DB, migrations fs.FS, boats []string) (int, error) {
applied := make([]int, len(boats))
failures := make([]error, len(boats))
var replicas sync.WaitGroup
for index, boat := range boats {
replicas.Go(func() {
applied[index], failures[index] = start(ctx, database, migrations, boat)
})
}
replicas.Wait()
total := 0
for _, count := range applied {
total += count
}
return total, errors.Join(failures...)
}
// start is what the program does each time it starts: migrate first, then work. Here the
// work is adding a boat.
//
// Takes database (*sql.DB) which is the database.
// Takes migrations (fs.FS) which holds the migration files.
// Takes boat (string) which names the boat to add.
//
// Returns int which is how many migrations this start applied.
// Returns error when migrating or the query fails.
func start(ctx context.Context, database *sql.DB, migrations fs.FS, boat string) (int, error) {
// migrate_backend_cockroachdb.New() is the PostgreSQL backend with a lock table in place
// of an advisory lock, which CockroachDB does not have. Up creates sgl_migration_lock if
// it is missing and holds its one row with SELECT ... FOR UPDATE in an open transaction,
// so a second replica waits on that row until the first commits.
//
// By default CockroachDB keeps a SERIALIZABLE transaction's FOR UPDATE locks in memory
// and drops them when a range splits or moves, which a schema change such as 0002's ADD
// COLUMN ... DEFAULT can cause. The backend turns on
// enable_durable_locking_for_serializable for its lock transaction, so the lock holds
// until the migrations are done; that needs CockroachDB 23.2 or later.
//
// The migrations run on a second connection, one statement at a time and not in a
// transaction: CockroachDB cannot roll a schema change back as part of a larger
// transaction, so the backend records each finished statement instead. A file that fails
// partway is marked dirty and resumes from the statement that failed once the cause is
// fixed. Keeping one schema change per file keeps each step its own record.
migrator, err := migrate.NewMigrator(database, migrate_backend_cockroachdb.New(), migrations, "db/migrations")
if err != nil {
return 0, err
}
// Up applies every migration not yet in the history and returns how many it applied. It
// takes the lock only when something is pending, and reads the history again once it
// holds the lock, so a replica that waited finds the work done.
applied, err := migrator.Up(ctx)
if err != nil {
return 0, fmt.Errorf("migrating: %w", err)
}
queries := store.New(database)
if err := queries.AddBoat(ctx, store.AddBoatParams{Name: boat, HomePort: "Padstow"}); err != nil {
return 0, fmt.Errorf("adding %s: %w", boat, err)
}
return applied, nil
}
// printBoats lists the harbour, which every replica wrote to once the schema existed.
//
// Takes database (*sql.DB) which is the migrated database.
// Takes output (io.Writer) which receives the list.
//
// Returns error when the query fails.
func printBoats(ctx context.Context, database *sql.DB, output io.Writer) error {
boats, err := store.New(database).ListBoats(ctx)
if err != nil {
return fmt.Errorf("listing boats: %w", err)
}
for _, row := range boats {
fmt.Fprintf(output, " %-9s of %s\n", row.Name, row.HomePort)
}
return nil
}
// refuseAdvisoryLock shows that the CockroachDB backend rejects an advisory lock, as a
// configuration copied from PostgreSQL might ask for, before it touches the database.
//
// Takes database (*sql.DB) which is the database.
// Takes migrations (fs.FS) which holds the migration files.
// Takes output (io.Writer) which receives the refusal.
//
// Returns error when the backend accepts the advisory lock.
func refuseAdvisoryLock(database *sql.DB, migrations fs.FS, output io.Writer) error {
backend := migrate_backend_cockroachdb.New(migrate.WithLock(migrate.LockAdvisory))
_, err := migrate.NewMigrator(database, backend, migrations, "db/migrations")
if err == nil {
return errors.New("an advisory lock on CockroachDB gave no error, want migrate.ErrUnsupportedLock")
}
if !errors.Is(err, migrate.ErrUnsupportedLock) {
return fmt.Errorf("an advisory lock on CockroachDB gave %w, want migrate.ErrUnsupportedLock", err)
}
fmt.Fprintf(output, "advisory lock refused: %v\n", err)
return nil
}
// printHistory prints the migration history the migrator keeps in the database, and the
// table its lock uses.
//
// Takes database (*sql.DB) which is the migrated database.
// Takes migrations (fs.FS) which holds the migration files.
// Takes output (io.Writer) which receives the history.
//
// Returns error when the history cannot be read.
func printHistory(ctx context.Context, database *sql.DB, migrations fs.FS, output io.Writer) error {
backend := migrate_backend_cockroachdb.New()
migrator, err := migrate.NewMigrator(database, backend, migrations, "db/migrations")
if err != nil {
return err
}
statuses, err := migrator.Status(ctx)
if err != nil {
return fmt.Errorf("reading migration status: %w", err)
}
names := backend.Names()
fmt.Fprintf(output, "history in %s, locked through %s:\n", names.History, names.MigrationLock)
for _, status := range statuses {
fmt.Fprintf(output, " %04d %-15s applied=%t\n", status.Version, status.Name, status.Applied)
}
return nil
}
main.go
// Command 027_migrations_at_startup connects to a CockroachDB database and runs the
// example in example.go, which starts several replicas at once, each applying the
// migrations embedded in the binary before it works, then restarts one.
//
// Run it with `go run .` against a local insecure node, which `docker run -d -p
// 26257:26257 cockroachdb/cockroach:latest-v25.1 start-single-node --insecure` starts, or
// point it elsewhere with `go run . -dsn <connection string>` or DATABASE_URL. The
// migrations run against an empty database; a second run finds nothing to migrate.
package main
import (
"cmp"
"context"
"embed"
"flag"
"log"
"os"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/stdlib"
)
// localDSN reaches an insecure single node on this machine.
const localDSN = "postgresql://root@localhost:26257/defaultdb?sslmode=disable"
// migrations holds the migration files, compiled into the binary, so the program needs no
// files beside it to bring a database up to date.
//
//go:embed db/migrations
var migrations embed.FS
func main() {
dsn := flag.String("dsn", cmp.Or(os.Getenv("DATABASE_URL"), localDSN), "CockroachDB connection string")
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 the database itself.
//
// Takes dsn (string) which is the connection string.
//
// Returns error when the connection string is invalid or the example fails.
func execute(ctx context.Context, dsn string) error {
config, err := pgx.ParseConfig(dsn)
if err != nil {
return err
}
database := stdlib.OpenDB(*config)
defer func() { _ = database.Close() }()
return run(ctx, database, migrations, os.Stdout)
}
internal/store/boats.sql.go
// Code generated by sgl. DO NOT EDIT.
package store
import "context"
const addBoatSQL = `INSERT INTO boats (name, home_port) VALUES ($1, $2);`
type AddBoatParams struct {
Name string
HomePort string
}
func (queries *Queries) AddBoat(ctx context.Context, params AddBoatParams) error {
_, err := queries.writer.ExecContext(ctx, addBoatSQL, params.Name, params.HomePort)
return err
}
const listBoatsSQL = `SELECT name, home_port FROM boats ORDER BY name;`
type ListBoatsRow struct {
Name string `json:"name"`
HomePort string `json:"home_port"`
}
func (queries *Queries) ListBoats(ctx context.Context) ([]ListBoatsRow, error) {
rows, err := queries.reader.QueryContext(ctx, listBoatsSQL)
if err != nil {
return nil, err
}
defer rows.Close()
var results []ListBoatsRow
if rows.Next() {
var row ListBoatsRow
scanArgs := []any{&row.Name, &row.HomePort}
for more := true; more; more = rows.Next() {
row = ListBoatsRow{}
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 Boats struct {
ID int64 `json:"id"`
Name string `json:"name"`
HomePort string `json:"home_port"`
}
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
3 replicas started at once and applied 2 migrations between them
a replica restarted and applied 0 migrations
Cormorant of Padstow
Gannet of Padstow
Kittiwake of Padstow
Puffin of Padstow
advisory lock refused: lock mode not supported by this backend: invalid configuration: advisory on CockroachDB
history in sgl_migrations, locked through sgl_migration_lock:
0001 boats applied=true
0002 boat_home_port applied=true