Skip to content

fix(service-datasource)!: remote-tables refuses a schema filter the driver's introspection cannot apply, instead of a wrong 200 - #24706

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24664-remote-tables-schema-filter
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24664-remote-tables-schema-filter

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24664

Clause-②: no (narrowing)

What changes

ExternalDatasourceService.listRemoteTables serves GET /api/v1/datasources/:name/remote-tables and its twin GET /api/v1/datasources/:name/external/tables. It learns a table's schema only from a schema-qualified name (sales.orders): IntrospectedTable declares no schema of its own. Both filters, ?schema= and the datasource's external.allowedSchemas, skipped every table whose name carries no schema. The in-tree SQL driver reports bare names on every dialect, so both filters did nothing.

Now a filter in force is applied to every table, or the call refuses:

  • A table whose schema the introspection did not name can't be judged by a filter. When ?schema= or external.allowedSchemas is in force and the introspection holds one, the call throws 400 EXTERNAL_DATASOURCE_ERROR, the code already registered for a refusal from this service. Both route spellings already answer a throw from this method with that envelope, so no route code changes. The message names the datasource, the filter and one unattributed table, and gives the fix.
  • A producer that reports qualified names is narrowed by both filters, as before. The old qualified fakes are kept unchanged as the control.
  • A filter no table can pass, whatever its schema, answers []: an empty allow-list, or a ?schema= the allow-list leaves out.
  • With neither filter, every table is listed, as before.

Files: external-datasource-service.ts (the listing, plus two module-private helpers), one comment paragraph in admin-routes.ts on the route, one new test file, and one changeset. No driver file and nothing under packages/spec is touched.

Premise, measured (A1)

I measured the real SqlDriver.introspectSchema() from the dist built at base eadea5720 against a private PostgreSQL 16 cluster. The database held public.customers, sales.orders and sales.customers. I then fed the result to the service as plugin.ts does, unmodified.

introspection tables it returns (names as emitted) ?schema=sales, base ?schema=sales, this branch
default search path customers (the public one) 200, [customers] 400 EXTERNAL_DATASOURCE_ERROR
searchPath: ['sales'] (config.schema: 'sales') orders, customers 200, both 400
database default search_path = public, sales customers (public), orders with 0 columns 200, both 400
SQLite, in memory customers, orders 200, both 400

Every table record's own keys were name, columns, foreignKeys and primaryKeys. No table carried a schema in its name or anywhere else. allowedSchemas: ['public'] with no ?schema= answered every table at base and refuses on this branch. allowedSchemas: ['public'] with ?schema=sales answers [] on this branch.

Card premise: confirmed. The roll-up's mechanism, the tableSchema && short-circuit on both filters, is exactly the :383/:385 pair.

Route choice: (b), refuse where the schema is unknown, for every in-tree dialect

Route (a), narrowing for real on Postgres, was measured out. Two reasons:

  1. There is nowhere declared to carry the schema. IntrospectedTable has no schema field, and adding one is a packages/spec change, which is out of scope here. The only carrier is a qualified name/key. The driver's introspection also feeds managed-datasource consumers (schema drift, the objectql util, the datasource health count) that key tables by bare object name. Qualifying there is not a cheap or local change.
  2. Even a qualified name would not reach sales. The Postgres listing is scoped to current_schemas(false), and every per-table read resolves the bare name through the search path. On a default deployment sales is never introspected. Narrowing to it would answer an empty 200, which is still silently wrong. Reaching it needs a schema-scoped introspection capability, a new driver contract member. That is its own packages/spec decision.

Four axes:

  • Actual business need. Measured producers of ?schema= are os datasource list-tables --schema and @objectstack/client's datasources.external.listTables(name, { schema }). The console's schema browser (objectui SchemaBrowser, DatasourceResourcePage) never sends it. No example app declares external.allowedSchemas, and its spec row is live in the liveness ledger. Nobody measured pulls real Postgres narrowing on in-tree drivers. The need the card states is correctness: narrow or refuse, never a wrong 200. (b) meets it.
  • Long-term soundness. (b) holds the service to what the producer attests. There is no dialect table inside the service (SQLite's main, MySQL's database, Postgres's search path), which would be a second source of truth about the driver. The qualified path stays, so a future schema-aware introspection narrows with no change here.
  • Preventing AI authoring errors. An author (or an AI) who sets allowedSchemas, or calls ?schema=, on an in-tree-driver datasource is told the filter is unenforceable. They no longer get a believable unfiltered list. What is declared is now enforced or refused.
  • No scope spread at the startup stage. No new spec key, config flag, error code or driver capability.

SQLite was decided on the same axes. A ?schema=main would happen to be answerable by a service that knew SQLite's schema name. Nobody sends it except the QA checklist step. Teaching the service that fact is the dialect table the third axis rejects, so SQLite refuses like the others.

Tests

New src/__tests__/remote-tables-schema-attribution.test.ts, 12 cases:

  • SQLite through a real in-memory SqlDriver introspection: an unfiltered list (preservation), ?schema= refused, and allowedSchemas refused.
  • Postgres through a fake holding exactly the measured introspection, on both the default path and the sales path: ?schema=sales refused (the card's row), refused even when every table is in fact in sales, allowedSchemas refused, an unsatisfiable filter answers [], and an unfiltered list.
  • Control: the qualified producer narrows by ?schema=, by the allow-list and by both. One bare name among qualified ones refuses.
  • Route: GET /api/v1/datasources/pg/remote-tables?schema=sales over the Postgres shape answers 400 with error.code: 'EXTERNAL_DATASOURCE_ERROR' through the real HonoHttpServer and the entitled-caller fixture. Without ?schema= it answers 200 with the table.

Refusal pins assert code and status, plus the named subject (the schema, or external.allowedSchemas, or the bare table). No pin asserts the prose.

Ablation (one, on the tested behaviour). I ran node scripts/ablation-replace.mjs in wrap mode, inside the lock. It replaced unattributed.push(table.name); with the old pass-through tables.push(...): anchor 1 to 0, blob ec53e2afb7e5 to e0b6c0c14fb9. Direction as predicted: 7 failed / 5 passed. The seven are every refusal pin plus the route pin. The five that stayed green are the unfiltered lists, the [] case and the qualified controls. The restore leg was proven: blob back to ec53e2afb7e5, equal to HEAD, git diff HEAD empty. The service is imported relatively in the test, so the subject resolves to src/ and no build sits between the mutation and the run.

Local verification

All readings are on head 118169806, the final commit, which is 0f038913f merged with origin/main 5018fc303. No conflict. The closure was rebuilt after the merge, and @objectstack/spec check:generated is green.

  • pnpm --filter @objectstack/service-datasource test: 46 files, 850 passed.
  • pnpm --filter @objectstack/service-datasource typecheck: exit 0. tsc --listFiles counts the new test file once.
  • pnpm --filter @objectstack/rest exec vitest run src/remote-tables-twin.equivalence.test.ts src/external-datasource-envelope.conformance.test.ts: 26 passed. The twin pin aliases service-datasource to src, so it reads this change.
  • Gate union: node scripts/pm/dispatch-gates.mjs --commands derived 68 commands, and all 68 exited 0. Reconciliation: --ran reported "68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN".
  • check-changeset-no-major level axis, driven offline with this body as --event: "this PR declares clause-② no (narrowing)" and no package moved is graded patch. The changeset is minor. check-adr-0087-registration accepts not-required (no-migration-prescription).
  • CI families (Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core) are not re-run here. They run on this PR.

Acceptance notes

  • The QA checklist item integration-system.external-schema-introspection changes reading. Step 2's ?schema=main on the stock SQLite showcase_external now answers 400 EXTERNAL_DATASOURCE_ERROR, where it used to answer the full set. The item's first knownGaps entry describes the old no-op. The next checklist pass should restate that step's expectation as "refused". This PR does not touch docs/qa.
  • Liveness ledger prose. The allowedSchemas row in packages/spec/liveness/datasource.json still says the filter "only bites on tables whose schema can be attributed". Its anchors (listRemoteTables, the const allowed = ds?.external?.allowedSchemas read) are unchanged and still resolve. Only the prose is now stale. Carrier: none, so this is noted here and not filed.
  • Narrowing for real needs a schema-scoped introspection, meaning a schema on IntrospectedTable or a schema-taking introspectSchema. That is a packages/spec contract decision with no measured consumer today. It is not filed.
  • A driver finding was measured on the way and is not fixed here. It is reported to the seat. When the Postgres session's search path names two schemas (a database default search_path = public, sales), SqlDriver.introspectSchema() gets two things wrong. It reports sales.orders with zero columns: per-table reads resolve through knex(name).columnInfo(), which reads only the session's current schema. It also silently drops sales.customers, because tables are keyed by bare name and the public one wins. The listing shows this as orders with columnCount: 0.
  • allowedSchemas reaches only the listing. generateObjectDraft, importObject and refreshCatalog do not consult it. ADR-0015 §6.1 scopes the filter to listRemoteTables, so this is noted and not filed.

Generated by Claude Code

…trospection cannot apply

?schema= and external.allowedSchemas were skipped for every table whose
name carries no schema, which is every table the in-tree SQL driver
introspects, so a named schema answered 200 with the wrong tables and an
allow-list let every table through. A filter in force now narrows or
refuses with 400 EXTERNAL_DATASOURCE_ERROR; a producer that qualifies its
names is narrowed as before.

Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s
Co-authored-by: Claude <noreply@anthropic.com>
…mote-tables-schema-filter

Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

6 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json d1173825b709dfd8ad4ef383757a5df0a2e82d2a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1b64b5283bb5467f6e7dfb9e788fded1d30d948f — the merge of head 1181698060abfe3cee3fbb48e164fc0429ba34e2 into base d1173825b709dfd8ad4ef383757a5df0a2e82d2a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1b64b5283bb5467f6e7dfb9e788fded1d30d948f && git checkout 1b64b5283bb5467f6e7dfb9e788fded1d30d948f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d1173825b709dfd8ad4ef383757a5df0a2e82d2a 1181698060abfe3cee3fbb48e164fc0429ba34e2 && git checkout -B drift-repro d1173825b709dfd8ad4ef383757a5df0a2e82d2a && git merge --no-ff 1181698060abfe3cee3fbb48e164fc0429ba34e2

node scripts/docs-audit/affected-docs.mjs --json d1173825b709dfd8ad4ef383757a5df0a2e82d2a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1181698060abfe3cee3fbb48e164fc0429ba34e2
Local-runs: none
Scope: packages/services/service-datasource/src/external-datasource-service.ts, packages/services/service-datasource/src/admin-routes.ts, packages/services/service-datasource/src/tests/remote-tables-schema-attribution.test.ts, .changeset/24664-remote-tables-schema-filter.md + packages/spec/src/contracts/external-datasource-service.ts, packages/spec/src/contracts/schema-diff-service.ts, packages/spec/src/api/error-code-ledger.zod.ts, packages/rest/src/external-datasource-routes.ts, packages/services/service-datasource/src/datasource-route-ledger.ts, packages/drivers/driver-sql/src/sql-driver.ts, packages/cli/src/commands/datasource/list-tables.ts, packages/client/src/index.ts, packages/services/service-datasource/src/tests/external-datasource-service.test.ts, packages/services/service-datasource/src/tests/entitled-caller.fixture.ts, packages/services/service-datasource/package.json, packages/services/service-datasource/tsconfig.json, packages/spec/liveness/datasource.json, docs/adr/0015-external-datasource-federation.md, docs/qa/platform-checklist/areas/integration-system.json, docs/qa/platform-checklist/areas/cli.json, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs, objectui at the pinned 12dc7b47c3 packages/app-shell/src/views/metadata-admin/datasource/DatasourceResourcePage.tsx

Read-only shape held: the net diff against origin/main (merge-base 5018fc303d), card #24664 with both comments, PR #24706's body, file list and one bot comment, and the head's check-runs. Nothing built, run or re-run.

Check-runs on this head 2026-10-20T00:59Z: 28 concluded success, among them Lint & Repo Gates, TypeScript Type Check, Build Core, Governed Surface Queue Guard, Check Changeset, Lint · docs & code guards, Dogfood Regression Gate and Test Core (5/6); 6 skipped by selector; still in_progress: Test Core (1/6), (2/6), (3/6), (4/6), (6/6). This record judges the diff and the concluded runs only; the five open shards are the landing seat's read, and this verdict does not vouch for them.

① Derived judgments

Every accept-set and public-surface change the diff implies, read in the cited code:

  1. ?schema= with at least one bare-name table in the introspection: was 200 with a list the filter never narrowed, now a throw served as 400 EXTERNAL_DATASOURCE_ERROR on both doors — RIGHT. The card's own acceptance names "an explicit refusal" as one of its two accepted outcomes, and Prime Directive chore: version packages #10 (declared = enforced) is the rule. The premise holds in the driver: sql-driver.ts#introspectSchema keys every table by the bare table_name from information_schema.tables (Postgres, scoped to current_schemas(false); MySQL, DATABASE()) or sqlite_master.name, and writes tables[tableName] = { name: tableName, columns, foreignKeys, primaryKeys } — no dialect qualifies a name, and IntrospectedTable (schema-diff-service.ts) declares no schema member, so a qualified name is the only carrier the service could read. Both doors already serve a throw from listRemoteTables with that envelope: the admin door's badRequest is sendError(res, 400, SERVICE_ERROR_CODE['external-datasource'], message) (admin-routes.ts), and the federation twin's refused is answerThrown(res, err, 'EXTERNAL_DATASOURCE_ERROR'), 400 unless a sandbox fault (external-datasource-routes.ts). The code is in the ADR-0112 ledger as "introspection/connection-test refusal from the external-datasource service", and datasource-route-ledger.ts already names it the listing's one failure contract. No route code moved; the diff confirms admin-routes.ts gained a comment paragraph only.
  2. external.allowedSchemas declared on a datasource whose introspection names no schema, with no ?schema=: was 200 listing every table (the allow-list inert — a security-shaped key, per the liveness ledger's own note), now 400 — RIGHT. The spec contract (IExternalDatasourceService.listRemoteTables: "filtered by external.allowedSchemas") and ADR-0015 §6.1 say the list is filtered; an allow-list that cannot be applied must refuse rather than over-list. Cost, correctly named in the changeset: a datasource on the in-tree driver that declares the key loses its listing (console "Sync objects" included) until the key is removed. No in-tree example declares it at this head (git grep allowedSchemas hits spec, generated docs and the ledger only), and the console at the pinned objectui sha sends no ?schema= (DatasourceResourcePage.tsx calls the bare remote-tables path).
  3. A filter no table can pass, whatever its schema (allowedSchemas: [], or a ?schema= the allow-list leaves out), answers [] without needing attribution — RIGHT. admittedSchemas returns an empty set and the method returns before the loop; the answer is independent of any table's schema and equals what a qualified producer answered before. For bare names it is narrower than before (the full list), which is the fix.
  4. A producer that qualifies its names is narrowed by ?schema=, by the allow-list, and by both — UNCHANGED, RIGHT. The pre-existing fixture (external-datasource-service.test.ts, mart.fact_orders / public.dim_customer, allowedSchemas: ['mart', 'public']) is exactly this producer and is untouched; the new file keeps the same shape as its control.
  5. A mixed producer, one bare name among qualified ones, refuses — RIGHT. A partially attributable list cannot be narrowed honestly, and the refusal names the bare table.
  6. No filter: every table, schema: undefined on a bare name — UNCHANGED, RIGHT. requested is undefined for an absent or empty ?schema=, and the repeated-key array still coerces to undefined at both doors (unchanged route code), so the twin pin's degrade-to-no-filter contract holds and nothing answers 500.
  7. Public surface: none widens — RIGHT. admittedSchemas and unattributedSchemaFilterError are module-private (no export); RemoteTable and the listRemoteTables signature are unchanged; no spec file, no new error code, no new query parameter, no driver file, no generated artifact owed. The only surface movement is the accept-set narrowing in 1–3.
  8. The refusal's shape — RIGHT, with one accepted limit. The thrown error carries code and status like importNameRefusedError; the admin door reads status only for the 503 SERVICE_UNAVAILABLE pair and otherwise answers 400, the twin the same, so declaration and served envelope agree. The runtime string carries no tracker number (check:doc-authoring), names the datasource, the filter, one unattributed table, the count and the remedies. Accepted limit: the refusal shares its code with every other refusal from this service (a connection failure included), so a client distinguishes it by the message only; a dedicated code would be an ADR-0112 ledger entry in packages/spec, outside this dispatch's surface and not owed by the card.
  9. Tests pin both halves — RIGHT. Refusal pins assert code + status + the named subject (the schema, external.allowedSchemas, or the bare table); preservation pins assert the unfiltered list and the 200 body through the real HonoHttpServer with the entitled-caller.fixture (present at the head). The SQLite leg reads the real SqlDriver.introspectSchema() from the driver's built package, so one dialect's bare-name premise is measured, not faked; the Postgres fake holds the measured shape. @objectstack/driver-sql is a dependency and @objectstack/plugin-hono-server a devDependency of the package; better-sqlite3 has five sibling-test precedents; tsconfig.json include: ["src"] reaches __tests__; no path escapes the package; the knex handle is destroyed in afterEach. The reported ablation (the old pass-through restored: 7 red, 5 green, the refusal pins and the route pin the red ones) is in the predicted direction.
  10. admin-routes.ts — comment only, RIGHT. The paragraph's claim ("that throw is the 400 EXTERNAL_DATASOURCE_ERROR below") matches badRequest as read.

No wrong derived judgment found. No governed surface in the file list; head repo equals base repo; +389/−4.

② Semver level

The changeset .changeset/24664-remote-tables-schema-filter.md grades @objectstack/service-datasource: minor, carries a fix(service-datasource)!: title, a BREAKING paragraph naming both public doors, the ADR-0087 marker not-required (no-migration-prescription), and a "What to do" paragraph that names the two in-tree ?schema= producers (os datasource list-tables --schema, @objectstack/client datasources.external.listTables(name, { schema })) and the console case. The PR body's and the changeset's Clause-②: no (narrowing) lines agree, and the arm is the truthful one: nothing widens (①-7) and the accept-set narrows (①-1 to ①-3). check-changeset-no-major.mjs reads no (narrowing) as carrying the level axis — a declared narrowing is BREAKING and ships minor in the launch window, major would promote the whole monorepo — and the one moved package is graded minor, so the axis is clean; the head's Check Changeset run concluded success. no-migration-prescription is a member of that gate's CATEGORIES and is honest here: no spec key, stored shape, export or error code is added, removed or renamed, so objectstack migrate meta has nothing to rewrite. The callers (cli, client, rest) are not moved, so no changeset is owed on them; the text names them for the upgrading reader. patch (the dispatch's default) would have been wrong for a declared narrowing, and skip-changeset wrong for a released package whose behaviour moves. Level: RIGHT.

③ Boundary flags

open_questions is empty. Every dev flag and out-of-scope finding, answered or escalated:

  • Refuse on every in-tree dialect rather than narrow for real — ANSWERED, right. The card accepts the refusal; a real narrowing needs a schema on IntrospectedTable or a schema-taking introspection (packages/spec, outside the dispatch surface), and even then the Postgres listing is bounded by current_schemas(false), so a schema off the search path would answer an empty 200 — still wrong. No dialect table in the consumer is the Prime Directive Add comprehensive test suite for Zod schema validation #12 reading.
  • SQLite ?schema=main refused like the rest — ANSWERED, consistent with the point above; the only in-tree sender of that spelling is a QA checklist step.
  • minor over the dispatch's default patch — ANSWERED in ②.
  • [] for an unsatisfiable filter — ANSWERED in ①-3.
  • admin-routes.ts comment-only; packages/rest untouched — ANSWERED in ①-1 and ①-10, both doors read.
  • Private PG 16 cluster under /tmp, stopped and removed; verify-lock queue timeout re-acquired; the bare -- spelling on the first package test run, re-run without it on the final head; test:repo left to CI — nothing to judge on the contract; the final readings are on the re-run and CI is the gate (Test Core shards open at reading, see above).
  • Driver finding, class a, named real producer — ESCALATED. SqlDriver.introspectSchema() on a Postgres session whose search path names two schemas drops a same-named table behind the first (keyed by bare name) and reports zero columns for a table outside the current schema (knex(name).columnInfo() reads the current schema only). That is a reproducible defect with a repro, in packages/drivers/driver-sql/src/sql-driver.ts, outside this card's surface and correctly not fixed here. The dev's relay files no cards, so the owning seat files it as its own card for the driver lane.
  • QA checklist drift — ESCALATED to the lane, not blocking. integration-system.external-schema-introspection step 3 (?schema=main on showcase_external) and acceptance[1]'s live reading ("both answer 200 with the full set"), and cli.datasource-introspect-codegen's "both runs list every table", now read 400 EXTERNAL_DATASOURCE_ERROR; the "never a 500" negative still holds. docs/qa is a hand-written tree with no gate on this prose (Lint · docs & code guards concluded green on this head); the next checklist pass restates those steps.
  • Liveness ledger prose — ESCALATED, not blocking. packages/spec/liveness/datasource.json external.allowedSchemas evidence still says the filter "only bites on tables whose schema can be attributed"; the path#symbol anchor (external-datasource-service.ts#listRemoteTables) and the key mention still resolve, which is what check-liveness.mts reads, and the gate is green on this head. A one-line prose refresh belongs to a spec-side follow-up; it was outside this dispatch's surface.
  • allowedSchemas reaches only the listing — ANSWERED: ADR-0015 §6.1 scopes it to listRemoteTables; nothing owed here.
  • Real narrowing is a spec contract decision — ANSWERED: not filed, no measured consumer, the card accepts the refusal; a future card if demand appears.

Implemented-by: claude/issue-24664-remote-tables-schema-filter
Reviewed-by: session_01LR1cD9JqgkeqY7k9rrZH8s

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #24664 · domain:services seat 2 · os-warren · session_01LR1cD9JqgkeqY7k9rrZH8s · 2026-10-20T01:04Z · head 1181698060, the review of record for claim 6279351290.

  1. Form: draft, base main. The first line is Fixes #24664, and no other closing keyword sits beside an issue number. Clause-②: no (narrowing) on the body and on the changeset's line: a 400 replaces a 200. minor for @objectstack/service-datasource (fix(…)!:), with the ADR-0087 disposition not-required (no-migration-prescription). 4 files, +389/−4. check-governed-merges: not governed.
  2. Premise, measured:
    • The contract review of record is 6280869258 (Served-tier: CONTRACT_REVIEW_TIER, narrowing face, whole PR in scope, Local-runs: none): PASS on this head. It judged the check-runs concluded at its reading, and the seat reads the rest below before landing.
    • The dev's live measurement, on a private PostgreSQL 16 with public and sales tables and on SQLite. At base, ?schema=sales answered the public tables or both sales tables unfiltered, and allowedSchemas let every table through. On this head the same request answers 400 EXTERNAL_DATASOURCE_ERROR.
    • The seat's read of the diff: admittedSchemas combines ?schema= and allowedSchemas. A table the introspection names no schema for, under a filter in force, refuses the whole listing with a message that names the remedy. A filter no table can pass answers []. A producer that reports qualified names is narrowed as before; the old qualified fixture is kept as the control. An unfiltered listing is unchanged. No driver, packages/spec or route code changes (one comment paragraph in admin-routes.ts); both route spellings already serve a throw from this method as that code.
    • Pins: 12 cases, including a real in-memory SqlDriver SQLite introspection and the route through a real HonoHttpServer. Ablation put the old pass-through back: 7 red, 5 green (unfiltered, [] and the qualified controls), restored to HEAD's blob.
  3. Gates:
    • Local on the head (after origin/main 5018fc303 was merged in): the package's 46 files pass (850 tests), typecheck exit 0, the rest twin and conformance tests pass (26), and the 68 derived families exited 0 (--ran: 0 NOT-MEASURED, 0 UNRUN).
    • The seat's diff read: 0 expect( lines removed in service-datasource/src.
    • CI on the head at this read: 30 success, 6 skipped, 3 in progress, 0 failed. Landing waits for every check, the Test Core shards the review could not see included.
  4. Accepted deviations:
    • Route (b) for every in-tree dialect, measured. Route (a) needs a schema on IntrospectedTable, a packages/spec contract change with no measured consumer. And Postgres introspection reaches only the search_path, so narrowing there would still answer a wrong, empty 200. No dialect table guesses a schema in the consumer.
    • SQLite ?schema=main is refused like any bare-name listing. The only sender measured is a QA checklist step.
    • An unsatisfiable filter answers []. No table could pass whatever its schema, so this needs no attribution.
    • The refusal shares EXTERNAL_DATASOURCE_ERROR with the service's other refusals and is told apart by its message. A dedicated code would be a spec ledger entry, which this card does not owe (review record).
  5. Out of scope:
  6. Landing: ordinary queue landing. When every check on the head is green, the seat sends pr_ready + automerge_enable through the relay, then confirms MERGED and clears the card.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 20, 2026 01:18
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 20, 2026 01:18
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit 60571dd Oct 20, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24664-remote-tables-schema-filter branch October 20, 2026 02:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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)

2 participants