gastownhall/beads · critical
creating %s: %w
Error message
creating %s: %w
What it means
This error wraps the failure of db.ExecContext when executing the bootstrap SQL that creates the migration cursor bookkeeping table (m.cursorTable) at the start of migrationSource.migrate (internal/storage/schema/schema.go:1563). The library throws it because migrations cannot be tracked until the cursor table exists; any SQL error here (syntax, permissions, connection, DDL restriction) aborts the whole migration run.
Source
Thrown at internal/storage/schema/schema.go:1563
// consumed; all other migrations keep the unchanged ExecContext path.
func execMigrationBody(ctx context.Context, db DBConn, sqlText string) error {
if !procedureCallRe.MatchString(sqlText) {
_, err := db.ExecContext(ctx, sqlText)
return err
}
return DrainCall(ctx, db, sqlText)
}
// migrate brings the source up to its latest version and returns the number of
// numbered migrations applied plus whether it added the content_hash column to a
// pre-existing cursor table. The column signal lets MigrateUp stage and commit
// that ALTER as schema work even when no numbered migration was applied.
// migrate applies pending migrations from this source. upTo bounds the highest
// version applied; pass 0 for the latest (the production path — only the
// MigrateUpTo test-support path passes a real bound).
func (m migrationSource) migrate(ctx context.Context, db DBConn, upTo int) (int, bool, error) {
if _, err := db.ExecContext(ctx, m.bootstrapSQL()); err != nil {
return 0, false, fmt.Errorf("creating %s: %w", m.cursorTable, err)
}
columnAdded, err := m.ensureContentHashColumn(ctx, db)
if err != nil {
return 0, false, err
}
target := m.latest()
if upTo > 0 && upTo < target {
target = upTo
}
// The cursor is read through currentVersion, never raw: that is where the
// cursor-reality check lives (gh 5033). A raw read here would believe the
// contradicted cursor that migrationWorkNeeded just disbelieved — MigrateUp
// would decide "work needed" on every open, run the whole pass, and then
// apply nothing, leaving the missing tables missing and the pass to repeat
// forever. The heal only happens if the applier disbelieves the cursor too.
current, err := m.currentVersion(ctx, db)View on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped error (%w) for the exact SQL failure (syntax, access denied, connection)
- Run migrations with a database user that has CREATE/ALTER privileges on the target schema
- Confirm you are connected to the intended database/host (not a read-only replica)
- Check whether an object named m.cursorTable already exists with an incompatible shape and drop/rename it
- Verify basic connectivity with a simple query before re-running migrations
Example fix
-- before: app user lacks DDL rights GRANT SELECT, INSERT, UPDATE, DELETE ON beads.* TO 'app'@'%'; -- after: grant DDL needed for bootstrap/migrations GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP ON beads.* TO 'app'@'%';
Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-flight: confirm DDL rights and connectivity before migrating.
var probe int
if err := db.QueryRowContext(ctx, "SELECT 1").Scan(&probe); err != nil {
return fmt.Errorf("database unreachable: %w", err)
}
if _, err := db.ExecContext(ctx, "CREATE TABLE IF NOT EXISTS _mig_probe (id INT); DROP TABLE _mig_probe"); err != nil {
return fmt.Errorf("DDL privileges missing for migration user: %w", err)
} Type guard
func bootstrapTableAbsent(ctx context.Context, db DBConn, table string) (bool, error) {
var n int
err := db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?`,
table).Scan(&n)
return n == 0, err
} Try / catch
applied, ok, err := src.migrate(ctx, db, 0)
if err != nil {
var mysqlErr *mysql.MySQLError
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1142 { // ER_TABLEACCESS_DENIED / privilege errors
return fmt.Errorf("run migrations with a DDL-capable user: %w", err)
}
return err
} Prevention
- Use a dedicated migration DB account with CREATE/ALTER privileges, not the app's least-privilege account
- Never point migrations at read-only replicas
- Check for name collisions with the cursor table when adopting an existing database
- Validate connectivity (ping) before long migration runs
- Keep bootstrap DDL free of transaction-only or server-restricted syntax
When it happens
Trigger: Any call into the migrate path (Migrate or MigrateUpTo) where executing bootstrapSQL fails: the DDL is rejected by the server, the user lacks CREATE privileges, the connection is broken, or the cursor table name collides with an incompatible existing object.
Common situations: Running migrations with a DB account lacking DDL privileges; connecting to the wrong database/schema; DDL blocked inside a transaction or by a read-only replica; a leftover object with the cursor table's name from an older schema layout.
Related errors
- adding wisp_dependencies.%s for migration 0053: %w
- reading SHOW CREATE TABLE %s: %w
- adding %s.content_hash: %w
- pre-repair for migration %s: %w
- dropping idx_wisp_dep_type_target for the 0058 repair: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/5ab0afdef8ea3fb0.
Report an issue: GitHub.