@tanstack/db 0.0.4 → 0.0.5

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 (102) hide show
  1. package/dist/cjs/collection.cjs +113 -94
  2. package/dist/cjs/collection.cjs.map +1 -1
  3. package/dist/cjs/collection.d.cts +38 -11
  4. package/dist/cjs/index.cjs +1 -0
  5. package/dist/cjs/index.cjs.map +1 -1
  6. package/dist/cjs/proxy.cjs +87 -248
  7. package/dist/cjs/proxy.cjs.map +1 -1
  8. package/dist/cjs/proxy.d.cts +5 -5
  9. package/dist/cjs/query/compiled-query.cjs +23 -14
  10. package/dist/cjs/query/compiled-query.cjs.map +1 -1
  11. package/dist/cjs/query/compiled-query.d.cts +3 -1
  12. package/dist/cjs/query/evaluators.cjs +20 -20
  13. package/dist/cjs/query/evaluators.cjs.map +1 -1
  14. package/dist/cjs/query/evaluators.d.cts +3 -2
  15. package/dist/cjs/query/extractors.cjs +20 -20
  16. package/dist/cjs/query/extractors.cjs.map +1 -1
  17. package/dist/cjs/query/extractors.d.cts +3 -3
  18. package/dist/cjs/query/group-by.cjs +12 -15
  19. package/dist/cjs/query/group-by.cjs.map +1 -1
  20. package/dist/cjs/query/group-by.d.cts +7 -7
  21. package/dist/cjs/query/joins.cjs +41 -55
  22. package/dist/cjs/query/joins.cjs.map +1 -1
  23. package/dist/cjs/query/joins.d.cts +3 -3
  24. package/dist/cjs/query/order-by.cjs +37 -84
  25. package/dist/cjs/query/order-by.cjs.map +1 -1
  26. package/dist/cjs/query/order-by.d.cts +2 -2
  27. package/dist/cjs/query/pipeline-compiler.cjs +13 -18
  28. package/dist/cjs/query/pipeline-compiler.cjs.map +1 -1
  29. package/dist/cjs/query/pipeline-compiler.d.cts +2 -1
  30. package/dist/cjs/query/query-builder.cjs +0 -12
  31. package/dist/cjs/query/query-builder.cjs.map +1 -1
  32. package/dist/cjs/query/query-builder.d.cts +4 -8
  33. package/dist/cjs/query/schema.d.cts +1 -6
  34. package/dist/cjs/query/select.cjs +35 -24
  35. package/dist/cjs/query/select.cjs.map +1 -1
  36. package/dist/cjs/query/select.d.cts +2 -2
  37. package/dist/cjs/query/types.d.cts +1 -0
  38. package/dist/cjs/transactions.cjs +17 -8
  39. package/dist/cjs/transactions.cjs.map +1 -1
  40. package/dist/cjs/types.d.cts +41 -7
  41. package/dist/esm/collection.d.ts +38 -11
  42. package/dist/esm/collection.js +113 -94
  43. package/dist/esm/collection.js.map +1 -1
  44. package/dist/esm/index.js +2 -1
  45. package/dist/esm/proxy.d.ts +5 -5
  46. package/dist/esm/proxy.js +87 -248
  47. package/dist/esm/proxy.js.map +1 -1
  48. package/dist/esm/query/compiled-query.d.ts +3 -1
  49. package/dist/esm/query/compiled-query.js +23 -14
  50. package/dist/esm/query/compiled-query.js.map +1 -1
  51. package/dist/esm/query/evaluators.d.ts +3 -2
  52. package/dist/esm/query/evaluators.js +21 -21
  53. package/dist/esm/query/evaluators.js.map +1 -1
  54. package/dist/esm/query/extractors.d.ts +3 -3
  55. package/dist/esm/query/extractors.js +20 -20
  56. package/dist/esm/query/extractors.js.map +1 -1
  57. package/dist/esm/query/group-by.d.ts +7 -7
  58. package/dist/esm/query/group-by.js +14 -17
  59. package/dist/esm/query/group-by.js.map +1 -1
  60. package/dist/esm/query/joins.d.ts +3 -3
  61. package/dist/esm/query/joins.js +42 -56
  62. package/dist/esm/query/joins.js.map +1 -1
  63. package/dist/esm/query/order-by.d.ts +2 -2
  64. package/dist/esm/query/order-by.js +39 -86
  65. package/dist/esm/query/order-by.js.map +1 -1
  66. package/dist/esm/query/pipeline-compiler.d.ts +2 -1
  67. package/dist/esm/query/pipeline-compiler.js +14 -19
  68. package/dist/esm/query/pipeline-compiler.js.map +1 -1
  69. package/dist/esm/query/query-builder.d.ts +4 -8
  70. package/dist/esm/query/query-builder.js +0 -12
  71. package/dist/esm/query/query-builder.js.map +1 -1
  72. package/dist/esm/query/schema.d.ts +1 -6
  73. package/dist/esm/query/select.d.ts +2 -2
  74. package/dist/esm/query/select.js +36 -25
  75. package/dist/esm/query/select.js.map +1 -1
  76. package/dist/esm/query/types.d.ts +1 -0
  77. package/dist/esm/transactions.js +17 -8
  78. package/dist/esm/transactions.js.map +1 -1
  79. package/dist/esm/types.d.ts +41 -7
  80. package/package.json +2 -2
  81. package/src/collection.ts +174 -121
  82. package/src/proxy.ts +141 -358
  83. package/src/query/compiled-query.ts +30 -15
  84. package/src/query/evaluators.ts +22 -21
  85. package/src/query/extractors.ts +24 -21
  86. package/src/query/group-by.ts +24 -22
  87. package/src/query/joins.ts +88 -75
  88. package/src/query/order-by.ts +56 -106
  89. package/src/query/pipeline-compiler.ts +34 -37
  90. package/src/query/query-builder.ts +9 -23
  91. package/src/query/schema.ts +1 -10
  92. package/src/query/select.ts +44 -32
  93. package/src/query/types.ts +1 -0
  94. package/src/transactions.ts +22 -13
  95. package/src/types.ts +48 -7
  96. package/dist/cjs/query/key-by.cjs +0 -43
  97. package/dist/cjs/query/key-by.cjs.map +0 -1
  98. package/dist/cjs/query/key-by.d.cts +0 -3
  99. package/dist/esm/query/key-by.d.ts +0 -3
  100. package/dist/esm/query/key-by.js +0 -43
  101. package/dist/esm/query/key-by.js.map +0 -1
  102. package/src/query/key-by.ts +0 -61
@@ -4,20 +4,25 @@ import {
4
4
  join as joinOperator,
5
5
  map,
6
6
  } from "@electric-sql/d2ts"
7
- import { evaluateConditionOnNestedRow } from "./evaluators.js"
7
+ import { evaluateConditionOnNamespacedRow } from "./evaluators.js"
8
8
  import { extractJoinKey } from "./extractors.js"
9
9
  import type { Query } from "./index.js"
10
10
  import type { IStreamBuilder, JoinType } from "@electric-sql/d2ts"
11
+ import type {
12
+ KeyedStream,
13
+ NamespacedAndKeyedStream,
14
+ NamespacedRow,
15
+ } from "../types.js"
11
16
 
12
17
  /**
13
18
  * Creates a processing pipeline for join clauses
14
19
  */
15
20
  export function processJoinClause(
16
- pipeline: IStreamBuilder<Record<string, unknown>>,
21
+ pipeline: NamespacedAndKeyedStream,
17
22
  query: Query,
18
- tables: Record<string, IStreamBuilder<Record<string, unknown>>>,
23
+ tables: Record<string, KeyedStream>,
19
24
  mainTableAlias: string,
20
- allInputs: Record<string, IStreamBuilder<Record<string, unknown>>>
25
+ allInputs: Record<string, KeyedStream>
21
26
  ) {
22
27
  if (!query.join) return pipeline
23
28
  const input = allInputs[query.from]
@@ -30,49 +35,56 @@ export function processJoinClause(
30
35
  const joinType: JoinType =
31
36
  joinClause.type === `cross` ? `inner` : joinClause.type
32
37
 
38
+ // The `in` is formatted as ['@mainKeyRef', '=', '@joinedKeyRef']
39
+ // Destructure the main key reference and the joined key references
40
+ const [mainKeyRef, , joinedKeyRefs] = joinClause.on
41
+
33
42
  // We need to prepare the main pipeline and the joined pipeline
34
43
  // to have the correct key format for joining
35
44
  const mainPipeline = pipeline.pipe(
36
- map((nestedRow: Record<string, unknown>) => {
45
+ map(([currentKey, namespacedRow]) => {
37
46
  // Extract the key from the ON condition left side for the main table
38
- const mainRow = nestedRow[mainTableAlias] as Record<string, unknown>
47
+ const mainRow = namespacedRow[mainTableAlias]!
39
48
 
40
49
  // Extract the join key from the main row
41
- const keyValue = extractJoinKey(
42
- mainRow,
43
- joinClause.on[0],
44
- mainTableAlias
45
- )
50
+ const key = extractJoinKey(mainRow, mainKeyRef, mainTableAlias)
46
51
 
47
- // Return [key, nestedRow] as a KeyValue type
48
- return [keyValue, nestedRow] as [unknown, Record<string, unknown>]
52
+ // Return [key, namespacedRow] as a KeyValue type
53
+ return [key, [currentKey, namespacedRow]] as [
54
+ unknown,
55
+ [string, typeof namespacedRow],
56
+ ]
49
57
  })
50
58
  )
51
59
 
52
60
  // Get the joined table input from the inputs map
53
- let joinedTableInput: IStreamBuilder<Record<string, unknown>>
61
+ let joinedTableInput: KeyedStream
54
62
 
55
63
  if (allInputs[joinClause.from]) {
56
64
  // Use the provided input if available
57
65
  joinedTableInput = allInputs[joinClause.from]!
58
66
  } else {
59
67
  // Create a new input if not provided
60
- joinedTableInput = input!.graph.newInput<Record<string, unknown>>()
68
+ joinedTableInput =
69
+ input!.graph.newInput<[string, Record<string, unknown>]>()
61
70
  }
62
71
 
63
72
  tables[joinedTableAlias] = joinedTableInput
64
73
 
65
74
  // Create a pipeline for the joined table
66
75
  const joinedPipeline = joinedTableInput.pipe(
67
- map((row: Record<string, unknown>) => {
76
+ map(([currentKey, row]) => {
68
77
  // Wrap the row in an object with the table alias as the key
69
- const nestedRow = { [joinedTableAlias]: row }
78
+ const namespacedRow: NamespacedRow = { [joinedTableAlias]: row }
70
79
 
71
80
  // Extract the key from the ON condition right side for the joined table
72
- const keyValue = extractJoinKey(row, joinClause.on[2], joinedTableAlias)
81
+ const key = extractJoinKey(row, joinedKeyRefs, joinedTableAlias)
73
82
 
74
- // Return [key, nestedRow] as a KeyValue type
75
- return [keyValue, nestedRow] as [unknown, Record<string, unknown>]
83
+ // Return [key, namespacedRow] as a KeyValue type
84
+ return [key, [currentKey, namespacedRow]] as [
85
+ string,
86
+ [string, typeof namespacedRow],
87
+ ]
76
88
  })
77
89
  )
78
90
 
@@ -123,76 +135,89 @@ export function processJoinClause(
123
135
  export function processJoinResults(
124
136
  mainTableAlias: string,
125
137
  joinedTableAlias: string,
126
- joinClause: { on: any; where?: any; type: string }
138
+ joinClause: { on: any; type: string }
127
139
  ) {
128
140
  return function (
129
- pipeline: IStreamBuilder<unknown>
130
- ): IStreamBuilder<Record<string, unknown>> {
141
+ pipeline: IStreamBuilder<
142
+ [
143
+ key: string,
144
+ [
145
+ [string, NamespacedRow] | undefined,
146
+ [string, NamespacedRow] | undefined,
147
+ ],
148
+ ]
149
+ >
150
+ ): NamespacedAndKeyedStream {
131
151
  return pipeline.pipe(
132
152
  // Process the join result and handle nulls in the same step
133
- map((result: unknown) => {
134
- const [_key, [mainNestedRow, joinedNestedRow]] = result as [
135
- unknown,
136
- [
137
- Record<string, unknown> | undefined,
138
- Record<string, unknown> | undefined,
139
- ],
140
- ]
153
+ map((result) => {
154
+ const [_key, [main, joined]] = result
155
+ const mainKey = main?.[0]
156
+ const mainNamespacedRow = main?.[1]
157
+ const joinedKey = joined?.[0]
158
+ const joinedNamespacedRow = joined?.[1]
141
159
 
142
160
  // For inner joins, both sides should be non-null
143
161
  if (joinClause.type === `inner` || joinClause.type === `cross`) {
144
- if (!mainNestedRow || !joinedNestedRow) {
162
+ if (!mainNamespacedRow || !joinedNamespacedRow) {
145
163
  return undefined // Will be filtered out
146
164
  }
147
165
  }
148
166
 
149
167
  // For left joins, the main row must be non-null
150
- if (joinClause.type === `left` && !mainNestedRow) {
168
+ if (joinClause.type === `left` && !mainNamespacedRow) {
151
169
  return undefined // Will be filtered out
152
170
  }
153
171
 
154
172
  // For right joins, the joined row must be non-null
155
- if (joinClause.type === `right` && !joinedNestedRow) {
173
+ if (joinClause.type === `right` && !joinedNamespacedRow) {
156
174
  return undefined // Will be filtered out
157
175
  }
158
176
 
159
177
  // Merge the nested rows
160
- const mergedNestedRow: Record<string, unknown> = {}
178
+ const mergedNamespacedRow: NamespacedRow = {}
161
179
 
162
180
  // Add main row data if it exists
163
- if (mainNestedRow) {
164
- Object.entries(mainNestedRow).forEach(([tableAlias, tableData]) => {
165
- mergedNestedRow[tableAlias] = tableData
166
- })
181
+ if (mainNamespacedRow) {
182
+ Object.entries(mainNamespacedRow).forEach(
183
+ ([tableAlias, tableData]) => {
184
+ mergedNamespacedRow[tableAlias] = tableData
185
+ }
186
+ )
167
187
  }
168
188
 
169
189
  // If we have a joined row, add it to the merged result
170
- if (joinedNestedRow) {
171
- Object.entries(joinedNestedRow).forEach(([tableAlias, tableData]) => {
172
- mergedNestedRow[tableAlias] = tableData
173
- })
190
+ if (joinedNamespacedRow) {
191
+ Object.entries(joinedNamespacedRow).forEach(
192
+ ([tableAlias, tableData]) => {
193
+ mergedNamespacedRow[tableAlias] = tableData
194
+ }
195
+ )
174
196
  } else if (joinClause.type === `left` || joinClause.type === `full`) {
175
- // For left or full joins, add the joined table with null data if missing
176
- mergedNestedRow[joinedTableAlias] = null
197
+ // For left or full joins, add the joined table with undefined data if missing
198
+ // mergedNamespacedRow[joinedTableAlias] = undefined
177
199
  }
178
200
 
179
- // For right or full joins, add the main table with null data if missing
201
+ // For right or full joins, add the main table with undefined data if missing
180
202
  if (
181
- !mainNestedRow &&
203
+ !mainNamespacedRow &&
182
204
  (joinClause.type === `right` || joinClause.type === `full`)
183
205
  ) {
184
- mergedNestedRow[mainTableAlias] = null
206
+ // mergedNamespacedRow[mainTableAlias] = undefined
185
207
  }
186
208
 
187
- return mergedNestedRow
209
+ // New key
210
+ const newKey = `[${mainKey},${joinedKey}]`
211
+
212
+ return [newKey, mergedNamespacedRow] as [
213
+ string,
214
+ typeof mergedNamespacedRow,
215
+ ]
188
216
  }),
189
217
  // Filter out undefined results
190
- filter(
191
- (value: unknown): value is Record<string, unknown> =>
192
- value !== undefined
193
- ),
218
+ filter((value) => value !== undefined),
194
219
  // Process the ON condition
195
- filter((nestedRow: Record<string, unknown>) => {
220
+ filter(([_key, namespacedRow]: [string, NamespacedRow]) => {
196
221
  // If there's no ON condition, or it's a cross join, always return true
197
222
  if (!joinClause.on || joinClause.type === `cross`) {
198
223
  return true
@@ -201,46 +226,34 @@ export function processJoinResults(
201
226
  // For LEFT JOIN, if the right side is null, we should include the row
202
227
  if (
203
228
  joinClause.type === `left` &&
204
- nestedRow[joinedTableAlias] === null
229
+ namespacedRow[joinedTableAlias] === undefined
205
230
  ) {
206
231
  return true
207
232
  }
208
233
 
209
234
  // For RIGHT JOIN, if the left side is null, we should include the row
210
- if (joinClause.type === `right` && nestedRow[mainTableAlias] === null) {
235
+ if (
236
+ joinClause.type === `right` &&
237
+ namespacedRow[mainTableAlias] === undefined
238
+ ) {
211
239
  return true
212
240
  }
213
241
 
214
242
  // For FULL JOIN, if either side is null, we should include the row
215
243
  if (
216
244
  joinClause.type === `full` &&
217
- (nestedRow[mainTableAlias] === null ||
218
- nestedRow[joinedTableAlias] === null)
245
+ (namespacedRow[mainTableAlias] === undefined ||
246
+ namespacedRow[joinedTableAlias] === undefined)
219
247
  ) {
220
248
  return true
221
249
  }
222
250
 
223
- const result = evaluateConditionOnNestedRow(
224
- nestedRow,
251
+ return evaluateConditionOnNamespacedRow(
252
+ namespacedRow,
225
253
  joinClause.on,
226
254
  mainTableAlias,
227
255
  joinedTableAlias
228
256
  )
229
- return result
230
- }),
231
- // Process the WHERE clause for the join if it exists
232
- filter((nestedRow: Record<string, unknown>) => {
233
- if (!joinClause.where) {
234
- return true
235
- }
236
-
237
- const result = evaluateConditionOnNestedRow(
238
- nestedRow,
239
- joinClause.where,
240
- mainTableAlias,
241
- joinedTableAlias
242
- )
243
- return result
244
257
  })
245
258
  )
246
259
  }
@@ -3,19 +3,18 @@ import {
3
3
  orderBy,
4
4
  orderByWithFractionalIndex,
5
5
  orderByWithIndex,
6
- topK,
7
- topKWithFractionalIndex,
8
- topKWithIndex,
9
6
  } from "@electric-sql/d2ts"
10
- import { evaluateOperandOnNestedRow } from "./extractors"
7
+ import { evaluateOperandOnNamespacedRow } from "./extractors"
11
8
  import { isOrderIndexFunctionCall } from "./utils"
12
9
  import type { ConditionOperand, Query } from "./schema"
13
- import type { IStreamBuilder } from "@electric-sql/d2ts"
10
+ import type {
11
+ KeyedNamespacedRow,
12
+ NamespacedAndKeyedStream,
13
+ NamespacedRow,
14
+ } from "../types"
14
15
 
15
16
  export function processOrderBy(
16
- resultPipeline: IStreamBuilder<
17
- Record<string, unknown> | [string | number, Record<string, unknown>]
18
- >,
17
+ resultPipeline: NamespacedAndKeyedStream,
19
18
  query: Query,
20
19
  mainTableAlias: string
21
20
  ) {
@@ -25,7 +24,9 @@ export function processOrderBy(
25
24
  let orderIndexAlias = ``
26
25
 
27
26
  // Scan the SELECT clause for ORDER_INDEX functions
28
- for (const item of query.select) {
27
+ // TODO: Select is going to be optional in future - we will automatically add an
28
+ // attribute for the index column
29
+ for (const item of query.select!) {
29
30
  if (typeof item === `object`) {
30
31
  for (const [alias, expr] of Object.entries(item)) {
31
32
  if (typeof expr === `object` && isOrderIndexFunctionCall(expr)) {
@@ -79,17 +80,13 @@ export function processOrderBy(
79
80
  }
80
81
 
81
82
  // Create a value extractor function for the orderBy operator
82
- const valueExtractor = (value: unknown) => {
83
- const row = value as Record<string, unknown>
84
-
85
- // Create a nested row structure for evaluateOperandOnNestedRow
86
- const nestedRow: Record<string, unknown> = { [mainTableAlias]: row }
87
-
83
+ // const valueExtractor = ([key, namespacedRow]: [
84
+ const valueExtractor = (namespacedRow: NamespacedRow) => {
88
85
  // For multiple orderBy columns, create a composite key
89
86
  if (orderByItems.length > 1) {
90
87
  return orderByItems.map((item) => {
91
- const val = evaluateOperandOnNestedRow(
92
- nestedRow,
88
+ const val = evaluateOperandOnNamespacedRow(
89
+ namespacedRow,
93
90
  item.operand,
94
91
  mainTableAlias
95
92
  )
@@ -106,8 +103,8 @@ export function processOrderBy(
106
103
  } else if (orderByItems.length === 1) {
107
104
  // For a single orderBy column, use the value directly
108
105
  const item = orderByItems[0]
109
- const val = evaluateOperandOnNestedRow(
110
- nestedRow,
106
+ const val = evaluateOperandOnNamespacedRow(
107
+ namespacedRow,
111
108
  item!.operand,
112
109
  mainTableAlias
113
110
  )
@@ -187,109 +184,62 @@ export function processOrderBy(
187
184
  return (a as any).toString().localeCompare((b as any).toString())
188
185
  }
189
186
 
190
- let topKComparator: (a: unknown, b: unknown) => number
191
- if (!query.keyBy) {
192
- topKComparator = (a, b) => {
193
- const aValue = valueExtractor(a)
194
- const bValue = valueExtractor(b)
195
- return comparator(aValue, bValue)
196
- }
197
- }
198
-
199
187
  // Apply the appropriate orderBy operator based on whether an ORDER_INDEX column is requested
200
188
  if (hasOrderIndexColumn) {
201
189
  if (orderIndexType === `numeric`) {
202
- if (query.keyBy) {
203
- // Use orderByWithIndex for numeric indices
204
- resultPipeline = resultPipeline.pipe(
205
- orderByWithIndex(valueExtractor, {
206
- limit: query.limit,
207
- offset: query.offset,
208
- comparator,
209
- }),
210
- map(([key, [value, index]]) => {
211
- // Add the index to the result
212
- const result = {
213
- ...(value as Record<string, unknown>),
214
- [orderIndexAlias]: index,
215
- }
216
- return [key, result]
217
- })
218
- )
219
- } else {
220
- // Use topKWithIndex for numeric indices
221
- resultPipeline = resultPipeline.pipe(
222
- map((value) => [null, value]),
223
- topKWithIndex(topKComparator!, {
224
- limit: query.limit,
225
- offset: query.offset,
226
- }),
227
- map(([_, [value, index]]) => {
228
- // Add the index to the result
229
- return {
230
- ...(value as Record<string, unknown>),
231
- [orderIndexAlias]: index,
232
- }
233
- })
234
- )
235
- }
236
- } else {
237
- if (query.keyBy) {
238
- // Use orderByWithFractionalIndex for fractional indices
239
- resultPipeline = resultPipeline.pipe(
240
- orderByWithFractionalIndex(valueExtractor, {
241
- limit: query.limit,
242
- offset: query.offset,
243
- comparator,
244
- }),
245
- map(([key, [value, index]]) => {
246
- // Add the index to the result
247
- const result = {
248
- ...(value as Record<string, unknown>),
249
- [orderIndexAlias]: index,
250
- }
251
- return [key, result]
252
- })
253
- )
254
- } else {
255
- // Use topKWithFractionalIndex for fractional indices
256
- resultPipeline = resultPipeline.pipe(
257
- map((value) => [null, value]),
258
- topKWithFractionalIndex(topKComparator!, {
259
- limit: query.limit,
260
- offset: query.offset,
261
- }),
262
- map(([_, [value, index]]) => {
263
- // Add the index to the result
264
- return {
265
- ...(value as Record<string, unknown>),
266
- [orderIndexAlias]: index,
267
- }
268
- })
269
- )
270
- }
271
- }
272
- } else {
273
- if (query.keyBy) {
274
- // Use regular orderBy if no index column is requested and but a keyBy is requested
190
+ // Use orderByWithIndex for numeric indices
275
191
  resultPipeline = resultPipeline.pipe(
276
- orderBy(valueExtractor, {
192
+ orderByWithIndex(valueExtractor, {
277
193
  limit: query.limit,
278
194
  offset: query.offset,
279
195
  comparator,
196
+ }),
197
+ map(([key, [value, index]]) => {
198
+ // Add the index to the result
199
+ // We add this to the main table alias for now
200
+ // TODO: re are going to need to refactor the whole order by pipeline
201
+ const result = {
202
+ ...(value as Record<string, unknown>),
203
+ [mainTableAlias]: {
204
+ ...value[mainTableAlias],
205
+ [orderIndexAlias]: index,
206
+ },
207
+ }
208
+ return [key, result] as KeyedNamespacedRow
280
209
  })
281
210
  )
282
211
  } else {
283
- // Use topK if no index column is requested and no keyBy is requested
212
+ // Use orderByWithFractionalIndex for fractional indices
284
213
  resultPipeline = resultPipeline.pipe(
285
- map((value) => [null, value]),
286
- topK(topKComparator!, {
214
+ orderByWithFractionalIndex(valueExtractor, {
287
215
  limit: query.limit,
288
216
  offset: query.offset,
217
+ comparator,
289
218
  }),
290
- map(([_, value]) => value as Record<string, unknown>)
219
+ map(([key, [value, index]]) => {
220
+ // Add the index to the result
221
+ // We add this to the main table alias for now
222
+ // TODO: re are going to need to refactor the whole order by pipeline
223
+ const result = {
224
+ ...(value as Record<string, unknown>),
225
+ [mainTableAlias]: {
226
+ ...value[mainTableAlias],
227
+ [orderIndexAlias]: index,
228
+ },
229
+ }
230
+ return [key, result] as KeyedNamespacedRow
231
+ })
291
232
  )
292
233
  }
234
+ } else {
235
+ // Use regular orderBy if no index column is requested
236
+ resultPipeline = resultPipeline.pipe(
237
+ orderBy(valueExtractor, {
238
+ limit: query.limit,
239
+ offset: query.offset,
240
+ comparator,
241
+ })
242
+ )
293
243
  }
294
244
 
295
245
  return resultPipeline
@@ -1,12 +1,16 @@
1
1
  import { filter, map } from "@electric-sql/d2ts"
2
- import { evaluateConditionOnNestedRow } from "./evaluators.js"
2
+ import { evaluateConditionOnNamespacedRow } from "./evaluators.js"
3
3
  import { processJoinClause } from "./joins.js"
4
4
  import { processGroupBy } from "./group-by.js"
5
5
  import { processOrderBy } from "./order-by.js"
6
- import { processKeyBy } from "./key-by.js"
7
6
  import { processSelect } from "./select.js"
8
- import type { Condition, Query } from "./schema.js"
7
+ import type { Query } from "./schema.js"
9
8
  import type { IStreamBuilder } from "@electric-sql/d2ts"
9
+ import type {
10
+ InputRow,
11
+ KeyedStream,
12
+ NamespacedAndKeyedStream,
13
+ } from "../types.js"
10
14
 
11
15
  /**
12
16
  * Compiles a query into a D2 pipeline
@@ -16,7 +20,7 @@ import type { IStreamBuilder } from "@electric-sql/d2ts"
16
20
  */
17
21
  export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
18
22
  query: Query,
19
- inputs: Record<string, IStreamBuilder<Record<string, unknown>>>
23
+ inputs: Record<string, KeyedStream>
20
24
  ): T {
21
25
  // Create a copy of the inputs map to avoid modifying the original
22
26
  const allInputs = { ...inputs }
@@ -30,11 +34,6 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
30
34
  throw new Error(`WITH query must have an "as" property`)
31
35
  }
32
36
 
33
- // Ensure the WITH query is not keyed
34
- if ((withQuery as Query).keyBy !== undefined) {
35
- throw new Error(`WITH query cannot have a "keyBy" property`)
36
- }
37
-
38
37
  // Check if this CTE name already exists in the inputs
39
38
  if (allInputs[withQuery.as]) {
40
39
  throw new Error(`CTE with name "${withQuery.as}" already exists`)
@@ -51,14 +50,12 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
51
50
  )
52
51
 
53
52
  // Add the compiled query to the inputs map using its alias
54
- allInputs[withQuery.as] = compiledWithQuery as IStreamBuilder<
55
- Record<string, unknown>
56
- >
53
+ allInputs[withQuery.as] = compiledWithQuery as KeyedStream
57
54
  }
58
55
  }
59
56
 
60
57
  // Create a map of table aliases to inputs
61
- const tables: Record<string, IStreamBuilder<Record<string, unknown>>> = {}
58
+ const tables: Record<string, KeyedStream> = {}
62
59
 
63
60
  // The main table is the one in the FROM clause
64
61
  const mainTableAlias = query.as || query.from
@@ -72,10 +69,14 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
72
69
  tables[mainTableAlias] = input
73
70
 
74
71
  // Prepare the initial pipeline with the main table wrapped in its alias
75
- let pipeline = input.pipe(
76
- map((row: unknown) => {
72
+ let pipeline: NamespacedAndKeyedStream = input.pipe(
73
+ map(([key, row]) => {
77
74
  // Initialize the record with a nested structure
78
- return { [mainTableAlias]: row } as Record<string, unknown>
75
+ const ret = [key, { [mainTableAlias]: row }] as [
76
+ string,
77
+ Record<string, typeof row>,
78
+ ]
79
+ return ret
79
80
  })
80
81
  )
81
82
 
@@ -93,10 +94,10 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
93
94
  // Process the WHERE clause if it exists
94
95
  if (query.where) {
95
96
  pipeline = pipeline.pipe(
96
- filter((nestedRow) => {
97
- const result = evaluateConditionOnNestedRow(
98
- nestedRow,
99
- query.where as Condition,
97
+ filter(([_key, row]) => {
98
+ const result = evaluateConditionOnNamespacedRow(
99
+ row,
100
+ query.where!,
100
101
  mainTableAlias
101
102
  )
102
103
  return result
@@ -113,12 +114,12 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
113
114
  // This works similarly to WHERE but is applied after any aggregations
114
115
  if (query.having) {
115
116
  pipeline = pipeline.pipe(
116
- filter((row) => {
117
+ filter(([_key, row]) => {
117
118
  // For HAVING, we're working with the flattened row that contains both
118
119
  // the group by keys and the aggregate results directly
119
- const result = evaluateConditionOnNestedRow(
120
- { [mainTableAlias]: row, ...row } as Record<string, unknown>,
121
- query.having as Condition,
120
+ const result = evaluateConditionOnNamespacedRow(
121
+ row,
122
+ query.having!,
122
123
  mainTableAlias
123
124
  )
124
125
  return result
@@ -126,21 +127,9 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
126
127
  )
127
128
  }
128
129
 
129
- // Process the SELECT clause - this is where we flatten the structure
130
- pipeline = processSelect(pipeline, query, mainTableAlias, allInputs)
131
-
132
- let resultPipeline: IStreamBuilder<
133
- Record<string, unknown> | [string | number, Record<string, unknown>]
134
- > = pipeline
135
-
136
- // Process keyBy parameter if it exists
137
- if (query.keyBy) {
138
- resultPipeline = processKeyBy(resultPipeline, query)
139
- }
140
-
141
130
  // Process orderBy parameter if it exists
142
131
  if (query.orderBy) {
143
- resultPipeline = processOrderBy(resultPipeline, query, mainTableAlias)
132
+ pipeline = processOrderBy(pipeline, query, mainTableAlias)
144
133
  } else if (query.limit !== undefined || query.offset !== undefined) {
145
134
  // If there's a limit or offset without orderBy, throw an error
146
135
  throw new Error(
@@ -148,5 +137,13 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
148
137
  )
149
138
  }
150
139
 
140
+ // Process the SELECT clause - this is where we flatten the structure
141
+ const resultPipeline: KeyedStream | NamespacedAndKeyedStream = query.select
142
+ ? processSelect(pipeline, query, mainTableAlias, allInputs)
143
+ : !query.join && !query.groupBy
144
+ ? pipeline.pipe(
145
+ map(([key, row]) => [key, row[mainTableAlias]] as InputRow)
146
+ )
147
+ : pipeline
151
148
  return resultPipeline as T
152
149
  }