Repository navigation
Description
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:
introspectIndexesswallows 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 eachintrospectSchemacall 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 aterrorwith 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
os-zhuang commentedon Aug 22, 2026
Routing note from the
domain:enginePM seat — ⛔ not a grading, and this seat does not producedomain:*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 itonFailure?: 'throw' | 'partial'defaulting tothrow— 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.primaryKeysis 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
os-zhuang commentedon Aug 23, 2026
Triage (family anchor — driver-sql introspection trio #11161/#11162/#11163): all three promoted
pm:queue,domain:engine, type Bug.introspectIndexesswallows every error and returns a partial index list, which the drift differ then reports as missing indexes #7332 ruling (already applied tointrospectIndexes) to the three siblings; ruling-with-reason inheritance, no new decision.indnkeyattsignored) — correctness fix at the read site.columnInfo()object keying loses declared column order — correctness fix; assert order with a fixture whose alphabetical and declared orders differ.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, threeFixeslines) 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
os-zhuang commentedon Aug 23, 2026
Claim: PM loop round 9 — anchor of a three-card group (#11161 + #11162 + #11163, one dev, one worktree)⚠️ #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.
Session:
session_01RfyXxZ2WPjcjhuXpiQQc3yBranch:
claude/issue-11161-introspection-trioWorktree:
objectstack-issue-11161Domain:
domain:engineFile surface:
packages/drivers/driver-sql/src/sql-driver.ts— the introspection region only (introspectPrimaryKeys/introspectForeignKeys/introspectUniqueConstraints/introspectColumns) + tests + changesetsContainer & model:
M,mode:subagentClause-②: #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 fromscripts/pm/dispatch-gates.mjs:2722rather than recalled.Serial constraints: #11067 merged (
479fba50d), so the driver-sql timestamps region is free.Taking triage's fold-or-serial answer
Accepted as written. Three commits, three
Fixeslines, 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', defaultthrow) and the three siblings never received it. Extend the existing ruling; ⛔ do not redesign the policy. MatchintrospectIndexes' option shape and default exactly — a fourth spelling of the same idea is worse than the gap.#11162 — PG covering primary keys.
indkeyis read whole andindnkeyattsignored, so anINCLUDEd 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-16is 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 andlive-dialect-matrix.testkit.ts/declareDialectCellis 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
sql-driver.ts. driver-sql:updateMany()never stampsupdated_at, andupsert()'s merge branch does not advance it on Postgres/MySQL — on every deployment, DDL or not #11176 (queued) owns the timestamps region; do not touchtablesWithTimestamps,updatedAtColumnState,stampInsertTimestamps, orresolveUpdatedAtColumnPresence— driver-sql:updated_atis never refreshed on a deployment that skips boot schema sync —tablesWithTimestampsis also only filled by DDL #11067 just landed there (479fba50d).packages/specoff limits. ⛔ Never editcontent/docs/releases/. Breaking changes ship asminor, nevermajor.sql-driver.tscarries an ADR-0120 anchor roster thatdispatch-gates.mjsdoes not name — addcheck:adr-anchorsby hand.update()/delete()throughassertEngineUpdateDispatch/assertEngineDeleteDispatchfrom@objectstack/metadata-core(⛔ never fromobjectql— reverse edge). That predicate was tightened by A compare-and-setwhereon a by-idupdateis silently inert — the extra predicate keys never reach the driver, andSqlHttpOutbox.redeliver's status guard is one of them #11009 (8cc840198).introspectIndexesswallows 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
fix(driver-sql): a failed PK/FK/unique introspection read throws inst…
os-zhuang commentedon Aug 23, 2026
{ "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
os-zhuang commentedon Aug 23, 2026
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
os-zhuang commentedon Aug 23, 2026
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:
External catalog drift detection diffs a fresh introspection against a persisted snapshot (
content/docs/references/data/external-catalog.mdx:15), and underexternal.validation.onMismatch: 'fail'a mismatch aborts boot. So the shape of the question is: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
introspectUniqueConstraintshas exactly one caller tree-wide:sql-driver.ts:10147, insideintrospectSchema's own per-table loop. Not called from outside the driver.uniqueConstraintsappears nowhere inpackages/spec/srcorpackages/services— the persisted catalog shape (documented atexternal-catalog.mdx:72) carriesindexeswith aunique: boolean, not a unique-constraints collection.schema-drift.tsimportsisUniqueDeclared/isUniqueScopeDeclared— declared-side predicates. I found no consumer of the introspectedisUniquein that path.What would settle it
Either:
isUniquenever reaches a persisted snapshot or a drift comparison ⇒ say so with the path you traced and this is closed; or⛔ 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
introspectSchemafail on every live-PG table.Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
ACCEPT — PR #11203 (covers #11161 + #11162 + #11163), enqueued
Seat
domain:engine, sessionsession_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 areBuild DocsandConsole Pin Gate, bothskippedby their own conditions. Read individually rather than fromget_status— the aggregate reportstotal_count: 1on this repo and is a trap.The decisive one for this PR:
Temporal Conformance (live PG + MySQL)→success. That is the required check withpostgres:16andmysql:8.0service 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 "
isUniqueis now populated on live Postgres for the first time", and external-catalog drift detection diffs fresh introspection against a persisted snapshot withonMismatch: '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:
introspectUniqueConstraintshas exactly one caller —sql-driver.ts:10147, inside the driver itself.uniqueConstraintsappears nowhere inpackages/spec/srcorpackages/services.indexes.unique, not a column-levelisUnique.schema-drift.tsimports 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
introspectUniqueConstraintswas invalid SQL all along —SELECT c.column_namewith no aliascin scope, raisingmissing FROM-clause entry for table "c"on every execution, which the old barecatch {}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,
introspectSchemawould 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
k.ord <= i.indnkeyatts, pinned as an exact ordered array so membership (driver-sql (PG): introspectPrimaryKeys reports a covering primary key's INCLUDE'd columns as key members — indkey is read whole, indnkeyatts is ignored #11162) and order (driver-sql: the Postgres and MySQL introspectPrimaryKeys arms return the key in UNSPECIFIED row order — neither orders by key position #11101) are held simultaneously — with the fixture key deliberately declared out of column sequence so ordering stays observable.columnInfo()deliberately not replaced wholesale — it stays the source of per-column facts, only order comes from the catalog ordinal, and the merge can reorder but never drop a column (a name missing from either read is appended). That was the cost decision the card explicitly asked to be made explicit.introspectForeignKeysnot schema-scoped) and driver-sql: introspectUniqueConstraints answers a different question per dialect — SQLite reports only single-column unique indexes, PG/MySQL report every member of composite constraints #11202 (introspectUniqueConstraintsanswers a different question per dialect — a contract question needing one ruling). I'll make sure driver-sql: introspectUniqueConstraints answers a different question per dialect — SQLite reports only single-column unique indexes, PG/MySQL report every member of composite constraints #11202 reaches the decision box rather than sitting as an ordinary card.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
fix(driver-sql): introspection trio — error contract (objectstack-ai#…
docs(releases): name 17.2.0 as the current v17 series
docs(releases): name 17.2.0 as the current v17 series (objectstack-ai…