{"record":{"id":"38f5d8a650c004f1","repo":"gastownhall/beads","slug":"checking-s-sentinel-table-s-w","errorCode":null,"errorMessage":"checking %s sentinel table %s: %w","messagePattern":"checking (.+?) sentinel table (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/schema/schema.go","lineNumber":1271,"sourceCode":"\treturn current, nil\n}\n\n// cursorContradictedBySchema reports whether this series' cursor claims work\n// that the schema does not corroborate.\n//\n// Returning \"cursor is 0\" rather than an error is deliberate: the series is\n// written to be re-runnable against a database that already has some of it.\n// migrations/ignored/0001 builds each table as __temp__<name> and then\n// `RENAME TABLE __temp__x TO x` only when x does not already exist, DROPping\n// the temp otherwise; later migrations gate their ALTERs on\n// INFORMATION_SCHEMA lookups. So re-running the series repairs the missing\n// tables and leaves existing data untouched — which is why this can heal\n// rather than merely diagnose.\nfunc (m migrationSource) cursorContradictedBySchema(ctx context.Context, db DBConn) (bool, error) {\n\tfor _, table := range m.sentinelTables {\n\t\tpresent, err := sentinelTableExists(ctx, db, table)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"checking %s sentinel table %s: %w\", m.cursorTable, table, err)\n\t\t}\n\t\tif !present {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\tfor _, column := range m.sentinelColumns {\n\t\tpresent, err := sentinelColumnExists(ctx, db, column.table, column.column)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"checking %s sentinel column %s.%s: %w\", m.cursorTable, column.table, column.column, err)\n\t\t}\n\t\tif !present {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, nil\n}\n\n// sentinelTableExists is a function variable for the same reason","sourceCodeStart":1253,"sourceCodeEnd":1289,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/schema.go#L1253-L1289","documentation":"cursorContradictedBySchema verifies the cursor's claimed migration version against reality by checking sentinel tables exist. If sentinelTableExists errors for a sentinel table, the failure is wrapped as `checking <cursor> sentinel table <table>: <cause>`. This check exists because a cursor can claim at-latest while its described tables are missing (clone/dump-restore divergence, gh 5033/4356).","triggerScenarios":"The `SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?` for a sentinel table fails: dead connection, context cancellation, permission issue, or poisoned Dolt session snapshot after an earlier failed statement in the same pooled connection.","commonSituations":"Long-lived pooled connections surviving a Dolt server restart; concurrent migrations issuing failing DDL on shared connections; network instability during startup; restricted INFORMATION_SCHEMA visibility.","solutions":["Retry MigrateUp on fresh connections — the probe is read-only and safe to repeat.","Recycle the connection pool if errors indicate stale sessions (Dolt catalog-snapshot poisoning after failed statements).","Check the wrapped cause for permission errors and GRANT SELECT on INFORMATION_SCHEMA lookups if restricted.","Serialize migrations with a lock so concurrent processes do not poison shared pooled sessions."],"exampleFix":"// before\npresent, err := sentinelTableExists(ctx, db, table)\nif err != nil {\n    return false, fmt.Errorf(\"checking %s sentinel table %s: %w\", m.cursorTable, table, err)\n}\n// after\npresent, err := sentinelTableExists(ctx, db, table)\nif err != nil {\n    if dberrors.IsBadConn(err) { // stale pooled session; retry once fresh\n        present, err = sentinelTableExists(ctx, freshConn(ctx, db), table)\n    }\n    if err != nil {\n        return false, fmt.Errorf(\"checking %s sentinel table %s: %w\", m.cursorTable, table, err)\n    }\n}","handlingStrategy":"retry","validationCode":"if err := db.PingContext(ctx); err != nil {\n    return fmt.Errorf(\"cannot run consistency probe: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"err := MigrateUp(ctx, db)\nif err != nil && strings.Contains(err.Error(), \"sentinel table\") {\n    // read-only probe failed; recycle pool and retry once\n    db.SetMaxIdleConns(0)\n    err = MigrateUp(ctx, db)\n}","preventionTips":["Serialize migrations with a lock so concurrent processes don't poison pooled Dolt sessions.","Recycle pooled connections after any failed statement on Dolt.","Use fresh connections for the migration phase of startup.","Keep retry/backoff around MigrateUp; all sentinel probes are read-only and safe to repeat."],"tags":["database","dolt","migration","consistency-check"],"backgroundTag":"sentinel-table-check-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}