Skip to content

Commit 0e473fe

Browse files
committed
wip(objectql): stored-expression backfill module, engine compute entry, boot advisory
Claude-Session: https://claude.ai/code/session_01Utp6Cb2FL7H75wB1wA6PpZ Co-authored-by: Claude <noreply@anthropic.com>
1 parent b7075d9 commit 0e473fe

4 files changed

Lines changed: 644 additions & 0 deletions

File tree

‎packages/objectql/src/engine.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2101,6 +2101,35 @@ export class ObjectQL implements IObjectQLEngine {
21012101
return SecurityResolversFacet.serveFormulaField.call(securityResolversHost.of(this), objectName, record, field, context);
21022102
}
21032103

2104+
/**
2105+
* [#29961] The values `object`'s stored computed fields ({@link planStoredExpressions})
2106+
* take over rows ALREADY stored, computed and served exactly as a write
2107+
* computes them: the declared option labels, the one hop, and the `can()`
2108+
* map, under `context`. Index-aligned with `rows`, keyed by every stored
2109+
* computed field the object declares; evaluated over copies, writes nothing.
2110+
*
2111+
* For a row that never went through a write since its field was declared —
2112+
* `os migrate stored-expressions` (`stored-expression-backfill.ts`) and the
2113+
* boot advisory it shares. A field that does not evaluate computes `null`,
2114+
* the write's answer, and is told to `onFault` when given (else the engine's
2115+
* once-per-field report).
2116+
*/
2117+
async computeStoredExpressionsOver(
2118+
object: string,
2119+
rows: ReadonlyArray<Record<string, unknown>>,
2120+
context?: ExecutionContext,
2121+
onFault?: (field: string, message: string) => void,
2122+
): Promise<Array<Record<string, unknown>>> {
2123+
const schema = this._registry.getObject(object);
2124+
const resolution = this.permissionResolution(context);
2125+
return computeStoredExpressions(planStoredExpressions(schema), rows, context, {
2126+
permissionsFor: (plan, records) => this.resolveFormulaPermissions(object, plan, records, context, resolution),
2127+
onFault: onFault ? (field, error) => onFault(field, error.message) : this.formulaFaultSink(object),
2128+
relatedFor: (plan, records) => this.resolveFormulaRelated(schema, plan, records, context),
2129+
optionLabelsFor: (plan) => declaredOptionLabels(schema, plan),
2130+
});
2131+
}
2132+
21042133
private formulaOptionLabels(
21052134
schema: unknown,
21062135
plan: FormulaPlanEntry[],

‎packages/objectql/src/index.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,26 @@ export type {
531531
SummaryBackfillLogger,
532532
SummaryBackfillOptions,
533533
} from './summary-backfill.js';
534+
// [#29961] The backfill behind `os migrate stored-expressions`: stored computed
535+
// fields over rows no write has computed since the field was declared, and the
536+
// boot advisory naming it. Same split as the roll-up backfill above.
537+
export {
538+
backfillStoredExpressions,
539+
formatStoredExpressionBackfillReport,
540+
storedExpressionBackfillComplete,
541+
announceStoredExpressionHoles,
542+
sampleStoredExpressionHoles,
543+
} from './stored-expression-backfill.js';
544+
export type {
545+
StoredExpressionBackfillReport,
546+
StoredExpressionBackfillFieldOutcome,
547+
StoredExpressionBackfillFailure,
548+
StoredExpressionFault,
549+
StoredExpressionBackfillEngine,
550+
StoredExpressionBackfillLogger,
551+
StoredExpressionBackfillOptions,
552+
StoredExpressionHoleSample,
553+
} from './stored-expression-backfill.js';
534554
// [ADR-0144, ruling 6492000994] The explicit code backfill behind
535555
// `os migrate record-currency-codes`: rows stored before an amount switched to
536556
// `currencyMode: 'record'`. Same split as the roll-up backfill above.

‎packages/objectql/src/plugin.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { applyProtection } from '@objectstack/spec/shared';
3434
import { isFederatedObject } from './federated-object.js';
3535
// [#22070] The builtin audit stamps are wrapped like a bound hook but registered in code.
3636
import { wrapDeclarativeHook } from './hook-wrappers.js';
37+
import { announceStoredExpressionHoles } from './stored-expression-backfill.js';
3738
import { LifecycleService } from './lifecycle/lifecycle-service.js';
3839
import { lifecycleSettingsManifest } from './lifecycle/lifecycle-settings.js';
3940
import type { DanglingReferenceAuditOptions } from './integrity/dangling-reference-audit.js';
@@ -1196,6 +1197,13 @@ export class ObjectQLPlugin implements Plugin {
11961197
);
11971198
if (unservable) throw unservable;
11981199
await this.ql?.announceOpenMigrationGates();
1200+
// [#29961] A stored computed field declared on an object that already
1201+
// held rows is NULL on them until each is next written: name the
1202+
// fields and `os migrate stored-expressions`. Here, beside the gate
1203+
// advisory, for its reason: schema sync has run and every object is
1204+
// registered. A bounded sample read that writes nothing and never fails
1205+
// the boot (`stored-expression-backfill.ts`).
1206+
if (this.ql) await announceStoredExpressionHoles(this.ql, ctx.logger);
11991207
});
12001208
ctx.hook('metadata:reloaded', async (payload?: unknown) => {
12011209
await this.resyncAuthoredHooks(ctx);

0 commit comments

Comments
 (0)