@tanstack/db 0.5.31 → 0.5.33

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 (69) hide show
  1. package/dist/cjs/collection/subscription.cjs +6 -6
  2. package/dist/cjs/collection/subscription.cjs.map +1 -1
  3. package/dist/cjs/errors.cjs +8 -0
  4. package/dist/cjs/errors.cjs.map +1 -1
  5. package/dist/cjs/errors.d.cts +3 -0
  6. package/dist/cjs/index.cjs +5 -0
  7. package/dist/cjs/index.cjs.map +1 -1
  8. package/dist/cjs/index.d.cts +1 -0
  9. package/dist/cjs/query/builder/types.d.cts +28 -31
  10. package/dist/cjs/query/compiler/index.cjs +3 -0
  11. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  12. package/dist/cjs/query/effect.cjs +602 -0
  13. package/dist/cjs/query/effect.cjs.map +1 -0
  14. package/dist/cjs/query/effect.d.cts +94 -0
  15. package/dist/cjs/query/index.d.cts +1 -0
  16. package/dist/cjs/query/live/collection-config-builder.cjs +5 -74
  17. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  18. package/dist/cjs/query/live/collection-subscriber.cjs +33 -100
  19. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  20. package/dist/cjs/query/live/collection-subscriber.d.cts +0 -1
  21. package/dist/cjs/query/live/utils.cjs +179 -0
  22. package/dist/cjs/query/live/utils.cjs.map +1 -0
  23. package/dist/cjs/query/live/utils.d.cts +109 -0
  24. package/dist/cjs/query/query-once.cjs +28 -0
  25. package/dist/cjs/query/query-once.cjs.map +1 -0
  26. package/dist/cjs/query/query-once.d.cts +57 -0
  27. package/dist/cjs/query/subset-dedupe.cjs +8 -7
  28. package/dist/cjs/query/subset-dedupe.cjs.map +1 -1
  29. package/dist/esm/collection/subscription.js +6 -6
  30. package/dist/esm/collection/subscription.js.map +1 -1
  31. package/dist/esm/errors.d.ts +3 -0
  32. package/dist/esm/errors.js +8 -0
  33. package/dist/esm/errors.js.map +1 -1
  34. package/dist/esm/index.d.ts +1 -0
  35. package/dist/esm/index.js +6 -1
  36. package/dist/esm/index.js.map +1 -1
  37. package/dist/esm/query/builder/types.d.ts +28 -31
  38. package/dist/esm/query/compiler/index.js +4 -1
  39. package/dist/esm/query/compiler/index.js.map +1 -1
  40. package/dist/esm/query/effect.d.ts +94 -0
  41. package/dist/esm/query/effect.js +602 -0
  42. package/dist/esm/query/effect.js.map +1 -0
  43. package/dist/esm/query/index.d.ts +1 -0
  44. package/dist/esm/query/live/collection-config-builder.js +1 -70
  45. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  46. package/dist/esm/query/live/collection-subscriber.d.ts +0 -1
  47. package/dist/esm/query/live/collection-subscriber.js +31 -98
  48. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  49. package/dist/esm/query/live/utils.d.ts +109 -0
  50. package/dist/esm/query/live/utils.js +179 -0
  51. package/dist/esm/query/live/utils.js.map +1 -0
  52. package/dist/esm/query/query-once.d.ts +57 -0
  53. package/dist/esm/query/query-once.js +28 -0
  54. package/dist/esm/query/query-once.js.map +1 -0
  55. package/dist/esm/query/subset-dedupe.js +8 -7
  56. package/dist/esm/query/subset-dedupe.js.map +1 -1
  57. package/package.json +1 -1
  58. package/src/collection/subscription.ts +6 -6
  59. package/src/errors.ts +11 -0
  60. package/src/index.ts +11 -0
  61. package/src/query/builder/types.ts +64 -50
  62. package/src/query/compiler/index.ts +5 -0
  63. package/src/query/effect.ts +1119 -0
  64. package/src/query/index.ts +3 -0
  65. package/src/query/live/collection-config-builder.ts +6 -132
  66. package/src/query/live/collection-subscriber.ts +40 -156
  67. package/src/query/live/utils.ts +356 -0
  68. package/src/query/query-once.ts +115 -0
  69. package/src/query/subset-dedupe.ts +14 -15
package/dist/esm/index.js CHANGED
@@ -6,13 +6,14 @@ import { createArrayChangeProxy, createChangeProxy, withArrayChangeTracking, wit
6
6
  import { createOptimisticAction } from "./optimistic-action.js";
7
7
  import { localOnlyCollectionOptions } from "./local-only.js";
8
8
  import { localStorageCollectionOptions } from "./local-storage.js";
9
- import { AggregateFunctionNotInSelectError, AggregateNotSupportedError, CannotCombineEmptyExpressionListError, CollectionConfigurationError, CollectionInErrorStateError, CollectionInputNotFoundError, CollectionIsInErrorStateError, CollectionOperationError, CollectionRequiresConfigError, CollectionRequiresSyncConfigError, CollectionStateError, DeleteKeyNotFoundError, DistinctRequiresSelectError, DuplicateAliasInSubqueryError, DuplicateDbInstanceError, DuplicateKeyError, DuplicateKeySyncError, EmptyReferencePathError, GroupByError, HavingRequiresGroupByError, InvalidCollectionStatusTransitionError, InvalidJoinCondition, InvalidJoinConditionLeftSourceError, InvalidJoinConditionRightSourceError, InvalidJoinConditionSameSourceError, InvalidJoinConditionSourceMismatchError, InvalidKeyError, InvalidSchemaError, InvalidSourceError, InvalidSourceTypeError, InvalidStorageDataFormatError, InvalidStorageObjectFormatError, InvalidWhereExpressionError, JoinCollectionNotFoundError, JoinConditionMustBeEqualityError, JoinError, KeyUpdateNotAllowedError, LimitOffsetRequireOrderByError, LocalStorageCollectionError, MissingAliasInputsError, MissingDeleteHandlerError, MissingHandlerError, MissingInsertHandlerError, MissingMutationFunctionError, MissingUpdateArgumentError, MissingUpdateHandlerError, NegativeActiveSubscribersError, NoKeysPassedToDeleteError, NoKeysPassedToUpdateError, NoPendingSyncTransactionCommitError, NoPendingSyncTransactionWriteError, NonAggregateExpressionNotInGroupByError, NonRetriableError, OnMutateMustBeSynchronousError, OnlyOneSourceAllowedError, QueryBuilderError, QueryCompilationError, QueryMustHaveFromClauseError, QueryOptimizerError, SchemaMustBeSynchronousError, SchemaValidationError, SerializationError, SetWindowRequiresOrderByError, StorageError, StorageKeyRequiredError, SubQueryMustHaveFromClauseError, SubscriptionNotFoundError, SyncCleanupError, SyncTransactionAlreadyCommittedError, SyncTransactionAlreadyCommittedWriteError, TanStackDBError, TransactionAlreadyCompletedRollbackError, TransactionError, TransactionNotPendingCommitError, TransactionNotPendingMutateError, UndefinedKeyError, UnknownExpressionTypeError, UnknownFunctionError, UnknownHavingExpressionTypeError, UnsupportedAggregateFunctionError, UnsupportedFromTypeError, UnsupportedJoinSourceTypeError, UnsupportedJoinTypeError, UpdateKeyNotFoundError, WhereClauseConversionError } from "./errors.js";
9
+ import { AggregateFunctionNotInSelectError, AggregateNotSupportedError, CannotCombineEmptyExpressionListError, CollectionConfigurationError, CollectionInErrorStateError, CollectionInputNotFoundError, CollectionIsInErrorStateError, CollectionOperationError, CollectionRequiresConfigError, CollectionRequiresSyncConfigError, CollectionStateError, DeleteKeyNotFoundError, DistinctRequiresSelectError, DuplicateAliasInSubqueryError, DuplicateDbInstanceError, DuplicateKeyError, DuplicateKeySyncError, EmptyReferencePathError, FnSelectWithGroupByError, GroupByError, HavingRequiresGroupByError, InvalidCollectionStatusTransitionError, InvalidJoinCondition, InvalidJoinConditionLeftSourceError, InvalidJoinConditionRightSourceError, InvalidJoinConditionSameSourceError, InvalidJoinConditionSourceMismatchError, InvalidKeyError, InvalidSchemaError, InvalidSourceError, InvalidSourceTypeError, InvalidStorageDataFormatError, InvalidStorageObjectFormatError, InvalidWhereExpressionError, JoinCollectionNotFoundError, JoinConditionMustBeEqualityError, JoinError, KeyUpdateNotAllowedError, LimitOffsetRequireOrderByError, LocalStorageCollectionError, MissingAliasInputsError, MissingDeleteHandlerError, MissingHandlerError, MissingInsertHandlerError, MissingMutationFunctionError, MissingUpdateArgumentError, MissingUpdateHandlerError, NegativeActiveSubscribersError, NoKeysPassedToDeleteError, NoKeysPassedToUpdateError, NoPendingSyncTransactionCommitError, NoPendingSyncTransactionWriteError, NonAggregateExpressionNotInGroupByError, NonRetriableError, OnMutateMustBeSynchronousError, OnlyOneSourceAllowedError, QueryBuilderError, QueryCompilationError, QueryMustHaveFromClauseError, QueryOptimizerError, SchemaMustBeSynchronousError, SchemaValidationError, SerializationError, SetWindowRequiresOrderByError, StorageError, StorageKeyRequiredError, SubQueryMustHaveFromClauseError, SubscriptionNotFoundError, SyncCleanupError, SyncTransactionAlreadyCommittedError, SyncTransactionAlreadyCommittedWriteError, TanStackDBError, TransactionAlreadyCompletedRollbackError, TransactionError, TransactionNotPendingCommitError, TransactionNotPendingMutateError, UndefinedKeyError, UnknownExpressionTypeError, UnknownFunctionError, UnknownHavingExpressionTypeError, UnsupportedAggregateFunctionError, UnsupportedFromTypeError, UnsupportedJoinSourceTypeError, UnsupportedJoinTypeError, UpdateKeyNotFoundError, WhereClauseConversionError } from "./errors.js";
10
10
  import { deepEquals } from "./utils.js";
11
11
  import { createPacedMutations } from "./paced-mutations.js";
12
12
  import { BaseIndex, IndexOperation } from "./indexes/base-index.js";
13
13
  import { BTreeIndex } from "./indexes/btree-index.js";
14
14
  import { IndexProxy, LazyIndexWrapper } from "./indexes/lazy-index.js";
15
15
  import { extractFieldPath, extractSimpleComparisons, extractValue, parseLoadSubsetOptions, parseOrderByExpression, parseWhereExpression, walkExpression } from "./query/expression-helpers.js";
16
+ import { createEffect } from "./query/effect.js";
16
17
  import { add, and, avg, coalesce, concat, count, eq, gt, gte, ilike, inArray, isNull, isUndefined, length, like, lower, lt, lte, max, min, not, operators, or, sum, upper } from "./query/builder/functions.js";
17
18
  import { BaseQueryBuilder, Query } from "./query/builder/index.js";
18
19
  import { DeduplicatedLoadSubset } from "./query/subset-dedupe.js";
@@ -20,6 +21,7 @@ import { compileQuery } from "./query/compiler/index.js";
20
21
  import { createLiveQueryCollection, liveQueryCollectionOptions } from "./query/live-query-collection.js";
21
22
  import { debounceStrategy } from "./strategies/debounceStrategy.js";
22
23
  import { isLimitSubset, isOffsetLimitSubset, isOrderBySubset, isPredicateSubset, isWhereSubset, minusWherePredicates, unionWherePredicates } from "./query/predicate-utils.js";
24
+ import { queryOnce } from "./query/query-once.js";
23
25
  import { queueStrategy } from "./strategies/queueStrategy.js";
24
26
  import { throttleStrategy } from "./strategies/throttleStrategy.js";
25
27
  export {
@@ -46,6 +48,7 @@ export {
46
48
  DuplicateKeyError,
47
49
  DuplicateKeySyncError,
48
50
  EmptyReferencePathError,
51
+ FnSelectWithGroupByError,
49
52
  GroupByError,
50
53
  HavingRequiresGroupByError,
51
54
  ir as IR,
@@ -129,6 +132,7 @@ export {
129
132
  createArrayChangeProxy,
130
133
  createChangeProxy,
131
134
  createCollection,
135
+ createEffect,
132
136
  createLiveQueryCollection,
133
137
  createOptimisticAction,
134
138
  createPacedMutations,
@@ -168,6 +172,7 @@ export {
168
172
  parseLoadSubsetOptions,
169
173
  parseOrderByExpression,
170
174
  parseWhereExpression,
175
+ queryOnce,
171
176
  queueStrategy,
172
177
  sum,
173
178
  throttleStrategy,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -181,7 +181,7 @@ export type SelectObject<T extends SelectShape = SelectShape> = T;
181
181
  * ```
182
182
  */
183
183
  export type ResultTypeFromSelect<TSelectObject> = WithoutRefBrand<Prettify<{
184
- [K in keyof TSelectObject]: NeedsExtraction<TSelectObject[K]> extends true ? ExtractExpressionType<TSelectObject[K]> : TSelectObject[K] extends Ref<infer _T> ? ExtractRef<TSelectObject[K]> : TSelectObject[K] extends RefLeaf<infer T> ? T : TSelectObject[K] extends RefLeaf<infer T> | undefined ? T | undefined : TSelectObject[K] extends RefLeaf<infer T> | null ? T | null : TSelectObject[K] extends Ref<infer _T> | undefined ? ExtractRef<TSelectObject[K]> | undefined : TSelectObject[K] extends Ref<infer _T> | null ? ExtractRef<TSelectObject[K]> | null : TSelectObject[K] extends Aggregate<infer T> ? T : TSelectObject[K] extends string | number | boolean | null | undefined ? TSelectObject[K] : TSelectObject[K] extends Record<string, any> ? ResultTypeFromSelect<TSelectObject[K]> : never;
184
+ [K in keyof TSelectObject]: NeedsExtraction<TSelectObject[K]> extends true ? ExtractExpressionType<TSelectObject[K]> : TSelectObject[K] extends Ref<infer _T> ? ExtractRef<TSelectObject[K]> : TSelectObject[K] extends RefLeaf<infer T> ? IsNullableRef<TSelectObject[K]> extends true ? T | undefined : T : TSelectObject[K] extends RefLeaf<infer T> | undefined ? T | undefined : TSelectObject[K] extends RefLeaf<infer T> | null ? IsNullableRef<Exclude<TSelectObject[K], null>> extends true ? T | null | undefined : T | null : TSelectObject[K] extends Ref<infer _T> | undefined ? ExtractRef<Exclude<TSelectObject[K], undefined>> | undefined : TSelectObject[K] extends Ref<infer _T> | null ? ExtractRef<Exclude<TSelectObject[K], null>> | null : TSelectObject[K] extends Aggregate<infer T> ? T : TSelectObject[K] extends string | number | boolean | null | undefined ? TSelectObject[K] : TSelectObject[K] extends Record<string, any> ? ResultTypeFromSelect<TSelectObject[K]> : never;
185
185
  }>>;
186
186
  type ExtractRef<T> = Prettify<ResultTypeFromSelect<WithoutRefBrand<T>>>;
187
187
  type ExtractExpressionType<T> = T extends PropRef<infer U> ? U : T extends Value<infer U> ? U : T extends Func<infer U> ? U : T extends Aggregate<infer U> ? U : T extends BasicExpression<infer U> ? U : T;
@@ -260,33 +260,23 @@ export type FunctionalHavingRow<TContext extends Context> = TContext[`schema`] &
260
260
  $selected: TContext[`result`];
261
261
  } : {});
262
262
  /**
263
- * RefProxyForContext - Creates ref proxies for all tables/collections in a query context
263
+ * RefsForContext - Creates ref proxies for all tables/collections in a query context
264
264
  *
265
265
  * This is the main entry point for creating ref objects in query builder callbacks.
266
- * It handles optionality by placing undefined/null OUTSIDE the RefProxy to enable
267
- * JavaScript's optional chaining operator (?.):
266
+ * For nullable join sides (left/right/full joins), it produces `Ref<T, true>` instead
267
+ * of `Ref<T> | undefined`. This accurately reflects that the proxy object is always
268
+ * present at build time (it's a truthy proxy that records property access paths),
269
+ * while the `Nullable` flag ensures the result type correctly includes `| undefined`.
268
270
  *
269
271
  * Examples:
270
- * - Required field: `RefProxy<User>` → user.name works
271
- * - Optional field: `RefProxy<User> | undefined` → user?.name works
272
- * - Nullable field: `RefProxy<User> | null` → user?.name works
273
- * - Both optional and nullable: `RefProxy<User> | undefined` → user?.name works
274
- *
275
- * The key insight is that `RefProxy<User | undefined>` would NOT allow `user?.name`
276
- * because the undefined is "inside" the proxy, but `RefProxy<User> | undefined`
277
- * does allow it because the undefined is "outside" the proxy.
278
- *
279
- * The logic prioritizes optional chaining by always placing `undefined` outside when
280
- * a type is both optional and nullable (e.g., `string | null | undefined`).
272
+ * - Required field: `Ref<User>` → user.name works, result is T
273
+ * - Nullable join side: `Ref<User, true>` → user.name works, result is T | undefined
281
274
  *
282
275
  * After `select()` is called, this type also includes `$selected` which provides access
283
276
  * to the SELECT result fields via `$selected.fieldName` syntax.
284
277
  */
285
278
  export type RefsForContext<TContext extends Context> = {
286
- [K in keyof TContext[`schema`]]: IsNonExactOptional<TContext[`schema`][K]> extends true ? IsNonExactNullable<TContext[`schema`][K]> extends true ? // T is both non-exact optional and non-exact nullable (e.g., string | null | undefined)
287
- Ref<NonNullable<TContext[`schema`][K]>> | undefined : // T is optional (T | undefined) but not exactly undefined, and not nullable
288
- Ref<NonUndefined<TContext[`schema`][K]>> | undefined : IsNonExactNullable<TContext[`schema`][K]> extends true ? // T is nullable (T | null) but not exactly null, and not optional
289
- Ref<NonNull<TContext[`schema`][K]>> | null : Ref<TContext[`schema`][K]>;
279
+ [K in keyof TContext[`schema`]]: IsNonExactOptional<TContext[`schema`][K]> extends true ? IsNonExactNullable<TContext[`schema`][K]> extends true ? Ref<NonNullable<TContext[`schema`][K]>, true> : Ref<NonUndefined<TContext[`schema`][K]>, true> : IsNonExactNullable<TContext[`schema`][K]> extends true ? Ref<NonNull<TContext[`schema`][K]>, true> : Ref<TContext[`schema`][K]>;
290
280
  } & (TContext[`result`] extends object ? {
291
281
  $selected: Ref<TContext[`result`]>;
292
282
  } : {});
@@ -331,23 +321,26 @@ type NonNull<T> = T extends null ? never : T;
331
321
  * It provides a recursive interface that allows nested property access while
332
322
  * preserving optionality and nullability correctly.
333
323
  *
334
- * When spread in select clauses, it correctly produces the underlying data type
335
- * without Ref wrappers, enabling clean spread operations.
324
+ * The `Nullable` parameter indicates whether this ref comes from a nullable
325
+ * join side (left/right/full). When `true`, the `Nullable` flag propagates
326
+ * through all nested property accesses, ensuring the result type includes
327
+ * `| undefined` for all fields accessed through this ref.
336
328
  *
337
329
  * Example usage:
338
330
  * ```typescript
339
- * // Clean interface - no internal properties visible
340
- * const users: Ref<{ id: number; profile?: { bio: string } }> = { ... }
341
- * users.id // Ref<number> - clean display
342
- * users.profile?.bio // Ref<string> - nested optional access works
331
+ * // Non-nullable ref (inner join or from table):
332
+ * select(({ user }) => ({ name: user.name })) // result: string
333
+ *
334
+ * // Nullable ref (left join right side):
335
+ * select(({ dept }) => ({ name: dept.name })) // result: string | undefined
343
336
  *
344
337
  * // Spread operations work cleanly:
345
338
  * select(({ user }) => ({ ...user })) // Returns User type, not Ref types
346
339
  * ```
347
340
  */
348
- export type Ref<T = any> = {
349
- [K in keyof T]: IsNonExactOptional<T[K]> extends true ? IsNonExactNullable<T[K]> extends true ? IsPlainObject<NonNullable<T[K]>> extends true ? Ref<NonNullable<T[K]>> | undefined : RefLeaf<NonNullable<T[K]>> | undefined : IsPlainObject<NonUndefined<T[K]>> extends true ? Ref<NonUndefined<T[K]>> | undefined : RefLeaf<NonUndefined<T[K]>> | undefined : IsNonExactNullable<T[K]> extends true ? IsPlainObject<NonNull<T[K]>> extends true ? Ref<NonNull<T[K]>> | null : RefLeaf<NonNull<T[K]>> | null : IsPlainObject<T[K]> extends true ? Ref<T[K]> : RefLeaf<T[K]>;
350
- } & RefLeaf<T>;
341
+ export type Ref<T = any, Nullable extends boolean = false> = {
342
+ [K in keyof T]: IsNonExactOptional<T[K]> extends true ? IsNonExactNullable<T[K]> extends true ? IsPlainObject<NonNullable<T[K]>> extends true ? Ref<NonNullable<T[K]>, Nullable> | undefined : RefLeaf<NonNullable<T[K]>, Nullable> | undefined : IsPlainObject<NonUndefined<T[K]>> extends true ? Ref<NonUndefined<T[K]>, Nullable> | undefined : RefLeaf<NonUndefined<T[K]>, Nullable> | undefined : IsNonExactNullable<T[K]> extends true ? IsPlainObject<NonNull<T[K]>> extends true ? Ref<NonNull<T[K]>, Nullable> | null : RefLeaf<NonNull<T[K]>, Nullable> | null : IsPlainObject<T[K]> extends true ? Ref<T[K], Nullable> : RefLeaf<T[K], Nullable>;
343
+ } & RefLeaf<T, Nullable>;
351
344
  /**
352
345
  * Ref - The user-facing ref type with clean IDE display
353
346
  *
@@ -360,10 +353,14 @@ export type Ref<T = any> = {
360
353
  * - No internal properties like __refProxy, __path, __type are visible
361
354
  */
362
355
  declare const RefBrand: unique symbol;
363
- export type RefLeaf<T = any> = {
356
+ declare const NullableBrand: unique symbol;
357
+ export type RefLeaf<T = any, Nullable extends boolean = false> = {
364
358
  readonly [RefBrand]?: T;
365
- };
366
- type WithoutRefBrand<T> = T extends Record<string, any> ? Omit<T, typeof RefBrand> : T;
359
+ } & ([Nullable] extends [true] ? {
360
+ readonly [NullableBrand]?: true;
361
+ } : {});
362
+ type IsNullableRef<T> = typeof NullableBrand extends keyof T ? true : false;
363
+ type WithoutRefBrand<T> = T extends Record<string, any> ? Omit<T, typeof RefBrand | typeof NullableBrand> : T;
367
364
  /**
368
365
  * PreserveSingleResultFlag - Conditionally includes the singleResult flag
369
366
  *
@@ -1,6 +1,6 @@
1
1
  import { map, filter, distinct } from "@tanstack/db-ivm";
2
2
  import { optimizeQuery } from "../optimizer.js";
3
- import { DistinctRequiresSelectError, HavingRequiresGroupByError, LimitOffsetRequireOrderByError, DuplicateAliasInSubqueryError, UnsupportedFromTypeError, CollectionInputNotFoundError } from "../../errors.js";
3
+ import { DistinctRequiresSelectError, FnSelectWithGroupByError, HavingRequiresGroupByError, LimitOffsetRequireOrderByError, DuplicateAliasInSubqueryError, UnsupportedFromTypeError, CollectionInputNotFoundError } from "../../errors.js";
4
4
  import { getWhereExpression, Value } from "../ir.js";
5
5
  import { compileExpression, toBooleanPredicate } from "./evaluators.js";
6
6
  import { processJoins } from "./joins.js";
@@ -92,6 +92,9 @@ function compileQuery(rawQuery, inputs, collections, subscriptions, callbacks, l
92
92
  if (query.distinct && !query.fnSelect && !query.select) {
93
93
  throw new DistinctRequiresSelectError();
94
94
  }
95
+ if (query.fnSelect && query.groupBy && query.groupBy.length > 0) {
96
+ throw new FnSelectWithGroupByError();
97
+ }
95
98
  if (query.fnSelect) {
96
99
  pipeline = pipeline.pipe(
97
100
  map(([key, namespacedRow]) => {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/query/compiler/index.ts"],"sourcesContent":["import { distinct, filter, map } from '@tanstack/db-ivm'\nimport { optimizeQuery } from '../optimizer.js'\nimport {\n CollectionInputNotFoundError,\n DistinctRequiresSelectError,\n DuplicateAliasInSubqueryError,\n HavingRequiresGroupByError,\n LimitOffsetRequireOrderByError,\n UnsupportedFromTypeError,\n} from '../../errors.js'\nimport { PropRef, Value as ValClass, getWhereExpression } from '../ir.js'\nimport { compileExpression, toBooleanPredicate } from './evaluators.js'\nimport { processJoins } from './joins.js'\nimport { containsAggregate, processGroupBy } from './group-by.js'\nimport { processOrderBy } from './order-by.js'\nimport { processSelect } from './select.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\nimport type { OrderByOptimizationInfo } from './order-by.js'\nimport type {\n BasicExpression,\n CollectionRef,\n QueryIR,\n QueryRef,\n} from '../ir.js'\nimport type { LazyCollectionCallbacks } from './joins.js'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n ResultStream,\n} from '../../types.js'\nimport type { QueryCache, QueryMapping, WindowOptions } from './types.js'\n\nexport type { WindowOptions } from './types.js'\n\n/**\n * Result of query compilation including both the pipeline and source-specific WHERE clauses\n */\nexport interface CompilationResult {\n /** The ID of the main collection */\n collectionId: string\n\n /** The compiled query pipeline (D2 stream) */\n pipeline: ResultStream\n\n /** Map of source aliases to their WHERE clauses for index optimization */\n sourceWhereClauses: Map<string, BasicExpression<boolean>>\n\n /**\n * Maps each source alias to its collection ID. Enables per-alias subscriptions for self-joins.\n * Example: `{ employee: 'employees-col-id', manager: 'employees-col-id' }`\n */\n aliasToCollectionId: Record<string, string>\n\n /**\n * Flattened mapping from outer alias to innermost alias for subqueries.\n * Always provides one-hop lookups, never recursive chains.\n *\n * Example: `{ activeUser: 'user' }` when `.from({ activeUser: subquery })`\n * where the subquery uses `.from({ user: collection })`.\n *\n * For deeply nested subqueries, the mapping goes directly to the innermost alias:\n * `{ author: 'user' }` (not `{ author: 'activeUser' }`), so `aliasRemapping[alias]`\n * always resolves in a single lookup.\n *\n * Used to resolve subscriptions during lazy loading when join aliases differ from\n * the inner aliases where collection subscriptions were created.\n */\n aliasRemapping: Record<string, string>\n}\n\n/**\n * Compiles a query IR into a D2 pipeline\n * @param rawQuery The query IR to compile\n * @param inputs Mapping of source aliases to input streams (e.g., `{ employee: input1, manager: input2 }`)\n * @param collections Mapping of collection IDs to Collection instances\n * @param subscriptions Mapping of source aliases to CollectionSubscription instances\n * @param callbacks Mapping of source aliases to lazy loading callbacks\n * @param lazySources Set of source aliases that should load data lazily\n * @param optimizableOrderByCollections Map of collection IDs to order-by optimization info\n * @param cache Optional cache for compiled subqueries (used internally for recursion)\n * @param queryMapping Optional mapping from optimized queries to original queries\n * @returns A CompilationResult with the pipeline, source WHERE clauses, and alias metadata\n */\nexport function compileQuery(\n rawQuery: QueryIR,\n inputs: Record<string, KeyedStream>,\n collections: Record<string, Collection<any, any, any, any, any>>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache = new WeakMap(),\n queryMapping: QueryMapping = new WeakMap(),\n): CompilationResult {\n // Check if the original raw query has already been compiled\n const cachedResult = cache.get(rawQuery)\n if (cachedResult) {\n return cachedResult\n }\n\n // Validate the raw query BEFORE optimization to check user's original structure.\n // This must happen before optimization because the optimizer may create internal\n // subqueries (e.g., for predicate pushdown) that reuse aliases, which is fine.\n validateQueryStructure(rawQuery)\n\n // Optimize the query before compilation\n const { optimizedQuery: query, sourceWhereClauses } = optimizeQuery(rawQuery)\n\n // Create mapping from optimized query to original for caching\n queryMapping.set(query, rawQuery)\n mapNestedQueries(query, rawQuery, queryMapping)\n\n // Create a copy of the inputs map to avoid modifying the original\n const allInputs = { ...inputs }\n\n // Track alias to collection id relationships discovered during compilation.\n // This includes all user-declared aliases plus inner aliases from subqueries.\n const aliasToCollectionId: Record<string, string> = {}\n\n // Track alias remapping for subqueries (outer alias → inner alias)\n // e.g., when .join({ activeUser: subquery }) where subquery uses .from({ user: collection })\n // we store: aliasRemapping['activeUser'] = 'user'\n const aliasRemapping: Record<string, string> = {}\n\n // Create a map of source aliases to input streams.\n // Inputs MUST be keyed by alias (e.g., `{ employee: input1, manager: input2 }`),\n // not by collection ID. This enables per-alias subscriptions where different aliases\n // of the same collection (e.g., self-joins) maintain independent filtered streams.\n const sources: Record<string, KeyedStream> = {}\n\n // Process the FROM clause to get the main source\n const {\n alias: mainSource,\n input: mainInput,\n collectionId: mainCollectionId,\n } = processFrom(\n query.from,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n aliasToCollectionId,\n aliasRemapping,\n sourceWhereClauses,\n )\n sources[mainSource] = mainInput\n\n // Prepare the initial pipeline with the main source wrapped in its alias\n let pipeline: NamespacedAndKeyedStream = mainInput.pipe(\n map(([key, row]) => {\n // Initialize the record with a nested structure\n const ret = [key, { [mainSource]: row }] as [\n string,\n Record<string, typeof row>,\n ]\n return ret\n }),\n )\n\n // Process JOIN clauses if they exist\n if (query.join && query.join.length > 0) {\n pipeline = processJoins(\n pipeline,\n query.join,\n sources,\n mainCollectionId,\n mainSource,\n allInputs,\n cache,\n queryMapping,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n rawQuery,\n compileQuery,\n aliasToCollectionId,\n aliasRemapping,\n sourceWhereClauses,\n )\n }\n\n // Process the WHERE clause if it exists\n if (query.where && query.where.length > 0) {\n // Apply each WHERE condition as a filter (they are ANDed together)\n for (const where of query.where) {\n const whereExpression = getWhereExpression(where)\n const compiledWhere = compileExpression(whereExpression)\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return toBooleanPredicate(compiledWhere(namespacedRow))\n }),\n )\n }\n }\n\n // Process functional WHERE clauses if they exist\n if (query.fnWhere && query.fnWhere.length > 0) {\n for (const fnWhere of query.fnWhere) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return toBooleanPredicate(fnWhere(namespacedRow))\n }),\n )\n }\n }\n\n if (query.distinct && !query.fnSelect && !query.select) {\n throw new DistinctRequiresSelectError()\n }\n\n // Process the SELECT clause early - always create $selected\n // This eliminates duplication and allows for DISTINCT implementation\n if (query.fnSelect) {\n // Handle functional select - apply the function to transform the row\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults = query.fnSelect!(namespacedRow)\n return [\n key,\n {\n ...namespacedRow,\n $selected: selectResults,\n },\n ] as [string, typeof namespacedRow & { $selected: any }]\n }),\n )\n } else if (query.select) {\n pipeline = processSelect(pipeline, query.select, allInputs)\n } else {\n // If no SELECT clause, create $selected with the main table data\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults =\n !query.join && !query.groupBy\n ? namespacedRow[mainSource]\n : namespacedRow\n\n return [\n key,\n {\n ...namespacedRow,\n $selected: selectResults,\n },\n ] as [string, typeof namespacedRow & { $selected: any }]\n }),\n )\n }\n\n // Process the GROUP BY clause if it exists\n if (query.groupBy && query.groupBy.length > 0) {\n pipeline = processGroupBy(\n pipeline,\n query.groupBy,\n query.having,\n query.select,\n query.fnHaving,\n )\n } else if (query.select) {\n // Check if SELECT contains aggregates but no GROUP BY (implicit single-group aggregation)\n const hasAggregates = Object.values(query.select).some(\n (expr) => expr.type === `agg` || containsAggregate(expr),\n )\n if (hasAggregates) {\n // Handle implicit single-group aggregation\n pipeline = processGroupBy(\n pipeline,\n [], // Empty group by means single group\n query.having,\n query.select,\n query.fnHaving,\n )\n }\n }\n\n // Process the HAVING clause if it exists (only applies after GROUP BY)\n if (query.having && (!query.groupBy || query.groupBy.length === 0)) {\n // Check if we have aggregates in SELECT that would trigger implicit grouping\n const hasAggregates = query.select\n ? Object.values(query.select).some((expr) => expr.type === `agg`)\n : false\n\n if (!hasAggregates) {\n throw new HavingRequiresGroupByError()\n }\n }\n\n // Process functional HAVING clauses outside of GROUP BY (treat as additional WHERE filters)\n if (\n query.fnHaving &&\n query.fnHaving.length > 0 &&\n (!query.groupBy || query.groupBy.length === 0)\n ) {\n // If there's no GROUP BY but there are fnHaving clauses, apply them as filters\n for (const fnHaving of query.fnHaving) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return fnHaving(namespacedRow)\n }),\n )\n }\n }\n\n // Process the DISTINCT clause if it exists\n if (query.distinct) {\n pipeline = pipeline.pipe(distinct(([_key, row]) => row.$selected))\n }\n\n // Process orderBy parameter if it exists\n if (query.orderBy && query.orderBy.length > 0) {\n const orderedPipeline = processOrderBy(\n rawQuery,\n pipeline,\n query.orderBy,\n query.select || {},\n collections[mainCollectionId]!,\n optimizableOrderByCollections,\n setWindowFn,\n query.limit,\n query.offset,\n )\n\n // Final step: extract the $selected and include orderBy index\n const resultPipeline = orderedPipeline.pipe(\n map(([key, [row, orderByIndex]]) => {\n // Extract the final results from $selected and include orderBy index\n const raw = (row as any).$selected\n const finalResults = unwrapValue(raw)\n return [key, [finalResults, orderByIndex]] as [unknown, [any, string]]\n }),\n )\n\n const result = resultPipeline\n // Cache the result before returning (use original query as key)\n const compilationResult = {\n collectionId: mainCollectionId,\n pipeline: result,\n sourceWhereClauses,\n aliasToCollectionId,\n aliasRemapping,\n }\n cache.set(rawQuery, compilationResult)\n\n return compilationResult\n } else if (query.limit !== undefined || query.offset !== undefined) {\n // If there's a limit or offset without orderBy, throw an error\n throw new LimitOffsetRequireOrderByError()\n }\n\n // Final step: extract the $selected and return tuple format (no orderBy)\n const resultPipeline: ResultStream = pipeline.pipe(\n map(([key, row]) => {\n // Extract the final results from $selected and return [key, [results, undefined]]\n const raw = (row as any).$selected\n const finalResults = unwrapValue(raw)\n return [key, [finalResults, undefined]] as [\n unknown,\n [any, string | undefined],\n ]\n }),\n )\n\n const result = resultPipeline\n // Cache the result before returning (use original query as key)\n const compilationResult = {\n collectionId: mainCollectionId,\n pipeline: result,\n sourceWhereClauses,\n aliasToCollectionId,\n aliasRemapping,\n }\n cache.set(rawQuery, compilationResult)\n\n return compilationResult\n}\n\n/**\n * Collects aliases used for DIRECT collection references (not subqueries).\n * Used to validate that subqueries don't reuse parent query collection aliases.\n * Only direct CollectionRef aliases matter - QueryRef aliases don't cause conflicts.\n */\nfunction collectDirectCollectionAliases(query: QueryIR): Set<string> {\n const aliases = new Set<string>()\n\n // Collect FROM alias only if it's a direct collection reference\n if (query.from.type === `collectionRef`) {\n aliases.add(query.from.alias)\n }\n\n // Collect JOIN aliases only for direct collection references\n if (query.join) {\n for (const joinClause of query.join) {\n if (joinClause.from.type === `collectionRef`) {\n aliases.add(joinClause.from.alias)\n }\n }\n }\n\n return aliases\n}\n\n/**\n * Validates the structure of a query and its subqueries.\n * Checks that subqueries don't reuse collection aliases from parent queries.\n * This must be called on the RAW query before optimization.\n */\nfunction validateQueryStructure(\n query: QueryIR,\n parentCollectionAliases: Set<string> = new Set(),\n): void {\n // Collect direct collection aliases from this query level\n const currentLevelAliases = collectDirectCollectionAliases(query)\n\n // Check if any current alias conflicts with parent aliases\n for (const alias of currentLevelAliases) {\n if (parentCollectionAliases.has(alias)) {\n throw new DuplicateAliasInSubqueryError(\n alias,\n Array.from(parentCollectionAliases),\n )\n }\n }\n\n // Combine parent and current aliases for checking nested subqueries\n const combinedAliases = new Set([\n ...parentCollectionAliases,\n ...currentLevelAliases,\n ])\n\n // Recursively validate FROM subquery\n if (query.from.type === `queryRef`) {\n validateQueryStructure(query.from.query, combinedAliases)\n }\n\n // Recursively validate JOIN subqueries\n if (query.join) {\n for (const joinClause of query.join) {\n if (joinClause.from.type === `queryRef`) {\n validateQueryStructure(joinClause.from.query, combinedAliases)\n }\n }\n }\n}\n\n/**\n * Processes the FROM clause, handling direct collection references and subqueries.\n * Populates `aliasToCollectionId` and `aliasRemapping` for per-alias subscription tracking.\n */\nfunction processFrom(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache,\n queryMapping: QueryMapping,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n sourceWhereClauses: Map<string, BasicExpression<boolean>>,\n): { alias: string; input: KeyedStream; collectionId: string } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.alias]\n if (!input) {\n throw new CollectionInputNotFoundError(\n from.alias,\n from.collection.id,\n Object.keys(allInputs),\n )\n }\n aliasToCollectionId[from.alias] = from.collection.id\n return { alias: from.alias, input, collectionId: from.collection.id }\n }\n case `queryRef`: {\n // Find the original query for caching purposes\n const originalQuery = queryMapping.get(from.query) || from.query\n\n // Recursively compile the sub-query with cache\n const subQueryResult = compileQuery(\n originalQuery,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n )\n\n // Pull up alias mappings from subquery to parent scope.\n // This includes both the innermost alias-to-collection mappings AND\n // any existing remappings from nested subquery levels.\n Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId)\n Object.assign(aliasRemapping, subQueryResult.aliasRemapping)\n\n // Pull up source WHERE clauses from subquery to parent scope.\n // This enables loadSubset to receive the correct where clauses for subquery collections.\n //\n // IMPORTANT: Skip pull-up for optimizer-created subqueries. These are detected when:\n // 1. The outer alias (from.alias) matches the inner alias (from.query.from.alias)\n // 2. The subquery was found in queryMapping (it's a user-defined subquery, not optimizer-created)\n //\n // For optimizer-created subqueries, the parent already has the sourceWhereClauses\n // extracted from the original raw query, so pulling up would be redundant.\n // More importantly, pulling up for optimizer-created subqueries can cause issues\n // when the optimizer has restructured the query.\n const isUserDefinedSubquery = queryMapping.has(from.query)\n const subqueryFromAlias = from.query.from.alias\n const isOptimizerCreated =\n !isUserDefinedSubquery && from.alias === subqueryFromAlias\n\n if (!isOptimizerCreated) {\n for (const [alias, whereClause] of subQueryResult.sourceWhereClauses) {\n sourceWhereClauses.set(alias, whereClause)\n }\n }\n\n // Create a FLATTENED remapping from outer alias to innermost alias.\n // For nested subqueries, this ensures one-hop lookups (not recursive chains).\n //\n // Example with 3-level nesting:\n // Inner: .from({ user: usersCollection })\n // Middle: .from({ activeUser: innerSubquery }) → creates: activeUser → user\n // Outer: .from({ author: middleSubquery }) → creates: author → user (not author → activeUser)\n //\n // The key insight: We search through the PULLED-UP aliasToCollectionId (which contains\n // the innermost 'user' alias), so we always map directly to the deepest level.\n // This means aliasRemapping[alias] is always a single lookup, never recursive.\n // Needed for subscription resolution during lazy loading.\n const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(\n (alias) =>\n subQueryResult.aliasToCollectionId[alias] ===\n subQueryResult.collectionId,\n )\n if (innerAlias && innerAlias !== from.alias) {\n aliasRemapping[from.alias] = innerAlias\n }\n\n // Extract the pipeline from the compilation result\n const subQueryInput = subQueryResult.pipeline\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n // Unwrap Value expressions that might have leaked through as the entire row\n const unwrapped = unwrapValue(value)\n return [key, unwrapped] as [unknown, any]\n }),\n )\n\n return {\n alias: from.alias,\n input: extractedInput,\n collectionId: subQueryResult.collectionId,\n }\n }\n default:\n throw new UnsupportedFromTypeError((from as any).type)\n }\n}\n\n// Helper to check if a value is a Value expression\nfunction isValue(raw: any): boolean {\n return (\n raw instanceof ValClass ||\n (raw && typeof raw === `object` && `type` in raw && raw.type === `val`)\n )\n}\n\n// Helper to unwrap a Value expression or return the value itself\nfunction unwrapValue(value: any): any {\n return isValue(value) ? value.value : value\n}\n\n/**\n * Recursively maps optimized subqueries to their original queries for proper caching.\n * This ensures that when we encounter the same QueryRef object in different contexts,\n * we can find the original query to check the cache.\n */\nfunction mapNestedQueries(\n optimizedQuery: QueryIR,\n originalQuery: QueryIR,\n queryMapping: QueryMapping,\n): void {\n // Map the FROM clause if it's a QueryRef\n if (\n optimizedQuery.from.type === `queryRef` &&\n originalQuery.from.type === `queryRef`\n ) {\n queryMapping.set(optimizedQuery.from.query, originalQuery.from.query)\n // Recursively map nested queries\n mapNestedQueries(\n optimizedQuery.from.query,\n originalQuery.from.query,\n queryMapping,\n )\n }\n\n // Map JOIN clauses if they exist\n if (optimizedQuery.join && originalQuery.join) {\n for (\n let i = 0;\n i < optimizedQuery.join.length && i < originalQuery.join.length;\n i++\n ) {\n const optimizedJoin = optimizedQuery.join[i]!\n const originalJoin = originalQuery.join[i]!\n\n if (\n optimizedJoin.from.type === `queryRef` &&\n originalJoin.from.type === `queryRef`\n ) {\n queryMapping.set(optimizedJoin.from.query, originalJoin.from.query)\n // Recursively map nested queries in joins\n mapNestedQueries(\n optimizedJoin.from.query,\n originalJoin.from.query,\n queryMapping,\n )\n }\n }\n }\n}\n\nfunction getRefFromAlias(\n query: QueryIR,\n alias: string,\n): CollectionRef | QueryRef | void {\n if (query.from.alias === alias) {\n return query.from\n }\n\n for (const join of query.join || []) {\n if (join.from.alias === alias) {\n return join.from\n }\n }\n}\n\n/**\n * Follows the given reference in a query\n * until its finds the root field the reference points to.\n * @returns The collection, its alias, and the path to the root field in this collection\n */\nexport function followRef(\n query: QueryIR,\n ref: PropRef<any>,\n collection: Collection,\n): { collection: Collection; path: Array<string> } | void {\n if (ref.path.length === 0) {\n return\n }\n\n if (ref.path.length === 1) {\n // This field should be part of this collection\n const field = ref.path[0]!\n // is it part of the select clause?\n if (query.select) {\n const selectedField = query.select[field]\n if (selectedField && selectedField.type === `ref`) {\n return followRef(query, selectedField, collection)\n }\n }\n\n // Either this field is not part of the select clause\n // and thus it must be part of the collection itself\n // or it is part of the select but is not a reference\n // so we can stop here and don't have to follow it\n return { collection, path: [field] }\n }\n\n if (ref.path.length > 1) {\n // This is a nested field\n const [alias, ...rest] = ref.path\n const aliasRef = getRefFromAlias(query, alias!)\n if (!aliasRef) {\n return\n }\n\n if (aliasRef.type === `queryRef`) {\n return followRef(aliasRef.query, new PropRef(rest), collection)\n } else {\n // This is a reference to a collection\n // we can't follow it further\n // so the field must be on the collection itself\n return { collection: aliasRef.collection, path: rest }\n }\n }\n}\n\nexport type CompileQueryFn = typeof compileQuery\n"],"names":["resultPipeline","result","compilationResult","ValClass"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../../src/query/compiler/index.ts"],"sourcesContent":["import { distinct, filter, map } from '@tanstack/db-ivm'\nimport { optimizeQuery } from '../optimizer.js'\nimport {\n CollectionInputNotFoundError,\n DistinctRequiresSelectError,\n DuplicateAliasInSubqueryError,\n FnSelectWithGroupByError,\n HavingRequiresGroupByError,\n LimitOffsetRequireOrderByError,\n UnsupportedFromTypeError,\n} from '../../errors.js'\nimport { PropRef, Value as ValClass, getWhereExpression } from '../ir.js'\nimport { compileExpression, toBooleanPredicate } from './evaluators.js'\nimport { processJoins } from './joins.js'\nimport { containsAggregate, processGroupBy } from './group-by.js'\nimport { processOrderBy } from './order-by.js'\nimport { processSelect } from './select.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\nimport type { OrderByOptimizationInfo } from './order-by.js'\nimport type {\n BasicExpression,\n CollectionRef,\n QueryIR,\n QueryRef,\n} from '../ir.js'\nimport type { LazyCollectionCallbacks } from './joins.js'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n ResultStream,\n} from '../../types.js'\nimport type { QueryCache, QueryMapping, WindowOptions } from './types.js'\n\nexport type { WindowOptions } from './types.js'\n\n/**\n * Result of query compilation including both the pipeline and source-specific WHERE clauses\n */\nexport interface CompilationResult {\n /** The ID of the main collection */\n collectionId: string\n\n /** The compiled query pipeline (D2 stream) */\n pipeline: ResultStream\n\n /** Map of source aliases to their WHERE clauses for index optimization */\n sourceWhereClauses: Map<string, BasicExpression<boolean>>\n\n /**\n * Maps each source alias to its collection ID. Enables per-alias subscriptions for self-joins.\n * Example: `{ employee: 'employees-col-id', manager: 'employees-col-id' }`\n */\n aliasToCollectionId: Record<string, string>\n\n /**\n * Flattened mapping from outer alias to innermost alias for subqueries.\n * Always provides one-hop lookups, never recursive chains.\n *\n * Example: `{ activeUser: 'user' }` when `.from({ activeUser: subquery })`\n * where the subquery uses `.from({ user: collection })`.\n *\n * For deeply nested subqueries, the mapping goes directly to the innermost alias:\n * `{ author: 'user' }` (not `{ author: 'activeUser' }`), so `aliasRemapping[alias]`\n * always resolves in a single lookup.\n *\n * Used to resolve subscriptions during lazy loading when join aliases differ from\n * the inner aliases where collection subscriptions were created.\n */\n aliasRemapping: Record<string, string>\n}\n\n/**\n * Compiles a query IR into a D2 pipeline\n * @param rawQuery The query IR to compile\n * @param inputs Mapping of source aliases to input streams (e.g., `{ employee: input1, manager: input2 }`)\n * @param collections Mapping of collection IDs to Collection instances\n * @param subscriptions Mapping of source aliases to CollectionSubscription instances\n * @param callbacks Mapping of source aliases to lazy loading callbacks\n * @param lazySources Set of source aliases that should load data lazily\n * @param optimizableOrderByCollections Map of collection IDs to order-by optimization info\n * @param cache Optional cache for compiled subqueries (used internally for recursion)\n * @param queryMapping Optional mapping from optimized queries to original queries\n * @returns A CompilationResult with the pipeline, source WHERE clauses, and alias metadata\n */\nexport function compileQuery(\n rawQuery: QueryIR,\n inputs: Record<string, KeyedStream>,\n collections: Record<string, Collection<any, any, any, any, any>>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache = new WeakMap(),\n queryMapping: QueryMapping = new WeakMap(),\n): CompilationResult {\n // Check if the original raw query has already been compiled\n const cachedResult = cache.get(rawQuery)\n if (cachedResult) {\n return cachedResult\n }\n\n // Validate the raw query BEFORE optimization to check user's original structure.\n // This must happen before optimization because the optimizer may create internal\n // subqueries (e.g., for predicate pushdown) that reuse aliases, which is fine.\n validateQueryStructure(rawQuery)\n\n // Optimize the query before compilation\n const { optimizedQuery: query, sourceWhereClauses } = optimizeQuery(rawQuery)\n\n // Create mapping from optimized query to original for caching\n queryMapping.set(query, rawQuery)\n mapNestedQueries(query, rawQuery, queryMapping)\n\n // Create a copy of the inputs map to avoid modifying the original\n const allInputs = { ...inputs }\n\n // Track alias to collection id relationships discovered during compilation.\n // This includes all user-declared aliases plus inner aliases from subqueries.\n const aliasToCollectionId: Record<string, string> = {}\n\n // Track alias remapping for subqueries (outer alias → inner alias)\n // e.g., when .join({ activeUser: subquery }) where subquery uses .from({ user: collection })\n // we store: aliasRemapping['activeUser'] = 'user'\n const aliasRemapping: Record<string, string> = {}\n\n // Create a map of source aliases to input streams.\n // Inputs MUST be keyed by alias (e.g., `{ employee: input1, manager: input2 }`),\n // not by collection ID. This enables per-alias subscriptions where different aliases\n // of the same collection (e.g., self-joins) maintain independent filtered streams.\n const sources: Record<string, KeyedStream> = {}\n\n // Process the FROM clause to get the main source\n const {\n alias: mainSource,\n input: mainInput,\n collectionId: mainCollectionId,\n } = processFrom(\n query.from,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n aliasToCollectionId,\n aliasRemapping,\n sourceWhereClauses,\n )\n sources[mainSource] = mainInput\n\n // Prepare the initial pipeline with the main source wrapped in its alias\n let pipeline: NamespacedAndKeyedStream = mainInput.pipe(\n map(([key, row]) => {\n // Initialize the record with a nested structure\n const ret = [key, { [mainSource]: row }] as [\n string,\n Record<string, typeof row>,\n ]\n return ret\n }),\n )\n\n // Process JOIN clauses if they exist\n if (query.join && query.join.length > 0) {\n pipeline = processJoins(\n pipeline,\n query.join,\n sources,\n mainCollectionId,\n mainSource,\n allInputs,\n cache,\n queryMapping,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n rawQuery,\n compileQuery,\n aliasToCollectionId,\n aliasRemapping,\n sourceWhereClauses,\n )\n }\n\n // Process the WHERE clause if it exists\n if (query.where && query.where.length > 0) {\n // Apply each WHERE condition as a filter (they are ANDed together)\n for (const where of query.where) {\n const whereExpression = getWhereExpression(where)\n const compiledWhere = compileExpression(whereExpression)\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return toBooleanPredicate(compiledWhere(namespacedRow))\n }),\n )\n }\n }\n\n // Process functional WHERE clauses if they exist\n if (query.fnWhere && query.fnWhere.length > 0) {\n for (const fnWhere of query.fnWhere) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return toBooleanPredicate(fnWhere(namespacedRow))\n }),\n )\n }\n }\n\n if (query.distinct && !query.fnSelect && !query.select) {\n throw new DistinctRequiresSelectError()\n }\n\n if (query.fnSelect && query.groupBy && query.groupBy.length > 0) {\n throw new FnSelectWithGroupByError()\n }\n\n // Process the SELECT clause early - always create $selected\n // This eliminates duplication and allows for DISTINCT implementation\n if (query.fnSelect) {\n // Handle functional select - apply the function to transform the row\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults = query.fnSelect!(namespacedRow)\n return [\n key,\n {\n ...namespacedRow,\n $selected: selectResults,\n },\n ] as [string, typeof namespacedRow & { $selected: any }]\n }),\n )\n } else if (query.select) {\n pipeline = processSelect(pipeline, query.select, allInputs)\n } else {\n // If no SELECT clause, create $selected with the main table data\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults =\n !query.join && !query.groupBy\n ? namespacedRow[mainSource]\n : namespacedRow\n\n return [\n key,\n {\n ...namespacedRow,\n $selected: selectResults,\n },\n ] as [string, typeof namespacedRow & { $selected: any }]\n }),\n )\n }\n\n // Process the GROUP BY clause if it exists\n if (query.groupBy && query.groupBy.length > 0) {\n pipeline = processGroupBy(\n pipeline,\n query.groupBy,\n query.having,\n query.select,\n query.fnHaving,\n )\n } else if (query.select) {\n // Check if SELECT contains aggregates but no GROUP BY (implicit single-group aggregation)\n const hasAggregates = Object.values(query.select).some(\n (expr) => expr.type === `agg` || containsAggregate(expr),\n )\n if (hasAggregates) {\n // Handle implicit single-group aggregation\n pipeline = processGroupBy(\n pipeline,\n [], // Empty group by means single group\n query.having,\n query.select,\n query.fnHaving,\n )\n }\n }\n\n // Process the HAVING clause if it exists (only applies after GROUP BY)\n if (query.having && (!query.groupBy || query.groupBy.length === 0)) {\n // Check if we have aggregates in SELECT that would trigger implicit grouping\n const hasAggregates = query.select\n ? Object.values(query.select).some((expr) => expr.type === `agg`)\n : false\n\n if (!hasAggregates) {\n throw new HavingRequiresGroupByError()\n }\n }\n\n // Process functional HAVING clauses outside of GROUP BY (treat as additional WHERE filters)\n if (\n query.fnHaving &&\n query.fnHaving.length > 0 &&\n (!query.groupBy || query.groupBy.length === 0)\n ) {\n // If there's no GROUP BY but there are fnHaving clauses, apply them as filters\n for (const fnHaving of query.fnHaving) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return fnHaving(namespacedRow)\n }),\n )\n }\n }\n\n // Process the DISTINCT clause if it exists\n if (query.distinct) {\n pipeline = pipeline.pipe(distinct(([_key, row]) => row.$selected))\n }\n\n // Process orderBy parameter if it exists\n if (query.orderBy && query.orderBy.length > 0) {\n const orderedPipeline = processOrderBy(\n rawQuery,\n pipeline,\n query.orderBy,\n query.select || {},\n collections[mainCollectionId]!,\n optimizableOrderByCollections,\n setWindowFn,\n query.limit,\n query.offset,\n )\n\n // Final step: extract the $selected and include orderBy index\n const resultPipeline = orderedPipeline.pipe(\n map(([key, [row, orderByIndex]]) => {\n // Extract the final results from $selected and include orderBy index\n const raw = (row as any).$selected\n const finalResults = unwrapValue(raw)\n return [key, [finalResults, orderByIndex]] as [unknown, [any, string]]\n }),\n )\n\n const result = resultPipeline\n // Cache the result before returning (use original query as key)\n const compilationResult = {\n collectionId: mainCollectionId,\n pipeline: result,\n sourceWhereClauses,\n aliasToCollectionId,\n aliasRemapping,\n }\n cache.set(rawQuery, compilationResult)\n\n return compilationResult\n } else if (query.limit !== undefined || query.offset !== undefined) {\n // If there's a limit or offset without orderBy, throw an error\n throw new LimitOffsetRequireOrderByError()\n }\n\n // Final step: extract the $selected and return tuple format (no orderBy)\n const resultPipeline: ResultStream = pipeline.pipe(\n map(([key, row]) => {\n // Extract the final results from $selected and return [key, [results, undefined]]\n const raw = (row as any).$selected\n const finalResults = unwrapValue(raw)\n return [key, [finalResults, undefined]] as [\n unknown,\n [any, string | undefined],\n ]\n }),\n )\n\n const result = resultPipeline\n // Cache the result before returning (use original query as key)\n const compilationResult = {\n collectionId: mainCollectionId,\n pipeline: result,\n sourceWhereClauses,\n aliasToCollectionId,\n aliasRemapping,\n }\n cache.set(rawQuery, compilationResult)\n\n return compilationResult\n}\n\n/**\n * Collects aliases used for DIRECT collection references (not subqueries).\n * Used to validate that subqueries don't reuse parent query collection aliases.\n * Only direct CollectionRef aliases matter - QueryRef aliases don't cause conflicts.\n */\nfunction collectDirectCollectionAliases(query: QueryIR): Set<string> {\n const aliases = new Set<string>()\n\n // Collect FROM alias only if it's a direct collection reference\n if (query.from.type === `collectionRef`) {\n aliases.add(query.from.alias)\n }\n\n // Collect JOIN aliases only for direct collection references\n if (query.join) {\n for (const joinClause of query.join) {\n if (joinClause.from.type === `collectionRef`) {\n aliases.add(joinClause.from.alias)\n }\n }\n }\n\n return aliases\n}\n\n/**\n * Validates the structure of a query and its subqueries.\n * Checks that subqueries don't reuse collection aliases from parent queries.\n * This must be called on the RAW query before optimization.\n */\nfunction validateQueryStructure(\n query: QueryIR,\n parentCollectionAliases: Set<string> = new Set(),\n): void {\n // Collect direct collection aliases from this query level\n const currentLevelAliases = collectDirectCollectionAliases(query)\n\n // Check if any current alias conflicts with parent aliases\n for (const alias of currentLevelAliases) {\n if (parentCollectionAliases.has(alias)) {\n throw new DuplicateAliasInSubqueryError(\n alias,\n Array.from(parentCollectionAliases),\n )\n }\n }\n\n // Combine parent and current aliases for checking nested subqueries\n const combinedAliases = new Set([\n ...parentCollectionAliases,\n ...currentLevelAliases,\n ])\n\n // Recursively validate FROM subquery\n if (query.from.type === `queryRef`) {\n validateQueryStructure(query.from.query, combinedAliases)\n }\n\n // Recursively validate JOIN subqueries\n if (query.join) {\n for (const joinClause of query.join) {\n if (joinClause.from.type === `queryRef`) {\n validateQueryStructure(joinClause.from.query, combinedAliases)\n }\n }\n }\n}\n\n/**\n * Processes the FROM clause, handling direct collection references and subqueries.\n * Populates `aliasToCollectionId` and `aliasRemapping` for per-alias subscription tracking.\n */\nfunction processFrom(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache,\n queryMapping: QueryMapping,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n sourceWhereClauses: Map<string, BasicExpression<boolean>>,\n): { alias: string; input: KeyedStream; collectionId: string } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.alias]\n if (!input) {\n throw new CollectionInputNotFoundError(\n from.alias,\n from.collection.id,\n Object.keys(allInputs),\n )\n }\n aliasToCollectionId[from.alias] = from.collection.id\n return { alias: from.alias, input, collectionId: from.collection.id }\n }\n case `queryRef`: {\n // Find the original query for caching purposes\n const originalQuery = queryMapping.get(from.query) || from.query\n\n // Recursively compile the sub-query with cache\n const subQueryResult = compileQuery(\n originalQuery,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n )\n\n // Pull up alias mappings from subquery to parent scope.\n // This includes both the innermost alias-to-collection mappings AND\n // any existing remappings from nested subquery levels.\n Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId)\n Object.assign(aliasRemapping, subQueryResult.aliasRemapping)\n\n // Pull up source WHERE clauses from subquery to parent scope.\n // This enables loadSubset to receive the correct where clauses for subquery collections.\n //\n // IMPORTANT: Skip pull-up for optimizer-created subqueries. These are detected when:\n // 1. The outer alias (from.alias) matches the inner alias (from.query.from.alias)\n // 2. The subquery was found in queryMapping (it's a user-defined subquery, not optimizer-created)\n //\n // For optimizer-created subqueries, the parent already has the sourceWhereClauses\n // extracted from the original raw query, so pulling up would be redundant.\n // More importantly, pulling up for optimizer-created subqueries can cause issues\n // when the optimizer has restructured the query.\n const isUserDefinedSubquery = queryMapping.has(from.query)\n const subqueryFromAlias = from.query.from.alias\n const isOptimizerCreated =\n !isUserDefinedSubquery && from.alias === subqueryFromAlias\n\n if (!isOptimizerCreated) {\n for (const [alias, whereClause] of subQueryResult.sourceWhereClauses) {\n sourceWhereClauses.set(alias, whereClause)\n }\n }\n\n // Create a FLATTENED remapping from outer alias to innermost alias.\n // For nested subqueries, this ensures one-hop lookups (not recursive chains).\n //\n // Example with 3-level nesting:\n // Inner: .from({ user: usersCollection })\n // Middle: .from({ activeUser: innerSubquery }) → creates: activeUser → user\n // Outer: .from({ author: middleSubquery }) → creates: author → user (not author → activeUser)\n //\n // The key insight: We search through the PULLED-UP aliasToCollectionId (which contains\n // the innermost 'user' alias), so we always map directly to the deepest level.\n // This means aliasRemapping[alias] is always a single lookup, never recursive.\n // Needed for subscription resolution during lazy loading.\n const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(\n (alias) =>\n subQueryResult.aliasToCollectionId[alias] ===\n subQueryResult.collectionId,\n )\n if (innerAlias && innerAlias !== from.alias) {\n aliasRemapping[from.alias] = innerAlias\n }\n\n // Extract the pipeline from the compilation result\n const subQueryInput = subQueryResult.pipeline\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n // Unwrap Value expressions that might have leaked through as the entire row\n const unwrapped = unwrapValue(value)\n return [key, unwrapped] as [unknown, any]\n }),\n )\n\n return {\n alias: from.alias,\n input: extractedInput,\n collectionId: subQueryResult.collectionId,\n }\n }\n default:\n throw new UnsupportedFromTypeError((from as any).type)\n }\n}\n\n// Helper to check if a value is a Value expression\nfunction isValue(raw: any): boolean {\n return (\n raw instanceof ValClass ||\n (raw && typeof raw === `object` && `type` in raw && raw.type === `val`)\n )\n}\n\n// Helper to unwrap a Value expression or return the value itself\nfunction unwrapValue(value: any): any {\n return isValue(value) ? value.value : value\n}\n\n/**\n * Recursively maps optimized subqueries to their original queries for proper caching.\n * This ensures that when we encounter the same QueryRef object in different contexts,\n * we can find the original query to check the cache.\n */\nfunction mapNestedQueries(\n optimizedQuery: QueryIR,\n originalQuery: QueryIR,\n queryMapping: QueryMapping,\n): void {\n // Map the FROM clause if it's a QueryRef\n if (\n optimizedQuery.from.type === `queryRef` &&\n originalQuery.from.type === `queryRef`\n ) {\n queryMapping.set(optimizedQuery.from.query, originalQuery.from.query)\n // Recursively map nested queries\n mapNestedQueries(\n optimizedQuery.from.query,\n originalQuery.from.query,\n queryMapping,\n )\n }\n\n // Map JOIN clauses if they exist\n if (optimizedQuery.join && originalQuery.join) {\n for (\n let i = 0;\n i < optimizedQuery.join.length && i < originalQuery.join.length;\n i++\n ) {\n const optimizedJoin = optimizedQuery.join[i]!\n const originalJoin = originalQuery.join[i]!\n\n if (\n optimizedJoin.from.type === `queryRef` &&\n originalJoin.from.type === `queryRef`\n ) {\n queryMapping.set(optimizedJoin.from.query, originalJoin.from.query)\n // Recursively map nested queries in joins\n mapNestedQueries(\n optimizedJoin.from.query,\n originalJoin.from.query,\n queryMapping,\n )\n }\n }\n }\n}\n\nfunction getRefFromAlias(\n query: QueryIR,\n alias: string,\n): CollectionRef | QueryRef | void {\n if (query.from.alias === alias) {\n return query.from\n }\n\n for (const join of query.join || []) {\n if (join.from.alias === alias) {\n return join.from\n }\n }\n}\n\n/**\n * Follows the given reference in a query\n * until its finds the root field the reference points to.\n * @returns The collection, its alias, and the path to the root field in this collection\n */\nexport function followRef(\n query: QueryIR,\n ref: PropRef<any>,\n collection: Collection,\n): { collection: Collection; path: Array<string> } | void {\n if (ref.path.length === 0) {\n return\n }\n\n if (ref.path.length === 1) {\n // This field should be part of this collection\n const field = ref.path[0]!\n // is it part of the select clause?\n if (query.select) {\n const selectedField = query.select[field]\n if (selectedField && selectedField.type === `ref`) {\n return followRef(query, selectedField, collection)\n }\n }\n\n // Either this field is not part of the select clause\n // and thus it must be part of the collection itself\n // or it is part of the select but is not a reference\n // so we can stop here and don't have to follow it\n return { collection, path: [field] }\n }\n\n if (ref.path.length > 1) {\n // This is a nested field\n const [alias, ...rest] = ref.path\n const aliasRef = getRefFromAlias(query, alias!)\n if (!aliasRef) {\n return\n }\n\n if (aliasRef.type === `queryRef`) {\n return followRef(aliasRef.query, new PropRef(rest), collection)\n } else {\n // This is a reference to a collection\n // we can't follow it further\n // so the field must be on the collection itself\n return { collection: aliasRef.collection, path: rest }\n }\n }\n}\n\nexport type CompileQueryFn = typeof compileQuery\n"],"names":["resultPipeline","result","compilationResult","ValClass"],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ import { InitialQueryBuilder, QueryBuilder } from './builder/index.js';
2
+ import { Context } from './builder/types.js';
3
+ /** Event types for query result deltas */
4
+ export type DeltaType = 'enter' | 'exit' | 'update';
5
+ /** Delta event emitted when a row enters, exits, or updates within a query result */
6
+ export type DeltaEvent<TRow extends object = Record<string, unknown>, TKey extends string | number = string | number> = {
7
+ type: 'enter';
8
+ key: TKey;
9
+ /** Current value for the entering row */
10
+ value: TRow;
11
+ metadata?: Record<string, unknown>;
12
+ } | {
13
+ type: 'exit';
14
+ key: TKey;
15
+ /** Current value for the exiting row */
16
+ value: TRow;
17
+ metadata?: Record<string, unknown>;
18
+ } | {
19
+ type: 'update';
20
+ key: TKey;
21
+ /** Current value after the update */
22
+ value: TRow;
23
+ /** Previous value before the batch */
24
+ previousValue: TRow;
25
+ metadata?: Record<string, unknown>;
26
+ };
27
+ /** Context passed to effect handlers */
28
+ export interface EffectContext {
29
+ /** ID of this effect (auto-generated if not provided) */
30
+ effectId: string;
31
+ /** Aborted when effect.dispose() is called */
32
+ signal: AbortSignal;
33
+ }
34
+ /** Query input - can be a builder function or a prebuilt query */
35
+ export type EffectQueryInput<TContext extends Context> = ((q: InitialQueryBuilder) => QueryBuilder<TContext>) | QueryBuilder<TContext>;
36
+ type EffectEventHandler<TRow extends object = Record<string, unknown>, TKey extends string | number = string | number> = (event: DeltaEvent<TRow, TKey>, ctx: EffectContext) => void | Promise<void>;
37
+ type EffectBatchHandler<TRow extends object = Record<string, unknown>, TKey extends string | number = string | number> = (events: Array<DeltaEvent<TRow, TKey>>, ctx: EffectContext) => void | Promise<void>;
38
+ /** Effect configuration */
39
+ export interface EffectConfig<TRow extends object = Record<string, unknown>, TKey extends string | number = string | number> {
40
+ /** Optional ID for debugging/tracing */
41
+ id?: string;
42
+ /** Query to watch for deltas */
43
+ query: EffectQueryInput<any>;
44
+ /** Called once for each row entering the query result */
45
+ onEnter?: EffectEventHandler<TRow, TKey>;
46
+ /** Called once for each row updating within the query result */
47
+ onUpdate?: EffectEventHandler<TRow, TKey>;
48
+ /** Called once for each row exiting the query result */
49
+ onExit?: EffectEventHandler<TRow, TKey>;
50
+ /** Called once per graph run with all delta events from that batch */
51
+ onBatch?: EffectBatchHandler<TRow, TKey>;
52
+ /** Error handler for exceptions thrown by effect callbacks */
53
+ onError?: (error: Error, event: DeltaEvent<TRow, TKey>) => void;
54
+ /**
55
+ * Called when a source collection enters an error or cleaned-up state.
56
+ * The effect is automatically disposed after this callback fires.
57
+ * If not provided, the error is logged to console.error.
58
+ */
59
+ onSourceError?: (error: Error) => void;
60
+ /**
61
+ * Skip deltas during initial collection load.
62
+ * Defaults to false (process all deltas including initial sync).
63
+ * Set to true for effects that should only process new changes.
64
+ */
65
+ skipInitial?: boolean;
66
+ }
67
+ /** Handle returned by createEffect */
68
+ export interface Effect {
69
+ /** Dispose the effect. Returns a promise that resolves when in-flight handlers complete. */
70
+ dispose: () => Promise<void>;
71
+ /** Whether this effect has been disposed */
72
+ readonly disposed: boolean;
73
+ }
74
+ /**
75
+ * Creates a reactive effect that fires handlers when rows enter, exit, or
76
+ * update within a query result. Effects process deltas only — they do not
77
+ * maintain or require the full materialised query result.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * const effect = createEffect({
82
+ * query: (q) => q.from({ msg: messagesCollection })
83
+ * .where(({ msg }) => eq(msg.role, 'user')),
84
+ * onEnter: async (event) => {
85
+ * await generateResponse(event.value)
86
+ * },
87
+ * })
88
+ *
89
+ * // Later: stop the effect
90
+ * await effect.dispose()
91
+ * ```
92
+ */
93
+ export declare function createEffect<TRow extends object = Record<string, unknown>, TKey extends string | number = string | number>(config: EffectConfig<TRow, TKey>): Effect;
94
+ export {};