{"record":{"id":"5ab0afdef8ea3fb0","repo":"gastownhall/beads","slug":"creating-s-w","errorCode":null,"errorMessage":"creating %s: %w","messagePattern":"creating (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"internal/storage/schema/schema.go","lineNumber":1563,"sourceCode":"// consumed; all other migrations keep the unchanged ExecContext path.\nfunc execMigrationBody(ctx context.Context, db DBConn, sqlText string) error {\n\tif !procedureCallRe.MatchString(sqlText) {\n\t\t_, err := db.ExecContext(ctx, sqlText)\n\t\treturn err\n\t}\n\treturn DrainCall(ctx, db, sqlText)\n}\n\n// migrate brings the source up to its latest version and returns the number of\n// numbered migrations applied plus whether it added the content_hash column to a\n// pre-existing cursor table. The column signal lets MigrateUp stage and commit\n// that ALTER as schema work even when no numbered migration was applied.\n// migrate applies pending migrations from this source. upTo bounds the highest\n// version applied; pass 0 for the latest (the production path — only the\n// MigrateUpTo test-support path passes a real bound).\nfunc (m migrationSource) migrate(ctx context.Context, db DBConn, upTo int) (int, bool, error) {\n\tif _, err := db.ExecContext(ctx, m.bootstrapSQL()); err != nil {\n\t\treturn 0, false, fmt.Errorf(\"creating %s: %w\", m.cursorTable, err)\n\t}\n\tcolumnAdded, err := m.ensureContentHashColumn(ctx, db)\n\tif err != nil {\n\t\treturn 0, false, err\n\t}\n\n\ttarget := m.latest()\n\tif upTo > 0 && upTo < target {\n\t\ttarget = upTo\n\t}\n\n\t// The cursor is read through currentVersion, never raw: that is where the\n\t// cursor-reality check lives (gh 5033). A raw read here would believe the\n\t// contradicted cursor that migrationWorkNeeded just disbelieved — MigrateUp\n\t// would decide \"work needed\" on every open, run the whole pass, and then\n\t// apply nothing, leaving the missing tables missing and the pass to repeat\n\t// forever. The heal only happens if the applier disbelieves the cursor too.\n\tcurrent, err := m.currentVersion(ctx, db)","sourceCodeStart":1545,"sourceCodeEnd":1581,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/schema.go#L1545-L1581","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"-- before: app user lacks DDL rights\nGRANT SELECT, INSERT, UPDATE, DELETE ON beads.* TO 'app'@'%';\n-- after: grant DDL needed for bootstrap/migrations\nGRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP ON beads.* TO 'app'@'%';","handlingStrategy":"try-catch","validationCode":"// Pre-flight: confirm DDL rights and connectivity before migrating.\nvar probe int\nif err := db.QueryRowContext(ctx, \"SELECT 1\").Scan(&probe); err != nil {\n    return fmt.Errorf(\"database unreachable: %w\", err)\n}\nif _, err := db.ExecContext(ctx, \"CREATE TABLE IF NOT EXISTS _mig_probe (id INT); DROP TABLE _mig_probe\"); err != nil {\n    return fmt.Errorf(\"DDL privileges missing for migration user: %w\", err)\n}","typeGuard":"func bootstrapTableAbsent(ctx context.Context, db DBConn, table string) (bool, error) {\n    var n int\n    err := db.QueryRowContext(ctx,\n        `SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?`,\n        table).Scan(&n)\n    return n == 0, err\n}","tryCatchPattern":"applied, ok, err := src.migrate(ctx, db, 0)\nif err != nil {\n    var mysqlErr *mysql.MySQLError\n    if errors.As(err, &mysqlErr) && mysqlErr.Number == 1142 { // ER_TABLEACCESS_DENIED / privilege errors\n        return fmt.Errorf(\"run migrations with a DDL-capable user: %w\", err)\n    }\n    return err\n}","preventionTips":["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"],"tags":["database","migration","ddl","permissions"],"backgroundTag":"migration-bootstrap-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}