@noy-db/hub 0.2.0-pre.30 → 0.2.0-pre.31

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.
Files changed (143) hide show
  1. package/dist/adapter/index.d.ts +9 -0
  2. package/dist/adapter/index.js +14 -0
  3. package/dist/attestation/index.d.ts +1 -1
  4. package/dist/attestation/index.js +3 -3
  5. package/dist/blobs/index.d.ts +3 -3
  6. package/dist/bundle/index.d.ts +3 -3
  7. package/dist/bundle/index.js +1 -1
  8. package/dist/{chunk-Y6YUWZTS.js → chunk-L5HHBOMS.js} +2 -2
  9. package/dist/{chunk-BVKLJBJJ.js → chunk-W5NVNJDX.js} +2 -1
  10. package/dist/{chunk-BVKLJBJJ.js.map → chunk-W5NVNJDX.js.map} +1 -1
  11. package/dist/consent/index.d.ts +2 -2
  12. package/dist/{decrypt-partition-Mhjh6nnG.d.ts → decrypt-partition-CXS5KopB.d.ts} +1 -1
  13. package/dist/derivations/index.d.ts +3 -3
  14. package/dist/{dev-unlock-DJ3IhgY9.d.ts → dev-unlock-CnHTTVea.d.ts} +1 -1
  15. package/dist/guards/index.d.ts +3 -3
  16. package/dist/{hash-DLwkYf1d.d.ts → hash-oc5paYl0.d.ts} +1 -1
  17. package/dist/history/index.d.ts +3 -3
  18. package/dist/i18n/index.d.ts +2 -2
  19. package/dist/{index-CMRIx_zx.d.ts → index-BmhGztUi.d.ts} +1 -1
  20. package/dist/index.d.ts +11 -11
  21. package/dist/index.js +2 -2
  22. package/dist/kernel/index.d.ts +2 -2
  23. package/dist/materialized-views/index.d.ts +3 -3
  24. package/dist/{mime-magic-CacDBwYp.d.ts → mime-magic-JeLKHRng.d.ts} +1 -1
  25. package/dist/{noydb-5MIBD77B.js → noydb-NAU2DTFP.js} +2 -2
  26. package/dist/noydb-NAU2DTFP.js.map +1 -0
  27. package/dist/overlay-views/index.d.ts +3 -3
  28. package/dist/periods/index.d.ts +2 -2
  29. package/dist/session/index.d.ts +3 -3
  30. package/dist/shadow/index.d.ts +2 -2
  31. package/dist/snapshots/index.d.ts +2 -2
  32. package/dist/store/index.d.ts +2 -2
  33. package/dist/sync/index.d.ts +1 -1
  34. package/dist/team/index.d.ts +2 -2
  35. package/dist/{transition-guard-J04-jime.d.ts → transition-guard-Dy3NioQr.d.ts} +1 -1
  36. package/dist/tx/index.d.ts +2 -2
  37. package/dist/{with-materialized-view-DNfzC5lH.d.ts → with-materialized-view-DIVeez0W.d.ts} +1 -1
  38. package/dist/{with-overlayed-view-qk3lbhOd.d.ts → with-overlayed-view-DVZrc5Hb.d.ts} +1 -1
  39. package/dist/{with-rollup-Dd3_ZG4b.d.ts → with-rollup-CbU-O4wP.d.ts} +1 -1
  40. package/package.json +62 -221
  41. package/dist/aggregate/index.cjs +0 -1144
  42. package/dist/aggregate/index.cjs.map +0 -1
  43. package/dist/aggregate/index.d.cts +0 -39
  44. package/dist/attestation/index.cjs +0 -305
  45. package/dist/attestation/index.cjs.map +0 -1
  46. package/dist/attestation/index.d.cts +0 -54
  47. package/dist/blobs/index.cjs +0 -1967
  48. package/dist/blobs/index.cjs.map +0 -1
  49. package/dist/blobs/index.d.cts +0 -48
  50. package/dist/bundle/index.cjs +0 -41045
  51. package/dist/bundle/index.cjs.map +0 -1
  52. package/dist/bundle/index.d.cts +0 -187
  53. package/dist/consent/index.cjs +0 -204
  54. package/dist/consent/index.cjs.map +0 -1
  55. package/dist/consent/index.d.cts +0 -27
  56. package/dist/crdt/index.cjs +0 -152
  57. package/dist/crdt/index.cjs.map +0 -1
  58. package/dist/crdt/index.d.cts +0 -30
  59. package/dist/decrypt-partition-C0iDpbSd.d.cts +0 -558
  60. package/dist/derivations/index.cjs +0 -469
  61. package/dist/derivations/index.cjs.map +0 -1
  62. package/dist/derivations/index.d.cts +0 -74
  63. package/dist/dev-unlock-0Z6yxfS7.d.cts +0 -263
  64. package/dist/discriminant-BN9REW3o.d.cts +0 -60
  65. package/dist/errors-BAWO5Z5a.d.cts +0 -1500
  66. package/dist/forget/index.cjs +0 -43
  67. package/dist/forget/index.cjs.map +0 -1
  68. package/dist/forget/index.d.cts +0 -1
  69. package/dist/guards/index.cjs +0 -455
  70. package/dist/guards/index.cjs.map +0 -1
  71. package/dist/guards/index.d.cts +0 -39
  72. package/dist/hash-Db8ncXGr.d.cts +0 -63
  73. package/dist/history/index.cjs +0 -1245
  74. package/dist/history/index.cjs.map +0 -1
  75. package/dist/history/index.d.cts +0 -65
  76. package/dist/i18n/index.cjs +0 -1280
  77. package/dist/i18n/index.cjs.map +0 -1
  78. package/dist/i18n/index.d.cts +0 -41
  79. package/dist/index-BMmajblo.d.cts +0 -362
  80. package/dist/index-C1SC1EPe.d.cts +0 -1325
  81. package/dist/index-xJEPkvMb.d.cts +0 -93
  82. package/dist/index.cjs +0 -48100
  83. package/dist/index.cjs.map +0 -1
  84. package/dist/index.d.cts +0 -1050
  85. package/dist/indexing/index.cjs +0 -803
  86. package/dist/indexing/index.cjs.map +0 -1
  87. package/dist/indexing/index.d.cts +0 -36
  88. package/dist/kernel/index.cjs +0 -751
  89. package/dist/kernel/index.cjs.map +0 -1
  90. package/dist/kernel/index.d.cts +0 -11
  91. package/dist/lazy-builder-eYZzLEL1.d.cts +0 -304
  92. package/dist/materialized-views/index.cjs +0 -1518
  93. package/dist/materialized-views/index.cjs.map +0 -1
  94. package/dist/materialized-views/index.d.cts +0 -187
  95. package/dist/mime-magic-sdMzsfVK.d.cts +0 -103
  96. package/dist/overlay-views/index.cjs +0 -399
  97. package/dist/overlay-views/index.cjs.map +0 -1
  98. package/dist/overlay-views/index.d.cts +0 -102
  99. package/dist/periods/index.cjs +0 -1041
  100. package/dist/periods/index.cjs.map +0 -1
  101. package/dist/periods/index.d.cts +0 -24
  102. package/dist/predicate-BmhBSPCH.d.cts +0 -267
  103. package/dist/query/index.cjs +0 -3480
  104. package/dist/query/index.cjs.map +0 -1
  105. package/dist/query/index.d.cts +0 -4
  106. package/dist/sealed-record/index.cjs +0 -139
  107. package/dist/sealed-record/index.cjs.map +0 -1
  108. package/dist/sealed-record/index.d.cts +0 -123
  109. package/dist/session/index.cjs +0 -495
  110. package/dist/session/index.cjs.map +0 -1
  111. package/dist/session/index.d.cts +0 -48
  112. package/dist/shadow/index.cjs +0 -133
  113. package/dist/shadow/index.cjs.map +0 -1
  114. package/dist/shadow/index.d.cts +0 -19
  115. package/dist/snapshots/index.cjs +0 -937
  116. package/dist/snapshots/index.cjs.map +0 -1
  117. package/dist/snapshots/index.d.cts +0 -30
  118. package/dist/store/index.cjs +0 -1091
  119. package/dist/store/index.cjs.map +0 -1
  120. package/dist/store/index.d.cts +0 -501
  121. package/dist/strategy-BSxFXGzb.d.cts +0 -110
  122. package/dist/strategy-mJExw4LQ.d.cts +0 -1069
  123. package/dist/sync/index.cjs +0 -1062
  124. package/dist/sync/index.cjs.map +0 -1
  125. package/dist/sync/index.d.cts +0 -45
  126. package/dist/team/index.cjs +0 -2805
  127. package/dist/team/index.cjs.map +0 -1
  128. package/dist/team/index.d.cts +0 -120
  129. package/dist/transition-guard-tWZIsaXe.d.cts +0 -165
  130. package/dist/tx/index.cjs +0 -614
  131. package/dist/tx/index.cjs.map +0 -1
  132. package/dist/tx/index.d.cts +0 -39
  133. package/dist/types-CdVfiQgt.d.cts +0 -15275
  134. package/dist/ulid-DRH25k3y.d.cts +0 -66
  135. package/dist/util/index.cjs +0 -237
  136. package/dist/util/index.cjs.map +0 -1
  137. package/dist/util/index.d.cts +0 -79
  138. package/dist/with-materialized-view-DfgPcvi9.d.cts +0 -27
  139. package/dist/with-overlayed-view-Cvfkx1lg.d.cts +0 -13
  140. package/dist/with-rollup-nIxo7h9z.d.cts +0 -47
  141. /package/dist/{noydb-5MIBD77B.js.map → adapter/index.js.map} +0 -0
  142. /package/dist/{chunk-Y6YUWZTS.js.map → chunk-L5HHBOMS.js.map} +0 -0
  143. /package/dist/{types-DHn9lEP0.d.ts → index-DHn9lEP0.d.ts} +0 -0
@@ -1,1325 +0,0 @@
1
- import { C as CollectionIndexes, a as Clause, O as Operator } from './predicate-BmhBSPCH.cjs';
2
- import { N as NoydbError } from './errors-BAWO5Z5a.cjs';
3
- import { a as I18nTextDescriptor, P as MoneyDescriptor, A as AggregateStrategy, k as AggregateSpec, l as Aggregation, j as AggregateResult, p as GroupedQuery, q as GroupedQueryN } from './strategy-mJExw4LQ.cjs';
4
-
5
- /**
6
- * Foreign-key references — the soft-FK mechanism.
7
- *
8
- * A collection declares its references as metadata at construction
9
- * time:
10
- *
11
- * ```ts
12
- * import { ref } from '@noy-db/hub'
13
- *
14
- * const invoices = company.collection<Invoice>('invoices', {
15
- * refs: {
16
- * clientId: ref('clients'), // default: strict
17
- * categoryId: ref('categories', 'warn'),
18
- * parentId: ref('invoices', 'cascade'), // self-reference OK
19
- * },
20
- * })
21
- * ```
22
- *
23
- * Three modes:
24
- *
25
- * - **strict** — the default. `put()` rejects records whose
26
- * reference target doesn't exist, and `delete()` of the target
27
- * rejects if any strict-referencing records still exist.
28
- * Matches SQL's default FK semantics.
29
- *
30
- * - **warn** — both operations succeed unconditionally. Broken
31
- * references surface only through
32
- * `vault.checkIntegrity()`, which walks every collection
33
- * and reports orphans. Use when you want soft validation for
34
- * imports from messy sources.
35
- *
36
- * - **cascade** — `put()` is same as warn. `delete()` of the
37
- * target deletes every referencing record. Cycles are detected
38
- * and broken via an in-progress set, so mutual cascades
39
- * terminate instead of recursing forever.
40
- *
41
- * Cross-vault refs are explicitly rejected: if the target
42
- * name contains a `/`, `ref()` throws `RefScopeError`. Cross-
43
- * vault refs need an auth story (multi-keyring reads) that
44
- * doesn't ship — tracked for.
45
- */
46
-
47
- /** The three enforcement modes. Default for new refs is `'strict'`. */
48
- type RefMode = 'strict' | 'warn' | 'cascade';
49
- /**
50
- * Descriptor returned by `ref()`. Collections accept a
51
- * `Record<string, RefDescriptor>` in their options. The key is the
52
- * field name on the record (top-level only — dotted paths are out of
53
- * scope), the value describes which target collection the
54
- * field references and under what mode.
55
- *
56
- * The descriptor carries only plain data so it can be serialized,
57
- * passed around, and introspected without any class machinery.
58
- */
59
- interface RefDescriptor {
60
- readonly target: string;
61
- readonly mode: RefMode;
62
- /**
63
- * Present and `true` only for an array ref (#377-A, `refArray()`): the
64
- * field holds an ARRAY of ids, each validated against `target`
65
- * independently (M:N). Absent for a scalar `ref()`. The same `mode`
66
- * semantics apply per element — strict rejects on a missing element at
67
- * put + blocks delete of a referenced target; cascade deletes every
68
- * record whose array contains the deleted id; warn surfaces orphans
69
- * only via `checkIntegrity()`.
70
- */
71
- readonly isArray?: true;
72
- }
73
- /** Runtime predicate: is this an array ref (`refArray()`) vs a scalar `ref()`? */
74
- declare function isRefArray(desc: RefDescriptor): boolean;
75
- /**
76
- * Thrown when a strict reference is violated — either `put()` with a
77
- * missing target id, or `delete()` of a target that still has
78
- * strict-referencing records.
79
- *
80
- * Carries structured detail so UI code (and a potential future
81
- * devtools panel) can render "client X cannot be deleted because
82
- * invoices 1, 2, and 3 reference it" instead of a bare error string.
83
- */
84
- declare class RefIntegrityError extends NoydbError {
85
- readonly collection: string;
86
- readonly id: string;
87
- readonly field: string;
88
- readonly refTo: string;
89
- readonly refId: string | null;
90
- constructor(opts: {
91
- collection: string;
92
- id: string;
93
- field: string;
94
- refTo: string;
95
- refId: string | null;
96
- message: string;
97
- });
98
- }
99
- /**
100
- * Thrown when `ref()` is called with a target name that looks like
101
- * a cross-vault reference (contains a `/`). Separate error
102
- * class because the fix is different: RefIntegrityError means "data
103
- * is wrong"; RefScopeError means "the ref declaration is wrong".
104
- */
105
- declare class RefScopeError extends NoydbError {
106
- constructor(target: string);
107
- }
108
- /**
109
- * Helper constructor. Thin wrapper around the object literal so user
110
- * code reads like `ref('clients')` instead of `{ target: 'clients',
111
- * mode: 'strict' }` — this is the only ergonomics reason it exists.
112
- *
113
- * Validates the target name eagerly so a misconfigured ref declaration
114
- * fails at collection construction time, not at the first put.
115
- */
116
- declare function ref(target: string, mode?: RefMode): RefDescriptor;
117
- /**
118
- * Array reference (#377-A) — the many-to-many soft-FK. The field holds an
119
- * array of ids; each element is validated against `target` independently.
120
- *
121
- * ```ts
122
- * const orders = company.collection<Order>('orders', {
123
- * refs: { productIds: refArray('products', 'warn') },
124
- * })
125
- * ```
126
- *
127
- * Same three `mode` semantics as `ref()`, applied per element:
128
- * - **strict** — `put()` rejects if ANY element's target is missing;
129
- * `delete()` of a target is blocked while any record's array still
130
- * contains its id.
131
- * - **warn** — both succeed; orphaned elements surface via
132
- * `vault.checkIntegrity()` (one violation per dangling element).
133
- * - **cascade** — `delete()` of a target deletes every record whose
134
- * array contains its id (cycle-safe, like scalar cascade).
135
- *
136
- * A `null`/`undefined` field is allowed (no links). Non-array values, or
137
- * non-string/number elements, are an integrity error. Cross-vault targets
138
- * are rejected exactly as in `ref()`.
139
- */
140
- declare function refArray(target: string, mode?: RefMode): RefDescriptor;
141
- /**
142
- * Per-vault registry of reference declarations.
143
- *
144
- * The registry is populated by `Collection` constructors (which pass
145
- * their `refs` option through the Vault) and consulted by the
146
- * Vault on every `put` / `delete` and by `checkIntegrity`. A
147
- * single instance lives on the Vault for its lifetime; there's
148
- * no global state.
149
- *
150
- * The data structure is two parallel maps:
151
- *
152
- * - `outbound`: `collection → { field → RefDescriptor }` — what
153
- * refs does `collection` declare? Used on put to check
154
- * strict-target-exists and on checkIntegrity to walk each
155
- * collection's outbound refs.
156
- *
157
- * - `inbound`: `target → Array<{ collection, field, mode }>` —
158
- * which collections reference `target`? Used on delete to find
159
- * the records that might be affected by cascade / strict.
160
- *
161
- * The two views are kept in sync by `register()` and never mutated
162
- * otherwise — refs can't be unregistered at runtime in.
163
- */
164
- declare class RefRegistry {
165
- private readonly outbound;
166
- private readonly inbound;
167
- /**
168
- * Register the refs declared by a single collection. Idempotent in
169
- * the happy path — calling twice with the same data is a no-op.
170
- * Calling twice with DIFFERENT data throws, because silent
171
- * overrides would be confusing ("I changed the ref and it doesn't
172
- * update" vs "I declared the same collection twice with different
173
- * refs and the second call won").
174
- */
175
- register(collection: string, refs: Record<string, RefDescriptor>): void;
176
- /** Get the outbound refs declared by a collection (or `{}` if none). */
177
- getOutbound(collection: string): Record<string, RefDescriptor>;
178
- /** Get the inbound refs that target a given collection (or `[]`). */
179
- getInbound(target: string): ReadonlyArray<{
180
- collection: string;
181
- field: string;
182
- mode: RefMode;
183
- isArray?: true;
184
- }>;
185
- /**
186
- * Iterate every (collection → refs) pair that has at least one
187
- * declared reference. Used by `checkIntegrity` to walk the full
188
- * universe of outbound refs without needing to track collection
189
- * names elsewhere.
190
- */
191
- entries(): Array<[string, Record<string, RefDescriptor>]>;
192
- /** Clear the registry. Test-only escape hatch; never called from production code. */
193
- clear(): void;
194
- }
195
- /**
196
- * Shape of a single violation reported by `vault.checkIntegrity()`.
197
- *
198
- * `refId` is the value we saw in the referencing field — it's the
199
- * ID we expected to find in `refTo`, but didn't. Left as `unknown`
200
- * because records are loosely typed at the integrity-check layer.
201
- */
202
- interface RefViolation {
203
- readonly collection: string;
204
- readonly id: string;
205
- readonly field: string;
206
- readonly refTo: string;
207
- readonly refId: unknown;
208
- readonly mode: RefMode;
209
- }
210
-
211
- /**
212
- * Query DSL `.join()` — eager, single-FK, intra-vault joins.
213
- *
214
- * resolves a ref()-declared foreign key into an attached
215
- * right-side record under an alias, using one of two planner paths
216
- * selected automatically:
217
- *
218
- * - **nested-loop** — right-side source exposes `lookupById`, so
219
- * each left row costs O(1). This is the common path for joins
220
- * against a Collection, which backs `lookupById` with a Map
221
- * lookup.
222
- * - **hash** — right-side has only `snapshot()`. Build a
223
- * `Map<id, record>` once, probe per left row. Same asymptotic
224
- * cost for our collections, but the path exists as a fallback
225
- * for custom QuerySource implementations and as an explicit
226
- * test-only override via `{ strategy: 'hash' }`.
227
- *
228
- * Scope:
229
- *
230
- * - Equi-joins on declared `ref()` fields only. Joins on
231
- * undeclared fields throw at plan time with an actionable error
232
- * naming the field and collection.
233
- * - Same-vault only. Cross-vault correlation goes
234
- * through `queryAcross`; this is an architectural
235
- * invariant, not a limitation we plan to lift.
236
- * - Hard row ceiling via `JoinTooLargeError` — default 50k per
237
- * side, override via `{ maxRows }`. Warns at 80% of the ceiling
238
- * on the existing warn channel.
239
- * - Three ref-mode behaviors on dangling refs:
240
- * strict → `DanglingReferenceError`,
241
- * warn → attach `null` with a one-shot warning,
242
- * cascade → attach `null` silently (cascade is a delete-time
243
- * mode; any dangling refs still present at read time are
244
- * mid-flight cascades or orphans from earlier, not a DSL error).
245
- *
246
- * Partition-awareness seam:
247
- *
248
- * Every `JoinLeg` carries a `partitionScope` field that is always
249
- * `'all'` in. The executor never reads this field.
250
- * partition-aware joins will start populating it from `where()`
251
- * predicates on the partition key without changing the planner's
252
- * external shape — this is the whole reason it exists now.
253
- *
254
- * Joins stay OUT of the ledger: reads don't touch `_ledger/`,
255
- * including joined reads.
256
- */
257
-
258
- /** Planner strategy for a single join leg. Auto-selected unless overridden. */
259
- type JoinStrategy = 'hash' | 'nested';
260
- /** Default per-side row ceiling before `.join()` throws `JoinTooLargeError`. */
261
- declare const DEFAULT_JOIN_MAX_ROWS = 50000;
262
- /**
263
- * Internal representation of a single join leg in the query plan.
264
- *
265
- * This is the primary place where constraint #1 is honored:
266
- * every leg carries a `partitionScope` field that is always `'all'`
267
- * in and is never read by the executor. partition-aware
268
- * joins will start populating it from `where()` predicates on the
269
- * partition key without changing the planner's external shape.
270
- */
271
- interface JoinLeg {
272
- /** Field on the left-side record holding the foreign key value. */
273
- readonly field: string;
274
- /** Alias key under which the joined right-side record attaches. */
275
- readonly as: string;
276
- /** Target collection name, resolved from the `ref()` declaration. */
277
- readonly target: string;
278
- /** Ref mode controlling behavior on dangling refs at read time. */
279
- readonly mode: RefMode;
280
- /** Manual planner strategy override. `undefined` → auto-select. */
281
- readonly strategy: JoinStrategy | undefined;
282
- /** Per-side row ceiling override. `undefined` → DEFAULT_JOIN_MAX_ROWS. */
283
- readonly maxRows: number | undefined;
284
- /**
285
- * Partition scope for future partition-aware joins. Always `'all'`
286
- * today — the executor never reads this field. Future versions will
287
- * populate it from `where()` predicates without breaking the
288
- * planner's external shape. Do not remove even though it looks
289
- * unused today — that's the whole point of having it.
290
- */
291
- readonly partitionScope: 'all' | readonly string[];
292
- /**
293
- * When `true`, this is a dictionary join. The executor
294
- * resolves the left-field value against the dict snapshot and
295
- * attaches `{ ...labels, key }` rather than a right-side record.
296
- * `target` holds the dictionary name (not a collection name).
297
- */
298
- readonly isDictJoin?: true;
299
- }
300
- /**
301
- * Minimal shape of a joinable right-side record source.
302
- *
303
- * Collections implement this structurally via their `QuerySource`;
304
- * sources without `lookupById` force the hash-join fallback. Kept as
305
- * a thin interface so tests can wire up plain-object sources without
306
- * pulling in the full Collection class.
307
- *
308
- * The optional `subscribe` is used by `Query.live()` to merge
309
- * right-side change streams into the live re-run trigger. Sources
310
- * that omit `subscribe` still work for live joins — they just
311
- * don't drive re-fires when their right side mutates. Collection
312
- * implements `subscribe` by hooking into the existing per-
313
- * vault event emitter.
314
- */
315
- interface JoinableSource {
316
- snapshot(): readonly unknown[];
317
- lookupById?(id: string): unknown;
318
- /**
319
- * Default locale a label-resolving query falls back to when the query
320
- * itself is locale-less. Set by a `staticDict()`-backed source from its
321
- * `displayLocale` so `{ by: 'label' }` resolves under a locale-less read
322
- * (#291). Plain `_dict_*`-backed sources omit it.
323
- */
324
- readonly displayLocale?: string;
325
- /**
326
- * i18nText descriptors of the right-side collection (#285 §3, `join`
327
- * layer). When present and the query carries a locale, each joined
328
- * right-side record's i18n fields resolve to that locale at the `join`
329
- * layer (`resolvePolicy(onMissing, 'join')`) BEFORE it is attached under
330
- * the leg's alias — so a joined `i18nText` field is a resolved string, not
331
- * a raw `{ locale }` map. Locale-less queries leave joined fields raw
332
- * (consistent with a locale-less read).
333
- */
334
- readonly i18nFields?: Record<string, I18nTextDescriptor>;
335
- /**
336
- * Subscribe to mutations on this source. The callback fires
337
- * AFTER the underlying record set has been updated. Returns an
338
- * unsubscribe function. Optional — sources without this method
339
- * cannot trigger live-join re-fires from their side.
340
- */
341
- subscribe?(cb: () => void): () => void;
342
- }
343
- /**
344
- * Join resolution context attached to a `Query` when it's constructed
345
- * from a `Collection`. Holds everything the `.join()` method needs to
346
- * translate a field name into a target collection + ref mode, and
347
- * everything the executor needs to read the right side.
348
- *
349
- * Kept as a structural interface so `Vault` can implement it
350
- * without `Query` needing to import `Vault` (circular-import
351
- * avoid). The Collection wires this up in its `query()` method using
352
- * the `joinResolver` back-reference the Vault passes in.
353
- */
354
- interface JoinContext {
355
- /** Name of the left-side (owning) collection. */
356
- readonly leftCollection: string;
357
- /**
358
- * The owning collection's default locale (#285 §3). Used to resolve joined
359
- * i18n fields at the `join` layer when a terminal call doesn't pass an
360
- * explicit locale — so `openVault({ locale })` flows to joins like it does
361
- * to `get`/`list`. A per-call `toArray({ locale })` overrides it.
362
- */
363
- readonly defaultLocale?: string;
364
- /** Look up a `RefDescriptor` by field name on the left collection. */
365
- resolveRef(field: string): RefDescriptor | null;
366
- /** Resolve a right-side source by target collection name. */
367
- resolveSource(collectionName: string): JoinableSource | null;
368
- /**
369
- * Resolve a dictKey join source. Returns a `JoinableSource`
370
- * whose snapshot exposes `{ key, ...labels }` records, keyed by the
371
- * stable dictionary key. `null` when the field is not a dictKey.
372
- *
373
- * The source is built from the compartment's in-memory dictionary
374
- * snapshot — same data as `DictionaryHandle.list()`, O(1) per lookup.
375
- */
376
- resolveDictSource?(field: string): JoinableSource | null;
377
- }
378
- /**
379
- * Apply every join leg in the plan against a base set of left-side
380
- * rows. Called by the query executor after `where` / `orderBy` /
381
- * `offset` / `limit` have narrowed the left set.
382
- *
383
- * Each leg attaches a `leg.as` field to every row. Returns a new
384
- * array of plain objects — the original left rows are not mutated
385
- * (structural sharing is fine for the inner fields, but the
386
- * top-level object is a fresh clone so consumers can further mutate
387
- * safely).
388
- *
389
- * **Ordering:** joins run AFTER orderBy / limit / offset in v1.
390
- * This keeps the planner simple and means queries like "top 10
391
- * invoices with client" sort and paginate the left side first, then
392
- * join. Sorting *by* a joined field is out of scope for — users
393
- * can post-sort the result array in userland or wait for
394
- * (multi-FK chaining) which can be layered on top.
395
- *
396
- * **Multi-FK chaining:** each leg's `maxRows` is enforced
397
- * against the current left-row count independently. Because
398
- * joins are equi-joins on the target's primary key (one-to-one or
399
- * one-to-null), the left row count is constant across legs — no
400
- * cartesian blowup. The per-leg left-side check is still necessary
401
- * so that a later leg with a tighter ceiling correctly fires on a
402
- * query like `.join('a', { maxRows: 100_000 }).join('b', { maxRows: 50 })`,
403
- * which should throw on the second leg if the left set exceeds 50.
404
- */
405
- declare function applyJoins(rows: readonly unknown[], joins: readonly JoinLeg[], context: JoinContext, locale?: string): unknown[];
406
- /**
407
- * Test-only: reset the join warning deduplication state between
408
- * tests. Production code never calls this — the dedup state is
409
- * intentionally process-scoped so a noisy query doesn't spam the
410
- * console once per component render.
411
- */
412
- declare function resetJoinWarnings(): void;
413
-
414
- /**
415
- * Reactive query primitive — `query.live()`.
416
- *
417
- * produces a `LiveQuery<T>` that re-runs the query and
418
- * updates its `value` whenever any source feeding it (the left
419
- * collection AND every right-side collection a join leg points at)
420
- * mutates.
421
- *
422
- * Framework-agnostic by design. The Vue layer wraps a `LiveQuery`
423
- * in a Vue `Ref<T[]>` by subscribing once and copying `value` into
424
- * the ref on every notification. React/Solid/Svelte adapters do the
425
- * same with their own primitives. Core never depends on a UI
426
- * framework.
427
- *
428
- * **Error semantics.** A `.live()` query may throw at re-run time —
429
- * a strict-mode `DanglingReferenceError` is the most common case
430
- * (a right-side record was deleted out-of-band, leaving a left
431
- * row's FK pointing at nothing). When the re-run throws, the
432
- * `LiveQuery` catches the error and stores it in the `error`
433
- * field; it does NOT propagate the throw out of the source's
434
- * change handler, because doing so would tear down whatever
435
- * upstream emitter is dispatching. Listeners check `error` after
436
- * each notification and render an error state in the UI.
437
- *
438
- * **Dedup of right-side subscriptions.** A multi-FK chain that
439
- * joins the same target twice (e.g.
440
- * `.join('billingClientId').join('shippingClientId')`, both
441
- * pointing at `clients`) only subscribes to that target once. We
442
- * dedup by target collection name, on the assumption that
443
- * `resolveSource(name)` returns a single subscribable source per
444
- * vault + name. Vault's `resolveSource` reads from
445
- * `collectionCache` so this assumption holds.
446
- *
447
- * **What .live() does NOT do in v1:**
448
- * - No granular delta updates — the whole query re-runs on every
449
- * change. Granular delta tracking is a v2 optimization once
450
- * the API is stable.
451
- * - No batching of bursty changes — one event in, one re-run
452
- * out. Batching with microtask coalescing is a v2 enhancement.
453
- * - No async notifications — every notification is synchronous
454
- * within the source's change handler.
455
- * - No re-planning under live mutations — the planner picks once
456
- * at subscription time and reuses the same plan for every
457
- * re-run.
458
- */
459
- /**
460
- * The reactive primitive returned by `Query.live()`.
461
- *
462
- * Listeners can read the current `value` snapshot at any time and
463
- * subscribe to changes via `.subscribe(cb)`. The `error` field
464
- * carries the most recent re-run error, if any — read it after
465
- * each notification to render error state.
466
- *
467
- * Always call `stop()` when the live query is no longer needed.
468
- * Without it, the upstream change-stream subscriptions stay live
469
- * forever and the query keeps re-running on every mutation.
470
- */
471
- interface LiveQuery<T> {
472
- /**
473
- * Current snapshot of the query result. Updated in place on
474
- * every upstream change. The reference returned is the same
475
- * `readonly T[]` array — consumers that want change detection by
476
- * reference should copy: `const arr = [...live.value]`.
477
- */
478
- readonly value: readonly T[];
479
- /**
480
- * Most recent re-run error, or `null` on success. Set when the
481
- * executor throws (e.g. `DanglingReferenceError` in strict mode
482
- * after a right-side delete). Cleared on the next successful
483
- * re-run.
484
- */
485
- readonly error: Error | null;
486
- /**
487
- * Register a notification callback. Fires AFTER `value` and
488
- * `error` have been updated for a given upstream change.
489
- * Returns an unsubscribe function.
490
- *
491
- * The first call to `subscribe` does NOT fire the callback
492
- * immediately — call sites that want the initial value should
493
- * read `live.value` directly before subscribing.
494
- */
495
- subscribe(cb: () => void): () => void;
496
- /**
497
- * Tear down every upstream subscription and clear the listener
498
- * set. Idempotent — calling twice is safe. After `stop()`, the
499
- * query no longer re-runs and `subscribe()` becomes a no-op
500
- * (the returned unsubscribe is still callable and is also a
501
- * no-op).
502
- */
503
- stop(): void;
504
- }
505
- /**
506
- * Internal subscription handle for an upstream source — left or
507
- * right side. The contract is just `subscribe(cb): unsubscribe`,
508
- * matching the existing `QuerySource.subscribe` and the new
509
- * `JoinableSource.subscribe` (added in ).
510
- */
511
- interface LiveUpstream {
512
- subscribe(cb: () => void): () => void;
513
- }
514
- /**
515
- * Build a LiveQuery from a `recompute` callback (typically the
516
- * Query's bound `toArray`) and a list of upstream sources to
517
- * subscribe to.
518
- *
519
- * The recompute fires once synchronously to populate the initial
520
- * value, then re-fires every time any upstream notifies. Errors
521
- * thrown by recompute are caught and stored in `error` instead of
522
- * propagating — see the file docstring for the rationale.
523
- */
524
- declare function buildLiveQuery<T>(recompute: () => T[], upstreams: readonly LiveUpstream[]): LiveQuery<T>;
525
-
526
- /**
527
- * Chainable, immutable query builder.
528
- *
529
- * Each builder operation returns a NEW Query — the underlying plan is never
530
- * mutated. This makes plans safe to share, cache, and serialize.
531
- */
532
-
533
- interface OrderBy {
534
- readonly field: string;
535
- readonly direction: 'asc' | 'desc';
536
- /**
537
- * Sort key for a `dictKey`/`staticDict` field (#285): `'value'` (default)
538
- * sorts by the stored code; `'label'` sorts by the code's resolved label at
539
- * the query locale (`toArray({ locale })`, or a `staticDict` `displayLocale`).
540
- * Falls back to the code when no label resolves.
541
- */
542
- readonly by?: 'value' | 'label';
543
- }
544
- /**
545
- * A complete query plan: zero-or-more clauses, optional ordering, pagination,
546
- * and optional joins.
547
- *
548
- * Plans are JSON-serializable as long as no FilterClause is present and no
549
- * join leg carries a manual `strategy` override (JoinLeg itself is plain
550
- * data, so it serializes cleanly).
551
- *
552
- * Plans are intentionally NOT parametric on T — see `predicate.ts` FilterClause
553
- * for the variance reasoning. The public `Query<T>` API attaches the type tag.
554
- */
555
- interface QueryPlan {
556
- readonly clauses: readonly Clause[];
557
- readonly orderBy: readonly OrderBy[];
558
- readonly limit: number | undefined;
559
- readonly offset: number;
560
- /**
561
- * Zero-or-more join legs to apply after where/orderBy/limit/offset.
562
- * Each leg attaches a resolved right-side record (or null) under its
563
- * alias. See `query/join.ts` for the full semantics.
564
- */
565
- readonly joins: readonly JoinLeg[];
566
- }
567
- /** Default row ceiling for cross-join expansion. Matches JoinTooLargeError's ceiling. */
568
- declare const DEFAULT_CROSS_JOIN_MAX_ROWS = 50000;
569
- /**
570
- * Source of records that a query executes against.
571
- *
572
- * The interface is non-parametric to keep variance friendly: callers cast
573
- * their typed source (e.g. `QuerySource<Invoice>`) into this opaque shape.
574
- *
575
- * `getIndexes` and `lookupById` are optional fast-path hooks. When both are
576
- * present and a where clause matches an indexed field, the executor uses
577
- * the index to skip a linear scan. Sources without these methods (or with
578
- * `getIndexes` returning `null`) always fall back to a linear scan.
579
- */
580
- interface QuerySource<T> {
581
- /** Snapshot of all current records. The query never mutates this array. */
582
- snapshot(): readonly T[];
583
- /** Subscribe to mutations; returns an unsubscribe function. */
584
- subscribe?(cb: () => void): () => void;
585
- /** Index store for the indexed-fast-path. Optional. */
586
- getIndexes?(): CollectionIndexes | null;
587
- /** O(1) record lookup by id, used to materialize index hits. */
588
- lookupById?(id: string): T | undefined;
589
- /**
590
- * Money field descriptors for the backing collection, used to rewrite
591
- * `sum`/`min`/`max` over money fields into exact BigInt reducers.
592
- */
593
- moneyFields?: Record<string, MoneyDescriptor>;
594
- /**
595
- * #308 L3 — id-paired snapshot for `Query._idArray()` (the `retrieve({within})`
596
- * id projection). Optional: only collection-backed queries supply it.
597
- */
598
- snapshotEntries?(): readonly {
599
- id: string;
600
- record: T;
601
- }[];
602
- }
603
- /**
604
- * The chainable builder. All methods return a new Query — the original
605
- * remains unchanged. Terminal methods (`toArray`, `first`, `count`,
606
- * `subscribe`) execute the plan against the source.
607
- *
608
- * Type parameter T flows through the public API for ergonomics, but the
609
- * internal storage uses `unknown` so Collection<T> stays covariant.
610
- *
611
- * The optional `joinContext` is attached when the Query is constructed
612
- * via `Collection.query()` (Collection passes in a context built from
613
- * the Vault's join resolver). A Query constructed via `new Query`
614
- * directly — e.g. from tests with a plain-object source — has no
615
- * joinContext, and calling `.join()` on it throws with an actionable
616
- * error. See `query/join.ts` for the full design.
617
- */
618
- /**
619
- * Declared deterministic predicate. Carries the consumer's
620
- * stable `hash` (for function-body identity), the function itself,
621
- * and is keyed by name when registered on a `Query<T>` via
622
- * `_withPredicates()`.
623
- */
624
- interface DeclaredPredicate {
625
- hash: string;
626
- fn: (record: unknown, ctx?: unknown) => boolean;
627
- }
628
- declare class Query<T> {
629
- private readonly source;
630
- private readonly plan;
631
- private readonly joinContext;
632
- private readonly aggregateStrategy;
633
- private readonly predicates;
634
- constructor(source: QuerySource<T>, plan?: QueryPlan, joinContext?: JoinContext, aggregateStrategy?: AggregateStrategy, predicates?: ReadonlyMap<string, DeclaredPredicate>);
635
- /**
636
- * @internal — accessor for the materialized-view dependency
637
- * analyzer. Not part of the public API; consumers should use the
638
- * builder methods, not inspect the plan directly.
639
- */
640
- _plan(): QueryPlan;
641
- /**
642
- * @internal — accessor for the materialized-view dependency
643
- * analyzer. Returns the join resolution context (or `undefined` for
644
- * queries constructed without a Collection backing).
645
- */
646
- _joinContext(): JoinContext | undefined;
647
- /**
648
- * @internal — clone this Query with a declared-predicate map
649
- * attached. Used by the materialized-view registry to enable
650
- * `.wherePredicate(name, ctx?)` for the MV's query callback.
651
- * Consumers don't call this directly.
652
- */
653
- _withPredicates(predicates: ReadonlyMap<string, DeclaredPredicate>): Query<T>;
654
- /**
655
- * @internal — #308 L3. The ids of records matching this query's plan,
656
- * recovered by reference identity: `executePlanWithSource` returns the
657
- * ORIGINAL snapshot record references (money-decode and joins are applied
658
- * later, in `toArray`), so each matched record is found in the id-paired
659
- * `snapshotEntries()` map. Used by `collection.retrieve({ within })`.
660
- * Throws if the source is not collection-backed (no `snapshotEntries`).
661
- */
662
- _idArray(): string[];
663
- /**
664
- * Filter by a registered deterministic predicate. Requires
665
- * the Query to have been augmented with a predicates map (typically
666
- * via the materialized-view registry — bare Queries constructed
667
- * outside an MV throw on `.wherePredicate()`).
668
- *
669
- * `ctx` is an optional opaque value passed verbatim to the predicate
670
- * function. Both `predicateHash` (from the registration) and a
671
- * canonical-JSON hash of `ctx` fold into the MV's `queryHash`, so
672
- * either changing forces refresh on next visit.
673
- */
674
- wherePredicate(name: string, ctx?: unknown): Query<T>;
675
- /**
676
- * Add a field comparison. Multiple where() calls are AND-combined.
677
- *
678
- * A declared money field compares in MAJOR units (#336): the operand
679
- * (`10000`, `'10000.00'`, or `{ amount, currency }` in multi mode) is
680
- * quantized into stored scaled-int space at build time and evaluated
681
- * BigInt-exact per record. A malformed operand or a string operator
682
- * (`contains`/`startsWith`) throws here, at the call site.
683
- */
684
- where(field: string, op: Operator, value: unknown): Query<T>;
685
- /**
686
- * Logical OR group. Pass a callback that builds a sub-query.
687
- * Each clause inside the callback is OR-combined; the group itself
688
- * joins the parent plan with AND.
689
- */
690
- or(builder: (q: Query<T>) => Query<T>): Query<T>;
691
- /**
692
- * Logical AND group. Same shape as `or()` but every clause inside the group
693
- * must match. Useful for explicit grouping inside a larger OR.
694
- */
695
- and(builder: (q: Query<T>) => Query<T>): Query<T>;
696
- /** Escape hatch: add an arbitrary predicate function. Not serializable. */
697
- filter(fn: (record: T) => boolean): Query<T>;
698
- /**
699
- * Sort by a field. Subsequent calls are tie-breakers. Pass
700
- * `{ by: 'label' }` to sort a `dictKey`/`staticDict` field by its resolved
701
- * label at the query locale instead of the stored code (#285).
702
- */
703
- orderBy(field: string, direction?: 'asc' | 'desc', opts?: {
704
- by?: 'value' | 'label';
705
- }): Query<T>;
706
- /** Cap the result size. */
707
- limit(n: number): Query<T>;
708
- /** Skip the first N matching records (after ordering). */
709
- offset(n: number): Query<T>;
710
- /**
711
- * Resolve a `ref()`-declared foreign key and attach the right-side
712
- * record under `opts.as`. — eager, single-FK, intra-
713
- * vault joins.
714
- *
715
- * ```ts
716
- * const rows = invoices.query()
717
- * .where('status', '==', 'open')
718
- * .join('clientId', { as: 'client' })
719
- * .toArray()
720
- * // → [{ id, amount, client: { id, name, ... } }, ...]
721
- * ```
722
- *
723
- * Preconditions:
724
- * - The Query must have a `joinContext` (constructed via
725
- * `Collection.query()`, not `new Query`).
726
- * - `field` must have a matching `refs: { [field]: ref('<target>') }`
727
- * declaration on the left collection.
728
- * - The target collection must be reachable via the vault
729
- * (either currently open or openable on demand).
730
- *
731
- * Strategy:
732
- * - Nested-loop against `lookupById` when the target source
733
- * provides it (the common path for Collection targets).
734
- * - Hash join otherwise, or when `{ strategy: 'hash' }` is
735
- * explicitly passed for test purposes.
736
- *
737
- * Ref-mode semantics on dangling refs (left record has a non-null
738
- * FK value pointing at a right-side id that doesn't exist):
739
- * - `strict` → throws `DanglingReferenceError` with the full
740
- * field / target / refId context.
741
- * - `warn` → attaches `null` and emits a one-shot warning per
742
- * unique dangling pair.
743
- * - `cascade` → attaches `null` silently. Cascade is a
744
- * delete-time mode; dangling refs visible at read time are
745
- * either mid-flight cascades or pre-existing orphans, not a
746
- * DSL-level error.
747
- *
748
- * A left-side record whose FK field is `null` / `undefined` is NOT
749
- * a dangling ref — it's "no reference at all", always allowed
750
- * regardless of mode.
751
- *
752
- * The return type widens `T` with `Record<As, R | null>`. The `R`
753
- * parameter is optional — supply it explicitly for type-checked
754
- * access to the joined fields:
755
- *
756
- * ```ts
757
- * invoices.query().join<'client', Client>('clientId', { as: 'client' })
758
- * // ^^^^^^^^^^^^^^^^^^^ alias literal + right-side type
759
- * ```
760
- *
761
- * Without the generic, the joined field is typed as `unknown`, which
762
- * still works but requires a cast to access its properties.
763
- *
764
- * Joins stay intra-vault by construction — cross-vault
765
- * correlation goes through `Noydb.queryAcross`, not
766
- * `.join()`.
767
- */
768
- join<As extends string, R = unknown>(field: string, opts: {
769
- as: As;
770
- strategy?: JoinStrategy;
771
- maxRows?: number;
772
- }): Query<T & Record<As, R | null>>;
773
- /**
774
- * Cartesian-product cross-join against `target` collection. Each result row
775
- * carries the original `T` fields plus `result[as]` populated from every
776
- * right-side row (or the filtered subset when `on:` is supplied).
777
- *
778
- * **Order matters:** `.where().crossJoin()` filters BEFORE expanding (cheaper);
779
- * `.crossJoin().where('alias.field', ...)` filters AFTER (required when the
780
- * where clause references the aliased fields).
781
- *
782
- * **Cost ceiling:** `CrossJoinTooLargeError` fires before allocation when
783
- * `leftRows × rightRows` (or the cumulative lateral count) exceeds the limit.
784
- * Default: 50,000 rows. Override per-clause with `{ maxRows: N }`.
785
- *
786
- * **`on:` shapes:**
787
- * - `on: (left) => TTarget[]` — subset form (most efficient)
788
- * - `on: (left) => (right) => boolean` — predicate form
789
- * - `on: { predicate: 'name' }` — MV-safe, hash-tracked form
790
- * (requires the Query to have been augmented via `_withPredicates`)
791
- *
792
- * Requires a JoinContext (constructed via `collection.query()`).
793
- */
794
- crossJoin<TTarget = unknown, As extends string = string>(target: string, opts: {
795
- as: As;
796
- on?: ((left: T) => unknown[] | ((right: TTarget) => boolean)) | {
797
- readonly predicate: string;
798
- };
799
- maxRows?: number;
800
- }): Query<T & {
801
- [K in As]: TTarget;
802
- }>;
803
- /**
804
- * Execute the plan and return the matching records. When the plan
805
- * carries any join legs, they are applied after `where` / `orderBy`
806
- * / `limit` / `offset` narrow the left set. See the `.join()` doc
807
- * for the ordering rationale.
808
- *
809
- * `opts.locale` (#285 §3) resolves JOINED right-side i18n fields at the
810
- * `join` layer to that locale; without it, the owning collection's default
811
- * locale applies, and a locale-less query leaves joined i18n fields raw.
812
- * (Left/base i18n fields are resolved by `get`/`list`, not here.)
813
- */
814
- toArray(opts?: {
815
- locale?: string;
816
- }): T[];
817
- /**
818
- * Decode this source's money fields on read (stored scaled-int → canonical
819
- * decimal), so `query().toArray()` agrees with `get()`/`sum()` on the value.
820
- * No-op when the source declares no money fields.
821
- *
822
- * The query layer carries no locale context, so we decode with `'raw'` —
823
- * canonical decimal, WITHOUT fabricating locale-formatted `<field>Formatted`
824
- * / `<field>Number` virtuals. Producing a guessed-locale string here would
825
- * just reintroduce #322's "two read paths disagree" failure on the virtual
826
- * field (e.g. it-IT via `get()` vs en-US here). Consumers who need formatted
827
- * money read through `get()`/`list()` with a locale.
828
- */
829
- private decodeMoney;
830
- /** Return the first matching record, or null. Joins are applied. `opts.locale` resolves joined i18n fields (#285 §3). */
831
- first(opts?: {
832
- locale?: string;
833
- }): T | null;
834
- /**
835
- * Return the number of matching records (after where/filter,
836
- * before limit). **Joins are NOT applied** — count() reports the
837
- * left-side cardinality, because joins in are projection-only
838
- * (they attach an aliased field; they never filter). Running joins
839
- * here just to discard the aliases would be wasteful, and in strict
840
- * mode it could throw `DanglingReferenceError` for a call whose
841
- * intent is purely to count.
842
- */
843
- count(): number;
844
- /**
845
- * Reduce the matching records through a named set of reducers.
846
- * the aggregation terminal.
847
- *
848
- * ```ts
849
- * const { total, n, avgAmount } = invoices.query()
850
- * .where('status', '==', 'open')
851
- * .aggregate({
852
- * total: sum('amount'),
853
- * n: count(),
854
- * avgAmount: avg('amount'),
855
- * })
856
- * .run()
857
- * ```
858
- *
859
- * Returns an `Aggregation<R>` wrapper with two terminals:
860
- * - `.run(): R` — synchronous one-shot reduction
861
- * - `.live(): LiveAggregation<R>` — reactive primitive that
862
- * re-runs the reduction whenever the source notifies of a
863
- * change. Always call `live.stop()` when finished.
864
- *
865
- * The reducer spec is bound here once and reused by both
866
- * terminals — this is why `.aggregate()` returns a wrapper instead
867
- * of being a direct terminal. Consumers who only need the static
868
- * value read `.run()`; consumers wiring a reactive UI read
869
- * `.live()`.
870
- *
871
- * Joins are intentionally NOT applied to aggregations in —
872
- * the same logic as `.count()`. Joins in are projection-only
873
- * (they attach an aliased field and never filter), so running
874
- * them just to throw the aliases away would be wasteful. If you
875
- * need a reducer that reads a joined field, open an issue —
876
- * aggregations-across-joins is explicitly out of scope for v1.
877
- *
878
- * Every reducer factory accepts an optional `{ seed }` parameter
879
- * that is plumbed through the protocol but unused by the
880
- * executor — that's constraint #2. When partition-aware
881
- * aggregation lands, the seed will carry running state across
882
- * partition boundaries without an API break.
883
- */
884
- aggregate<Spec extends AggregateSpec>(spec: Spec): Aggregation<AggregateResult<Spec>>;
885
- /**
886
- * Partition matching records into buckets keyed by a field, then
887
- * terminate with `.aggregate(spec)` to compute per-bucket
888
- * reducers..
889
- *
890
- * ```ts
891
- * const byClient = invoices.query()
892
- * .where('status', '==', 'open')
893
- * .groupBy('clientId')
894
- * .aggregate({ total: sum('amount'), n: count() })
895
- * .run()
896
- * // → [ { clientId: 'c1', total: 5250, n: 3 }, … ]
897
- * ```
898
- *
899
- * Result rows carry the group key value under the grouping field
900
- * name plus every reducer output from the spec. Buckets are
901
- * emitted in first-seen order — consumers who want a specific
902
- * ordering should `.sort()` downstream.
903
- *
904
- * **Cardinality caps:** a one-shot warning fires at 10_000
905
- * distinct groups; `GroupCardinalityError` throws at 100_000.
906
- * Grouping on a high-uniqueness field like `id` or `createdAt` is
907
- * almost always a query mistake — the error message names the
908
- * field and observed cardinality and suggests narrowing with
909
- * `.where()` first.
910
- *
911
- * **Null / undefined keys:** records with a missing or explicitly
912
- * `null` group field get their own buckets. `Map`-based
913
- * partitioning distinguishes `undefined` from `null`, so the two
914
- * cases do NOT merge. Consumers who want them merged should
915
- * coalesce upstream with `.filter()`.
916
- *
917
- * **Joins are not applied** — same rationale as `.count()` and
918
- * `.aggregate()`. Joined fields in are projection-only, so
919
- * running a join inside a grouping pipeline would be wasteful and
920
- * could trigger `DanglingReferenceError` in strict mode for a
921
- * call whose intent is purely to bucket-and-reduce. Grouping by
922
- * a joined field is explicitly out of scope for — file an
923
- * issue if a real consumer needs it.
924
- *
925
- * **Filter clauses (`.filter(fn)`):** grouped queries still
926
- * support filter clauses in the underlying plan — they run in
927
- * the same candidate/filter pipeline that `.aggregate()` uses.
928
- * The performance caveat is the same: filter clauses cost O(N)
929
- * per record and can't be index-accelerated.
930
- */
931
- groupBy<F extends string>(field: F): GroupedQuery<T, F>;
932
- groupBy<F extends readonly [string, string, ...string[]]>(...fields: F): GroupedQueryN<T, F>;
933
- /**
934
- * Re-run the query whenever the source notifies of changes.
935
- * Returns an unsubscribe function. The callback receives the latest result.
936
- * Throws if the source does not support subscriptions.
937
- *
938
- * **For joined queries, prefer `.live()`** — `subscribe()`
939
- * only re-fires on LEFT-side changes, so joined data can be
940
- * stale if the right side mutates between emissions. `.live()`
941
- * merges change streams from every join target.
942
- */
943
- subscribe(cb: (result: T[]) => void): () => void;
944
- /**
945
- * Reactive terminal — returns a `LiveQuery<T>` that re-runs the
946
- * query and updates its `value` whenever any source feeding it
947
- * mutates..
948
- *
949
- * For non-joined queries, `.live()` is a convenience over the
950
- * existing `.subscribe()` callback shape: a hand-rolled reactive
951
- * primitive with `value` / `error` fields and a `subscribe(cb)`
952
- * notification channel. Frame-agnostic — Vue / React / Solid
953
- * adapters wrap it in their own primitive.
954
- *
955
- * For joined queries, `.live()` additionally subscribes to every
956
- * join target's change stream. Mutations on a right-side
957
- * collection (insert / update / delete of a client referenced by
958
- * an invoice) re-fire the live query and re-evaluate every
959
- * dependent left row. Right-side targets are deduped by
960
- * collection name, so a chain that joins the same target twice
961
- * (e.g. billing client + shipping client → both 'clients') only
962
- * subscribes once.
963
- *
964
- * **Ref-mode behavior on right-side disappearance** — matches the
965
- * eager `.toArray()` contract from :
966
- * - `strict` → re-run throws `DanglingReferenceError`. The
967
- * LiveQuery catches the throw, stores it in `live.error`, and
968
- * notifies listeners (the throw does NOT propagate out of
969
- * the source's change handler — that would tear down the
970
- * emitter). Consumers check `live.error` after each
971
- * notification and render an error state in the UI.
972
- * - `warn` → joined value flips to `null`; the existing
973
- * warn-channel deduplication keeps repeated re-runs from
974
- * spamming the console.
975
- * - `cascade` → no special handling needed; the cascade-
976
- * delete mechanism propagates the right-side delete into the
977
- * left collection on the next tick, and the live query
978
- * naturally re-fires with the orphaned left rows gone.
979
- *
980
- * Always call `live.stop()` when finished — it tears down every
981
- * upstream subscription. The Vue layer's `onUnmounted` hook
982
- * should call `stop()` automatically; raw consumers must do it
983
- * themselves.
984
- *
985
- * **Limitations:**
986
- * - No granular delta updates — the whole query re-runs on
987
- * every change.
988
- * - No microtask batching — bursty changes produce one re-run
989
- * per change.
990
- * - No re-planning under live mutations — the planner picks
991
- * once at subscription time and reuses the same plan.
992
- * - Streaming live joins are deferred.
993
- */
994
- live(): LiveQuery<T>;
995
- /**
996
- * Return the plan as a JSON-friendly object. FilterClause entries are
997
- * stripped (their `fn` cannot be serialized) and replaced with
998
- * { type: 'filter', fn: '[function]' } so devtools can still see them.
999
- */
1000
- toPlan(): unknown;
1001
- }
1002
- /**
1003
- * Execute a plan against a snapshot of records.
1004
- * Pure function — same input, same output, no side effects.
1005
- *
1006
- * Records are typed as `unknown` because plans are non-parametric; callers
1007
- * cast the return type at the API surface (see `Query.toArray()`).
1008
- */
1009
- declare function executePlan(records: readonly unknown[], plan: QueryPlan): unknown[];
1010
-
1011
- /**
1012
- * Streaming scan builder with filter + aggregate support.
1013
- *
1014
- * `Collection.scan()` now returns a `ScanBuilder<T>` that
1015
- * implements `AsyncIterable<T>` (for existing `for await … of`
1016
- * consumers) AND exposes chainable `.where()` / `.filter()` clauses
1017
- * plus a `.aggregate(spec)` async terminal that reduces the scan
1018
- * stream through the same reducer protocol as `Query.aggregate()`
1019
- *.
1020
- *
1021
- * **Memory model:** O(reducers), not O(records). The aggregate
1022
- * terminal initializes one state per reducer, iterates through the
1023
- * scan one record at a time via `for await`, applies every reducer's
1024
- * `step` per record, and never collects the stream into an array.
1025
- * This is what makes `scan().aggregate()` suitable for collections
1026
- * that don't fit in memory — the bound is a code-level invariant
1027
- * visible in the function body, not a runtime assertion.
1028
- *
1029
- * **Paginated iteration:** the builder holds a `pageProvider`
1030
- * closure that maps `(cursor, limit) → Promise<page>`, plumbed by
1031
- * `Collection.scan()` to `collection.listPage(...)`. The page
1032
- * iterator walks cursors forward until exhaustion, same as the
1033
- * previous async-generator `scan()` did.
1034
- *
1035
- * **Backward compatibility:** existing `for await (const rec of
1036
- * collection.scan()) { … }` code continues to work because
1037
- * `ScanBuilder` implements `[Symbol.asyncIterator]`. The previous
1038
- * signature returned an `AsyncIterableIterator<T>` (which has both
1039
- * `[Symbol.asyncIterator]` and `.next()`). We verified at grep time
1040
- * that no call sites use `.next()` on the scan result directly, so
1041
- * the narrowed interface is safe.
1042
- *
1043
- * **Immutability:** each `.where()` / `.filter()` call returns a
1044
- * fresh builder sharing the same page provider and page size. This
1045
- * lets a base scan be reused for multiple parallel aggregations:
1046
- *
1047
- * ```ts
1048
- * const scan = invoices.scan()
1049
- * const [open, paid] = await Promise.all([
1050
- * scan.where('status', '==', 'open').aggregate({ n: count() }),
1051
- * scan.where('status', '==', 'paid').aggregate({ n: count() }),
1052
- * ])
1053
- * ```
1054
- *
1055
- * Note that each aggregation pays a full scan — there's no shared
1056
- * iteration across the two. Multi-way aggregation in a single pass
1057
- * is out of scope; consumers who need it should build a compound spec
1058
- * and run a single `.aggregate({ openN, paidN })` at the DSL level.
1059
- *
1060
- * **Out of scope for (tracked separately):**
1061
- * - `scan().aggregate().live()` — unbounded scan + change-stream
1062
- * reconciliation is a design problem, not just a code one
1063
- * - `scan().groupBy().aggregate()` — high-cardinality grouping on
1064
- * huge collections would re-introduce the O(groups) memory
1065
- * problem that aggregate fixes
1066
- * - Parallel scan across pages — race-safe page cursor contracts
1067
- * are not in the adapter API yet
1068
- * - `scan().join(...)` — tracked under (streaming join)
1069
- */
1070
-
1071
- /**
1072
- * Page provider — the Collection-shaped hook the builder calls to
1073
- * walk cursors forward. Kept as a structural interface so tests can
1074
- * wire up a synthetic provider without pulling in the full
1075
- * Collection class. Collection's `listPage` matches this shape
1076
- * exactly.
1077
- */
1078
- interface ScanPageProvider<T> {
1079
- listPage(opts: {
1080
- cursor?: string;
1081
- limit?: number;
1082
- }): Promise<{
1083
- items: T[];
1084
- nextCursor: string | null;
1085
- }>;
1086
- }
1087
- /**
1088
- * Chainable streaming scan. Implements `AsyncIterable<T>` for
1089
- * drop-in use with `for await … of`; adds `.where()` / `.filter()`
1090
- * chainable clauses and a `.aggregate(spec)` async terminal.
1091
- *
1092
- * The builder is immutable per operation — each chained call
1093
- * returns a fresh `ScanBuilder` sharing the same page provider and
1094
- * page size. The original builder is never mutated, so it's safe
1095
- * to reuse across multiple parallel consumers.
1096
- */
1097
- declare class ScanBuilder<T> implements AsyncIterable<T> {
1098
- private readonly pageProvider;
1099
- private readonly pageSize;
1100
- private readonly clauses;
1101
- /**
1102
- * Zero-or-more join legs to apply per record as the stream flows.
1103
- * Each leg attaches the resolved right-side record (or null) under
1104
- * its alias. — streaming joins.
1105
- *
1106
- * Joins are evaluated AFTER clauses, so a `where()` filtered-out
1107
- * record never triggers a right-side lookup. This is the same
1108
- * ordering as `Query.toArray()` (clauses first, joins after) and
1109
- * keeps the streaming path from doing wasted work.
1110
- */
1111
- private readonly joins;
1112
- /**
1113
- * Join resolution context. Required for `.join()` to translate a
1114
- * field name into a target collection + ref mode and to resolve
1115
- * the right-side `JoinableSource`. Optional because tests
1116
- * construct ScanBuilder directly with synthetic page providers
1117
- * that don't know about ref() — calling `.join()` without a
1118
- * context throws with an actionable error.
1119
- */
1120
- private readonly joinContext;
1121
- /**
1122
- * Money field descriptors for the backing collection. When present, yielded
1123
- * records are decoded (stored scaled-int → canonical decimal) so `scan()`
1124
- * agrees with `get()`/`list()`/`query().toArray()` — #322. Decoded with
1125
- * `'raw'` (canonical decimal, no locale-formatted virtuals) since the scan
1126
- * stream carries no locale context, mirroring `Query.toArray()`.
1127
- */
1128
- private readonly moneyFields;
1129
- constructor(pageProvider: ScanPageProvider<T>, pageSize?: number, clauses?: readonly Clause[], joins?: readonly JoinLeg[], joinContext?: JoinContext, moneyFields?: Record<string, MoneyDescriptor>);
1130
- /**
1131
- * Decode this scan's money fields on a record (stored scaled-int → canonical
1132
- * decimal). No-op when no money fields are declared. See {@link moneyFields}.
1133
- */
1134
- private decodeMoney;
1135
- /**
1136
- * Add a field comparison. Runs per record as the scan stream
1137
- * flows through, so non-matching records are dropped before they
1138
- * reach `.aggregate()` or the iteration consumer. Multiple
1139
- * `.where()` calls are AND-combined — same semantics as
1140
- * `Query.where()`.
1141
- *
1142
- * Clauses cannot use the secondary-index fast path here because
1143
- * the scan sources records from the adapter's paginator, not from
1144
- * the in-memory cache where indexes live. Index-accelerated scans
1145
- * are a future optimization — the current implementation
1146
- * evaluates clauses per record in O(1) per clause.
1147
- */
1148
- where(field: string, op: Operator, value: unknown): ScanBuilder<T>;
1149
- /**
1150
- * Escape hatch: add an arbitrary predicate function. Same
1151
- * non-serializable caveat as `Query.filter()` — filter clauses
1152
- * don't round-trip through `toPlan()`. Prefer `.where()` when
1153
- * possible.
1154
- */
1155
- filter(fn: (record: T) => boolean): ScanBuilder<T>;
1156
- /**
1157
- * Resolve a `ref()`-declared foreign key per record as the scan
1158
- * stream flows, attaching the right-side record (or null) under
1159
- * `opts.as`. — streaming joins over `scan()`.
1160
- *
1161
- * ```ts
1162
- * for await (const inv of invoices.scan().join('clientId', { as: 'client' })) {
1163
- * await processInvoice(inv) // inv.client is attached
1164
- * }
1165
- *
1166
- * // Or terminate with .aggregate() for streaming joined aggregation
1167
- * const { total } = await invoices.scan()
1168
- * .where('status', '==', 'open')
1169
- * .join('clientId', { as: 'client' })
1170
- * .aggregate({ total: sum('amount') })
1171
- * ```
1172
- *
1173
- * **The key difference from eager `.join()`:** the LEFT
1174
- * side streams page-by-page from the adapter and is never
1175
- * materialized. Memory ceiling on the left is O(pageSize), not
1176
- * O(rowCount). This is what makes streaming joins suitable for
1177
- * collections that exceed the eager join's 50_000-row ceiling.
1178
- *
1179
- * **Right-side strategy** is auto-selected per leg:
1180
- * - **Indexed** — right source exposes `lookupById`, so each
1181
- * left row costs O(1). This is the common path for
1182
- * Collection right sides, which back `lookupById` with a Map
1183
- * lookup over the in-memory cache. The right collection must
1184
- * be in eager mode (the same constraint as eager join's
1185
- * `querySourceForJoin` from ).
1186
- * - **Hash** — right source has only `snapshot()`. Build a
1187
- * `Map<id, record>` once at iteration start, probe per left
1188
- * row. Same correctness, same per-row cost as the indexed
1189
- * path; the difference is the upfront cost of materializing
1190
- * the right side once.
1191
- *
1192
- * Both strategies hold the right side in memory for the duration
1193
- * of the iteration. The "streaming" property applies to the LEFT
1194
- * side only — true left-and-right streaming joins (where neither
1195
- * side fits in memory) require a sort-merge join planner that's
1196
- * out of scope for.
1197
- *
1198
- * **Ref-mode semantics** match eager `.join()` exactly:
1199
- * - `strict` → throws `DanglingReferenceError` mid-stream
1200
- * when a left record points at a non-existent right id.
1201
- * The throw aborts the async iterator — consumers should
1202
- * wrap the `for await` in try/catch if they want to recover.
1203
- * - `warn` → attaches `null` and emits a one-shot warning
1204
- * per unique dangling pair (deduped via the same warn
1205
- * channel as eager join).
1206
- * - `cascade` → attaches `null` silently. A delete-time mode;
1207
- * dangling refs at read time are mid-flight or pre-existing
1208
- * orphans, not a DSL error.
1209
- *
1210
- * Left records with null/undefined FK values attach `null`
1211
- * regardless of mode — same "no reference at all" policy as
1212
- * eager join and write-time `enforceRefsOnPut`.
1213
- *
1214
- * **Multi-FK chaining** is supported via repeated `.join()`
1215
- * calls: each leg resolves an independent ref. Each leg
1216
- * independently picks its right-side strategy and applies its
1217
- * own ref mode.
1218
- *
1219
- * **Joins are NOT applied** to a `.aggregate()` terminal that
1220
- * doesn't reference joined fields — wait, that's not quite
1221
- * right. The streaming path actually DOES apply joins before
1222
- * `.aggregate()` because the join attaches a field that the
1223
- * spec might reference. Unlike `Query.aggregate()` (which skips
1224
- * joins entirely as a projection-only short-circuit), the
1225
- * streaming aggregation can't know whether the spec touches a
1226
- * joined field, so it always applies joins. Consumers who want
1227
- * unjoined streaming aggregation should leave `.join()` off the
1228
- * chain — the chain is composable for a reason.
1229
- *
1230
- * constraint #1 — every JoinLeg carries `partitionScope:
1231
- * 'all'` plumbed through but never read by. Same seam as
1232
- * eager join.
1233
- */
1234
- join<As extends string, R = unknown>(field: string, opts: {
1235
- as: As;
1236
- }): ScanBuilder<T & Record<As, R | null>>;
1237
- /**
1238
- * Iterate the scan as an async iterable. Walks the page
1239
- * provider's cursors forward until exhaustion, applying every
1240
- * clause per record — only matching records are yielded.
1241
- *
1242
- * Backward-compatible with the previous async-generator `scan()`
1243
- * return type for `for await … of` consumers.
1244
- */
1245
- [Symbol.asyncIterator](): AsyncIterator<T>;
1246
- /**
1247
- * Per-leg right-side resolution state. Built once at iteration
1248
- * start and reused for every left record. Two strategies:
1249
- *
1250
- * - `lookupById`: present when the right source exposes the
1251
- * hook directly (typical Collection right side). Per-row
1252
- * cost is O(1).
1253
- * - `hashByPrimaryKey`: built from `snapshot()` when no
1254
- * lookupById. Per-row cost is O(1) after the upfront O(N)
1255
- * materialization. Same as eager join's hash strategy.
1256
- *
1257
- * `warnedKeys` is the per-leg dedup set for ref-mode 'warn'. We
1258
- * key on `field→target:refId` so the same dangling pair only
1259
- * warns once per iteration. The dedup is per-iteration, not
1260
- * per-process — a long-running scan that re-iterates would warn
1261
- * again, which is the desired behavior (the data may have
1262
- * changed between iterations).
1263
- */
1264
- private buildJoinResolvers;
1265
- /**
1266
- * Resolve a single join leg for one left record and return the
1267
- * left record with the joined field attached under
1268
- * `leg.as`. Pure function over `(left, resolver)`; never
1269
- * mutates the input.
1270
- *
1271
- * Ref-mode dispatch matches eager `applyJoins` from :
1272
- * - null/undefined FK → attach null silently (always allowed)
1273
- * - dangling FK + strict → throw `DanglingReferenceError`
1274
- * - dangling FK + warn → attach null, warn-once per pair
1275
- * - dangling FK + cascade → attach null silently
1276
- */
1277
- private applyOneJoinStreaming;
1278
- /**
1279
- * Reduce the scan stream through a named set of reducers and
1280
- * return the final aggregated shape.
1281
- *
1282
- * Memory is O(reducers): one mutable state slot per spec key.
1283
- * Records flow through the pipeline one at a time via
1284
- * `for await` and are discarded after their `step()` is applied
1285
- * — never collected into an array. This is the distinguishing
1286
- * property from `Query.aggregate()`, which materializes the full
1287
- * match set first.
1288
- *
1289
- * Reuses the same reducer protocol as `Query.aggregate()`,
1290
- * so `count()`, `sum(field)`, `avg(field)`, `min(field)`,
1291
- * `max(field)` all work unchanged. The `{ seed }` parameter
1292
- * plumbing from constraint #2 is honored transparently — the
1293
- * factories ignore it in and the scan executor never
1294
- * touches the per-reducer state construction.
1295
- *
1296
- * **Returns a Promise**, unlike `Query.aggregate().run()` which
1297
- * is synchronous. The scan is inherently async because it walks
1298
- * adapter pages, so the terminal has to be too. Consumers
1299
- * destructure with await:
1300
- *
1301
- * ```ts
1302
- * const { total, n } = await invoices.scan()
1303
- * .where('year', '==', 2025)
1304
- * .aggregate({ total: sum('amount'), n: count() })
1305
- * ```
1306
- *
1307
- * **No `.live()` in.** `scan().aggregate().live()` would
1308
- * require reconciling an unbounded streaming iteration with a
1309
- * change-stream subscription — a design problem, not just a code
1310
- * one. Consumers with huge collections and live needs should
1311
- * narrow with `.where()` enough to fit in the 50k `query()`
1312
- * limit and use `query().aggregate().live()` instead.
1313
- */
1314
- aggregate<Spec extends AggregateSpec>(spec: Spec): Promise<AggregateResult<Spec>>;
1315
- /**
1316
- * Evaluate the clause list against a single record. Linear in
1317
- * the clause count; short-circuits on first false. Clauses on a
1318
- * scan are always re-evaluated per record — no index-accelerated
1319
- * path, because the stream sources records from the adapter
1320
- * paginator, not from the in-memory cache where indexes live.
1321
- */
1322
- private recordMatches;
1323
- }
1324
-
1325
- export { DEFAULT_CROSS_JOIN_MAX_ROWS as D, type JoinContext as J, type LiveQuery as L, type OrderBy as O, Query as Q, type RefDescriptor as R, ScanBuilder as S, DEFAULT_JOIN_MAX_ROWS as a, type JoinLeg as b, type JoinStrategy as c, type JoinableSource as d, type LiveUpstream as e, type QueryPlan as f, type QuerySource as g, RefIntegrityError as h, type RefMode as i, RefRegistry as j, RefScopeError as k, type RefViolation as l, type ScanPageProvider as m, applyJoins as n, buildLiveQuery as o, executePlan as p, isRefArray as q, ref as r, refArray as s, resetJoinWarnings as t };