Description
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 defaultsearch_pathnames two schemas (ALTER DATABASE … SET search_path = public, sales), withpublic.customers,sales.ordersandsales.customers.- What happens (measured):
introspectSchemalists the tables of everycurrent_schemas(false)schema, keyed by BARE name (sql-driver.ts~:4984). Sosales.customersis silently dropped behindpublic.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. Sosales.ordersreports zero columns (measured:orders: columns=[]), andlistRemoteTablesanswered it withcolumnCount: 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.
- Related: since 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 (PR fix(service-datasource)!: remote-tables refuses a schema filter the driver's introspection cannot apply, instead of a wrong 200 #24706) a schema filter on these bare names is refused rather than ignored, which leaves this producer defect visible on the unfiltered listing. driver-sql (PG): introspectForeignKeys' information_schema query is not schema-scoped — a same-named table in another schema contributes its foreign keys #11201 (closed) fixed the same shape forintrospectForeignKeys(not schema-scoped). A schema onIntrospectedTablewould be 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 no measured consumer for one.
Dedupe
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.
Generated by Claude Code
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