@tanstack/db 0.4.3 → 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 (168) 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 +3 -4
  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/joins.cjs +6 -6
  44. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  45. package/dist/cjs/query/compiler/order-by.cjs +4 -5
  46. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  47. package/dist/cjs/query/compiler/order-by.d.cts +2 -2
  48. package/dist/cjs/query/compiler/select.cjs +1 -1
  49. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  50. package/dist/cjs/query/index.d.cts +1 -1
  51. package/dist/cjs/query/ir.cjs.map +1 -1
  52. package/dist/cjs/query/ir.d.cts +1 -0
  53. package/dist/cjs/query/live/collection-config-builder.cjs +3 -2
  54. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  55. package/dist/cjs/query/live/collection-config-builder.d.cts +2 -2
  56. package/dist/cjs/query/live/collection-subscriber.cjs +2 -3
  57. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  58. package/dist/cjs/query/live/types.d.cts +4 -0
  59. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  60. package/dist/cjs/query/live-query-collection.d.cts +7 -4
  61. package/dist/cjs/query/optimizer.cjs +2 -4
  62. package/dist/cjs/query/optimizer.cjs.map +1 -1
  63. package/dist/cjs/transactions.cjs +2 -3
  64. package/dist/cjs/transactions.cjs.map +1 -1
  65. package/dist/cjs/types.d.cts +13 -0
  66. package/dist/cjs/utils/btree.cjs +1 -1
  67. package/dist/cjs/utils/btree.cjs.map +1 -1
  68. package/dist/cjs/utils/index-optimization.cjs +7 -2
  69. package/dist/cjs/utils/index-optimization.cjs.map +1 -1
  70. package/dist/cjs/utils/index-optimization.d.cts +3 -2
  71. package/dist/cjs/utils.cjs +6 -0
  72. package/dist/cjs/utils.cjs.map +1 -1
  73. package/dist/cjs/utils.d.cts +2 -3
  74. package/dist/esm/collection/change-events.js +1 -1
  75. package/dist/esm/collection/change-events.js.map +1 -1
  76. package/dist/esm/collection/changes.js +7 -3
  77. package/dist/esm/collection/changes.js.map +1 -1
  78. package/dist/esm/collection/events.js +3 -6
  79. package/dist/esm/collection/events.js.map +1 -1
  80. package/dist/esm/collection/index.d.ts +16 -4
  81. package/dist/esm/collection/index.js +4 -1
  82. package/dist/esm/collection/index.js.map +1 -1
  83. package/dist/esm/collection/mutations.js +13 -20
  84. package/dist/esm/collection/mutations.js.map +1 -1
  85. package/dist/esm/collection/state.js +9 -2
  86. package/dist/esm/collection/state.js.map +1 -1
  87. package/dist/esm/collection/subscription.d.ts +2 -2
  88. package/dist/esm/collection/subscription.js +3 -4
  89. package/dist/esm/collection/subscription.js.map +1 -1
  90. package/dist/esm/collection/sync.js +10 -2
  91. package/dist/esm/collection/sync.js.map +1 -1
  92. package/dist/esm/indexes/auto-index.d.ts +2 -1
  93. package/dist/esm/indexes/auto-index.js +4 -3
  94. package/dist/esm/indexes/auto-index.js.map +1 -1
  95. package/dist/esm/indexes/base-index.d.ts +47 -2
  96. package/dist/esm/indexes/base-index.js +26 -0
  97. package/dist/esm/indexes/base-index.js.map +1 -1
  98. package/dist/esm/indexes/btree-index.d.ts +15 -0
  99. package/dist/esm/indexes/btree-index.js +45 -9
  100. package/dist/esm/indexes/btree-index.js.map +1 -1
  101. package/dist/esm/indexes/lazy-index.js +3 -6
  102. package/dist/esm/indexes/lazy-index.js.map +1 -1
  103. package/dist/esm/indexes/reverse-index.d.ts +30 -0
  104. package/dist/esm/indexes/reverse-index.js +78 -0
  105. package/dist/esm/indexes/reverse-index.js.map +1 -0
  106. package/dist/esm/proxy.js +1 -1
  107. package/dist/esm/proxy.js.map +1 -1
  108. package/dist/esm/query/builder/index.d.ts +16 -1
  109. package/dist/esm/query/builder/index.js +21 -0
  110. package/dist/esm/query/builder/index.js.map +1 -1
  111. package/dist/esm/query/builder/types.d.ts +7 -0
  112. package/dist/esm/query/compiler/evaluators.js +1 -1
  113. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  114. package/dist/esm/query/compiler/group-by.js +3 -11
  115. package/dist/esm/query/compiler/group-by.js.map +1 -1
  116. package/dist/esm/query/compiler/joins.js +6 -6
  117. package/dist/esm/query/compiler/joins.js.map +1 -1
  118. package/dist/esm/query/compiler/order-by.d.ts +2 -2
  119. package/dist/esm/query/compiler/order-by.js +4 -5
  120. package/dist/esm/query/compiler/order-by.js.map +1 -1
  121. package/dist/esm/query/compiler/select.js +1 -1
  122. package/dist/esm/query/compiler/select.js.map +1 -1
  123. package/dist/esm/query/index.d.ts +1 -1
  124. package/dist/esm/query/ir.d.ts +1 -0
  125. package/dist/esm/query/ir.js.map +1 -1
  126. package/dist/esm/query/live/collection-config-builder.d.ts +2 -2
  127. package/dist/esm/query/live/collection-config-builder.js +3 -2
  128. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  129. package/dist/esm/query/live/collection-subscriber.js +2 -3
  130. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  131. package/dist/esm/query/live/types.d.ts +4 -0
  132. package/dist/esm/query/live-query-collection.d.ts +7 -4
  133. package/dist/esm/query/live-query-collection.js.map +1 -1
  134. package/dist/esm/query/optimizer.js +2 -4
  135. package/dist/esm/query/optimizer.js.map +1 -1
  136. package/dist/esm/transactions.js +2 -3
  137. package/dist/esm/transactions.js.map +1 -1
  138. package/dist/esm/types.d.ts +13 -0
  139. package/dist/esm/utils/btree.js +1 -1
  140. package/dist/esm/utils/btree.js.map +1 -1
  141. package/dist/esm/utils/index-optimization.d.ts +3 -2
  142. package/dist/esm/utils/index-optimization.js +7 -2
  143. package/dist/esm/utils/index-optimization.js.map +1 -1
  144. package/dist/esm/utils.d.ts +2 -3
  145. package/dist/esm/utils.js +6 -0
  146. package/dist/esm/utils.js.map +1 -1
  147. package/package.json +1 -1
  148. package/src/collection/changes.ts +10 -4
  149. package/src/collection/index.ts +38 -5
  150. package/src/collection/state.ts +22 -5
  151. package/src/collection/subscription.ts +3 -3
  152. package/src/collection/sync.ts +17 -2
  153. package/src/indexes/auto-index.ts +8 -3
  154. package/src/indexes/base-index.ts +94 -4
  155. package/src/indexes/btree-index.ts +58 -7
  156. package/src/indexes/reverse-index.ts +120 -0
  157. package/src/query/builder/index.ts +30 -2
  158. package/src/query/builder/types.ts +12 -0
  159. package/src/query/compiler/group-by.ts +1 -10
  160. package/src/query/compiler/order-by.ts +15 -18
  161. package/src/query/index.ts +1 -0
  162. package/src/query/ir.ts +1 -0
  163. package/src/query/live/collection-config-builder.ts +3 -2
  164. package/src/query/live/types.ts +5 -0
  165. package/src/query/live-query-collection.ts +34 -8
  166. package/src/types.ts +22 -0
  167. package/src/utils/index-optimization.ts +19 -5
  168. package/src/utils.ts +8 -0
@@ -1 +1 @@
1
- {"version":3,"file":"group-by.js","sources":["../../../../src/query/compiler/group-by.ts"],"sourcesContent":["import { filter, groupBy, groupByOperators, map } from \"@tanstack/db-ivm\"\nimport { Func, PropRef, getHavingExpression } from \"../ir.js\"\nimport {\n AggregateFunctionNotInSelectError,\n NonAggregateExpressionNotInGroupByError,\n UnknownHavingExpressionTypeError,\n UnsupportedAggregateFunctionError,\n} from \"../../errors.js\"\nimport { compileExpression } from \"./evaluators.js\"\nimport type {\n Aggregate,\n BasicExpression,\n GroupBy,\n Having,\n Select,\n} from \"../ir.js\"\nimport type { NamespacedAndKeyedStream, NamespacedRow } from \"../../types.js\"\n\nconst { sum, count, avg, min, max } = groupByOperators\n\n/**\n * Interface for caching the mapping between GROUP BY expressions and SELECT expressions\n */\ninterface GroupBySelectMapping {\n selectToGroupByIndex: Map<string, number> // Maps SELECT alias to GROUP BY expression index\n groupByExpressions: Array<any> // The GROUP BY expressions for reference\n}\n\n/**\n * Validates that all non-aggregate expressions in SELECT are present in GROUP BY\n * and creates a cached mapping for efficient lookup during processing\n */\nfunction validateAndCreateMapping(\n groupByClause: GroupBy,\n selectClause?: Select\n): GroupBySelectMapping {\n const selectToGroupByIndex = new Map<string, number>()\n const groupByExpressions = [...groupByClause]\n\n if (!selectClause) {\n return { selectToGroupByIndex, groupByExpressions }\n }\n\n // Validate each SELECT expression\n for (const [alias, expr] of Object.entries(selectClause)) {\n if (expr.type === `agg`) {\n // Aggregate expressions are allowed and don't need to be in GROUP BY\n continue\n }\n\n // Non-aggregate expression must be in GROUP BY\n const groupIndex = groupByExpressions.findIndex((groupExpr) =>\n expressionsEqual(expr, groupExpr)\n )\n\n if (groupIndex === -1) {\n throw new NonAggregateExpressionNotInGroupByError(alias)\n }\n\n // Cache the mapping\n selectToGroupByIndex.set(alias, groupIndex)\n }\n\n return { selectToGroupByIndex, groupByExpressions }\n}\n\n/**\n * Processes the GROUP BY clause with optional HAVING and SELECT\n * Works with the new __select_results structure from early SELECT processing\n */\nexport function processGroupBy(\n pipeline: NamespacedAndKeyedStream,\n groupByClause: GroupBy,\n havingClauses?: Array<Having>,\n selectClause?: Select,\n fnHavingClauses?: Array<(row: any) => any>\n): NamespacedAndKeyedStream {\n // Handle empty GROUP BY (single-group aggregation)\n if (groupByClause.length === 0) {\n // For single-group aggregation, create a single group with all data\n const aggregates: Record<string, any> = {}\n\n if (selectClause) {\n // Scan the SELECT clause for aggregate functions\n for (const [alias, expr] of Object.entries(selectClause)) {\n if (expr.type === `agg`) {\n const aggExpr = expr\n aggregates[alias] = getAggregateFunction(aggExpr)\n }\n }\n }\n\n // Use a constant key for single group\n const keyExtractor = () => ({ __singleGroup: true })\n\n // Apply the groupBy operator with single group\n pipeline = pipeline.pipe(\n groupBy(keyExtractor, aggregates)\n ) as NamespacedAndKeyedStream\n\n // Update __select_results to include aggregate values\n pipeline = pipeline.pipe(\n map(([, aggregatedRow]) => {\n // Start with the existing __select_results from early SELECT processing\n const selectResults = (aggregatedRow as any).__select_results || {}\n const finalResults: Record<string, any> = { ...selectResults }\n\n if (selectClause) {\n // Update with aggregate results\n for (const [alias, expr] of Object.entries(selectClause)) {\n if (expr.type === `agg`) {\n finalResults[alias] = aggregatedRow[alias]\n }\n // Non-aggregates keep their original values from early SELECT processing\n }\n }\n\n // Use a single key for the result and update __select_results\n return [\n `single_group`,\n {\n ...aggregatedRow,\n __select_results: finalResults,\n },\n ] as [unknown, Record<string, any>]\n })\n )\n\n // Apply HAVING clauses if present\n if (havingClauses && havingClauses.length > 0) {\n for (const havingClause of havingClauses) {\n const havingExpression = getHavingExpression(havingClause)\n const transformedHavingClause = replaceAggregatesByRefs(\n havingExpression,\n selectClause || {}\n )\n const compiledHaving = compileExpression(transformedHavingClause)\n\n pipeline = pipeline.pipe(\n filter(([, row]) => {\n // Create a namespaced row structure for HAVING evaluation\n const namespacedRow = { result: (row as any).__select_results }\n return compiledHaving(namespacedRow)\n })\n )\n }\n }\n\n // Apply functional HAVING clauses if present\n if (fnHavingClauses && fnHavingClauses.length > 0) {\n for (const fnHaving of fnHavingClauses) {\n pipeline = pipeline.pipe(\n filter(([, row]) => {\n // Create a namespaced row structure for functional HAVING evaluation\n const namespacedRow = { result: (row as any).__select_results }\n return fnHaving(namespacedRow)\n })\n )\n }\n }\n\n return pipeline\n }\n\n // Multi-group aggregation logic...\n // Validate and create mapping for non-aggregate expressions in SELECT\n const mapping = validateAndCreateMapping(groupByClause, selectClause)\n\n // Pre-compile groupBy expressions\n const compiledGroupByExpressions = groupByClause.map((e) =>\n compileExpression(e)\n )\n\n // Create a key extractor function using simple __key_X format\n const keyExtractor = ([, row]: [\n string,\n NamespacedRow & { __select_results?: any },\n ]) => {\n // Use the original namespaced row for GROUP BY expressions, not __select_results\n const namespacedRow = { ...row }\n delete (namespacedRow as any).__select_results\n\n const key: Record<string, unknown> = {}\n\n // Use simple __key_X format for each groupBy expression\n for (let i = 0; i < groupByClause.length; i++) {\n const compiledExpr = compiledGroupByExpressions[i]!\n const value = compiledExpr(namespacedRow)\n key[`__key_${i}`] = value\n }\n\n return key\n }\n\n // Create aggregate functions for any aggregated columns in the SELECT clause\n const aggregates: Record<string, any> = {}\n\n if (selectClause) {\n // Scan the SELECT clause for aggregate functions\n for (const [alias, expr] of Object.entries(selectClause)) {\n if (expr.type === `agg`) {\n const aggExpr = expr\n aggregates[alias] = getAggregateFunction(aggExpr)\n }\n }\n }\n\n // Apply the groupBy operator\n pipeline = pipeline.pipe(groupBy(keyExtractor, aggregates))\n\n // Update __select_results to handle GROUP BY results\n pipeline = pipeline.pipe(\n map(([, aggregatedRow]) => {\n // Start with the existing __select_results from early SELECT processing\n const selectResults = (aggregatedRow as any).__select_results || {}\n const finalResults: Record<string, any> = {}\n\n if (selectClause) {\n // Process each SELECT expression\n for (const [alias, expr] of Object.entries(selectClause)) {\n if (expr.type !== `agg`) {\n // Use cached mapping to get the corresponding __key_X for non-aggregates\n const groupIndex = mapping.selectToGroupByIndex.get(alias)\n if (groupIndex !== undefined) {\n finalResults[alias] = aggregatedRow[`__key_${groupIndex}`]\n } else {\n // Fallback to original SELECT results\n finalResults[alias] = selectResults[alias]\n }\n } else {\n // Use aggregate results\n finalResults[alias] = aggregatedRow[alias]\n }\n }\n } else {\n // No SELECT clause - just use the group keys\n for (let i = 0; i < groupByClause.length; i++) {\n finalResults[`__key_${i}`] = aggregatedRow[`__key_${i}`]\n }\n }\n\n // Generate a simple key for the live collection using group values\n let finalKey: unknown\n if (groupByClause.length === 1) {\n finalKey = aggregatedRow[`__key_0`]\n } else {\n const keyParts: Array<unknown> = []\n for (let i = 0; i < groupByClause.length; i++) {\n keyParts.push(aggregatedRow[`__key_${i}`])\n }\n finalKey = JSON.stringify(keyParts)\n }\n\n return [\n finalKey,\n {\n ...aggregatedRow,\n __select_results: finalResults,\n },\n ] as [unknown, Record<string, any>]\n })\n )\n\n // Apply HAVING clauses if present\n if (havingClauses && havingClauses.length > 0) {\n for (const havingClause of havingClauses) {\n const havingExpression = getHavingExpression(havingClause)\n const transformedHavingClause = replaceAggregatesByRefs(\n havingExpression,\n selectClause || {}\n )\n const compiledHaving = compileExpression(transformedHavingClause)\n\n pipeline = pipeline.pipe(\n filter(([, row]) => {\n // Create a namespaced row structure for HAVING evaluation\n const namespacedRow = { result: (row as any).__select_results }\n return compiledHaving(namespacedRow)\n })\n )\n }\n }\n\n // Apply functional HAVING clauses if present\n if (fnHavingClauses && fnHavingClauses.length > 0) {\n for (const fnHaving of fnHavingClauses) {\n pipeline = pipeline.pipe(\n filter(([, row]) => {\n // Create a namespaced row structure for functional HAVING evaluation\n const namespacedRow = { result: (row as any).__select_results }\n return fnHaving(namespacedRow)\n })\n )\n }\n }\n\n return pipeline\n}\n\n/**\n * Helper function to check if two expressions are equal\n */\nfunction expressionsEqual(expr1: any, expr2: any): boolean {\n if (!expr1 || !expr2) return false\n if (expr1.type !== expr2.type) return false\n\n switch (expr1.type) {\n case `ref`:\n // Compare paths as arrays\n if (!expr1.path || !expr2.path) return false\n if (expr1.path.length !== expr2.path.length) return false\n return expr1.path.every(\n (segment: string, i: number) => segment === expr2.path[i]\n )\n case `val`:\n return expr1.value === expr2.value\n case `func`:\n return (\n expr1.name === expr2.name &&\n expr1.args?.length === expr2.args?.length &&\n (expr1.args || []).every((arg: any, i: number) =>\n expressionsEqual(arg, expr2.args[i])\n )\n )\n case `agg`:\n return (\n expr1.name === expr2.name &&\n expr1.args?.length === expr2.args?.length &&\n (expr1.args || []).every((arg: any, i: number) =>\n expressionsEqual(arg, expr2.args[i])\n )\n )\n default:\n return false\n }\n}\n\n/**\n * Helper function to get an aggregate function based on the Agg expression\n */\nfunction getAggregateFunction(aggExpr: Aggregate) {\n // Pre-compile the value extractor expression\n const compiledExpr = compileExpression(aggExpr.args[0]!)\n\n // Create a value extractor function for the expression to aggregate\n const valueExtractor = ([, namespacedRow]: [string, NamespacedRow]) => {\n const value = compiledExpr(namespacedRow)\n // Ensure we return a number for numeric aggregate functions\n return typeof value === `number` ? value : value != null ? Number(value) : 0\n }\n\n // Create a value extractor function for the expression to aggregate\n const valueExtractorWithDate = ([, namespacedRow]: [\n string,\n NamespacedRow,\n ]) => {\n const value = compiledExpr(namespacedRow)\n return typeof value === `number` || value instanceof Date\n ? value\n : value != null\n ? Number(value)\n : 0\n }\n\n // Create a raw value extractor function for the expression to aggregate\n const rawValueExtractor = ([, namespacedRow]: [string, NamespacedRow]) => {\n return compiledExpr(namespacedRow)\n }\n\n // Return the appropriate aggregate function\n switch (aggExpr.name.toLowerCase()) {\n case `sum`:\n return sum(valueExtractor)\n case `count`:\n return count(rawValueExtractor)\n case `avg`:\n return avg(valueExtractor)\n case `min`:\n return min(valueExtractorWithDate)\n case `max`:\n return max(valueExtractorWithDate)\n default:\n throw new UnsupportedAggregateFunctionError(aggExpr.name)\n }\n}\n\n/**\n * Transforms basic expressions and aggregates to replace Agg expressions with references to computed values\n */\nexport function replaceAggregatesByRefs(\n havingExpr: BasicExpression | Aggregate,\n selectClause: Select,\n resultAlias: string = `result`\n): BasicExpression {\n switch (havingExpr.type) {\n case `agg`: {\n const aggExpr = havingExpr\n // Find matching aggregate in SELECT clause\n for (const [alias, selectExpr] of Object.entries(selectClause)) {\n if (selectExpr.type === `agg` && aggregatesEqual(aggExpr, selectExpr)) {\n // Replace with a reference to the computed aggregate\n return new PropRef([resultAlias, alias])\n }\n }\n // If no matching aggregate found in SELECT, throw error\n throw new AggregateFunctionNotInSelectError(aggExpr.name)\n }\n\n case `func`: {\n const funcExpr = havingExpr\n // Transform function arguments recursively\n const transformedArgs = funcExpr.args.map(\n (arg: BasicExpression | Aggregate) =>\n replaceAggregatesByRefs(arg, selectClause)\n )\n return new Func(funcExpr.name, transformedArgs)\n }\n\n case `ref`: {\n const refExpr = havingExpr\n // Check if this is a direct reference to a SELECT alias\n if (refExpr.path.length === 1) {\n const alias = refExpr.path[0]!\n if (selectClause[alias]) {\n // This is a reference to a SELECT alias, convert to result.alias\n return new PropRef([resultAlias, alias])\n }\n }\n // Return as-is for other refs\n return havingExpr as BasicExpression\n }\n\n case `val`:\n // Return as-is\n return havingExpr as BasicExpression\n\n default:\n throw new UnknownHavingExpressionTypeError((havingExpr as any).type)\n }\n}\n\n/**\n * Checks if two aggregate expressions are equal\n */\nfunction aggregatesEqual(agg1: Aggregate, agg2: Aggregate): boolean {\n return (\n agg1.name === agg2.name &&\n agg1.args.length === agg2.args.length &&\n agg1.args.every((arg, i) => expressionsEqual(arg, agg2.args[i]))\n )\n}\n"],"names":["aggregates","keyExtractor"],"mappings":"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"}
1
+ {"version":3,"file":"group-by.js","sources":["../../../../src/query/compiler/group-by.ts"],"sourcesContent":["import { filter, groupBy, groupByOperators, map } from \"@tanstack/db-ivm\"\nimport { Func, PropRef, getHavingExpression } from \"../ir.js\"\nimport {\n AggregateFunctionNotInSelectError,\n NonAggregateExpressionNotInGroupByError,\n UnknownHavingExpressionTypeError,\n UnsupportedAggregateFunctionError,\n} from \"../../errors.js\"\nimport { compileExpression } from \"./evaluators.js\"\nimport type {\n Aggregate,\n BasicExpression,\n GroupBy,\n Having,\n Select,\n} from \"../ir.js\"\nimport type { NamespacedAndKeyedStream, NamespacedRow } from \"../../types.js\"\n\nconst { sum, count, avg, min, max } = groupByOperators\n\n/**\n * Interface for caching the mapping between GROUP BY expressions and SELECT expressions\n */\ninterface GroupBySelectMapping {\n selectToGroupByIndex: Map<string, number> // Maps SELECT alias to GROUP BY expression index\n groupByExpressions: Array<any> // The GROUP BY expressions for reference\n}\n\n/**\n * Validates that all non-aggregate expressions in SELECT are present in GROUP BY\n * and creates a cached mapping for efficient lookup during processing\n */\nfunction validateAndCreateMapping(\n groupByClause: GroupBy,\n selectClause?: Select\n): GroupBySelectMapping {\n const selectToGroupByIndex = new Map<string, number>()\n const groupByExpressions = [...groupByClause]\n\n if (!selectClause) {\n return { selectToGroupByIndex, groupByExpressions }\n }\n\n // Validate each SELECT expression\n for (const [alias, expr] of Object.entries(selectClause)) {\n if (expr.type === `agg`) {\n // Aggregate expressions are allowed and don't need to be in GROUP BY\n continue\n }\n\n // Non-aggregate expression must be in GROUP BY\n const groupIndex = groupByExpressions.findIndex((groupExpr) =>\n expressionsEqual(expr, groupExpr)\n )\n\n if (groupIndex === -1) {\n throw new NonAggregateExpressionNotInGroupByError(alias)\n }\n\n // Cache the mapping\n selectToGroupByIndex.set(alias, groupIndex)\n }\n\n return { selectToGroupByIndex, groupByExpressions }\n}\n\n/**\n * Processes the GROUP BY clause with optional HAVING and SELECT\n * Works with the new __select_results structure from early SELECT processing\n */\nexport function processGroupBy(\n pipeline: NamespacedAndKeyedStream,\n groupByClause: GroupBy,\n havingClauses?: Array<Having>,\n selectClause?: Select,\n fnHavingClauses?: Array<(row: any) => any>\n): NamespacedAndKeyedStream {\n // Handle empty GROUP BY (single-group aggregation)\n if (groupByClause.length === 0) {\n // For single-group aggregation, create a single group with all data\n const aggregates: Record<string, any> = {}\n\n if (selectClause) {\n // Scan the SELECT clause for aggregate functions\n for (const [alias, expr] of Object.entries(selectClause)) {\n if (expr.type === `agg`) {\n const aggExpr = expr\n aggregates[alias] = getAggregateFunction(aggExpr)\n }\n }\n }\n\n // Use a constant key for single group\n const keyExtractor = () => ({ __singleGroup: true })\n\n // Apply the groupBy operator with single group\n pipeline = pipeline.pipe(\n groupBy(keyExtractor, aggregates)\n ) as NamespacedAndKeyedStream\n\n // Update __select_results to include aggregate values\n pipeline = pipeline.pipe(\n map(([, aggregatedRow]) => {\n // Start with the existing __select_results from early SELECT processing\n const selectResults = (aggregatedRow as any).__select_results || {}\n const finalResults: Record<string, any> = { ...selectResults }\n\n if (selectClause) {\n // Update with aggregate results\n for (const [alias, expr] of Object.entries(selectClause)) {\n if (expr.type === `agg`) {\n finalResults[alias] = aggregatedRow[alias]\n }\n // Non-aggregates keep their original values from early SELECT processing\n }\n }\n\n // Use a single key for the result and update __select_results\n return [\n `single_group`,\n {\n ...aggregatedRow,\n __select_results: finalResults,\n },\n ] as [unknown, Record<string, any>]\n })\n )\n\n // Apply HAVING clauses if present\n if (havingClauses && havingClauses.length > 0) {\n for (const havingClause of havingClauses) {\n const havingExpression = getHavingExpression(havingClause)\n const transformedHavingClause = replaceAggregatesByRefs(\n havingExpression,\n selectClause || {}\n )\n const compiledHaving = compileExpression(transformedHavingClause)\n\n pipeline = pipeline.pipe(\n filter(([, row]) => {\n // Create a namespaced row structure for HAVING evaluation\n const namespacedRow = { result: (row as any).__select_results }\n return compiledHaving(namespacedRow)\n })\n )\n }\n }\n\n // Apply functional HAVING clauses if present\n if (fnHavingClauses && fnHavingClauses.length > 0) {\n for (const fnHaving of fnHavingClauses) {\n pipeline = pipeline.pipe(\n filter(([, row]) => {\n // Create a namespaced row structure for functional HAVING evaluation\n const namespacedRow = { result: (row as any).__select_results }\n return fnHaving(namespacedRow)\n })\n )\n }\n }\n\n return pipeline\n }\n\n // Multi-group aggregation logic...\n // Validate and create mapping for non-aggregate expressions in SELECT\n const mapping = validateAndCreateMapping(groupByClause, selectClause)\n\n // Pre-compile groupBy expressions\n const compiledGroupByExpressions = groupByClause.map((e) =>\n compileExpression(e)\n )\n\n // Create a key extractor function using simple __key_X format\n const keyExtractor = ([, row]: [\n string,\n NamespacedRow & { __select_results?: any },\n ]) => {\n // Use the original namespaced row for GROUP BY expressions, not __select_results\n const namespacedRow = { ...row }\n delete (namespacedRow as any).__select_results\n\n const key: Record<string, unknown> = {}\n\n // Use simple __key_X format for each groupBy expression\n for (let i = 0; i < groupByClause.length; i++) {\n const compiledExpr = compiledGroupByExpressions[i]!\n const value = compiledExpr(namespacedRow)\n key[`__key_${i}`] = value\n }\n\n return key\n }\n\n // Create aggregate functions for any aggregated columns in the SELECT clause\n const aggregates: Record<string, any> = {}\n\n if (selectClause) {\n // Scan the SELECT clause for aggregate functions\n for (const [alias, expr] of Object.entries(selectClause)) {\n if (expr.type === `agg`) {\n const aggExpr = expr\n aggregates[alias] = getAggregateFunction(aggExpr)\n }\n }\n }\n\n // Apply the groupBy operator\n pipeline = pipeline.pipe(groupBy(keyExtractor, aggregates))\n\n // Update __select_results to handle GROUP BY results\n pipeline = pipeline.pipe(\n map(([, aggregatedRow]) => {\n // Start with the existing __select_results from early SELECT processing\n const selectResults = (aggregatedRow as any).__select_results || {}\n const finalResults: Record<string, any> = {}\n\n if (selectClause) {\n // Process each SELECT expression\n for (const [alias, expr] of Object.entries(selectClause)) {\n if (expr.type !== `agg`) {\n // Use cached mapping to get the corresponding __key_X for non-aggregates\n const groupIndex = mapping.selectToGroupByIndex.get(alias)\n if (groupIndex !== undefined) {\n finalResults[alias] = aggregatedRow[`__key_${groupIndex}`]\n } else {\n // Fallback to original SELECT results\n finalResults[alias] = selectResults[alias]\n }\n } else {\n // Use aggregate results\n finalResults[alias] = aggregatedRow[alias]\n }\n }\n } else {\n // No SELECT clause - just use the group keys\n for (let i = 0; i < groupByClause.length; i++) {\n finalResults[`__key_${i}`] = aggregatedRow[`__key_${i}`]\n }\n }\n\n // Generate a simple key for the live collection using group values\n let finalKey: unknown\n if (groupByClause.length === 1) {\n finalKey = aggregatedRow[`__key_0`]\n } else {\n const keyParts: Array<unknown> = []\n for (let i = 0; i < groupByClause.length; i++) {\n keyParts.push(aggregatedRow[`__key_${i}`])\n }\n finalKey = JSON.stringify(keyParts)\n }\n\n return [\n finalKey,\n {\n ...aggregatedRow,\n __select_results: finalResults,\n },\n ] as [unknown, Record<string, any>]\n })\n )\n\n // Apply HAVING clauses if present\n if (havingClauses && havingClauses.length > 0) {\n for (const havingClause of havingClauses) {\n const havingExpression = getHavingExpression(havingClause)\n const transformedHavingClause = replaceAggregatesByRefs(\n havingExpression,\n selectClause || {}\n )\n const compiledHaving = compileExpression(transformedHavingClause)\n\n pipeline = pipeline.pipe(\n filter(([, row]) => {\n // Create a namespaced row structure for HAVING evaluation\n const namespacedRow = { result: (row as any).__select_results }\n return compiledHaving(namespacedRow)\n })\n )\n }\n }\n\n // Apply functional HAVING clauses if present\n if (fnHavingClauses && fnHavingClauses.length > 0) {\n for (const fnHaving of fnHavingClauses) {\n pipeline = pipeline.pipe(\n filter(([, row]) => {\n // Create a namespaced row structure for functional HAVING evaluation\n const namespacedRow = { result: (row as any).__select_results }\n return fnHaving(namespacedRow)\n })\n )\n }\n }\n\n return pipeline\n}\n\n/**\n * Helper function to check if two expressions are equal\n */\nfunction expressionsEqual(expr1: any, expr2: any): boolean {\n if (!expr1 || !expr2) return false\n if (expr1.type !== expr2.type) return false\n\n switch (expr1.type) {\n case `ref`:\n // Compare paths as arrays\n if (!expr1.path || !expr2.path) return false\n if (expr1.path.length !== expr2.path.length) return false\n return expr1.path.every(\n (segment: string, i: number) => segment === expr2.path[i]\n )\n case `val`:\n return expr1.value === expr2.value\n case `func`:\n return (\n expr1.name === expr2.name &&\n expr1.args?.length === expr2.args?.length &&\n (expr1.args || []).every((arg: any, i: number) =>\n expressionsEqual(arg, expr2.args[i])\n )\n )\n case `agg`:\n return (\n expr1.name === expr2.name &&\n expr1.args?.length === expr2.args?.length &&\n (expr1.args || []).every((arg: any, i: number) =>\n expressionsEqual(arg, expr2.args[i])\n )\n )\n default:\n return false\n }\n}\n\n/**\n * Helper function to get an aggregate function based on the Agg expression\n */\nfunction getAggregateFunction(aggExpr: Aggregate) {\n // Pre-compile the value extractor expression\n const compiledExpr = compileExpression(aggExpr.args[0]!)\n\n // Create a value extractor function for the expression to aggregate\n const valueExtractor = ([, namespacedRow]: [string, NamespacedRow]) => {\n const value = compiledExpr(namespacedRow)\n // Ensure we return a number for numeric aggregate functions\n return typeof value === `number` ? value : value != null ? Number(value) : 0\n }\n\n // Create a value extractor function for the expression to aggregate\n const valueExtractorWithDate = ([, namespacedRow]: [\n string,\n NamespacedRow,\n ]) => {\n const value = compiledExpr(namespacedRow)\n return typeof value === `number` || value instanceof Date\n ? value\n : value != null\n ? Number(value)\n : 0\n }\n\n // Create a raw value extractor function for the expression to aggregate\n const rawValueExtractor = ([, namespacedRow]: [string, NamespacedRow]) => {\n return compiledExpr(namespacedRow)\n }\n\n // Return the appropriate aggregate function\n switch (aggExpr.name.toLowerCase()) {\n case `sum`:\n return sum(valueExtractor)\n case `count`:\n return count(rawValueExtractor)\n case `avg`:\n return avg(valueExtractor)\n case `min`:\n return min(valueExtractorWithDate)\n case `max`:\n return max(valueExtractorWithDate)\n default:\n throw new UnsupportedAggregateFunctionError(aggExpr.name)\n }\n}\n\n/**\n * Transforms basic expressions and aggregates to replace Agg expressions with references to computed values\n */\nexport function replaceAggregatesByRefs(\n havingExpr: BasicExpression | Aggregate,\n selectClause: Select,\n resultAlias: string = `result`\n): BasicExpression {\n switch (havingExpr.type) {\n case `agg`: {\n const aggExpr = havingExpr\n // Find matching aggregate in SELECT clause\n for (const [alias, selectExpr] of Object.entries(selectClause)) {\n if (selectExpr.type === `agg` && aggregatesEqual(aggExpr, selectExpr)) {\n // Replace with a reference to the computed aggregate\n return new PropRef([resultAlias, alias])\n }\n }\n // If no matching aggregate found in SELECT, throw error\n throw new AggregateFunctionNotInSelectError(aggExpr.name)\n }\n\n case `func`: {\n const funcExpr = havingExpr\n // Transform function arguments recursively\n const transformedArgs = funcExpr.args.map(\n (arg: BasicExpression | Aggregate) =>\n replaceAggregatesByRefs(arg, selectClause)\n )\n return new Func(funcExpr.name, transformedArgs)\n }\n\n case `ref`: {\n // Non-aggregate refs are passed through unchanged (they reference table columns)\n return havingExpr as BasicExpression\n }\n\n case `val`:\n // Return as-is\n return havingExpr as BasicExpression\n\n default:\n throw new UnknownHavingExpressionTypeError((havingExpr as any).type)\n }\n}\n\n/**\n * Checks if two aggregate expressions are equal\n */\nfunction aggregatesEqual(agg1: Aggregate, agg2: Aggregate): boolean {\n return (\n agg1.name === agg2.name &&\n agg1.args.length === agg2.args.length &&\n agg1.args.every((arg, i) => expressionsEqual(arg, agg2.args[i]))\n )\n}\n"],"names":["aggregates","keyExtractor"],"mappings":"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"}
@@ -229,8 +229,8 @@ function processJoinResults(joinType) {
229
229
  // Process the join result and handle nulls
230
230
  filter((result) => {
231
231
  const [_key, [main, joined]] = result;
232
- const mainNamespacedRow = main == null ? void 0 : main[1];
233
- const joinedNamespacedRow = joined == null ? void 0 : joined[1];
232
+ const mainNamespacedRow = main?.[1];
233
+ const joinedNamespacedRow = joined?.[1];
234
234
  if (joinType === `inner`) {
235
235
  return !!(mainNamespacedRow && joinedNamespacedRow);
236
236
  }
@@ -244,10 +244,10 @@ function processJoinResults(joinType) {
244
244
  }),
245
245
  map((result) => {
246
246
  const [_key, [main, joined]] = result;
247
- const mainKey = main == null ? void 0 : main[0];
248
- const mainNamespacedRow = main == null ? void 0 : main[1];
249
- const joinedKey = joined == null ? void 0 : joined[0];
250
- const joinedNamespacedRow = joined == null ? void 0 : joined[1];
247
+ const mainKey = main?.[0];
248
+ const mainNamespacedRow = main?.[1];
249
+ const joinedKey = joined?.[0];
250
+ const joinedNamespacedRow = joined?.[1];
251
251
  const mergedNamespacedRow = {};
252
252
  if (mainNamespacedRow) {
253
253
  Object.assign(mergedNamespacedRow, mainNamespacedRow);
@@ -1 +1 @@
1
- {"version":3,"file":"joins.js","sources":["../../../../src/query/compiler/joins.ts"],"sourcesContent":["import {\n consolidate,\n filter,\n join as joinOperator,\n map,\n tap,\n} from \"@tanstack/db-ivm\"\nimport {\n CollectionInputNotFoundError,\n InvalidJoinCondition,\n InvalidJoinConditionLeftTableError,\n InvalidJoinConditionRightTableError,\n InvalidJoinConditionSameTableError,\n InvalidJoinConditionTableMismatchError,\n JoinCollectionNotFoundError,\n UnsupportedJoinSourceTypeError,\n UnsupportedJoinTypeError,\n} from \"../../errors.js\"\nimport { ensureIndexForField } from \"../../indexes/auto-index.js\"\nimport { PropRef, followRef } from \"../ir.js\"\nimport { inArray } from \"../builder/functions.js\"\nimport { compileExpression } from \"./evaluators.js\"\nimport type { CompileQueryFn } from \"./index.js\"\nimport type { OrderByOptimizationInfo } from \"./order-by.js\"\nimport type {\n BasicExpression,\n CollectionRef,\n JoinClause,\n QueryIR,\n QueryRef,\n} from \"../ir.js\"\nimport type { IStreamBuilder, JoinType } from \"@tanstack/db-ivm\"\nimport type { Collection } from \"../../collection/index.js\"\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n NamespacedRow,\n} from \"../../types.js\"\nimport type { QueryCache, QueryMapping } from \"./types.js\"\nimport type { CollectionSubscription } from \"../../collection/subscription.js\"\n\nexport type LoadKeysFn = (key: Set<string | number>) => void\nexport type LazyCollectionCallbacks = {\n loadKeys: LoadKeysFn\n loadInitialState: () => void\n}\n\n/**\n * Processes all join clauses in a query\n */\nexport function processJoins(\n pipeline: NamespacedAndKeyedStream,\n joinClauses: Array<JoinClause>,\n tables: Record<string, KeyedStream>,\n mainTableId: string,\n mainTableAlias: string,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazyCollections: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n rawQuery: QueryIR,\n onCompileSubquery: CompileQueryFn\n): NamespacedAndKeyedStream {\n let resultPipeline = pipeline\n\n for (const joinClause of joinClauses) {\n resultPipeline = processJoin(\n resultPipeline,\n joinClause,\n tables,\n mainTableId,\n mainTableAlias,\n allInputs,\n cache,\n queryMapping,\n collections,\n subscriptions,\n callbacks,\n lazyCollections,\n optimizableOrderByCollections,\n rawQuery,\n onCompileSubquery\n )\n }\n\n return resultPipeline\n}\n\n/**\n * Processes a single join clause\n */\nfunction processJoin(\n pipeline: NamespacedAndKeyedStream,\n joinClause: JoinClause,\n tables: Record<string, KeyedStream>,\n mainTableId: string,\n mainTableAlias: string,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazyCollections: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n rawQuery: QueryIR,\n onCompileSubquery: CompileQueryFn\n): NamespacedAndKeyedStream {\n // Get the joined table alias and input stream\n const {\n alias: joinedTableAlias,\n input: joinedInput,\n collectionId: joinedCollectionId,\n } = processJoinSource(\n joinClause.from,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazyCollections,\n optimizableOrderByCollections,\n cache,\n queryMapping,\n onCompileSubquery\n )\n\n // Add the joined table to the tables map\n tables[joinedTableAlias] = joinedInput\n\n const mainCollection = collections[mainTableId]\n const joinedCollection = collections[joinedCollectionId]\n\n if (!mainCollection) {\n throw new JoinCollectionNotFoundError(mainTableId)\n }\n\n if (!joinedCollection) {\n throw new JoinCollectionNotFoundError(joinedCollectionId)\n }\n\n const { activeCollection, lazyCollection } = getActiveAndLazyCollections(\n joinClause.type,\n mainCollection,\n joinedCollection\n )\n\n // Analyze which table each expression refers to and swap if necessary\n const availableTableAliases = Object.keys(tables)\n const { mainExpr, joinedExpr } = analyzeJoinExpressions(\n joinClause.left,\n joinClause.right,\n availableTableAliases,\n joinedTableAlias\n )\n\n // Pre-compile the join expressions\n const compiledMainExpr = compileExpression(mainExpr)\n const compiledJoinedExpr = compileExpression(joinedExpr)\n\n // Prepare the main pipeline for joining\n let mainPipeline = pipeline.pipe(\n map(([currentKey, namespacedRow]) => {\n // Extract the join key from the main table expression\n const mainKey = compiledMainExpr(namespacedRow)\n\n // Return [joinKey, [originalKey, namespacedRow]]\n return [mainKey, [currentKey, namespacedRow]] as [\n unknown,\n [string, typeof namespacedRow],\n ]\n })\n )\n\n // Prepare the joined pipeline\n let joinedPipeline = joinedInput.pipe(\n map(([currentKey, row]) => {\n // Wrap the row in a namespaced structure\n const namespacedRow: NamespacedRow = { [joinedTableAlias]: row }\n\n // Extract the join key from the joined table expression\n const joinedKey = compiledJoinedExpr(namespacedRow)\n\n // Return [joinKey, [originalKey, namespacedRow]]\n return [joinedKey, [currentKey, namespacedRow]] as [\n unknown,\n [string, typeof namespacedRow],\n ]\n })\n )\n\n // Apply the join operation\n if (![`inner`, `left`, `right`, `full`].includes(joinClause.type)) {\n throw new UnsupportedJoinTypeError(joinClause.type)\n }\n\n if (activeCollection) {\n // If the lazy collection comes from a subquery that has a limit and/or an offset clause\n // then we need to deoptimize the join because we don't know which rows are in the result set\n // since we simply lookup matching keys in the index but the index contains all rows\n // (not just the ones that pass the limit and offset clauses)\n const lazyFrom =\n activeCollection === `main` ? joinClause.from : rawQuery.from\n const limitedSubquery =\n lazyFrom.type === `queryRef` &&\n (lazyFrom.query.limit || lazyFrom.query.offset)\n\n // If join expressions contain computed values (like concat functions)\n // we don't optimize the join because we don't have an index over the computed values\n const hasComputedJoinExpr =\n mainExpr.type === `func` || joinedExpr.type === `func`\n\n if (!limitedSubquery && !hasComputedJoinExpr) {\n // This join can be optimized by having the active collection\n // dynamically load keys into the lazy collection\n // based on the value of the joinKey and by looking up\n // matching rows in the index of the lazy collection\n\n // Mark the lazy collection as lazy\n // this Set is passed by the liveQueryCollection to the compiler\n // such that the liveQueryCollection can check it after compilation\n // to know which collections are lazy collections\n lazyCollections.add(lazyCollection.id)\n\n const activePipeline =\n activeCollection === `main` ? mainPipeline : joinedPipeline\n\n const lazyCollectionJoinExpr =\n activeCollection === `main`\n ? (joinedExpr as PropRef)\n : (mainExpr as PropRef)\n\n const followRefResult = followRef(\n rawQuery,\n lazyCollectionJoinExpr,\n lazyCollection\n )!\n const followRefCollection = followRefResult.collection\n\n const fieldName = followRefResult.path[0]\n if (fieldName) {\n ensureIndexForField(\n fieldName,\n followRefResult.path,\n followRefCollection\n )\n }\n\n const activePipelineWithLoading: IStreamBuilder<\n [key: unknown, [originalKey: string, namespacedRow: NamespacedRow]]\n > = activePipeline.pipe(\n tap((data) => {\n const lazyCollectionSubscription = subscriptions[lazyCollection.id]\n\n if (!lazyCollectionSubscription) {\n throw new Error(\n `Internal error: subscription for collection is missing in join pipeline. Make sure the live query collection sets the subscription before running the pipeline.`\n )\n }\n\n if (lazyCollectionSubscription.hasLoadedInitialState()) {\n // Entire state was already loaded because we deoptimized the join\n return\n }\n\n const joinKeys = data.getInner().map(([[joinKey]]) => joinKey)\n const lazyJoinRef = new PropRef(followRefResult.path)\n const loaded = lazyCollectionSubscription.requestSnapshot({\n where: inArray(lazyJoinRef, joinKeys),\n optimizedOnly: true,\n })\n\n if (!loaded) {\n // Snapshot wasn't sent because it could not be loaded from the indexes\n lazyCollectionSubscription.requestSnapshot()\n }\n })\n )\n\n if (activeCollection === `main`) {\n mainPipeline = activePipelineWithLoading\n } else {\n joinedPipeline = activePipelineWithLoading\n }\n }\n }\n\n return mainPipeline.pipe(\n joinOperator(joinedPipeline, joinClause.type as JoinType),\n consolidate(),\n processJoinResults(joinClause.type)\n )\n}\n\n/**\n * Analyzes join expressions to determine which refers to which table\n * and returns them in the correct order (available table expression first, joined table expression second)\n */\nfunction analyzeJoinExpressions(\n left: BasicExpression,\n right: BasicExpression,\n allAvailableTableAliases: Array<string>,\n joinedTableAlias: string\n): { mainExpr: BasicExpression; joinedExpr: BasicExpression } {\n // Filter out the joined table alias from the available table aliases\n const availableTableAliases = allAvailableTableAliases.filter(\n (alias) => alias !== joinedTableAlias\n )\n\n const leftTableAlias = getTableAliasFromExpression(left)\n const rightTableAlias = getTableAliasFromExpression(right)\n\n // If left expression refers to an available table and right refers to joined table, keep as is\n if (\n leftTableAlias &&\n availableTableAliases.includes(leftTableAlias) &&\n rightTableAlias === joinedTableAlias\n ) {\n return { mainExpr: left, joinedExpr: right }\n }\n\n // If left expression refers to joined table and right refers to an available table, swap them\n if (\n leftTableAlias === joinedTableAlias &&\n rightTableAlias &&\n availableTableAliases.includes(rightTableAlias)\n ) {\n return { mainExpr: right, joinedExpr: left }\n }\n\n // If one expression doesn't refer to any table, this is an invalid join\n if (!leftTableAlias || !rightTableAlias) {\n // For backward compatibility, use the first available table alias in error message\n throw new InvalidJoinConditionTableMismatchError()\n }\n\n // If both expressions refer to the same alias, this is an invalid join\n if (leftTableAlias === rightTableAlias) {\n throw new InvalidJoinConditionSameTableError(leftTableAlias)\n }\n\n // Left side must refer to an available table\n // This cannot happen with the query builder as there is no way to build a ref\n // to an unavailable table, but just in case, but could happen with the IR\n if (!availableTableAliases.includes(leftTableAlias)) {\n throw new InvalidJoinConditionLeftTableError(leftTableAlias)\n }\n\n // Right side must refer to the joined table\n if (rightTableAlias !== joinedTableAlias) {\n throw new InvalidJoinConditionRightTableError(joinedTableAlias)\n }\n\n // This should not be reachable given the logic above, but just in case\n throw new InvalidJoinCondition()\n}\n\n/**\n * Extracts the table alias from a join expression\n */\nfunction getTableAliasFromExpression(expr: BasicExpression): string | null {\n switch (expr.type) {\n case `ref`:\n // PropRef path has the table alias as the first element\n return expr.path[0] || null\n case `func`: {\n // For function expressions, we need to check if all arguments refer to the same table\n const tableAliases = new Set<string>()\n for (const arg of expr.args) {\n const alias = getTableAliasFromExpression(arg)\n if (alias) {\n tableAliases.add(alias)\n }\n }\n // If all arguments refer to the same table, return that table alias\n return tableAliases.size === 1 ? Array.from(tableAliases)[0]! : null\n }\n default:\n // Values (type='val') don't reference any table\n return null\n }\n}\n\n/**\n * Processes the join source (collection or sub-query)\n */\nfunction processJoinSource(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazyCollections: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n cache: QueryCache,\n queryMapping: QueryMapping,\n onCompileSubquery: CompileQueryFn\n): { alias: string; input: KeyedStream; collectionId: string } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.collection.id]\n if (!input) {\n throw new CollectionInputNotFoundError(from.collection.id)\n }\n return { alias: from.alias, input, collectionId: from.collection.id }\n }\n case `queryRef`: {\n // Find the original query for caching purposes\n const originalQuery = queryMapping.get(from.query) || from.query\n\n // Recursively compile the sub-query with cache\n const subQueryResult = onCompileSubquery(\n originalQuery,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazyCollections,\n optimizableOrderByCollections,\n cache,\n queryMapping\n )\n\n // Extract the pipeline from the compilation result\n const subQueryInput = subQueryResult.pipeline\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n return [key, value] as [unknown, any]\n })\n )\n\n return {\n alias: from.alias,\n input: extractedInput as KeyedStream,\n collectionId: subQueryResult.collectionId,\n }\n }\n default:\n throw new UnsupportedJoinSourceTypeError((from as any).type)\n }\n}\n\n/**\n * Processes the results of a join operation\n */\nfunction processJoinResults(joinType: string) {\n return function (\n pipeline: IStreamBuilder<\n [\n key: string,\n [\n [string, NamespacedRow] | undefined,\n [string, NamespacedRow] | undefined,\n ],\n ]\n >\n ): NamespacedAndKeyedStream {\n return pipeline.pipe(\n // Process the join result and handle nulls\n filter((result) => {\n const [_key, [main, joined]] = result\n const mainNamespacedRow = main?.[1]\n const joinedNamespacedRow = joined?.[1]\n\n // Handle different join types\n if (joinType === `inner`) {\n return !!(mainNamespacedRow && joinedNamespacedRow)\n }\n\n if (joinType === `left`) {\n return !!mainNamespacedRow\n }\n\n if (joinType === `right`) {\n return !!joinedNamespacedRow\n }\n\n // For full joins, always include\n return true\n }),\n map((result) => {\n const [_key, [main, joined]] = result\n const mainKey = main?.[0]\n const mainNamespacedRow = main?.[1]\n const joinedKey = joined?.[0]\n const joinedNamespacedRow = joined?.[1]\n\n // Merge the namespaced rows\n const mergedNamespacedRow: NamespacedRow = {}\n\n // Add main row data if it exists\n if (mainNamespacedRow) {\n Object.assign(mergedNamespacedRow, mainNamespacedRow)\n }\n\n // Add joined row data if it exists\n if (joinedNamespacedRow) {\n Object.assign(mergedNamespacedRow, joinedNamespacedRow)\n }\n\n // We create a composite key that combines the main and joined keys\n const resultKey = `[${mainKey},${joinedKey}]`\n\n return [resultKey, mergedNamespacedRow] as [string, NamespacedRow]\n })\n )\n }\n}\n\n/**\n * Returns the active and lazy collections for a join clause.\n * The active collection is the one that we need to fully iterate over\n * and it can be the main table (i.e. left collection) or the joined table (i.e. right collection).\n * The lazy collection is the one that we should join-in lazily based on matches in the active collection.\n * @param joinClause - The join clause to analyze\n * @param leftCollection - The left collection\n * @param rightCollection - The right collection\n * @returns The active and lazy collections. They are undefined if we need to loop over both collections (i.e. both are active)\n */\nfunction getActiveAndLazyCollections(\n joinType: JoinClause[`type`],\n leftCollection: Collection,\n rightCollection: Collection\n):\n | { activeCollection: `main` | `joined`; lazyCollection: Collection }\n | { activeCollection: undefined; lazyCollection: undefined } {\n if (leftCollection.id === rightCollection.id) {\n // We can't apply this optimization if there's only one collection\n // because `liveQueryCollection` will detect that the collection is lazy\n // and treat it lazily (because the collection is shared)\n // and thus it will not load any keys because both sides of the join\n // will be handled lazily\n return { activeCollection: undefined, lazyCollection: undefined }\n }\n\n switch (joinType) {\n case `left`:\n return { activeCollection: `main`, lazyCollection: rightCollection }\n case `right`:\n return { activeCollection: `joined`, lazyCollection: leftCollection }\n case `inner`:\n // The smallest collection should be the active collection\n // and the biggest collection should be lazy\n return leftCollection.size < rightCollection.size\n ? { activeCollection: `main`, lazyCollection: rightCollection }\n : { activeCollection: `joined`, lazyCollection: leftCollection }\n default:\n return { activeCollection: undefined, lazyCollection: undefined }\n }\n}\n"],"names":["joinOperator"],"mappings":"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"}
1
+ {"version":3,"file":"joins.js","sources":["../../../../src/query/compiler/joins.ts"],"sourcesContent":["import {\n consolidate,\n filter,\n join as joinOperator,\n map,\n tap,\n} from \"@tanstack/db-ivm\"\nimport {\n CollectionInputNotFoundError,\n InvalidJoinCondition,\n InvalidJoinConditionLeftTableError,\n InvalidJoinConditionRightTableError,\n InvalidJoinConditionSameTableError,\n InvalidJoinConditionTableMismatchError,\n JoinCollectionNotFoundError,\n UnsupportedJoinSourceTypeError,\n UnsupportedJoinTypeError,\n} from \"../../errors.js\"\nimport { ensureIndexForField } from \"../../indexes/auto-index.js\"\nimport { PropRef, followRef } from \"../ir.js\"\nimport { inArray } from \"../builder/functions.js\"\nimport { compileExpression } from \"./evaluators.js\"\nimport type { CompileQueryFn } from \"./index.js\"\nimport type { OrderByOptimizationInfo } from \"./order-by.js\"\nimport type {\n BasicExpression,\n CollectionRef,\n JoinClause,\n QueryIR,\n QueryRef,\n} from \"../ir.js\"\nimport type { IStreamBuilder, JoinType } from \"@tanstack/db-ivm\"\nimport type { Collection } from \"../../collection/index.js\"\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n NamespacedRow,\n} from \"../../types.js\"\nimport type { QueryCache, QueryMapping } from \"./types.js\"\nimport type { CollectionSubscription } from \"../../collection/subscription.js\"\n\nexport type LoadKeysFn = (key: Set<string | number>) => void\nexport type LazyCollectionCallbacks = {\n loadKeys: LoadKeysFn\n loadInitialState: () => void\n}\n\n/**\n * Processes all join clauses in a query\n */\nexport function processJoins(\n pipeline: NamespacedAndKeyedStream,\n joinClauses: Array<JoinClause>,\n tables: Record<string, KeyedStream>,\n mainTableId: string,\n mainTableAlias: string,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazyCollections: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n rawQuery: QueryIR,\n onCompileSubquery: CompileQueryFn\n): NamespacedAndKeyedStream {\n let resultPipeline = pipeline\n\n for (const joinClause of joinClauses) {\n resultPipeline = processJoin(\n resultPipeline,\n joinClause,\n tables,\n mainTableId,\n mainTableAlias,\n allInputs,\n cache,\n queryMapping,\n collections,\n subscriptions,\n callbacks,\n lazyCollections,\n optimizableOrderByCollections,\n rawQuery,\n onCompileSubquery\n )\n }\n\n return resultPipeline\n}\n\n/**\n * Processes a single join clause\n */\nfunction processJoin(\n pipeline: NamespacedAndKeyedStream,\n joinClause: JoinClause,\n tables: Record<string, KeyedStream>,\n mainTableId: string,\n mainTableAlias: string,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazyCollections: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n rawQuery: QueryIR,\n onCompileSubquery: CompileQueryFn\n): NamespacedAndKeyedStream {\n // Get the joined table alias and input stream\n const {\n alias: joinedTableAlias,\n input: joinedInput,\n collectionId: joinedCollectionId,\n } = processJoinSource(\n joinClause.from,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazyCollections,\n optimizableOrderByCollections,\n cache,\n queryMapping,\n onCompileSubquery\n )\n\n // Add the joined table to the tables map\n tables[joinedTableAlias] = joinedInput\n\n const mainCollection = collections[mainTableId]\n const joinedCollection = collections[joinedCollectionId]\n\n if (!mainCollection) {\n throw new JoinCollectionNotFoundError(mainTableId)\n }\n\n if (!joinedCollection) {\n throw new JoinCollectionNotFoundError(joinedCollectionId)\n }\n\n const { activeCollection, lazyCollection } = getActiveAndLazyCollections(\n joinClause.type,\n mainCollection,\n joinedCollection\n )\n\n // Analyze which table each expression refers to and swap if necessary\n const availableTableAliases = Object.keys(tables)\n const { mainExpr, joinedExpr } = analyzeJoinExpressions(\n joinClause.left,\n joinClause.right,\n availableTableAliases,\n joinedTableAlias\n )\n\n // Pre-compile the join expressions\n const compiledMainExpr = compileExpression(mainExpr)\n const compiledJoinedExpr = compileExpression(joinedExpr)\n\n // Prepare the main pipeline for joining\n let mainPipeline = pipeline.pipe(\n map(([currentKey, namespacedRow]) => {\n // Extract the join key from the main table expression\n const mainKey = compiledMainExpr(namespacedRow)\n\n // Return [joinKey, [originalKey, namespacedRow]]\n return [mainKey, [currentKey, namespacedRow]] as [\n unknown,\n [string, typeof namespacedRow],\n ]\n })\n )\n\n // Prepare the joined pipeline\n let joinedPipeline = joinedInput.pipe(\n map(([currentKey, row]) => {\n // Wrap the row in a namespaced structure\n const namespacedRow: NamespacedRow = { [joinedTableAlias]: row }\n\n // Extract the join key from the joined table expression\n const joinedKey = compiledJoinedExpr(namespacedRow)\n\n // Return [joinKey, [originalKey, namespacedRow]]\n return [joinedKey, [currentKey, namespacedRow]] as [\n unknown,\n [string, typeof namespacedRow],\n ]\n })\n )\n\n // Apply the join operation\n if (![`inner`, `left`, `right`, `full`].includes(joinClause.type)) {\n throw new UnsupportedJoinTypeError(joinClause.type)\n }\n\n if (activeCollection) {\n // If the lazy collection comes from a subquery that has a limit and/or an offset clause\n // then we need to deoptimize the join because we don't know which rows are in the result set\n // since we simply lookup matching keys in the index but the index contains all rows\n // (not just the ones that pass the limit and offset clauses)\n const lazyFrom =\n activeCollection === `main` ? joinClause.from : rawQuery.from\n const limitedSubquery =\n lazyFrom.type === `queryRef` &&\n (lazyFrom.query.limit || lazyFrom.query.offset)\n\n // If join expressions contain computed values (like concat functions)\n // we don't optimize the join because we don't have an index over the computed values\n const hasComputedJoinExpr =\n mainExpr.type === `func` || joinedExpr.type === `func`\n\n if (!limitedSubquery && !hasComputedJoinExpr) {\n // This join can be optimized by having the active collection\n // dynamically load keys into the lazy collection\n // based on the value of the joinKey and by looking up\n // matching rows in the index of the lazy collection\n\n // Mark the lazy collection as lazy\n // this Set is passed by the liveQueryCollection to the compiler\n // such that the liveQueryCollection can check it after compilation\n // to know which collections are lazy collections\n lazyCollections.add(lazyCollection.id)\n\n const activePipeline =\n activeCollection === `main` ? mainPipeline : joinedPipeline\n\n const lazyCollectionJoinExpr =\n activeCollection === `main`\n ? (joinedExpr as PropRef)\n : (mainExpr as PropRef)\n\n const followRefResult = followRef(\n rawQuery,\n lazyCollectionJoinExpr,\n lazyCollection\n )!\n const followRefCollection = followRefResult.collection\n\n const fieldName = followRefResult.path[0]\n if (fieldName) {\n ensureIndexForField(\n fieldName,\n followRefResult.path,\n followRefCollection\n )\n }\n\n const activePipelineWithLoading: IStreamBuilder<\n [key: unknown, [originalKey: string, namespacedRow: NamespacedRow]]\n > = activePipeline.pipe(\n tap((data) => {\n const lazyCollectionSubscription = subscriptions[lazyCollection.id]\n\n if (!lazyCollectionSubscription) {\n throw new Error(\n `Internal error: subscription for collection is missing in join pipeline. Make sure the live query collection sets the subscription before running the pipeline.`\n )\n }\n\n if (lazyCollectionSubscription.hasLoadedInitialState()) {\n // Entire state was already loaded because we deoptimized the join\n return\n }\n\n const joinKeys = data.getInner().map(([[joinKey]]) => joinKey)\n const lazyJoinRef = new PropRef(followRefResult.path)\n const loaded = lazyCollectionSubscription.requestSnapshot({\n where: inArray(lazyJoinRef, joinKeys),\n optimizedOnly: true,\n })\n\n if (!loaded) {\n // Snapshot wasn't sent because it could not be loaded from the indexes\n lazyCollectionSubscription.requestSnapshot()\n }\n })\n )\n\n if (activeCollection === `main`) {\n mainPipeline = activePipelineWithLoading\n } else {\n joinedPipeline = activePipelineWithLoading\n }\n }\n }\n\n return mainPipeline.pipe(\n joinOperator(joinedPipeline, joinClause.type as JoinType),\n consolidate(),\n processJoinResults(joinClause.type)\n )\n}\n\n/**\n * Analyzes join expressions to determine which refers to which table\n * and returns them in the correct order (available table expression first, joined table expression second)\n */\nfunction analyzeJoinExpressions(\n left: BasicExpression,\n right: BasicExpression,\n allAvailableTableAliases: Array<string>,\n joinedTableAlias: string\n): { mainExpr: BasicExpression; joinedExpr: BasicExpression } {\n // Filter out the joined table alias from the available table aliases\n const availableTableAliases = allAvailableTableAliases.filter(\n (alias) => alias !== joinedTableAlias\n )\n\n const leftTableAlias = getTableAliasFromExpression(left)\n const rightTableAlias = getTableAliasFromExpression(right)\n\n // If left expression refers to an available table and right refers to joined table, keep as is\n if (\n leftTableAlias &&\n availableTableAliases.includes(leftTableAlias) &&\n rightTableAlias === joinedTableAlias\n ) {\n return { mainExpr: left, joinedExpr: right }\n }\n\n // If left expression refers to joined table and right refers to an available table, swap them\n if (\n leftTableAlias === joinedTableAlias &&\n rightTableAlias &&\n availableTableAliases.includes(rightTableAlias)\n ) {\n return { mainExpr: right, joinedExpr: left }\n }\n\n // If one expression doesn't refer to any table, this is an invalid join\n if (!leftTableAlias || !rightTableAlias) {\n // For backward compatibility, use the first available table alias in error message\n throw new InvalidJoinConditionTableMismatchError()\n }\n\n // If both expressions refer to the same alias, this is an invalid join\n if (leftTableAlias === rightTableAlias) {\n throw new InvalidJoinConditionSameTableError(leftTableAlias)\n }\n\n // Left side must refer to an available table\n // This cannot happen with the query builder as there is no way to build a ref\n // to an unavailable table, but just in case, but could happen with the IR\n if (!availableTableAliases.includes(leftTableAlias)) {\n throw new InvalidJoinConditionLeftTableError(leftTableAlias)\n }\n\n // Right side must refer to the joined table\n if (rightTableAlias !== joinedTableAlias) {\n throw new InvalidJoinConditionRightTableError(joinedTableAlias)\n }\n\n // This should not be reachable given the logic above, but just in case\n throw new InvalidJoinCondition()\n}\n\n/**\n * Extracts the table alias from a join expression\n */\nfunction getTableAliasFromExpression(expr: BasicExpression): string | null {\n switch (expr.type) {\n case `ref`:\n // PropRef path has the table alias as the first element\n return expr.path[0] || null\n case `func`: {\n // For function expressions, we need to check if all arguments refer to the same table\n const tableAliases = new Set<string>()\n for (const arg of expr.args) {\n const alias = getTableAliasFromExpression(arg)\n if (alias) {\n tableAliases.add(alias)\n }\n }\n // If all arguments refer to the same table, return that table alias\n return tableAliases.size === 1 ? Array.from(tableAliases)[0]! : null\n }\n default:\n // Values (type='val') don't reference any table\n return null\n }\n}\n\n/**\n * Processes the join source (collection or sub-query)\n */\nfunction processJoinSource(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazyCollections: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n cache: QueryCache,\n queryMapping: QueryMapping,\n onCompileSubquery: CompileQueryFn\n): { alias: string; input: KeyedStream; collectionId: string } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.collection.id]\n if (!input) {\n throw new CollectionInputNotFoundError(from.collection.id)\n }\n return { alias: from.alias, input, collectionId: from.collection.id }\n }\n case `queryRef`: {\n // Find the original query for caching purposes\n const originalQuery = queryMapping.get(from.query) || from.query\n\n // Recursively compile the sub-query with cache\n const subQueryResult = onCompileSubquery(\n originalQuery,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazyCollections,\n optimizableOrderByCollections,\n cache,\n queryMapping\n )\n\n // Extract the pipeline from the compilation result\n const subQueryInput = subQueryResult.pipeline\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n return [key, value] as [unknown, any]\n })\n )\n\n return {\n alias: from.alias,\n input: extractedInput as KeyedStream,\n collectionId: subQueryResult.collectionId,\n }\n }\n default:\n throw new UnsupportedJoinSourceTypeError((from as any).type)\n }\n}\n\n/**\n * Processes the results of a join operation\n */\nfunction processJoinResults(joinType: string) {\n return function (\n pipeline: IStreamBuilder<\n [\n key: string,\n [\n [string, NamespacedRow] | undefined,\n [string, NamespacedRow] | undefined,\n ],\n ]\n >\n ): NamespacedAndKeyedStream {\n return pipeline.pipe(\n // Process the join result and handle nulls\n filter((result) => {\n const [_key, [main, joined]] = result\n const mainNamespacedRow = main?.[1]\n const joinedNamespacedRow = joined?.[1]\n\n // Handle different join types\n if (joinType === `inner`) {\n return !!(mainNamespacedRow && joinedNamespacedRow)\n }\n\n if (joinType === `left`) {\n return !!mainNamespacedRow\n }\n\n if (joinType === `right`) {\n return !!joinedNamespacedRow\n }\n\n // For full joins, always include\n return true\n }),\n map((result) => {\n const [_key, [main, joined]] = result\n const mainKey = main?.[0]\n const mainNamespacedRow = main?.[1]\n const joinedKey = joined?.[0]\n const joinedNamespacedRow = joined?.[1]\n\n // Merge the namespaced rows\n const mergedNamespacedRow: NamespacedRow = {}\n\n // Add main row data if it exists\n if (mainNamespacedRow) {\n Object.assign(mergedNamespacedRow, mainNamespacedRow)\n }\n\n // Add joined row data if it exists\n if (joinedNamespacedRow) {\n Object.assign(mergedNamespacedRow, joinedNamespacedRow)\n }\n\n // We create a composite key that combines the main and joined keys\n const resultKey = `[${mainKey},${joinedKey}]`\n\n return [resultKey, mergedNamespacedRow] as [string, NamespacedRow]\n })\n )\n }\n}\n\n/**\n * Returns the active and lazy collections for a join clause.\n * The active collection is the one that we need to fully iterate over\n * and it can be the main table (i.e. left collection) or the joined table (i.e. right collection).\n * The lazy collection is the one that we should join-in lazily based on matches in the active collection.\n * @param joinClause - The join clause to analyze\n * @param leftCollection - The left collection\n * @param rightCollection - The right collection\n * @returns The active and lazy collections. They are undefined if we need to loop over both collections (i.e. both are active)\n */\nfunction getActiveAndLazyCollections(\n joinType: JoinClause[`type`],\n leftCollection: Collection,\n rightCollection: Collection\n):\n | { activeCollection: `main` | `joined`; lazyCollection: Collection }\n | { activeCollection: undefined; lazyCollection: undefined } {\n if (leftCollection.id === rightCollection.id) {\n // We can't apply this optimization if there's only one collection\n // because `liveQueryCollection` will detect that the collection is lazy\n // and treat it lazily (because the collection is shared)\n // and thus it will not load any keys because both sides of the join\n // will be handled lazily\n return { activeCollection: undefined, lazyCollection: undefined }\n }\n\n switch (joinType) {\n case `left`:\n return { activeCollection: `main`, lazyCollection: rightCollection }\n case `right`:\n return { activeCollection: `joined`, lazyCollection: leftCollection }\n case `inner`:\n // The smallest collection should be the active collection\n // and the biggest collection should be lazy\n return leftCollection.size < rightCollection.size\n ? { activeCollection: `main`, lazyCollection: rightCollection }\n : { activeCollection: `joined`, lazyCollection: leftCollection }\n default:\n return { activeCollection: undefined, lazyCollection: undefined }\n }\n}\n"],"names":["joinOperator"],"mappings":"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"}
@@ -1,14 +1,14 @@
1
1
  import { OrderByClause, QueryIR, Select } from '../ir.js';
2
2
  import { NamespacedAndKeyedStream, NamespacedRow } from '../../types.js';
3
3
  import { IStreamBuilder, KeyValue } from '@tanstack/db-ivm';
4
- import { BaseIndex } from '../../indexes/base-index.js';
4
+ import { IndexInterface } from '../../indexes/base-index.js';
5
5
  import { Collection } from '../../collection/index.js';
6
6
  export type OrderByOptimizationInfo = {
7
7
  offset: number;
8
8
  limit: number;
9
9
  comparator: (a: Record<string, unknown> | null | undefined, b: Record<string, unknown> | null | undefined) => number;
10
10
  valueExtractorForRawRow: (row: Record<string, unknown>) => any;
11
- index: BaseIndex<string | number>;
11
+ index: IndexInterface<string | number>;
12
12
  dataNeeded?: () => number;
13
13
  };
14
14
  /**
@@ -18,10 +18,7 @@ function processOrderBy(rawQuery, pipeline, orderByClause, selectClause, collect
18
18
  };
19
19
  });
20
20
  const valueExtractor = (row) => {
21
- const orderByContext = { ...row };
22
- if (row.__select_results) {
23
- Object.assign(orderByContext, row.__select_results);
24
- }
21
+ const orderByContext = row;
25
22
  if (orderByClause.length > 1) {
26
23
  return compiledOrderBy.map(
27
24
  (compiled) => compiled.compiledExpression(orderByContext)
@@ -70,6 +67,7 @@ function processOrderBy(rawQuery, pipeline, orderByClause, selectClause, collect
70
67
  fieldName,
71
68
  followRefResult.path,
72
69
  followRefCollection,
70
+ clause.compareOptions,
73
71
  compare
74
72
  );
75
73
  }
@@ -84,7 +82,8 @@ function processOrderBy(rawQuery, pipeline, orderByClause, selectClause, collect
84
82
  };
85
83
  const index = findIndexForField(
86
84
  followRefCollection.indexes,
87
- followRefResult.path
85
+ followRefResult.path,
86
+ clause.compareOptions
88
87
  );
89
88
  if (index && index.supports(`gt`)) {
90
89
  const orderByOptimizationInfo = {
@@ -1 +1 @@
1
- {"version":3,"file":"order-by.js","sources":["../../../../src/query/compiler/order-by.ts"],"sourcesContent":["import { orderByWithFractionalIndex } from \"@tanstack/db-ivm\"\nimport { defaultComparator, makeComparator } from \"../../utils/comparison.js\"\nimport { PropRef, followRef } from \"../ir.js\"\nimport { ensureIndexForField } from \"../../indexes/auto-index.js\"\nimport { findIndexForField } from \"../../utils/index-optimization.js\"\nimport { compileExpression } from \"./evaluators.js\"\nimport { replaceAggregatesByRefs } from \"./group-by.js\"\nimport type { CompiledSingleRowExpression } from \"./evaluators.js\"\nimport type { OrderByClause, QueryIR, Select } from \"../ir.js\"\nimport type { NamespacedAndKeyedStream, NamespacedRow } from \"../../types.js\"\nimport type { IStreamBuilder, KeyValue } from \"@tanstack/db-ivm\"\nimport type { BaseIndex } from \"../../indexes/base-index.js\"\nimport type { Collection } from \"../../collection/index.js\"\n\nexport type OrderByOptimizationInfo = {\n offset: number\n limit: number\n comparator: (\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined\n ) => number\n valueExtractorForRawRow: (row: Record<string, unknown>) => any\n index: BaseIndex<string | number>\n dataNeeded?: () => number\n}\n\n/**\n * Processes the ORDER BY clause\n * Works with the new structure that has both namespaced row data and __select_results\n * Always uses fractional indexing and adds the index as __ordering_index to the result\n */\nexport function processOrderBy(\n rawQuery: QueryIR,\n pipeline: NamespacedAndKeyedStream,\n orderByClause: Array<OrderByClause>,\n selectClause: Select,\n collection: Collection,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n limit?: number,\n offset?: number\n): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> {\n // Pre-compile all order by expressions\n const compiledOrderBy = orderByClause.map((clause) => {\n const clauseWithoutAggregates = replaceAggregatesByRefs(\n clause.expression,\n selectClause,\n `__select_results`\n )\n return {\n compiledExpression: compileExpression(clauseWithoutAggregates),\n compareOptions: clause.compareOptions,\n }\n })\n\n // Create a value extractor function for the orderBy operator\n const valueExtractor = (row: NamespacedRow & { __select_results?: any }) => {\n // For ORDER BY expressions, we need to provide access to both:\n // 1. The original namespaced row data (for direct table column references)\n // 2. The __select_results (for SELECT alias references)\n\n // Create a merged context for expression evaluation\n const orderByContext = { ...row }\n\n // If there are select results, merge them at the top level for alias access\n if (row.__select_results) {\n // Add select results as top-level properties for alias access\n Object.assign(orderByContext, row.__select_results)\n }\n\n if (orderByClause.length > 1) {\n // For multiple orderBy columns, create a composite key\n return compiledOrderBy.map((compiled) =>\n compiled.compiledExpression(orderByContext)\n )\n } else if (orderByClause.length === 1) {\n // For a single orderBy column, use the value directly\n const compiled = compiledOrderBy[0]!\n return compiled.compiledExpression(orderByContext)\n }\n\n // Default case - no ordering\n return null\n }\n\n // Create a multi-property comparator that respects the order and direction of each property\n const compare = (a: unknown, b: unknown) => {\n // If we're comparing arrays (multiple properties), compare each property in order\n if (orderByClause.length > 1) {\n const arrayA = a as Array<unknown>\n const arrayB = b as Array<unknown>\n for (let i = 0; i < orderByClause.length; i++) {\n const clause = orderByClause[i]!\n const compareFn = makeComparator(clause.compareOptions)\n const result = compareFn(arrayA[i], arrayB[i])\n if (result !== 0) {\n return result\n }\n }\n return arrayA.length - arrayB.length\n }\n\n // Single property comparison\n if (orderByClause.length === 1) {\n const clause = orderByClause[0]!\n const compareFn = makeComparator(clause.compareOptions)\n return compareFn(a, b)\n }\n\n return defaultComparator(a, b)\n }\n\n let setSizeCallback: ((getSize: () => number) => void) | undefined\n\n // Optimize the orderBy operator to lazily load elements\n // by using the range index of the collection.\n // Only for orderBy clause on a single column for now (no composite ordering)\n if (limit && orderByClause.length === 1) {\n const clause = orderByClause[0]!\n const orderByExpression = clause.expression\n\n if (orderByExpression.type === `ref`) {\n const followRefResult = followRef(\n rawQuery,\n orderByExpression,\n collection\n )!\n\n const followRefCollection = followRefResult.collection\n const fieldName = followRefResult.path[0]\n if (fieldName) {\n ensureIndexForField(\n fieldName,\n followRefResult.path,\n followRefCollection,\n compare\n )\n }\n\n const valueExtractorForRawRow = compileExpression(\n new PropRef(followRefResult.path),\n true\n ) as CompiledSingleRowExpression\n\n const comparator = (\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined\n ) => {\n const extractedA = a ? valueExtractorForRawRow(a) : a\n const extractedB = b ? valueExtractorForRawRow(b) : b\n return compare(extractedA, extractedB)\n }\n\n const index: BaseIndex<string | number> | undefined = findIndexForField(\n followRefCollection.indexes,\n followRefResult.path\n )\n\n if (index && index.supports(`gt`)) {\n // We found an index that we can use to lazily load ordered data\n const orderByOptimizationInfo = {\n offset: offset ?? 0,\n limit,\n comparator,\n valueExtractorForRawRow,\n index,\n }\n\n optimizableOrderByCollections[followRefCollection.id] =\n orderByOptimizationInfo\n\n setSizeCallback = (getSize: () => number) => {\n optimizableOrderByCollections[followRefCollection.id] = {\n ...optimizableOrderByCollections[followRefCollection.id]!,\n dataNeeded: () => {\n const size = getSize()\n return Math.max(0, limit - size)\n },\n }\n }\n }\n }\n }\n\n // Use fractional indexing and return the tuple [value, index]\n return pipeline.pipe(\n orderByWithFractionalIndex(valueExtractor, {\n limit,\n offset,\n comparator: compare,\n setSizeCallback,\n })\n // orderByWithFractionalIndex returns [key, [value, index]] - we keep this format\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"order-by.js","sources":["../../../../src/query/compiler/order-by.ts"],"sourcesContent":["import { orderByWithFractionalIndex } from \"@tanstack/db-ivm\"\nimport { defaultComparator, makeComparator } from \"../../utils/comparison.js\"\nimport { PropRef, followRef } from \"../ir.js\"\nimport { ensureIndexForField } from \"../../indexes/auto-index.js\"\nimport { findIndexForField } from \"../../utils/index-optimization.js\"\nimport { compileExpression } from \"./evaluators.js\"\nimport { replaceAggregatesByRefs } from \"./group-by.js\"\nimport type { CompiledSingleRowExpression } from \"./evaluators.js\"\nimport type { OrderByClause, QueryIR, Select } from \"../ir.js\"\nimport type { NamespacedAndKeyedStream, NamespacedRow } from \"../../types.js\"\nimport type { IStreamBuilder, KeyValue } from \"@tanstack/db-ivm\"\nimport type { IndexInterface } from \"../../indexes/base-index.js\"\nimport type { Collection } from \"../../collection/index.js\"\n\nexport type OrderByOptimizationInfo = {\n offset: number\n limit: number\n comparator: (\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined\n ) => number\n valueExtractorForRawRow: (row: Record<string, unknown>) => any\n index: IndexInterface<string | number>\n dataNeeded?: () => number\n}\n\n/**\n * Processes the ORDER BY clause\n * Works with the new structure that has both namespaced row data and __select_results\n * Always uses fractional indexing and adds the index as __ordering_index to the result\n */\nexport function processOrderBy(\n rawQuery: QueryIR,\n pipeline: NamespacedAndKeyedStream,\n orderByClause: Array<OrderByClause>,\n selectClause: Select,\n collection: Collection,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n limit?: number,\n offset?: number\n): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> {\n // Pre-compile all order by expressions\n const compiledOrderBy = orderByClause.map((clause) => {\n const clauseWithoutAggregates = replaceAggregatesByRefs(\n clause.expression,\n selectClause,\n `__select_results`\n )\n return {\n compiledExpression: compileExpression(clauseWithoutAggregates),\n compareOptions: clause.compareOptions,\n }\n })\n\n // Create a value extractor function for the orderBy operator\n const valueExtractor = (row: NamespacedRow & { __select_results?: any }) => {\n // The namespaced row contains:\n // 1. Table aliases as top-level properties (e.g., row[\"tableName\"])\n // 2. SELECT results in __select_results (e.g., row.__select_results[\"aggregateAlias\"])\n // The replaceAggregatesByRefs function has already transformed any aggregate expressions\n // that match SELECT aggregates to use the __select_results namespace.\n const orderByContext = row\n\n if (orderByClause.length > 1) {\n // For multiple orderBy columns, create a composite key\n return compiledOrderBy.map((compiled) =>\n compiled.compiledExpression(orderByContext)\n )\n } else if (orderByClause.length === 1) {\n // For a single orderBy column, use the value directly\n const compiled = compiledOrderBy[0]!\n return compiled.compiledExpression(orderByContext)\n }\n\n // Default case - no ordering\n return null\n }\n\n // Create a multi-property comparator that respects the order and direction of each property\n const compare = (a: unknown, b: unknown) => {\n // If we're comparing arrays (multiple properties), compare each property in order\n if (orderByClause.length > 1) {\n const arrayA = a as Array<unknown>\n const arrayB = b as Array<unknown>\n for (let i = 0; i < orderByClause.length; i++) {\n const clause = orderByClause[i]!\n const compareFn = makeComparator(clause.compareOptions)\n const result = compareFn(arrayA[i], arrayB[i])\n if (result !== 0) {\n return result\n }\n }\n return arrayA.length - arrayB.length\n }\n\n // Single property comparison\n if (orderByClause.length === 1) {\n const clause = orderByClause[0]!\n const compareFn = makeComparator(clause.compareOptions)\n return compareFn(a, b)\n }\n\n return defaultComparator(a, b)\n }\n\n let setSizeCallback: ((getSize: () => number) => void) | undefined\n\n // Optimize the orderBy operator to lazily load elements\n // by using the range index of the collection.\n // Only for orderBy clause on a single column for now (no composite ordering)\n if (limit && orderByClause.length === 1) {\n const clause = orderByClause[0]!\n const orderByExpression = clause.expression\n\n if (orderByExpression.type === `ref`) {\n const followRefResult = followRef(\n rawQuery,\n orderByExpression,\n collection\n )!\n\n const followRefCollection = followRefResult.collection\n const fieldName = followRefResult.path[0]\n if (fieldName) {\n ensureIndexForField(\n fieldName,\n followRefResult.path,\n followRefCollection,\n clause.compareOptions,\n compare\n )\n }\n\n const valueExtractorForRawRow = compileExpression(\n new PropRef(followRefResult.path),\n true\n ) as CompiledSingleRowExpression\n\n const comparator = (\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined\n ) => {\n const extractedA = a ? valueExtractorForRawRow(a) : a\n const extractedB = b ? valueExtractorForRawRow(b) : b\n return compare(extractedA, extractedB)\n }\n\n const index: IndexInterface<string | number> | undefined =\n findIndexForField(\n followRefCollection.indexes,\n followRefResult.path,\n clause.compareOptions\n )\n\n if (index && index.supports(`gt`)) {\n // We found an index that we can use to lazily load ordered data\n const orderByOptimizationInfo = {\n offset: offset ?? 0,\n limit,\n comparator,\n valueExtractorForRawRow,\n index,\n }\n\n optimizableOrderByCollections[followRefCollection.id] =\n orderByOptimizationInfo\n\n setSizeCallback = (getSize: () => number) => {\n optimizableOrderByCollections[followRefCollection.id] = {\n ...optimizableOrderByCollections[followRefCollection.id]!,\n dataNeeded: () => {\n const size = getSize()\n return Math.max(0, limit - size)\n },\n }\n }\n }\n }\n }\n\n // Use fractional indexing and return the tuple [value, index]\n return pipeline.pipe(\n orderByWithFractionalIndex(valueExtractor, {\n limit,\n offset,\n comparator: compare,\n setSizeCallback,\n })\n // orderByWithFractionalIndex returns [key, [value, index]] - we keep this format\n )\n}\n"],"names":[],"mappings":"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"}
@@ -18,7 +18,7 @@ function processMerge(op, namespacedRow, selectResults) {
18
18
  for (let i = 0; i < path.length; i++) {
19
19
  const seg = path[i];
20
20
  if (i === path.length - 1) {
21
- const dest = cursor[seg] ?? (cursor[seg] = {});
21
+ const dest = cursor[seg] ??= {};
22
22
  if (typeof dest === `object`) {
23
23
  for (const [k, v] of Object.entries(value)) {
24
24
  dest[k] = unwrapVal(v);
@@ -1 +1 @@
1
- {"version":3,"file":"select.js","sources":["../../../../src/query/compiler/select.ts"],"sourcesContent":["import { map } from \"@tanstack/db-ivm\"\nimport { PropRef, Value as ValClass, isExpressionLike } from \"../ir.js\"\nimport { compileExpression } from \"./evaluators.js\"\nimport type { Aggregate, BasicExpression, Select } from \"../ir.js\"\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n NamespacedRow,\n} from \"../../types.js\"\n\n/**\n * Type for operations array used in select processing\n */\ntype SelectOp =\n | {\n kind: `merge`\n targetPath: Array<string>\n source: (row: NamespacedRow) => any\n }\n | { kind: `field`; alias: string; compiled: (row: NamespacedRow) => any }\n\n/**\n * Unwraps any Value expressions\n */\nfunction unwrapVal(input: any): any {\n if (input instanceof ValClass) return input.value\n return input\n}\n\n/**\n * Processes a merge operation by merging source values into the target path\n */\nfunction processMerge(\n op: Extract<SelectOp, { kind: `merge` }>,\n namespacedRow: NamespacedRow,\n selectResults: Record<string, any>\n): void {\n const value = op.source(namespacedRow)\n if (value && typeof value === `object`) {\n // Ensure target object exists\n let cursor: any = selectResults\n const path = op.targetPath\n if (path.length === 0) {\n // Top-level merge\n for (const [k, v] of Object.entries(value)) {\n selectResults[k] = unwrapVal(v)\n }\n } else {\n for (let i = 0; i < path.length; i++) {\n const seg = path[i]!\n if (i === path.length - 1) {\n const dest = (cursor[seg] ??= {})\n if (typeof dest === `object`) {\n for (const [k, v] of Object.entries(value)) {\n dest[k] = unwrapVal(v)\n }\n }\n } else {\n const next = cursor[seg]\n if (next == null || typeof next !== `object`) {\n cursor[seg] = {}\n }\n cursor = cursor[seg]\n }\n }\n }\n }\n}\n\n/**\n * Processes a non-merge operation by setting the field value at the specified alias path\n */\nfunction processNonMergeOp(\n op: Extract<SelectOp, { kind: `field` }>,\n namespacedRow: NamespacedRow,\n selectResults: Record<string, any>\n): void {\n // Support nested alias paths like \"meta.author.name\"\n const path = op.alias.split(`.`)\n if (path.length === 1) {\n selectResults[op.alias] = op.compiled(namespacedRow)\n } else {\n let cursor: any = selectResults\n for (let i = 0; i < path.length - 1; i++) {\n const seg = path[i]!\n const next = cursor[seg]\n if (next == null || typeof next !== `object`) {\n cursor[seg] = {}\n }\n cursor = cursor[seg]\n }\n cursor[path[path.length - 1]!] = unwrapVal(op.compiled(namespacedRow))\n }\n}\n\n/**\n * Processes a single row to generate select results\n */\nfunction processRow(\n [key, namespacedRow]: [unknown, NamespacedRow],\n ops: Array<SelectOp>\n): [unknown, typeof namespacedRow & { __select_results: any }] {\n const selectResults: Record<string, any> = {}\n\n for (const op of ops) {\n if (op.kind === `merge`) {\n processMerge(op, namespacedRow, selectResults)\n } else {\n processNonMergeOp(op, namespacedRow, selectResults)\n }\n }\n\n // Return the namespaced row with __select_results added\n return [\n key,\n {\n ...namespacedRow,\n __select_results: selectResults,\n },\n ] as [\n unknown,\n typeof namespacedRow & { __select_results: typeof selectResults },\n ]\n}\n\n/**\n * Processes the SELECT clause and places results in __select_results\n * while preserving the original namespaced row for ORDER BY access\n */\nexport function processSelect(\n pipeline: NamespacedAndKeyedStream,\n select: Select,\n _allInputs: Record<string, KeyedStream>\n): NamespacedAndKeyedStream {\n // Build ordered operations to preserve authoring order (spreads and fields)\n const ops: Array<SelectOp> = []\n\n addFromObject([], select, ops)\n\n return pipeline.pipe(map((row) => processRow(row, ops)))\n}\n\n/**\n * Helper function to check if an expression is an aggregate\n */\nfunction isAggregateExpression(\n expr: BasicExpression | Aggregate\n): expr is Aggregate {\n return expr.type === `agg`\n}\n\n/**\n * Processes a single argument in a function context\n */\nexport function processArgument(\n arg: BasicExpression | Aggregate,\n namespacedRow: NamespacedRow\n): any {\n if (isAggregateExpression(arg)) {\n throw new Error(\n `Aggregate expressions are not supported in this context. Use GROUP BY clause for aggregates.`\n )\n }\n\n // Pre-compile the expression and evaluate immediately\n const compiledExpression = compileExpression(arg)\n const value = compiledExpression(namespacedRow)\n\n return value\n}\n\n/**\n * Helper function to check if an object is a nested select object\n *\n * .select({\n * id: users.id,\n * profile: { // <-- this is a nested select object\n * name: users.name,\n * email: users.email\n * }\n * })\n */\nfunction isNestedSelectObject(obj: any): boolean {\n return obj && typeof obj === `object` && !isExpressionLike(obj)\n}\n\n/**\n * Helper function to process select objects and build operations array\n */\nfunction addFromObject(\n prefixPath: Array<string>,\n obj: any,\n ops: Array<SelectOp>\n) {\n for (const [key, value] of Object.entries(obj)) {\n if (key.startsWith(`__SPREAD_SENTINEL__`)) {\n const rest = key.slice(`__SPREAD_SENTINEL__`.length)\n const splitIndex = rest.lastIndexOf(`__`)\n const pathStr = splitIndex >= 0 ? rest.slice(0, splitIndex) : rest\n const isRefExpr =\n value &&\n typeof value === `object` &&\n `type` in (value as any) &&\n (value as any).type === `ref`\n if (pathStr.includes(`.`) || isRefExpr) {\n // Merge into the current destination (prefixPath) from the referenced source path\n const targetPath = [...prefixPath]\n const expr = isRefExpr\n ? (value as BasicExpression)\n : (new PropRef(pathStr.split(`.`)) as BasicExpression)\n const compiled = compileExpression(expr)\n ops.push({ kind: `merge`, targetPath, source: compiled })\n } else {\n // Table-level: pathStr is the alias; merge from namespaced row at the current prefix\n const tableAlias = pathStr\n const targetPath = [...prefixPath]\n ops.push({\n kind: `merge`,\n targetPath,\n source: (row) => (row as any)[tableAlias],\n })\n }\n continue\n }\n\n const expression = value as any\n if (isNestedSelectObject(expression)) {\n // Nested selection object\n addFromObject([...prefixPath, key], expression, ops)\n continue\n }\n\n if (isAggregateExpression(expression)) {\n // Placeholder for group-by processing later\n ops.push({\n kind: `field`,\n alias: [...prefixPath, key].join(`.`),\n compiled: () => null,\n })\n } else {\n if (expression === undefined || !isExpressionLike(expression)) {\n ops.push({\n kind: `field`,\n alias: [...prefixPath, key].join(`.`),\n compiled: () => expression,\n })\n continue\n }\n // If the expression is a Value wrapper, embed the literal to avoid re-compilation mishaps\n if (expression instanceof ValClass) {\n const val = expression.value\n ops.push({\n kind: `field`,\n alias: [...prefixPath, key].join(`.`),\n compiled: () => val,\n })\n } else {\n ops.push({\n kind: `field`,\n alias: [...prefixPath, key].join(`.`),\n compiled: compileExpression(expression as BasicExpression),\n })\n }\n }\n }\n}\n"],"names":["ValClass"],"mappings":"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"}
1
+ {"version":3,"file":"select.js","sources":["../../../../src/query/compiler/select.ts"],"sourcesContent":["import { map } from \"@tanstack/db-ivm\"\nimport { PropRef, Value as ValClass, isExpressionLike } from \"../ir.js\"\nimport { compileExpression } from \"./evaluators.js\"\nimport type { Aggregate, BasicExpression, Select } from \"../ir.js\"\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n NamespacedRow,\n} from \"../../types.js\"\n\n/**\n * Type for operations array used in select processing\n */\ntype SelectOp =\n | {\n kind: `merge`\n targetPath: Array<string>\n source: (row: NamespacedRow) => any\n }\n | { kind: `field`; alias: string; compiled: (row: NamespacedRow) => any }\n\n/**\n * Unwraps any Value expressions\n */\nfunction unwrapVal(input: any): any {\n if (input instanceof ValClass) return input.value\n return input\n}\n\n/**\n * Processes a merge operation by merging source values into the target path\n */\nfunction processMerge(\n op: Extract<SelectOp, { kind: `merge` }>,\n namespacedRow: NamespacedRow,\n selectResults: Record<string, any>\n): void {\n const value = op.source(namespacedRow)\n if (value && typeof value === `object`) {\n // Ensure target object exists\n let cursor: any = selectResults\n const path = op.targetPath\n if (path.length === 0) {\n // Top-level merge\n for (const [k, v] of Object.entries(value)) {\n selectResults[k] = unwrapVal(v)\n }\n } else {\n for (let i = 0; i < path.length; i++) {\n const seg = path[i]!\n if (i === path.length - 1) {\n const dest = (cursor[seg] ??= {})\n if (typeof dest === `object`) {\n for (const [k, v] of Object.entries(value)) {\n dest[k] = unwrapVal(v)\n }\n }\n } else {\n const next = cursor[seg]\n if (next == null || typeof next !== `object`) {\n cursor[seg] = {}\n }\n cursor = cursor[seg]\n }\n }\n }\n }\n}\n\n/**\n * Processes a non-merge operation by setting the field value at the specified alias path\n */\nfunction processNonMergeOp(\n op: Extract<SelectOp, { kind: `field` }>,\n namespacedRow: NamespacedRow,\n selectResults: Record<string, any>\n): void {\n // Support nested alias paths like \"meta.author.name\"\n const path = op.alias.split(`.`)\n if (path.length === 1) {\n selectResults[op.alias] = op.compiled(namespacedRow)\n } else {\n let cursor: any = selectResults\n for (let i = 0; i < path.length - 1; i++) {\n const seg = path[i]!\n const next = cursor[seg]\n if (next == null || typeof next !== `object`) {\n cursor[seg] = {}\n }\n cursor = cursor[seg]\n }\n cursor[path[path.length - 1]!] = unwrapVal(op.compiled(namespacedRow))\n }\n}\n\n/**\n * Processes a single row to generate select results\n */\nfunction processRow(\n [key, namespacedRow]: [unknown, NamespacedRow],\n ops: Array<SelectOp>\n): [unknown, typeof namespacedRow & { __select_results: any }] {\n const selectResults: Record<string, any> = {}\n\n for (const op of ops) {\n if (op.kind === `merge`) {\n processMerge(op, namespacedRow, selectResults)\n } else {\n processNonMergeOp(op, namespacedRow, selectResults)\n }\n }\n\n // Return the namespaced row with __select_results added\n return [\n key,\n {\n ...namespacedRow,\n __select_results: selectResults,\n },\n ] as [\n unknown,\n typeof namespacedRow & { __select_results: typeof selectResults },\n ]\n}\n\n/**\n * Processes the SELECT clause and places results in __select_results\n * while preserving the original namespaced row for ORDER BY access\n */\nexport function processSelect(\n pipeline: NamespacedAndKeyedStream,\n select: Select,\n _allInputs: Record<string, KeyedStream>\n): NamespacedAndKeyedStream {\n // Build ordered operations to preserve authoring order (spreads and fields)\n const ops: Array<SelectOp> = []\n\n addFromObject([], select, ops)\n\n return pipeline.pipe(map((row) => processRow(row, ops)))\n}\n\n/**\n * Helper function to check if an expression is an aggregate\n */\nfunction isAggregateExpression(\n expr: BasicExpression | Aggregate\n): expr is Aggregate {\n return expr.type === `agg`\n}\n\n/**\n * Processes a single argument in a function context\n */\nexport function processArgument(\n arg: BasicExpression | Aggregate,\n namespacedRow: NamespacedRow\n): any {\n if (isAggregateExpression(arg)) {\n throw new Error(\n `Aggregate expressions are not supported in this context. Use GROUP BY clause for aggregates.`\n )\n }\n\n // Pre-compile the expression and evaluate immediately\n const compiledExpression = compileExpression(arg)\n const value = compiledExpression(namespacedRow)\n\n return value\n}\n\n/**\n * Helper function to check if an object is a nested select object\n *\n * .select({\n * id: users.id,\n * profile: { // <-- this is a nested select object\n * name: users.name,\n * email: users.email\n * }\n * })\n */\nfunction isNestedSelectObject(obj: any): boolean {\n return obj && typeof obj === `object` && !isExpressionLike(obj)\n}\n\n/**\n * Helper function to process select objects and build operations array\n */\nfunction addFromObject(\n prefixPath: Array<string>,\n obj: any,\n ops: Array<SelectOp>\n) {\n for (const [key, value] of Object.entries(obj)) {\n if (key.startsWith(`__SPREAD_SENTINEL__`)) {\n const rest = key.slice(`__SPREAD_SENTINEL__`.length)\n const splitIndex = rest.lastIndexOf(`__`)\n const pathStr = splitIndex >= 0 ? rest.slice(0, splitIndex) : rest\n const isRefExpr =\n value &&\n typeof value === `object` &&\n `type` in (value as any) &&\n (value as any).type === `ref`\n if (pathStr.includes(`.`) || isRefExpr) {\n // Merge into the current destination (prefixPath) from the referenced source path\n const targetPath = [...prefixPath]\n const expr = isRefExpr\n ? (value as BasicExpression)\n : (new PropRef(pathStr.split(`.`)) as BasicExpression)\n const compiled = compileExpression(expr)\n ops.push({ kind: `merge`, targetPath, source: compiled })\n } else {\n // Table-level: pathStr is the alias; merge from namespaced row at the current prefix\n const tableAlias = pathStr\n const targetPath = [...prefixPath]\n ops.push({\n kind: `merge`,\n targetPath,\n source: (row) => (row as any)[tableAlias],\n })\n }\n continue\n }\n\n const expression = value as any\n if (isNestedSelectObject(expression)) {\n // Nested selection object\n addFromObject([...prefixPath, key], expression, ops)\n continue\n }\n\n if (isAggregateExpression(expression)) {\n // Placeholder for group-by processing later\n ops.push({\n kind: `field`,\n alias: [...prefixPath, key].join(`.`),\n compiled: () => null,\n })\n } else {\n if (expression === undefined || !isExpressionLike(expression)) {\n ops.push({\n kind: `field`,\n alias: [...prefixPath, key].join(`.`),\n compiled: () => expression,\n })\n continue\n }\n // If the expression is a Value wrapper, embed the literal to avoid re-compilation mishaps\n if (expression instanceof ValClass) {\n const val = expression.value\n ops.push({\n kind: `field`,\n alias: [...prefixPath, key].join(`.`),\n compiled: () => val,\n })\n } else {\n ops.push({\n kind: `field`,\n alias: [...prefixPath, key].join(`.`),\n compiled: compileExpression(expression as BasicExpression),\n })\n }\n }\n }\n}\n"],"names":["ValClass"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- export { BaseQueryBuilder, Query, type InitialQueryBuilder, type QueryBuilder, type Context, type Source, type GetResult, } from './builder/index.js';
1
+ export { BaseQueryBuilder, Query, type InitialQueryBuilder, type QueryBuilder, type Context, type Source, type GetResult, type InferResultType, } from './builder/index.js';
2
2
  export { eq, gt, gte, lt, lte, and, or, not, inArray, like, ilike, isUndefined, isNull, upper, lower, length, concat, coalesce, add, count, avg, sum, min, max, } from './builder/functions.js';
3
3
  export type { Ref } from './builder/types.js';
4
4
  export { compileQuery } from './compiler/index.js';
@@ -12,6 +12,7 @@ export interface QueryIR {
12
12
  limit?: Limit;
13
13
  offset?: Offset;
14
14
  distinct?: true;
15
+ singleResult?: true;
15
16
  fnSelect?: (row: NamespacedRow) => any;
16
17
  fnWhere?: Array<(row: NamespacedRow) => any>;
17
18
  fnHaving?: Array<(row: NamespacedRow) => any>;
@@ -1 +1 @@
1
- {"version":3,"file":"ir.js","sources":["../../../src/query/ir.ts"],"sourcesContent":["/*\nThis is the intermediate representation of the query.\n*/\n\nimport type { CompareOptions } from \"./builder/types\"\nimport type { Collection, CollectionImpl } from \"../collection/index.js\"\nimport type { NamespacedRow } from \"../types\"\n\nexport interface QueryIR {\n from: From\n select?: Select\n join?: Join\n where?: Array<Where>\n groupBy?: GroupBy\n having?: Array<Having>\n orderBy?: OrderBy\n limit?: Limit\n offset?: Offset\n distinct?: true\n\n // Functional variants\n fnSelect?: (row: NamespacedRow) => any\n fnWhere?: Array<(row: NamespacedRow) => any>\n fnHaving?: Array<(row: NamespacedRow) => any>\n}\n\nexport type From = CollectionRef | QueryRef\n\nexport type Select = {\n [alias: string]: BasicExpression | Aggregate | Select\n}\n\nexport type Join = Array<JoinClause>\n\nexport interface JoinClause {\n from: CollectionRef | QueryRef\n type: `left` | `right` | `inner` | `outer` | `full` | `cross`\n left: BasicExpression\n right: BasicExpression\n}\n\nexport type Where =\n | BasicExpression<boolean>\n | { expression: BasicExpression<boolean>; residual?: boolean }\n\nexport type GroupBy = Array<BasicExpression>\n\nexport type Having = Where\n\nexport type OrderBy = Array<OrderByClause>\n\nexport type OrderByClause = {\n expression: BasicExpression\n compareOptions: CompareOptions\n}\n\nexport type OrderByDirection = `asc` | `desc`\n\nexport type Limit = number\n\nexport type Offset = number\n\n/* Expressions */\n\nabstract class BaseExpression<T = any> {\n public abstract type: string\n /** @internal - Type brand for TypeScript inference */\n declare readonly __returnType: T\n}\n\nexport class CollectionRef extends BaseExpression {\n public type = `collectionRef` as const\n constructor(\n public collection: CollectionImpl,\n public alias: string\n ) {\n super()\n }\n}\n\nexport class QueryRef extends BaseExpression {\n public type = `queryRef` as const\n constructor(\n public query: QueryIR,\n public alias: string\n ) {\n super()\n }\n}\n\nexport class PropRef<T = any> extends BaseExpression<T> {\n public type = `ref` as const\n constructor(\n public path: Array<string> // path to the property in the collection, with the alias as the first element\n ) {\n super()\n }\n}\n\nexport class Value<T = any> extends BaseExpression<T> {\n public type = `val` as const\n constructor(\n public value: T // any js value\n ) {\n super()\n }\n}\n\nexport class Func<T = any> extends BaseExpression<T> {\n public type = `func` as const\n constructor(\n public name: string, // such as eq, gt, lt, upper, lower, etc.\n public args: Array<BasicExpression>\n ) {\n super()\n }\n}\n\n// This is the basic expression type that is used in the majority of expression\n// builder callbacks (select, where, groupBy, having, orderBy, etc.)\n// it doesn't include aggregate functions as those are only used in the select clause\nexport type BasicExpression<T = any> = PropRef<T> | Value<T> | Func<T>\n\nexport class Aggregate<T = any> extends BaseExpression<T> {\n public type = `agg` as const\n constructor(\n public name: string, // such as count, avg, sum, min, max, etc.\n public args: Array<BasicExpression>\n ) {\n super()\n }\n}\n\n/**\n * Runtime helper to detect IR expression-like objects.\n * Prefer this over ad-hoc local implementations to keep behavior consistent.\n */\nexport function isExpressionLike(value: any): boolean {\n return (\n value instanceof Aggregate ||\n value instanceof Func ||\n value instanceof PropRef ||\n value instanceof Value\n )\n}\n\n/**\n * Helper functions for working with Where clauses\n */\n\n/**\n * Extract the expression from a Where clause\n */\nexport function getWhereExpression(where: Where): BasicExpression<boolean> {\n return typeof where === `object` && `expression` in where\n ? where.expression\n : where\n}\n\n/**\n * Extract the expression from a HAVING clause\n * HAVING clauses can contain aggregates, unlike regular WHERE clauses\n */\nexport function getHavingExpression(\n having: Having\n): BasicExpression | Aggregate {\n return typeof having === `object` && `expression` in having\n ? having.expression\n : having\n}\n\n/**\n * Check if a Where clause is marked as residual\n */\nexport function isResidualWhere(where: Where): boolean {\n return (\n typeof where === `object` &&\n `expression` in where &&\n where.residual === true\n )\n}\n\n/**\n * Create a residual Where clause from an expression\n */\nexport function createResidualWhere(\n expression: BasicExpression<boolean>\n): Where {\n return { expression, residual: true }\n}\n\nfunction getRefFromAlias(\n query: QueryIR,\n alias: string\n): CollectionRef | QueryRef | void {\n if (query.from.alias === alias) {\n return query.from\n }\n\n for (const join of query.join || []) {\n if (join.from.alias === alias) {\n return join.from\n }\n }\n}\n\n/**\n * Follows the given reference in a query\n * until its finds the root field the reference points to.\n * @returns The collection, its alias, and the path to the root field in this collection\n */\nexport function followRef(\n query: QueryIR,\n ref: PropRef<any>,\n collection: Collection\n): { collection: Collection; path: Array<string> } | void {\n if (ref.path.length === 0) {\n return\n }\n\n if (ref.path.length === 1) {\n // This field should be part of this collection\n const field = ref.path[0]!\n // is it part of the select clause?\n if (query.select) {\n const selectedField = query.select[field]\n if (selectedField && selectedField.type === `ref`) {\n return followRef(query, selectedField, collection)\n }\n }\n\n // Either this field is not part of the select clause\n // and thus it must be part of the collection itself\n // or it is part of the select but is not a reference\n // so we can stop here and don't have to follow it\n return { collection, path: [field] }\n }\n\n if (ref.path.length > 1) {\n // This is a nested field\n const [alias, ...rest] = ref.path\n const aliasRef = getRefFromAlias(query, alias!)\n if (!aliasRef) {\n return\n }\n\n if (aliasRef.type === `queryRef`) {\n return followRef(aliasRef.query, new PropRef(rest), collection)\n } else {\n // This is a reference to a collection\n // we can't follow it further\n // so the field must be on the collection itself\n return { collection: aliasRef.collection, path: rest }\n }\n }\n}\n"],"names":[],"mappings":"AAgEA,MAAe,eAAwB;AAIvC;AAEO,MAAM,sBAAsB,eAAe;AAAA,EAEhD,YACS,YACA,OACP;AACA,UAAA;AAHO,SAAA,aAAA;AACA,SAAA,QAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAEO,MAAM,iBAAiB,eAAe;AAAA,EAE3C,YACS,OACA,OACP;AACA,UAAA;AAHO,SAAA,QAAA;AACA,SAAA,QAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAEO,MAAM,gBAAyB,eAAkB;AAAA,EAEtD,YACS,MACP;AACA,UAAA;AAFO,SAAA,OAAA;AAFT,SAAO,OAAO;AAAA,EAKd;AACF;AAEO,MAAM,cAAuB,eAAkB;AAAA,EAEpD,YACS,OACP;AACA,UAAA;AAFO,SAAA,QAAA;AAFT,SAAO,OAAO;AAAA,EAKd;AACF;AAEO,MAAM,aAAsB,eAAkB;AAAA,EAEnD,YACS,MACA,MACP;AACA,UAAA;AAHO,SAAA,OAAA;AACA,SAAA,OAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAOO,MAAM,kBAA2B,eAAkB;AAAA,EAExD,YACS,MACA,MACP;AACA,UAAA;AAHO,SAAA,OAAA;AACA,SAAA,OAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAMO,SAAS,iBAAiB,OAAqB;AACpD,SACE,iBAAiB,aACjB,iBAAiB,QACjB,iBAAiB,WACjB,iBAAiB;AAErB;AASO,SAAS,mBAAmB,OAAwC;AACzE,SAAO,OAAO,UAAU,YAAY,gBAAgB,QAChD,MAAM,aACN;AACN;AAMO,SAAS,oBACd,QAC6B;AAC7B,SAAO,OAAO,WAAW,YAAY,gBAAgB,SACjD,OAAO,aACP;AACN;AAKO,SAAS,gBAAgB,OAAuB;AACrD,SACE,OAAO,UAAU,YACjB,gBAAgB,SAChB,MAAM,aAAa;AAEvB;AAKO,SAAS,oBACd,YACO;AACP,SAAO,EAAE,YAAY,UAAU,KAAA;AACjC;AAEA,SAAS,gBACP,OACA,OACiC;AACjC,MAAI,MAAM,KAAK,UAAU,OAAO;AAC9B,WAAO,MAAM;AAAA,EACf;AAEA,aAAW,QAAQ,MAAM,QAAQ,CAAA,GAAI;AACnC,QAAI,KAAK,KAAK,UAAU,OAAO;AAC7B,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACF;AAOO,SAAS,UACd,OACA,KACA,YACwD;AACxD,MAAI,IAAI,KAAK,WAAW,GAAG;AACzB;AAAA,EACF;AAEA,MAAI,IAAI,KAAK,WAAW,GAAG;AAEzB,UAAM,QAAQ,IAAI,KAAK,CAAC;AAExB,QAAI,MAAM,QAAQ;AAChB,YAAM,gBAAgB,MAAM,OAAO,KAAK;AACxC,UAAI,iBAAiB,cAAc,SAAS,OAAO;AACjD,eAAO,UAAU,OAAO,eAAe,UAAU;AAAA,MACnD;AAAA,IACF;AAMA,WAAO,EAAE,YAAY,MAAM,CAAC,KAAK,EAAA;AAAA,EACnC;AAEA,MAAI,IAAI,KAAK,SAAS,GAAG;AAEvB,UAAM,CAAC,OAAO,GAAG,IAAI,IAAI,IAAI;AAC7B,UAAM,WAAW,gBAAgB,OAAO,KAAM;AAC9C,QAAI,CAAC,UAAU;AACb;AAAA,IACF;AAEA,QAAI,SAAS,SAAS,YAAY;AAChC,aAAO,UAAU,SAAS,OAAO,IAAI,QAAQ,IAAI,GAAG,UAAU;AAAA,IAChE,OAAO;AAIL,aAAO,EAAE,YAAY,SAAS,YAAY,MAAM,KAAA;AAAA,IAClD;AAAA,EACF;AACF;"}
1
+ {"version":3,"file":"ir.js","sources":["../../../src/query/ir.ts"],"sourcesContent":["/*\nThis is the intermediate representation of the query.\n*/\n\nimport type { CompareOptions } from \"./builder/types\"\nimport type { Collection, CollectionImpl } from \"../collection/index.js\"\nimport type { NamespacedRow } from \"../types\"\n\nexport interface QueryIR {\n from: From\n select?: Select\n join?: Join\n where?: Array<Where>\n groupBy?: GroupBy\n having?: Array<Having>\n orderBy?: OrderBy\n limit?: Limit\n offset?: Offset\n distinct?: true\n singleResult?: true\n\n // Functional variants\n fnSelect?: (row: NamespacedRow) => any\n fnWhere?: Array<(row: NamespacedRow) => any>\n fnHaving?: Array<(row: NamespacedRow) => any>\n}\n\nexport type From = CollectionRef | QueryRef\n\nexport type Select = {\n [alias: string]: BasicExpression | Aggregate | Select\n}\n\nexport type Join = Array<JoinClause>\n\nexport interface JoinClause {\n from: CollectionRef | QueryRef\n type: `left` | `right` | `inner` | `outer` | `full` | `cross`\n left: BasicExpression\n right: BasicExpression\n}\n\nexport type Where =\n | BasicExpression<boolean>\n | { expression: BasicExpression<boolean>; residual?: boolean }\n\nexport type GroupBy = Array<BasicExpression>\n\nexport type Having = Where\n\nexport type OrderBy = Array<OrderByClause>\n\nexport type OrderByClause = {\n expression: BasicExpression\n compareOptions: CompareOptions\n}\n\nexport type OrderByDirection = `asc` | `desc`\n\nexport type Limit = number\n\nexport type Offset = number\n\n/* Expressions */\n\nabstract class BaseExpression<T = any> {\n public abstract type: string\n /** @internal - Type brand for TypeScript inference */\n declare readonly __returnType: T\n}\n\nexport class CollectionRef extends BaseExpression {\n public type = `collectionRef` as const\n constructor(\n public collection: CollectionImpl,\n public alias: string\n ) {\n super()\n }\n}\n\nexport class QueryRef extends BaseExpression {\n public type = `queryRef` as const\n constructor(\n public query: QueryIR,\n public alias: string\n ) {\n super()\n }\n}\n\nexport class PropRef<T = any> extends BaseExpression<T> {\n public type = `ref` as const\n constructor(\n public path: Array<string> // path to the property in the collection, with the alias as the first element\n ) {\n super()\n }\n}\n\nexport class Value<T = any> extends BaseExpression<T> {\n public type = `val` as const\n constructor(\n public value: T // any js value\n ) {\n super()\n }\n}\n\nexport class Func<T = any> extends BaseExpression<T> {\n public type = `func` as const\n constructor(\n public name: string, // such as eq, gt, lt, upper, lower, etc.\n public args: Array<BasicExpression>\n ) {\n super()\n }\n}\n\n// This is the basic expression type that is used in the majority of expression\n// builder callbacks (select, where, groupBy, having, orderBy, etc.)\n// it doesn't include aggregate functions as those are only used in the select clause\nexport type BasicExpression<T = any> = PropRef<T> | Value<T> | Func<T>\n\nexport class Aggregate<T = any> extends BaseExpression<T> {\n public type = `agg` as const\n constructor(\n public name: string, // such as count, avg, sum, min, max, etc.\n public args: Array<BasicExpression>\n ) {\n super()\n }\n}\n\n/**\n * Runtime helper to detect IR expression-like objects.\n * Prefer this over ad-hoc local implementations to keep behavior consistent.\n */\nexport function isExpressionLike(value: any): boolean {\n return (\n value instanceof Aggregate ||\n value instanceof Func ||\n value instanceof PropRef ||\n value instanceof Value\n )\n}\n\n/**\n * Helper functions for working with Where clauses\n */\n\n/**\n * Extract the expression from a Where clause\n */\nexport function getWhereExpression(where: Where): BasicExpression<boolean> {\n return typeof where === `object` && `expression` in where\n ? where.expression\n : where\n}\n\n/**\n * Extract the expression from a HAVING clause\n * HAVING clauses can contain aggregates, unlike regular WHERE clauses\n */\nexport function getHavingExpression(\n having: Having\n): BasicExpression | Aggregate {\n return typeof having === `object` && `expression` in having\n ? having.expression\n : having\n}\n\n/**\n * Check if a Where clause is marked as residual\n */\nexport function isResidualWhere(where: Where): boolean {\n return (\n typeof where === `object` &&\n `expression` in where &&\n where.residual === true\n )\n}\n\n/**\n * Create a residual Where clause from an expression\n */\nexport function createResidualWhere(\n expression: BasicExpression<boolean>\n): Where {\n return { expression, residual: true }\n}\n\nfunction getRefFromAlias(\n query: QueryIR,\n alias: string\n): CollectionRef | QueryRef | void {\n if (query.from.alias === alias) {\n return query.from\n }\n\n for (const join of query.join || []) {\n if (join.from.alias === alias) {\n return join.from\n }\n }\n}\n\n/**\n * Follows the given reference in a query\n * until its finds the root field the reference points to.\n * @returns The collection, its alias, and the path to the root field in this collection\n */\nexport function followRef(\n query: QueryIR,\n ref: PropRef<any>,\n collection: Collection\n): { collection: Collection; path: Array<string> } | void {\n if (ref.path.length === 0) {\n return\n }\n\n if (ref.path.length === 1) {\n // This field should be part of this collection\n const field = ref.path[0]!\n // is it part of the select clause?\n if (query.select) {\n const selectedField = query.select[field]\n if (selectedField && selectedField.type === `ref`) {\n return followRef(query, selectedField, collection)\n }\n }\n\n // Either this field is not part of the select clause\n // and thus it must be part of the collection itself\n // or it is part of the select but is not a reference\n // so we can stop here and don't have to follow it\n return { collection, path: [field] }\n }\n\n if (ref.path.length > 1) {\n // This is a nested field\n const [alias, ...rest] = ref.path\n const aliasRef = getRefFromAlias(query, alias!)\n if (!aliasRef) {\n return\n }\n\n if (aliasRef.type === `queryRef`) {\n return followRef(aliasRef.query, new PropRef(rest), collection)\n } else {\n // This is a reference to a collection\n // we can't follow it further\n // so the field must be on the collection itself\n return { collection: aliasRef.collection, path: rest }\n }\n }\n}\n"],"names":[],"mappings":"AAiEA,MAAe,eAAwB;AAIvC;AAEO,MAAM,sBAAsB,eAAe;AAAA,EAEhD,YACS,YACA,OACP;AACA,UAAA;AAHO,SAAA,aAAA;AACA,SAAA,QAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAEO,MAAM,iBAAiB,eAAe;AAAA,EAE3C,YACS,OACA,OACP;AACA,UAAA;AAHO,SAAA,QAAA;AACA,SAAA,QAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAEO,MAAM,gBAAyB,eAAkB;AAAA,EAEtD,YACS,MACP;AACA,UAAA;AAFO,SAAA,OAAA;AAFT,SAAO,OAAO;AAAA,EAKd;AACF;AAEO,MAAM,cAAuB,eAAkB;AAAA,EAEpD,YACS,OACP;AACA,UAAA;AAFO,SAAA,QAAA;AAFT,SAAO,OAAO;AAAA,EAKd;AACF;AAEO,MAAM,aAAsB,eAAkB;AAAA,EAEnD,YACS,MACA,MACP;AACA,UAAA;AAHO,SAAA,OAAA;AACA,SAAA,OAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAOO,MAAM,kBAA2B,eAAkB;AAAA,EAExD,YACS,MACA,MACP;AACA,UAAA;AAHO,SAAA,OAAA;AACA,SAAA,OAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAMO,SAAS,iBAAiB,OAAqB;AACpD,SACE,iBAAiB,aACjB,iBAAiB,QACjB,iBAAiB,WACjB,iBAAiB;AAErB;AASO,SAAS,mBAAmB,OAAwC;AACzE,SAAO,OAAO,UAAU,YAAY,gBAAgB,QAChD,MAAM,aACN;AACN;AAMO,SAAS,oBACd,QAC6B;AAC7B,SAAO,OAAO,WAAW,YAAY,gBAAgB,SACjD,OAAO,aACP;AACN;AAKO,SAAS,gBAAgB,OAAuB;AACrD,SACE,OAAO,UAAU,YACjB,gBAAgB,SAChB,MAAM,aAAa;AAEvB;AAKO,SAAS,oBACd,YACO;AACP,SAAO,EAAE,YAAY,UAAU,KAAA;AACjC;AAEA,SAAS,gBACP,OACA,OACiC;AACjC,MAAI,MAAM,KAAK,UAAU,OAAO;AAC9B,WAAO,MAAM;AAAA,EACf;AAEA,aAAW,QAAQ,MAAM,QAAQ,CAAA,GAAI;AACnC,QAAI,KAAK,KAAK,UAAU,OAAO;AAC7B,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACF;AAOO,SAAS,UACd,OACA,KACA,YACwD;AACxD,MAAI,IAAI,KAAK,WAAW,GAAG;AACzB;AAAA,EACF;AAEA,MAAI,IAAI,KAAK,WAAW,GAAG;AAEzB,UAAM,QAAQ,IAAI,KAAK,CAAC;AAExB,QAAI,MAAM,QAAQ;AAChB,YAAM,gBAAgB,MAAM,OAAO,KAAK;AACxC,UAAI,iBAAiB,cAAc,SAAS,OAAO;AACjD,eAAO,UAAU,OAAO,eAAe,UAAU;AAAA,MACnD;AAAA,IACF;AAMA,WAAO,EAAE,YAAY,MAAM,CAAC,KAAK,EAAA;AAAA,EACnC;AAEA,MAAI,IAAI,KAAK,SAAS,GAAG;AAEvB,UAAM,CAAC,OAAO,GAAG,IAAI,IAAI,IAAI;AAC7B,UAAM,WAAW,gBAAgB,OAAO,KAAM;AAC9C,QAAI,CAAC,UAAU;AACb;AAAA,IACF;AAEA,QAAI,SAAS,SAAS,YAAY;AAChC,aAAO,UAAU,SAAS,OAAO,IAAI,QAAQ,IAAI,GAAG,UAAU;AAAA,IAChE,OAAO;AAIL,aAAO,EAAE,YAAY,SAAS,YAAY,MAAM,KAAA;AAAA,IAClD;AAAA,EACF;AACF;"}
@@ -1,6 +1,6 @@
1
1
  import { CollectionSubscription } from '../../collection/subscription.js';
2
2
  import { OrderByOptimizationInfo } from '../compiler/order-by.js';
3
- import { CollectionConfig, SyncConfig } from '../../types.js';
3
+ import { CollectionConfigSingleRowOption, SyncConfig } from '../../types.js';
4
4
  import { Context, GetResult } from '../builder/types.js';
5
5
  import { BasicExpression, QueryIR } from '../ir.js';
6
6
  import { LazyCollectionCallbacks } from '../compiler/joins.js';
@@ -23,7 +23,7 @@ export declare class CollectionConfigBuilder<TContext extends Context, TResult e
23
23
  readonly lazyCollections: Set<string>;
24
24
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>;
25
25
  constructor(config: LiveQueryCollectionConfig<TContext, TResult>);
26
- getConfig(): CollectionConfig<TResult>;
26
+ getConfig(): CollectionConfigSingleRowOption<TResult>;
27
27
  maybeRunGraph(config: Parameters<SyncConfig<TResult>[`sync`]>[0], syncState: FullSyncState, callback?: () => boolean): void;
28
28
  private getSyncConfig;
29
29
  private syncFn;
@@ -33,7 +33,8 @@ class CollectionConfigBuilder {
33
33
  onInsert: this.config.onInsert,
34
34
  onUpdate: this.config.onUpdate,
35
35
  onDelete: this.config.onDelete,
36
- startSync: this.config.startSync
36
+ startSync: this.config.startSync,
37
+ singleResult: this.query.singleResult
37
38
  };
38
39
  }
39
40
  // The callback function is called after the graph has run.
@@ -54,7 +55,7 @@ class CollectionConfigBuilder {
54
55
  if (this.allCollectionsReadyOrInitialCommit() && syncState.subscribedToAllCollections) {
55
56
  while (syncState.graph.pendingWork()) {
56
57
  syncState.graph.run();
57
- callback == null ? void 0 : callback();
58
+ callback?.();
58
59
  }
59
60
  if (syncState.messagesCount === 0) {
60
61
  begin();