Repository navigation
Commit 26d710e
fix(objectql): runtime syncSchemas() binds federated objects instead of sending them DDL (#21796)
Fixes #21777
Clause-②: no
## What this changes
`ObjectQL.syncSchemas()` is the schema sync that install-local installs,
rehydrates and template seeding run after they register objects at
runtime. It now routes a federated object (ADR-0015, `external` set) by
the same predicate the boot sync
(`ObjectQLPlugin.syncRegisteredSchemas`) uses, and gives it the same
treatment:
- it binds the object to its remote table with the DDL-free
`registerExternalObject` and logs that at `debug`;
- it never calls `syncSchema` for the object.
Before this, `syncSchemas()` had no federated branch. It sent DDL to
every federated object in the registry, the external-schema datasource
refused it as designed, and the refusal was logged as the #4632
durability ERROR. The ERROR is unchanged for every other object whose
sync fails.
Files:
- `packages/objectql/src/engine.ts`: `syncSchemas()` gets the federated
branch. This is the expected landing point. The file's four other inline
`external != null` spellings now call the shared predicate; each is a
pure rename. They are in `buildDriverOptions`, the tenant-audit
exemption, the related-object own-read branch and `syncObjectSchema`.
- `packages/objectql/src/federated-object.ts` (new):
`isFederatedObject(schema)`, the one predicate. It is internal and not
re-exported from either entry (`index`, `core`), so the public surface
is unchanged.
- `packages/objectql/src/plugin.ts`: the boot path's four inline
spellings adopt the same predicate. They are in `syncRegisteredSchemas`
(two sites), `registerSchemasWithoutDdl` and
`reconcileFederatedBindings`. These are pure renames with no behaviour
change, plus one comment. This file is outside the claim's declared
surface. It is the half that makes "cannot drift from the boot path"
structural (see H3).
- `packages/objectql/src/sync-schemas-federated-object.test.ts` (new):
the three pins.
- `.changeset/21777-sync-schemas-federated-object.md`: `patch` for
`@objectstack/objectql`.
There is no edit in
`packages/cloud-connection/src/marketplace-install-local-plugin.ts` (no
plugin-side filter), none in `packages/spec/src/**`, and none on a
governed surface.
## Measurements (at base `ebfe658c72` unless stated)
### H1: every engine path that sends schema DDL to a driver
| Path | Where | What it does with an object whose `external` is set |
|---|---|---|
| Boot sync, `ObjectQLPlugin.syncRegisteredSchemas` (start phases 1 and
3, and `metadata:reloaded`) | `plugin.ts` | With a driver:
`registerExternalObject`, and a throw logs `warn`. Without a driver:
`debug`, deferred to the `kernel:ready` reconciliation. With a driver
that has no `registerExternalObject`: `debug` skip. Never `syncSchema`
or `syncSchemasBatch`. |
| DDL-free boot (`skipSchemaSync`), `registerSchemasWithoutDdl` |
`plugin.ts` | Skip, counted as `federated`. The binding is left to the
reconciliation. |
| `kernel:ready`, `reconcileFederatedBindings` | `plugin.ts` |
`registerExternalObject`. Unbound, unsupported or failed objects are
reported at ERROR. |
| `ObjectQL.syncObjectSchema`, also reached from the
`DatasourceConnectionService` connect re-drive, the `metadata-protocol`
publish path and the CLI schema-migration plugins | `engine.ts` |
`registerExternalObject`, then return. A throw propagates. |
| **`ObjectQL.syncSchemas`** (install-local install and rehydrate,
template seeding) | `engine.ts` | **`syncSchema`, refused, then the
#4632 ERROR. This is the defect.** |
The lifecycle archive pass in `lifecycle-service.ts` also calls
`syncSchema`, but against the archive target datasource, not the
object's own. It is outside this census.
### H2: what "skip" must mean (real `SqlDriver` on SQLite)
A scratch script (not committed) set up a `schemaMode: 'external'`
SqlDriver with a pre-existing `customers` table. After `init()`, which
is the install-local shape, it registered an object with `external: {
remoteName: 'customers' }` and then read it:
| Variant | ERROR lines | Read |
|---|---|---|
| A: `syncSchemas()` at base | 1, "Schema sync FAILED for object
'ext_customer' …" | fails with `no such table: ext_customer` |
| B: skip only (nothing after registering) | 0 | fails with `no such
table: ext_customer` |
| C: `registerExternalObject` (the boot path's treatment) | 0 | 1 row
from `customers` |
| D: `syncObjectSchema` | 0 | 1 row from `customers` |
| A: `syncSchemas()` with this PR | 0 | 1 row from `customers` |
A bare skip would silence the alarm and leave a runtime-registered
federated object unreadable. So `syncSchemas()` follows the boot path
and binds without DDL. A failed binding is logged at `warn`, exactly as
the boot path logs it.
### H3: one predicate
This PR extracts ONE named predicate, `isFederatedObject` (`external !=
null`), and adopts it at every inline site in `engine.ts` and
`plugin.ts`.
The alternative was to route `syncSchemas()` through
`syncObjectSchema()`'s branch. It was not chosen because it ties
`syncSchemas()` only to `syncObjectSchema()`. The boot path lives in
`ObjectQLPlugin` and never goes through `syncObjectSchema()`, so the
runtime and boot syncs could still drift apart.
Every adoption outside `syncSchemas()` is a pure rename, with the same
expression and the same null-safety. No other call site's behaviour
changes.
### H4: the showcase reach
Both objects named in the card's log declare `external` on datasource
`showcase_external` (`schemaMode: 'external'`):
- `showcase_ext_customer`: `external: { remoteName: 'customers' }`;
- `showcase_ext_order`: `external: { remoteName: 'orders' }`.
The H2 script measured that `external` survives registration on the
registry entry. The predicate covers both objects.
### H5: the ERROR stays loud
This diff leaves the #4632 `logger.error` block in `syncSchemas()`
byte-for-byte unchanged. Pin 2 holds it, checking the level, the
subject, the Error slot and the context fields, in two cases:
- an internal object whose driver refuses;
- an object WITHOUT `external` that lands on the external-schema
datasource.
### Reach through the public door (dogfood: real showcase boot plus
install-local)
`packages/qa/dogfood/test/install-local-purge-sample-data.dogfood.test.ts`
boots the showcase and installs CRM through install-local. It resolves
`@objectstack/objectql` through `dist/`.
| `dist/` | `Schema sync FAILED` lines | Objects named | Control:
`syncSchemas() ran after registering` lines | Tests |
|---|---|---|---|---|
| `syncSchemas()` federated branch disabled (marker planted and rebuilt;
`ablation-dist-preflight` found it in 4 built files) | 4 |
`showcase_ext_customer` ×2, `showcase_ext_order` ×2 | 2 | 8/8 |
| This PR | 0 | none | 2 | 8/8 |
Restore leg:
- `git checkout HEAD -- PATH`; the blob equals the HEAD blob and `git
diff HEAD` is 0 bytes;
- rebuilt, and `ablation-dist-preflight --absent` is clean over 14 built
files;
- `git status --porcelain` is empty.
The suite stays green in both legs, because nothing in it reads the
false alarm.
## Pins
The pins are in
`packages/objectql/src/sync-schemas-federated-object.test.ts`, on the
real `ObjectQL` engine. The driver double records its calls and, on the
external datasource, refuses DDL with
`ExternalSchemaModeViolationError`.
1. `syncSchemas()` over a registry holding the two showcase-shaped
federated objects:
- logs no ERROR;
- makes zero `syncSchema` calls on the external datasource;
- makes one `registerExternalObject` call per federated object;
- still syncs the managed object beside them.
2. The #4632 ERROR still fires in two cases: (a) an internal object
whose driver refuses; (b) an object without `external` routed to the
external-schema datasource.
3. On one fixture, the boot sync and `syncSchemas()` produce the
identical driver-call list and report the identical objects at ERROR.
### Reverse verification (committed first, at `ef5099f4df`)
- **Mutation.** `syncSchemas()`'s `if (isFederatedObject(obj)) {` became
`if (false as boolean) {`, written through
`scripts/ablation-replace.mjs`: anchor 1 to 0, blob `397b5ffb636d` to
`06523188d217`. The test imports the subject relatively, so it resolves
to `src/` and no rebuild leg was needed.
- **Predicted and observed:**
- pin 1 red: "expected … to have a length of +0 but got 2";
- both pin-3 cases red: the call lists differ, and the runtime's errored
set is `showcase_ext_customer, showcase_ext_order, stray_ledger` against
the boot's `stray_ledger`;
- both pin-2 cases green.
- **Restore.** `git checkout HEAD -- PATH`; the blob equals HEAD
`397b5ffb636d`, `git diff HEAD` is 0 bytes, and porcelain is empty.
## Verification (HEAD `260392d6a6`, after merging `origin/main`
`e83c9f6154`)
- **Package suite.** `pnpm --filter @objectstack/objectql test` passed
373 files and 7463 tests.
- **Typecheck.** `pnpm --filter @objectstack/objectql typecheck` exited
0. `--listFiles` confirms the new test file and module are inside the
`tsconfig.test.json` program, with zero errors of their own.
- **Build refresh.** After the merge: `pnpm install --frozen-lockfile`,
then a full turbo build (72/72), then `pnpm --filter @objectstack/spec
check:generated` (15/15 artifacts up to date).
- **Derived gates.** `node scripts/pm/dispatch-gates.mjs --commands`
with no paths derived 67 families. All 67 ran, and `--ran` reconciled
67/67 with 0 NOT-MEASURED and 0 UNRUN.
- **Artifact-roster block.** All 54 rows ran. 51 exited 0. Three bare
scripts are PR-context guards that exit 2 NOT WIRED without a PR
(nothing measured): `check-closing-target-claim.mjs`,
`check-partof-closing-keyword.mjs` and `check-single-claim-paths.mjs`.
Their workflows supply that context on this PR.
- **Symbol-anchor sweeps.** All four exited 0:
`check:adr-symbol-anchors`, `check:scripts-symbol-anchors`,
`check:spec-docblock-symbol-anchors` and `check:adr-anchors`.
- **Engine split ratio.** `check-engine-split-ratio --days 90` is
report-only: 98.4% over a complete-clone horizon. It raises no objection
to the lines added in `engine.ts`.
## Acceptance notes
- **Observation: dead code.** `syncSchemas()` keeps an empty block, `if
(... syncSchemasBatch ... batchSchemaSync) { }`, commented "Already
handled per-driver below". It does nothing and is untouched here.
- **Observation: warn-only binding failure at runtime.**
- At boot, the `warn` on a failed `registerExternalObject` is backed by
the `kernel:ready` reconciliation, which reports any still-unbound
federated object at ERROR.
- A runtime `syncSchemas()` has no reconciliation after it, so a binding
failure there is reported only at `warn`.
- This was not measured as reachable: `SqlDriver.registerExternalObject`
is synchronous metadata assignment with no observed throw path.
- **Observation: one inline spelling left.**
`packages/objectql/src/search-companion.ts` still spells the same test
inline (`schema.external == null`), and its docblock requires it to
equal the sync seam's predicate. It is semantically identical. Adopting
`isFederatedObject` there would be a pure rename, left out to keep this
diff to the sync seams.
- **Observation: the dogfood suite cannot see the alarm.** The
install-local dogfood suite stays green while the false ERROR lines are
present, because nothing in it reads the server log.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 045f764 commit 26d710e
5 files changed
Lines changed: 322 additions & 10 deletions
File tree
- .changeset
- packages/objectql/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
298 | 298 | | |
299 | 299 | | |
300 | 300 | | |
| 301 | + | |
| 302 | + | |
301 | 303 | | |
302 | 304 | | |
303 | 305 | | |
| |||
5483 | 5485 | | |
5484 | 5486 | | |
5485 | 5487 | | |
5486 | | - | |
| 5488 | + | |
5487 | 5489 | | |
5488 | 5490 | | |
5489 | | - | |
| 5491 | + | |
5490 | 5492 | | |
5491 | 5493 | | |
5492 | 5494 | | |
| |||
5730 | 5732 | | |
5731 | 5733 | | |
5732 | 5734 | | |
5733 | | - | |
| 5735 | + | |
5734 | 5736 | | |
5735 | 5737 | | |
5736 | 5738 | | |
| |||
8410 | 8412 | | |
8411 | 8413 | | |
8412 | 8414 | | |
8413 | | - | |
| 8415 | + | |
8414 | 8416 | | |
8415 | 8417 | | |
8416 | 8418 | | |
| |||
18304 | 18306 | | |
18305 | 18307 | | |
18306 | 18308 | | |
| 18309 | + | |
| 18310 | + | |
| 18311 | + | |
| 18312 | + | |
18307 | 18313 | | |
18308 | 18314 | | |
18309 | 18315 | | |
18310 | 18316 | | |
18311 | 18317 | | |
18312 | 18318 | | |
| 18319 | + | |
| 18320 | + | |
| 18321 | + | |
| 18322 | + | |
| 18323 | + | |
| 18324 | + | |
| 18325 | + | |
| 18326 | + | |
| 18327 | + | |
| 18328 | + | |
| 18329 | + | |
| 18330 | + | |
| 18331 | + | |
| 18332 | + | |
| 18333 | + | |
| 18334 | + | |
| 18335 | + | |
| 18336 | + | |
| 18337 | + | |
| 18338 | + | |
| 18339 | + | |
| 18340 | + | |
| 18341 | + | |
| 18342 | + | |
| 18343 | + | |
| 18344 | + | |
| 18345 | + | |
| 18346 | + | |
| 18347 | + | |
| 18348 | + | |
| 18349 | + | |
18313 | 18350 | | |
18314 | 18351 | | |
18315 | 18352 | | |
| |||
18358 | 18395 | | |
18359 | 18396 | | |
18360 | 18397 | | |
18361 | | - | |
| 18398 | + | |
18362 | 18399 | | |
18363 | 18400 | | |
18364 | 18401 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
| 11 | + | |
10 | 12 | | |
11 | 13 | | |
12 | 14 | | |
| |||
1492 | 1494 | | |
1493 | 1495 | | |
1494 | 1496 | | |
1495 | | - | |
| 1497 | + | |
1496 | 1498 | | |
1497 | 1499 | | |
1498 | 1500 | | |
| |||
1614 | 1616 | | |
1615 | 1617 | | |
1616 | 1618 | | |
1617 | | - | |
| 1619 | + | |
1618 | 1620 | | |
1619 | 1621 | | |
1620 | 1622 | | |
| |||
1757 | 1759 | | |
1758 | 1760 | | |
1759 | 1761 | | |
1760 | | - | |
| 1762 | + | |
1761 | 1763 | | |
1762 | 1764 | | |
1763 | 1765 | | |
| |||
1776 | 1778 | | |
1777 | 1779 | | |
1778 | 1780 | | |
1779 | | - | |
1780 | | - | |
| 1781 | + | |
| 1782 | + | |
| 1783 | + | |
| 1784 | + | |
1781 | 1785 | | |
1782 | 1786 | | |
1783 | 1787 | | |
| |||
0 commit comments