@tanstack/db 0.4.2 → 0.4.4

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 (180) hide show
  1. package/dist/cjs/collection/change-events.cjs +1 -1
  2. package/dist/cjs/collection/change-events.cjs.map +1 -1
  3. package/dist/cjs/collection/changes.cjs +7 -3
  4. package/dist/cjs/collection/changes.cjs.map +1 -1
  5. package/dist/cjs/collection/events.cjs +3 -6
  6. package/dist/cjs/collection/events.cjs.map +1 -1
  7. package/dist/cjs/collection/index.cjs +4 -1
  8. package/dist/cjs/collection/index.cjs.map +1 -1
  9. package/dist/cjs/collection/index.d.cts +16 -4
  10. package/dist/cjs/collection/mutations.cjs +13 -20
  11. package/dist/cjs/collection/mutations.cjs.map +1 -1
  12. package/dist/cjs/collection/state.cjs +9 -2
  13. package/dist/cjs/collection/state.cjs.map +1 -1
  14. package/dist/cjs/collection/subscription.cjs +4 -5
  15. package/dist/cjs/collection/subscription.cjs.map +1 -1
  16. package/dist/cjs/collection/subscription.d.cts +2 -2
  17. package/dist/cjs/collection/sync.cjs +10 -2
  18. package/dist/cjs/collection/sync.cjs.map +1 -1
  19. package/dist/cjs/indexes/auto-index.cjs +4 -3
  20. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  21. package/dist/cjs/indexes/auto-index.d.cts +2 -1
  22. package/dist/cjs/indexes/base-index.cjs +26 -0
  23. package/dist/cjs/indexes/base-index.cjs.map +1 -1
  24. package/dist/cjs/indexes/base-index.d.cts +47 -2
  25. package/dist/cjs/indexes/btree-index.cjs +45 -9
  26. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  27. package/dist/cjs/indexes/btree-index.d.cts +15 -0
  28. package/dist/cjs/indexes/lazy-index.cjs +3 -6
  29. package/dist/cjs/indexes/lazy-index.cjs.map +1 -1
  30. package/dist/cjs/indexes/reverse-index.cjs +78 -0
  31. package/dist/cjs/indexes/reverse-index.cjs.map +1 -0
  32. package/dist/cjs/indexes/reverse-index.d.cts +30 -0
  33. package/dist/cjs/proxy.cjs +1 -1
  34. package/dist/cjs/proxy.cjs.map +1 -1
  35. package/dist/cjs/query/builder/index.cjs +21 -0
  36. package/dist/cjs/query/builder/index.cjs.map +1 -1
  37. package/dist/cjs/query/builder/index.d.cts +16 -1
  38. package/dist/cjs/query/builder/types.d.cts +7 -0
  39. package/dist/cjs/query/compiler/evaluators.cjs +1 -1
  40. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  41. package/dist/cjs/query/compiler/group-by.cjs +2 -10
  42. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  43. package/dist/cjs/query/compiler/index.cjs +2 -39
  44. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  45. package/dist/cjs/query/compiler/index.d.cts +1 -0
  46. package/dist/cjs/query/compiler/joins.cjs +15 -14
  47. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  48. package/dist/cjs/query/compiler/joins.d.cts +2 -1
  49. package/dist/cjs/query/compiler/order-by.cjs +8 -10
  50. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  51. package/dist/cjs/query/compiler/order-by.d.cts +2 -2
  52. package/dist/cjs/query/compiler/select.cjs +1 -1
  53. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  54. package/dist/cjs/query/index.d.cts +1 -1
  55. package/dist/cjs/query/ir.cjs +38 -0
  56. package/dist/cjs/query/ir.cjs.map +1 -1
  57. package/dist/cjs/query/ir.d.cts +11 -1
  58. package/dist/cjs/query/live/collection-config-builder.cjs +3 -2
  59. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  60. package/dist/cjs/query/live/collection-config-builder.d.cts +2 -2
  61. package/dist/cjs/query/live/collection-subscriber.cjs +2 -3
  62. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  63. package/dist/cjs/query/live/types.d.cts +4 -0
  64. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  65. package/dist/cjs/query/live-query-collection.d.cts +7 -4
  66. package/dist/cjs/query/optimizer.cjs +2 -4
  67. package/dist/cjs/query/optimizer.cjs.map +1 -1
  68. package/dist/cjs/transactions.cjs +2 -3
  69. package/dist/cjs/transactions.cjs.map +1 -1
  70. package/dist/cjs/types.d.cts +13 -0
  71. package/dist/cjs/utils/btree.cjs +1 -1
  72. package/dist/cjs/utils/btree.cjs.map +1 -1
  73. package/dist/cjs/utils/index-optimization.cjs +7 -2
  74. package/dist/cjs/utils/index-optimization.cjs.map +1 -1
  75. package/dist/cjs/utils/index-optimization.d.cts +3 -2
  76. package/dist/cjs/utils.cjs +6 -0
  77. package/dist/cjs/utils.cjs.map +1 -1
  78. package/dist/cjs/utils.d.cts +2 -3
  79. package/dist/esm/collection/change-events.js +1 -1
  80. package/dist/esm/collection/change-events.js.map +1 -1
  81. package/dist/esm/collection/changes.js +7 -3
  82. package/dist/esm/collection/changes.js.map +1 -1
  83. package/dist/esm/collection/events.js +3 -6
  84. package/dist/esm/collection/events.js.map +1 -1
  85. package/dist/esm/collection/index.d.ts +16 -4
  86. package/dist/esm/collection/index.js +4 -1
  87. package/dist/esm/collection/index.js.map +1 -1
  88. package/dist/esm/collection/mutations.js +13 -20
  89. package/dist/esm/collection/mutations.js.map +1 -1
  90. package/dist/esm/collection/state.js +9 -2
  91. package/dist/esm/collection/state.js.map +1 -1
  92. package/dist/esm/collection/subscription.d.ts +2 -2
  93. package/dist/esm/collection/subscription.js +4 -5
  94. package/dist/esm/collection/subscription.js.map +1 -1
  95. package/dist/esm/collection/sync.js +10 -2
  96. package/dist/esm/collection/sync.js.map +1 -1
  97. package/dist/esm/indexes/auto-index.d.ts +2 -1
  98. package/dist/esm/indexes/auto-index.js +4 -3
  99. package/dist/esm/indexes/auto-index.js.map +1 -1
  100. package/dist/esm/indexes/base-index.d.ts +47 -2
  101. package/dist/esm/indexes/base-index.js +26 -0
  102. package/dist/esm/indexes/base-index.js.map +1 -1
  103. package/dist/esm/indexes/btree-index.d.ts +15 -0
  104. package/dist/esm/indexes/btree-index.js +45 -9
  105. package/dist/esm/indexes/btree-index.js.map +1 -1
  106. package/dist/esm/indexes/lazy-index.js +3 -6
  107. package/dist/esm/indexes/lazy-index.js.map +1 -1
  108. package/dist/esm/indexes/reverse-index.d.ts +30 -0
  109. package/dist/esm/indexes/reverse-index.js +78 -0
  110. package/dist/esm/indexes/reverse-index.js.map +1 -0
  111. package/dist/esm/proxy.js +1 -1
  112. package/dist/esm/proxy.js.map +1 -1
  113. package/dist/esm/query/builder/index.d.ts +16 -1
  114. package/dist/esm/query/builder/index.js +21 -0
  115. package/dist/esm/query/builder/index.js.map +1 -1
  116. package/dist/esm/query/builder/types.d.ts +7 -0
  117. package/dist/esm/query/compiler/evaluators.js +1 -1
  118. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  119. package/dist/esm/query/compiler/group-by.js +3 -11
  120. package/dist/esm/query/compiler/group-by.js.map +1 -1
  121. package/dist/esm/query/compiler/index.d.ts +1 -0
  122. package/dist/esm/query/compiler/index.js +4 -41
  123. package/dist/esm/query/compiler/index.js.map +1 -1
  124. package/dist/esm/query/compiler/joins.d.ts +2 -1
  125. package/dist/esm/query/compiler/joins.js +15 -14
  126. package/dist/esm/query/compiler/joins.js.map +1 -1
  127. package/dist/esm/query/compiler/order-by.d.ts +2 -2
  128. package/dist/esm/query/compiler/order-by.js +5 -7
  129. package/dist/esm/query/compiler/order-by.js.map +1 -1
  130. package/dist/esm/query/compiler/select.js +1 -1
  131. package/dist/esm/query/compiler/select.js.map +1 -1
  132. package/dist/esm/query/index.d.ts +1 -1
  133. package/dist/esm/query/ir.d.ts +11 -1
  134. package/dist/esm/query/ir.js +38 -0
  135. package/dist/esm/query/ir.js.map +1 -1
  136. package/dist/esm/query/live/collection-config-builder.d.ts +2 -2
  137. package/dist/esm/query/live/collection-config-builder.js +3 -2
  138. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  139. package/dist/esm/query/live/collection-subscriber.js +2 -3
  140. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  141. package/dist/esm/query/live/types.d.ts +4 -0
  142. package/dist/esm/query/live-query-collection.d.ts +7 -4
  143. package/dist/esm/query/live-query-collection.js.map +1 -1
  144. package/dist/esm/query/optimizer.js +2 -4
  145. package/dist/esm/query/optimizer.js.map +1 -1
  146. package/dist/esm/transactions.js +2 -3
  147. package/dist/esm/transactions.js.map +1 -1
  148. package/dist/esm/types.d.ts +13 -0
  149. package/dist/esm/utils/btree.js +1 -1
  150. package/dist/esm/utils/btree.js.map +1 -1
  151. package/dist/esm/utils/index-optimization.d.ts +3 -2
  152. package/dist/esm/utils/index-optimization.js +7 -2
  153. package/dist/esm/utils/index-optimization.js.map +1 -1
  154. package/dist/esm/utils.d.ts +2 -3
  155. package/dist/esm/utils.js +6 -0
  156. package/dist/esm/utils.js.map +1 -1
  157. package/package.json +1 -1
  158. package/src/collection/changes.ts +10 -4
  159. package/src/collection/index.ts +38 -5
  160. package/src/collection/state.ts +22 -5
  161. package/src/collection/subscription.ts +4 -4
  162. package/src/collection/sync.ts +17 -2
  163. package/src/indexes/auto-index.ts +8 -3
  164. package/src/indexes/base-index.ts +94 -4
  165. package/src/indexes/btree-index.ts +58 -7
  166. package/src/indexes/reverse-index.ts +120 -0
  167. package/src/query/builder/index.ts +30 -2
  168. package/src/query/builder/types.ts +12 -0
  169. package/src/query/compiler/group-by.ts +1 -10
  170. package/src/query/compiler/index.ts +4 -1
  171. package/src/query/compiler/joins.ts +13 -8
  172. package/src/query/compiler/order-by.ts +16 -20
  173. package/src/query/index.ts +1 -0
  174. package/src/query/ir.ts +68 -1
  175. package/src/query/live/collection-config-builder.ts +3 -2
  176. package/src/query/live/types.ts +5 -0
  177. package/src/query/live-query-collection.ts +34 -8
  178. package/src/types.ts +22 -0
  179. package/src/utils/index-optimization.ts +19 -5
  180. package/src/utils.ts +8 -0
@@ -1 +1 @@
1
- {"version":3,"file":"optimizer.js","sources":["../../../src/query/optimizer.ts"],"sourcesContent":["/**\n * # Query Optimizer\n *\n * The query optimizer improves query performance by implementing predicate pushdown optimization.\n * It rewrites the intermediate representation (IR) to push WHERE clauses as close to the data\n * source as possible, reducing the amount of data processed during joins.\n *\n * ## How It Works\n *\n * The optimizer follows a 4-step process:\n *\n * ### 1. AND Clause Splitting\n * Splits AND clauses at the root level into separate WHERE clauses for granular optimization.\n * ```javascript\n * // Before: WHERE and(eq(users.department_id, 1), gt(users.age, 25))\n * // After: WHERE eq(users.department_id, 1) + WHERE gt(users.age, 25)\n * ```\n *\n * ### 2. Source Analysis\n * Analyzes each WHERE clause to determine which table sources it references:\n * - Single-source clauses: Touch only one table (e.g., `users.department_id = 1`)\n * - Multi-source clauses: Touch multiple tables (e.g., `users.id = posts.user_id`)\n *\n * ### 3. Clause Grouping\n * Groups WHERE clauses by the sources they touch:\n * - Single-source clauses are grouped by their respective table\n * - Multi-source clauses are combined for the main query\n *\n * ### 4. Subquery Creation\n * Lifts single-source WHERE clauses into subqueries that wrap the original table references.\n *\n * ## Safety & Edge Cases\n *\n * The optimizer includes targeted safety checks to prevent predicate pushdown when it could\n * break query semantics:\n *\n * ### Always Safe Operations\n * - **Creating new subqueries**: Wrapping collection references in subqueries with WHERE clauses\n * - **Main query optimizations**: Moving single-source WHERE clauses from main query to subqueries\n * - **Queries with aggregates/ORDER BY/HAVING**: Can still create new filtered subqueries\n *\n * ### Unsafe Operations (blocked by safety checks)\n * Pushing WHERE clauses **into existing subqueries** that have:\n * - **Aggregates**: GROUP BY, HAVING, or aggregate functions in SELECT (would change aggregation)\n * - **Ordering + Limits**: ORDER BY combined with LIMIT/OFFSET (would change result set)\n * - **Functional Operations**: fnSelect, fnWhere, fnHaving (potential side effects)\n *\n * ### Residual WHERE Clauses\n * For outer joins (LEFT, RIGHT, FULL), WHERE clauses are copied to subqueries for optimization\n * but also kept as \"residual\" clauses in the main query to preserve semantics. This ensures\n * that NULL values from outer joins are properly filtered according to SQL standards.\n *\n * The optimizer tracks which clauses were actually optimized and only removes those from the\n * main query. Subquery reuse is handled safely through immutable query copies.\n *\n * ## Example Optimizations\n *\n * ### Basic Query with Joins\n * **Original Query:**\n * ```javascript\n * query\n * .from({ users: usersCollection })\n * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id))\n * .where(({users}) => eq(users.department_id, 1))\n * .where(({posts}) => gt(posts.views, 100))\n * .where(({users, posts}) => eq(users.id, posts.author_id))\n * ```\n *\n * **Optimized Query:**\n * ```javascript\n * query\n * .from({\n * users: subquery\n * .from({ users: usersCollection })\n * .where(({users}) => eq(users.department_id, 1))\n * })\n * .join({\n * posts: subquery\n * .from({ posts: postsCollection })\n * .where(({posts}) => gt(posts.views, 100))\n * }, ({users, posts}) => eq(users.id, posts.user_id))\n * .where(({users, posts}) => eq(users.id, posts.author_id))\n * ```\n *\n * ### Query with Aggregates (Now Optimizable!)\n * **Original Query:**\n * ```javascript\n * query\n * .from({ users: usersCollection })\n * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id))\n * .where(({users}) => eq(users.department_id, 1))\n * .groupBy(['users.department_id'])\n * .select({ count: agg('count', '*') })\n * ```\n *\n * **Optimized Query:**\n * ```javascript\n * query\n * .from({\n * users: subquery\n * .from({ users: usersCollection })\n * .where(({users}) => eq(users.department_id, 1))\n * })\n * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id))\n * .groupBy(['users.department_id'])\n * .select({ count: agg('count', '*') })\n * ```\n *\n * ## Benefits\n *\n * - **Reduced Data Processing**: Filters applied before joins reduce intermediate result size\n * - **Better Performance**: Smaller datasets lead to faster query execution\n * - **Automatic Optimization**: No manual query rewriting required\n * - **Preserves Semantics**: Optimized queries return identical results\n * - **Safe by Design**: Comprehensive checks prevent semantic-breaking optimizations\n *\n * ## Integration\n *\n * The optimizer is automatically called during query compilation before the IR is\n * transformed into a D2Mini pipeline.\n */\n\nimport { deepEquals } from \"../utils.js\"\nimport { CannotCombineEmptyExpressionListError } from \"../errors.js\"\nimport {\n CollectionRef as CollectionRefClass,\n Func,\n PropRef,\n QueryRef as QueryRefClass,\n createResidualWhere,\n getWhereExpression,\n isResidualWhere,\n} from \"./ir.js\"\nimport { isConvertibleToCollectionFilter } from \"./compiler/expressions.js\"\nimport type { BasicExpression, From, QueryIR, Select, Where } from \"./ir.js\"\n\n/**\n * Represents a WHERE clause after source analysis\n */\nexport interface AnalyzedWhereClause {\n /** The WHERE expression */\n expression: BasicExpression<boolean>\n /** Set of table/source aliases that this WHERE clause touches */\n touchedSources: Set<string>\n /** Whether this clause contains namespace-only references that prevent pushdown */\n hasNamespaceOnlyRef: boolean\n}\n\n/**\n * Represents WHERE clauses grouped by the sources they touch\n */\nexport interface GroupedWhereClauses {\n /** WHERE clauses that touch only a single source, grouped by source alias */\n singleSource: Map<string, BasicExpression<boolean>>\n /** WHERE clauses that touch multiple sources, combined into one expression */\n multiSource?: BasicExpression<boolean>\n}\n\n/**\n * Result of query optimization including both the optimized query and collection-specific WHERE clauses\n */\nexport interface OptimizationResult {\n /** The optimized query with WHERE clauses potentially moved to subqueries */\n optimizedQuery: QueryIR\n /** Map of collection aliases to their extracted WHERE clauses for index optimization */\n collectionWhereClauses: Map<string, BasicExpression<boolean>>\n}\n\n/**\n * Main query optimizer entry point that lifts WHERE clauses into subqueries.\n *\n * This function implements multi-level predicate pushdown optimization by recursively\n * moving WHERE clauses through nested subqueries to get them as close to the data\n * sources as possible, then removing redundant subqueries.\n *\n * @param query - The QueryIR to optimize\n * @returns An OptimizationResult with the optimized query and collection WHERE clause mapping\n *\n * @example\n * ```typescript\n * const originalQuery = {\n * from: new CollectionRef(users, 'u'),\n * join: [{ from: new CollectionRef(posts, 'p'), ... }],\n * where: [eq(u.dept_id, 1), gt(p.views, 100)]\n * }\n *\n * const { optimizedQuery, collectionWhereClauses } = optimizeQuery(originalQuery)\n * // Result: Single-source clauses moved to deepest possible subqueries\n * // collectionWhereClauses: Map { 'u' => eq(u.dept_id, 1), 'p' => gt(p.views, 100) }\n * ```\n */\nexport function optimizeQuery(query: QueryIR): OptimizationResult {\n // First, extract collection WHERE clauses before optimization\n const collectionWhereClauses = extractCollectionWhereClauses(query)\n\n // Apply multi-level predicate pushdown with iterative convergence\n let optimized = query\n let previousOptimized: QueryIR | undefined\n let iterations = 0\n const maxIterations = 10 // Prevent infinite loops\n\n // Keep optimizing until no more changes occur or max iterations reached\n while (\n iterations < maxIterations &&\n !deepEquals(optimized, previousOptimized)\n ) {\n previousOptimized = optimized\n optimized = applyRecursiveOptimization(optimized)\n iterations++\n }\n\n // Remove redundant subqueries\n const cleaned = removeRedundantSubqueries(optimized)\n\n return {\n optimizedQuery: cleaned,\n collectionWhereClauses,\n }\n}\n\n/**\n * Extracts collection-specific WHERE clauses from a query for index optimization.\n * This analyzes the original query to identify WHERE clauses that can be pushed down\n * to specific collections, but only for simple queries without joins.\n *\n * @param query - The original QueryIR to analyze\n * @returns Map of collection aliases to their WHERE clauses\n */\nfunction extractCollectionWhereClauses(\n query: QueryIR\n): Map<string, BasicExpression<boolean>> {\n const collectionWhereClauses = new Map<string, BasicExpression<boolean>>()\n\n // Only analyze queries that have WHERE clauses\n if (!query.where || query.where.length === 0) {\n return collectionWhereClauses\n }\n\n // Split all AND clauses at the root level for granular analysis\n const splitWhereClauses = splitAndClauses(query.where)\n\n // Analyze each WHERE clause to determine which sources it touches\n const analyzedClauses = splitWhereClauses.map((clause) =>\n analyzeWhereClause(clause)\n )\n\n // Group clauses by single-source vs multi-source\n const groupedClauses = groupWhereClauses(analyzedClauses)\n\n // Only include single-source clauses that reference collections directly\n // and can be converted to BasicExpression format for collection indexes\n for (const [sourceAlias, whereClause] of groupedClauses.singleSource) {\n // Check if this source alias corresponds to a collection reference\n if (isCollectionReference(query, sourceAlias)) {\n // Check if the WHERE clause can be converted to collection-compatible format\n if (isConvertibleToCollectionFilter(whereClause)) {\n collectionWhereClauses.set(sourceAlias, whereClause)\n }\n }\n }\n\n return collectionWhereClauses\n}\n\n/**\n * Determines if a source alias refers to a collection reference (not a subquery).\n * This is used to identify WHERE clauses that can be pushed down to collection subscriptions.\n *\n * @param query - The query to analyze\n * @param sourceAlias - The source alias to check\n * @returns True if the alias refers to a collection reference\n */\nfunction isCollectionReference(query: QueryIR, sourceAlias: string): boolean {\n // Check the FROM clause\n if (query.from.alias === sourceAlias) {\n return query.from.type === `collectionRef`\n }\n\n // Check JOIN clauses\n if (query.join) {\n for (const joinClause of query.join) {\n if (joinClause.from.alias === sourceAlias) {\n return joinClause.from.type === `collectionRef`\n }\n }\n }\n\n return false\n}\n\n/**\n * Applies recursive predicate pushdown optimization.\n *\n * @param query - The QueryIR to optimize\n * @returns A new QueryIR with optimizations applied\n */\nfunction applyRecursiveOptimization(query: QueryIR): QueryIR {\n // First, recursively optimize any existing subqueries\n const subqueriesOptimized = {\n ...query,\n from:\n query.from.type === `queryRef`\n ? new QueryRefClass(\n applyRecursiveOptimization(query.from.query),\n query.from.alias\n )\n : query.from,\n join: query.join?.map((joinClause) => ({\n ...joinClause,\n from:\n joinClause.from.type === `queryRef`\n ? new QueryRefClass(\n applyRecursiveOptimization(joinClause.from.query),\n joinClause.from.alias\n )\n : joinClause.from,\n })),\n }\n\n // Then apply single-level optimization to this query\n return applySingleLevelOptimization(subqueriesOptimized)\n}\n\n/**\n * Applies single-level predicate pushdown optimization (existing logic)\n */\nfunction applySingleLevelOptimization(query: QueryIR): QueryIR {\n // Skip optimization if no WHERE clauses exist\n if (!query.where || query.where.length === 0) {\n return query\n }\n\n // Skip optimization if there are no joins - predicate pushdown only benefits joins\n // Single-table queries don't benefit from this optimization\n if (!query.join || query.join.length === 0) {\n return query\n }\n\n // Filter out residual WHERE clauses to prevent them from being optimized again\n const nonResidualWhereClauses = query.where.filter(\n (where) => !isResidualWhere(where)\n )\n\n // Step 1: Split all AND clauses at the root level for granular optimization\n const splitWhereClauses = splitAndClauses(nonResidualWhereClauses)\n\n // Step 2: Analyze each WHERE clause to determine which sources it touches\n const analyzedClauses = splitWhereClauses.map((clause) =>\n analyzeWhereClause(clause)\n )\n\n // Step 3: Group clauses by single-source vs multi-source\n const groupedClauses = groupWhereClauses(analyzedClauses)\n\n // Step 4: Apply optimizations by lifting single-source clauses into subqueries\n const optimizedQuery = applyOptimizations(query, groupedClauses)\n\n // Add back any residual WHERE clauses that were filtered out\n const residualWhereClauses = query.where.filter((where) =>\n isResidualWhere(where)\n )\n if (residualWhereClauses.length > 0) {\n optimizedQuery.where = [\n ...(optimizedQuery.where || []),\n ...residualWhereClauses,\n ]\n }\n\n return optimizedQuery\n}\n\n/**\n * Removes redundant subqueries that don't add value.\n * A subquery is redundant if it only wraps another query without adding\n * WHERE, SELECT, GROUP BY, HAVING, ORDER BY, or LIMIT/OFFSET clauses.\n *\n * @param query - The QueryIR to process\n * @returns A new QueryIR with redundant subqueries removed\n */\nfunction removeRedundantSubqueries(query: QueryIR): QueryIR {\n return {\n ...query,\n from: removeRedundantFromClause(query.from),\n join: query.join?.map((joinClause) => ({\n ...joinClause,\n from: removeRedundantFromClause(joinClause.from),\n })),\n }\n}\n\n/**\n * Removes redundant subqueries from a FROM clause.\n *\n * @param from - The FROM clause to process\n * @returns A FROM clause with redundant subqueries removed\n */\nfunction removeRedundantFromClause(from: From): From {\n if (from.type === `collectionRef`) {\n return from\n }\n\n const processedQuery = removeRedundantSubqueries(from.query)\n\n // Check if this subquery is redundant\n if (isRedundantSubquery(processedQuery)) {\n // Return the inner query's FROM clause with this alias\n const innerFrom = removeRedundantFromClause(processedQuery.from)\n if (innerFrom.type === `collectionRef`) {\n return new CollectionRefClass(innerFrom.collection, from.alias)\n } else {\n return new QueryRefClass(innerFrom.query, from.alias)\n }\n }\n\n return new QueryRefClass(processedQuery, from.alias)\n}\n\n/**\n * Determines if a subquery is redundant (adds no value).\n *\n * @param query - The query to check\n * @returns True if the query is redundant and can be removed\n */\nfunction isRedundantSubquery(query: QueryIR): boolean {\n return (\n (!query.where || query.where.length === 0) &&\n !query.select &&\n (!query.groupBy || query.groupBy.length === 0) &&\n (!query.having || query.having.length === 0) &&\n (!query.orderBy || query.orderBy.length === 0) &&\n (!query.join || query.join.length === 0) &&\n query.limit === undefined &&\n query.offset === undefined &&\n !query.fnSelect &&\n (!query.fnWhere || query.fnWhere.length === 0) &&\n (!query.fnHaving || query.fnHaving.length === 0)\n )\n}\n\n/**\n * Step 1: Split all AND clauses recursively into separate WHERE clauses.\n *\n * This enables more granular optimization by treating each condition independently.\n * OR clauses are preserved as they cannot be split without changing query semantics.\n *\n * @param whereClauses - Array of WHERE expressions to split\n * @returns Flattened array with AND clauses split into separate expressions\n *\n * @example\n * ```typescript\n * // Input: [and(eq(a, 1), gt(b, 2)), eq(c, 3)]\n * // Output: [eq(a, 1), gt(b, 2), eq(c, 3)]\n * ```\n */\nfunction splitAndClauses(\n whereClauses: Array<Where>\n): Array<BasicExpression<boolean>> {\n const result: Array<BasicExpression<boolean>> = []\n\n for (const whereClause of whereClauses) {\n const clause = getWhereExpression(whereClause)\n result.push(...splitAndClausesRecursive(clause))\n }\n\n return result\n}\n\n// Helper function for recursive splitting of BasicExpression arrays\nfunction splitAndClausesRecursive(\n clause: BasicExpression<boolean>\n): Array<BasicExpression<boolean>> {\n if (clause.type === `func` && clause.name === `and`) {\n // Recursively split nested AND clauses to handle complex expressions\n const result: Array<BasicExpression<boolean>> = []\n for (const arg of clause.args as Array<BasicExpression<boolean>>) {\n result.push(...splitAndClausesRecursive(arg))\n }\n return result\n } else {\n // Preserve non-AND clauses as-is (including OR clauses)\n return [clause]\n }\n}\n\n/**\n * Step 2: Analyze which table sources a WHERE clause touches.\n *\n * This determines whether a clause can be pushed down to a specific table\n * or must remain in the main query (for multi-source clauses like join conditions).\n *\n * Special handling for namespace-only references in outer joins:\n * WHERE clauses that reference only a table namespace (e.g., isUndefined(special), eq(special, value))\n * rather than specific properties (e.g., isUndefined(special.id), eq(special.id, value)) are treated as\n * multi-source to prevent incorrect predicate pushdown that would change join semantics.\n *\n * @param clause - The WHERE expression to analyze\n * @returns Analysis result with the expression and touched source aliases\n *\n * @example\n * ```typescript\n * // eq(users.department_id, 1) -> touches ['users'], hasNamespaceOnlyRef: false\n * // eq(users.id, posts.user_id) -> touches ['users', 'posts'], hasNamespaceOnlyRef: false\n * // isUndefined(special) -> touches ['special'], hasNamespaceOnlyRef: true (prevents pushdown)\n * // eq(special, someValue) -> touches ['special'], hasNamespaceOnlyRef: true (prevents pushdown)\n * // isUndefined(special.id) -> touches ['special'], hasNamespaceOnlyRef: false (allows pushdown)\n * // eq(special.id, 5) -> touches ['special'], hasNamespaceOnlyRef: false (allows pushdown)\n * ```\n */\nfunction analyzeWhereClause(\n clause: BasicExpression<boolean>\n): AnalyzedWhereClause {\n // Track which table aliases this WHERE clause touches\n const touchedSources = new Set<string>()\n // Track whether this clause contains namespace-only references that prevent pushdown\n let hasNamespaceOnlyRef = false\n\n /**\n * Recursively collect all table aliases referenced in an expression\n */\n function collectSources(expr: BasicExpression | any): void {\n switch (expr.type) {\n case `ref`:\n // PropRef path has the table alias as the first element\n if (expr.path && expr.path.length > 0) {\n const firstElement = expr.path[0]\n if (firstElement) {\n touchedSources.add(firstElement)\n\n // If the path has only one element (just the namespace),\n // this is a namespace-only reference that should not be pushed down\n // This applies to ANY function, not just existence-checking functions\n if (expr.path.length === 1) {\n hasNamespaceOnlyRef = true\n }\n }\n }\n break\n case `func`:\n // Recursively analyze function arguments (e.g., eq, gt, and, or)\n if (expr.args) {\n expr.args.forEach(collectSources)\n }\n break\n case `val`:\n // Values don't reference any sources\n break\n case `agg`:\n // Aggregates can reference sources in their arguments\n if (expr.args) {\n expr.args.forEach(collectSources)\n }\n break\n }\n }\n\n collectSources(clause)\n\n return {\n expression: clause,\n touchedSources,\n hasNamespaceOnlyRef,\n }\n}\n\n/**\n * Step 3: Group WHERE clauses by the sources they touch.\n *\n * Single-source clauses can be pushed down to subqueries for optimization.\n * Multi-source clauses must remain in the main query to preserve join semantics.\n *\n * @param analyzedClauses - Array of analyzed WHERE clauses\n * @returns Grouped clauses ready for optimization\n */\nfunction groupWhereClauses(\n analyzedClauses: Array<AnalyzedWhereClause>\n): GroupedWhereClauses {\n const singleSource = new Map<string, Array<BasicExpression<boolean>>>()\n const multiSource: Array<BasicExpression<boolean>> = []\n\n // Categorize each clause based on how many sources it touches\n for (const clause of analyzedClauses) {\n if (clause.touchedSources.size === 1 && !clause.hasNamespaceOnlyRef) {\n // Single source clause without namespace-only references - can be optimized\n const source = Array.from(clause.touchedSources)[0]!\n if (!singleSource.has(source)) {\n singleSource.set(source, [])\n }\n singleSource.get(source)!.push(clause.expression)\n } else if (clause.touchedSources.size > 1 || clause.hasNamespaceOnlyRef) {\n // Multi-source clause or namespace-only reference - must stay in main query\n multiSource.push(clause.expression)\n }\n // Skip clauses that touch no sources (constants) - they don't need optimization\n }\n\n // Combine multiple clauses for each source with AND\n const combinedSingleSource = new Map<string, BasicExpression<boolean>>()\n for (const [source, clauses] of singleSource) {\n combinedSingleSource.set(source, combineWithAnd(clauses))\n }\n\n // Combine multi-source clauses with AND\n const combinedMultiSource =\n multiSource.length > 0 ? combineWithAnd(multiSource) : undefined\n\n return {\n singleSource: combinedSingleSource,\n multiSource: combinedMultiSource,\n }\n}\n\n/**\n * Step 4: Apply optimizations by lifting single-source clauses into subqueries.\n *\n * Creates a new QueryIR with single-source WHERE clauses moved to subqueries\n * that wrap the original table references. This ensures immutability and prevents\n * infinite recursion issues.\n *\n * @param query - Original QueryIR to optimize\n * @param groupedClauses - WHERE clauses grouped by optimization strategy\n * @returns New QueryIR with optimizations applied\n */\nfunction applyOptimizations(\n query: QueryIR,\n groupedClauses: GroupedWhereClauses\n): QueryIR {\n // Track which single-source clauses were actually optimized\n const actuallyOptimized = new Set<string>()\n\n // Optimize the main FROM clause and track what was optimized\n const optimizedFrom = optimizeFromWithTracking(\n query.from,\n groupedClauses.singleSource,\n actuallyOptimized\n )\n\n // Optimize JOIN clauses and track what was optimized\n const optimizedJoins = query.join\n ? query.join.map((joinClause) => ({\n ...joinClause,\n from: optimizeFromWithTracking(\n joinClause.from,\n groupedClauses.singleSource,\n actuallyOptimized\n ),\n }))\n : undefined\n\n // Build the remaining WHERE clauses: multi-source + residual single-source clauses\n const remainingWhereClauses: Array<Where> = []\n\n // Add multi-source clauses\n if (groupedClauses.multiSource) {\n remainingWhereClauses.push(groupedClauses.multiSource)\n }\n\n // Determine if we need residual clauses (when query has outer JOINs)\n const hasOuterJoins =\n query.join &&\n query.join.some(\n (join) =>\n join.type === `left` || join.type === `right` || join.type === `full`\n )\n\n // Add single-source clauses\n for (const [source, clause] of groupedClauses.singleSource) {\n if (!actuallyOptimized.has(source)) {\n // Wasn't optimized at all - keep as regular WHERE clause\n remainingWhereClauses.push(clause)\n } else if (hasOuterJoins) {\n // Was optimized AND query has outer JOINs - keep as residual WHERE clause\n remainingWhereClauses.push(createResidualWhere(clause))\n }\n // If optimized and no outer JOINs - don't keep (original behavior)\n }\n\n // Create a completely new query object to ensure immutability\n const optimizedQuery: QueryIR = {\n // Copy all non-optimized fields as-is\n select: query.select,\n groupBy: query.groupBy ? [...query.groupBy] : undefined,\n having: query.having ? [...query.having] : undefined,\n orderBy: query.orderBy ? [...query.orderBy] : undefined,\n limit: query.limit,\n offset: query.offset,\n distinct: query.distinct,\n fnSelect: query.fnSelect,\n fnWhere: query.fnWhere ? [...query.fnWhere] : undefined,\n fnHaving: query.fnHaving ? [...query.fnHaving] : undefined,\n\n // Use the optimized FROM and JOIN clauses\n from: optimizedFrom,\n join: optimizedJoins,\n\n // Only include WHERE clauses that weren't successfully optimized\n where: remainingWhereClauses.length > 0 ? remainingWhereClauses : [],\n }\n\n return optimizedQuery\n}\n\n/**\n * Helper function to create a deep copy of a QueryIR object for immutability.\n *\n * This ensures that all optimizations create new objects rather than modifying\n * existing ones, preventing infinite recursion and shared reference issues.\n *\n * @param query - QueryIR to deep copy\n * @returns New QueryIR object with all nested objects copied\n */\nfunction deepCopyQuery(query: QueryIR): QueryIR {\n return {\n // Recursively copy the FROM clause\n from:\n query.from.type === `collectionRef`\n ? new CollectionRefClass(query.from.collection, query.from.alias)\n : new QueryRefClass(deepCopyQuery(query.from.query), query.from.alias),\n\n // Copy all other fields, creating new arrays where necessary\n select: query.select,\n join: query.join\n ? query.join.map((joinClause) => ({\n type: joinClause.type,\n left: joinClause.left,\n right: joinClause.right,\n from:\n joinClause.from.type === `collectionRef`\n ? new CollectionRefClass(\n joinClause.from.collection,\n joinClause.from.alias\n )\n : new QueryRefClass(\n deepCopyQuery(joinClause.from.query),\n joinClause.from.alias\n ),\n }))\n : undefined,\n where: query.where ? [...query.where] : undefined,\n groupBy: query.groupBy ? [...query.groupBy] : undefined,\n having: query.having ? [...query.having] : undefined,\n orderBy: query.orderBy ? [...query.orderBy] : undefined,\n limit: query.limit,\n offset: query.offset,\n fnSelect: query.fnSelect,\n fnWhere: query.fnWhere ? [...query.fnWhere] : undefined,\n fnHaving: query.fnHaving ? [...query.fnHaving] : undefined,\n }\n}\n\n/**\n * Helper function to optimize a FROM clause while tracking what was actually optimized.\n *\n * @param from - FROM clause to optimize\n * @param singleSourceClauses - Map of source aliases to their WHERE clauses\n * @param actuallyOptimized - Set to track which sources were actually optimized\n * @returns New FROM clause, potentially wrapped in a subquery\n */\nfunction optimizeFromWithTracking(\n from: From,\n singleSourceClauses: Map<string, BasicExpression<boolean>>,\n actuallyOptimized: Set<string>\n): From {\n const whereClause = singleSourceClauses.get(from.alias)\n\n if (!whereClause) {\n // No optimization needed, but return a copy to maintain immutability\n if (from.type === `collectionRef`) {\n return new CollectionRefClass(from.collection, from.alias)\n }\n // Must be queryRef due to type system\n return new QueryRefClass(deepCopyQuery(from.query), from.alias)\n }\n\n if (from.type === `collectionRef`) {\n // Create a new subquery with the WHERE clause for the collection\n // This is always safe since we're creating a new subquery\n const subQuery: QueryIR = {\n from: new CollectionRefClass(from.collection, from.alias),\n where: [whereClause],\n }\n actuallyOptimized.add(from.alias) // Mark as successfully optimized\n return new QueryRefClass(subQuery, from.alias)\n }\n\n // Must be queryRef due to type system\n\n // SAFETY CHECK: Only check safety when pushing WHERE clauses into existing subqueries\n // We need to be careful about pushing WHERE clauses into subqueries that already have\n // aggregates, HAVING, or ORDER BY + LIMIT since that could change their semantics\n if (!isSafeToPushIntoExistingSubquery(from.query, whereClause, from.alias)) {\n // Return a copy without optimization to maintain immutability\n // Do NOT mark as optimized since we didn't actually optimize it\n return new QueryRefClass(deepCopyQuery(from.query), from.alias)\n }\n\n // Add the WHERE clause to the existing subquery\n // Create a deep copy to ensure immutability\n const existingWhere = from.query.where || []\n const optimizedSubQuery: QueryIR = {\n ...deepCopyQuery(from.query),\n where: [...existingWhere, whereClause],\n }\n actuallyOptimized.add(from.alias) // Mark as successfully optimized\n return new QueryRefClass(optimizedSubQuery, from.alias)\n}\n\nfunction unsafeSelect(\n query: QueryIR,\n whereClause: BasicExpression<boolean>,\n outerAlias: string\n): boolean {\n if (!query.select) return false\n\n return (\n selectHasAggregates(query.select) ||\n whereReferencesComputedSelectFields(query.select, whereClause, outerAlias)\n )\n}\n\nfunction unsafeGroupBy(query: QueryIR) {\n return query.groupBy && query.groupBy.length > 0\n}\n\nfunction unsafeHaving(query: QueryIR) {\n return query.having && query.having.length > 0\n}\n\nfunction unsafeOrderBy(query: QueryIR) {\n return (\n query.orderBy &&\n query.orderBy.length > 0 &&\n (query.limit !== undefined || query.offset !== undefined)\n )\n}\n\nfunction unsafeFnSelect(query: QueryIR) {\n return (\n query.fnSelect ||\n (query.fnWhere && query.fnWhere.length > 0) ||\n (query.fnHaving && query.fnHaving.length > 0)\n )\n}\n\nfunction isSafeToPushIntoExistingSubquery(\n query: QueryIR,\n whereClause: BasicExpression<boolean>,\n outerAlias: string\n): boolean {\n return !(\n unsafeSelect(query, whereClause, outerAlias) ||\n unsafeGroupBy(query) ||\n unsafeHaving(query) ||\n unsafeOrderBy(query) ||\n unsafeFnSelect(query)\n )\n}\n\n/**\n * Detects whether a SELECT projection contains any aggregate expressions.\n * Recursively traverses nested select objects.\n *\n * @param select - The SELECT object from the IR\n * @returns True if any field is an aggregate, false otherwise\n */\nfunction selectHasAggregates(select: Select): boolean {\n for (const value of Object.values(select)) {\n if (typeof value === `object`) {\n const v: any = value\n if (v.type === `agg`) return true\n if (!(`type` in v)) {\n if (selectHasAggregates(v as unknown as Select)) return true\n }\n }\n }\n return false\n}\n\n/**\n * Recursively collects all PropRef references from an expression.\n *\n * @param expr - The expression to traverse\n * @returns Array of PropRef references found in the expression\n */\nfunction collectRefs(expr: any): Array<PropRef> {\n const refs: Array<PropRef> = []\n\n if (expr == null || typeof expr !== `object`) return refs\n\n switch (expr.type) {\n case `ref`:\n refs.push(expr as PropRef)\n break\n case `func`:\n case `agg`:\n for (const arg of expr.args ?? []) {\n refs.push(...collectRefs(arg))\n }\n break\n default:\n break\n }\n\n return refs\n}\n\n/**\n * Determines whether the provided WHERE clause references fields that are\n * computed by a subquery SELECT rather than pass-through properties.\n *\n * If true, pushing the WHERE clause into the subquery could change semantics\n * (since computed fields do not necessarily exist at the subquery input level),\n * so predicate pushdown must be avoided.\n *\n * @param select - The subquery SELECT map\n * @param whereClause - The WHERE expression to analyze\n * @param outerAlias - The alias of the subquery in the outer query\n * @returns True if WHERE references computed fields, otherwise false\n */\nfunction whereReferencesComputedSelectFields(\n select: Select,\n whereClause: BasicExpression<boolean>,\n outerAlias: string\n): boolean {\n // Build a set of computed field names at the top-level of the subquery output\n const computed = new Set<string>()\n for (const [key, value] of Object.entries(select)) {\n if (key.startsWith(`__SPREAD_SENTINEL__`)) continue\n if (value instanceof PropRef) continue\n // Nested object or non-PropRef expression counts as computed\n computed.add(key)\n }\n\n const refs = collectRefs(whereClause)\n\n for (const ref of refs) {\n const path = (ref as any).path as Array<string>\n if (!Array.isArray(path) || path.length < 2) continue\n const alias = path[0]\n const field = path[1] as string\n if (alias !== outerAlias) continue\n if (computed.has(field)) return true\n }\n return false\n}\n\n/**\n * Helper function to combine multiple expressions with AND.\n *\n * If there's only one expression, it's returned as-is.\n * If there are multiple expressions, they're combined with an AND function.\n *\n * @param expressions - Array of expressions to combine\n * @returns Single expression representing the AND combination\n * @throws Error if the expressions array is empty\n */\nfunction combineWithAnd(\n expressions: Array<BasicExpression<boolean>>\n): BasicExpression<boolean> {\n if (expressions.length === 0) {\n throw new CannotCombineEmptyExpressionListError()\n }\n\n if (expressions.length === 1) {\n return expressions[0]!\n }\n\n // Create an AND function with all expressions as arguments\n return new Func(`and`, expressions)\n}\n"],"names":["QueryRefClass","CollectionRefClass"],"mappings":"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"}
1
+ {"version":3,"file":"optimizer.js","sources":["../../../src/query/optimizer.ts"],"sourcesContent":["/**\n * # Query Optimizer\n *\n * The query optimizer improves query performance by implementing predicate pushdown optimization.\n * It rewrites the intermediate representation (IR) to push WHERE clauses as close to the data\n * source as possible, reducing the amount of data processed during joins.\n *\n * ## How It Works\n *\n * The optimizer follows a 4-step process:\n *\n * ### 1. AND Clause Splitting\n * Splits AND clauses at the root level into separate WHERE clauses for granular optimization.\n * ```javascript\n * // Before: WHERE and(eq(users.department_id, 1), gt(users.age, 25))\n * // After: WHERE eq(users.department_id, 1) + WHERE gt(users.age, 25)\n * ```\n *\n * ### 2. Source Analysis\n * Analyzes each WHERE clause to determine which table sources it references:\n * - Single-source clauses: Touch only one table (e.g., `users.department_id = 1`)\n * - Multi-source clauses: Touch multiple tables (e.g., `users.id = posts.user_id`)\n *\n * ### 3. Clause Grouping\n * Groups WHERE clauses by the sources they touch:\n * - Single-source clauses are grouped by their respective table\n * - Multi-source clauses are combined for the main query\n *\n * ### 4. Subquery Creation\n * Lifts single-source WHERE clauses into subqueries that wrap the original table references.\n *\n * ## Safety & Edge Cases\n *\n * The optimizer includes targeted safety checks to prevent predicate pushdown when it could\n * break query semantics:\n *\n * ### Always Safe Operations\n * - **Creating new subqueries**: Wrapping collection references in subqueries with WHERE clauses\n * - **Main query optimizations**: Moving single-source WHERE clauses from main query to subqueries\n * - **Queries with aggregates/ORDER BY/HAVING**: Can still create new filtered subqueries\n *\n * ### Unsafe Operations (blocked by safety checks)\n * Pushing WHERE clauses **into existing subqueries** that have:\n * - **Aggregates**: GROUP BY, HAVING, or aggregate functions in SELECT (would change aggregation)\n * - **Ordering + Limits**: ORDER BY combined with LIMIT/OFFSET (would change result set)\n * - **Functional Operations**: fnSelect, fnWhere, fnHaving (potential side effects)\n *\n * ### Residual WHERE Clauses\n * For outer joins (LEFT, RIGHT, FULL), WHERE clauses are copied to subqueries for optimization\n * but also kept as \"residual\" clauses in the main query to preserve semantics. This ensures\n * that NULL values from outer joins are properly filtered according to SQL standards.\n *\n * The optimizer tracks which clauses were actually optimized and only removes those from the\n * main query. Subquery reuse is handled safely through immutable query copies.\n *\n * ## Example Optimizations\n *\n * ### Basic Query with Joins\n * **Original Query:**\n * ```javascript\n * query\n * .from({ users: usersCollection })\n * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id))\n * .where(({users}) => eq(users.department_id, 1))\n * .where(({posts}) => gt(posts.views, 100))\n * .where(({users, posts}) => eq(users.id, posts.author_id))\n * ```\n *\n * **Optimized Query:**\n * ```javascript\n * query\n * .from({\n * users: subquery\n * .from({ users: usersCollection })\n * .where(({users}) => eq(users.department_id, 1))\n * })\n * .join({\n * posts: subquery\n * .from({ posts: postsCollection })\n * .where(({posts}) => gt(posts.views, 100))\n * }, ({users, posts}) => eq(users.id, posts.user_id))\n * .where(({users, posts}) => eq(users.id, posts.author_id))\n * ```\n *\n * ### Query with Aggregates (Now Optimizable!)\n * **Original Query:**\n * ```javascript\n * query\n * .from({ users: usersCollection })\n * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id))\n * .where(({users}) => eq(users.department_id, 1))\n * .groupBy(['users.department_id'])\n * .select({ count: agg('count', '*') })\n * ```\n *\n * **Optimized Query:**\n * ```javascript\n * query\n * .from({\n * users: subquery\n * .from({ users: usersCollection })\n * .where(({users}) => eq(users.department_id, 1))\n * })\n * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id))\n * .groupBy(['users.department_id'])\n * .select({ count: agg('count', '*') })\n * ```\n *\n * ## Benefits\n *\n * - **Reduced Data Processing**: Filters applied before joins reduce intermediate result size\n * - **Better Performance**: Smaller datasets lead to faster query execution\n * - **Automatic Optimization**: No manual query rewriting required\n * - **Preserves Semantics**: Optimized queries return identical results\n * - **Safe by Design**: Comprehensive checks prevent semantic-breaking optimizations\n *\n * ## Integration\n *\n * The optimizer is automatically called during query compilation before the IR is\n * transformed into a D2Mini pipeline.\n */\n\nimport { deepEquals } from \"../utils.js\"\nimport { CannotCombineEmptyExpressionListError } from \"../errors.js\"\nimport {\n CollectionRef as CollectionRefClass,\n Func,\n PropRef,\n QueryRef as QueryRefClass,\n createResidualWhere,\n getWhereExpression,\n isResidualWhere,\n} from \"./ir.js\"\nimport { isConvertibleToCollectionFilter } from \"./compiler/expressions.js\"\nimport type { BasicExpression, From, QueryIR, Select, Where } from \"./ir.js\"\n\n/**\n * Represents a WHERE clause after source analysis\n */\nexport interface AnalyzedWhereClause {\n /** The WHERE expression */\n expression: BasicExpression<boolean>\n /** Set of table/source aliases that this WHERE clause touches */\n touchedSources: Set<string>\n /** Whether this clause contains namespace-only references that prevent pushdown */\n hasNamespaceOnlyRef: boolean\n}\n\n/**\n * Represents WHERE clauses grouped by the sources they touch\n */\nexport interface GroupedWhereClauses {\n /** WHERE clauses that touch only a single source, grouped by source alias */\n singleSource: Map<string, BasicExpression<boolean>>\n /** WHERE clauses that touch multiple sources, combined into one expression */\n multiSource?: BasicExpression<boolean>\n}\n\n/**\n * Result of query optimization including both the optimized query and collection-specific WHERE clauses\n */\nexport interface OptimizationResult {\n /** The optimized query with WHERE clauses potentially moved to subqueries */\n optimizedQuery: QueryIR\n /** Map of collection aliases to their extracted WHERE clauses for index optimization */\n collectionWhereClauses: Map<string, BasicExpression<boolean>>\n}\n\n/**\n * Main query optimizer entry point that lifts WHERE clauses into subqueries.\n *\n * This function implements multi-level predicate pushdown optimization by recursively\n * moving WHERE clauses through nested subqueries to get them as close to the data\n * sources as possible, then removing redundant subqueries.\n *\n * @param query - The QueryIR to optimize\n * @returns An OptimizationResult with the optimized query and collection WHERE clause mapping\n *\n * @example\n * ```typescript\n * const originalQuery = {\n * from: new CollectionRef(users, 'u'),\n * join: [{ from: new CollectionRef(posts, 'p'), ... }],\n * where: [eq(u.dept_id, 1), gt(p.views, 100)]\n * }\n *\n * const { optimizedQuery, collectionWhereClauses } = optimizeQuery(originalQuery)\n * // Result: Single-source clauses moved to deepest possible subqueries\n * // collectionWhereClauses: Map { 'u' => eq(u.dept_id, 1), 'p' => gt(p.views, 100) }\n * ```\n */\nexport function optimizeQuery(query: QueryIR): OptimizationResult {\n // First, extract collection WHERE clauses before optimization\n const collectionWhereClauses = extractCollectionWhereClauses(query)\n\n // Apply multi-level predicate pushdown with iterative convergence\n let optimized = query\n let previousOptimized: QueryIR | undefined\n let iterations = 0\n const maxIterations = 10 // Prevent infinite loops\n\n // Keep optimizing until no more changes occur or max iterations reached\n while (\n iterations < maxIterations &&\n !deepEquals(optimized, previousOptimized)\n ) {\n previousOptimized = optimized\n optimized = applyRecursiveOptimization(optimized)\n iterations++\n }\n\n // Remove redundant subqueries\n const cleaned = removeRedundantSubqueries(optimized)\n\n return {\n optimizedQuery: cleaned,\n collectionWhereClauses,\n }\n}\n\n/**\n * Extracts collection-specific WHERE clauses from a query for index optimization.\n * This analyzes the original query to identify WHERE clauses that can be pushed down\n * to specific collections, but only for simple queries without joins.\n *\n * @param query - The original QueryIR to analyze\n * @returns Map of collection aliases to their WHERE clauses\n */\nfunction extractCollectionWhereClauses(\n query: QueryIR\n): Map<string, BasicExpression<boolean>> {\n const collectionWhereClauses = new Map<string, BasicExpression<boolean>>()\n\n // Only analyze queries that have WHERE clauses\n if (!query.where || query.where.length === 0) {\n return collectionWhereClauses\n }\n\n // Split all AND clauses at the root level for granular analysis\n const splitWhereClauses = splitAndClauses(query.where)\n\n // Analyze each WHERE clause to determine which sources it touches\n const analyzedClauses = splitWhereClauses.map((clause) =>\n analyzeWhereClause(clause)\n )\n\n // Group clauses by single-source vs multi-source\n const groupedClauses = groupWhereClauses(analyzedClauses)\n\n // Only include single-source clauses that reference collections directly\n // and can be converted to BasicExpression format for collection indexes\n for (const [sourceAlias, whereClause] of groupedClauses.singleSource) {\n // Check if this source alias corresponds to a collection reference\n if (isCollectionReference(query, sourceAlias)) {\n // Check if the WHERE clause can be converted to collection-compatible format\n if (isConvertibleToCollectionFilter(whereClause)) {\n collectionWhereClauses.set(sourceAlias, whereClause)\n }\n }\n }\n\n return collectionWhereClauses\n}\n\n/**\n * Determines if a source alias refers to a collection reference (not a subquery).\n * This is used to identify WHERE clauses that can be pushed down to collection subscriptions.\n *\n * @param query - The query to analyze\n * @param sourceAlias - The source alias to check\n * @returns True if the alias refers to a collection reference\n */\nfunction isCollectionReference(query: QueryIR, sourceAlias: string): boolean {\n // Check the FROM clause\n if (query.from.alias === sourceAlias) {\n return query.from.type === `collectionRef`\n }\n\n // Check JOIN clauses\n if (query.join) {\n for (const joinClause of query.join) {\n if (joinClause.from.alias === sourceAlias) {\n return joinClause.from.type === `collectionRef`\n }\n }\n }\n\n return false\n}\n\n/**\n * Applies recursive predicate pushdown optimization.\n *\n * @param query - The QueryIR to optimize\n * @returns A new QueryIR with optimizations applied\n */\nfunction applyRecursiveOptimization(query: QueryIR): QueryIR {\n // First, recursively optimize any existing subqueries\n const subqueriesOptimized = {\n ...query,\n from:\n query.from.type === `queryRef`\n ? new QueryRefClass(\n applyRecursiveOptimization(query.from.query),\n query.from.alias\n )\n : query.from,\n join: query.join?.map((joinClause) => ({\n ...joinClause,\n from:\n joinClause.from.type === `queryRef`\n ? new QueryRefClass(\n applyRecursiveOptimization(joinClause.from.query),\n joinClause.from.alias\n )\n : joinClause.from,\n })),\n }\n\n // Then apply single-level optimization to this query\n return applySingleLevelOptimization(subqueriesOptimized)\n}\n\n/**\n * Applies single-level predicate pushdown optimization (existing logic)\n */\nfunction applySingleLevelOptimization(query: QueryIR): QueryIR {\n // Skip optimization if no WHERE clauses exist\n if (!query.where || query.where.length === 0) {\n return query\n }\n\n // Skip optimization if there are no joins - predicate pushdown only benefits joins\n // Single-table queries don't benefit from this optimization\n if (!query.join || query.join.length === 0) {\n return query\n }\n\n // Filter out residual WHERE clauses to prevent them from being optimized again\n const nonResidualWhereClauses = query.where.filter(\n (where) => !isResidualWhere(where)\n )\n\n // Step 1: Split all AND clauses at the root level for granular optimization\n const splitWhereClauses = splitAndClauses(nonResidualWhereClauses)\n\n // Step 2: Analyze each WHERE clause to determine which sources it touches\n const analyzedClauses = splitWhereClauses.map((clause) =>\n analyzeWhereClause(clause)\n )\n\n // Step 3: Group clauses by single-source vs multi-source\n const groupedClauses = groupWhereClauses(analyzedClauses)\n\n // Step 4: Apply optimizations by lifting single-source clauses into subqueries\n const optimizedQuery = applyOptimizations(query, groupedClauses)\n\n // Add back any residual WHERE clauses that were filtered out\n const residualWhereClauses = query.where.filter((where) =>\n isResidualWhere(where)\n )\n if (residualWhereClauses.length > 0) {\n optimizedQuery.where = [\n ...(optimizedQuery.where || []),\n ...residualWhereClauses,\n ]\n }\n\n return optimizedQuery\n}\n\n/**\n * Removes redundant subqueries that don't add value.\n * A subquery is redundant if it only wraps another query without adding\n * WHERE, SELECT, GROUP BY, HAVING, ORDER BY, or LIMIT/OFFSET clauses.\n *\n * @param query - The QueryIR to process\n * @returns A new QueryIR with redundant subqueries removed\n */\nfunction removeRedundantSubqueries(query: QueryIR): QueryIR {\n return {\n ...query,\n from: removeRedundantFromClause(query.from),\n join: query.join?.map((joinClause) => ({\n ...joinClause,\n from: removeRedundantFromClause(joinClause.from),\n })),\n }\n}\n\n/**\n * Removes redundant subqueries from a FROM clause.\n *\n * @param from - The FROM clause to process\n * @returns A FROM clause with redundant subqueries removed\n */\nfunction removeRedundantFromClause(from: From): From {\n if (from.type === `collectionRef`) {\n return from\n }\n\n const processedQuery = removeRedundantSubqueries(from.query)\n\n // Check if this subquery is redundant\n if (isRedundantSubquery(processedQuery)) {\n // Return the inner query's FROM clause with this alias\n const innerFrom = removeRedundantFromClause(processedQuery.from)\n if (innerFrom.type === `collectionRef`) {\n return new CollectionRefClass(innerFrom.collection, from.alias)\n } else {\n return new QueryRefClass(innerFrom.query, from.alias)\n }\n }\n\n return new QueryRefClass(processedQuery, from.alias)\n}\n\n/**\n * Determines if a subquery is redundant (adds no value).\n *\n * @param query - The query to check\n * @returns True if the query is redundant and can be removed\n */\nfunction isRedundantSubquery(query: QueryIR): boolean {\n return (\n (!query.where || query.where.length === 0) &&\n !query.select &&\n (!query.groupBy || query.groupBy.length === 0) &&\n (!query.having || query.having.length === 0) &&\n (!query.orderBy || query.orderBy.length === 0) &&\n (!query.join || query.join.length === 0) &&\n query.limit === undefined &&\n query.offset === undefined &&\n !query.fnSelect &&\n (!query.fnWhere || query.fnWhere.length === 0) &&\n (!query.fnHaving || query.fnHaving.length === 0)\n )\n}\n\n/**\n * Step 1: Split all AND clauses recursively into separate WHERE clauses.\n *\n * This enables more granular optimization by treating each condition independently.\n * OR clauses are preserved as they cannot be split without changing query semantics.\n *\n * @param whereClauses - Array of WHERE expressions to split\n * @returns Flattened array with AND clauses split into separate expressions\n *\n * @example\n * ```typescript\n * // Input: [and(eq(a, 1), gt(b, 2)), eq(c, 3)]\n * // Output: [eq(a, 1), gt(b, 2), eq(c, 3)]\n * ```\n */\nfunction splitAndClauses(\n whereClauses: Array<Where>\n): Array<BasicExpression<boolean>> {\n const result: Array<BasicExpression<boolean>> = []\n\n for (const whereClause of whereClauses) {\n const clause = getWhereExpression(whereClause)\n result.push(...splitAndClausesRecursive(clause))\n }\n\n return result\n}\n\n// Helper function for recursive splitting of BasicExpression arrays\nfunction splitAndClausesRecursive(\n clause: BasicExpression<boolean>\n): Array<BasicExpression<boolean>> {\n if (clause.type === `func` && clause.name === `and`) {\n // Recursively split nested AND clauses to handle complex expressions\n const result: Array<BasicExpression<boolean>> = []\n for (const arg of clause.args as Array<BasicExpression<boolean>>) {\n result.push(...splitAndClausesRecursive(arg))\n }\n return result\n } else {\n // Preserve non-AND clauses as-is (including OR clauses)\n return [clause]\n }\n}\n\n/**\n * Step 2: Analyze which table sources a WHERE clause touches.\n *\n * This determines whether a clause can be pushed down to a specific table\n * or must remain in the main query (for multi-source clauses like join conditions).\n *\n * Special handling for namespace-only references in outer joins:\n * WHERE clauses that reference only a table namespace (e.g., isUndefined(special), eq(special, value))\n * rather than specific properties (e.g., isUndefined(special.id), eq(special.id, value)) are treated as\n * multi-source to prevent incorrect predicate pushdown that would change join semantics.\n *\n * @param clause - The WHERE expression to analyze\n * @returns Analysis result with the expression and touched source aliases\n *\n * @example\n * ```typescript\n * // eq(users.department_id, 1) -> touches ['users'], hasNamespaceOnlyRef: false\n * // eq(users.id, posts.user_id) -> touches ['users', 'posts'], hasNamespaceOnlyRef: false\n * // isUndefined(special) -> touches ['special'], hasNamespaceOnlyRef: true (prevents pushdown)\n * // eq(special, someValue) -> touches ['special'], hasNamespaceOnlyRef: true (prevents pushdown)\n * // isUndefined(special.id) -> touches ['special'], hasNamespaceOnlyRef: false (allows pushdown)\n * // eq(special.id, 5) -> touches ['special'], hasNamespaceOnlyRef: false (allows pushdown)\n * ```\n */\nfunction analyzeWhereClause(\n clause: BasicExpression<boolean>\n): AnalyzedWhereClause {\n // Track which table aliases this WHERE clause touches\n const touchedSources = new Set<string>()\n // Track whether this clause contains namespace-only references that prevent pushdown\n let hasNamespaceOnlyRef = false\n\n /**\n * Recursively collect all table aliases referenced in an expression\n */\n function collectSources(expr: BasicExpression | any): void {\n switch (expr.type) {\n case `ref`:\n // PropRef path has the table alias as the first element\n if (expr.path && expr.path.length > 0) {\n const firstElement = expr.path[0]\n if (firstElement) {\n touchedSources.add(firstElement)\n\n // If the path has only one element (just the namespace),\n // this is a namespace-only reference that should not be pushed down\n // This applies to ANY function, not just existence-checking functions\n if (expr.path.length === 1) {\n hasNamespaceOnlyRef = true\n }\n }\n }\n break\n case `func`:\n // Recursively analyze function arguments (e.g., eq, gt, and, or)\n if (expr.args) {\n expr.args.forEach(collectSources)\n }\n break\n case `val`:\n // Values don't reference any sources\n break\n case `agg`:\n // Aggregates can reference sources in their arguments\n if (expr.args) {\n expr.args.forEach(collectSources)\n }\n break\n }\n }\n\n collectSources(clause)\n\n return {\n expression: clause,\n touchedSources,\n hasNamespaceOnlyRef,\n }\n}\n\n/**\n * Step 3: Group WHERE clauses by the sources they touch.\n *\n * Single-source clauses can be pushed down to subqueries for optimization.\n * Multi-source clauses must remain in the main query to preserve join semantics.\n *\n * @param analyzedClauses - Array of analyzed WHERE clauses\n * @returns Grouped clauses ready for optimization\n */\nfunction groupWhereClauses(\n analyzedClauses: Array<AnalyzedWhereClause>\n): GroupedWhereClauses {\n const singleSource = new Map<string, Array<BasicExpression<boolean>>>()\n const multiSource: Array<BasicExpression<boolean>> = []\n\n // Categorize each clause based on how many sources it touches\n for (const clause of analyzedClauses) {\n if (clause.touchedSources.size === 1 && !clause.hasNamespaceOnlyRef) {\n // Single source clause without namespace-only references - can be optimized\n const source = Array.from(clause.touchedSources)[0]!\n if (!singleSource.has(source)) {\n singleSource.set(source, [])\n }\n singleSource.get(source)!.push(clause.expression)\n } else if (clause.touchedSources.size > 1 || clause.hasNamespaceOnlyRef) {\n // Multi-source clause or namespace-only reference - must stay in main query\n multiSource.push(clause.expression)\n }\n // Skip clauses that touch no sources (constants) - they don't need optimization\n }\n\n // Combine multiple clauses for each source with AND\n const combinedSingleSource = new Map<string, BasicExpression<boolean>>()\n for (const [source, clauses] of singleSource) {\n combinedSingleSource.set(source, combineWithAnd(clauses))\n }\n\n // Combine multi-source clauses with AND\n const combinedMultiSource =\n multiSource.length > 0 ? combineWithAnd(multiSource) : undefined\n\n return {\n singleSource: combinedSingleSource,\n multiSource: combinedMultiSource,\n }\n}\n\n/**\n * Step 4: Apply optimizations by lifting single-source clauses into subqueries.\n *\n * Creates a new QueryIR with single-source WHERE clauses moved to subqueries\n * that wrap the original table references. This ensures immutability and prevents\n * infinite recursion issues.\n *\n * @param query - Original QueryIR to optimize\n * @param groupedClauses - WHERE clauses grouped by optimization strategy\n * @returns New QueryIR with optimizations applied\n */\nfunction applyOptimizations(\n query: QueryIR,\n groupedClauses: GroupedWhereClauses\n): QueryIR {\n // Track which single-source clauses were actually optimized\n const actuallyOptimized = new Set<string>()\n\n // Optimize the main FROM clause and track what was optimized\n const optimizedFrom = optimizeFromWithTracking(\n query.from,\n groupedClauses.singleSource,\n actuallyOptimized\n )\n\n // Optimize JOIN clauses and track what was optimized\n const optimizedJoins = query.join\n ? query.join.map((joinClause) => ({\n ...joinClause,\n from: optimizeFromWithTracking(\n joinClause.from,\n groupedClauses.singleSource,\n actuallyOptimized\n ),\n }))\n : undefined\n\n // Build the remaining WHERE clauses: multi-source + residual single-source clauses\n const remainingWhereClauses: Array<Where> = []\n\n // Add multi-source clauses\n if (groupedClauses.multiSource) {\n remainingWhereClauses.push(groupedClauses.multiSource)\n }\n\n // Determine if we need residual clauses (when query has outer JOINs)\n const hasOuterJoins =\n query.join &&\n query.join.some(\n (join) =>\n join.type === `left` || join.type === `right` || join.type === `full`\n )\n\n // Add single-source clauses\n for (const [source, clause] of groupedClauses.singleSource) {\n if (!actuallyOptimized.has(source)) {\n // Wasn't optimized at all - keep as regular WHERE clause\n remainingWhereClauses.push(clause)\n } else if (hasOuterJoins) {\n // Was optimized AND query has outer JOINs - keep as residual WHERE clause\n remainingWhereClauses.push(createResidualWhere(clause))\n }\n // If optimized and no outer JOINs - don't keep (original behavior)\n }\n\n // Create a completely new query object to ensure immutability\n const optimizedQuery: QueryIR = {\n // Copy all non-optimized fields as-is\n select: query.select,\n groupBy: query.groupBy ? [...query.groupBy] : undefined,\n having: query.having ? [...query.having] : undefined,\n orderBy: query.orderBy ? [...query.orderBy] : undefined,\n limit: query.limit,\n offset: query.offset,\n distinct: query.distinct,\n fnSelect: query.fnSelect,\n fnWhere: query.fnWhere ? [...query.fnWhere] : undefined,\n fnHaving: query.fnHaving ? [...query.fnHaving] : undefined,\n\n // Use the optimized FROM and JOIN clauses\n from: optimizedFrom,\n join: optimizedJoins,\n\n // Only include WHERE clauses that weren't successfully optimized\n where: remainingWhereClauses.length > 0 ? remainingWhereClauses : [],\n }\n\n return optimizedQuery\n}\n\n/**\n * Helper function to create a deep copy of a QueryIR object for immutability.\n *\n * This ensures that all optimizations create new objects rather than modifying\n * existing ones, preventing infinite recursion and shared reference issues.\n *\n * @param query - QueryIR to deep copy\n * @returns New QueryIR object with all nested objects copied\n */\nfunction deepCopyQuery(query: QueryIR): QueryIR {\n return {\n // Recursively copy the FROM clause\n from:\n query.from.type === `collectionRef`\n ? new CollectionRefClass(query.from.collection, query.from.alias)\n : new QueryRefClass(deepCopyQuery(query.from.query), query.from.alias),\n\n // Copy all other fields, creating new arrays where necessary\n select: query.select,\n join: query.join\n ? query.join.map((joinClause) => ({\n type: joinClause.type,\n left: joinClause.left,\n right: joinClause.right,\n from:\n joinClause.from.type === `collectionRef`\n ? new CollectionRefClass(\n joinClause.from.collection,\n joinClause.from.alias\n )\n : new QueryRefClass(\n deepCopyQuery(joinClause.from.query),\n joinClause.from.alias\n ),\n }))\n : undefined,\n where: query.where ? [...query.where] : undefined,\n groupBy: query.groupBy ? [...query.groupBy] : undefined,\n having: query.having ? [...query.having] : undefined,\n orderBy: query.orderBy ? [...query.orderBy] : undefined,\n limit: query.limit,\n offset: query.offset,\n fnSelect: query.fnSelect,\n fnWhere: query.fnWhere ? [...query.fnWhere] : undefined,\n fnHaving: query.fnHaving ? [...query.fnHaving] : undefined,\n }\n}\n\n/**\n * Helper function to optimize a FROM clause while tracking what was actually optimized.\n *\n * @param from - FROM clause to optimize\n * @param singleSourceClauses - Map of source aliases to their WHERE clauses\n * @param actuallyOptimized - Set to track which sources were actually optimized\n * @returns New FROM clause, potentially wrapped in a subquery\n */\nfunction optimizeFromWithTracking(\n from: From,\n singleSourceClauses: Map<string, BasicExpression<boolean>>,\n actuallyOptimized: Set<string>\n): From {\n const whereClause = singleSourceClauses.get(from.alias)\n\n if (!whereClause) {\n // No optimization needed, but return a copy to maintain immutability\n if (from.type === `collectionRef`) {\n return new CollectionRefClass(from.collection, from.alias)\n }\n // Must be queryRef due to type system\n return new QueryRefClass(deepCopyQuery(from.query), from.alias)\n }\n\n if (from.type === `collectionRef`) {\n // Create a new subquery with the WHERE clause for the collection\n // This is always safe since we're creating a new subquery\n const subQuery: QueryIR = {\n from: new CollectionRefClass(from.collection, from.alias),\n where: [whereClause],\n }\n actuallyOptimized.add(from.alias) // Mark as successfully optimized\n return new QueryRefClass(subQuery, from.alias)\n }\n\n // Must be queryRef due to type system\n\n // SAFETY CHECK: Only check safety when pushing WHERE clauses into existing subqueries\n // We need to be careful about pushing WHERE clauses into subqueries that already have\n // aggregates, HAVING, or ORDER BY + LIMIT since that could change their semantics\n if (!isSafeToPushIntoExistingSubquery(from.query, whereClause, from.alias)) {\n // Return a copy without optimization to maintain immutability\n // Do NOT mark as optimized since we didn't actually optimize it\n return new QueryRefClass(deepCopyQuery(from.query), from.alias)\n }\n\n // Add the WHERE clause to the existing subquery\n // Create a deep copy to ensure immutability\n const existingWhere = from.query.where || []\n const optimizedSubQuery: QueryIR = {\n ...deepCopyQuery(from.query),\n where: [...existingWhere, whereClause],\n }\n actuallyOptimized.add(from.alias) // Mark as successfully optimized\n return new QueryRefClass(optimizedSubQuery, from.alias)\n}\n\nfunction unsafeSelect(\n query: QueryIR,\n whereClause: BasicExpression<boolean>,\n outerAlias: string\n): boolean {\n if (!query.select) return false\n\n return (\n selectHasAggregates(query.select) ||\n whereReferencesComputedSelectFields(query.select, whereClause, outerAlias)\n )\n}\n\nfunction unsafeGroupBy(query: QueryIR) {\n return query.groupBy && query.groupBy.length > 0\n}\n\nfunction unsafeHaving(query: QueryIR) {\n return query.having && query.having.length > 0\n}\n\nfunction unsafeOrderBy(query: QueryIR) {\n return (\n query.orderBy &&\n query.orderBy.length > 0 &&\n (query.limit !== undefined || query.offset !== undefined)\n )\n}\n\nfunction unsafeFnSelect(query: QueryIR) {\n return (\n query.fnSelect ||\n (query.fnWhere && query.fnWhere.length > 0) ||\n (query.fnHaving && query.fnHaving.length > 0)\n )\n}\n\nfunction isSafeToPushIntoExistingSubquery(\n query: QueryIR,\n whereClause: BasicExpression<boolean>,\n outerAlias: string\n): boolean {\n return !(\n unsafeSelect(query, whereClause, outerAlias) ||\n unsafeGroupBy(query) ||\n unsafeHaving(query) ||\n unsafeOrderBy(query) ||\n unsafeFnSelect(query)\n )\n}\n\n/**\n * Detects whether a SELECT projection contains any aggregate expressions.\n * Recursively traverses nested select objects.\n *\n * @param select - The SELECT object from the IR\n * @returns True if any field is an aggregate, false otherwise\n */\nfunction selectHasAggregates(select: Select): boolean {\n for (const value of Object.values(select)) {\n if (typeof value === `object`) {\n const v: any = value\n if (v.type === `agg`) return true\n if (!(`type` in v)) {\n if (selectHasAggregates(v as unknown as Select)) return true\n }\n }\n }\n return false\n}\n\n/**\n * Recursively collects all PropRef references from an expression.\n *\n * @param expr - The expression to traverse\n * @returns Array of PropRef references found in the expression\n */\nfunction collectRefs(expr: any): Array<PropRef> {\n const refs: Array<PropRef> = []\n\n if (expr == null || typeof expr !== `object`) return refs\n\n switch (expr.type) {\n case `ref`:\n refs.push(expr as PropRef)\n break\n case `func`:\n case `agg`:\n for (const arg of expr.args ?? []) {\n refs.push(...collectRefs(arg))\n }\n break\n default:\n break\n }\n\n return refs\n}\n\n/**\n * Determines whether the provided WHERE clause references fields that are\n * computed by a subquery SELECT rather than pass-through properties.\n *\n * If true, pushing the WHERE clause into the subquery could change semantics\n * (since computed fields do not necessarily exist at the subquery input level),\n * so predicate pushdown must be avoided.\n *\n * @param select - The subquery SELECT map\n * @param whereClause - The WHERE expression to analyze\n * @param outerAlias - The alias of the subquery in the outer query\n * @returns True if WHERE references computed fields, otherwise false\n */\nfunction whereReferencesComputedSelectFields(\n select: Select,\n whereClause: BasicExpression<boolean>,\n outerAlias: string\n): boolean {\n // Build a set of computed field names at the top-level of the subquery output\n const computed = new Set<string>()\n for (const [key, value] of Object.entries(select)) {\n if (key.startsWith(`__SPREAD_SENTINEL__`)) continue\n if (value instanceof PropRef) continue\n // Nested object or non-PropRef expression counts as computed\n computed.add(key)\n }\n\n const refs = collectRefs(whereClause)\n\n for (const ref of refs) {\n const path = (ref as any).path as Array<string>\n if (!Array.isArray(path) || path.length < 2) continue\n const alias = path[0]\n const field = path[1] as string\n if (alias !== outerAlias) continue\n if (computed.has(field)) return true\n }\n return false\n}\n\n/**\n * Helper function to combine multiple expressions with AND.\n *\n * If there's only one expression, it's returned as-is.\n * If there are multiple expressions, they're combined with an AND function.\n *\n * @param expressions - Array of expressions to combine\n * @returns Single expression representing the AND combination\n * @throws Error if the expressions array is empty\n */\nfunction combineWithAnd(\n expressions: Array<BasicExpression<boolean>>\n): BasicExpression<boolean> {\n if (expressions.length === 0) {\n throw new CannotCombineEmptyExpressionListError()\n }\n\n if (expressions.length === 1) {\n return expressions[0]!\n }\n\n // Create an AND function with all expressions as arguments\n return new Func(`and`, expressions)\n}\n"],"names":["QueryRefClass","CollectionRefClass"],"mappings":"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"}
@@ -222,8 +222,7 @@ class Transaction {
222
222
  * }
223
223
  */
224
224
  rollback(config) {
225
- var _a;
226
- const isSecondaryRollback = (config == null ? void 0 : config.isSecondaryRollback) ?? false;
225
+ const isSecondaryRollback = config?.isSecondaryRollback ?? false;
227
226
  if (this.state === `completed`) {
228
227
  throw new TransactionAlreadyCompletedRollbackError();
229
228
  }
@@ -235,7 +234,7 @@ class Transaction {
235
234
  t.state === `pending` && t.mutations.some((m) => mutationIds.has(m.globalKey)) && t.rollback({ isSecondaryRollback: true });
236
235
  }
237
236
  }
238
- this.isPersisted.reject((_a = this.error) == null ? void 0 : _a.error);
237
+ this.isPersisted.reject(this.error?.error);
239
238
  this.touchCollection();
240
239
  return this;
241
240
  }
@@ -1 +1 @@
1
- {"version":3,"file":"transactions.js","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport {\n MissingMutationFunctionError,\n TransactionAlreadyCompletedRollbackError,\n TransactionNotPendingCommitError,\n TransactionNotPendingMutateError,\n} from \"./errors\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\n/**\n * Merges two pending mutations for the same item within a transaction\n *\n * Merge behavior truth table:\n * - (insert, update) → insert (merge changes, keep empty original)\n * - (insert, delete) → null (cancel both mutations)\n * - (update, delete) → delete (delete dominates)\n * - (update, update) → update (replace with latest, union changes)\n * - (delete, delete) → delete (replace with latest)\n * - (insert, insert) → insert (replace with latest)\n *\n * Note: (delete, update) and (delete, insert) should never occur as the collection\n * layer prevents operations on deleted items within the same transaction.\n *\n * @param existing - The existing mutation in the transaction\n * @param incoming - The new mutation being applied\n * @returns The merged mutation, or null if both should be removed\n */\nfunction mergePendingMutations<T extends object>(\n existing: PendingMutation<T>,\n incoming: PendingMutation<T>\n): PendingMutation<T> | null {\n // Truth table implementation\n switch (`${existing.type}-${incoming.type}` as const) {\n case `insert-update`: {\n // Update after insert: keep as insert but merge changes\n // For insert-update, the key should remain the same since collections don't allow key changes\n return {\n ...existing,\n type: `insert` as const,\n original: {},\n modified: incoming.modified,\n changes: { ...existing.changes, ...incoming.changes },\n // Keep existing keys (key changes not allowed in updates)\n key: existing.key,\n globalKey: existing.globalKey,\n // Merge metadata (last-write-wins)\n metadata: incoming.metadata ?? existing.metadata,\n syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },\n // Update tracking info\n mutationId: incoming.mutationId,\n updatedAt: incoming.updatedAt,\n }\n }\n\n case `insert-delete`:\n // Delete after insert: cancel both mutations\n return null\n\n case `update-delete`:\n // Delete after update: delete dominates\n return incoming\n\n case `update-update`: {\n // Update after update: replace with latest, union changes\n return {\n ...incoming,\n // Keep original from first update\n original: existing.original,\n // Union the changes from both updates\n changes: { ...existing.changes, ...incoming.changes },\n // Merge metadata\n metadata: incoming.metadata ?? existing.metadata,\n syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },\n }\n }\n\n case `delete-delete`:\n case `insert-insert`:\n // Same type: replace with latest\n return incoming\n\n default: {\n // Exhaustiveness check\n const _exhaustive: never = `${existing.type}-${incoming.type}` as never\n throw new Error(`Unhandled mutation combination: ${_exhaustive}`)\n }\n }\n}\n\n/**\n * Creates a new transaction for grouping multiple collection operations\n * @param config - Transaction configuration with mutation function\n * @returns A new Transaction instance\n * @example\n * // Basic transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Send all mutations to API\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle transaction errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"New item\" })\n * })\n *\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async () => {\n * // API call\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later\n * await tx.commit()\n */\nexport function createTransaction<T extends object = Record<string, unknown>>(\n config: TransactionConfig<T>\n): Transaction<T> {\n const newTransaction = new Transaction<T>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\n/**\n * Gets the currently active ambient transaction, if any\n * Used internally by collection operations to join existing transactions\n * @returns The active transaction or undefined if none is active\n * @example\n * // Check if operations will join an ambient transaction\n * const ambientTx = getActiveTransaction()\n * if (ambientTx) {\n * console.log('Operations will join transaction:', ambientTx.id)\n * }\n */\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<T extends object = Record<string, unknown>> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T>>\n public isPersisted: Deferred<Transaction<T>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw new MissingMutationFunctionError()\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n\n if (newState === `completed` || newState === `failed`) {\n removeFromPendingList(this)\n }\n }\n\n /**\n * Execute collection operations within this transaction\n * @param callback - Function containing collection operations to group together\n * @returns This transaction for chaining\n * @example\n * // Group multiple operations\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * collection.delete(\"3\")\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle mutate errors\n * try {\n * tx.mutate(() => {\n * collection.insert({ id: \"invalid\" }) // This might throw\n * })\n * } catch (error) {\n * console.log('Mutation failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later when ready\n * await tx.commit()\n */\n mutate(callback: () => void): Transaction<T> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingMutateError()\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit().catch(() => {\n // Errors from autoCommit are handled via isPersisted.promise\n // This catch prevents unhandled promise rejections\n })\n }\n\n return this\n }\n\n /**\n * Apply new mutations to this transaction, intelligently merging with existing mutations\n *\n * When mutations operate on the same item (same globalKey), they are merged according to\n * the following rules:\n *\n * - **insert + update** → insert (merge changes, keep empty original)\n * - **insert + delete** → removed (mutations cancel each other out)\n * - **update + delete** → delete (delete dominates)\n * - **update + update** → update (union changes, keep first original)\n * - **same type** → replace with latest\n *\n * This merging reduces over-the-wire churn and keeps the optimistic local view\n * aligned with user intent.\n *\n * @param mutations - Array of new mutations to apply\n */\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.globalKey === newMutation.globalKey\n )\n\n if (existingIndex >= 0) {\n const existingMutation = this.mutations[existingIndex]!\n const mergeResult = mergePendingMutations(existingMutation, newMutation)\n\n if (mergeResult === null) {\n // Remove the mutation (e.g., delete after insert cancels both)\n this.mutations.splice(existingIndex, 1)\n } else {\n // Replace with merged mutation\n this.mutations[existingIndex] = mergeResult\n }\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n /**\n * Rollback the transaction and any conflicting transactions\n * @param config - Configuration for rollback behavior\n * @returns This transaction for chaining\n * @example\n * // Manual rollback\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * // Rollback if needed\n * if (shouldCancel) {\n * tx.rollback()\n * }\n *\n * @example\n * // Handle rollback cascade (automatic)\n * const tx1 = createTransaction({ mutationFn: async () => {} })\n * const tx2 = createTransaction({ mutationFn: async () => {} })\n *\n * tx1.mutate(() => collection.update(\"1\", draft => { draft.value = \"A\" }))\n * tx2.mutate(() => collection.update(\"1\", draft => { draft.value = \"B\" })) // Same item\n *\n * tx1.rollback() // This will also rollback tx2 due to conflict\n *\n * @example\n * // Handle rollback in error scenarios\n * try {\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction was rolled back:', error)\n * // Transaction automatically rolled back on mutation function failure\n * }\n */\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw new TransactionAlreadyCompletedRollbackError()\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same ids\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationIds = new Set()\n this.mutations.forEach((m) => mutationIds.add(m.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\n t.rollback({ isSecondaryRollback: true })\n }\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n for (const mutation of this.mutations) {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection._state.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection._state.pendingSyncedTransactions.length > 0) {\n mutation.collection._state.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n /**\n * Commit the transaction and execute the mutation function\n * @returns Promise that resolves to this transaction when complete\n * @example\n * // Manual commit (when autoCommit is false)\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * await tx.commit() // Manually commit\n *\n * @example\n * // Handle commit errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * await tx.commit()\n * } catch (error) {\n * console.log('Commit failed, transaction rolled back:', error)\n * }\n *\n * @example\n * // Check transaction state after commit\n * await tx.commit()\n * console.log(tx.state) // \"completed\" or \"failed\"\n */\n async commit(): Promise<Transaction<T>> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingCommitError()\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n this.isPersisted.resolve(this)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Preserve the original error for rethrowing\n const originalError =\n error instanceof Error ? error : new Error(String(error))\n\n // Update transaction with error information\n this.error = {\n message: originalError.message,\n error: originalError,\n }\n\n // rollback the transaction\n this.rollback()\n\n // Re-throw the original error to preserve identity and stack\n throw originalError\n }\n\n return this\n }\n\n /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @returns -1 if this transaction was created before the other, 1 if it was created after, 0 if they were created at the same time\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transactions.js","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport {\n MissingMutationFunctionError,\n TransactionAlreadyCompletedRollbackError,\n TransactionNotPendingCommitError,\n TransactionNotPendingMutateError,\n} from \"./errors\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\n/**\n * Merges two pending mutations for the same item within a transaction\n *\n * Merge behavior truth table:\n * - (insert, update) → insert (merge changes, keep empty original)\n * - (insert, delete) → null (cancel both mutations)\n * - (update, delete) → delete (delete dominates)\n * - (update, update) → update (replace with latest, union changes)\n * - (delete, delete) → delete (replace with latest)\n * - (insert, insert) → insert (replace with latest)\n *\n * Note: (delete, update) and (delete, insert) should never occur as the collection\n * layer prevents operations on deleted items within the same transaction.\n *\n * @param existing - The existing mutation in the transaction\n * @param incoming - The new mutation being applied\n * @returns The merged mutation, or null if both should be removed\n */\nfunction mergePendingMutations<T extends object>(\n existing: PendingMutation<T>,\n incoming: PendingMutation<T>\n): PendingMutation<T> | null {\n // Truth table implementation\n switch (`${existing.type}-${incoming.type}` as const) {\n case `insert-update`: {\n // Update after insert: keep as insert but merge changes\n // For insert-update, the key should remain the same since collections don't allow key changes\n return {\n ...existing,\n type: `insert` as const,\n original: {},\n modified: incoming.modified,\n changes: { ...existing.changes, ...incoming.changes },\n // Keep existing keys (key changes not allowed in updates)\n key: existing.key,\n globalKey: existing.globalKey,\n // Merge metadata (last-write-wins)\n metadata: incoming.metadata ?? existing.metadata,\n syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },\n // Update tracking info\n mutationId: incoming.mutationId,\n updatedAt: incoming.updatedAt,\n }\n }\n\n case `insert-delete`:\n // Delete after insert: cancel both mutations\n return null\n\n case `update-delete`:\n // Delete after update: delete dominates\n return incoming\n\n case `update-update`: {\n // Update after update: replace with latest, union changes\n return {\n ...incoming,\n // Keep original from first update\n original: existing.original,\n // Union the changes from both updates\n changes: { ...existing.changes, ...incoming.changes },\n // Merge metadata\n metadata: incoming.metadata ?? existing.metadata,\n syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },\n }\n }\n\n case `delete-delete`:\n case `insert-insert`:\n // Same type: replace with latest\n return incoming\n\n default: {\n // Exhaustiveness check\n const _exhaustive: never = `${existing.type}-${incoming.type}` as never\n throw new Error(`Unhandled mutation combination: ${_exhaustive}`)\n }\n }\n}\n\n/**\n * Creates a new transaction for grouping multiple collection operations\n * @param config - Transaction configuration with mutation function\n * @returns A new Transaction instance\n * @example\n * // Basic transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Send all mutations to API\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle transaction errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"New item\" })\n * })\n *\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async () => {\n * // API call\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later\n * await tx.commit()\n */\nexport function createTransaction<T extends object = Record<string, unknown>>(\n config: TransactionConfig<T>\n): Transaction<T> {\n const newTransaction = new Transaction<T>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\n/**\n * Gets the currently active ambient transaction, if any\n * Used internally by collection operations to join existing transactions\n * @returns The active transaction or undefined if none is active\n * @example\n * // Check if operations will join an ambient transaction\n * const ambientTx = getActiveTransaction()\n * if (ambientTx) {\n * console.log('Operations will join transaction:', ambientTx.id)\n * }\n */\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<T extends object = Record<string, unknown>> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T>>\n public isPersisted: Deferred<Transaction<T>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw new MissingMutationFunctionError()\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n\n if (newState === `completed` || newState === `failed`) {\n removeFromPendingList(this)\n }\n }\n\n /**\n * Execute collection operations within this transaction\n * @param callback - Function containing collection operations to group together\n * @returns This transaction for chaining\n * @example\n * // Group multiple operations\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * collection.delete(\"3\")\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle mutate errors\n * try {\n * tx.mutate(() => {\n * collection.insert({ id: \"invalid\" }) // This might throw\n * })\n * } catch (error) {\n * console.log('Mutation failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later when ready\n * await tx.commit()\n */\n mutate(callback: () => void): Transaction<T> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingMutateError()\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit().catch(() => {\n // Errors from autoCommit are handled via isPersisted.promise\n // This catch prevents unhandled promise rejections\n })\n }\n\n return this\n }\n\n /**\n * Apply new mutations to this transaction, intelligently merging with existing mutations\n *\n * When mutations operate on the same item (same globalKey), they are merged according to\n * the following rules:\n *\n * - **insert + update** → insert (merge changes, keep empty original)\n * - **insert + delete** → removed (mutations cancel each other out)\n * - **update + delete** → delete (delete dominates)\n * - **update + update** → update (union changes, keep first original)\n * - **same type** → replace with latest\n *\n * This merging reduces over-the-wire churn and keeps the optimistic local view\n * aligned with user intent.\n *\n * @param mutations - Array of new mutations to apply\n */\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.globalKey === newMutation.globalKey\n )\n\n if (existingIndex >= 0) {\n const existingMutation = this.mutations[existingIndex]!\n const mergeResult = mergePendingMutations(existingMutation, newMutation)\n\n if (mergeResult === null) {\n // Remove the mutation (e.g., delete after insert cancels both)\n this.mutations.splice(existingIndex, 1)\n } else {\n // Replace with merged mutation\n this.mutations[existingIndex] = mergeResult\n }\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n /**\n * Rollback the transaction and any conflicting transactions\n * @param config - Configuration for rollback behavior\n * @returns This transaction for chaining\n * @example\n * // Manual rollback\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * // Rollback if needed\n * if (shouldCancel) {\n * tx.rollback()\n * }\n *\n * @example\n * // Handle rollback cascade (automatic)\n * const tx1 = createTransaction({ mutationFn: async () => {} })\n * const tx2 = createTransaction({ mutationFn: async () => {} })\n *\n * tx1.mutate(() => collection.update(\"1\", draft => { draft.value = \"A\" }))\n * tx2.mutate(() => collection.update(\"1\", draft => { draft.value = \"B\" })) // Same item\n *\n * tx1.rollback() // This will also rollback tx2 due to conflict\n *\n * @example\n * // Handle rollback in error scenarios\n * try {\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction was rolled back:', error)\n * // Transaction automatically rolled back on mutation function failure\n * }\n */\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw new TransactionAlreadyCompletedRollbackError()\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same ids\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationIds = new Set()\n this.mutations.forEach((m) => mutationIds.add(m.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\n t.rollback({ isSecondaryRollback: true })\n }\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n for (const mutation of this.mutations) {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection._state.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection._state.pendingSyncedTransactions.length > 0) {\n mutation.collection._state.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n /**\n * Commit the transaction and execute the mutation function\n * @returns Promise that resolves to this transaction when complete\n * @example\n * // Manual commit (when autoCommit is false)\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * await tx.commit() // Manually commit\n *\n * @example\n * // Handle commit errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * await tx.commit()\n * } catch (error) {\n * console.log('Commit failed, transaction rolled back:', error)\n * }\n *\n * @example\n * // Check transaction state after commit\n * await tx.commit()\n * console.log(tx.state) // \"completed\" or \"failed\"\n */\n async commit(): Promise<Transaction<T>> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingCommitError()\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n this.isPersisted.resolve(this)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Preserve the original error for rethrowing\n const originalError =\n error instanceof Error ? error : new Error(String(error))\n\n // Update transaction with error information\n this.error = {\n message: originalError.message,\n error: originalError,\n }\n\n // rollback the transaction\n this.rollback()\n\n // Re-throw the original error to preserve identity and stack\n throw originalError\n }\n\n return this\n }\n\n /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @returns -1 if this transaction was created before the other, 1 if it was created after, 0 if they were created at the same time\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":[],"mappings":"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"}
@@ -375,6 +375,19 @@ export interface BaseCollectionConfig<T extends object = Record<string, unknown>
375
375
  export interface CollectionConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = never> extends BaseCollectionConfig<T, TKey, TSchema> {
376
376
  sync: SyncConfig<T, TKey>;
377
377
  }
378
+ export type SingleResult = {
379
+ singleResult: true;
380
+ };
381
+ export type NonSingleResult = {
382
+ singleResult?: never;
383
+ };
384
+ export type MaybeSingleResult = {
385
+ /**
386
+ * If enabled the collection will return a single object instead of an array
387
+ */
388
+ singleResult?: true;
389
+ };
390
+ export type CollectionConfigSingleRowOption<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = never> = CollectionConfig<T, TKey, TSchema> & MaybeSingleResult;
378
391
  export type ChangesPayload<T extends object = Record<string, unknown>> = Array<ChangeMessage<T>>;
379
392
  /**
380
393
  * An input row from a collection
@@ -248,7 +248,7 @@ class BTree {
248
248
  } finally {
249
249
  let isShared;
250
250
  while (root.keys.length <= 1 && !root.isLeaf) {
251
- isShared || (isShared = root.isShared);
251
+ isShared ||= root.isShared;
252
252
  this._root = root = root.keys.length === 0 ? EmptyLeaf : root.children[0];
253
253
  }
254
254
  if (isShared) {