{"record":{"id":"3cf3a95daf904b85","repo":"gastownhall/beads","slug":"reading-s-version-w","errorCode":null,"errorMessage":"reading %s version: %w","messagePattern":"reading (.+?) version: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/schema/schema.go","lineNumber":1232,"sourceCode":"\t// the rest of its life in the pool (be-bv7x).\n\tvar cursorExists int\n\tif err := db.QueryRowContext(ctx,\n\t\t\"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?\",\n\t\tm.cursorTable,\n\t).Scan(&cursorExists); err != nil {\n\t\treturn 0, fmt.Errorf(\"probing %s existence: %w\", m.cursorTable, err)\n\t}\n\tif cursorExists == 0 {\n\t\treturn 0, nil\n\t}\n\n\tvar current int\n\terr := db.QueryRowContext(ctx, \"SELECT COALESCE(MAX(version), 0) FROM \"+m.cursorTable).Scan(&current)\n\tif err != nil && err != sql.ErrNoRows {\n\t\tif dberrors.IsTableNotExist(err) {\n\t\t\treturn 0, nil\n\t\t}\n\t\treturn 0, fmt.Errorf(\"reading %s version: %w\", m.cursorTable, err)\n\t}\n\tif current == 0 {\n\t\treturn 0, nil\n\t}\n\t// A missing cursor TABLE already meant \"nothing applied\". A cursor whose\n\t// tables are absent means the same thing and was previously believed\n\t// (gh 5033, gh 4356): ignored_schema_migrations is itself dolt-ignored and\n\t// clone-local, so a database materialized out of band — table-by-table\n\t// copy, dump restore, or a clone that picked up the cursor rows without\n\t// the clone-local tables they describe — arrives claiming at-latest with\n\t// no wisps tables. atLatest() then short-circuits migrationWorkNeeded()\n\t// and the series never re-runs, surfacing much later and much further away\n\t// as \"table not found: wisp_dependencies\" on `bd close`.\n\tcontradicted, cerr := m.cursorContradictedBySchema(ctx, db)\n\tif cerr != nil {\n\t\treturn 0, cerr\n\t}\n\tif contradicted {","sourceCodeStart":1214,"sourceCodeEnd":1250,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/schema.go#L1214-L1250","documentation":"After confirming the cursor table exists, currentVersion runs `SELECT COALESCE(MAX(version), 0) FROM <cursorTable>` to read the applied migration version. If this read fails with something other than ErrNoRows or a table-not-exist error (both handled as 'version 0'), the error is wrapped as `reading <cursor> version: <cause>`.","triggerScenarios":"The MAX(version) query fails on an existing cursor table: permission denial on SELECT, corrupted cursor table, unsupported column type in version, connection loss mid-query, or a poisoned Dolt session snapshot masking the table despite the successful existence probe.","commonSituations":"Cursor table created by a different/older schema (version column renamed or re-typed); partial clone/dump restore leaving a broken cursor; revoked SELECT grants between probe and read; transient network failure.","solutions":["Inspect the wrapped cause: for permission errors, GRANT SELECT on the cursor table to the app user.","If the table looks corrupted or incompatible (e.g. version column missing), drop or repair the cursor table and re-run MigrateUp — migrations are re-runnable.","Retry on a fresh connection; a Dolt session that previously hit a failing statement can hold a stale snapshot even when the probe succeeded.","Verify no concurrent DDL is renaming/altering the cursor table during startup."],"exampleFix":"// before\nerr := db.QueryRowContext(ctx, \"SELECT COALESCE(MAX(version), 0) FROM \"+m.cursorTable).Scan(&current)\nif err != nil && err != sql.ErrNoRows {\n    if dberrors.IsTableNotExist(err) {\n        return 0, nil\n    }\n    return 0, fmt.Errorf(\"reading %s version: %w\", m.cursorTable, err)\n}\n// after\nerr := db.QueryRowContext(ctx, \"SELECT COALESCE(MAX(version), 0) FROM \"+m.cursorTable).Scan(&current)\nif err != nil && !errors.Is(err, sql.ErrNoRows) {\n    if dberrors.IsTableNotExist(err) {\n        return 0, nil\n    }\n    if dberrors.IsBadField(err) { // cursor schema drifted; treat as unapplied\n        return 0, nil\n    }\n    return 0, fmt.Errorf(\"reading %s version: %w\", m.cursorTable, err)\n}","handlingStrategy":"try-catch","validationCode":"// check cursor table shape before reading version\ncols, err := probeColumns(ctx, db, cursorTable)\nif err == nil && !slices.Contains(cols, \"version\") {\n    return fmt.Errorf(\"cursor table %s missing version column; drop it and re-run migrations\", cursorTable)\n}","typeGuard":"func isCursorSchemaDrift(err error) bool {\n    return dberrors.IsTableNotExist(err) || dberrors.IsBadField(err) || errors.Is(err, sql.ErrNoRows)\n}","tryCatchPattern":"err := MigrateUp(ctx, db)\nif err != nil && strings.Contains(err.Error(), \"reading \") && strings.Contains(err.Error(), \"version\") {\n    // benign causes are already mapped to version 0; anything here is real\n    log.Printf(\"cursor version read failed: %v — verifying cursor table integrity\", err)\n}","preventionTips":["Never hand-edit or re-type the cursor table's version column.","Restore clones/dumps atomically so the cursor and its tables arrive together.","Keep SELECT grants on the cursor table for the app user.","Avoid mixing pooled sessions that have hit failing statements with Dolt."],"tags":["database","dolt","migration","cursor-table"],"backgroundTag":"migration-version-read-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}