From 053c8bb7522e8c74d8152df3d1a4ea330a44335b Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 14 Jun 2026 02:46:48 +0500 Subject: [PATCH] feat(objectql): opt-in sys_metadata hydration for isolated project kernels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Boot Phase-2 hydration (restoreMetadataFromDb → loadMetaFromDb, which registers objects WITH their fields into the SchemaRegistry) is gated on `environmentId === undefined`. That gate assumes every project kernel sources metadata from a remote artifact / control-plane proxy and has no local sys_metadata. It's untrue for an isolated, proxy-free project kernel that persists its OWN sys_metadata locally (the cloud single-env tenant runtime on Turso): objects CREATED AT RUNTIME there — absent from the boot artifact manifest — never re-enter the registry after a restart, so `registry.getObject(name)` returns nothing for them and every registry consumer silently degrades (notably the engine.find unknown-$select guard, which then lets an unknown projected column zero the result set). Add an explicit `hydrateMetadataFromDb` plugin option (default false, so the control-plane/proxy path is untouched). When set, hydration runs even with `environmentId` defined. Safe because each engine owns its registry instance (no cross-kernel leakage — the historical process-wide singleton is gone, see engine.ts:274) and loadMetaFromDb already tolerates a missing table. The cloud artifact-kernel-factory — proxy-free, local sys_metadata, isolated registry — opts in. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../objectql/src/plugin.integration.test.ts | 80 +++++++++++++++++++ packages/objectql/src/plugin.ts | 42 ++++++++-- 2 files changed, 117 insertions(+), 5 deletions(-) diff --git a/packages/objectql/src/plugin.integration.test.ts b/packages/objectql/src/plugin.integration.test.ts index dbe68a5965..fa8ddc1cc7 100644 --- a/packages/objectql/src/plugin.integration.test.ts +++ b/packages/objectql/src/plugin.integration.test.ts @@ -939,6 +939,86 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => { }); }); + // A driver that serves one runtime-created object (with inline fields) + // from sys_metadata — models an isolated, proxy-free project kernel. + const makeLocalMetadataDriver = (findCalls: Array<{ object: string }>) => ({ + name: 'local-meta-driver', + version: '1.0.0', + connect: async () => {}, + disconnect: async () => {}, + find: async (object: string) => { + findCalls.push({ object }); + if (object === 'sys_metadata') { + return [ + { + id: '1', + type: 'object', + name: 'product', + state: 'active', + metadata: JSON.stringify({ + name: 'product', + label: 'Product', + fields: { sku: { name: 'sku', label: 'SKU', type: 'text' } }, + }), + packageId: 'inventory_pkg', + }, + ]; + } + return []; + }, + findOne: async () => null, + create: async (_o: string, d: any) => d, + update: async (_o: string, _i: any, d: any) => d, + delete: async () => true, + syncSchema: async () => {}, + }); + + it('does NOT hydrate a project kernel (environmentId set) without the opt-in', async () => { + // Default behavior: a project kernel sources metadata from the artifact / + // control-plane proxy, so boot must not read a local sys_metadata. + const findCalls: Array<{ object: string }> = []; + await kernel.use({ + name: 'mock-noopt-driver', + type: 'driver', + version: '1.0.0', + init: async (ctx) => ctx.registerService('driver.noopt', makeLocalMetadataDriver(findCalls)), + }); + + const plugin = new ObjectQLPlugin({ environmentId: 'env_1' }); + await kernel.use(plugin); + await kernel.bootstrap(); + + expect(findCalls.find((c) => c.object === 'sys_metadata')).toBeUndefined(); + const registry = (kernel.getService('objectql') as any).registry; + expect(registry.getObject('product')).toBeUndefined(); + }); + + it('hydrates a project kernel from local sys_metadata when hydrateMetadataFromDb is set (runtime objects regain their fields)', async () => { + // The single-env tenant runtime case: an isolated, proxy-free kernel that + // persists its OWN sys_metadata. Objects created at runtime (here: + // `product`, not in any boot manifest) must re-enter the registry WITH + // their fields after a restart — otherwise registry.getObject('product') + // is empty and the engine.find unknown-$select guard can't fire. + const findCalls: Array<{ object: string }> = []; + await kernel.use({ + name: 'mock-opt-driver', + type: 'driver', + version: '1.0.0', + init: async (ctx) => ctx.registerService('driver.opt', makeLocalMetadataDriver(findCalls)), + }); + + const plugin = new ObjectQLPlugin({ environmentId: 'env_1', hydrateMetadataFromDb: true }); + await kernel.use(plugin); + await kernel.bootstrap(); + + expect(findCalls.find((c) => c.object === 'sys_metadata')).toBeDefined(); + const registry = (kernel.getService('objectql') as any).registry; + const product = registry.getObject('product') as any; + expect(product).toBeDefined(); + expect(product.fields).toBeDefined(); + expect(Object.keys(product.fields)).toContain('sku'); + }); + it('should not throw when protocol.loadMetaFromDb fails (graceful degradation)', async () => { // Arrange — driver that throws on find('sys_metadata') const mockDriver = { diff --git a/packages/objectql/src/plugin.ts b/packages/objectql/src/plugin.ts index e989b2644a..730e47970a 100644 --- a/packages/objectql/src/plugin.ts +++ b/packages/objectql/src/plugin.ts @@ -75,6 +75,28 @@ export interface ObjectQLPluginOptions { * code change. */ skipSchemaSync?: boolean; + /** + * Hydrate the SchemaRegistry from this kernel's local `sys_metadata` + * even when `environmentId` is set. + * + * By default Phase-2 hydration in `start()` is gated on + * `environmentId === undefined`, because the original multi-environment + * model assumed project kernels source metadata from a remote artifact / + * control-plane proxy and have NO local `sys_metadata` to read. That is + * NOT true for an isolated, proxy-free project kernel that persists its + * OWN `sys_metadata` locally (e.g. the cloud single-env tenant runtime on + * Turso): objects CREATED AT RUNTIME there — not present in the boot + * artifact manifest — would otherwise never re-enter the registry after a + * restart, so `registry.getObject(name)` returns nothing for them and any + * registry consumer (the unknown-`$select` guard, hooks, relationships) + * silently degrades. + * + * Set this ONLY when the kernel's registry is per-instance isolated AND + * `sys_metadata` lives on the kernel's own local driver (no control-plane + * proxy) — hydrating a proxied kernel would read the wrong database. + * Safe to leave unset: hydration tolerates a missing table. + */ + hydrateMetadataFromDb?: boolean; } export class ObjectQLPlugin implements Plugin { @@ -92,6 +114,7 @@ export class ObjectQLPlugin implements Plugin { private hostContext?: Record; private environmentId?: string; private skipSchemaSync = false; + private hydrateMetadataFromDb = false; /** Unsubscribe handles for metadata-event subscriptions (ADR-0008 PR-7). */ private metadataUnsubscribes: Array<() => void> = []; @@ -116,6 +139,7 @@ export class ObjectQLPlugin implements Plugin { typeof opts.skipSchemaSync === 'boolean' ? opts.skipSchemaSync : process.env.OS_SKIP_SCHEMA_SYNC === '1'; + this.hydrateMetadataFromDb = opts.hydrateMetadataFromDb === true; } init = async (ctx: PluginContext) => { @@ -313,11 +337,19 @@ export class ObjectQLPlugin implements Plugin { } // Phase 2: Hydrate SchemaRegistry from sys_metadata (loads custom/template objects). - // Project kernels (environmentId set) never persist sys_metadata locally — - // metadata is sourced from the artifact (MetadataPlugin) or routed to the - // control plane via ControlPlaneProxyDriver. Skip to avoid querying a table - // that does not exist on local project DBs. - if (this.environmentId === undefined) { + // Project kernels (environmentId set) USUALLY source metadata from the + // artifact (MetadataPlugin) or a control-plane proxy and have no local + // sys_metadata, so hydration is skipped to avoid querying a table that + // does not exist (or, worse, a proxied remote one). EXCEPTION: an + // isolated, proxy-free project kernel that persists its OWN sys_metadata + // locally (the cloud single-env tenant runtime) opts in via + // `hydrateMetadataFromDb` so objects CREATED AT RUNTIME there re-enter the + // registry after a restart — otherwise registry.getObject() returns + // nothing for them and every registry consumer (the unknown-$select + // guard, hooks, relationships) silently degrades. Safe because each engine + // owns its registry (no cross-kernel leakage) and hydration tolerates a + // missing table. + if (this.environmentId === undefined || this.hydrateMetadataFromDb) { await this.restoreMetadataFromDb(ctx); } else { ctx.logger.info('Project kernel — skipping sys_metadata hydration (metadata sourced from artifact)');