@tanstack/db 0.0.1

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 (154) hide show
  1. package/README.md +37 -0
  2. package/dist/cjs/SortedMap.cjs +140 -0
  3. package/dist/cjs/SortedMap.cjs.map +1 -0
  4. package/dist/cjs/SortedMap.d.cts +91 -0
  5. package/dist/cjs/collection.cjs +597 -0
  6. package/dist/cjs/collection.cjs.map +1 -0
  7. package/dist/cjs/collection.d.cts +176 -0
  8. package/dist/cjs/deferred.cjs +25 -0
  9. package/dist/cjs/deferred.cjs.map +1 -0
  10. package/dist/cjs/deferred.d.cts +20 -0
  11. package/dist/cjs/errors.cjs +10 -0
  12. package/dist/cjs/errors.cjs.map +1 -0
  13. package/dist/cjs/errors.d.cts +3 -0
  14. package/dist/cjs/index.cjs +33 -0
  15. package/dist/cjs/index.cjs.map +1 -0
  16. package/dist/cjs/index.d.cts +9 -0
  17. package/dist/cjs/proxy.cjs +654 -0
  18. package/dist/cjs/proxy.cjs.map +1 -0
  19. package/dist/cjs/proxy.d.cts +59 -0
  20. package/dist/cjs/query/compiled-query.cjs +162 -0
  21. package/dist/cjs/query/compiled-query.cjs.map +1 -0
  22. package/dist/cjs/query/compiled-query.d.cts +22 -0
  23. package/dist/cjs/query/evaluators.cjs +146 -0
  24. package/dist/cjs/query/evaluators.cjs.map +1 -0
  25. package/dist/cjs/query/evaluators.d.cts +9 -0
  26. package/dist/cjs/query/extractors.cjs +122 -0
  27. package/dist/cjs/query/extractors.cjs.map +1 -0
  28. package/dist/cjs/query/extractors.d.cts +22 -0
  29. package/dist/cjs/query/functions.cjs +152 -0
  30. package/dist/cjs/query/functions.cjs.map +1 -0
  31. package/dist/cjs/query/functions.d.cts +21 -0
  32. package/dist/cjs/query/group-by.cjs +91 -0
  33. package/dist/cjs/query/group-by.cjs.map +1 -0
  34. package/dist/cjs/query/group-by.d.cts +40 -0
  35. package/dist/cjs/query/index.d.cts +5 -0
  36. package/dist/cjs/query/joins.cjs +155 -0
  37. package/dist/cjs/query/joins.cjs.map +1 -0
  38. package/dist/cjs/query/joins.d.cts +14 -0
  39. package/dist/cjs/query/key-by.cjs +43 -0
  40. package/dist/cjs/query/key-by.cjs.map +1 -0
  41. package/dist/cjs/query/key-by.d.cts +3 -0
  42. package/dist/cjs/query/order-by.cjs +229 -0
  43. package/dist/cjs/query/order-by.cjs.map +1 -0
  44. package/dist/cjs/query/order-by.d.cts +3 -0
  45. package/dist/cjs/query/pipeline-compiler.cjs +94 -0
  46. package/dist/cjs/query/pipeline-compiler.cjs.map +1 -0
  47. package/dist/cjs/query/pipeline-compiler.d.cts +9 -0
  48. package/dist/cjs/query/query-builder.cjs +314 -0
  49. package/dist/cjs/query/query-builder.cjs.map +1 -0
  50. package/dist/cjs/query/query-builder.d.cts +219 -0
  51. package/dist/cjs/query/schema.d.cts +98 -0
  52. package/dist/cjs/query/select.cjs +107 -0
  53. package/dist/cjs/query/select.cjs.map +1 -0
  54. package/dist/cjs/query/select.d.cts +3 -0
  55. package/dist/cjs/query/types.d.cts +188 -0
  56. package/dist/cjs/query/utils.cjs +154 -0
  57. package/dist/cjs/query/utils.cjs.map +1 -0
  58. package/dist/cjs/query/utils.d.cts +37 -0
  59. package/dist/cjs/transactions.cjs +137 -0
  60. package/dist/cjs/transactions.cjs.map +1 -0
  61. package/dist/cjs/transactions.d.cts +27 -0
  62. package/dist/cjs/types.d.cts +94 -0
  63. package/dist/cjs/utils.cjs +17 -0
  64. package/dist/cjs/utils.cjs.map +1 -0
  65. package/dist/cjs/utils.d.cts +3 -0
  66. package/dist/esm/SortedMap.d.ts +91 -0
  67. package/dist/esm/SortedMap.js +140 -0
  68. package/dist/esm/SortedMap.js.map +1 -0
  69. package/dist/esm/collection.d.ts +176 -0
  70. package/dist/esm/collection.js +597 -0
  71. package/dist/esm/collection.js.map +1 -0
  72. package/dist/esm/deferred.d.ts +20 -0
  73. package/dist/esm/deferred.js +25 -0
  74. package/dist/esm/deferred.js.map +1 -0
  75. package/dist/esm/errors.d.ts +3 -0
  76. package/dist/esm/errors.js +10 -0
  77. package/dist/esm/errors.js.map +1 -0
  78. package/dist/esm/index.d.ts +9 -0
  79. package/dist/esm/index.js +33 -0
  80. package/dist/esm/index.js.map +1 -0
  81. package/dist/esm/proxy.d.ts +59 -0
  82. package/dist/esm/proxy.js +654 -0
  83. package/dist/esm/proxy.js.map +1 -0
  84. package/dist/esm/query/compiled-query.d.ts +22 -0
  85. package/dist/esm/query/compiled-query.js +162 -0
  86. package/dist/esm/query/compiled-query.js.map +1 -0
  87. package/dist/esm/query/evaluators.d.ts +9 -0
  88. package/dist/esm/query/evaluators.js +146 -0
  89. package/dist/esm/query/evaluators.js.map +1 -0
  90. package/dist/esm/query/extractors.d.ts +22 -0
  91. package/dist/esm/query/extractors.js +122 -0
  92. package/dist/esm/query/extractors.js.map +1 -0
  93. package/dist/esm/query/functions.d.ts +21 -0
  94. package/dist/esm/query/functions.js +152 -0
  95. package/dist/esm/query/functions.js.map +1 -0
  96. package/dist/esm/query/group-by.d.ts +40 -0
  97. package/dist/esm/query/group-by.js +91 -0
  98. package/dist/esm/query/group-by.js.map +1 -0
  99. package/dist/esm/query/index.d.ts +5 -0
  100. package/dist/esm/query/joins.d.ts +14 -0
  101. package/dist/esm/query/joins.js +155 -0
  102. package/dist/esm/query/joins.js.map +1 -0
  103. package/dist/esm/query/key-by.d.ts +3 -0
  104. package/dist/esm/query/key-by.js +43 -0
  105. package/dist/esm/query/key-by.js.map +1 -0
  106. package/dist/esm/query/order-by.d.ts +3 -0
  107. package/dist/esm/query/order-by.js +229 -0
  108. package/dist/esm/query/order-by.js.map +1 -0
  109. package/dist/esm/query/pipeline-compiler.d.ts +9 -0
  110. package/dist/esm/query/pipeline-compiler.js +94 -0
  111. package/dist/esm/query/pipeline-compiler.js.map +1 -0
  112. package/dist/esm/query/query-builder.d.ts +219 -0
  113. package/dist/esm/query/query-builder.js +314 -0
  114. package/dist/esm/query/query-builder.js.map +1 -0
  115. package/dist/esm/query/schema.d.ts +98 -0
  116. package/dist/esm/query/select.d.ts +3 -0
  117. package/dist/esm/query/select.js +107 -0
  118. package/dist/esm/query/select.js.map +1 -0
  119. package/dist/esm/query/types.d.ts +188 -0
  120. package/dist/esm/query/utils.d.ts +37 -0
  121. package/dist/esm/query/utils.js +154 -0
  122. package/dist/esm/query/utils.js.map +1 -0
  123. package/dist/esm/transactions.d.ts +27 -0
  124. package/dist/esm/transactions.js +137 -0
  125. package/dist/esm/transactions.js.map +1 -0
  126. package/dist/esm/types.d.ts +94 -0
  127. package/dist/esm/utils.d.ts +3 -0
  128. package/dist/esm/utils.js +17 -0
  129. package/dist/esm/utils.js.map +1 -0
  130. package/package.json +57 -0
  131. package/src/SortedMap.ts +163 -0
  132. package/src/collection.ts +919 -0
  133. package/src/deferred.ts +47 -0
  134. package/src/errors.ts +6 -0
  135. package/src/index.ts +12 -0
  136. package/src/proxy.ts +1104 -0
  137. package/src/query/compiled-query.ts +193 -0
  138. package/src/query/evaluators.ts +222 -0
  139. package/src/query/extractors.ts +211 -0
  140. package/src/query/functions.ts +297 -0
  141. package/src/query/group-by.ts +137 -0
  142. package/src/query/index.ts +5 -0
  143. package/src/query/joins.ts +247 -0
  144. package/src/query/key-by.ts +61 -0
  145. package/src/query/order-by.ts +312 -0
  146. package/src/query/pipeline-compiler.ts +152 -0
  147. package/src/query/query-builder.ts +898 -0
  148. package/src/query/schema.ts +255 -0
  149. package/src/query/select.ts +173 -0
  150. package/src/query/types.ts +417 -0
  151. package/src/query/utils.ts +245 -0
  152. package/src/transactions.ts +198 -0
  153. package/src/types.ts +125 -0
  154. package/src/utils.ts +15 -0
@@ -0,0 +1,193 @@
1
+ import { D2, MessageType, MultiSet, output } from "@electric-sql/d2ts"
2
+ import { Effect, batch } from "@tanstack/store"
3
+ import { Collection } from "../collection.js"
4
+ import { compileQueryPipeline } from "./pipeline-compiler.js"
5
+ import type { ChangeMessage, SyncConfig } from "../types.js"
6
+ import type {
7
+ IStreamBuilder,
8
+ MultiSetArray,
9
+ RootStreamBuilder,
10
+ } from "@electric-sql/d2ts"
11
+ import type { QueryBuilder, ResultsFromContext } from "./query-builder.js"
12
+ import type { Context, Schema } from "./types.js"
13
+
14
+ export function compileQuery<TContext extends Context<Schema>>(
15
+ queryBuilder: QueryBuilder<TContext>
16
+ ) {
17
+ return new CompiledQuery<ResultsFromContext<TContext>>(queryBuilder)
18
+ }
19
+
20
+ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
21
+ private graph: D2
22
+ private inputs: Record<string, RootStreamBuilder<any>>
23
+ private inputCollections: Record<string, Collection<any>>
24
+ private resultCollection: Collection<TResults>
25
+ public state: `compiled` | `running` | `stopped` = `compiled`
26
+ private version = 0
27
+ private unsubscribeEffect?: () => void
28
+
29
+ constructor(queryBuilder: QueryBuilder<Context<Schema>>) {
30
+ const query = queryBuilder._query
31
+ const collections = query.collections
32
+
33
+ if (!collections) {
34
+ throw new Error(`No collections provided`)
35
+ }
36
+
37
+ this.inputCollections = collections
38
+
39
+ const graph = new D2({ initialFrontier: this.version })
40
+ const inputs = Object.fromEntries(
41
+ Object.entries(collections).map(([key]) => [key, graph.newInput<any>()])
42
+ )
43
+
44
+ const sync: SyncConfig<TResults>[`sync`] = ({ begin, write, commit }) => {
45
+ compileQueryPipeline<IStreamBuilder<[unknown, TResults]>>(
46
+ query,
47
+ inputs
48
+ ).pipe(
49
+ output(({ type, data }) => {
50
+ if (type === MessageType.DATA) {
51
+ begin()
52
+ data.collection
53
+ .getInner()
54
+ .reduce((acc, [[key, value], multiplicity]) => {
55
+ const changes = acc.get(key) || {
56
+ deletes: 0,
57
+ inserts: 0,
58
+ value,
59
+ }
60
+ if (multiplicity < 0) {
61
+ changes.deletes += Math.abs(multiplicity)
62
+ } else if (multiplicity > 0) {
63
+ changes.inserts += multiplicity
64
+ changes.value = value
65
+ }
66
+ acc.set(key, changes)
67
+ return acc
68
+ }, new Map<unknown, { deletes: number; inserts: number; value: TResults }>())
69
+ .forEach((changes, rawKey) => {
70
+ const key = (rawKey as any).toString()
71
+ const { deletes, inserts, value } = changes
72
+ if (inserts && !deletes) {
73
+ write({
74
+ key,
75
+ value: value,
76
+ type: `insert`,
77
+ })
78
+ } else if (inserts >= deletes) {
79
+ write({
80
+ key,
81
+ value: value,
82
+ type: `update`,
83
+ })
84
+ } else if (deletes > 0) {
85
+ write({
86
+ key,
87
+ value: value,
88
+ type: `delete`,
89
+ })
90
+ }
91
+ })
92
+ commit()
93
+ }
94
+ })
95
+ )
96
+ graph.finalize()
97
+ }
98
+
99
+ this.graph = graph
100
+ this.inputs = inputs
101
+ this.resultCollection = new Collection<TResults>({
102
+ id: crypto.randomUUID(), // TODO: remove when we don't require any more
103
+ sync: {
104
+ sync,
105
+ },
106
+ })
107
+ }
108
+
109
+ get results() {
110
+ return this.resultCollection
111
+ }
112
+
113
+ private sendChangesToInput(inputKey: string, changes: Array<ChangeMessage>) {
114
+ const input = this.inputs[inputKey]!
115
+ const multiSetArray: MultiSetArray<unknown> = []
116
+ for (const change of changes) {
117
+ if (change.type === `insert`) {
118
+ multiSetArray.push([change.value, 1])
119
+ } else if (change.type === `update`) {
120
+ multiSetArray.push([change.previousValue, -1])
121
+ multiSetArray.push([change.value, 1])
122
+ } else {
123
+ // change.type === `delete`
124
+ multiSetArray.push([change.value, -1])
125
+ }
126
+ }
127
+ input.sendData(this.version, new MultiSet(multiSetArray))
128
+ }
129
+
130
+ private sendFrontierToInput(inputKey: string) {
131
+ const input = this.inputs[inputKey]!
132
+ input.sendFrontier(this.version)
133
+ }
134
+
135
+ private sendFrontierToAllInputs() {
136
+ Object.entries(this.inputs).forEach(([key]) => {
137
+ this.sendFrontierToInput(key)
138
+ })
139
+ }
140
+
141
+ private incrementVersion() {
142
+ this.version++
143
+ }
144
+
145
+ private runGraph() {
146
+ this.graph.run()
147
+ }
148
+
149
+ start() {
150
+ if (this.state === `running`) {
151
+ throw new Error(`Query is already running`)
152
+ } else if (this.state === `stopped`) {
153
+ throw new Error(`Query is stopped`)
154
+ }
155
+
156
+ batch(() => {
157
+ Object.entries(this.inputCollections).forEach(([key, collection]) => {
158
+ this.sendChangesToInput(key, collection.currentStateAsChanges())
159
+ })
160
+ this.incrementVersion()
161
+ this.sendFrontierToAllInputs()
162
+ this.runGraph()
163
+ })
164
+
165
+ const changeEffect = new Effect({
166
+ fn: () => {
167
+ batch(() => {
168
+ Object.entries(this.inputCollections).forEach(([key, collection]) => {
169
+ this.sendChangesToInput(key, collection.derivedChanges.state)
170
+ })
171
+ this.incrementVersion()
172
+ this.sendFrontierToAllInputs()
173
+ this.runGraph()
174
+ })
175
+ },
176
+ deps: Object.values(this.inputCollections).map(
177
+ (collection) => collection.derivedChanges
178
+ ),
179
+ })
180
+ this.unsubscribeEffect = changeEffect.mount()
181
+
182
+ this.state = `running`
183
+ return () => {
184
+ this.stop()
185
+ }
186
+ }
187
+
188
+ stop() {
189
+ this.unsubscribeEffect?.()
190
+ this.unsubscribeEffect = undefined
191
+ this.state = `stopped`
192
+ }
193
+ }
@@ -0,0 +1,222 @@
1
+ import { evaluateOperandOnNestedRow } from "./extractors.js"
2
+ import { compareValues, convertLikeToRegex, isValueInArray } from "./utils.js"
3
+ import type {
4
+ Comparator,
5
+ Condition,
6
+ ConditionOperand,
7
+ LogicalOperator,
8
+ SimpleCondition,
9
+ } from "./schema.js"
10
+
11
+ /**
12
+ * Evaluates a condition against a nested row structure
13
+ */
14
+ export function evaluateConditionOnNestedRow(
15
+ nestedRow: Record<string, unknown>,
16
+ condition: Condition,
17
+ mainTableAlias?: string,
18
+ joinedTableAlias?: string
19
+ ): boolean {
20
+ // Handle simple conditions with exactly 3 elements
21
+ if (condition.length === 3 && !Array.isArray(condition[0])) {
22
+ const [left, comparator, right] = condition as SimpleCondition
23
+ return evaluateSimpleConditionOnNestedRow(
24
+ nestedRow,
25
+ left,
26
+ comparator,
27
+ right,
28
+ mainTableAlias,
29
+ joinedTableAlias
30
+ )
31
+ }
32
+
33
+ // Handle flat composite conditions (multiple conditions in a single array)
34
+ if (
35
+ condition.length > 3 &&
36
+ !Array.isArray(condition[0]) &&
37
+ typeof condition[1] === `string` &&
38
+ ![`and`, `or`].includes(condition[1] as string)
39
+ ) {
40
+ // Start with the first condition (first 3 elements)
41
+ let result = evaluateSimpleConditionOnNestedRow(
42
+ nestedRow,
43
+ condition[0],
44
+ condition[1] as Comparator,
45
+ condition[2],
46
+ mainTableAlias,
47
+ joinedTableAlias
48
+ )
49
+
50
+ // Process the rest in groups: logical operator, then 3 elements for each condition
51
+ for (let i = 3; i < condition.length; i += 4) {
52
+ const logicalOp = condition[i] as LogicalOperator
53
+
54
+ // Make sure we have a complete condition to evaluate
55
+ if (i + 3 <= condition.length) {
56
+ const nextResult = evaluateSimpleConditionOnNestedRow(
57
+ nestedRow,
58
+ condition[i + 1],
59
+ condition[i + 2] as Comparator,
60
+ condition[i + 3],
61
+ mainTableAlias,
62
+ joinedTableAlias
63
+ )
64
+
65
+ // Apply the logical operator
66
+ if (logicalOp === `and`) {
67
+ result = result && nextResult
68
+ } else {
69
+ // logicalOp === `or`
70
+ result = result || nextResult
71
+ }
72
+ }
73
+ }
74
+
75
+ return result
76
+ }
77
+
78
+ // Handle nested composite conditions where the first element is an array
79
+ if (condition.length > 0 && Array.isArray(condition[0])) {
80
+ // Start with the first condition
81
+ let result = evaluateConditionOnNestedRow(
82
+ nestedRow,
83
+ condition[0] as Condition,
84
+ mainTableAlias,
85
+ joinedTableAlias
86
+ )
87
+
88
+ // Process the rest of the conditions and logical operators in pairs
89
+ for (let i = 1; i < condition.length; i += 2) {
90
+ if (i + 1 >= condition.length) break // Make sure we have a pair
91
+
92
+ const operator = condition[i] as LogicalOperator
93
+ const nextCondition = condition[i + 1] as Condition
94
+
95
+ // Apply the logical operator
96
+ if (operator === `and`) {
97
+ result =
98
+ result &&
99
+ evaluateConditionOnNestedRow(
100
+ nestedRow,
101
+ nextCondition,
102
+ mainTableAlias,
103
+ joinedTableAlias
104
+ )
105
+ } else {
106
+ // logicalOp === `or`
107
+ result =
108
+ result ||
109
+ evaluateConditionOnNestedRow(
110
+ nestedRow,
111
+ nextCondition,
112
+ mainTableAlias,
113
+ joinedTableAlias
114
+ )
115
+ }
116
+ }
117
+
118
+ return result
119
+ }
120
+
121
+ // Fallback - this should not happen with valid conditions
122
+ return true
123
+ }
124
+
125
+ /**
126
+ * Evaluates a simple condition against a nested row structure
127
+ */
128
+ export function evaluateSimpleConditionOnNestedRow(
129
+ nestedRow: Record<string, unknown>,
130
+ left: ConditionOperand,
131
+ comparator: Comparator,
132
+ right: ConditionOperand,
133
+ mainTableAlias?: string,
134
+ joinedTableAlias?: string
135
+ ): boolean {
136
+ const leftValue = evaluateOperandOnNestedRow(
137
+ nestedRow,
138
+ left,
139
+ mainTableAlias,
140
+ joinedTableAlias
141
+ )
142
+
143
+ const rightValue = evaluateOperandOnNestedRow(
144
+ nestedRow,
145
+ right,
146
+ mainTableAlias,
147
+ joinedTableAlias
148
+ )
149
+
150
+ // The rest of the function remains the same as evaluateSimpleCondition
151
+ switch (comparator) {
152
+ case `=`:
153
+ return leftValue === rightValue
154
+ case `!=`:
155
+ return leftValue !== rightValue
156
+ case `<`:
157
+ return compareValues(leftValue, rightValue, `<`)
158
+ case `<=`:
159
+ return compareValues(leftValue, rightValue, `<=`)
160
+ case `>`:
161
+ return compareValues(leftValue, rightValue, `>`)
162
+ case `>=`:
163
+ return compareValues(leftValue, rightValue, `>=`)
164
+ case `like`:
165
+ case `not like`:
166
+ if (typeof leftValue === `string` && typeof rightValue === `string`) {
167
+ // Convert SQL LIKE pattern to proper regex pattern
168
+ const pattern = convertLikeToRegex(rightValue)
169
+ const matches = new RegExp(`^${pattern}$`, `i`).test(leftValue)
170
+ return comparator === `like` ? matches : !matches
171
+ }
172
+ return comparator === `like` ? false : true
173
+ case `in`:
174
+ // If right value is not an array, we can't do an IN operation
175
+ if (!Array.isArray(rightValue)) {
176
+ return false
177
+ }
178
+
179
+ // For empty arrays, nothing is contained in them
180
+ if (rightValue.length === 0) {
181
+ return false
182
+ }
183
+
184
+ // Handle array-to-array comparison (check if any element in leftValue exists in rightValue)
185
+ if (Array.isArray(leftValue)) {
186
+ return leftValue.some((item) => isValueInArray(item, rightValue))
187
+ }
188
+
189
+ // Handle single value comparison
190
+ return isValueInArray(leftValue, rightValue)
191
+
192
+ case `not in`:
193
+ // If right value is not an array, everything is "not in" it
194
+ if (!Array.isArray(rightValue)) {
195
+ return true
196
+ }
197
+
198
+ // For empty arrays, everything is "not in" them
199
+ if (rightValue.length === 0) {
200
+ return true
201
+ }
202
+
203
+ // Handle array-to-array comparison (check if no element in leftValue exists in rightValue)
204
+ if (Array.isArray(leftValue)) {
205
+ return !leftValue.some((item) => isValueInArray(item, rightValue))
206
+ }
207
+
208
+ // Handle single value comparison
209
+ return !isValueInArray(leftValue, rightValue)
210
+
211
+ case `is`:
212
+ return leftValue === rightValue
213
+ case `is not`:
214
+ // Properly handle null/undefined checks
215
+ if (rightValue === null) {
216
+ return leftValue !== null && leftValue !== undefined
217
+ }
218
+ return leftValue !== rightValue
219
+ default:
220
+ return false
221
+ }
222
+ }
@@ -0,0 +1,211 @@
1
+ import { evaluateFunction, isFunctionCall } from "./functions.js"
2
+ import type { AllowedFunctionName, ConditionOperand } from "./schema.js"
3
+
4
+ /**
5
+ * Extracts a value from a nested row structure
6
+ * @param nestedRow The nested row structure
7
+ * @param columnRef The column reference (may include table.column format)
8
+ * @param mainTableAlias The main table alias to check first for columns without table reference
9
+ * @param joinedTableAlias The joined table alias to check second for columns without table reference
10
+ * @returns The extracted value or undefined if not found
11
+ */
12
+ export function extractValueFromNestedRow(
13
+ nestedRow: Record<string, unknown>,
14
+ columnRef: string,
15
+ mainTableAlias?: string,
16
+ joinedTableAlias?: string
17
+ ): unknown {
18
+ // Check if it's a table.column reference
19
+ if (columnRef.includes(`.`)) {
20
+ const [tableAlias, colName] = columnRef.split(`.`) as [string, string]
21
+
22
+ // Get the table data
23
+ const tableData = nestedRow[tableAlias] as
24
+ | Record<string, unknown>
25
+ | null
26
+ | undefined
27
+
28
+ if (!tableData) {
29
+ return null
30
+ }
31
+
32
+ // Return the column value from that table
33
+ const value = tableData[colName]
34
+ return value
35
+ } else {
36
+ // If no table is specified, first try to find in the main table if provided
37
+ if (mainTableAlias && nestedRow[mainTableAlias]) {
38
+ const mainTableData = nestedRow[mainTableAlias] as Record<string, unknown>
39
+ if (typeof mainTableData === `object` && columnRef in mainTableData) {
40
+ return mainTableData[columnRef]
41
+ }
42
+ }
43
+
44
+ // Then try the joined table if provided
45
+ if (joinedTableAlias && nestedRow[joinedTableAlias]) {
46
+ const joinedTableData = nestedRow[joinedTableAlias] as Record<
47
+ string,
48
+ unknown
49
+ >
50
+ if (typeof joinedTableData === `object` && columnRef in joinedTableData) {
51
+ return joinedTableData[columnRef]
52
+ }
53
+ }
54
+
55
+ // If not found in main or joined table, try to find the column in any table
56
+ for (const [_tableAlias, tableData] of Object.entries(nestedRow)) {
57
+ if (
58
+ tableData &&
59
+ typeof tableData === `object` &&
60
+ columnRef in (tableData as Record<string, unknown>)
61
+ ) {
62
+ return (tableData as Record<string, unknown>)[columnRef]
63
+ }
64
+ }
65
+ return undefined
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Evaluates an operand against a nested row structure
71
+ */
72
+ export function evaluateOperandOnNestedRow(
73
+ nestedRow: Record<string, unknown>,
74
+ operand: ConditionOperand,
75
+ mainTableAlias?: string,
76
+ joinedTableAlias?: string
77
+ ): unknown {
78
+ // Handle column references
79
+ if (typeof operand === `string` && operand.startsWith(`@`)) {
80
+ const columnRef = operand.substring(1)
81
+ return extractValueFromNestedRow(
82
+ nestedRow,
83
+ columnRef,
84
+ mainTableAlias,
85
+ joinedTableAlias
86
+ )
87
+ }
88
+
89
+ // Handle explicit column references
90
+ if (operand && typeof operand === `object` && `col` in operand) {
91
+ const colRef = (operand as { col: unknown }).col
92
+
93
+ if (typeof colRef === `string`) {
94
+ // First try to extract from nested row structure
95
+ const nestedValue = extractValueFromNestedRow(
96
+ nestedRow,
97
+ colRef,
98
+ mainTableAlias,
99
+ joinedTableAlias
100
+ )
101
+
102
+ // If not found in nested structure, check if it's a direct property of the row
103
+ // This is important for HAVING clauses that reference aggregated values
104
+ if (nestedValue === undefined && colRef in nestedRow) {
105
+ return nestedRow[colRef]
106
+ }
107
+
108
+ return nestedValue
109
+ }
110
+
111
+ return undefined
112
+ }
113
+
114
+ // Handle function calls
115
+ if (operand && typeof operand === `object` && isFunctionCall(operand)) {
116
+ // Get the function name (the only key in the object)
117
+ const functionName = Object.keys(operand)[0] as AllowedFunctionName
118
+ // Get the arguments using type assertion with specific function name
119
+ const args = (operand as any)[functionName]
120
+
121
+ // If the arguments are a reference or another expression, evaluate them first
122
+ const evaluatedArgs = Array.isArray(args)
123
+ ? args.map((arg) =>
124
+ evaluateOperandOnNestedRow(
125
+ nestedRow,
126
+ arg as ConditionOperand,
127
+ mainTableAlias,
128
+ joinedTableAlias
129
+ )
130
+ )
131
+ : evaluateOperandOnNestedRow(
132
+ nestedRow,
133
+ args as ConditionOperand,
134
+ mainTableAlias,
135
+ joinedTableAlias
136
+ )
137
+
138
+ // Call the function with the evaluated arguments
139
+ return evaluateFunction(
140
+ functionName,
141
+ evaluatedArgs as ConditionOperand | Array<ConditionOperand>
142
+ )
143
+ }
144
+
145
+ // Handle explicit literals
146
+ if (operand && typeof operand === `object` && `value` in operand) {
147
+ return (operand as { value: unknown }).value
148
+ }
149
+
150
+ // Handle literal values
151
+ return operand
152
+ }
153
+
154
+ /**
155
+ * Extracts a join key value from a row based on the operand
156
+ * @param row The data row (not nested)
157
+ * @param operand The operand to extract the key from
158
+ * @param defaultTableAlias The default table alias
159
+ * @returns The extracted key value
160
+ */
161
+ export function extractJoinKey<T extends Record<string, unknown>>(
162
+ row: T,
163
+ operand: ConditionOperand,
164
+ defaultTableAlias?: string
165
+ ): unknown {
166
+ let keyValue: unknown
167
+
168
+ // Handle column references (e.g., "@orders.id" or "@id")
169
+ if (typeof operand === `string` && operand.startsWith(`@`)) {
170
+ const columnRef = operand.substring(1)
171
+
172
+ // If it contains a dot, extract the table and column
173
+ if (columnRef.includes(`.`)) {
174
+ const [tableAlias, colName] = columnRef.split(`.`) as [string, string]
175
+ // If this is referencing the current table, extract from row directly
176
+ if (tableAlias === defaultTableAlias) {
177
+ keyValue = row[colName]
178
+ } else {
179
+ // This might be a column from another table, return undefined
180
+ keyValue = undefined
181
+ }
182
+ } else {
183
+ // No table specified, look directly in the row
184
+ keyValue = row[columnRef]
185
+ }
186
+ } else if (operand && typeof operand === `object` && `col` in operand) {
187
+ // Handle explicit column references like { col: "orders.id" } or { col: "id" }
188
+ const colRef = (operand as { col: unknown }).col
189
+
190
+ if (typeof colRef === `string`) {
191
+ if (colRef.includes(`.`)) {
192
+ const [tableAlias, colName] = colRef.split(`.`) as [string, string]
193
+ // If this is referencing the current table, extract from row directly
194
+ if (tableAlias === defaultTableAlias) {
195
+ keyValue = row[colName]
196
+ } else {
197
+ // This might be a column from another table, return undefined
198
+ keyValue = undefined
199
+ }
200
+ } else {
201
+ // No table specified, look directly in the row
202
+ keyValue = row[colRef]
203
+ }
204
+ }
205
+ } else {
206
+ // Handle literals or other types
207
+ keyValue = operand
208
+ }
209
+
210
+ return keyValue
211
+ }