@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.
- package/README.md +37 -0
- package/dist/cjs/SortedMap.cjs +140 -0
- package/dist/cjs/SortedMap.cjs.map +1 -0
- package/dist/cjs/SortedMap.d.cts +91 -0
- package/dist/cjs/collection.cjs +597 -0
- package/dist/cjs/collection.cjs.map +1 -0
- package/dist/cjs/collection.d.cts +176 -0
- package/dist/cjs/deferred.cjs +25 -0
- package/dist/cjs/deferred.cjs.map +1 -0
- package/dist/cjs/deferred.d.cts +20 -0
- package/dist/cjs/errors.cjs +10 -0
- package/dist/cjs/errors.cjs.map +1 -0
- package/dist/cjs/errors.d.cts +3 -0
- package/dist/cjs/index.cjs +33 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +9 -0
- package/dist/cjs/proxy.cjs +654 -0
- package/dist/cjs/proxy.cjs.map +1 -0
- package/dist/cjs/proxy.d.cts +59 -0
- package/dist/cjs/query/compiled-query.cjs +162 -0
- package/dist/cjs/query/compiled-query.cjs.map +1 -0
- package/dist/cjs/query/compiled-query.d.cts +22 -0
- package/dist/cjs/query/evaluators.cjs +146 -0
- package/dist/cjs/query/evaluators.cjs.map +1 -0
- package/dist/cjs/query/evaluators.d.cts +9 -0
- package/dist/cjs/query/extractors.cjs +122 -0
- package/dist/cjs/query/extractors.cjs.map +1 -0
- package/dist/cjs/query/extractors.d.cts +22 -0
- package/dist/cjs/query/functions.cjs +152 -0
- package/dist/cjs/query/functions.cjs.map +1 -0
- package/dist/cjs/query/functions.d.cts +21 -0
- package/dist/cjs/query/group-by.cjs +91 -0
- package/dist/cjs/query/group-by.cjs.map +1 -0
- package/dist/cjs/query/group-by.d.cts +40 -0
- package/dist/cjs/query/index.d.cts +5 -0
- package/dist/cjs/query/joins.cjs +155 -0
- package/dist/cjs/query/joins.cjs.map +1 -0
- package/dist/cjs/query/joins.d.cts +14 -0
- package/dist/cjs/query/key-by.cjs +43 -0
- package/dist/cjs/query/key-by.cjs.map +1 -0
- package/dist/cjs/query/key-by.d.cts +3 -0
- package/dist/cjs/query/order-by.cjs +229 -0
- package/dist/cjs/query/order-by.cjs.map +1 -0
- package/dist/cjs/query/order-by.d.cts +3 -0
- package/dist/cjs/query/pipeline-compiler.cjs +94 -0
- package/dist/cjs/query/pipeline-compiler.cjs.map +1 -0
- package/dist/cjs/query/pipeline-compiler.d.cts +9 -0
- package/dist/cjs/query/query-builder.cjs +314 -0
- package/dist/cjs/query/query-builder.cjs.map +1 -0
- package/dist/cjs/query/query-builder.d.cts +219 -0
- package/dist/cjs/query/schema.d.cts +98 -0
- package/dist/cjs/query/select.cjs +107 -0
- package/dist/cjs/query/select.cjs.map +1 -0
- package/dist/cjs/query/select.d.cts +3 -0
- package/dist/cjs/query/types.d.cts +188 -0
- package/dist/cjs/query/utils.cjs +154 -0
- package/dist/cjs/query/utils.cjs.map +1 -0
- package/dist/cjs/query/utils.d.cts +37 -0
- package/dist/cjs/transactions.cjs +137 -0
- package/dist/cjs/transactions.cjs.map +1 -0
- package/dist/cjs/transactions.d.cts +27 -0
- package/dist/cjs/types.d.cts +94 -0
- package/dist/cjs/utils.cjs +17 -0
- package/dist/cjs/utils.cjs.map +1 -0
- package/dist/cjs/utils.d.cts +3 -0
- package/dist/esm/SortedMap.d.ts +91 -0
- package/dist/esm/SortedMap.js +140 -0
- package/dist/esm/SortedMap.js.map +1 -0
- package/dist/esm/collection.d.ts +176 -0
- package/dist/esm/collection.js +597 -0
- package/dist/esm/collection.js.map +1 -0
- package/dist/esm/deferred.d.ts +20 -0
- package/dist/esm/deferred.js +25 -0
- package/dist/esm/deferred.js.map +1 -0
- package/dist/esm/errors.d.ts +3 -0
- package/dist/esm/errors.js +10 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.js +33 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/proxy.d.ts +59 -0
- package/dist/esm/proxy.js +654 -0
- package/dist/esm/proxy.js.map +1 -0
- package/dist/esm/query/compiled-query.d.ts +22 -0
- package/dist/esm/query/compiled-query.js +162 -0
- package/dist/esm/query/compiled-query.js.map +1 -0
- package/dist/esm/query/evaluators.d.ts +9 -0
- package/dist/esm/query/evaluators.js +146 -0
- package/dist/esm/query/evaluators.js.map +1 -0
- package/dist/esm/query/extractors.d.ts +22 -0
- package/dist/esm/query/extractors.js +122 -0
- package/dist/esm/query/extractors.js.map +1 -0
- package/dist/esm/query/functions.d.ts +21 -0
- package/dist/esm/query/functions.js +152 -0
- package/dist/esm/query/functions.js.map +1 -0
- package/dist/esm/query/group-by.d.ts +40 -0
- package/dist/esm/query/group-by.js +91 -0
- package/dist/esm/query/group-by.js.map +1 -0
- package/dist/esm/query/index.d.ts +5 -0
- package/dist/esm/query/joins.d.ts +14 -0
- package/dist/esm/query/joins.js +155 -0
- package/dist/esm/query/joins.js.map +1 -0
- package/dist/esm/query/key-by.d.ts +3 -0
- package/dist/esm/query/key-by.js +43 -0
- package/dist/esm/query/key-by.js.map +1 -0
- package/dist/esm/query/order-by.d.ts +3 -0
- package/dist/esm/query/order-by.js +229 -0
- package/dist/esm/query/order-by.js.map +1 -0
- package/dist/esm/query/pipeline-compiler.d.ts +9 -0
- package/dist/esm/query/pipeline-compiler.js +94 -0
- package/dist/esm/query/pipeline-compiler.js.map +1 -0
- package/dist/esm/query/query-builder.d.ts +219 -0
- package/dist/esm/query/query-builder.js +314 -0
- package/dist/esm/query/query-builder.js.map +1 -0
- package/dist/esm/query/schema.d.ts +98 -0
- package/dist/esm/query/select.d.ts +3 -0
- package/dist/esm/query/select.js +107 -0
- package/dist/esm/query/select.js.map +1 -0
- package/dist/esm/query/types.d.ts +188 -0
- package/dist/esm/query/utils.d.ts +37 -0
- package/dist/esm/query/utils.js +154 -0
- package/dist/esm/query/utils.js.map +1 -0
- package/dist/esm/transactions.d.ts +27 -0
- package/dist/esm/transactions.js +137 -0
- package/dist/esm/transactions.js.map +1 -0
- package/dist/esm/types.d.ts +94 -0
- package/dist/esm/utils.d.ts +3 -0
- package/dist/esm/utils.js +17 -0
- package/dist/esm/utils.js.map +1 -0
- package/package.json +57 -0
- package/src/SortedMap.ts +163 -0
- package/src/collection.ts +919 -0
- package/src/deferred.ts +47 -0
- package/src/errors.ts +6 -0
- package/src/index.ts +12 -0
- package/src/proxy.ts +1104 -0
- package/src/query/compiled-query.ts +193 -0
- package/src/query/evaluators.ts +222 -0
- package/src/query/extractors.ts +211 -0
- package/src/query/functions.ts +297 -0
- package/src/query/group-by.ts +137 -0
- package/src/query/index.ts +5 -0
- package/src/query/joins.ts +247 -0
- package/src/query/key-by.ts +61 -0
- package/src/query/order-by.ts +312 -0
- package/src/query/pipeline-compiler.ts +152 -0
- package/src/query/query-builder.ts +898 -0
- package/src/query/schema.ts +255 -0
- package/src/query/select.ts +173 -0
- package/src/query/types.ts +417 -0
- package/src/query/utils.ts +245 -0
- package/src/transactions.ts +198 -0
- package/src/types.ts +125 -0
- 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
|
+
}
|