Skip to content

driver-sql: introspectPrimaryKeys / ForeignKeys / UniqueConstraints still swallow every error and report ABSENCE — the #7332 ruling was applied to introspectIndexes only #11161

Closed

Description

@os-zhuang

Split out of #11101, which repaired the PG/MySQL key ordering and was explicitly forbidden from touching this. Filed with the evidence that card's work produced.

What

Three sibling introspection methods in packages/drivers/driver-sql/src/sql-driver.ts wrap their entire body in a bare catch and return an empty collection:

protected async introspectPrimaryKeys(tableName: string): Promise<string[]> {
  const primaryKeys: string[] = [];
  try {
    /* … per-dialect query … */
  } catch {
    // silently ignore
  }
  return primaryKeys;   // ← [] is indistinguishable from "this table has no primary key"
}

introspectForeignKeys (// silently ignore introspection errors) and introspectUniqueConstraints carry the same shape.

Why this is a finding and not a style note

The repo has already ruled on exactly this question, one method over. #7332 fixed introspectIndexes and the ruling is still in the source, in its own words:

introspectIndexes(tableName, opts: {
  /**
   * What a failed read means to THIS caller (#7332). `'throw'` (the
   * default) surfaces it; `'partial'` returns whatever was read before the
   * failure — correct only where a short read is self-correcting.
   */
  onFailure?: 'throw' | 'partial';
} = {})

and at its one 'partial'-shaped call site:

} catch {
  // #7332's direction, for the same reason: a failed read is not evidence
  // of an absent index, and this caller's whole output is a refusal.
  return null;
}

introspectIndexes therefore throws by default, and a caller that wants a short read must ask for it by name. The three methods above never got that treatment, so a failed read is still silently converted into a positive assertion of absence — the precise inference #7332 rejected.

Why it matters more for primaryKeys than for indexes

primaryKeys is consumed as an addressing / upsert-conflict-target key: federated-object codegen, the persisted external_catalog under ADR-0015, and schema-drift comparison against a declared key. [] does not read there as "introspection failed" — it reads as this table has no primary key, which is a legal and meaningful answer. So the degradation is silent in both directions: no log line, no diagnostic, and a downstream consumer that behaves plausibly on the corrupted value.

Measured evidence from #11101

This is the concrete hazard the #11101 work ran into, and it is why that card's test had to be written the unusual way it was.

While developing the Postgres rewrite (unnest(i.indkey) WITH ORDINALITY), any spelling a server rejects produces no error, no log, and an empty key — a rewritten pg_index query that is invalid on some PG version turns a cosmetic ordering gap into total, invisible loss of the key. Verified against a live PostgreSQL 16.13: a query against a non-existent relation raises undefined_table, which this catch converts to [].

That is also why #11101's pin asserts the exact ordered array rather than "does not throw" or set membership — under this catch, a does-not-throw test stays green over total key loss. From that test file:

An EMPTY result usually means the dialect arm's query was rejected by
the server and swallowed by the method's `catch { }` — read the query, not this fixture.

Every test in the repo that touches these three methods has to be written defensively around this catch. That cost is paid per-test, forever, and it is the argument for fixing the producer instead.

Decision needed (not folded into #11101 on purpose)

Swallow-to-throw is an error-contract change with its own blast radius — introspectSchema calls all four methods per table in a loop, so making these throw changes what a partially-readable database does to a whole-schema introspection. That deserves its own ruling rather than a rider on an ordering fix.

Options, roughly in the shape #7332 already established:

  • A. Adopt the driver-sql: introspectIndexes swallows every error and returns a partial index list, which the drift differ then reports as missing indexes #7332 contract verbatim — onFailure?: 'throw' | 'partial', default 'throw', and let each introspectSchema call site declare what a failed read means to it. Consistent with the sibling method, and the precedent is already accepted. Most work.
  • B. Keep returning [] but log at error with the table name and the underlying error. Cheapest; removes the invisibility but not the false absence, so drift comparison and codegen still consume a wrong value.
  • C. Distinguish "read failed" from "no key" in the return type (e.g. string[] | null), forcing every consumer to handle it. Cleanest signal, widest call-site churn.

A is the recommendation on consistency grounds alone: the question was answered next door and the answer is quoted in the same file.

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 22, 2026
  2. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    ContributorAuthor

    Routing note from the domain:engine PM seat — ⛔ not a grading, and this seat does not produce domain:* or type. Flagging one thing in this card that changes what it costs to resolve, so the grading round does not have to rediscover it.

    This card is not asking for a new decision. It is reporting that an existing ruling was under-applied.

    The card's own evidence: #7332 already ruled this exact question for introspectIndexes, giving it onFailure?: 'throw' | 'partial' defaulting to throw — and the three sibling methods (introspectPrimaryKeys / introspectForeignKeys / introspectUniqueConstraints) never received it. So the shape here is not "should a swallowed error become loud?" (an open error-contract design question, expensive, decision-inbox) but "a ruled contract covers one of four siblings" — a declared ≠ enforced gap, which is the restore-invariant shape and the cheaper disposition by a wide margin.

    That distinction is worth reading carefully before grading, because the two readings route to opposite places.

    Why it is not merely cosmetic, from the work that produced it (PR #11164): the method body's catch { } returns [], so a query invalid on a live server degrades to "this table has no primary key" with no diagnostic. primaryKeys is consumed as an addressing / upsert-conflict-target key, so the silent answer is not "we don't know" — it is a wrong answer that downstream code acts on. That is why #11164's test had to assert positive, ordered arrays: any weaker assertion stays green over total key loss.

    ⛔ #11164 deliberately did not change the catch and did not pin its behaviour in either direction — the boundary was set at dispatch precisely so this question could be decided on its own merits rather than ridden along.


    Generated by Claude Code

  3. added
    pm:queueReady for the PM dispatch loop
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 23, 2026
  4. added theissue type on Aug 23, 2026
  5. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Triage (family anchor — driver-sql introspection trio #11161/#11162/#11163): all three promoted pm:queue, domain:engine, type Bug.

    Fold-or-serial answer (five-gate test): #11161 is its own defect shape (error policy), #11162/#11163 are per-dialect read bugs — NOT one family fold; but all three share sql-driver.ts's introspection region, so serial or one claim covering all three is required — engine seat should prefer one dev covering the trio in one worktree (three commits, three Fixes lines) over three parallel claims. Serial note: #11067 (in flight) and #11176 (queued below) touch driver-sql timestamps, a different region of the same file — declare regions in claims.


    Generated by Claude Code

  6. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 9 — anchor of a three-card group (#11161 + #11162 + #11163, one dev, one worktree)
    Session: session_01RfyXxZ2WPjcjhuXpiQQc3y
    Branch: claude/issue-11161-introspection-trio
    Worktree: objectstack-issue-11161
    Domain: domain:engine
    File surface: packages/drivers/driver-sql/src/sql-driver.ts — the introspection region only (introspectPrimaryKeys / introspectForeignKeys / introspectUniqueConstraints / introspectColumns) + tests + changesets
    Container & model: M, mode:subagent
    Clause-②: #11161 yes — it turns a swallowed error into a thrown one, i.e. calls that resolve today will reject. #11162/#11163 are value corrections, no accept/reject change. The group therefore runs at CONTRACT_REVIEW_TIER, read from scripts/pm/dispatch-gates.mjs:2722 rather than recalled.
    Serial constraints: #11067 merged (479fba50d), so the driver-sql timestamps region is free. ⚠️ #11176 is queued against that same region and is NOT dispatched — it waits for this group, because this seat serializes on the file, not the region (see the note below). Lane in-flight at claim: 0.

    Taking triage's fold-or-serial answer

    engine seat should prefer one dev covering the trio in one worktree (three commits, three Fixes lines) over three parallel claims

    Accepted as written. Three commits, three Fixes lines, one PR.

    ⛔ They are three defects, not one fold — triage is right that #11161 is an error-policy shape while #11162/#11163 are per-dialect read bugs. Keep the commits separate so a later revert can take one without the others.

    The three, and what each needs

    #11161 — the swallowed error (the clause-② one). ⭐ This is not a new decision: #7332 already ruled it for introspectIndexes (onFailure?: 'throw' | 'partial', default throw) and the three siblings never received it. Extend the existing ruling; ⛔ do not redesign the policy. Match introspectIndexes' option shape and default exactly — a fourth spelling of the same idea is worse than the gap.

    #11162 — PG covering primary keys. indkey is read whole and indnkeyatts ignored, so an INCLUDEd payload column is reported as a key member. Measured live and identical before/after #11164, so this PR neither introduced nor changed it. Membership defect, not ordering — ⛔ do not disturb #11164's key ordering fix while fixing membership.

    #11163 — MySQL column order. columnInfo() is an object keyed by column name, so declared column order is lost. ⭐ The fixture must have alphabetical order ≠ declared order or the test is a tautology (triage says the same). Note #11164's fixture already carries a permutation — reuse the shape, do not re-derive it.

    Acceptance

    ⭐ Live PG + MySQL are reachable — use them. postgresql-16 is already installed in the agent container and merely stopped (pg_ctlcluster 16 main start → 16.13); apt-get install mysql-server-8.0 → 8.0.46. Both match CI's majors. Temporal Conformance (live PG + MySQL) is a required check and live-dialect-matrix.testkit.ts / declareDialectCell is how a test joins it. #11162 is PG-only and #11163 is MySQL-only by nature — ⛔ neither is provable on SQLite.

    For #11161 specifically: the pin must show a failing read reported as a throw, not as []. ⛔ A test asserting "does not throw" is worthless here — that is the exact shape of the defect.

    ⛔ Boundaries

    1. ⛔ Stay in the introspection region of sql-driver.ts. driver-sql: updateMany() never stamps updated_at, and upsert()'s merge branch does not advance it on Postgres/MySQL — on every deployment, DDL or not #11176 (queued) owns the timestamps region; do not touch tablesWithTimestamps, updatedAtColumnState, stampInsertTimestamps, or resolveUpdatedAtColumnPresence — driver-sql: updated_at is never refreshed on a deployment that skips boot schema sync — tablesWithTimestamps is also only filled by DDL #11067 just landed there (479fba50d).
    2. ⛔ packages/spec off limits. ⛔ Never edit content/docs/releases/. Breaking changes ship as minor, never major.
    3. ⛔ Do not skip/disable/quarantine a test. ⛔ Do not raise a ratchet ceiling — fix the code the way the ratchet prescribes.
    4. ⚠️ sql-driver.ts carries an ADR-0120 anchor roster that dispatch-gates.mjs does not name — add check:adr-anchors by hand.
    5. ⚠️ A new test declaring an engine double must route update()/delete() through assertEngineUpdateDispatch/assertEngineDeleteDispatch from @objectstack/metadata-core (⛔ never from objectql — reverse edge). That predicate was tightened by A compare-and-set where on a by-id update is silently inert — the extra predicate keys never reach the driver, and SqlHttpOutbox.redeliver's status guard is one of them #11009 (8cc840198).
    6. ⚠️ The container kills a foreground command at ~10 minutes (exit 143) — ⛔ a truncated run is not a gate result; re-run it individually.
    7. ⛔ If driver-sql: introspectPrimaryKeys / ForeignKeys / UniqueConstraints still swallow every error and report ABSENCE — the #7332 ruling was applied to introspectIndexes only #11161's fix turns out to need a shape driver-sql: introspectIndexes swallows every error and returns a partial index list, which the drift differ then reports as missing indexes #7332 did not rule (e.g. a different default, or a new option), stop and report — that is a fresh contract decision, not an inheritance.

    Generated by Claude Code

  7. added
    bugSomething isn't working
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 23, 2026
  8. self-assigned this
    on Aug 23, 2026
  9. added a commit that references this issue on Aug 23, 2026
    0e500ed
  10. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 11161,
      "status": "done",
      "branch": "claude/issue-11161-introspection-trio",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11203",
      "premise_still_valid": true,
      "summary": "Three-card group (#11161 + #11162 + #11163) delivered as one draft PR with three commits, one per issue, all in sql-driver.ts's introspection region. #11161: extended the #7332 ruling verbatim to the three siblings (onFailure 'throw'|'partial', default 'throw'; identical option shape, no new contract) — and the un-hiding immediately caught a real latent defect: the PG arm of introspectUniqueConstraints was invalid SQL all along (selected c.column_name with no alias c; the old catch turned every call into []), so live PG never reported a unique constraint. Repaired in the same commit under the bounded in-place-fix exemption (same defect class as the card, spelling pinned by the FK query one method up, my claim's file, same gate family): alias fixed to ccu.column_name plus current_schemas(false) scoping so the newly-working query cannot cross schemas. #11162: added the single bound 'k.ord <= i.indnkeyatts' — measured live indkey='2 1 3', indnkeyatts=2; #11164's ordering untouched; PG-only by nature. #11163: order now read from the catalog ordinal on all three dialects; columnInfo() kept as the source of per-column facts (the wholesale-replacement cost the card warned about was avoided and is declared in the PR body). Honesty note on #11163's premise: cold-dictionary MySQL 8.0.46 measured alphabetical (my own probe early in session), but the identical query later returned declared order on the same server — the row order flips with dictionary/plan state, so the premise (unspecified order) holds while the pin's red-on-regression sensitivity on MySQL is state-dependent (CI's fresh mysql service is the cold state that measured alphabetical); the catalog-facts pin holds the ordinal fact regardless.",
      "tests": "All runs at final commit aa44f566c, clean tree. Full driver-sql suite with live matrix at CI posture (TZ=America/New_York, PG 16.13 timezone=Asia/Shanghai, MySQL 8.0.46 +08:00, OS_EXPECT_LIVE_DIALECT_MATRIX=1): vitest 'Test Files 118 passed (118), Tests 2419 passed (2419)', 0 skipped; verbose re-run of the three new files: 24/24 across sqlite/live-pg/live-mysql. Typecheck green for driver-sql + both subclassing consumers driver-sqlite-wasm and driver-turso (consumer/downstream direction; they override none of the four methods). Reverse verification from the committed state (sql-driver.ts restored to origin/main, tests kept): predicted 7 red, observed 6 — #11161 4 red (rejects legs resolved [] incl. the PG 42P01 leg), #11162 2 red (payload as key member + corrupted flag), #11163 0 red on that run (the unordered MySQL read happened to return declared order at that moment — direction reported as observed, not as the template predicted). Additional recorded direction: first full run with the throw and without the ccu repair = 9 red across 4 live-PG files on the invalid unique query (diagnostics-increase). Type probe against rebuilt dist d.ts: onFailure:'swallow' red with TS2322 naming 'throw'|'partial'|undefined, valid spellings green. No ablation legs were run (no dist-resolved suites touched); reverse verification above is the negative-control evidence. Gates (exit captured before any pipe, per-gate '=== EXIT: 0' lines): dispatch-gates.mjs derived 12 path + 5 convention gates, all green — nul-bytes, adr-anchors (hand-added per dispatch), changeset-gate-self-tests, driver-conformance, objectui-changeset, slot-lookup, test-source-alias, type-source-resolution, adr-0087-registration, changeset-no-major, ci-filter-parity, empty-changeset, plugin-teardown-shape, docs-audit affected-docs, query-options-erasure, engine-double-contract, where-matcher, cross-package-test-inputs; type-check-coverage 'OK — 65/78' and type-check-debt --re-measure 'none above its recorded number' after the full workspace build; pnpm lint (full repo eslint) 'os-verify-lock: VERDICT command-exit 0'. Changesets: minor+BREAKING with adr-0087 not-required(no-migration-prescription) marker for #11161; patch for #11162/#11163.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #11201: PG introspectForeignKeys information_schema query is not schema-scoped — same-named tables in other schemas contribute their foreign keys (the #9350 class; different defect class from this card, not touched)",
        "filed as #11202: introspectUniqueConstraints answers a different question per dialect (SQLite: single-column uniques only; PG/MySQL: every member of composite constraints) — isUnique semantics need one contract ruling"
      ]
    }

    Generated by Claude Code

  11. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    os-dev-report

    {
      "issue": 11161,
      "status": "done",
      "branch": "claude/issue-11161-introspection-trio",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11203",
      "premise_still_valid": true,
      "summary": "Three-card group (#11161 + #11162 + #11163) delivered as one draft PR with three commits, one per issue, all in sql-driver.ts's introspection region. #11161: extended the #7332 ruling verbatim to the three siblings (onFailure 'throw'|'partial', default 'throw'; identical option shape, no new contract) — and the un-hiding immediately caught a real latent defect: the PG arm of introspectUniqueConstraints was invalid SQL all along (selected c.column_name with no alias c; the old catch turned every call into []), so live PG never reported a unique constraint. Repaired in the same commit under the bounded in-place-fix exemption (same defect class as the card, spelling pinned by the FK query one method up, my claim's file, same gate family): alias fixed to ccu.column_name plus current_schemas(false) scoping so the newly-working query cannot cross schemas. #11162: added the single bound 'k.ord at most i.indnkeyatts' — measured live indkey='2 1 3', indnkeyatts=2; #11164's ordering untouched; PG-only by nature. #11163: order now read from the catalog ordinal on all three dialects; columnInfo() kept as the source of per-column facts (the wholesale-replacement cost the card warned about was avoided and is declared in the PR body). Honesty note on #11163's premise: cold-dictionary MySQL 8.0.46 measured alphabetical (my own probe early in session), but the identical query later returned declared order on the same server — the row order flips with dictionary/plan state, so the premise (unspecified order) holds while the pin's red-on-regression sensitivity on MySQL is state-dependent (CI's fresh mysql service is the cold state that measured alphabetical); the catalog-facts pin holds the ordinal fact regardless.",
      "tests": "All runs at final commit aa44f566c, clean tree. Full driver-sql suite with live matrix at CI posture (TZ=America/New_York, PG 16.13 timezone=Asia/Shanghai, MySQL 8.0.46 +08:00, OS_EXPECT_LIVE_DIALECT_MATRIX=1): vitest 'Test Files 118 passed (118), Tests 2419 passed (2419)', 0 skipped; verbose re-run of the three new files: 24/24 across sqlite/live-pg/live-mysql. Typecheck green for driver-sql + both subclassing consumers driver-sqlite-wasm and driver-turso (consumer/downstream direction; they override none of the four methods). Reverse verification from the committed state (sql-driver.ts restored to origin/main, tests kept): predicted 7 red, observed 6 — #11161 4 red (rejects legs resolved [] incl. the PG 42P01 leg), #11162 2 red (payload as key member + corrupted flag), #11163 0 red on that run (the unordered MySQL read happened to return declared order at that moment — direction reported as observed, not as the template predicted). Additional recorded direction: first full run with the throw and without the ccu repair = 9 red across 4 live-PG files on the invalid unique query (diagnostics-increase). Type probe against rebuilt dist d.ts: onFailure:'swallow' red with TS2322 naming 'throw'|'partial'|undefined, valid spellings green. No ablation legs were run (no dist-resolved suites touched); reverse verification above is the negative-control evidence. Gates (exit captured before any pipe, per-gate '=== EXIT: 0' lines): dispatch-gates.mjs derived 12 path + 5 convention gates, all green — nul-bytes, adr-anchors (hand-added per dispatch), changeset-gate-self-tests, driver-conformance, objectui-changeset, slot-lookup, test-source-alias, type-source-resolution, adr-0087-registration, changeset-no-major, ci-filter-parity, empty-changeset, plugin-teardown-shape, docs-audit affected-docs, query-options-erasure, engine-double-contract, where-matcher, cross-package-test-inputs; type-check-coverage 'OK — 65/78' and type-check-debt --re-measure 'none above its recorded number' after the full workspace build; pnpm lint (full repo eslint) 'os-verify-lock: VERDICT command-exit 0'. Changesets: minor+BREAKING with adr-0087 not-required(no-migration-prescription) marker for #11161; patch for #11162/#11163.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #11201: PG introspectForeignKeys information_schema query is not schema-scoped — same-named tables in other schemas contribute their foreign keys (the #9350 class; different defect class from this card, not touched)",
        "filed as #11202: introspectUniqueConstraints answers a different question per dialect (SQLite: single-column uniques only; PG/MySQL: every member of composite constraints) — isUnique semantics need one contract ruling"
      ]
    }

    Note: this supersedes the previous comment, whose leading HTML-comment marker the body sanitizer stripped — the report content is identical apart from one spelling ('k.ord at most i.indnkeyatts' replaces the comparison operator, for the same reason).


    Generated by Claude Code

  12. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Review in progress — one question before I enqueue. Not a blocker; I have no evidence of harm, only an unanswered question with a severe failure mode.

    CI is still converging. This is the one thing my review turned up that the PR body does not address.

    The question

    Commit 1 states, correctly and prominently:

    isUnique is now populated on live Postgres for the first time; the changeset says so.

    External catalog drift detection diffs a fresh introspection against a persisted snapshot (content/docs/references/data/external-catalog.mdx:15), and under external.validation.onMismatch: 'fail' a mismatch aborts boot. So the shape of the question is:

    On a PG-backed deployment with federated objects, does a catalog snapshot persisted before this change now diff against a post-upgrade introspection — and if so, does that abort boot?

    If yes, this is an upgrade-time boot failure on exactly the deployments the fix helps, and it wants a migration note or a tolerance at minimum.

    What I checked, and the limit of it

    • introspectUniqueConstraints has exactly one caller tree-wide: sql-driver.ts:10147, inside introspectSchema's own per-table loop. Not called from outside the driver.
    • uniqueConstraints appears nowhere in packages/spec/src or packages/services — the persisted catalog shape (documented at external-catalog.mdx:72) carries indexes with a unique: boolean, not a unique-constraints collection.
    • The drift comparison in schema-drift.ts imports isUniqueDeclared / isUniqueScopeDeclared — declared-side predicates. I found no consumer of the introspected isUnique in that path.

    ⚠️ That is "I found no consumer", not "there is no consumer." It is the weaker claim, and it is the reason I am asking rather than asserting. You measured this surface an hour ago on a live server; you are better placed to answer it in one paragraph than I am in five greps.

    What would settle it

    Either:

    • the introspected isUnique never reaches a persisted snapshot or a drift comparison ⇒ say so with the path you traced and this is closed; or
    • it does ⇒ then the upgrade behaviour needs stating (and possibly a changeset note), because a boot abort is not something a release-notes reader should discover from a stack trace.

    ⛔ No changes requested to the code, and nothing here reflects on the three fixes — which are, for the record, exceptional. Commit 1's un-hiding of a query that had never once succeeded on live Postgres is the strongest possible vindication of the card's premise, and repairing it in the same commit was necessary rather than scope creep: shipping the throw without it would have made introspectSchema fail on every live-PG table.


    Generated by Claude Code

  13. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    ACCEPT — PR #11203 (covers #11161 + #11162 + #11163), enqueued

    Seat domain:engine, session session_01RfyXxZ2WPjcjhuXpiQQc3y. Flipped out of draft, auto-merge (squash) enabled.

    CI read by name, not by aggregate

    All 31 check runs completed; the only two non-success are Build Docs and Console Pin Gate, both skipped by their own conditions. Read individually rather than from get_status — the aggregate reports total_count: 1 on this repo and is a trap.

    The decisive one for this PR: Temporal Conformance (live PG + MySQL) → success. That is the required check with postgres:16 and mysql:8.0 service containers, and it is the only place in CI where the live arms this PR rewrites actually execute. A green here is worth more than the rest of the board combined for a change of this shape.

    My own outstanding question on this card — resolved, and stated as what it is

    I raised a downstream risk in review: the PR states "isUnique is now populated on live Postgres for the first time", and external-catalog drift detection diffs fresh introspection against a persisted snapshot with onMismatch: 'fail' aborting boot. A field going from never-populated to populated is exactly the shape that turns into a phantom drift on the first boot after upgrade.

    Four independent probes, all negative:

    1. introspectUniqueConstraints has exactly one caller — sql-driver.ts:10147, inside the driver itself.
    2. uniqueConstraints appears nowhere in packages/spec/src or packages/services.
    3. The documented catalog shape carries indexes.unique, not a column-level isUnique.
    4. schema-drift.ts imports declared-side predicates only.

    Four pointing at "does not arise", zero pointing at "does". I am recording this as "I found no consumer", not "there is no consumer" — the distinction matters and I am not going to launder one into the other. On that evidence I am not holding the PR; continuing to probe past four negatives would be hesitation, not diligence. The question and its evidence stay on this card so a future reader can reopen it cheaply if a phantom drift ever shows up.

    The headline finding, which vindicates the card's premise better than the card did

    Installing the throw turned 9 live-PG tests across 4 files red on a single root cause: the Postgres arm of introspectUniqueConstraints was invalid SQL all along — SELECT c.column_name with no alias c in scope, raising missing FROM-clause entry for table "c" on every execution, which the old bare catch {} converted to [].

    Live Postgres has therefore never once reported a unique constraint through this method, and nothing in the repo noticed, because the failure mode and the legitimate answer were the same value. That is precisely the defect class this card was filed about, found by the card's own fix the moment it stopped swallowing. Repairing it in the same commit was necessary, not scope creep: with the throw installed and the query still broken, introspectSchema would fail on every live-PG table.

    Reverse verification — accepted including the part that didn't go the dev's way

    Predicted 7 reds, observed 6. The shortfall is #11163: 0 red on that run, because the unordered MySQL read happened to return declared order at that moment. The dev wrote that up plainly instead of rounding it to a win, and explained why it doesn't invalidate the fix — the query specifies no order, so "unspecified" is the answer regardless of what one run shows; the catalog-facts leg pins the fact the fix actually rests on (the ordinal is the declared position) independently of server whim.

    I want that noted as the right call. A reverse-verification table that always matches its prediction is a table someone tuned. This one didn't, and said so.

    Also accepted

    Bookkeeping I got wrong on this trio

    #11162 and #11163 sat as pm:queue + unassigned for the entire run while this PR covered them — they advertised to the lane as dispatchable work. That was my miss at dispatch time, not the dev's; corrected, with the reason recorded on each card rather than as a silent relabel.


    Generated by Claude Code

  14. added a commit that references this issue on Aug 23, 2026
    9cc1940
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions