{"record":{"id":"c5f32496a7144ee5","repo":"gastownhall/beads","slug":"recording-s-in-s-w","errorCode":null,"errorMessage":"recording %s in %s: %w","messagePattern":"recording (.+?) in (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/schema/schema.go","lineNumber":1677,"sourceCode":"\t\t\tdirtyBeforeStep, err = dirtyTables(ctx, db, true)\n\t\t\tif err != nil {\n\t\t\t\treturn count, fmt.Errorf(\"snapshotting dirty tables before %s: %w\", mf.name, err)\n\t\t\t}\n\t\t}\n\n\t\tif err := src.preMigrationRepair(ctx, db, mf.version); err != nil {\n\t\t\treturn count, fmt.Errorf(\"pre-repair for migration %s: %w\", mf.name, err)\n\t\t}\n\n\t\tfmt.Fprintf(stderr, \"Applying migration %04d: %s…\\n\", mf.version, humanMigrationName(mf.name))\n\t\tstart := time.Now()\n\t\tif err := execMigrationBody(ctx, db, string(data)); err != nil {\n\t\t\treturn count, fmt.Errorf(\"migration %s: %w\", mf.name, err)\n\t\t}\n\t\tsum := sha256.Sum256(data)\n\t\tcontentHash := hex.EncodeToString(sum[:])\n\t\tif _, err := db.ExecContext(ctx, \"INSERT IGNORE INTO \"+src.cursorTable+\" (version, content_hash) VALUES (?, ?)\", mf.version, contentHash); err != nil {\n\t\t\treturn count, fmt.Errorf(\"recording %s in %s: %w\", mf.name, src.cursorTable, err)\n\t\t}\n\t\tcount++\n\n\t\t// commitEachStep's DOLT_ADD/DOLT_COMMIT is the expensive, fallible\n\t\t// part of this step on the production embedded path. The \"done\" line\n\t\t// (and its timing) must land after that commit succeeds, not before\n\t\t// it: printing \"done\" and then hitting a commit error would show an\n\t\t// operator a false completion, and timing that stopped before the\n\t\t// commit would understate the step's real cost. A failed commit\n\t\t// returns before either print statement below runs.\n\t\tif commitEachStep {\n\t\t\tif err := commitMigrationStep(ctx, db, src.cursorTable, mf.name, dirtyBeforeStep); err != nil {\n\t\t\t\treturn count, fmt.Errorf(\"committing migration %s: %w\", mf.name, err)\n\t\t\t}\n\t\t}\n\t\tfmt.Fprintf(stderr, \"  done (%.1fs)\\n\", time.Since(start).Seconds())\n\n\t\tif migrateStepFaultHook != nil {","sourceCodeStart":1659,"sourceCodeEnd":1695,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/schema.go#L1659-L1695","documentation":"This error is returned when the library fails to record a successfully-applied migration into its cursor table via `INSERT IGNORE INTO <cursorTable> (version, content_hash)` (internal/storage/schema/schema.go:1677). The cursor row is what marks a migration version as applied; without it, migrations would re-run on the next pass. The wrap text names the migration file and the cursor table involved.","triggerScenarios":"The ExecContext INSERT into the schema cursor table fails right after the migration SQL succeeded: cursor table missing or corrupted, a constraint/index conflict not tolerated by INSERT IGNORE, permissions/lock errors on the table, or the connection/context was cancelled between the migration body and the insert.","commonSituations":"A corrupted or manually-deleted .beads database missing the cursor table; a context cancellation/timeout expiring exactly at the insert; disk-full or read-only filesystem making the write fail; concurrent `bd` processes contending for the same database lock.","solutions":["Read the wrapped inner error; if the cursor table is missing or corrupted, rebuild it (or restore the database from backup).","Check for concurrent processes: ensure only one `bd` instance is running against the database (beads uses a lock/lockfile; remove a stale lock if safe).","Check disk space and filesystem write permissions for the .beads database directory.","Re-run the migration pass; because the migration succeeded but the cursor row did not, the same migration will be retried — verify its SQL is idempotent or clean up its effects first if the inner error indicates a write failure that partially applied.","If context timeouts recur, increase the timeout budget or avoid killing the process mid-migration."],"exampleFix":"// before: stale lock from a killed process blocks the cursor insert\nbd doctor  # reports database lock held\n// after: remove the stale lock with a single writer, then retry\nrm -f .beads/db.lock\nbd migrate up","handlingStrategy":"try-catch","validationCode":"// Before migrating: single-writer check and cursor table sanity\nif _, err := os.Stat(\".beads/db.lock\"); err == nil {\n    return fmt.Errorf(\"another bd process may hold the database lock\")\n}\nvar n int\nif err := db.QueryRowContext(ctx, \"SELECT COUNT(*) FROM \"+cursorTable).Scan(&n); err != nil {\n    return fmt.Errorf(\"cursor table %s unreadable: %w\", cursorTable, err)\n}","typeGuard":"var timeout bool\nif errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {\n    timeout = true // cursor insert was cut off by context cancellation\n}\nvar dbErr *database.DBError\nif errors.As(err, &dbErr) && dbErr.Code == mysql.ErrNoSuchTable {\n    // cursor table missing: rebuild/restore instead of retrying the insert\n}","tryCatchPattern":"if err := migrateUp(ctx, db); err != nil {\n    if strings.Contains(err.Error(), \"recording \") {\n        // migration body likely applied but cursor row missing:\n        // the same migration will re-run next pass; verify idempotency\n        // and repair cursor state via bd doctor before retrying\n    }\n    return err\n}","preventionTips":["Run only one bd process per database at a time.","Monitor disk space on the volume holding .beads.","Never manually delete or edit the schema cursor table.","Increase context timeouts for migration runs on large rigs.","Back up before upgrades so a torn step can be restored."],"tags":["database","migration","cursor-table","dolt"],"backgroundTag":"migration-cursor-write-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}