Uh oh!
There was an error while loading. Please reload this page.
fix(objectql,service-datasource): bind federated objects whatever the boot order, and report what could not be bound (#7737) - #7788
Conversation
… 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
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 15 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
Uh oh!
There was an error while loading. Please reload this page.
Part of #7737
Part of, notFixes— read this first. The dispatch asked forFixes #7737. Merging this does not makeGET /api/v1/data/showcase_ext_customerreturn 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 atpackages/objectql/src/plugin.tsL1124-1126, soregisterExternalObjectnever runs and the object → remote-table mapping is never installed.Measured on a real
os devboot ofexamples/app-showcaseatorigin/main@8669e5d:The skip is real —
Schema sync complete {"synced":94,"skipped":2,"total":96}; the 2 skipped are the two federated objects.But it is compensated.
DatasourceConnectionService.attemptConnectre-drivesengine.syncObjectSchema(objectName)for every explicitly-bound object right afterregisterDriver, andshowcase_ext_customercarries an explicitdatasource:. InstrumentingSqlDriver.findshows 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.The 200-empty comes from the org-scoping predicate, not from a missing binding. The authenticated read issues, against the remote table:
The remote
customerstable has noorganization_idcolumn (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.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: falsefor #7737 as written. A missing binding is not silent either: with no driver the read refuses withDatasource '…' is not registered; with a driver but no mapping it fails withno 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:
datasourceMappingrule (datasource: an object mapped to an unreachable datasource silently reads/writes the DEFAULT store — no boot failure, /ready still 200 #4462) rather than an explicitobject.datasource:connectDeclaredcomputesboundandmapped, passes both toconnect, and the post-connect re-drive iterated onlyopts.objects. Its mapping is never installed and every read fails withno such table:naming the object.OS_SKIP_SCHEMA_SYNC: that flag is about DDL managed out of band, but it skips bothsyncRegisteredSchemas()calls and takes the only in-plugin binding site with them.Changes:
packages/objectql/src/plugin.ts— a federated-binding reconciliation onkernel:ready, i.e. after every plugin'sstart()(includingAppPlugin's auto-connect) has completed. It re-drivesregisterExternalObjectfor 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 staysdebug: at boot schema-sync the driver legitimately does not exist yet on a healthy boot, soerrorthere would fire on every good boot — the mirror-image failure. The loudness lands atkernel:ready, where "still no driver" is final. That pass reports aterror, 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 iteratesmappedObjectsalongsideobjects, 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
registerExternalObjectitself (packages/drivers/driver-sql/src/sql-driver.ts,domain:drivers) is not touched — measurement showed it is correct and never the producer. Theservice-datasourceedit 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, notregisterExternalObjectunit tests (one of those passes with or without the fix and pins nothing). Reverse-verified by taking the fix out withgit 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: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
errorline, andengine.find('fed_orphan')rejecting with the datasource name.Verification
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.@objectstack/objectql183 files / 3237 tests,@objectstack/service-datasource12 / 326,@objectstack/runtime130 / 2033 — all passed.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-conformancenot run — no driver code is touched.os devonexamples/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