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

  1. Inspect the wrapped error (%w) for the exact SQL failure (syntax, access denied, connection)
  2. Run migrations with a database user that has CREATE/ALTER privileges on the target schema
  3. Confirm you are connected to the intended database/host (not a read-only replica)
  4. Check whether an object named m.cursorTable already exists with an incompatible shape and drop/rename it
  5. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/5ab0afdef8ea3fb0. Report an issue: GitHub.