You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
driver-sql (PG): under a multi-schema search_path, introspectSchema keys tables by bare name, dropping a same-named table in the second schema, and reads a later schema's table as zero columns #24707
Filing gate ①, class (a): a real producer answers wrong, measured on a live database. Found by #24664's dev (PR #24706, out-of-scope finding 1) on a private PostgreSQL 16 cluster through SqlDriver.introspectSchema and listRemoteTables. The public HTTP door was not exercised. Filed by domain:services seat 2 (seat post #21118) · os-warren · session_01LR1cD9JqgkeqY7k9rrZH8s. ⛔ Not a claim. Grade and lane are triage's; the producer is packages/drivers/driver-sql.
reach: a Postgres external datasource whose default search_path names two schemas (ALTER DATABASE … SET search_path = public, sales), with public.customers, sales.orders and sales.customers.
What happens (measured):
introspectSchema lists the tables of every current_schemas(false) schema, keyed by BARE name (sql-driver.ts ~:4984). So sales.customers is silently dropped behind public.customers: one key, two tables.
Per-table columns come from knex columnInfo() on the bare name, which resolves only the first schema on the path. So sales.orders reports zero columns (measured: orders: columns=[]), and listRemoteTables answered it with columnCount: 0. A draft or catalog entry built from it would carry no fields.
Expected: every table the connection reaches is listed once with its own columns. Where one key cannot hold two same-named tables, say so loudly; never drop one, and never report a table with no columns because its columns were read from another schema.
objectstack, MCP search_issues "postgres introspectSchema search_path two schemas same-named table dropped introspectColumns columnInfo zero columns current schema": 8 hits, closed included. #11201 (foreign keys not schema-scoped, closed), #11163 (MySQL column order), #10998 (missing dialect / introspectedAt) and #7955 (the route twins' ?schema=) are neighbours. None covers a same-named table dropped or a zero-column table under a multi-schema search path.
Dedupe words: postgres introspectSchema search_path two schemas zero columns; introspectColumns columnInfo current_schema; same-named table second schema dropped introspection.
Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch. Fresh read on origin/main0904d856e3.
Lane. The producer is packages/drivers/driver-sql, which is domain:engine (SKILL.md:161).
Premise holds on the code path.sql-driver.ts:4984 scopes introspectSchema to current_schemas(false), and the result is keyed by bare name. The live PostgreSQL 16 measurement is the dev's.
Why p3.
It reaches only an external Postgres datasource whose search_path names more than one schema.
It is silent for the dropped twin, but loud enough for the zero-column table, which shows columnCount: 0 on the listing.
No row is written.
Shape.
Read each table's columns in its own schema (table_schema), not through columnInfo() on the bare name.
Where one bare key would hold two tables, say so loudly instead of dropping one.
Claim: PM loop round 13 · 2026-10-20T13:37Z · p3, domain:engine, after the triage 6282275557 (the maintainer's order admits p3 cards now). On PostgreSQL, SqlDriver.introspectSchema keys the tables of every current_schemas(false) schema by bare name, so a same-named table in a later schema is silently dropped. It also reads each table's columns through knex columnInfo() on the bare name, which resolves only the first schema on the path, so a later schema's table reports zero columns.
Session: session_01JtSQQ3EWsbNPzyYMug1tXy (the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)
Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24707-pg-multischema-introspect
Worktree: objectstack-issue-24707
Domain: domain:engine
Seat: domain:engine#1 (seat post #6367)
File surface:
Measured first, on a throwaway PostgreSQL cluster (as the finder did): a database whose search_path names public, sales, with public.customers, sales.orders and sales.customers. Read back what introspectSchema and listRemoteTables answer, before and after.
packages/drivers/driver-sql/src/sql-driver.ts, the PostgreSQL branch only:
introspectSchema: each table's columns are read in its own schema (table_schema), not through columnInfo() on the bare name;
where one bare key would hold two same-named tables from different schemas, the answer says so loudly instead of dropping one;
introspectColumns where it reads the columns.
Pins in driver-sql's tests: the zero-column table reads its own columns, and a same-named pair is never silently collapsed.
The changeset:@objectstack/driver-sql.
⛔ Not in scope: a schema member on IntrospectedTable (a packages/spec contract decision with no measured consumer, per the triage); the SQLite and MySQL branches; service-external-datasource's listRemoteTables, which is read only, as the consumer.
Container & model: M, mode:cloud (measured first on a live PostgreSQL cluster: the two defects before and after), model: TIER_DEFAULT (this run's dispatch-gates.mjs --tier: "no path-derived mandate")
Clause-②: no (narrowing) (a multi-schema PostgreSQL listing that silently dropped a same-named table now says so loudly, which may refuse a listing that answered today; it owes a CONTRACT_REVIEW_TIER review on the narrowing face before the queue)
Responsibility: an external Postgres datasource lists a table with no columns, or hides one of two same-named tables | introspection keys tables by bare name and reads columns through the first schema on the path | every external Postgres datasource whose search_path names more than one schema
Thread-read: 6282275557
Serial constraints cleared: open PRs' file lists and the pm:dispatched claims were read at this claim.
Dispatch · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-20T13:40Z: mode:cloud. The dev session is session_013timUNNCthRfQkhy9Hz2fV. It builds on branch claude/issue-24707-pg-multischema-introspect, under claim 6292170810. The review of record is at CONTRACT_REVIEW_TIER, on the narrowing face.
Measured first: the card's database, search_path = public, sales, on a throwaway PostgreSQL cluster, read back through introspectSchema (and listRemoteTables if reachable without a server). The before is recorded ahead of any edit.
What lands, PostgreSQL branch only:
each table's columns read in its own schema, by table_schema, not by the bare name;
a same-named pair across schemas answered loudly: a refusal naming both schemas, or a reported collision. The dev settles which by the four axes and names every caller of introspectSchema; never a silent drop;
the single-schema and default (public) answer unchanged;
pins on a live PostgreSQL with an ablation, and a minor BREAKING changeset for @objectstack/driver-sql.
Not built: a schema member on IntrospectedTable, any packages/spec change, the SQLite or MySQL branches, or any edit to service-external-datasource. If the right fix needs one, the dev stops and reports needs_decision.
{
"issue": 24707,
"status": "done",
"branch": "claude/issue-24707-pg-multischema-introspect",
"pr": "https://github.com/objectstack-ai/objectstack/pull/24920",
"session": "session_013timUNNCthRfQkhy9Hz2fV — this run's harness-stamped id (dispatched by domain:engine seat 1, session_01JtSQQ3EWsbNPzyYMug1tXy, under claim 6292170810)",
"premise_still_valid": true,
"summary": "Measured first on a throwaway PostgreSQL 16.15 cluster (local postgres binary, port 54724), the card's database with search_path = public, sales. At fcb40ad673, introspectSchema listed customers (public's 3 columns) and orders with columns [] and primaryKeys ['id']. sales.customers was absent, and listRemoteTables answered orders columnCount 0. Fix, PostgreSQL branch of SqlDriver.introspectSchema only: the listing also selects table_schema and its current_schemas(false) position. Each table's columns are read in its own schema (knex withSchema(schema).columnInfo() plus introspectColumnOrder with table_schema = ?, both via a new optional trailing schema parameter on the two protected methods; other callers' SQL is byte-identical). A bare name reached in more than one schema is refused with RESOURCE_CONFLICT / 409, naming each colliding table and its schemas in search_path order, plus the remedy (one schema per connection via the datasource's schema setting, or a rename). After, at 1eeab8089: the card's database answers the 409 through both introspectSchema and listRemoteTables. Without the twin, sales.orders reads its 4 columns (listRemoteTables columnCount 3 and 4). A single-schema control database's JSON answer (introspectedAt masked) has the same sha256 before and after, a273d463...f8. No spec change, no IntrospectedTable member, SQLite/MySQL untouched, service-datasource read only.",
"tests": "Head 1eeab8089 (after merging origin/main a8b44d407). Before (fcb40ad673) / after measurement: see summary; probe script in the session scratchpad, not committed. New pin file packages/drivers/driver-sql/src/sql-driver-24707-introspect-pg-multischema.test.ts (live-PG cell via declareDialectCell), 4 pins: non-vacuity (path is both schemas; bare columnInfo reads [] for the later table), later schema's table reads its own columns (exact array), same-named pair refused ({code: RESOURCE_CONFLICT, status: 409} + message names table and both schemas), single-schema control (schema-scoped read equals bare read). Live PG on the merged head: 'Tests 4 passed (4)', exit 0. Ablation, fix committed, node scripts/ablation-replace.mjs, one leg per mutation, test imports ../src so no dist leg: leg A, anchor 'this.introspectColumns(tableName, schemaByTable.get(tableName))' replaced by the bare call ('anchor 1 to 0, blob 74cd151dd2be to 352e20d7e04b'): pin 2 red, 'Tests 1 failed | 3 passed (4)'. Leg B, refusal line deleted ('anchor 1 to 0, blob 74cd151dd2be to ca243e1d6171'): pin 3 red, 'expected undefined to be defined', 1 failed | 3 passed. Both legs: 'ok restored: blob == HEAD (74cd151dd2be) and git diff HEAD is empty'. pnpm --filter @objectstack/driver-sql typecheck: exit 0 on b61cf5026 and on 1eeab8089 (tsconfig include src/**/*, so the test is covered). pnpm --filter @objectstack/driver-sql test (full, no live URL): exit 0, 'Test Files 224 passed | 12 skipped (236)', 'Tests 3687 passed | 209 skipped (3896)', on b61cf5026 (the merge brought only platform-objects files). The 21 introspection-family live files with OS_TEST_POSTGRES_URL: exit 0, 'Tests 170 passed | 10 skipped (180)' (skips = MySQL cells). pnpm --filter @objectstack/service-datasource test (consumer), after building its dependency closure: exit 0, 'Test Files 48 passed (48)', 'Tests 862 passed (862)'. pnpm --filter @objectstack/core test:repo: exit 0, 'Test Files 6 passed (6)', 'Tests 82 passed (82)'. pnpm exec turbo run build --concurrency=2 on 1eeab8089: exit 0, 'Tasks: 77 successful, 77 total'.",
"gates": "Local, head 1eeab8089: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 70 families over 3 paths (+271/-11). Every line was run with its exit code captured before any pipe: 70 x exit 0. Two families needed a second run. pnpm check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET, no dist) and was re-run after the full turbo build: exit 0, '112 published require entry point(s) across 69 package(s) load'. pnpm check:type-check-debt first hit my own 280 s per-command timeout (124) while the full build held the box, and was re-run: exit 0, '1 ledger entr(ies) re-measured ... none above its recorded number'. Reconciliation, node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE (cmd :: exit N form): exit 0, 'Run reconciliation — 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN', 'all 70 accounted famil(ies) carry one'. The clone was deepened first (git fetch --shallow-since=2026-07-13). check-changeset-no-major ran locally without --event, so it could not read the PR body's Clause-② line; CI's Check Changeset is the reading. CI check-runs on PR #24920: in_progress at report time, not waited for (os-dev contract). Line budget: none set. Diff per file: .changeset/24707-pg-multischema-introspect.md +19/-0; packages/drivers/driver-sql/src/sql-driver-24707-introspect-pg-multischema.test.ts +159/-0; packages/drivers/driver-sql/src/sql-driver.ts +93/-11.",
"files_changed": [
".changeset/24707-pg-multischema-introspect.md (+19/-0)",
"packages/drivers/driver-sql/src/sql-driver-24707-introspect-pg-multischema.test.ts (+159/-0)",
"packages/drivers/driver-sql/src/sql-driver.ts (+93/-11)"
],
"line_budget": "none set; +271/-11 over 3 files, per file as in files_changed",
"deviations": [
"Changeset level: minor with a BREAKING banner (FROM / TO / Fix), per the claim's 'Clause-②: no (narrowing)'. The measured fix DOES move an accept set: a multi-schema PG connection with a same-named pair answered introspectSchema successfully and now throws 409. So the narrowing is real, and no patch re-declaration is asked for. The zero-column half alone is a wrong answer made right; the changeset says so.",
"Pins run in CI only in main's full battery: Temporal Conformance (live PG + MySQL), ci.yml step 'Run driver-sql suite against both live servers'. In the PR's Test Core lane the file is a named skip (no OS_TEST_POSTGRES_URL). Locally they ran on my own throwaway cluster.",
"The prompt names the consumer 'service-external-datasource'. In this tree the package is @objectstack/service-datasource (packages/services/service-datasource, ExternalDatasourceService). That suite was run, and nothing in it was edited.",
"#11201 and #24664 / PR #24706 were read through the in-tree record (sql-driver-11201-introspect-fk-schema-scope.test.ts, the IntrospectedForeignKey docblock, external-datasource-service.ts unattributedSchemaFilterError and its #24664 notes) and the card body, not by opening those threads.",
"The throwaway PostgreSQL data directory lived at /tmp/os-issue-24707-pg, not in the scratchpad: the scratchpad's parent (/tmp/claude-0) is mode 700, which the postgres user cannot enter. It was stopped (postmaster PID 2522, the one I started) and deleted.",
"Writes: git push to my branch ran 5 times (the empty branch, then WIP commits and the merge, per os-dev's commit-and-push-each-step rule). It counts as the one 'git push' budget line.",
"Commit trailers: the AGENTS.md model-free pair (Claude-Session + Co-authored-by: Claude), per os-dev's rule that the repo's attribution outranks the harness reminder; the harness reminder named a model-bearing Co-Authored-By."
],
"mcp_calls": "3 — mcp__github__issue_read (get #24707), mcp__github__issue_read (get_comments #24707), mcp__github__pull_request_read (get #24920, draft and assignee read-back). No MCP write tool.",
"api_writes": "3 budget lines. (1) git push to claude/issue-24707-pg-multischema-introspect (5 pushes, branch only; not a REST write). (2) pr_create via scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches (request fw-20261020T143626Z-1ddc12, run 39440777841 success), executed by the relay as POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24920/assignees; read-back '9068 byte(s) sent, 9068 stored — identical', assignee os-project-manager. (3) This os-dev-report comment via scripts/pm/post-stamped.mjs: POST /repos/objectstack-ai/objectstack/issues/24707/comments.",
"open_questions": [
{
"question": "Same-named table pair across schemas on one PG search_path: refuse, or keep the first and name the other?",
"options": [
"A — refuse introspectSchema with RESOURCE_CONFLICT / 409, naming each colliding table and its schemas in search_path order, plus the remedy (one schema per connection via the datasource 'schema' setting, or a rename)",
"B — keep the first schema's table and report the other: with no spec change the only channel is a server log line",
"C (outside the claim's two options, considered and rejected) — key the shadowed twin as a qualified 'sales.customers'"
],
"recommendation": "A, built. Business need: only an external PG datasource whose search_path names 2+ schemas holding a same-named table can collide (the finder's measured shape). A single-schema connection, the default public deployment included, can never collide, so no internal caller that sees one schema is refused. Callers: ExternalDatasourceService listRemoteTables / generateObjectDraft / refreshCatalog / validateObject / testConnection, via ObjectQL.introspectDatasource or the driver directly. The admin connection test reaches it only as a last fallback after ping / checkHealth. No migration differ or os command calls it. All callers already surface a throw (testConnection answers {ok:false, error}). Long-term: B has no caller-visible channel without the spec member the triage withheld. Anti-AI-error: B's log line is invisible to HTTP/AI callers, i.e. the same silent drop. Startup focus: A adds no surface, option, key or gate. C is a context-dependent key spelling, the shape the IntrospectedForeignKey.referencedTable note (#11377) rejects."
},
{
"question": "Which error code and status for the refusal?",
"options": ["A — RESOURCE_CONFLICT / 409 (existing StandardErrorCode; two tables conflict on one key)", "B — VALIDATION_ERROR or DATABASE_ERROR / 400"],
"recommendation": "A, built: an existing ledger code, no new error code. The conflict is between two remote objects on one key, not a malformed request."
},
{
"question": "Prompt vs os-dev.md conflicts?",
"options": ["none found"],
"recommendation": "None. The prompt's 'service-external-datasource' is the in-tree @objectstack/service-datasource (see deviations)."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed (PR Acceptance notes) · read-only inference, not measured: introspectPrimaryKeys resolves by ?::regclass along search_path, so a VIEW or other relation named like a later schema's base table but sitting in an earlier schema would capture the primary-key read; the collision refusal judges base tables only · dedupe words: introspectPrimaryKeys regclass view shadows table search_path; pg primary key introspection wrong relation"
]
}
CONTRACT_REVIEW_TIER, narrowing face. Record 6294518392 PASS on 1eeab8089f. ACCEPT 6294549692. Not governed.
Queue
It entered the merge queue at 2026-10-20T15:33Z and left it merged at 2026-10-20T16:04Z. No gh-readonly-queue/main/pr-24920-* branch remains.
origin/main
74fcec1edb is on origin/main, one squash commit carrying PR #24920's 3 files, +271 / −11.
Card
Closed completed through Fixes #24707; pm:dispatched is cleared.
What main now holds:
On PostgreSQL, each introspected table reads its own schema's columns. A table that lives only in a later schema on the connection's search_path (sales.orders behind public) now reports its columns, where it reported none. listRemoteTables counts them, and a draft built from it has fields.
A table name reached in two schemas is refused, not silently dropped.introspectSchema throws RESOURCE_CONFLICT (status: 409), naming each colliding table with its schemas in search_path order, plus the remedy.
That pair is the driver's in-process contract.
The HTTP doors (the datasource admin routes and rest's external-datasource routes) answer it as 400 EXTERNAL_DATASOURCE_ERROR carrying the same message.
The boot-time federated validation rows the datasource's objects unreachable, with a warning; boot does not stop.
Who can meet it: only a saved external PostgreSQL datasource that sets no schema, on a server whose default search_path names two schemas holding a same-named base table.
The remedy: set the datasource's schema (it becomes the connection's search_path), or rename one table on the remote database. A deployment that needs both schemas uses one datasource per schema.
Unchanged: a connection that reaches one schema, the default public deployment included, answers exactly as before. SQLite and MySQL are untouched. There is no spec change.
Shipped as:minor of @objectstack/driver-sql, BREAKING (an accept-set narrowing), Clause-②: no (narrowing).
Still open beside it:
The four live-PostgreSQL pins skip by name in a PR's Test Core lane. They run in main's Temporal Conformance battery, and its first run after this merge is their CI measurement. The seat reads it.
introspectPrimaryKeys resolves by ?::regclass along the path, so a view in an earlier schema named like a later schema's base table could capture the key read. That is an inference, not measured; it is noted in the PR, not filed.
The dev session session_013timUNNCthRfQkhy9Hz2fV is archived.
Activity
objectstack-fleet commentedon Oct 20, 2026
Triage: p3 ·
bug· lanedomain:engine·area:api·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-20T02:57ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch. Fresh read on
origin/main0904d856e3.Lane. The producer is
packages/drivers/driver-sql, which isdomain:engine(SKILL.md:161).Premise holds on the code path.
sql-driver.ts:4984scopesintrospectSchematocurrent_schemas(false), and the result is keyed by bare name. The live PostgreSQL 16 measurement is the dev's.Why p3.
search_pathnames more than one schema.columnCount: 0on the listing.Shape.
table_schema), not throughcolumnInfo()on the bare name.schemamember onIntrospectedTablewithout a measured consumer. That is apackages/speccontract decision, and service-datasource:remote-tables?schema=never narrows on the in-tree SQL drivers, so a named schema answers 200 with the wrong tables (#24619 row c) #24664's dev found none.findingcomes off.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 13 · 2026-10-20T13:37Z · p3,
domain:engine, after the triage 6282275557 (the maintainer's order admits p3 cards now). On PostgreSQL,SqlDriver.introspectSchemakeys the tables of everycurrent_schemas(false)schema by bare name, so a same-named table in a later schema is silently dropped. It also reads each table's columns through knexcolumnInfo()on the bare name, which resolves only the first schema on the path, so a later schema's table reports zero columns.Session:
session_01JtSQQ3EWsbNPzyYMug1tXy(the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)Account:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24707-pg-multischema-introspectWorktree:
objectstack-issue-24707Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
search_pathnamespublic, sales, withpublic.customers,sales.ordersandsales.customers. Read back whatintrospectSchemaandlistRemoteTablesanswer, before and after.packages/drivers/driver-sql/src/sql-driver.ts, the PostgreSQL branch only:introspectSchema: each table's columns are read in its own schema (table_schema), not throughcolumnInfo()on the bare name;introspectColumnswhere it reads the columns.driver-sql's tests: the zero-column table reads its own columns, and a same-named pair is never silently collapsed.@objectstack/driver-sql.⛔ Not in scope: a
schemamember onIntrospectedTable(apackages/speccontract decision with no measured consumer, per the triage); the SQLite and MySQL branches;service-external-datasource'slistRemoteTables, which is read only, as the consumer.Container & model:
M,mode:cloud(measured first on a live PostgreSQL cluster: the two defects before and after),model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no (narrowing) (a multi-schema PostgreSQL listing that silently dropped a same-named table now says so loudly, which may refuse a listing that answered today; it owes a
CONTRACT_REVIEW_TIERreview on the narrowing face before the queue)Responsibility: an external Postgres datasource lists a table with no columns, or hides one of two same-named tables | introspection keys tables by bare name and reads columns through the first schema on the path | every external Postgres datasource whose search_path names more than one schema
Thread-read: 6282275557
Serial constraints cleared: open PRs' file lists and the
pm:dispatchedclaims were read at this claim.sql-driver.tsin hunks up to about:20313. This card's regions,introspectSchema(about:15450) andintrospectColumns(about:20907), are outside them. Whichever lands second mergesmain.domain:services' On PostgreSQL every webhook delivery is dropped at enqueue — "[webhook-auto-enqueuer] enqueue failed … Converting circular structure to JSON (Timeout)" — and the loss is logged at WARN #24835 (update()androtatedUpdateById(), about:8778and:10207) is a region apart.sql-driver.ts.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Dispatch ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-20T13:40Z:mode:cloud. The dev session issession_013timUNNCthRfQkhy9Hz2fV. It builds on branchclaude/issue-24707-pg-multischema-introspect, under claim 6292170810. The review of record is atCONTRACT_REVIEW_TIER, on the narrowing face.search_path = public, sales, on a throwaway PostgreSQL cluster, read back throughintrospectSchema(andlistRemoteTablesif reachable without a server). The before is recorded ahead of any edit.table_schema, not by the bare name;introspectSchema; never a silent drop;public) answer unchanged;minorBREAKING changeset for@objectstack/driver-sql.schemamember onIntrospectedTable, anypackages/specchange, the SQLite or MySQL branches, or any edit toservice-external-datasource. If the right fix needs one, the dev stops and reportsneeds_decision.sql-driver.tsin other regions; whichever lands second mergesmain.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{ "issue": 24707, "status": "done", "branch": "claude/issue-24707-pg-multischema-introspect", "pr": "https://github.com/objectstack-ai/objectstack/pull/24920", "session": "session_013timUNNCthRfQkhy9Hz2fV — this run's harness-stamped id (dispatched by domain:engine seat 1, session_01JtSQQ3EWsbNPzyYMug1tXy, under claim 6292170810)", "premise_still_valid": true, "summary": "Measured first on a throwaway PostgreSQL 16.15 cluster (local postgres binary, port 54724), the card's database with search_path = public, sales. At fcb40ad673, introspectSchema listed customers (public's 3 columns) and orders with columns [] and primaryKeys ['id']. sales.customers was absent, and listRemoteTables answered orders columnCount 0. Fix, PostgreSQL branch of SqlDriver.introspectSchema only: the listing also selects table_schema and its current_schemas(false) position. Each table's columns are read in its own schema (knex withSchema(schema).columnInfo() plus introspectColumnOrder with table_schema = ?, both via a new optional trailing schema parameter on the two protected methods; other callers' SQL is byte-identical). A bare name reached in more than one schema is refused with RESOURCE_CONFLICT / 409, naming each colliding table and its schemas in search_path order, plus the remedy (one schema per connection via the datasource's schema setting, or a rename). After, at 1eeab8089: the card's database answers the 409 through both introspectSchema and listRemoteTables. Without the twin, sales.orders reads its 4 columns (listRemoteTables columnCount 3 and 4). A single-schema control database's JSON answer (introspectedAt masked) has the same sha256 before and after, a273d463...f8. No spec change, no IntrospectedTable member, SQLite/MySQL untouched, service-datasource read only.", "tests": "Head 1eeab8089 (after merging origin/main a8b44d407). Before (fcb40ad673) / after measurement: see summary; probe script in the session scratchpad, not committed. New pin file packages/drivers/driver-sql/src/sql-driver-24707-introspect-pg-multischema.test.ts (live-PG cell via declareDialectCell), 4 pins: non-vacuity (path is both schemas; bare columnInfo reads [] for the later table), later schema's table reads its own columns (exact array), same-named pair refused ({code: RESOURCE_CONFLICT, status: 409} + message names table and both schemas), single-schema control (schema-scoped read equals bare read). Live PG on the merged head: 'Tests 4 passed (4)', exit 0. Ablation, fix committed, node scripts/ablation-replace.mjs, one leg per mutation, test imports ../src so no dist leg: leg A, anchor 'this.introspectColumns(tableName, schemaByTable.get(tableName))' replaced by the bare call ('anchor 1 to 0, blob 74cd151dd2be to 352e20d7e04b'): pin 2 red, 'Tests 1 failed | 3 passed (4)'. Leg B, refusal line deleted ('anchor 1 to 0, blob 74cd151dd2be to ca243e1d6171'): pin 3 red, 'expected undefined to be defined', 1 failed | 3 passed. Both legs: 'ok restored: blob == HEAD (74cd151dd2be) and git diff HEAD is empty'. pnpm --filter @objectstack/driver-sql typecheck: exit 0 on b61cf5026 and on 1eeab8089 (tsconfig include src/**/*, so the test is covered). pnpm --filter @objectstack/driver-sql test (full, no live URL): exit 0, 'Test Files 224 passed | 12 skipped (236)', 'Tests 3687 passed | 209 skipped (3896)', on b61cf5026 (the merge brought only platform-objects files). The 21 introspection-family live files with OS_TEST_POSTGRES_URL: exit 0, 'Tests 170 passed | 10 skipped (180)' (skips = MySQL cells). pnpm --filter @objectstack/service-datasource test (consumer), after building its dependency closure: exit 0, 'Test Files 48 passed (48)', 'Tests 862 passed (862)'. pnpm --filter @objectstack/core test:repo: exit 0, 'Test Files 6 passed (6)', 'Tests 82 passed (82)'. pnpm exec turbo run build --concurrency=2 on 1eeab8089: exit 0, 'Tasks: 77 successful, 77 total'.", "gates": "Local, head 1eeab8089: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 70 families over 3 paths (+271/-11). Every line was run with its exit code captured before any pipe: 70 x exit 0. Two families needed a second run. pnpm check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET, no dist) and was re-run after the full turbo build: exit 0, '112 published require entry point(s) across 69 package(s) load'. pnpm check:type-check-debt first hit my own 280 s per-command timeout (124) while the full build held the box, and was re-run: exit 0, '1 ledger entr(ies) re-measured ... none above its recorded number'. Reconciliation, node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE (cmd :: exit N form): exit 0, 'Run reconciliation — 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN', 'all 70 accounted famil(ies) carry one'. The clone was deepened first (git fetch --shallow-since=2026-07-13). check-changeset-no-major ran locally without --event, so it could not read the PR body's Clause-② line; CI's Check Changeset is the reading. CI check-runs on PR #24920: in_progress at report time, not waited for (os-dev contract). Line budget: none set. Diff per file: .changeset/24707-pg-multischema-introspect.md +19/-0; packages/drivers/driver-sql/src/sql-driver-24707-introspect-pg-multischema.test.ts +159/-0; packages/drivers/driver-sql/src/sql-driver.ts +93/-11.", "files_changed": [ ".changeset/24707-pg-multischema-introspect.md (+19/-0)", "packages/drivers/driver-sql/src/sql-driver-24707-introspect-pg-multischema.test.ts (+159/-0)", "packages/drivers/driver-sql/src/sql-driver.ts (+93/-11)" ], "line_budget": "none set; +271/-11 over 3 files, per file as in files_changed", "deviations": [ "Changeset level: minor with a BREAKING banner (FROM / TO / Fix), per the claim's 'Clause-②: no (narrowing)'. The measured fix DOES move an accept set: a multi-schema PG connection with a same-named pair answered introspectSchema successfully and now throws 409. So the narrowing is real, and no patch re-declaration is asked for. The zero-column half alone is a wrong answer made right; the changeset says so.", "Pins run in CI only in main's full battery: Temporal Conformance (live PG + MySQL), ci.yml step 'Run driver-sql suite against both live servers'. In the PR's Test Core lane the file is a named skip (no OS_TEST_POSTGRES_URL). Locally they ran on my own throwaway cluster.", "The prompt names the consumer 'service-external-datasource'. In this tree the package is @objectstack/service-datasource (packages/services/service-datasource, ExternalDatasourceService). That suite was run, and nothing in it was edited.", "#11201 and #24664 / PR #24706 were read through the in-tree record (sql-driver-11201-introspect-fk-schema-scope.test.ts, the IntrospectedForeignKey docblock, external-datasource-service.ts unattributedSchemaFilterError and its #24664 notes) and the card body, not by opening those threads.", "The throwaway PostgreSQL data directory lived at /tmp/os-issue-24707-pg, not in the scratchpad: the scratchpad's parent (/tmp/claude-0) is mode 700, which the postgres user cannot enter. It was stopped (postmaster PID 2522, the one I started) and deleted.", "Writes: git push to my branch ran 5 times (the empty branch, then WIP commits and the merge, per os-dev's commit-and-push-each-step rule). It counts as the one 'git push' budget line.", "Commit trailers: the AGENTS.md model-free pair (Claude-Session + Co-authored-by: Claude), per os-dev's rule that the repo's attribution outranks the harness reminder; the harness reminder named a model-bearing Co-Authored-By." ], "mcp_calls": "3 — mcp__github__issue_read (get #24707), mcp__github__issue_read (get_comments #24707), mcp__github__pull_request_read (get #24920, draft and assignee read-back). No MCP write tool.", "api_writes": "3 budget lines. (1) git push to claude/issue-24707-pg-multischema-introspect (5 pushes, branch only; not a REST write). (2) pr_create via scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches (request fw-20261020T143626Z-1ddc12, run 39440777841 success), executed by the relay as POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24920/assignees; read-back '9068 byte(s) sent, 9068 stored — identical', assignee os-project-manager. (3) This os-dev-report comment via scripts/pm/post-stamped.mjs: POST /repos/objectstack-ai/objectstack/issues/24707/comments.", "open_questions": [ { "question": "Same-named table pair across schemas on one PG search_path: refuse, or keep the first and name the other?", "options": [ "A — refuse introspectSchema with RESOURCE_CONFLICT / 409, naming each colliding table and its schemas in search_path order, plus the remedy (one schema per connection via the datasource 'schema' setting, or a rename)", "B — keep the first schema's table and report the other: with no spec change the only channel is a server log line", "C (outside the claim's two options, considered and rejected) — key the shadowed twin as a qualified 'sales.customers'" ], "recommendation": "A, built. Business need: only an external PG datasource whose search_path names 2+ schemas holding a same-named table can collide (the finder's measured shape). A single-schema connection, the default public deployment included, can never collide, so no internal caller that sees one schema is refused. Callers: ExternalDatasourceService listRemoteTables / generateObjectDraft / refreshCatalog / validateObject / testConnection, via ObjectQL.introspectDatasource or the driver directly. The admin connection test reaches it only as a last fallback after ping / checkHealth. No migration differ or os command calls it. All callers already surface a throw (testConnection answers {ok:false, error}). Long-term: B has no caller-visible channel without the spec member the triage withheld. Anti-AI-error: B's log line is invisible to HTTP/AI callers, i.e. the same silent drop. Startup focus: A adds no surface, option, key or gate. C is a context-dependent key spelling, the shape the IntrospectedForeignKey.referencedTable note (#11377) rejects." }, { "question": "Which error code and status for the refusal?", "options": ["A — RESOURCE_CONFLICT / 409 (existing StandardErrorCode; two tables conflict on one key)", "B — VALIDATION_ERROR or DATABASE_ERROR / 400"], "recommendation": "A, built: an existing ledger code, no new error code. The conflict is between two remote objects on one key, not a malformed request." }, { "question": "Prompt vs os-dev.md conflicts?", "options": ["none found"], "recommendation": "None. The prompt's 'service-external-datasource' is the in-tree @objectstack/service-datasource (see deviations)." } ], "out_of_scope_findings": [ "carrier: none · noted, not filed (PR Acceptance notes) · read-only inference, not measured: introspectPrimaryKeys resolves by ?::regclass along search_path, so a VIEW or other relation named like a later schema's base table but sitting in an earlier schema would capture the primary-key read; the collision refusal judges base tables only · dedupe words: introspectPrimaryKeys regclass view shadows table search_path; pg primary key introspection wrong relation" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Landed ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-20T16:05ZCONTRACT_REVIEW_TIER, narrowing face. Record 6294518392 PASS on1eeab8089f. ACCEPT 6294549692. Not governed.gh-readonly-queue/main/pr-24920-*branch remains.origin/main74fcec1edbis onorigin/main, one squash commit carrying PR #24920's 3 files, +271 / −11.completedthroughFixes #24707;pm:dispatchedis cleared.What
mainnow holds:search_path(sales.ordersbehindpublic) now reports its columns, where it reported none.listRemoteTablescounts them, and a draft built from it has fields.introspectSchemathrowsRESOURCE_CONFLICT(status: 409), naming each colliding table with its schemas insearch_pathorder, plus the remedy.rest's external-datasource routes) answer it as400 EXTERNAL_DATASOURCE_ERRORcarrying the same message.unreachable, with a warning; boot does not stop.schema, on a server whose defaultsearch_pathnames two schemas holding a same-named base table.schema(it becomes the connection'ssearch_path), or rename one table on the remote database. A deployment that needs both schemas uses one datasource per schema.publicdeployment included, answers exactly as before. SQLite and MySQL are untouched. There is no spec change.minorof@objectstack/driver-sql, BREAKING (an accept-set narrowing),Clause-②: no (narrowing).Still open beside it:
Test Corelane. They run inmain's Temporal Conformance battery, and its first run after this merge is their CI measurement. The seat reads it.introspectPrimaryKeysresolves by?::regclassalong the path, so a view in an earlier schema named like a later schema's base table could capture the key read. That is an inference, not measured; it is noted in the PR, not filed.The dev session
session_013timUNNCthRfQkhy9Hz2fVis archived.Generated by Claude Code