@nanobpm/urban 0.81.0 → 0.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/core/modules/workers.js +25 -1
- package/dist/runtime/core/read-model.d.ts +268 -5
- package/dist/runtime/core/read-model.js +1033 -35
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.js +6 -1
- package/package.json +1 -1
- package/src/runtime/core/modules/workers.ts +24 -1
- package/src/runtime/core/read-model.test.ts +27 -2
- package/src/runtime/core/read-model.ts +1313 -41
- package/src/runtime/core/rollup.test.ts +818 -0
- package/src/runtime/index.ts +43 -0
|
@@ -7,7 +7,7 @@ import { runLlmJob } from "./llm.js";
|
|
|
7
7
|
import { runInJobContext } from "../execContext.js";
|
|
8
8
|
import { readLineage } from "../lineage.js";
|
|
9
9
|
import { LineageStore } from "./lineage-store.js";
|
|
10
|
-
import { readModelRegistry } from "../read-model.js";
|
|
10
|
+
import { readModelRegistry, rollupRegistry } from "../read-model.js";
|
|
11
11
|
import { registerCanonicalProjections } from "./canonical-projections.js";
|
|
12
12
|
import { InstanceStateStore } from "./instance-state-store.js";
|
|
13
13
|
import { OpenUserTasksStore } from "./open-user-tasks-store.js";
|
|
@@ -99,6 +99,26 @@ function tryLineageStore(app, log) {
|
|
|
99
99
|
return undefined;
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Apply every managed rollup VIEW registered in the process-wide `rollupRegistry` (ADR 0065, #468) to
|
|
104
|
+
* the app's DEFAULT data source, in dependency order (a composed rollup after the rollup it reads).
|
|
105
|
+
* Mirrors {@link tryProvisionReadModelViews}: empty until a sibling task registers a rollup, and
|
|
106
|
+
* absent-/error-safe so a missing datasource or a VIEW-provisioning failure never breaks a worker mount.
|
|
107
|
+
*/
|
|
108
|
+
function tryProvisionRollupViews(app, log) {
|
|
109
|
+
if (rollupRegistry.all().length === 0)
|
|
110
|
+
return;
|
|
111
|
+
if (!app.data.hasDefaultSource()) {
|
|
112
|
+
log("debug", "rollups: no default data source; managed VIEW provisioning skipped");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
rollupRegistry.ensureViews(app.data.source().db);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
log("warn", "rollups: failed to provision managed VIEWs", { error: String(err) });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
102
122
|
/**
|
|
103
123
|
* Apply every managed read-model VIEW registered in the process-wide `readModelRegistry` to the app's
|
|
104
124
|
* DEFAULT data source (ADR 0065, surface #3). A `defineReadModel(...)` declaration compiles to a
|
|
@@ -202,6 +222,10 @@ export async function mountWorkers(ctx, app, sched = defaultScheduler()) {
|
|
|
202
222
|
// provision the projection tables per source (absent-safe), and finally the read-model VIEWs.
|
|
203
223
|
registerCanonicalProjections();
|
|
204
224
|
tryProvisionCanonicalProjections(app, ctx.host.log);
|
|
225
|
+
// Rollup VIEWs (ADR 0065, #468) BEFORE the read-model VIEWs: a read model's key-correlated rollup
|
|
226
|
+
// lookup `LEFT JOIN`s a rollup's `*_counts` VIEW, and a composed rollup reads another rollup's VIEW,
|
|
227
|
+
// so every rollup VIEW (in dependency order) must exist before any dependent VIEW compiles.
|
|
228
|
+
tryProvisionRollupViews(app, ctx.host.log);
|
|
205
229
|
tryProvisionReadModelViews(app, ctx.host.log);
|
|
206
230
|
// Cache module loads so a multi-type handler module is imported once.
|
|
207
231
|
const moduleCache = new Map();
|
|
@@ -10,7 +10,7 @@ export type Literal = string | number | boolean | null;
|
|
|
10
10
|
* `col` references a column of the BASE row. `pcol` references a column of the CORRELATED projection
|
|
11
11
|
* row and is only meaningful inside an {@link ExistsExpr} `where` predicate.
|
|
12
12
|
*/
|
|
13
|
-
export type Expr = LitExpr | ColExpr | ProjColExpr | CompareExpr | AndExpr | OrExpr | NotExpr | CaseExpr | ExistsExpr;
|
|
13
|
+
export type Expr = LitExpr | ColExpr | ProjColExpr | RollupColExpr | CompareExpr | AndExpr | OrExpr | NotExpr | IsNullExpr | CaseExpr | ExistsExpr;
|
|
14
14
|
export interface LitExpr {
|
|
15
15
|
readonly kind: "lit";
|
|
16
16
|
readonly value: Literal;
|
|
@@ -23,6 +23,13 @@ export interface ProjColExpr {
|
|
|
23
23
|
readonly kind: "pcol";
|
|
24
24
|
readonly name: string;
|
|
25
25
|
}
|
|
26
|
+
export interface RollupColExpr {
|
|
27
|
+
readonly kind: "rcol";
|
|
28
|
+
/** The read-model rollup-lookup alias this column is read from (see {@link RollupLookupDecl}). */
|
|
29
|
+
readonly lookup: string;
|
|
30
|
+
/** The rollup output column referenced (a column of the looked-up `*_counts` rollup row). */
|
|
31
|
+
readonly name: string;
|
|
32
|
+
}
|
|
26
33
|
export interface CompareExpr {
|
|
27
34
|
readonly kind: "compare";
|
|
28
35
|
readonly op: CompareOp;
|
|
@@ -41,6 +48,10 @@ export interface NotExpr {
|
|
|
41
48
|
readonly kind: "not";
|
|
42
49
|
readonly expr: Expr;
|
|
43
50
|
}
|
|
51
|
+
export interface IsNullExpr {
|
|
52
|
+
readonly kind: "isNull";
|
|
53
|
+
readonly expr: Expr;
|
|
54
|
+
}
|
|
44
55
|
export interface WhenClause {
|
|
45
56
|
readonly when: Expr;
|
|
46
57
|
readonly then: Expr;
|
|
@@ -63,6 +74,13 @@ export declare const lit: (value: Literal) => LitExpr;
|
|
|
63
74
|
export declare const col: (name: string) => ColExpr;
|
|
64
75
|
/** A reference to a correlated PROJECTION-row column (only valid inside {@link exists}). */
|
|
65
76
|
export declare const pcol: (name: string) => ProjColExpr;
|
|
77
|
+
/**
|
|
78
|
+
* A reference to a column of a key-correlated ROLLUP LOOKUP row (see {@link ReadModelDecl.lookups}).
|
|
79
|
+
* `rcol("c", "prs_opened")` reads the `prs_opened` column of the rollup joined under alias `"c"` for
|
|
80
|
+
* the base row (compiling to `LEFT JOIN <rollup> ON <key>` on the SQL side / the matching group row on
|
|
81
|
+
* the TS side). Only valid in a {@link defineReadModel} whose `lookups` declares that alias.
|
|
82
|
+
*/
|
|
83
|
+
export declare const rcol: (lookup: string, name: string) => RollupColExpr;
|
|
66
84
|
export declare const eq: (left: Expr, right: Expr) => CompareExpr;
|
|
67
85
|
export declare const neq: (left: Expr, right: Expr) => CompareExpr;
|
|
68
86
|
export declare const lt: (left: Expr, right: Expr) => CompareExpr;
|
|
@@ -72,6 +90,10 @@ export declare const gte: (left: Expr, right: Expr) => CompareExpr;
|
|
|
72
90
|
export declare const and: (...clauses: Expr[]) => AndExpr;
|
|
73
91
|
export declare const or: (...clauses: Expr[]) => OrExpr;
|
|
74
92
|
export declare const not: (expr: Expr) => NotExpr;
|
|
93
|
+
/** `<expr> IS NULL` — a null test (1/0 in SQL; `value == null` in TS). `not(isNull(x))` is `IS NOT NULL`. */
|
|
94
|
+
export declare const isNull: (expr: Expr) => IsNullExpr;
|
|
95
|
+
/** Sugar for `not(isNull(expr))` — `<expr> IS NOT NULL`. */
|
|
96
|
+
export declare const isNotNull: (expr: Expr) => NotExpr;
|
|
75
97
|
/** Build a `CASE WHEN … THEN … [WHEN …] ELSE … END` expression. */
|
|
76
98
|
export declare const caseWhen: (whens: WhenClause[], otherwise: Expr) => CaseExpr;
|
|
77
99
|
/** Sugar for a single `{ when, then }` clause used by {@link caseWhen}. */
|
|
@@ -127,6 +149,19 @@ export interface SqlCompileOptions {
|
|
|
127
149
|
readonly baseAlias?: string;
|
|
128
150
|
/** Resolve a projection NAME to its physical SQL table/view. Defaults to {@link projectionRegistry}. */
|
|
129
151
|
readonly resolveProjectionTable?: (name: string) => string;
|
|
152
|
+
/**
|
|
153
|
+
* Resolve a {@link col} reference to its qualified SQL expression, overriding the default
|
|
154
|
+
* `<baseAlias>."name"`. A rollup over a key-JOIN uses this to map a flat output-column name onto the
|
|
155
|
+
* correct joined relation (`col("pr_status")` → `"r"."status"`) so the closed {@link Expr} predicate
|
|
156
|
+
* machinery is reused unchanged across single-relation and join sources.
|
|
157
|
+
*/
|
|
158
|
+
readonly resolveColumn?: (name: string) => string;
|
|
159
|
+
/**
|
|
160
|
+
* Resolve an {@link rcol} reference (a key-correlated rollup-lookup column) to its qualified SQL
|
|
161
|
+
* expression. {@link defineReadModel} supplies this from its `lookups` so the lookup's NULL default
|
|
162
|
+
* (a LEFT-JOIN miss) compiles to `COALESCE(<alias>."name", <default>)`.
|
|
163
|
+
*/
|
|
164
|
+
readonly resolveRollupColumn?: (lookup: string, name: string) => string;
|
|
130
165
|
}
|
|
131
166
|
/** A conservative SQL identifier guard — we interpolate names directly into DDL/SQL. This is the
|
|
132
167
|
* single source of truth for "is this a legal identifier"; the manifest validator imports it (rather
|
|
@@ -155,8 +190,17 @@ export declare function isReservedObjectName(name: string): boolean;
|
|
|
155
190
|
* {@link compileToFn} produces cannot diverge.
|
|
156
191
|
*/
|
|
157
192
|
export declare function compileToSqlSelect(expr: Expr, options?: SqlCompileOptions): string;
|
|
193
|
+
/** The resolved single rollup-lookup rows for one evaluation, keyed by the FOLDED (case-insensitive,
|
|
194
|
+
* SQLite-folded) lookup alias — see {@link rcol}, whose `node.lookup` is folded the same way on read.
|
|
195
|
+
* Every declared lookup alias maps to exactly one resolved row: the matched rollup group with its
|
|
196
|
+
* missing/NULL columns already filled from the lookup's declared defaults (the TS twin of the VIEW's
|
|
197
|
+
* `COALESCE(alias.col, default)`). A LEFT-JOIN miss is therefore represented as a defaults/NULL-filled
|
|
198
|
+
* row — NOT an absent alias — so `rcol` always reads a stable object. Framework resolvers
|
|
199
|
+
* ({@link ReadModel.evaluate}, the parity guard) populate this; a raw `fnFor` caller on a model with
|
|
200
|
+
* lookups must pass the resolved map. */
|
|
201
|
+
export type LookupRows = Record<string, Record<string, unknown>>;
|
|
158
202
|
/** The compiled runtime derivation: same value as the SQL VIEW, computed in-process. */
|
|
159
|
-
export type DerivationFn = (baseRow: BaseRow, projections?: ProjectionRows) => unknown;
|
|
203
|
+
export type DerivationFn = (baseRow: BaseRow, projections?: ProjectionRows, lookups?: LookupRows) => unknown;
|
|
160
204
|
/**
|
|
161
205
|
* Compile ONE derivation expression to a TS function computing the SAME value as {@link compileToSqlSelect}.
|
|
162
206
|
* The function reads the base row for `col`, and — for `exists` — the `projections` argument for the
|
|
@@ -187,6 +231,37 @@ export interface ReadModelDecl {
|
|
|
187
231
|
* ones. Defaults to true (a read model is usually the base row PLUS its derived columns).
|
|
188
232
|
*/
|
|
189
233
|
readonly selectBaseColumns?: boolean;
|
|
234
|
+
/**
|
|
235
|
+
* Optional key-correlated ROLLUP LOOKUPS (ADR 0065, #468). Each declares a `LEFT JOIN <rollup> ON
|
|
236
|
+
* <key>` so a derived column can read the rollup's `*_counts` columns for the base row via
|
|
237
|
+
* {@link rcol}. This is the per-row half of the layering: the aggregate is single-sourced in
|
|
238
|
+
* {@link defineRollup}; the read model consumes its columns for a `CASE`-style per-row signal (the
|
|
239
|
+
* `plan_delivery`/`plan_wave_progress` shape). The join `on` must map the rollup's FULL group key so
|
|
240
|
+
* the lookup is single-valued (no fan-out of base rows).
|
|
241
|
+
*/
|
|
242
|
+
readonly lookups?: readonly RollupLookupDecl[];
|
|
243
|
+
}
|
|
244
|
+
/** One key-correlated rollup lookup on a {@link defineReadModel} (see {@link ReadModelDecl.lookups}). */
|
|
245
|
+
export interface RollupLookupDecl {
|
|
246
|
+
/** The alias `rcol(alias, …)` reads this lookup's columns under (a SQL identifier). */
|
|
247
|
+
readonly as: string;
|
|
248
|
+
/** The rollup whose `*_counts` VIEW is joined (its `name` is the physical relation, its columns the
|
|
249
|
+
* readable set). Pass the {@link Rollup} object so the read model can validate referenced columns. */
|
|
250
|
+
readonly rollup: Rollup;
|
|
251
|
+
/**
|
|
252
|
+
* The equi-join key: each `{ base, rollup }` pair equates a base column to a rollup group-key column.
|
|
253
|
+
* Must cover the rollup's ENTIRE `groupBy` so the joined row is unique (≤1 match per base row).
|
|
254
|
+
*/
|
|
255
|
+
readonly on: ReadonlyArray<{
|
|
256
|
+
readonly base: string;
|
|
257
|
+
readonly rollup: string;
|
|
258
|
+
}>;
|
|
259
|
+
/**
|
|
260
|
+
* Optional per-column defaults substituted when the join misses (no matching rollup group) or the
|
|
261
|
+
* rollup value is NULL — the `COALESCE(<alias>."col", <default>)` in the hand-authored VIEWs. Applied
|
|
262
|
+
* identically in both backends so an epic with no PRs reads `0` rather than NULL.
|
|
263
|
+
*/
|
|
264
|
+
readonly defaults?: Record<string, Literal>;
|
|
190
265
|
}
|
|
191
266
|
/** A compiled read model: the declaration plus its derived SQL, derived functions, and managed VIEW. */
|
|
192
267
|
export interface ReadModel {
|
|
@@ -197,8 +272,18 @@ export interface ReadModel {
|
|
|
197
272
|
sqlSelectFor(column: string, options?: SqlCompileOptions): string;
|
|
198
273
|
/** The TS derivation function for one derived column. */
|
|
199
274
|
fnFor(column: string): DerivationFn;
|
|
200
|
-
/** Evaluate ALL derived columns for a base row in-process (the TS backend of the whole model).
|
|
201
|
-
|
|
275
|
+
/** Evaluate ALL derived columns for a base row in-process (the TS backend of the whole model). The
|
|
276
|
+
* optional `lookupRows` supplies each rollup-lookup's candidate rows (keyed by lookup alias); the
|
|
277
|
+
* model resolves the single matching group row per lookup (applying declared defaults) before
|
|
278
|
+
* evaluating {@link rcol} references. */
|
|
279
|
+
evaluate(baseRow: BaseRow, projections?: ProjectionRows, lookupRows?: Record<string, ReadonlyArray<Record<string, unknown>>>): Record<string, unknown>;
|
|
280
|
+
/** Resolve each declared rollup lookup's single matching group row for `baseRow` (match on the join
|
|
281
|
+
* keys, fill declared defaults), producing the {@link LookupRows} a {@link DerivationFn} reads via
|
|
282
|
+
* {@link rcol}. The single source of the lookup-resolution rule: {@link evaluate} and the parity guard
|
|
283
|
+
* both call this so they can never drift. Throws if a lookup's candidate set yields MORE than one
|
|
284
|
+
* match for the join key — the lookup is required to be single-valued (its join covers the rollup's
|
|
285
|
+
* full group key), so multiple matches would fan out in SQL while TS silently picked one. */
|
|
286
|
+
resolveLookups(baseRow: BaseRow, lookupRows: Record<string, ReadonlyArray<Record<string, unknown>>>): LookupRows;
|
|
202
287
|
/** The managed `CREATE VIEW` DDL (seam (a) applies this at boot). Pass `{ temp: true }` to emit a
|
|
203
288
|
* `CREATE TEMP VIEW` in SQLite's TEMP schema (used by the parity guard so it never touches the
|
|
204
289
|
* application's real objects); the SELECT body is byte-identical either way. */
|
|
@@ -251,10 +336,15 @@ export declare class ReadModelRegistry {
|
|
|
251
336
|
* task registers its derived status-edge read model here so the runtime provisions its VIEW at boot.
|
|
252
337
|
*/
|
|
253
338
|
export declare const readModelRegistry: ReadModelRegistry;
|
|
254
|
-
/** A single parity check case: a base row
|
|
339
|
+
/** A single parity check case: a base row, the projection rows in scope for it, and (for a read model
|
|
340
|
+
* with rollup lookups) the candidate rollup rows per lookup alias. */
|
|
255
341
|
export interface ParitySample {
|
|
256
342
|
readonly baseRow: BaseRow;
|
|
257
343
|
readonly projections?: ProjectionRows;
|
|
344
|
+
/** Candidate rows for each declared rollup lookup, keyed by lookup alias (see {@link rcol}). The
|
|
345
|
+
* guard inserts them into the lookup rollup's fixture table so the VIEW's `LEFT JOIN` materialises,
|
|
346
|
+
* and hands the same rows to the TS backend to resolve — closing the lookup's parity surface. */
|
|
347
|
+
readonly lookups?: Record<string, ReadonlyArray<Record<string, unknown>>>;
|
|
258
348
|
}
|
|
259
349
|
/** The seam a parity guard needs to run the SQL backend: a SQLite handle it can create/query/drop. */
|
|
260
350
|
export interface ParityDb {
|
|
@@ -293,3 +383,176 @@ export interface ParityOptions {
|
|
|
293
383
|
* tables/views the caller's DB happens to hold under the same names.
|
|
294
384
|
*/
|
|
295
385
|
export declare function assertReadModelParity(model: ReadModel, db: ParityDb, samples: ParitySample[], options?: ParityOptions): void;
|
|
386
|
+
/** A closed, flat GROUP-BY aggregate expression (D2). `coalesce`/`add` wrap other aggregates so the
|
|
387
|
+
* exact `COALESCE(MIN(…), MAX(…))` and `MAX(…)+1` shapes the plan VIEWs use are expressible; every
|
|
388
|
+
* variant is handled exhaustively by both backends (SQL + TS reduce). */
|
|
389
|
+
export type AggExpr = CountAgg | CountWhereAgg | MaxAgg | MinWhereAgg | CoalesceAgg | AddAgg;
|
|
390
|
+
export interface CountAgg {
|
|
391
|
+
readonly kind: "count";
|
|
392
|
+
/** When set, `COUNT(col)` (non-NULL rows); omitted → `COUNT(*)` (all group rows). */
|
|
393
|
+
readonly column?: string;
|
|
394
|
+
}
|
|
395
|
+
export interface CountWhereAgg {
|
|
396
|
+
readonly kind: "countWhere";
|
|
397
|
+
/** `SUM(CASE WHEN pred THEN 1 ELSE 0 END)` — count of group rows satisfying the closed predicate. */
|
|
398
|
+
readonly pred: Expr;
|
|
399
|
+
}
|
|
400
|
+
export interface MaxAgg {
|
|
401
|
+
readonly kind: "max";
|
|
402
|
+
readonly column: string;
|
|
403
|
+
}
|
|
404
|
+
export interface MinWhereAgg {
|
|
405
|
+
readonly kind: "minWhere";
|
|
406
|
+
/** `MIN(CASE WHEN pred THEN col END)` — min of `col` over rows satisfying `pred` (NULL if none). */
|
|
407
|
+
readonly column: string;
|
|
408
|
+
readonly pred: Expr;
|
|
409
|
+
}
|
|
410
|
+
export interface CoalesceAgg {
|
|
411
|
+
readonly kind: "coalesce";
|
|
412
|
+
readonly a: AggExpr;
|
|
413
|
+
readonly b: AggExpr;
|
|
414
|
+
}
|
|
415
|
+
export interface AddAgg {
|
|
416
|
+
readonly kind: "add";
|
|
417
|
+
readonly expr: AggExpr;
|
|
418
|
+
/** An INTEGER literal added to `expr` (e.g. `MAX(wave) + 1`). NULL + literal is NULL, both backends. */
|
|
419
|
+
readonly literal: number;
|
|
420
|
+
}
|
|
421
|
+
/** `COUNT(col)` (non-NULL) when `column` is given, else `COUNT(*)`. */
|
|
422
|
+
export declare const count: (column?: string) => CountAgg;
|
|
423
|
+
/** `SUM(CASE WHEN pred THEN 1 ELSE 0 END)` — the count of group rows satisfying `pred`. */
|
|
424
|
+
export declare const countWhere: (pred: Expr) => CountWhereAgg;
|
|
425
|
+
/** `MAX(col)` over the group (ignores NULLs; NULL for an all-NULL/empty group). */
|
|
426
|
+
export declare const max: (column: string) => MaxAgg;
|
|
427
|
+
/** `MIN(CASE WHEN pred THEN col END)` — the min of `col` over group rows satisfying `pred`. */
|
|
428
|
+
export declare const minWhere: (column: string, pred: Expr) => MinWhereAgg;
|
|
429
|
+
/** `COALESCE(a, b)` over two aggregates — `a` unless it is NULL, then `b`. */
|
|
430
|
+
export declare const coalesce: (a: AggExpr, b: AggExpr) => CoalesceAgg;
|
|
431
|
+
/** `expr + <integer literal>` (e.g. `MAX(wave) + 1`). Rejects a non-integer literal at build time. */
|
|
432
|
+
export declare const add: (expr: AggExpr, literal: number) => AddAgg;
|
|
433
|
+
/** The relation a rollup groups over. `table`/`projection`/`rollup` expose their columns by name; a
|
|
434
|
+
* key-`join` exposes a FLAT namespace mapped by {@link RollupJoinSource.columns} (the two-hop reach —
|
|
435
|
+
* D4). All three are single-mechanism composable: a `rollup` source is another {@link Rollup}. */
|
|
436
|
+
export type RollupSource = RollupTableSource | RollupProjectionSource | RollupJoinSource | RollupRollupSource;
|
|
437
|
+
export interface RollupTableSource {
|
|
438
|
+
readonly kind: "table";
|
|
439
|
+
readonly table: string;
|
|
440
|
+
}
|
|
441
|
+
export interface RollupProjectionSource {
|
|
442
|
+
readonly kind: "projection";
|
|
443
|
+
readonly projection: string;
|
|
444
|
+
}
|
|
445
|
+
/** One relation in a {@link RollupJoinSource} — a base table/view/projection under a join alias. */
|
|
446
|
+
export interface RollupRelation {
|
|
447
|
+
readonly relation: string;
|
|
448
|
+
readonly alias: string;
|
|
449
|
+
}
|
|
450
|
+
export interface RollupJoinSource {
|
|
451
|
+
readonly kind: "join";
|
|
452
|
+
/** `"left"` (default) or `"inner"`. LEFT keeps unmatched left rows with NULL right columns. */
|
|
453
|
+
readonly type?: "left" | "inner";
|
|
454
|
+
readonly left: RollupRelation;
|
|
455
|
+
readonly right: RollupRelation;
|
|
456
|
+
/** Equi-join key(s): `left.<left> = right.<right>`. */
|
|
457
|
+
readonly on: ReadonlyArray<{
|
|
458
|
+
readonly left: string;
|
|
459
|
+
readonly right: string;
|
|
460
|
+
}>;
|
|
461
|
+
/**
|
|
462
|
+
* The FLAT output namespace: output column name → `[side, physicalColumn]`. This resolves collisions
|
|
463
|
+
* (both sides may have a `status`) and is what `groupBy`/aggregate `col(...)` references resolve
|
|
464
|
+
* against, so the closed predicate/aggregate machinery is reused unchanged over a join.
|
|
465
|
+
*/
|
|
466
|
+
readonly columns: Record<string, readonly [side: "left" | "right", column: string]>;
|
|
467
|
+
}
|
|
468
|
+
export interface RollupRollupSource {
|
|
469
|
+
readonly kind: "rollup";
|
|
470
|
+
readonly rollup: Rollup;
|
|
471
|
+
}
|
|
472
|
+
/** A rollup over a base table. */
|
|
473
|
+
export declare const fromTable: (table: string) => RollupTableSource;
|
|
474
|
+
/** A rollup over a canonical projection (resolved to its physical relation via {@link projectionRegistry}). */
|
|
475
|
+
export declare const fromProjection: (projection: string) => RollupProjectionSource;
|
|
476
|
+
/** A rollup over another rollup's `*_counts` VIEW (composable — e.g. wave's two levels). */
|
|
477
|
+
export declare const fromRollup: (rollup: Rollup) => RollupRollupSource;
|
|
478
|
+
/** A rollup over a declared key-join of two relations (the two-hop reach — D4). */
|
|
479
|
+
export declare const joinSource: (spec: Omit<RollupJoinSource, "kind">) => RollupJoinSource;
|
|
480
|
+
/** The in-memory rows the TS group-reduce sees per LEAF relation (base table/view/projection name). */
|
|
481
|
+
export type RollupInputs = Record<string, ReadonlyArray<Record<string, unknown>>>;
|
|
482
|
+
/** The declarative rollup input passed to {@link defineRollup}. */
|
|
483
|
+
export interface RollupDecl {
|
|
484
|
+
/** The managed VIEW name (also the rollup / canonical-projection identifier). */
|
|
485
|
+
readonly name: string;
|
|
486
|
+
/** The relation to group over (table / projection / key-join / another rollup). */
|
|
487
|
+
readonly source: RollupSource;
|
|
488
|
+
/** One or more group-key columns (referenced by name in the source's output namespace). */
|
|
489
|
+
readonly groupBy: readonly string[];
|
|
490
|
+
/** Optional pre-aggregation row filter (the `WHERE` — e.g. `isNotNull(col("wave"))`). */
|
|
491
|
+
readonly where?: Expr;
|
|
492
|
+
/** The aggregate columns: name → closed {@link AggExpr}. Authored ONCE, compiled to both backends. */
|
|
493
|
+
readonly aggregates: Record<string, AggExpr>;
|
|
494
|
+
}
|
|
495
|
+
/** A compiled rollup: the declaration plus both backends and the managed VIEW DDL. */
|
|
496
|
+
export interface Rollup {
|
|
497
|
+
readonly decl: RollupDecl;
|
|
498
|
+
/** The canonical-projection name (= `decl.name`). */
|
|
499
|
+
readonly projectionName: string;
|
|
500
|
+
/** The group-key columns (order preserved from the declaration). */
|
|
501
|
+
readonly groupBy: readonly string[];
|
|
502
|
+
/** All output columns: the group keys followed by the aggregate names (aggregates sorted by name). */
|
|
503
|
+
readonly outputColumns: readonly string[];
|
|
504
|
+
/** Leaf relation names this rollup reads, transitively (for fixtures / feeding the TS reduce). */
|
|
505
|
+
readonly sourceRelations: readonly string[];
|
|
506
|
+
/** This rollup and every rollup it composes, in dependency order (dependencies first) — the order the
|
|
507
|
+
* managed VIEWs must be created in. */
|
|
508
|
+
readonly viewChain: readonly Rollup[];
|
|
509
|
+
/** The SQLite select-list expression for one aggregate column. */
|
|
510
|
+
sqlAggFor(column: string, options?: SqlCompileOptions): string;
|
|
511
|
+
/** The in-process group-reduce (the TS backend): leaf rows → one output row per group. */
|
|
512
|
+
reduce(inputs: RollupInputs): Record<string, unknown>[];
|
|
513
|
+
/** The managed `CREATE VIEW` DDL. `{ temp: true }` emits a `CREATE TEMP VIEW` (parity-guard use). */
|
|
514
|
+
viewDdl(options?: SqlCompileOptions, viewOptions?: ViewDdlOptions): string;
|
|
515
|
+
/** Fixture column requirements per leaf relation (referenced physical columns), for the parity guard. */
|
|
516
|
+
fixtureColumns(): Map<string, Set<string>>;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Declare a GROUP-BY rollup ONCE (ADR 0065, #468). Returns a {@link Rollup} exposing both backends (the
|
|
520
|
+
* managed `*_counts` VIEW DDL and the TS group-reduce) driven from one closed spec, plus the metadata
|
|
521
|
+
* the parity guard and registry need. Register it in {@link rollupRegistry} to have the runtime apply
|
|
522
|
+
* its VIEW at boot and expose its name as a canonical projection.
|
|
523
|
+
*/
|
|
524
|
+
export declare function defineRollup(decl: RollupDecl): Rollup;
|
|
525
|
+
/**
|
|
526
|
+
* The rollup registry (ADR 0065, #468) — the GROUP-BY sibling of {@link ReadModelRegistry}. Registering
|
|
527
|
+
* a {@link Rollup} both (a) queues its managed VIEW for boot provisioning (in dependency order) and (b)
|
|
528
|
+
* registers its name as a canonical projection so read-model rollup lookups resolve. Idempotent: a
|
|
529
|
+
* re-register whose VIEW DDL matches is a no-op; a conflicting redefinition throws.
|
|
530
|
+
*/
|
|
531
|
+
export declare class RollupRegistry {
|
|
532
|
+
#private;
|
|
533
|
+
register(rollup: Rollup, projections?: ProjectionRegistry): void;
|
|
534
|
+
all(): Rollup[];
|
|
535
|
+
get(name: string): Rollup | undefined;
|
|
536
|
+
/** Apply every registered rollup's managed VIEW to a database, dependency-ordered (a composed rollup
|
|
537
|
+
* after the rollup it reads) and MANAGED (drop+recreate `main.`-qualified, mirroring
|
|
538
|
+
* {@link ReadModelRegistry.ensureViews}). Safe to call repeatedly. */
|
|
539
|
+
ensureViews(db: {
|
|
540
|
+
exec(sql: string): void;
|
|
541
|
+
}, options?: SqlCompileOptions): void;
|
|
542
|
+
clear(): void;
|
|
543
|
+
}
|
|
544
|
+
/** Process-wide rollup registry — the concrete registration point apps/sidecars import and call. */
|
|
545
|
+
export declare const rollupRegistry: RollupRegistry;
|
|
546
|
+
/** Options for {@link assertRollupParity}. */
|
|
547
|
+
export interface RollupParityOptions {
|
|
548
|
+
readonly onMismatch?: (message: string) => never;
|
|
549
|
+
readonly sql?: SqlCompileOptions;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* The framework PARITY GUARD for grouped rollups (#468) — the GROUP-BY sibling of
|
|
553
|
+
* {@link assertReadModelParity}. For each input-set it materialises the rollup's managed VIEW (and any
|
|
554
|
+
* composed dependency VIEWs) over throwaway TEMP fixtures built from the leaf rows, reads the SQL-derived
|
|
555
|
+
* group rows, computes the TS `reduce` for the same inputs, and asserts they agree as a SET keyed by the
|
|
556
|
+
* group columns (GROUP BY output order is unspecified, so rows are matched by key, not position).
|
|
557
|
+
*/
|
|
558
|
+
export declare function assertRollupParity(rollup: Rollup, db: ParityDb, sampleSets: RollupInputs[], options?: RollupParityOptions): void;
|