Skip to content

driver-sql: introspectIndexes swallows every error and returns a partial index list, which the drift differ then reports as missing indexes #7332

Description

@os-zhuang

Finding, filed unassigned — recording only, no ownership taken. Surfaced while measuring #6522 and verified independently by the drivers seat against origin/main @ 88154bee1.

⚠️ This is a code-shape finding, not an observed incident. Nothing here was reproduced. It is filed because the mechanism is exact, cheap to state, and would be invisible if it ever fired.

The shape

SqlDriver.introspectIndexes (packages/drivers/driver-sql/src/sql-driver.ts:6781) wraps its entire dialect dispatch — the SQLite, Postgres and MySQL branches alike — in one bare catch:

    } catch {
      // Best-effort — fall through and let creation handle conflicts.
    }
    return [...byName.values()];
  }

sql-driver.ts:6861. On any throw, byName is returned in whatever half-built state it reached: partial, or empty. The caller cannot distinguish "this table genuinely has no such index" from "introspection failed and I am guessing".

The comment justifies the swallow with "let creation handle conflicts" — which is sound for the creation path, where a wrong-but-optimistic reading is corrected by the database rejecting a duplicate. It is not sound for the drift-detection path, which consumes the same function and has no such backstop.

Why the downstream matters

diffManagedIndexes (packages/drivers/driver-sql/src/schema-drift.ts) takes the declared-index-missing branch on exactly this input:

    const p = byName.get(e.name);
    if (!p) {
      out.push({ kind: 'index_mismatch',  actual: '(absent)', severity: 'warning',});

So a transient failure — SQLite busy, a WAL read landing mid-flush, any I/O hiccup — is not surfaced as an error. It is laundered into a confident, specific, false report that the database is missing indexes the metadata declares.

Why this was worth pulling out of #6522

In #6522 the stays silent on the SECOND boot… case is the only case in its file backed by a real on-disk database (tempDbFile() has exactly one call site, sql-driver-overlay-index-drift.test.ts:158); every other case is :memory:. It is therefore the only one exposed to WAL and real file I/O, and the mechanism above produces precisely its reported symptom — a non-empty drift under an assertion demanding [].

That makes it a better-shaped explanation for that flake than the shared-ledger story the card originally proposed (which is false by constructionruntimeCreatedIndexes is a per-instance field, sql-driver.ts:4511). But it is a defect in production driver code, not in a test, so it does not belong inside a flake card. #6522 stays where it is; this is the separable half.

What a dev should measure before changing anything

  1. Who consumes drift, and does anything act destructively on a false (absent)? severity: 'warning' suggests not, but that needs measuring rather than assuming — the neighbouring case "never points --allow-destructive at an index the framework created" exists because this area has had teeth before.
  2. Does the creation path actually still need the swallow? If yes, the fix is to keep it there and let the detection path see the error — not to remove it wholesale.
  3. Distinguishing failure from absence likely means returning an explicit outcome (or letting detection callers opt into throwing) rather than an ambiguous array. Worth pricing both.

⛔ Not a #5499 card — driver-sql is outside that freeze. ⛔ Ungraded on purpose; grading is triage's call.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions