Skip to content

fix(objectql,service-datasource): bind federated objects whatever the boot order, and report what could not be bound (#7737) - #7788

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-7737-federated-boot-ordering
Aug 11, 2026
Merged

fix(objectql,service-datasource): bind federated objects whatever the boot order, and report what could not be bound (#7737)#7788
os-zhuang merged 1 commit into
mainfrom
claude/issue-7737-federated-boot-ordering

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Part of #7737

⚠️Part of, not Fixes — read this first. The dispatch asked for Fixes #7737. Merging this does not make GET /api/v1/data/showcase_ext_customer return the 3 fixture rows, so closing #7737 on merge would close a card whose acceptance criteria still fail. The reported symptom is produced by a different defect (#7738), measured below. The conflict is surfaced rather than resolved either way — the PM owns the call.

Premise verification (the card's stated root cause is falsified)

The card's root cause: boot schema-sync takes the if (!driver) skip at packages/objectql/src/plugin.ts L1124-1126, so registerExternalObject never runs and the object → remote-table mapping is never installed.

Measured on a real os dev boot of examples/app-showcase at origin/main @ 8669e5d:

  1. The skip is realSchema sync complete {"synced":94,"skipped":2,"total":96}; the 2 skipped are the two federated objects.

  2. But it is compensated.DatasourceConnectionService.attemptConnect re-drives engine.syncObjectSchema(objectName) for every explicitly-bound object right after registerDriver, and showcase_ext_customer carries an explicit datasource:. Instrumenting SqlDriver.find shows the mapping is in place and the read reaches the external driver: physicalTableByObject = {"showcase_ext_customer":"customers"}, and a boot-time system read returns 3 rows.

  3. The 200-empty comes from the org-scoping predicate, not from a missing binding. The authenticated read issues, against the remote table:

    select * from `customers` where (`organization_id` = ? or `organization_id` is null)
    -- bindings=["org_msoroxgurm6423gz"]
    

    The remote customers table has no organization_id column (id, created_at, updated_at, name, email, region, lifetime_value). SQLite's quoted-identifier fallback turns the unresolvable `organization_id` into the string literal'organization_id', so both disjuncts are constant-false: 0 rows, no error, HTTP 200. That is external-datasource-federated-read: the platform injects its org-scoping predicate onto a federated remote table that has no organization_id column #7738 — the card deliberately queued behind this one on the assumption its symptom would only appear once the mapping registered. It is observable today.

  4. The card's second piece of evidence has already expired. A filter on a non-existent column now answers 400 INVALID_FIELD (Unknown field 'nonexistent_col' on object 'showcase_ext_customer'), not 200 empty.

So premise_still_valid: false for #7737 as written. A missing binding is not silent either: with no driver the read refuses with Datasource '…' is not registered; with a driver but no mapping it fails with no such table: naming the object.

What this PR does fix (same class, real, still live on main)

The ordering dependence the card names is real — it is just reached by routes the showcase does not use, where nothing compensates:

Changes:

  • packages/objectql/src/plugin.ts — a federated-binding reconciliation on kernel:ready, i.e. after every plugin's start() (including AppPlugin's auto-connect) has completed. It re-drives registerExternalObject for every registered external object — idempotent, DDL-free, and therefore correct no matter which plugin connected the datasource, in which slot, or whether DDL was skipped. Boot order stops deciding whether federation works. This is the PM's shape (b)/(c).
  • packages/objectql/src/plugin.ts — the ruling that the skip must stop being silent. The skip site itself stays debug: at boot schema-sync the driver legitimately does not exist yet on a healthy boot, so error there would fire on every good boot — the mirror-image failure. The loudness lands at kernel:ready, where "still no driver" is final. That pass reports at error, naming the objects, their datasources, the consequence (registered and served, REST routes live, reads hitting a table named after the object) and the fix. A boot with nothing to report stays silent.
  • packages/services/service-datasource/src/datasource-connection-service.ts — the post-connect re-drive now iterates mappedObjects alongside objects, so a mapping-routed federated object is bound by a runtime (UI-created) datasource connect too, not only at boot. The two lists were already equals to the fail-fast policy below it; they were unequal only here.

Cross-seat note

registerExternalObject itself (packages/drivers/driver-sql/src/sql-driver.ts, domain:drivers) is not touched — measurement showed it is correct and never the producer. The service-datasource edit is the one cross-seat touch, and it is at the producer of that hole rather than patched from the consumer side.

How the pins fail on unfixed code

packages/runtime/src/federated-boot-binding.test.ts — boot-sequence tests, not registerExternalObject unit tests (one of those passes with or without the fix and pins nothing). Reverse-verified by taking the fix out with git checkout origin/main -- on both source files, rebuilding, and re-running. Predicted direction was red-before / green-after, and that is what happened — 3 of 4 red:

× binds BOTH binding routes to their remote tables and serves the remote rows
SqliteError: select * from `fed_invoice` - no such table: fed_invoice
× binds federated objects even when boot schema sync is skipped (OS_SKIP_SCHEMA_SYNC)
SqliteError: select * from `fed_invoice` - no such table: fed_invoice
× names the object, its datasource, the consequence and the fix at error level
AssertionError: boot must report the unbound federated object at ERROR level:
expected undefined to be defined
Test Files 1 failed (1)
Tests 3 failed | 1 passed (4)

The fourth (says nothing when every federated object bound) is green both before and after by construction — it guards against a false alarm, and it is reported as a guard rather than dressed up as a pin.

Reported honestly: the diagnostic is a log assertion, not an ADR-0112 envelope assertion. An unbound federated object does not throw at boot; it is reported, and the read that follows refuses on its own. The test pins both halves — the error line, and engine.find('fed_orphan') rejecting with the datasource name.

Verification

  • Build closure: pnpm --filter '@objectstack/runtime^...' build, then the three changed packages rebuilt with DTS.
  • pnpm --filter @objectstack/objectql --filter @objectstack/service-datasource --filter @objectstack/runtime typecheck — all Done.
  • Suites: @objectstack/objectql 183 files / 3237 tests, @objectstack/service-datasource 12 / 326, @objectstack/runtime 130 / 2033 — all passed.
  • Gates: pnpm check:durability-log-level ✓ (24 seams, all loud) · pnpm check:engine-double-contract ✓ (150 pinned) · node scripts/check-engine-split-ratio.mjs ✓ · node scripts/check-nul-bytes.mjs ✓. check:driver-conformance not run — no driver code is touched.
  • Real-stack regression check: os dev on examples/app-showcase (47 plugins) boots with 0 errors and the reconciliation stays silent, confirming both that nothing regressed and that the showcase's federated objects were never the ones hit by this ordering hole.

Generated by Claude Code

… boot order, and report what could not be bound (#7737)
`driver.registerExternalObject(obj)` is the only thing that installs an
ADR-0015 federated object's read metadata (object -> remote-table mapping,
columnMap translation, coercion maps). Without it a read resolves to a table
named after the OBJECT instead of the remote table it declares.
`ObjectQLPlugin`'s boot schema-sync calls it from `start()`, but the declared
datasource that owns the remote database is auto-connected in
`AppPlugin.start()` — a later `start()` — so on a healthy boot the driver does
not exist yet at that point and the call is skipped. Whether an object ended up
bound therefore depended on some other component re-driving it. Two cases where
nothing did: an object routed by a `datasourceMapping` rule (#4462), and any
deployment running with `OS_SKIP_SCHEMA_SYNC` (a DDL flag, while this binding is
DDL-free).
- ObjectQLPlugin now reconciles federated bindings on `kernel:ready`, after
every `start()` has run: idempotent re-drive for every registered external
object, independent of boot order.
- The same pass reports what it could not bind at `error`, naming the objects,
their datasources, the consequence and the fix. The previous diagnosis was one
`debug` line ("No driver available for object, skipping schema sync"). Silent
on a boot with nothing to report.
- DatasourceConnectionService re-drives `mappedObjects` alongside `objects`, so
a mapping-routed federated object is bound by a runtime datasource connect too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5pui68hQhfFRR1fB1iuvh
@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 11, 2026 2:59pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/service-datasource.

15 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx(via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx(via packages/objectql)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/objectql)
  • content/docs/kernel/services.mdx(via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql)
  • content/docs/permissions/system-context.mdx(via packages/objectql)
  • content/docs/plugins/index.mdx(via @objectstack/objectql)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx(via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/objectql)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 11, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 11, 2026 17:13
@os-zhuang
os-zhuang added this pull request to the merge queueAug 11, 2026
Merged via the queue into main with commit 199ec47Aug 11, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7737-federated-boot-ordering branch August 11, 2026 18:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude