Skip to content

isMissingTableError's read-table check compares an object API name against a federated object's remoteName — a genuinely absent external remote now reads as loud, not benign #13438

Closed

Description

@claude

Found while implementing #13324 (PR #13437); out of that card's scope, filed unassigned for triage. It is a residual of that repair, not a pre-existing defect.

What #13324 changed

isMissingTableError(error, readObject) now refuses the benign "table not provisioned yet" verdict when the dialect phrase names a relation other than the one the caller read. Call sites pass the object API name they were reading.

The gap

For a federated (external) object, the object API name is not the name the driver puts in the SQL. SqlDriver.registerExternalObject records external.remoteName in physicalTableByObject, and getBuilder targets that:

this.physicalTableByObject[object] ?? StorageNameMapping.resolveTableName({ name: object })

So when a federated object declares external: { remoteName: 'legacy_orders' } and that remote table is genuinely absent, the driver raises a phrase naming legacy_orders while the call site declares the object name (say crm_order). The names do not match, so the repaired predicate answers not benign for what is in fact a genuine missing table.

The comparison folds away schema/database qualifiers, the legacy namespace__short prefix and case, but it cannot fold away an arbitrary remoteName — nothing at the call site knows the mapping, which lives on the driver instance.

Reach

Only the call sites that read an arbitrary, caller-named object can hit this — a federated object is never one of the fixed sys_* reads:

  • ObjectQL.seedAutonumber (packages/objectql/src/engine.ts) — passes object
  • the cascade-delete dependents probe (same file) — passes childName
  • SeedLoaderService.loadExistingRecords (packages/metadata-protocol/src/seed-loader.ts) — passes objectName
  • ObjectQL.reportFindFailure — log level only, no data consequence

Severity, honestly

The failure direction is the loud one: a genuinely-unprovisioned federated remote surfaces as an error instead of being treated as truthful emptiness. Per the module's own docblock that is the cheap direction ("one extra error line" vs silent data loss), and #13324's ruling explicitly prefers it when the measurement is ambiguous. So this is a narrowing, not a corruption — but it IS a behaviour change for federated objects that nobody asked for, and it should be either closed or consciously accepted.

Not measured against a live federated deployment — derived from the mapping code and the call sites. Whether any shipped federated object also carries autonumber or inbound references (the two paths with a data consequence) is worth establishing before grading this.

Options, not decided here

  1. Let the predicate take the resolved physical name. The engine's resolveObjectName does not consult the driver's physicalTableByObject, so this needs a driver-side accessor.
  2. Let the driver declare the table it targeted on the error envelope. backendStatementFaultError(object, cause) already exists in packages/drivers/driver-sql/src/sql-driver.ts and knows both names; the predicate would prefer a declared name over the caller's. This is the cleanest shape and removes the parameter from the narrow paths entirely, but it touches this lane's serial hot file and only helps drivers that adopt it.
  3. Accept and document: for external objects, a missing remote surfaces loudly.

Generated by Claude Code


Generated by Claude Code

Activity

  1. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    Triage → domain:engine · p3 · bug.

    Anchoring. All three candidate fix sites are one lane: option 1 lands in packages/objectql (engine accessor), option 2 in packages/drivers/driver-sql (backendStatementFaultError), option 3 is docs on the predicate. objectql and drivers/* are both domain:engine after the 2026-08-19 lane merge, so the option choice does not move the lane — it is the lane PM's to make.

    Grade p3, and the card argues its own grade correctly. The failure direction is the loud one: a genuinely-absent federated remote surfaces as an error rather than as truthful emptiness. Per the module's own docblock that is the cheap direction, and #13324's ruling prefers it when the measurement is ambiguous. Narrowing, not corruption.

    ⛔ Pre-dispatch premise, from the card's own open question — this must be discharged, not carried. Establish whether any shipped federated object exists, and whether one carries autonumber or inbound references (seedAutonumber and the cascade-delete dependents probe are the only two of the four listed call sites with a data consequence; reportFindFailure is log-level only and SeedLoaderService is boot-time). If the answer is zero, option 3 (accept and document) is the honest close and this card is a docs edit. If non-zero, the grade moves and option 2 becomes the shape to argue for.

    Recording the premise rather than guessing it: I did not run that census, because "is any federated object shipped" is a deployment question, not a git grep — the tree can only show that the code path exists, which the card already established.


    Generated by Claude Code

  2. zhuangjianguo commented on Aug 31, 2026

    @zhuangjianguo
    Collaborator

    The measurement this card says is owed before grading — done, with controls. Lane PM triage; ⛔ no code touched, no option chosen.

    The card states it plainly:

    Whether any shipped federated object also carries autonumber or inbound references (the two paths with a data consequence) is worth establishing before grading this.

    Measured on origin/main. Answer: neither path is exercised by any shipped federated object.

    What ships

    Exactly two objects declare external.remoteName:

    object remote table file
    showcase_ext_customer customers examples/app-showcase/src/data/objects/external/customer.object.ts
    showcase_ext_order orders examples/app-showcase/src/data/objects/external/order.object.ts

    Both are documented read-only ADR-0015 federation demos.

    Autonumber — zero, and the control fired

    Neither declares an autonumber field. That zero is a reading, not a broken grep: the same pattern over the same corpus returns a hit in examples/app-showcase/src/data/objects/field-zoo.object.ts, so the search demonstrably finds autonumber where it exists.

    Inbound references — zero, and the needle was established before trusting it

    ⚠️ My first attempt at this half was not a valid zero — the shell || bound to head, which succeeds on empty input, so "no hits" was an artifact rather than a measurement. Redone properly:

    1. Establish how this corpus spells a reference: Field.lookup( — 13 occurrences across 9 files. (No reference_to / referenceTo / master_detail spellings appear in showcase objects at all.)
    2. Control: that needle returns 9 files, so it works.
    3. Query: references to showcase_ext_customer / showcase_ext_order from outside their own declaration files — 2 hits, both prose in examples/app-showcase/README.md (lines 179 and 191). No object declares a lookup at either.

    What this does to the grading

    The card's own severity reasoning was conditional on this: the failure direction is the loud one (an error line where truthful emptiness was intended), which the module's docblock calls the cheap direction and which #13324's ruling explicitly prefers when the measurement is ambiguous. The measurement is no longer ambiguous — no shipped federated object reaches seedAutonumber or the cascade-delete dependents probe, the two paths with a data consequence.

    So option 3 (accept and document) is now the well-supported route, and option 2 in particular — which the card notes "touches this lane's serial hot file" and "only helps drivers that adopt it" — buys nothing measurable against shipped code today.

    ⚠️ Scope of this measurement, stated rather than implied. It covers federated objects shipped in this repo. A customer-authored federated object could carry autonumber or an inbound lookup, and nothing here rules that out — this is "no shipped instance", not "cannot happen". If that possibility is what the grading should turn on, this measurement does not settle it.

    Routing

    Left in the decision box rather than dispatched: choosing between "accept the narrowing and document it" and "close it" is still a call about behaviour nobody asked to change, even with the blast radius now measured as zero in-repo. The deliverable under option 3 is small and mechanical once chosen — a docblock line at the predicate saying that for external objects a genuinely-missing remote surfaces loudly, and why.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions