Skip to content

fix(driver-sql): a failed index read is an error, not an empty index list (#7332) - #7394

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-7332-introspect-indexes-blind-catch
Aug 10, 2026
Merged

fix(driver-sql): a failed index read is an error, not an empty index list (#7332)#7394
os-zhuang merged 1 commit into
mainfrom
claude/issue-7332-introspect-indexes-blind-catch

Conversation

@claude

@claude claude Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Closes #7332.

What was wrong

SqlDriver.introspectIndexes wrapped its entire dialect dispatch — the SQLite, Postgres and MySQL branches alike — in one bare catch {} and returned its accumulator in whatever half-built state it had reached. The caller could not tell "this table genuinely has no such index" from "the read failed and I am guessing".

Drift detection consumes that same function. diffManagedIndexes takes its declared-index-missing branch on exactly that input, so a transient failure was not surfaced as an error — it was laundered into a confident, specific and false report. Measured on this branch before the fix, by failing PRAGMA index_list with SQLITE_BUSY against a table whose declared index demonstrably exists:

{
  "kind": "index_mismatch",
  "table": "product",
  "expected": "(code)",
  "actual": "(absent)",
  "severity": "warning",
  "category": "safe",
  "op": { "type": "create_index", "indexName": "idx_product_code", "columns": ["code"] },
  "message": "product: metadata declares index 'idx_product_code' (code) but the database has no such index — run \"os migrate apply\" to create it."
}

What changed

packages/drivers/driver-sql/src/sql-driver.ts only — the swallow is split by call site, not removed.

  • introspectIndexes(tableName, opts?) now throws by default, and takes { onFailure: 'partial' } for a caller that can correct a short read.
  • getExistingIndexNames — the creation seam — passes { onFailure: 'partial' }. Nothing else does.
  • Both doc comments rewritten to state which call site the best-effort reading belongs to and why.

Its return type is unchanged, so sql-driver-overlay-index-drift.test.ts:194 (which types the direct call as PhysicalIndex[]) still compiles and passes untouched.

Why the swallow stays on creation

Its stated justification — "let creation handle conflicts" — is sound exactly where it was written. syncDeclaredIndexes calls getExistingIndexNames on its first line, outside any try: a throw there takes the whole boot down on a transient read. And the backstop is real and verified — the create is attempted and the duplicate absorbed by the /already exists|duplicate key name|exists/i branch in the same function.

Detection has no such backstop, and inherited the swallow only because #3728 wired a second consumer onto a function written for the first. Its callers were already built for a throw:

  • reconcileAndWarnDrift wraps detectTableDrift in catch → logger.warn("could not introspect '<table>' for drift detection") → return. That handler was reachable only via introspectColumns, which has never swallowed — the index dimension was the odd one out in its own detect path.
  • os migrate plan and os migrate apply both catch → printError → this.exit(1).

Not a destructive defect — measured, and pinned

No consumer ever acted destructively on the false (absent). Dropping entries from the physical list is monotone: replace_unique_index needs the legacy index present, drop_index needs the orphan present, recreate_index needs the declared name present. A short read can therefore only remove a destructive proposal, never arm one. The last case in the new test pins that direction rather than leaving it as prose.

Tests

New file sql-driver-index-introspection-failure.test.ts (8 cases). Reverse-verified: 4 predicted red, 4 predicted green as controls — measured exactly 4/4 before the fix, 8/8 after.

Relationship to #6522 — unproven

#6522 closed as not reproduced in 67 runs. This is a better-shaped hypothesis for that flake, not a demonstrated cause — nobody has reproduced it, and nothing here claims to fix it. sql-driver-overlay-index-drift.test.ts is untouched by this PR.


Generated by Claude Code

…list (#7332)

`SqlDriver.introspectIndexes` wrapped its entire dialect dispatch — SQLite,
Postgres and MySQL alike — in one bare `catch {}` and returned its accumulator
half-built. Callers could not tell "this table genuinely has no such index"
from "the read failed and I am guessing".

`diffManagedIndexes` takes its declared-index-missing branch on exactly that
input, so a transient SQLITE_BUSY or a WAL read landing mid-flush became a
confident, specific and false `actual: '(absent)'` report about an index that
was there the whole time.

Split the swallow by call site rather than removing it. Its justification —
"let creation handle conflicts" — holds at `getExistingIndexNames`, whose
caller `syncDeclaredIndexes` corrects an optimistic wrong reading by attempting
the create and absorbing "already exists"; a throw there would take a boot down
on a transient read. Detection has no such backstop and inherited the swallow
only because #3728 wired a second consumer onto the same function.
`introspectIndexes` now throws by default and takes an explicit
`{ onFailure: 'partial' }` opt-in that only the creation seam passes.

The detection callers were already built for this: `reconcileAndWarnDrift`
catches and warns "could not introspect '<table>' for drift detection", and
`os migrate plan` / `apply` both catch, print and exit non-zero. The sibling
read in the same detect path, `introspectColumns`, has never swallowed.

Measured: no consumer ever acted destructively on the false reading. Dropping
entries from the physical list is monotone — `replace_unique_index`,
`drop_index` and `recreate_index` all require an index to be present — so a
short read can only remove a destructive proposal, never arm one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wv1i1AwBy8eETqaDCXV6B
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 9:11am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-sql.

8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-sql)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-sql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/packages.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-sql)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/driver-sql)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31377184925 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Temporal Conformance (live PG + MySQL) — 失败步骤: Run driver-sql suite against both live servers

    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mretires a legacy global unique index and replaces it with the composite
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mretires the legacy `uniq_<table>_<col>` index left by the drift rebuild path
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mbare-composite tightening + duplicate pre-flight (ADR-0120 D4)�[2m > �[22m�[2ma
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mbare-composite tightening + duplicate pre-flight (ADR-0120 D4)�[2m > �[22m�[2mB
    �[22m�[39m[schema-drift] product: cannot tighten 'uniq_product_organization_id_code' as UNIQUE (COALESCE(organization_id, '__global__'), code) — existing rows already violate the NULL-safe unique cons
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mbare-composite tightening + duplicate pre-flight (ADR-0120 D4)�[2m > �[22m�[2mB
    �[22m�[39m[schema-drift] REFUSING to rebuild 'uniq_product_organization_id_code' on 'product' as a NULL-safe unique — 1 duplicate group(s) violate it (e.g. organization_id="__global__", code="DUP" × 2
    �[41m�[1m FAIL �[22m�[49m src/adr0120-three-posture-conformance.test.ts�[2m > �[22mADR-0120 — one app package, three tenancy postures (S13/S14 acceptance)�[2m > �[22mposture portability�[2m > �[22ma p
    �[41m�[1m FAIL �[22m�[49m src/sql-driver-autonumber-persistence.test.ts�[2m > �[22mSqlDriver auto_number — persistence across driver restart�[2m > �[22mcontinues the sequence from persisted state afte
    �[41m�[1m FAIL �[22m�[49m src/sql-driver-autonumber-persistence.test.ts�[2m > �[22mSqlDriver auto_number — persistence across driver restart�[2m > �[22mkeeps per-tenant sequences independent across a 
    �[41m�[1m FAIL �[22m�[49m src/sql-driver-datetime-mysql-storage.test.ts�[2m > �[22mMySQL TIMESTAMP → DATETIME(3) migration (#3942)�[2m > �[22mis idempotent — a second sync leaves the schema alone
    �[41m�[1m FAIL �[22m�[49m src/sql-driver-datetime-mysql-storage.test.ts�[2m > �[22mMySQL TIMESTAMP → DATETIME(3) migration (#3942)�[2m > �[22mkeeps the audit default, so inserts do not start writing N
    

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 12 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit ef678d0 Aug 10, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7332-introspect-indexes-blind-catch branch August 10, 2026 10:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants