nicefox-graphdb 0.1.0
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/LICENSE +21 -0
- package/README.md +417 -0
- package/package.json +78 -0
- package/packages/nicefox-graphdb/LICENSE +21 -0
- package/packages/nicefox-graphdb/README.md +417 -0
- package/packages/nicefox-graphdb/dist/auth.d.ts +66 -0
- package/packages/nicefox-graphdb/dist/auth.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/auth.js +148 -0
- package/packages/nicefox-graphdb/dist/auth.js.map +1 -0
- package/packages/nicefox-graphdb/dist/backup.d.ts +51 -0
- package/packages/nicefox-graphdb/dist/backup.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/backup.js +201 -0
- package/packages/nicefox-graphdb/dist/backup.js.map +1 -0
- package/packages/nicefox-graphdb/dist/cli-helpers.d.ts +17 -0
- package/packages/nicefox-graphdb/dist/cli-helpers.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/cli-helpers.js +121 -0
- package/packages/nicefox-graphdb/dist/cli-helpers.js.map +1 -0
- package/packages/nicefox-graphdb/dist/cli.d.ts +3 -0
- package/packages/nicefox-graphdb/dist/cli.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/cli.js +660 -0
- package/packages/nicefox-graphdb/dist/cli.js.map +1 -0
- package/packages/nicefox-graphdb/dist/db.d.ts +118 -0
- package/packages/nicefox-graphdb/dist/db.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/db.js +245 -0
- package/packages/nicefox-graphdb/dist/db.js.map +1 -0
- package/packages/nicefox-graphdb/dist/executor.d.ts +272 -0
- package/packages/nicefox-graphdb/dist/executor.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/executor.js +3579 -0
- package/packages/nicefox-graphdb/dist/executor.js.map +1 -0
- package/packages/nicefox-graphdb/dist/index.d.ts +54 -0
- package/packages/nicefox-graphdb/dist/index.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/index.js +74 -0
- package/packages/nicefox-graphdb/dist/index.js.map +1 -0
- package/packages/nicefox-graphdb/dist/local.d.ts +7 -0
- package/packages/nicefox-graphdb/dist/local.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/local.js +115 -0
- package/packages/nicefox-graphdb/dist/local.js.map +1 -0
- package/packages/nicefox-graphdb/dist/parser.d.ts +300 -0
- package/packages/nicefox-graphdb/dist/parser.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/parser.js +1891 -0
- package/packages/nicefox-graphdb/dist/parser.js.map +1 -0
- package/packages/nicefox-graphdb/dist/remote.d.ts +6 -0
- package/packages/nicefox-graphdb/dist/remote.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/remote.js +87 -0
- package/packages/nicefox-graphdb/dist/remote.js.map +1 -0
- package/packages/nicefox-graphdb/dist/routes.d.ts +31 -0
- package/packages/nicefox-graphdb/dist/routes.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/routes.js +202 -0
- package/packages/nicefox-graphdb/dist/routes.js.map +1 -0
- package/packages/nicefox-graphdb/dist/translator.d.ts +136 -0
- package/packages/nicefox-graphdb/dist/translator.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/translator.js +4849 -0
- package/packages/nicefox-graphdb/dist/translator.js.map +1 -0
- package/packages/nicefox-graphdb/dist/types.d.ts +133 -0
- package/packages/nicefox-graphdb/dist/types.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/types.js +21 -0
- package/packages/nicefox-graphdb/dist/types.js.map +1 -0
|
@@ -0,0 +1,3579 @@
|
|
|
1
|
+
// Query Executor - Full pipeline: Cypher → Parse → Translate → Execute → Format
|
|
2
|
+
import { parse, } from "./parser.js";
|
|
3
|
+
import { Translator } from "./translator.js";
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Executor
|
|
6
|
+
// ============================================================================
|
|
7
|
+
export class Executor {
|
|
8
|
+
db;
|
|
9
|
+
constructor(db) {
|
|
10
|
+
this.db = db;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Execute a Cypher query and return formatted results
|
|
14
|
+
*/
|
|
15
|
+
execute(cypher, params = {}) {
|
|
16
|
+
const startTime = performance.now();
|
|
17
|
+
try {
|
|
18
|
+
// 1. Parse the Cypher query
|
|
19
|
+
const parseResult = parse(cypher);
|
|
20
|
+
if (!parseResult.success) {
|
|
21
|
+
return {
|
|
22
|
+
success: false,
|
|
23
|
+
error: {
|
|
24
|
+
message: parseResult.error.message,
|
|
25
|
+
position: parseResult.error.position,
|
|
26
|
+
line: parseResult.error.line,
|
|
27
|
+
column: parseResult.error.column,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// 2. Check for UNWIND with CREATE pattern (needs special handling)
|
|
32
|
+
const unwindCreateResult = this.tryUnwindCreateExecution(parseResult.query, params);
|
|
33
|
+
if (unwindCreateResult !== null) {
|
|
34
|
+
const endTime = performance.now();
|
|
35
|
+
return {
|
|
36
|
+
success: true,
|
|
37
|
+
data: unwindCreateResult,
|
|
38
|
+
meta: {
|
|
39
|
+
count: unwindCreateResult.length,
|
|
40
|
+
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// 2.2. Check for UNWIND with MERGE pattern (needs special handling)
|
|
45
|
+
const unwindMergeResult = this.tryUnwindMergeExecution(parseResult.query, params);
|
|
46
|
+
if (unwindMergeResult !== null) {
|
|
47
|
+
const endTime = performance.now();
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
data: unwindMergeResult,
|
|
51
|
+
meta: {
|
|
52
|
+
count: unwindMergeResult.length,
|
|
53
|
+
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// 2.3. Check for MATCH+WITH(COLLECT)+UNWIND+RETURN pattern (needs subquery for aggregates)
|
|
58
|
+
const collectUnwindResult = this.tryCollectUnwindExecution(parseResult.query, params);
|
|
59
|
+
if (collectUnwindResult !== null) {
|
|
60
|
+
const endTime = performance.now();
|
|
61
|
+
return {
|
|
62
|
+
success: true,
|
|
63
|
+
data: collectUnwindResult,
|
|
64
|
+
meta: {
|
|
65
|
+
count: collectUnwindResult.length,
|
|
66
|
+
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// 2.4. Check for MATCH+WITH(COLLECT)+DELETE[expr] pattern
|
|
71
|
+
const collectDeleteResult = this.tryCollectDeleteExecution(parseResult.query, params);
|
|
72
|
+
if (collectDeleteResult !== null) {
|
|
73
|
+
const endTime = performance.now();
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
data: collectDeleteResult,
|
|
77
|
+
meta: {
|
|
78
|
+
count: collectDeleteResult.length,
|
|
79
|
+
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// 2.5. Check for CREATE...RETURN pattern (needs special handling)
|
|
84
|
+
const createReturnResult = this.tryCreateReturnExecution(parseResult.query, params);
|
|
85
|
+
if (createReturnResult !== null) {
|
|
86
|
+
const endTime = performance.now();
|
|
87
|
+
return {
|
|
88
|
+
success: true,
|
|
89
|
+
data: createReturnResult,
|
|
90
|
+
meta: {
|
|
91
|
+
count: createReturnResult.length,
|
|
92
|
+
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// 2.5. Check for MERGE with ON CREATE SET / ON MATCH SET (needs special handling)
|
|
97
|
+
const mergeResult = this.tryMergeExecution(parseResult.query, params);
|
|
98
|
+
if (mergeResult !== null) {
|
|
99
|
+
const endTime = performance.now();
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
data: mergeResult,
|
|
103
|
+
meta: {
|
|
104
|
+
count: mergeResult.length,
|
|
105
|
+
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// 3. Check if this is a pattern that needs multi-phase execution
|
|
110
|
+
// (MATCH...CREATE, MATCH...SET, MATCH...DELETE with relationship patterns)
|
|
111
|
+
const multiPhaseResult = this.tryMultiPhaseExecution(parseResult.query, params);
|
|
112
|
+
if (multiPhaseResult !== null) {
|
|
113
|
+
const endTime = performance.now();
|
|
114
|
+
return {
|
|
115
|
+
success: true,
|
|
116
|
+
data: multiPhaseResult,
|
|
117
|
+
meta: {
|
|
118
|
+
count: multiPhaseResult.length,
|
|
119
|
+
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// 3. Standard single-phase execution: Translate to SQL
|
|
124
|
+
const translator = new Translator(params);
|
|
125
|
+
const translation = translator.translate(parseResult.query);
|
|
126
|
+
// 4. Execute SQL statements
|
|
127
|
+
let rows = [];
|
|
128
|
+
const returnColumns = translation.returnColumns;
|
|
129
|
+
this.db.transaction(() => {
|
|
130
|
+
for (const stmt of translation.statements) {
|
|
131
|
+
const result = this.db.execute(stmt.sql, stmt.params);
|
|
132
|
+
// If this is a SELECT (RETURN clause), capture the results
|
|
133
|
+
if (result.rows.length > 0 || stmt.sql.trim().toUpperCase().startsWith("SELECT")) {
|
|
134
|
+
rows = result.rows;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
// 5. Format results
|
|
139
|
+
const formattedRows = this.formatResults(rows, returnColumns);
|
|
140
|
+
const endTime = performance.now();
|
|
141
|
+
return {
|
|
142
|
+
success: true,
|
|
143
|
+
data: formattedRows,
|
|
144
|
+
meta: {
|
|
145
|
+
count: formattedRows.length,
|
|
146
|
+
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return {
|
|
152
|
+
success: false,
|
|
153
|
+
error: {
|
|
154
|
+
message: error instanceof Error ? error.message : String(error),
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Handle UNWIND with CREATE pattern
|
|
161
|
+
* UNWIND expands an array and executes CREATE for each element
|
|
162
|
+
*/
|
|
163
|
+
tryUnwindCreateExecution(query, params) {
|
|
164
|
+
const clauses = query.clauses;
|
|
165
|
+
// Find UNWIND, CREATE, WITH, and RETURN clauses
|
|
166
|
+
const unwindClauses = [];
|
|
167
|
+
const createClauses = [];
|
|
168
|
+
const withClauses = [];
|
|
169
|
+
let returnClause = null;
|
|
170
|
+
for (const clause of clauses) {
|
|
171
|
+
if (clause.type === "UNWIND") {
|
|
172
|
+
unwindClauses.push(clause);
|
|
173
|
+
}
|
|
174
|
+
else if (clause.type === "CREATE") {
|
|
175
|
+
createClauses.push(clause);
|
|
176
|
+
}
|
|
177
|
+
else if (clause.type === "RETURN") {
|
|
178
|
+
returnClause = clause;
|
|
179
|
+
}
|
|
180
|
+
else if (clause.type === "WITH") {
|
|
181
|
+
withClauses.push(clause);
|
|
182
|
+
}
|
|
183
|
+
else if (clause.type === "MATCH") {
|
|
184
|
+
// If there's a MATCH, don't handle here
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Only handle if we have both UNWIND and CREATE
|
|
189
|
+
if (unwindClauses.length === 0 || createClauses.length === 0) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
// For each UNWIND, expand the array and execute CREATE
|
|
193
|
+
const results = [];
|
|
194
|
+
// Get the values from the UNWIND expression
|
|
195
|
+
const unwindValues = this.evaluateUnwindExpressions(unwindClauses, params);
|
|
196
|
+
// Generate all combinations (cartesian product) of UNWIND values
|
|
197
|
+
const combinations = this.generateCartesianProduct(unwindValues);
|
|
198
|
+
// Check if RETURN has aggregate functions
|
|
199
|
+
const hasAggregates = returnClause?.items.some(item => item.expression.type === "function" &&
|
|
200
|
+
["sum", "count", "avg", "min", "max", "collect"].includes(item.expression.functionName?.toLowerCase() || ""));
|
|
201
|
+
// Check if any WITH clause has aggregate functions
|
|
202
|
+
const hasWithAggregates = withClauses.some(clause => clause.items.some(item => item.expression.type === "function" &&
|
|
203
|
+
["sum", "count", "avg", "min", "max", "collect"].includes(item.expression.functionName?.toLowerCase() || "")));
|
|
204
|
+
// Extract WITH aggregate info
|
|
205
|
+
const withAggregateMap = new Map();
|
|
206
|
+
for (const withClause of withClauses) {
|
|
207
|
+
for (const item of withClause.items) {
|
|
208
|
+
if (item.alias && item.expression.type === "function") {
|
|
209
|
+
const funcName = item.expression.functionName?.toLowerCase();
|
|
210
|
+
if (funcName && ["sum", "count", "avg", "min", "max", "collect"].includes(funcName)) {
|
|
211
|
+
const args = item.expression.args || [];
|
|
212
|
+
if (args.length > 0) {
|
|
213
|
+
const arg = args[0];
|
|
214
|
+
if (arg.type === "variable") {
|
|
215
|
+
withAggregateMap.set(item.alias, {
|
|
216
|
+
functionName: funcName,
|
|
217
|
+
argVariable: arg.variable,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
else if (arg.type === "property") {
|
|
221
|
+
withAggregateMap.set(item.alias, {
|
|
222
|
+
functionName: funcName,
|
|
223
|
+
argVariable: arg.variable,
|
|
224
|
+
argProperty: arg.property,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Check if RETURN references WITH aggregate aliases
|
|
233
|
+
const returnsWithAggregateAliases = returnClause?.items.some(item => item.expression.type === "variable" && withAggregateMap.has(item.expression.variable)) || false;
|
|
234
|
+
// For aggregates, collect intermediate values
|
|
235
|
+
const aggregateValues = new Map();
|
|
236
|
+
// Also collect values for WITH aggregates
|
|
237
|
+
const withAggregateValues = new Map();
|
|
238
|
+
this.db.transaction(() => {
|
|
239
|
+
for (const combination of combinations) {
|
|
240
|
+
// Build a map of unwind variable -> current value
|
|
241
|
+
const unwindContext = {};
|
|
242
|
+
for (let i = 0; i < unwindClauses.length; i++) {
|
|
243
|
+
unwindContext[unwindClauses[i].alias] = combination[i];
|
|
244
|
+
}
|
|
245
|
+
// Execute CREATE with the unwind context
|
|
246
|
+
const createdIds = new Map();
|
|
247
|
+
for (const createClause of createClauses) {
|
|
248
|
+
for (const pattern of createClause.patterns) {
|
|
249
|
+
if (this.isRelationshipPattern(pattern)) {
|
|
250
|
+
this.executeCreateRelationshipPatternWithUnwind(pattern, createdIds, params, unwindContext);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
const id = crypto.randomUUID();
|
|
254
|
+
const labelJson = this.normalizeLabelToJson(pattern.label);
|
|
255
|
+
const props = this.resolvePropertiesWithUnwind(pattern.properties || {}, params, unwindContext);
|
|
256
|
+
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [id, labelJson, JSON.stringify(props)]);
|
|
257
|
+
if (pattern.variable) {
|
|
258
|
+
createdIds.set(pattern.variable, id);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Handle RETURN if present
|
|
264
|
+
if (returnClause) {
|
|
265
|
+
// Check WITH filter first
|
|
266
|
+
let passesWithFilter = true;
|
|
267
|
+
for (const withClause of withClauses) {
|
|
268
|
+
if (withClause.where) {
|
|
269
|
+
passesWithFilter = this.evaluateWithWhereCondition(withClause.where, createdIds, params);
|
|
270
|
+
if (!passesWithFilter)
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (!passesWithFilter)
|
|
275
|
+
continue;
|
|
276
|
+
if (hasAggregates || hasWithAggregates) {
|
|
277
|
+
// Collect values for WITH aggregates
|
|
278
|
+
if (hasWithAggregates) {
|
|
279
|
+
for (const [alias, aggInfo] of withAggregateMap) {
|
|
280
|
+
let value;
|
|
281
|
+
if (aggInfo.argProperty) {
|
|
282
|
+
const id = createdIds.get(aggInfo.argVariable);
|
|
283
|
+
if (id) {
|
|
284
|
+
let result = this.db.execute("SELECT properties FROM nodes WHERE id = ?", [id]);
|
|
285
|
+
if (result.rows.length === 0) {
|
|
286
|
+
result = this.db.execute("SELECT properties FROM edges WHERE id = ?", [id]);
|
|
287
|
+
}
|
|
288
|
+
if (result.rows.length > 0) {
|
|
289
|
+
const props = typeof result.rows[0].properties === "string"
|
|
290
|
+
? JSON.parse(result.rows[0].properties)
|
|
291
|
+
: result.rows[0].properties;
|
|
292
|
+
value = props[aggInfo.argProperty];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (value !== undefined) {
|
|
297
|
+
if (!withAggregateValues.has(alias)) {
|
|
298
|
+
withAggregateValues.set(alias, []);
|
|
299
|
+
}
|
|
300
|
+
withAggregateValues.get(alias).push(value);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Collect values for RETURN aggregates
|
|
305
|
+
if (hasAggregates) {
|
|
306
|
+
for (const item of returnClause.items) {
|
|
307
|
+
const alias = item.alias || this.getExpressionName(item.expression);
|
|
308
|
+
if (item.expression.type === "function") {
|
|
309
|
+
const funcName = item.expression.functionName?.toLowerCase();
|
|
310
|
+
const args = item.expression.args || [];
|
|
311
|
+
if (args.length > 0) {
|
|
312
|
+
const arg = args[0];
|
|
313
|
+
let value;
|
|
314
|
+
if (arg.type === "property") {
|
|
315
|
+
const variable = arg.variable;
|
|
316
|
+
const property = arg.property;
|
|
317
|
+
const id = createdIds.get(variable);
|
|
318
|
+
if (id) {
|
|
319
|
+
// Try nodes first, then edges
|
|
320
|
+
let result = this.db.execute("SELECT properties FROM nodes WHERE id = ?", [id]);
|
|
321
|
+
if (result.rows.length === 0) {
|
|
322
|
+
// Try edges table
|
|
323
|
+
result = this.db.execute("SELECT properties FROM edges WHERE id = ?", [id]);
|
|
324
|
+
}
|
|
325
|
+
if (result.rows.length > 0) {
|
|
326
|
+
const props = typeof result.rows[0].properties === "string"
|
|
327
|
+
? JSON.parse(result.rows[0].properties)
|
|
328
|
+
: result.rows[0].properties;
|
|
329
|
+
value = props[property];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (value !== undefined) {
|
|
334
|
+
if (!aggregateValues.has(alias)) {
|
|
335
|
+
aggregateValues.set(alias, []);
|
|
336
|
+
}
|
|
337
|
+
aggregateValues.get(alias).push(value);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else if (funcName === "count") {
|
|
341
|
+
// count(*) - just count iterations
|
|
342
|
+
if (!aggregateValues.has(alias)) {
|
|
343
|
+
aggregateValues.set(alias, []);
|
|
344
|
+
}
|
|
345
|
+
aggregateValues.get(alias).push(1);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
// Non-aggregate case - build result row as before
|
|
353
|
+
const resultRow = {};
|
|
354
|
+
for (const item of returnClause.items) {
|
|
355
|
+
const alias = item.alias || this.getExpressionName(item.expression);
|
|
356
|
+
if (item.expression.type === "variable") {
|
|
357
|
+
const variable = item.expression.variable;
|
|
358
|
+
const id = createdIds.get(variable);
|
|
359
|
+
if (id) {
|
|
360
|
+
const nodeResult = this.db.execute("SELECT id, label, properties FROM nodes WHERE id = ?", [id]);
|
|
361
|
+
if (nodeResult.rows.length > 0) {
|
|
362
|
+
const row = nodeResult.rows[0];
|
|
363
|
+
// Neo4j 3.5 format: return properties directly
|
|
364
|
+
resultRow[alias] = typeof row.properties === "string"
|
|
365
|
+
? JSON.parse(row.properties)
|
|
366
|
+
: row.properties;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else if (item.expression.type === "property") {
|
|
371
|
+
// Handle property access like n.num or r.prop
|
|
372
|
+
const variable = item.expression.variable;
|
|
373
|
+
const property = item.expression.property;
|
|
374
|
+
const id = createdIds.get(variable);
|
|
375
|
+
if (id) {
|
|
376
|
+
// Try nodes first, then edges
|
|
377
|
+
let nodeResult = this.db.execute("SELECT properties FROM nodes WHERE id = ?", [id]);
|
|
378
|
+
if (nodeResult.rows.length === 0) {
|
|
379
|
+
// Try edges table
|
|
380
|
+
nodeResult = this.db.execute("SELECT properties FROM edges WHERE id = ?", [id]);
|
|
381
|
+
}
|
|
382
|
+
if (nodeResult.rows.length > 0) {
|
|
383
|
+
const props = typeof nodeResult.rows[0].properties === "string"
|
|
384
|
+
? JSON.parse(nodeResult.rows[0].properties)
|
|
385
|
+
: nodeResult.rows[0].properties;
|
|
386
|
+
resultRow[alias] = props[property];
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (Object.keys(resultRow).length > 0) {
|
|
392
|
+
results.push(resultRow);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
// Compute WITH aggregate results if RETURN references them
|
|
399
|
+
if (returnsWithAggregateAliases && returnClause) {
|
|
400
|
+
const withAggregateResult = {};
|
|
401
|
+
for (const item of returnClause.items) {
|
|
402
|
+
if (item.expression.type === "variable") {
|
|
403
|
+
const alias = item.expression.variable;
|
|
404
|
+
if (withAggregateMap.has(alias)) {
|
|
405
|
+
const aggInfo = withAggregateMap.get(alias);
|
|
406
|
+
const values = withAggregateValues.get(alias) || [];
|
|
407
|
+
switch (aggInfo.functionName) {
|
|
408
|
+
case "sum":
|
|
409
|
+
withAggregateResult[alias] = values.reduce((a, b) => a + b, 0);
|
|
410
|
+
break;
|
|
411
|
+
case "count":
|
|
412
|
+
withAggregateResult[alias] = values.length;
|
|
413
|
+
break;
|
|
414
|
+
case "avg":
|
|
415
|
+
withAggregateResult[alias] = values.length > 0
|
|
416
|
+
? values.reduce((a, b) => a + b, 0) / values.length
|
|
417
|
+
: null;
|
|
418
|
+
break;
|
|
419
|
+
case "min":
|
|
420
|
+
withAggregateResult[alias] = values.length > 0 ? Math.min(...values) : null;
|
|
421
|
+
break;
|
|
422
|
+
case "max":
|
|
423
|
+
withAggregateResult[alias] = values.length > 0 ? Math.max(...values) : null;
|
|
424
|
+
break;
|
|
425
|
+
case "collect":
|
|
426
|
+
withAggregateResult[alias] = values;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (Object.keys(withAggregateResult).length > 0) {
|
|
433
|
+
results.push(withAggregateResult);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// Compute aggregate results if needed
|
|
437
|
+
if (hasAggregates && returnClause) {
|
|
438
|
+
const aggregateResult = {};
|
|
439
|
+
for (const item of returnClause.items) {
|
|
440
|
+
const alias = item.alias || this.getExpressionName(item.expression);
|
|
441
|
+
if (item.expression.type === "function") {
|
|
442
|
+
const funcName = item.expression.functionName?.toLowerCase();
|
|
443
|
+
const values = aggregateValues.get(alias) || [];
|
|
444
|
+
switch (funcName) {
|
|
445
|
+
case "sum":
|
|
446
|
+
aggregateResult[alias] = values.reduce((a, b) => a + b, 0);
|
|
447
|
+
break;
|
|
448
|
+
case "count":
|
|
449
|
+
aggregateResult[alias] = values.length;
|
|
450
|
+
break;
|
|
451
|
+
case "avg":
|
|
452
|
+
aggregateResult[alias] = values.length > 0
|
|
453
|
+
? values.reduce((a, b) => a + b, 0) / values.length
|
|
454
|
+
: null;
|
|
455
|
+
break;
|
|
456
|
+
case "min":
|
|
457
|
+
aggregateResult[alias] = values.length > 0 ? Math.min(...values) : null;
|
|
458
|
+
break;
|
|
459
|
+
case "max":
|
|
460
|
+
aggregateResult[alias] = values.length > 0 ? Math.max(...values) : null;
|
|
461
|
+
break;
|
|
462
|
+
case "collect":
|
|
463
|
+
aggregateResult[alias] = values;
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (Object.keys(aggregateResult).length > 0) {
|
|
469
|
+
results.push(aggregateResult);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// Apply SKIP and LIMIT if present in RETURN clause
|
|
473
|
+
let finalResults = results;
|
|
474
|
+
if (returnClause) {
|
|
475
|
+
if (returnClause.skip !== undefined && returnClause.skip !== null) {
|
|
476
|
+
const skipValue = typeof returnClause.skip === "number"
|
|
477
|
+
? returnClause.skip
|
|
478
|
+
: params[returnClause.skip] || 0;
|
|
479
|
+
finalResults = finalResults.slice(skipValue);
|
|
480
|
+
}
|
|
481
|
+
if (returnClause.limit !== undefined && returnClause.limit !== null) {
|
|
482
|
+
const limitValue = typeof returnClause.limit === "number"
|
|
483
|
+
? returnClause.limit
|
|
484
|
+
: params[returnClause.limit] || 0;
|
|
485
|
+
finalResults = finalResults.slice(0, limitValue);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return finalResults;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Evaluate a WITH clause WHERE condition against created nodes
|
|
492
|
+
*/
|
|
493
|
+
evaluateWithWhereCondition(condition, createdIds, params) {
|
|
494
|
+
if (condition.type === "comparison") {
|
|
495
|
+
// Handle comparison: n.prop % 2 = 0
|
|
496
|
+
const left = condition.left;
|
|
497
|
+
const right = condition.right;
|
|
498
|
+
const operator = condition.operator;
|
|
499
|
+
const leftValue = this.evaluateExpressionForFilter(left, createdIds, params);
|
|
500
|
+
const rightValue = this.evaluateExpressionForFilter(right, createdIds, params);
|
|
501
|
+
switch (operator) {
|
|
502
|
+
case "=": return leftValue === rightValue;
|
|
503
|
+
case "<>": return leftValue !== rightValue;
|
|
504
|
+
case "<": return leftValue < rightValue;
|
|
505
|
+
case ">": return leftValue > rightValue;
|
|
506
|
+
case "<=": return leftValue <= rightValue;
|
|
507
|
+
case ">=": return leftValue >= rightValue;
|
|
508
|
+
default: return true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
else if (condition.type === "and") {
|
|
512
|
+
return condition.conditions.every((c) => this.evaluateWithWhereCondition(c, createdIds, params));
|
|
513
|
+
}
|
|
514
|
+
else if (condition.type === "or") {
|
|
515
|
+
return condition.conditions.some((c) => this.evaluateWithWhereCondition(c, createdIds, params));
|
|
516
|
+
}
|
|
517
|
+
// For other condition types, pass through
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Evaluate a WITH clause WHERE condition using captured property values
|
|
522
|
+
* This is used for patterns like: WITH n.num AS num ... DELETE n ... WITH num WHERE num % 2 = 0
|
|
523
|
+
*/
|
|
524
|
+
evaluateWithWhereConditionWithPropertyAliases(condition, resolvedIds, capturedPropertyValues, propertyAliasMap, params) {
|
|
525
|
+
if (condition.type === "comparison") {
|
|
526
|
+
const left = condition.left;
|
|
527
|
+
const right = condition.right;
|
|
528
|
+
const operator = condition.operator;
|
|
529
|
+
const leftValue = this.evaluateExpressionWithPropertyAliases(left, resolvedIds, capturedPropertyValues, propertyAliasMap, params);
|
|
530
|
+
const rightValue = this.evaluateExpressionWithPropertyAliases(right, resolvedIds, capturedPropertyValues, propertyAliasMap, params);
|
|
531
|
+
switch (operator) {
|
|
532
|
+
case "=": return leftValue === rightValue;
|
|
533
|
+
case "<>": return leftValue !== rightValue;
|
|
534
|
+
case "<": return leftValue < rightValue;
|
|
535
|
+
case ">": return leftValue > rightValue;
|
|
536
|
+
case "<=": return leftValue <= rightValue;
|
|
537
|
+
case ">=": return leftValue >= rightValue;
|
|
538
|
+
default: return true;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
else if (condition.type === "and") {
|
|
542
|
+
return condition.conditions.every((c) => this.evaluateWithWhereConditionWithPropertyAliases(c, resolvedIds, capturedPropertyValues, propertyAliasMap, params));
|
|
543
|
+
}
|
|
544
|
+
else if (condition.type === "or") {
|
|
545
|
+
return condition.conditions.some((c) => this.evaluateWithWhereConditionWithPropertyAliases(c, resolvedIds, capturedPropertyValues, propertyAliasMap, params));
|
|
546
|
+
}
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Evaluate an expression using captured property values (for property alias references)
|
|
551
|
+
*/
|
|
552
|
+
evaluateExpressionWithPropertyAliases(expr, resolvedIds, capturedPropertyValues, propertyAliasMap, params) {
|
|
553
|
+
if (expr.type === "literal") {
|
|
554
|
+
return expr.value;
|
|
555
|
+
}
|
|
556
|
+
else if (expr.type === "variable") {
|
|
557
|
+
const varName = expr.variable;
|
|
558
|
+
// Check if this is a property alias
|
|
559
|
+
if (propertyAliasMap.has(varName)) {
|
|
560
|
+
return capturedPropertyValues[varName];
|
|
561
|
+
}
|
|
562
|
+
// Otherwise it might be a node ID
|
|
563
|
+
return resolvedIds[varName];
|
|
564
|
+
}
|
|
565
|
+
else if (expr.type === "property") {
|
|
566
|
+
const variable = expr.variable;
|
|
567
|
+
const property = expr.property;
|
|
568
|
+
const id = resolvedIds[variable];
|
|
569
|
+
if (id) {
|
|
570
|
+
// Try nodes first, then edges
|
|
571
|
+
let result = this.db.execute("SELECT properties FROM nodes WHERE id = ?", [id]);
|
|
572
|
+
if (result.rows.length === 0) {
|
|
573
|
+
result = this.db.execute("SELECT properties FROM edges WHERE id = ?", [id]);
|
|
574
|
+
}
|
|
575
|
+
if (result.rows.length > 0) {
|
|
576
|
+
const props = typeof result.rows[0].properties === "string"
|
|
577
|
+
? JSON.parse(result.rows[0].properties)
|
|
578
|
+
: result.rows[0].properties;
|
|
579
|
+
return props[property];
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
else if (expr.type === "binary") {
|
|
585
|
+
const left = this.evaluateExpressionWithPropertyAliases(expr.left, resolvedIds, capturedPropertyValues, propertyAliasMap, params);
|
|
586
|
+
const right = this.evaluateExpressionWithPropertyAliases(expr.right, resolvedIds, capturedPropertyValues, propertyAliasMap, params);
|
|
587
|
+
switch (expr.operator) {
|
|
588
|
+
case "+": return left + right;
|
|
589
|
+
case "-": return left - right;
|
|
590
|
+
case "*": return left * right;
|
|
591
|
+
case "/": return left / right;
|
|
592
|
+
case "%": return left % right;
|
|
593
|
+
default: return null;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
else if (expr.type === "parameter") {
|
|
597
|
+
return params[expr.name];
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Evaluate an expression for filtering in UNWIND+CREATE+WITH context
|
|
603
|
+
*/
|
|
604
|
+
evaluateExpressionForFilter(expr, createdIds, params) {
|
|
605
|
+
if (expr.type === "literal") {
|
|
606
|
+
return expr.value;
|
|
607
|
+
}
|
|
608
|
+
else if (expr.type === "property") {
|
|
609
|
+
const variable = expr.variable;
|
|
610
|
+
const property = expr.property;
|
|
611
|
+
const id = createdIds.get(variable);
|
|
612
|
+
if (id) {
|
|
613
|
+
// Try nodes first, then edges
|
|
614
|
+
let result = this.db.execute("SELECT properties FROM nodes WHERE id = ?", [id]);
|
|
615
|
+
if (result.rows.length === 0) {
|
|
616
|
+
// Try edges table
|
|
617
|
+
result = this.db.execute("SELECT properties FROM edges WHERE id = ?", [id]);
|
|
618
|
+
}
|
|
619
|
+
if (result.rows.length > 0) {
|
|
620
|
+
const props = typeof result.rows[0].properties === "string"
|
|
621
|
+
? JSON.parse(result.rows[0].properties)
|
|
622
|
+
: result.rows[0].properties;
|
|
623
|
+
return props[property];
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
else if (expr.type === "binary") {
|
|
629
|
+
const left = this.evaluateExpressionForFilter(expr.left, createdIds, params);
|
|
630
|
+
const right = this.evaluateExpressionForFilter(expr.right, createdIds, params);
|
|
631
|
+
switch (expr.operator) {
|
|
632
|
+
case "+": return left + right;
|
|
633
|
+
case "-": return left - right;
|
|
634
|
+
case "*": return left * right;
|
|
635
|
+
case "/": return left / right;
|
|
636
|
+
case "%": return left % right;
|
|
637
|
+
default: return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
else if (expr.type === "parameter") {
|
|
641
|
+
return params[expr.name];
|
|
642
|
+
}
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Handle UNWIND + MERGE pattern
|
|
647
|
+
* This requires special handling to resolve UNWIND variables in MERGE patterns
|
|
648
|
+
*/
|
|
649
|
+
tryUnwindMergeExecution(query, params) {
|
|
650
|
+
const clauses = query.clauses;
|
|
651
|
+
// Find UNWIND and MERGE clauses
|
|
652
|
+
const unwindClauses = [];
|
|
653
|
+
let mergeClause = null;
|
|
654
|
+
let returnClause = null;
|
|
655
|
+
for (const clause of clauses) {
|
|
656
|
+
if (clause.type === "UNWIND") {
|
|
657
|
+
unwindClauses.push(clause);
|
|
658
|
+
}
|
|
659
|
+
else if (clause.type === "MERGE") {
|
|
660
|
+
mergeClause = clause;
|
|
661
|
+
}
|
|
662
|
+
else if (clause.type === "RETURN") {
|
|
663
|
+
returnClause = clause;
|
|
664
|
+
}
|
|
665
|
+
else if (clause.type === "MATCH" || clause.type === "CREATE") {
|
|
666
|
+
// If there's a MATCH or CREATE, don't handle here
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
// Only handle if we have both UNWIND and MERGE
|
|
671
|
+
if (unwindClauses.length === 0 || !mergeClause) {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
// Get the values from the UNWIND expression
|
|
675
|
+
const unwindValues = this.evaluateUnwindExpressions(unwindClauses, params);
|
|
676
|
+
// Generate all combinations (cartesian product) of UNWIND values
|
|
677
|
+
const combinations = this.generateCartesianProduct(unwindValues);
|
|
678
|
+
// Track created/merged node count
|
|
679
|
+
let mergedCount = 0;
|
|
680
|
+
this.db.transaction(() => {
|
|
681
|
+
for (const combination of combinations) {
|
|
682
|
+
// Build a map of unwind variable -> current value
|
|
683
|
+
const unwindContext = {};
|
|
684
|
+
for (let i = 0; i < unwindClauses.length; i++) {
|
|
685
|
+
unwindContext[unwindClauses[i].alias] = combination[i];
|
|
686
|
+
}
|
|
687
|
+
// Execute MERGE for each pattern
|
|
688
|
+
for (const pattern of mergeClause.patterns) {
|
|
689
|
+
if (!this.isRelationshipPattern(pattern)) {
|
|
690
|
+
// Node pattern MERGE
|
|
691
|
+
const nodePattern = pattern;
|
|
692
|
+
const props = this.resolvePropertiesWithUnwind(nodePattern.properties || {}, params, unwindContext);
|
|
693
|
+
const labelJson = this.normalizeLabelToJson(nodePattern.label);
|
|
694
|
+
// Check if node exists
|
|
695
|
+
let whereConditions = [];
|
|
696
|
+
let whereParams = [];
|
|
697
|
+
if (nodePattern.label) {
|
|
698
|
+
whereConditions.push("EXISTS (SELECT 1 FROM json_each(label) WHERE value = ?)");
|
|
699
|
+
whereParams.push(nodePattern.label);
|
|
700
|
+
}
|
|
701
|
+
for (const [key, value] of Object.entries(props)) {
|
|
702
|
+
whereConditions.push(`json_extract(properties, '$.${key}') = ?`);
|
|
703
|
+
whereParams.push(value);
|
|
704
|
+
}
|
|
705
|
+
const existsQuery = whereConditions.length > 0
|
|
706
|
+
? `SELECT id FROM nodes WHERE ${whereConditions.join(" AND ")}`
|
|
707
|
+
: "SELECT id FROM nodes LIMIT 1";
|
|
708
|
+
const existsResult = this.db.execute(existsQuery, whereParams);
|
|
709
|
+
if (existsResult.rows.length === 0) {
|
|
710
|
+
// Node doesn't exist, create it
|
|
711
|
+
const id = crypto.randomUUID();
|
|
712
|
+
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [id, labelJson, JSON.stringify(props)]);
|
|
713
|
+
}
|
|
714
|
+
mergedCount++;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
// Handle RETURN clause
|
|
720
|
+
if (returnClause) {
|
|
721
|
+
// For count(*) return the number of UNWIND iterations
|
|
722
|
+
for (const item of returnClause.items) {
|
|
723
|
+
if (item.expression.type === "function" &&
|
|
724
|
+
item.expression.functionName?.toLowerCase() === "count" &&
|
|
725
|
+
(!item.expression.args || item.expression.args.length === 0 ||
|
|
726
|
+
(item.expression.args.length === 1 &&
|
|
727
|
+
item.expression.args[0].type === "literal" &&
|
|
728
|
+
item.expression.args[0].value === "*"))) {
|
|
729
|
+
const alias = item.alias || "count(*)";
|
|
730
|
+
return [{ [alias]: mergedCount }];
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return [];
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Handle MATCH+WITH(COLLECT)+UNWIND+RETURN pattern
|
|
738
|
+
* This requires a subquery for the aggregate function because SQLite doesn't
|
|
739
|
+
* allow aggregate functions directly inside json_each()
|
|
740
|
+
*/
|
|
741
|
+
tryCollectUnwindExecution(query, params) {
|
|
742
|
+
const clauses = query.clauses;
|
|
743
|
+
// Find the pattern: MATCH + WITH (containing COLLECT) + UNWIND + RETURN
|
|
744
|
+
let matchClauses = [];
|
|
745
|
+
let withClause = null;
|
|
746
|
+
let unwindClause = null;
|
|
747
|
+
let returnClause = null;
|
|
748
|
+
for (const clause of clauses) {
|
|
749
|
+
if (clause.type === "MATCH" || clause.type === "OPTIONAL_MATCH") {
|
|
750
|
+
matchClauses.push(clause);
|
|
751
|
+
}
|
|
752
|
+
else if (clause.type === "WITH") {
|
|
753
|
+
withClause = clause;
|
|
754
|
+
}
|
|
755
|
+
else if (clause.type === "UNWIND") {
|
|
756
|
+
unwindClause = clause;
|
|
757
|
+
}
|
|
758
|
+
else if (clause.type === "RETURN") {
|
|
759
|
+
returnClause = clause;
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
// Unsupported clause in this pattern
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
// Must have MATCH, WITH, UNWIND, and RETURN
|
|
767
|
+
if (matchClauses.length === 0 || !withClause || !unwindClause || !returnClause) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
// WITH must have exactly one item that's a COLLECT function
|
|
771
|
+
if (withClause.items.length !== 1) {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
const withItem = withClause.items[0];
|
|
775
|
+
if (withItem.expression.type !== "function" || withItem.expression.functionName !== "COLLECT") {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
const collectAlias = withItem.alias;
|
|
779
|
+
if (!collectAlias) {
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
// UNWIND must reference the COLLECT alias
|
|
783
|
+
if (unwindClause.expression.type !== "variable" || unwindClause.expression.variable !== collectAlias) {
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
// Execute in two phases:
|
|
787
|
+
// Phase 1: Run MATCH with COLLECT to get the aggregated array
|
|
788
|
+
// Phase 2: Expand the array and return results
|
|
789
|
+
// Build a query to get the collected values
|
|
790
|
+
const collectArg = withItem.expression.args?.[0];
|
|
791
|
+
if (!collectArg) {
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
// Build a MATCH...RETURN query that collects the values
|
|
795
|
+
const collectQuery = {
|
|
796
|
+
clauses: [
|
|
797
|
+
...matchClauses,
|
|
798
|
+
{
|
|
799
|
+
type: "RETURN",
|
|
800
|
+
items: [{
|
|
801
|
+
expression: withItem.expression,
|
|
802
|
+
alias: collectAlias,
|
|
803
|
+
}],
|
|
804
|
+
},
|
|
805
|
+
],
|
|
806
|
+
};
|
|
807
|
+
// Translate and execute the collect query
|
|
808
|
+
const translator = new Translator(params);
|
|
809
|
+
const collectTranslation = translator.translate(collectQuery);
|
|
810
|
+
let collectedValues = [];
|
|
811
|
+
for (const stmt of collectTranslation.statements) {
|
|
812
|
+
const result = this.db.execute(stmt.sql, stmt.params);
|
|
813
|
+
if (result.rows.length > 0) {
|
|
814
|
+
// The result should have a single row with the collected array
|
|
815
|
+
const row = result.rows[0];
|
|
816
|
+
const collected = row[collectAlias];
|
|
817
|
+
if (typeof collected === "string") {
|
|
818
|
+
try {
|
|
819
|
+
collectedValues = JSON.parse(collected);
|
|
820
|
+
}
|
|
821
|
+
catch {
|
|
822
|
+
collectedValues = [collected];
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
else if (Array.isArray(collected)) {
|
|
826
|
+
collectedValues = collected;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
// Build results by expanding the collected values
|
|
831
|
+
const results = [];
|
|
832
|
+
const unwindAlias = unwindClause.alias;
|
|
833
|
+
for (const value of collectedValues) {
|
|
834
|
+
const resultRow = {};
|
|
835
|
+
for (const item of returnClause.items) {
|
|
836
|
+
const alias = item.alias || this.getExpressionName(item.expression);
|
|
837
|
+
if (item.expression.type === "variable" && item.expression.variable === unwindAlias) {
|
|
838
|
+
resultRow[alias] = value;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (Object.keys(resultRow).length > 0) {
|
|
842
|
+
results.push(resultRow);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return results;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Handle MATCH+WITH(COLLECT)+DELETE[expr] pattern
|
|
849
|
+
* This handles queries like:
|
|
850
|
+
* MATCH (:User)-[:FRIEND]->(n)
|
|
851
|
+
* WITH collect(n) AS friends
|
|
852
|
+
* DETACH DELETE friends[$friendIndex]
|
|
853
|
+
*/
|
|
854
|
+
tryCollectDeleteExecution(query, params) {
|
|
855
|
+
const clauses = query.clauses;
|
|
856
|
+
// Find the pattern: MATCH + WITH (containing COLLECT) + DELETE
|
|
857
|
+
let matchClauses = [];
|
|
858
|
+
let withClause = null;
|
|
859
|
+
let deleteClause = null;
|
|
860
|
+
for (const clause of clauses) {
|
|
861
|
+
if (clause.type === "MATCH" || clause.type === "OPTIONAL_MATCH") {
|
|
862
|
+
matchClauses.push(clause);
|
|
863
|
+
}
|
|
864
|
+
else if (clause.type === "WITH") {
|
|
865
|
+
withClause = clause;
|
|
866
|
+
}
|
|
867
|
+
else if (clause.type === "DELETE") {
|
|
868
|
+
deleteClause = clause;
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
// Unsupported clause in this pattern
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// Must have MATCH, WITH, and DELETE with expressions
|
|
876
|
+
if (matchClauses.length === 0 || !withClause || !deleteClause) {
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
// DELETE must have expressions (not just simple variables)
|
|
880
|
+
if (!deleteClause.expressions || deleteClause.expressions.length === 0) {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
// WITH must have exactly one item that's a COLLECT function
|
|
884
|
+
if (withClause.items.length !== 1) {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
const withItem = withClause.items[0];
|
|
888
|
+
if (withItem.expression.type !== "function" ||
|
|
889
|
+
withItem.expression.functionName?.toUpperCase() !== "COLLECT") {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
const collectAlias = withItem.alias;
|
|
893
|
+
if (!collectAlias) {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
// Execute in phases:
|
|
897
|
+
// Phase 1: Run MATCH to get individual node IDs, then collect them manually
|
|
898
|
+
const collectArg = withItem.expression.args?.[0];
|
|
899
|
+
if (!collectArg) {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
// The collect arg should be a variable like 'n'
|
|
903
|
+
if (collectArg.type !== "variable") {
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
const collectVarName = collectArg.variable;
|
|
907
|
+
// Build a MATCH...RETURN query that returns the node IDs individually
|
|
908
|
+
const matchQuery = {
|
|
909
|
+
clauses: [
|
|
910
|
+
...matchClauses,
|
|
911
|
+
{
|
|
912
|
+
type: "RETURN",
|
|
913
|
+
items: [{
|
|
914
|
+
expression: {
|
|
915
|
+
type: "function",
|
|
916
|
+
functionName: "ID",
|
|
917
|
+
args: [collectArg],
|
|
918
|
+
},
|
|
919
|
+
alias: "_nodeId",
|
|
920
|
+
}],
|
|
921
|
+
},
|
|
922
|
+
],
|
|
923
|
+
};
|
|
924
|
+
// Translate and execute the match query
|
|
925
|
+
const translator = new Translator(params);
|
|
926
|
+
const matchTranslation = translator.translate(matchQuery);
|
|
927
|
+
let collectedIds = [];
|
|
928
|
+
for (const stmt of matchTranslation.statements) {
|
|
929
|
+
const result = this.db.execute(stmt.sql, stmt.params);
|
|
930
|
+
for (const row of result.rows) {
|
|
931
|
+
const nodeId = row["_nodeId"];
|
|
932
|
+
if (typeof nodeId === "string") {
|
|
933
|
+
collectedIds.push(nodeId);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
// Phase 2: Evaluate each delete expression and delete the nodes
|
|
938
|
+
const context = {
|
|
939
|
+
[collectAlias]: collectedIds,
|
|
940
|
+
};
|
|
941
|
+
this.db.transaction(() => {
|
|
942
|
+
for (const expr of deleteClause.expressions) {
|
|
943
|
+
// Evaluate the expression to get the node ID
|
|
944
|
+
const nodeId = this.evaluateDeleteExpression(expr, params, context);
|
|
945
|
+
if (nodeId) {
|
|
946
|
+
if (deleteClause.detach) {
|
|
947
|
+
// DETACH DELETE: First delete all edges connected to this node
|
|
948
|
+
this.db.execute("DELETE FROM edges WHERE source_id = ? OR target_id = ?", [nodeId, nodeId]);
|
|
949
|
+
}
|
|
950
|
+
// Try deleting from nodes first
|
|
951
|
+
const nodeResult = this.db.execute("DELETE FROM nodes WHERE id = ?", [nodeId]);
|
|
952
|
+
if (nodeResult.changes === 0) {
|
|
953
|
+
// Try deleting from edges
|
|
954
|
+
this.db.execute("DELETE FROM edges WHERE id = ?", [nodeId]);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
// Return empty result (DELETE doesn't return rows)
|
|
960
|
+
return [];
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Evaluate a DELETE expression (like friends[$index]) with collected context
|
|
964
|
+
*/
|
|
965
|
+
evaluateDeleteExpression(expr, params, context) {
|
|
966
|
+
if (expr.type === "variable") {
|
|
967
|
+
const value = context[expr.variable];
|
|
968
|
+
if (typeof value === "string") {
|
|
969
|
+
return value;
|
|
970
|
+
}
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
if (expr.type === "function" && expr.functionName === "INDEX") {
|
|
974
|
+
// List access: list[index]
|
|
975
|
+
const listExpr = expr.args[0];
|
|
976
|
+
const indexExpr = expr.args[1];
|
|
977
|
+
// Get the list
|
|
978
|
+
let list;
|
|
979
|
+
if (listExpr.type === "variable") {
|
|
980
|
+
list = context[listExpr.variable];
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
// Unsupported list expression
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
// Get the index
|
|
987
|
+
let index;
|
|
988
|
+
if (indexExpr.type === "literal") {
|
|
989
|
+
index = indexExpr.value;
|
|
990
|
+
}
|
|
991
|
+
else if (indexExpr.type === "parameter") {
|
|
992
|
+
index = params[indexExpr.name];
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
// Unsupported index expression
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
// Handle negative indices
|
|
999
|
+
if (index < 0) {
|
|
1000
|
+
index = list.length + index;
|
|
1001
|
+
}
|
|
1002
|
+
if (index >= 0 && index < list.length) {
|
|
1003
|
+
const value = list[index];
|
|
1004
|
+
return typeof value === "string" ? value : null;
|
|
1005
|
+
}
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Evaluate UNWIND expressions to get the arrays to iterate over
|
|
1012
|
+
*/
|
|
1013
|
+
evaluateUnwindExpressions(unwindClauses, params) {
|
|
1014
|
+
return unwindClauses.map((clause) => {
|
|
1015
|
+
return this.evaluateListExpression(clause.expression, params);
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Evaluate an expression that should return a list
|
|
1020
|
+
*/
|
|
1021
|
+
evaluateListExpression(expr, params) {
|
|
1022
|
+
if (expr.type === "literal") {
|
|
1023
|
+
return expr.value;
|
|
1024
|
+
}
|
|
1025
|
+
else if (expr.type === "parameter") {
|
|
1026
|
+
return params[expr.name];
|
|
1027
|
+
}
|
|
1028
|
+
else if (expr.type === "function") {
|
|
1029
|
+
const funcName = expr.functionName?.toUpperCase();
|
|
1030
|
+
// range(start, end[, step])
|
|
1031
|
+
if (funcName === "RANGE") {
|
|
1032
|
+
const args = expr.args || [];
|
|
1033
|
+
if (args.length < 2) {
|
|
1034
|
+
throw new Error("range() requires at least 2 arguments");
|
|
1035
|
+
}
|
|
1036
|
+
const startVal = this.evaluateSimpleExpression(args[0], params);
|
|
1037
|
+
const endVal = this.evaluateSimpleExpression(args[1], params);
|
|
1038
|
+
const stepVal = args.length > 2 ? this.evaluateSimpleExpression(args[2], params) : 1;
|
|
1039
|
+
if (typeof startVal !== "number" || typeof endVal !== "number" || typeof stepVal !== "number") {
|
|
1040
|
+
throw new Error("range() arguments must be numbers");
|
|
1041
|
+
}
|
|
1042
|
+
const result = [];
|
|
1043
|
+
if (stepVal > 0) {
|
|
1044
|
+
for (let i = startVal; i <= endVal; i += stepVal) {
|
|
1045
|
+
result.push(i);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
else if (stepVal < 0) {
|
|
1049
|
+
for (let i = startVal; i >= endVal; i += stepVal) {
|
|
1050
|
+
result.push(i);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return result;
|
|
1054
|
+
}
|
|
1055
|
+
throw new Error(`Unsupported function in UNWIND: ${funcName}`);
|
|
1056
|
+
}
|
|
1057
|
+
throw new Error(`Unsupported UNWIND expression type: ${expr.type}`);
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Evaluate a simple expression (literals, parameters, basic arithmetic)
|
|
1061
|
+
*/
|
|
1062
|
+
evaluateSimpleExpression(expr, params) {
|
|
1063
|
+
if (expr.type === "literal") {
|
|
1064
|
+
return expr.value;
|
|
1065
|
+
}
|
|
1066
|
+
else if (expr.type === "parameter") {
|
|
1067
|
+
return params[expr.name];
|
|
1068
|
+
}
|
|
1069
|
+
else if (expr.type === "binary") {
|
|
1070
|
+
const left = this.evaluateSimpleExpression(expr.left, params);
|
|
1071
|
+
const right = this.evaluateSimpleExpression(expr.right, params);
|
|
1072
|
+
switch (expr.operator) {
|
|
1073
|
+
case "+": return left + right;
|
|
1074
|
+
case "-": return left - right;
|
|
1075
|
+
case "*": return left * right;
|
|
1076
|
+
case "/": return left / right;
|
|
1077
|
+
case "%": return left % right;
|
|
1078
|
+
case "^": return Math.pow(left, right);
|
|
1079
|
+
default: throw new Error(`Unsupported operator: ${expr.operator}`);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
throw new Error(`Cannot evaluate expression type: ${expr.type}`);
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Generate cartesian product of arrays
|
|
1086
|
+
*/
|
|
1087
|
+
generateCartesianProduct(arrays) {
|
|
1088
|
+
if (arrays.length === 0)
|
|
1089
|
+
return [[]];
|
|
1090
|
+
return arrays.reduce((acc, curr) => {
|
|
1091
|
+
const result = [];
|
|
1092
|
+
for (const a of acc) {
|
|
1093
|
+
for (const c of curr) {
|
|
1094
|
+
result.push([...a, c]);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return result;
|
|
1098
|
+
}, [[]]);
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Resolve properties, including unwind variable references and binary expressions
|
|
1102
|
+
*/
|
|
1103
|
+
resolvePropertiesWithUnwind(props, params, unwindContext) {
|
|
1104
|
+
const resolved = {};
|
|
1105
|
+
for (const [key, value] of Object.entries(props)) {
|
|
1106
|
+
resolved[key] = this.resolvePropertyValueWithUnwind(value, params, unwindContext);
|
|
1107
|
+
}
|
|
1108
|
+
return resolved;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Resolve a single property value, handling binary expressions recursively
|
|
1112
|
+
*/
|
|
1113
|
+
resolvePropertyValueWithUnwind(value, params, unwindContext) {
|
|
1114
|
+
if (typeof value === "object" &&
|
|
1115
|
+
value !== null &&
|
|
1116
|
+
"type" in value) {
|
|
1117
|
+
const typedValue = value;
|
|
1118
|
+
if (typedValue.type === "parameter" && typedValue.name) {
|
|
1119
|
+
return params[typedValue.name];
|
|
1120
|
+
}
|
|
1121
|
+
else if (typedValue.type === "variable" && typedValue.name) {
|
|
1122
|
+
// This is an unwind variable reference
|
|
1123
|
+
if (!(typedValue.name in unwindContext)) {
|
|
1124
|
+
throw new Error(`Variable \`${typedValue.name}\` not defined`);
|
|
1125
|
+
}
|
|
1126
|
+
return unwindContext[typedValue.name];
|
|
1127
|
+
}
|
|
1128
|
+
else if (typedValue.type === "property" && typedValue.variable && typedValue.property) {
|
|
1129
|
+
// Property access: x.prop - look up variable value and get property
|
|
1130
|
+
const varValue = unwindContext[typedValue.variable];
|
|
1131
|
+
if (varValue && typeof varValue === "object" && typedValue.property in varValue) {
|
|
1132
|
+
return varValue[typedValue.property];
|
|
1133
|
+
}
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
1136
|
+
else if (typedValue.type === "binary" && typedValue.operator && typedValue.left !== undefined && typedValue.right !== undefined) {
|
|
1137
|
+
// Binary expression: evaluate left and right, then apply operator
|
|
1138
|
+
const leftVal = this.resolvePropertyValueWithUnwind(typedValue.left, params, unwindContext);
|
|
1139
|
+
const rightVal = this.resolvePropertyValueWithUnwind(typedValue.right, params, unwindContext);
|
|
1140
|
+
// Both must be numbers for arithmetic operations
|
|
1141
|
+
const leftNum = typeof leftVal === "number" ? leftVal : Number(leftVal);
|
|
1142
|
+
const rightNum = typeof rightVal === "number" ? rightVal : Number(rightVal);
|
|
1143
|
+
switch (typedValue.operator) {
|
|
1144
|
+
case "+": return leftNum + rightNum;
|
|
1145
|
+
case "-": return leftNum - rightNum;
|
|
1146
|
+
case "*": return leftNum * rightNum;
|
|
1147
|
+
case "/": return leftNum / rightNum;
|
|
1148
|
+
case "%": return leftNum % rightNum;
|
|
1149
|
+
case "^": return Math.pow(leftNum, rightNum);
|
|
1150
|
+
default: return null;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
else if (typedValue.type === "function") {
|
|
1154
|
+
// Function call in property value (e.g., datetime())
|
|
1155
|
+
const funcValue = typedValue;
|
|
1156
|
+
return this.evaluateFunctionInProperty(funcValue.name, funcValue.args || [], params, unwindContext);
|
|
1157
|
+
}
|
|
1158
|
+
return value;
|
|
1159
|
+
}
|
|
1160
|
+
return value;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Evaluate a function call within a property value context
|
|
1164
|
+
*/
|
|
1165
|
+
evaluateFunctionInProperty(funcName, args, params, unwindContext) {
|
|
1166
|
+
const upperName = funcName.toUpperCase();
|
|
1167
|
+
switch (upperName) {
|
|
1168
|
+
case "DATETIME": {
|
|
1169
|
+
// datetime() returns current ISO datetime string
|
|
1170
|
+
// datetime(string) parses the string
|
|
1171
|
+
if (args.length > 0) {
|
|
1172
|
+
const arg = this.resolvePropertyValueWithUnwind(args[0], params, unwindContext);
|
|
1173
|
+
return String(arg);
|
|
1174
|
+
}
|
|
1175
|
+
return new Date().toISOString();
|
|
1176
|
+
}
|
|
1177
|
+
case "DATE": {
|
|
1178
|
+
// date() returns current date string (YYYY-MM-DD)
|
|
1179
|
+
// date(string) parses the string
|
|
1180
|
+
if (args.length > 0) {
|
|
1181
|
+
const arg = this.resolvePropertyValueWithUnwind(args[0], params, unwindContext);
|
|
1182
|
+
return String(arg).split("T")[0];
|
|
1183
|
+
}
|
|
1184
|
+
return new Date().toISOString().split("T")[0];
|
|
1185
|
+
}
|
|
1186
|
+
case "TIME": {
|
|
1187
|
+
// time() returns current time string (HH:MM:SS)
|
|
1188
|
+
if (args.length > 0) {
|
|
1189
|
+
const arg = this.resolvePropertyValueWithUnwind(args[0], params, unwindContext);
|
|
1190
|
+
const str = String(arg);
|
|
1191
|
+
const match = str.match(/(\d{2}:\d{2}:\d{2})/);
|
|
1192
|
+
return match ? match[1] : str;
|
|
1193
|
+
}
|
|
1194
|
+
return new Date().toISOString().split("T")[1].split(".")[0];
|
|
1195
|
+
}
|
|
1196
|
+
case "TIMESTAMP": {
|
|
1197
|
+
// timestamp() returns milliseconds since epoch
|
|
1198
|
+
return Date.now();
|
|
1199
|
+
}
|
|
1200
|
+
case "RANDOMUUID": {
|
|
1201
|
+
// randomUUID() returns a UUID v4
|
|
1202
|
+
return crypto.randomUUID();
|
|
1203
|
+
}
|
|
1204
|
+
default:
|
|
1205
|
+
throw new Error(`Unknown function in property value: ${funcName}`);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Execute CREATE relationship pattern with unwind context
|
|
1210
|
+
*/
|
|
1211
|
+
executeCreateRelationshipPatternWithUnwind(rel, createdIds, params, unwindContext) {
|
|
1212
|
+
let sourceId;
|
|
1213
|
+
let targetId;
|
|
1214
|
+
// Determine source node ID
|
|
1215
|
+
if (rel.source.variable && createdIds.has(rel.source.variable)) {
|
|
1216
|
+
sourceId = createdIds.get(rel.source.variable);
|
|
1217
|
+
}
|
|
1218
|
+
else if (rel.source.variable && !createdIds.has(rel.source.variable) && !rel.source.label) {
|
|
1219
|
+
// Variable referenced but not found and no label - error
|
|
1220
|
+
throw new Error(`Cannot resolve source node: ${rel.source.variable}`);
|
|
1221
|
+
}
|
|
1222
|
+
else {
|
|
1223
|
+
// Create new source node (with or without label - anonymous nodes are valid)
|
|
1224
|
+
sourceId = crypto.randomUUID();
|
|
1225
|
+
const props = this.resolvePropertiesWithUnwind(rel.source.properties || {}, params, unwindContext);
|
|
1226
|
+
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [sourceId, this.normalizeLabelToJson(rel.source.label), JSON.stringify(props)]);
|
|
1227
|
+
if (rel.source.variable) {
|
|
1228
|
+
createdIds.set(rel.source.variable, sourceId);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
// Determine target node ID
|
|
1232
|
+
if (rel.target.variable && createdIds.has(rel.target.variable)) {
|
|
1233
|
+
targetId = createdIds.get(rel.target.variable);
|
|
1234
|
+
}
|
|
1235
|
+
else if (rel.target.variable && !createdIds.has(rel.target.variable) && !rel.target.label) {
|
|
1236
|
+
// Variable referenced but not found and no label - error
|
|
1237
|
+
throw new Error(`Cannot resolve target node: ${rel.target.variable}`);
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
// Create new target node (with or without label - anonymous nodes are valid)
|
|
1241
|
+
targetId = crypto.randomUUID();
|
|
1242
|
+
const props = this.resolvePropertiesWithUnwind(rel.target.properties || {}, params, unwindContext);
|
|
1243
|
+
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [targetId, this.normalizeLabelToJson(rel.target.label), JSON.stringify(props)]);
|
|
1244
|
+
if (rel.target.variable) {
|
|
1245
|
+
createdIds.set(rel.target.variable, targetId);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
// Swap source/target for left-directed relationships
|
|
1249
|
+
const [actualSource, actualTarget] = rel.edge.direction === "left" ? [targetId, sourceId] : [sourceId, targetId];
|
|
1250
|
+
// Create edge
|
|
1251
|
+
const edgeId = crypto.randomUUID();
|
|
1252
|
+
const edgeType = rel.edge.type || "";
|
|
1253
|
+
const edgeProps = this.resolvePropertiesWithUnwind(rel.edge.properties || {}, params, unwindContext);
|
|
1254
|
+
this.db.execute("INSERT INTO edges (id, type, source_id, target_id, properties) VALUES (?, ?, ?, ?, ?)", [edgeId, edgeType, actualSource, actualTarget, JSON.stringify(edgeProps)]);
|
|
1255
|
+
if (rel.edge.variable) {
|
|
1256
|
+
createdIds.set(rel.edge.variable, edgeId);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Handle CREATE...RETURN pattern by creating nodes/edges and then querying them back
|
|
1261
|
+
*/
|
|
1262
|
+
tryCreateReturnExecution(query, params) {
|
|
1263
|
+
// Check if this is a CREATE...RETURN pattern (no MATCH)
|
|
1264
|
+
const clauses = query.clauses;
|
|
1265
|
+
// Must have at least CREATE and RETURN
|
|
1266
|
+
if (clauses.length < 2)
|
|
1267
|
+
return null;
|
|
1268
|
+
// Find CREATE and RETURN clauses
|
|
1269
|
+
const createClauses = [];
|
|
1270
|
+
const setClauses = [];
|
|
1271
|
+
let returnClause = null;
|
|
1272
|
+
for (const clause of clauses) {
|
|
1273
|
+
if (clause.type === "CREATE") {
|
|
1274
|
+
createClauses.push(clause);
|
|
1275
|
+
}
|
|
1276
|
+
else if (clause.type === "SET") {
|
|
1277
|
+
setClauses.push(clause);
|
|
1278
|
+
}
|
|
1279
|
+
else if (clause.type === "RETURN") {
|
|
1280
|
+
returnClause = clause;
|
|
1281
|
+
}
|
|
1282
|
+
else if (clause.type === "MATCH") {
|
|
1283
|
+
// If there's a MATCH, this is not a pure CREATE...RETURN pattern
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1286
|
+
else if (clause.type === "MERGE") {
|
|
1287
|
+
// If there's a MERGE, let tryMergeExecution handle it
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (createClauses.length === 0 || !returnClause)
|
|
1292
|
+
return null;
|
|
1293
|
+
// Execute CREATE and track created node IDs
|
|
1294
|
+
const createdIds = new Map();
|
|
1295
|
+
for (const createClause of createClauses) {
|
|
1296
|
+
for (const pattern of createClause.patterns) {
|
|
1297
|
+
if (this.isRelationshipPattern(pattern)) {
|
|
1298
|
+
// Handle relationship pattern
|
|
1299
|
+
this.executeCreateRelationshipPattern(pattern, createdIds, params);
|
|
1300
|
+
}
|
|
1301
|
+
else {
|
|
1302
|
+
// Handle node pattern
|
|
1303
|
+
const id = crypto.randomUUID();
|
|
1304
|
+
const labelJson = this.normalizeLabelToJson(pattern.label);
|
|
1305
|
+
const props = this.resolveProperties(pattern.properties || {}, params);
|
|
1306
|
+
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [id, labelJson, JSON.stringify(props)]);
|
|
1307
|
+
if (pattern.variable) {
|
|
1308
|
+
createdIds.set(pattern.variable, id);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
// Execute SET clauses if present
|
|
1314
|
+
if (setClauses.length > 0) {
|
|
1315
|
+
// Convert createdIds Map to Record for executeSetWithResolvedIds
|
|
1316
|
+
const resolvedIds = {};
|
|
1317
|
+
for (const [variable, id] of createdIds) {
|
|
1318
|
+
resolvedIds[variable] = id;
|
|
1319
|
+
}
|
|
1320
|
+
for (const setClause of setClauses) {
|
|
1321
|
+
this.executeSetWithResolvedIds(setClause, resolvedIds, params);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
// Now query the created nodes/edges based on RETURN items
|
|
1325
|
+
const results = [];
|
|
1326
|
+
const resultRow = {};
|
|
1327
|
+
for (const item of returnClause.items) {
|
|
1328
|
+
const alias = item.alias || this.getExpressionName(item.expression);
|
|
1329
|
+
if (item.expression.type === "variable") {
|
|
1330
|
+
const variable = item.expression.variable;
|
|
1331
|
+
const id = createdIds.get(variable);
|
|
1332
|
+
if (id) {
|
|
1333
|
+
// Query the node
|
|
1334
|
+
const nodeResult = this.db.execute("SELECT id, label, properties FROM nodes WHERE id = ?", [id]);
|
|
1335
|
+
if (nodeResult.rows.length > 0) {
|
|
1336
|
+
const row = nodeResult.rows[0];
|
|
1337
|
+
// Neo4j 3.5 format: return properties directly
|
|
1338
|
+
resultRow[alias] = typeof row.properties === "string"
|
|
1339
|
+
? JSON.parse(row.properties)
|
|
1340
|
+
: row.properties;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
else if (item.expression.type === "property") {
|
|
1345
|
+
const variable = item.expression.variable;
|
|
1346
|
+
const property = item.expression.property;
|
|
1347
|
+
const id = createdIds.get(variable);
|
|
1348
|
+
if (id) {
|
|
1349
|
+
// Try nodes first
|
|
1350
|
+
const nodeResult = this.db.execute(`SELECT json_extract(properties, '$.${property}') as value FROM nodes WHERE id = ?`, [id]);
|
|
1351
|
+
if (nodeResult.rows.length > 0) {
|
|
1352
|
+
resultRow[alias] = this.deepParseJson(nodeResult.rows[0].value);
|
|
1353
|
+
}
|
|
1354
|
+
else {
|
|
1355
|
+
// Try edges if not found in nodes
|
|
1356
|
+
const edgeResult = this.db.execute(`SELECT json_extract(properties, '$.${property}') as value FROM edges WHERE id = ?`, [id]);
|
|
1357
|
+
if (edgeResult.rows.length > 0) {
|
|
1358
|
+
resultRow[alias] = this.deepParseJson(edgeResult.rows[0].value);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
else if (item.expression.type === "function" && item.expression.functionName === "ID") {
|
|
1364
|
+
// Handle id(n) function
|
|
1365
|
+
const args = item.expression.args;
|
|
1366
|
+
if (args && args.length > 0 && args[0].type === "variable") {
|
|
1367
|
+
const variable = args[0].variable;
|
|
1368
|
+
const id = createdIds.get(variable);
|
|
1369
|
+
if (id) {
|
|
1370
|
+
resultRow[alias] = id;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
else if (item.expression.type === "function" && item.expression.functionName?.toUpperCase() === "LABELS") {
|
|
1375
|
+
// Handle labels(n) function
|
|
1376
|
+
const args = item.expression.args;
|
|
1377
|
+
if (args && args.length > 0 && args[0].type === "variable") {
|
|
1378
|
+
const variable = args[0].variable;
|
|
1379
|
+
const id = createdIds.get(variable);
|
|
1380
|
+
if (id) {
|
|
1381
|
+
const nodeResult = this.db.execute("SELECT label FROM nodes WHERE id = ?", [id]);
|
|
1382
|
+
if (nodeResult.rows.length > 0) {
|
|
1383
|
+
const label = nodeResult.rows[0].label;
|
|
1384
|
+
const parsed = typeof label === "string" ? JSON.parse(label) : label;
|
|
1385
|
+
resultRow[alias] = Array.isArray(parsed) ? parsed : [parsed];
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
if (Object.keys(resultRow).length > 0) {
|
|
1392
|
+
results.push(resultRow);
|
|
1393
|
+
}
|
|
1394
|
+
// Apply SKIP and LIMIT from returnClause (mutations already happened, now filter results)
|
|
1395
|
+
let finalResults = results;
|
|
1396
|
+
if (returnClause.skip !== undefined && returnClause.skip !== null) {
|
|
1397
|
+
const skipValue = typeof returnClause.skip === "number"
|
|
1398
|
+
? returnClause.skip
|
|
1399
|
+
: params[returnClause.skip] || 0;
|
|
1400
|
+
finalResults = finalResults.slice(skipValue);
|
|
1401
|
+
}
|
|
1402
|
+
if (returnClause.limit !== undefined && returnClause.limit !== null) {
|
|
1403
|
+
const limitValue = typeof returnClause.limit === "number"
|
|
1404
|
+
? returnClause.limit
|
|
1405
|
+
: params[returnClause.limit] || 0;
|
|
1406
|
+
finalResults = finalResults.slice(0, limitValue);
|
|
1407
|
+
}
|
|
1408
|
+
return finalResults;
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Handle MERGE clauses that need special execution (relationship patterns or ON CREATE/MATCH SET)
|
|
1412
|
+
* Returns null if this is not a MERGE pattern that needs special handling
|
|
1413
|
+
*/
|
|
1414
|
+
tryMergeExecution(query, params) {
|
|
1415
|
+
const clauses = query.clauses;
|
|
1416
|
+
let matchClauses = [];
|
|
1417
|
+
let createClauses = [];
|
|
1418
|
+
let withClauses = [];
|
|
1419
|
+
let mergeClause = null;
|
|
1420
|
+
let returnClause = null;
|
|
1421
|
+
for (const clause of clauses) {
|
|
1422
|
+
if (clause.type === "MERGE") {
|
|
1423
|
+
mergeClause = clause;
|
|
1424
|
+
}
|
|
1425
|
+
else if (clause.type === "RETURN") {
|
|
1426
|
+
returnClause = clause;
|
|
1427
|
+
}
|
|
1428
|
+
else if (clause.type === "MATCH") {
|
|
1429
|
+
matchClauses.push(clause);
|
|
1430
|
+
}
|
|
1431
|
+
else if (clause.type === "CREATE") {
|
|
1432
|
+
createClauses.push(clause);
|
|
1433
|
+
}
|
|
1434
|
+
else if (clause.type === "WITH") {
|
|
1435
|
+
withClauses.push(clause);
|
|
1436
|
+
}
|
|
1437
|
+
else {
|
|
1438
|
+
// Other clause types present - don't handle
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (!mergeClause) {
|
|
1443
|
+
return null;
|
|
1444
|
+
}
|
|
1445
|
+
// Check if this MERGE needs special handling:
|
|
1446
|
+
// 1. Has relationship patterns
|
|
1447
|
+
// 2. Has ON CREATE SET or ON MATCH SET
|
|
1448
|
+
// 3. Has RETURN clause (translator can't handle MERGE + RETURN properly for new nodes)
|
|
1449
|
+
const hasRelationshipPattern = mergeClause.patterns.some(p => this.isRelationshipPattern(p));
|
|
1450
|
+
const hasSetClauses = mergeClause.onCreateSet || mergeClause.onMatchSet;
|
|
1451
|
+
if (!hasRelationshipPattern && !hasSetClauses && !returnClause) {
|
|
1452
|
+
// Simple node MERGE without SET clauses and no RETURN - let translator handle it
|
|
1453
|
+
return null;
|
|
1454
|
+
}
|
|
1455
|
+
// Execute MERGE with special handling
|
|
1456
|
+
return this.executeMergeWithSetClauses(matchClauses, createClauses, withClauses, mergeClause, returnClause, params);
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Execute a MERGE clause with ON CREATE SET and/or ON MATCH SET
|
|
1460
|
+
*/
|
|
1461
|
+
executeMergeWithSetClauses(matchClauses, createClauses, withClauses, mergeClause, returnClause, params) {
|
|
1462
|
+
// Track matched/created nodes
|
|
1463
|
+
const matchedNodes = new Map();
|
|
1464
|
+
// Execute CREATE clauses first to create nodes
|
|
1465
|
+
for (const createClause of createClauses) {
|
|
1466
|
+
for (const pattern of createClause.patterns) {
|
|
1467
|
+
if (this.isRelationshipPattern(pattern)) {
|
|
1468
|
+
// Skip relationship patterns for now - just focus on node creation
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
const nodePattern = pattern;
|
|
1472
|
+
const props = this.resolveProperties(nodePattern.properties || {}, params);
|
|
1473
|
+
const id = crypto.randomUUID();
|
|
1474
|
+
const labelJson = nodePattern.label ? JSON.stringify([nodePattern.label]) : "[]";
|
|
1475
|
+
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [id, labelJson, JSON.stringify(props)]);
|
|
1476
|
+
if (nodePattern.variable) {
|
|
1477
|
+
const labelStr = Array.isArray(nodePattern.label)
|
|
1478
|
+
? nodePattern.label.join(":")
|
|
1479
|
+
: (nodePattern.label || "");
|
|
1480
|
+
matchedNodes.set(nodePattern.variable, {
|
|
1481
|
+
id,
|
|
1482
|
+
label: labelStr,
|
|
1483
|
+
properties: props,
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
// Execute MATCH clauses to get referenced nodes
|
|
1489
|
+
for (const matchClause of matchClauses) {
|
|
1490
|
+
for (const pattern of matchClause.patterns) {
|
|
1491
|
+
if (this.isRelationshipPattern(pattern)) {
|
|
1492
|
+
// For now, only handle simple node patterns in MATCH before MERGE
|
|
1493
|
+
throw new Error("Relationship patterns in MATCH before MERGE not yet supported");
|
|
1494
|
+
}
|
|
1495
|
+
const nodePattern = pattern;
|
|
1496
|
+
const matchProps = this.resolveProperties(nodePattern.properties || {}, params);
|
|
1497
|
+
// Build WHERE conditions
|
|
1498
|
+
const conditions = [];
|
|
1499
|
+
const conditionParams = [];
|
|
1500
|
+
if (nodePattern.label) {
|
|
1501
|
+
const labelCondition = this.generateLabelCondition(nodePattern.label);
|
|
1502
|
+
conditions.push(labelCondition.sql);
|
|
1503
|
+
conditionParams.push(...labelCondition.params);
|
|
1504
|
+
}
|
|
1505
|
+
for (const [key, value] of Object.entries(matchProps)) {
|
|
1506
|
+
conditions.push(`json_extract(properties, '$.${key}') = ?`);
|
|
1507
|
+
conditionParams.push(value);
|
|
1508
|
+
}
|
|
1509
|
+
const findSql = `SELECT id, label, properties FROM nodes WHERE ${conditions.join(" AND ")}`;
|
|
1510
|
+
const findResult = this.db.execute(findSql, conditionParams);
|
|
1511
|
+
if (findResult.rows.length > 0 && nodePattern.variable) {
|
|
1512
|
+
const row = findResult.rows[0];
|
|
1513
|
+
const labelValue = typeof row.label === "string" ? JSON.parse(row.label) : row.label;
|
|
1514
|
+
matchedNodes.set(nodePattern.variable, {
|
|
1515
|
+
id: row.id,
|
|
1516
|
+
label: labelValue,
|
|
1517
|
+
properties: typeof row.properties === "string" ? JSON.parse(row.properties) : row.properties,
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
// Process WITH clauses to handle aliasing
|
|
1523
|
+
// e.g., WITH n AS a, m AS b - creates aliases that map a->n, b->m in matched nodes
|
|
1524
|
+
for (const withClause of withClauses) {
|
|
1525
|
+
for (const item of withClause.items) {
|
|
1526
|
+
const alias = item.alias;
|
|
1527
|
+
const expr = item.expression;
|
|
1528
|
+
// Handle WITH n AS a - aliasing matched variable
|
|
1529
|
+
if (alias && expr.type === "variable" && expr.variable) {
|
|
1530
|
+
const sourceVar = expr.variable;
|
|
1531
|
+
const sourceNode = matchedNodes.get(sourceVar);
|
|
1532
|
+
if (sourceNode) {
|
|
1533
|
+
// Create alias pointing to the same node
|
|
1534
|
+
matchedNodes.set(alias, sourceNode);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
// Now handle the MERGE pattern
|
|
1540
|
+
const patterns = mergeClause.patterns;
|
|
1541
|
+
// Check if this is a relationship pattern or a simple node pattern
|
|
1542
|
+
if (patterns.length === 1 && !this.isRelationshipPattern(patterns[0])) {
|
|
1543
|
+
// Simple node MERGE
|
|
1544
|
+
return this.executeMergeNode(patterns[0], mergeClause, returnClause, params, matchedNodes);
|
|
1545
|
+
}
|
|
1546
|
+
else if (patterns.length === 1 && this.isRelationshipPattern(patterns[0])) {
|
|
1547
|
+
// Relationship MERGE
|
|
1548
|
+
return this.executeMergeRelationship(patterns[0], mergeClause, returnClause, params, matchedNodes);
|
|
1549
|
+
}
|
|
1550
|
+
else {
|
|
1551
|
+
throw new Error("Complex MERGE patterns not yet supported");
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Execute a simple node MERGE
|
|
1556
|
+
*/
|
|
1557
|
+
executeMergeNode(pattern, mergeClause, returnClause, params, matchedNodes) {
|
|
1558
|
+
const matchProps = this.resolveProperties(pattern.properties || {}, params);
|
|
1559
|
+
// Build WHERE conditions to find existing node
|
|
1560
|
+
const conditions = [];
|
|
1561
|
+
const conditionParams = [];
|
|
1562
|
+
if (pattern.label) {
|
|
1563
|
+
const labelCondition = this.generateLabelCondition(pattern.label);
|
|
1564
|
+
conditions.push(labelCondition.sql);
|
|
1565
|
+
conditionParams.push(...labelCondition.params);
|
|
1566
|
+
}
|
|
1567
|
+
for (const [key, value] of Object.entries(matchProps)) {
|
|
1568
|
+
conditions.push(`json_extract(properties, '$.${key}') = ?`);
|
|
1569
|
+
conditionParams.push(value);
|
|
1570
|
+
}
|
|
1571
|
+
// Try to find existing node
|
|
1572
|
+
let findResult;
|
|
1573
|
+
if (conditions.length > 0) {
|
|
1574
|
+
const findSql = `SELECT id, label, properties FROM nodes WHERE ${conditions.join(" AND ")}`;
|
|
1575
|
+
findResult = this.db.execute(findSql, conditionParams);
|
|
1576
|
+
}
|
|
1577
|
+
else {
|
|
1578
|
+
// MERGE with no label and no properties - match any node
|
|
1579
|
+
findResult = this.db.execute("SELECT id, label, properties FROM nodes LIMIT 1");
|
|
1580
|
+
}
|
|
1581
|
+
let nodeId;
|
|
1582
|
+
let wasCreated = false;
|
|
1583
|
+
if (findResult.rows.length === 0) {
|
|
1584
|
+
// Node doesn't exist - create it
|
|
1585
|
+
nodeId = crypto.randomUUID();
|
|
1586
|
+
wasCreated = true;
|
|
1587
|
+
// Start with match properties
|
|
1588
|
+
const nodeProps = { ...matchProps };
|
|
1589
|
+
// Collect additional labels from ON CREATE SET
|
|
1590
|
+
const additionalLabels = [];
|
|
1591
|
+
// Apply ON CREATE SET properties
|
|
1592
|
+
if (mergeClause.onCreateSet) {
|
|
1593
|
+
// Convert matchedNodes to resolvedIds format for expression evaluation
|
|
1594
|
+
const resolvedIds = {};
|
|
1595
|
+
for (const [varName, nodeInfo] of matchedNodes) {
|
|
1596
|
+
resolvedIds[varName] = nodeInfo.id;
|
|
1597
|
+
}
|
|
1598
|
+
for (const assignment of mergeClause.onCreateSet) {
|
|
1599
|
+
// Handle label assignments: ON CREATE SET a:Label
|
|
1600
|
+
if (assignment.labels && assignment.labels.length > 0) {
|
|
1601
|
+
additionalLabels.push(...assignment.labels);
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
if (!assignment.value || !assignment.property)
|
|
1605
|
+
continue;
|
|
1606
|
+
const value = assignment.value.type === "property" || assignment.value.type === "binary"
|
|
1607
|
+
? this.evaluateExpressionWithContext(assignment.value, params, resolvedIds)
|
|
1608
|
+
: this.evaluateExpression(assignment.value, params);
|
|
1609
|
+
nodeProps[assignment.property] = value;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
// Combine pattern label with additional labels
|
|
1613
|
+
const allLabels = pattern.label
|
|
1614
|
+
? (Array.isArray(pattern.label) ? [...pattern.label] : [pattern.label])
|
|
1615
|
+
: [];
|
|
1616
|
+
allLabels.push(...additionalLabels);
|
|
1617
|
+
const labelJson = JSON.stringify(allLabels);
|
|
1618
|
+
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [nodeId, labelJson, JSON.stringify(nodeProps)]);
|
|
1619
|
+
}
|
|
1620
|
+
else {
|
|
1621
|
+
// Node exists - apply ON MATCH SET
|
|
1622
|
+
nodeId = findResult.rows[0].id;
|
|
1623
|
+
if (mergeClause.onMatchSet) {
|
|
1624
|
+
// Convert matchedNodes to resolvedIds format for expression evaluation
|
|
1625
|
+
const resolvedIds = {};
|
|
1626
|
+
for (const [varName, nodeInfo] of matchedNodes) {
|
|
1627
|
+
resolvedIds[varName] = nodeInfo.id;
|
|
1628
|
+
}
|
|
1629
|
+
for (const assignment of mergeClause.onMatchSet) {
|
|
1630
|
+
// Handle label assignments: ON MATCH SET a:Label
|
|
1631
|
+
if (assignment.labels && assignment.labels.length > 0) {
|
|
1632
|
+
const newLabelsJson = JSON.stringify(assignment.labels);
|
|
1633
|
+
this.db.execute(`UPDATE nodes SET label = (SELECT json_group_array(value) FROM (
|
|
1634
|
+
SELECT DISTINCT value FROM (
|
|
1635
|
+
SELECT value FROM json_each(nodes.label)
|
|
1636
|
+
UNION ALL
|
|
1637
|
+
SELECT value FROM json_each(?)
|
|
1638
|
+
) ORDER BY value
|
|
1639
|
+
)) WHERE id = ?`, [newLabelsJson, nodeId]);
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
if (!assignment.value || !assignment.property)
|
|
1643
|
+
continue;
|
|
1644
|
+
const value = assignment.value.type === "property" || assignment.value.type === "binary"
|
|
1645
|
+
? this.evaluateExpressionWithContext(assignment.value, params, resolvedIds)
|
|
1646
|
+
: this.evaluateExpression(assignment.value, params);
|
|
1647
|
+
this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
// Store the node in matchedNodes for RETURN processing
|
|
1652
|
+
if (pattern.variable) {
|
|
1653
|
+
const nodeResult = this.db.execute("SELECT id, label, properties FROM nodes WHERE id = ?", [nodeId]);
|
|
1654
|
+
if (nodeResult.rows.length > 0) {
|
|
1655
|
+
const row = nodeResult.rows[0];
|
|
1656
|
+
matchedNodes.set(pattern.variable, {
|
|
1657
|
+
id: row.id,
|
|
1658
|
+
label: row.label,
|
|
1659
|
+
properties: typeof row.properties === "string" ? JSON.parse(row.properties) : row.properties,
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
// If there's a RETURN clause, process it
|
|
1664
|
+
if (returnClause) {
|
|
1665
|
+
return this.processReturnClause(returnClause, matchedNodes, params);
|
|
1666
|
+
}
|
|
1667
|
+
return [];
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Execute a relationship MERGE: MERGE (a)-[:TYPE]->(b)
|
|
1671
|
+
* Handles multiple scenarios:
|
|
1672
|
+
* 1. MATCH (a), (b) MERGE (a)-[:REL]->(b) - both nodes already matched
|
|
1673
|
+
* 2. MATCH (a) MERGE (a)-[:REL]->(b:Label {props}) - source matched, target to find/create
|
|
1674
|
+
* 3. MERGE (a:Label)-[:REL]->(b:Label) - entire pattern to find/create
|
|
1675
|
+
*/
|
|
1676
|
+
executeMergeRelationship(pattern, mergeClause, returnClause, params, matchedNodes) {
|
|
1677
|
+
const sourceVar = pattern.source.variable;
|
|
1678
|
+
const targetVar = pattern.target.variable;
|
|
1679
|
+
const edgeType = pattern.edge.type || "";
|
|
1680
|
+
const edgeProps = this.resolveProperties(pattern.edge.properties || {}, params);
|
|
1681
|
+
const sourceProps = this.resolveProperties(pattern.source.properties || {}, params);
|
|
1682
|
+
const targetProps = this.resolveProperties(pattern.target.properties || {}, params);
|
|
1683
|
+
// Track edges for RETURN
|
|
1684
|
+
const matchedEdges = new Map();
|
|
1685
|
+
// Resolve or create source node
|
|
1686
|
+
let sourceNodeId;
|
|
1687
|
+
if (sourceVar && matchedNodes.has(sourceVar)) {
|
|
1688
|
+
// Source already matched from MATCH clause
|
|
1689
|
+
sourceNodeId = matchedNodes.get(sourceVar).id;
|
|
1690
|
+
}
|
|
1691
|
+
else {
|
|
1692
|
+
// Need to find or create source node
|
|
1693
|
+
const sourceResult = this.findOrCreateNode(pattern.source, sourceProps, params);
|
|
1694
|
+
sourceNodeId = sourceResult.id;
|
|
1695
|
+
if (sourceVar) {
|
|
1696
|
+
matchedNodes.set(sourceVar, sourceResult);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
// Resolve or create target node
|
|
1700
|
+
let targetNodeId;
|
|
1701
|
+
if (targetVar && matchedNodes.has(targetVar)) {
|
|
1702
|
+
// Target already matched from MATCH clause
|
|
1703
|
+
targetNodeId = matchedNodes.get(targetVar).id;
|
|
1704
|
+
}
|
|
1705
|
+
else {
|
|
1706
|
+
// Need to find or create target node
|
|
1707
|
+
const targetResult = this.findOrCreateNode(pattern.target, targetProps, params);
|
|
1708
|
+
targetNodeId = targetResult.id;
|
|
1709
|
+
if (targetVar) {
|
|
1710
|
+
matchedNodes.set(targetVar, targetResult);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
// Check if the relationship already exists between these two nodes
|
|
1714
|
+
const findEdgeConditions = [
|
|
1715
|
+
"source_id = ?",
|
|
1716
|
+
"target_id = ?",
|
|
1717
|
+
];
|
|
1718
|
+
const findEdgeParams = [sourceNodeId, targetNodeId];
|
|
1719
|
+
if (edgeType) {
|
|
1720
|
+
findEdgeConditions.push("type = ?");
|
|
1721
|
+
findEdgeParams.push(edgeType);
|
|
1722
|
+
}
|
|
1723
|
+
// Add edge property conditions if any
|
|
1724
|
+
for (const [key, value] of Object.entries(edgeProps)) {
|
|
1725
|
+
findEdgeConditions.push(`json_extract(properties, '$.${key}') = ?`);
|
|
1726
|
+
findEdgeParams.push(value);
|
|
1727
|
+
}
|
|
1728
|
+
const findEdgeSql = `SELECT id, type, source_id, target_id, properties FROM edges WHERE ${findEdgeConditions.join(" AND ")}`;
|
|
1729
|
+
const findEdgeResult = this.db.execute(findEdgeSql, findEdgeParams);
|
|
1730
|
+
let edgeId;
|
|
1731
|
+
let wasCreated = false;
|
|
1732
|
+
if (findEdgeResult.rows.length === 0) {
|
|
1733
|
+
// Relationship doesn't exist - create it
|
|
1734
|
+
edgeId = crypto.randomUUID();
|
|
1735
|
+
wasCreated = true;
|
|
1736
|
+
// Start with the pattern properties
|
|
1737
|
+
const finalEdgeProps = { ...edgeProps };
|
|
1738
|
+
// Apply ON CREATE SET properties (these apply to the target node, not the edge in this pattern)
|
|
1739
|
+
if (mergeClause.onCreateSet) {
|
|
1740
|
+
for (const assignment of mergeClause.onCreateSet) {
|
|
1741
|
+
// Skip label assignments (handled separately)
|
|
1742
|
+
if (assignment.labels)
|
|
1743
|
+
continue;
|
|
1744
|
+
if (!assignment.value || !assignment.property)
|
|
1745
|
+
continue;
|
|
1746
|
+
const value = this.evaluateExpression(assignment.value, params);
|
|
1747
|
+
// Update target node with ON CREATE SET
|
|
1748
|
+
this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), targetNodeId]);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
this.db.execute("INSERT INTO edges (id, type, source_id, target_id, properties) VALUES (?, ?, ?, ?, ?)", [edgeId, edgeType, sourceNodeId, targetNodeId, JSON.stringify(finalEdgeProps)]);
|
|
1752
|
+
}
|
|
1753
|
+
else {
|
|
1754
|
+
// Relationship exists - apply ON MATCH SET
|
|
1755
|
+
edgeId = findEdgeResult.rows[0].id;
|
|
1756
|
+
if (mergeClause.onMatchSet) {
|
|
1757
|
+
for (const assignment of mergeClause.onMatchSet) {
|
|
1758
|
+
// Skip label assignments (handled separately)
|
|
1759
|
+
if (assignment.labels)
|
|
1760
|
+
continue;
|
|
1761
|
+
if (!assignment.value || !assignment.property)
|
|
1762
|
+
continue;
|
|
1763
|
+
const value = this.evaluateExpression(assignment.value, params);
|
|
1764
|
+
// Update target node with ON MATCH SET
|
|
1765
|
+
this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), targetNodeId]);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
// Store the edge in matchedEdges for RETURN processing
|
|
1770
|
+
if (pattern.edge.variable) {
|
|
1771
|
+
const edgeResult = this.db.execute("SELECT id, type, source_id, target_id, properties FROM edges WHERE id = ?", [edgeId]);
|
|
1772
|
+
if (edgeResult.rows.length > 0) {
|
|
1773
|
+
const row = edgeResult.rows[0];
|
|
1774
|
+
matchedEdges.set(pattern.edge.variable, {
|
|
1775
|
+
id: row.id,
|
|
1776
|
+
type: row.type,
|
|
1777
|
+
source_id: row.source_id,
|
|
1778
|
+
target_id: row.target_id,
|
|
1779
|
+
properties: typeof row.properties === "string" ? JSON.parse(row.properties) : row.properties,
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
// Refresh target node data in matchedNodes (may have been updated by ON CREATE/MATCH SET)
|
|
1784
|
+
if (targetVar) {
|
|
1785
|
+
const nodeResult = this.db.execute("SELECT id, label, properties FROM nodes WHERE id = ?", [targetNodeId]);
|
|
1786
|
+
if (nodeResult.rows.length > 0) {
|
|
1787
|
+
const row = nodeResult.rows[0];
|
|
1788
|
+
matchedNodes.set(targetVar, {
|
|
1789
|
+
id: row.id,
|
|
1790
|
+
label: row.label,
|
|
1791
|
+
properties: typeof row.properties === "string" ? JSON.parse(row.properties) : row.properties,
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
// If there's a RETURN clause, process it
|
|
1796
|
+
if (returnClause) {
|
|
1797
|
+
return this.processReturnClauseWithEdges(returnClause, matchedNodes, matchedEdges, params);
|
|
1798
|
+
}
|
|
1799
|
+
return [];
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Find an existing node matching the pattern, or create a new one
|
|
1803
|
+
*/
|
|
1804
|
+
findOrCreateNode(pattern, props, params) {
|
|
1805
|
+
// Build conditions to find existing node
|
|
1806
|
+
const conditions = [];
|
|
1807
|
+
const conditionParams = [];
|
|
1808
|
+
if (pattern.label) {
|
|
1809
|
+
const labelCondition = this.generateLabelCondition(pattern.label);
|
|
1810
|
+
conditions.push(labelCondition.sql);
|
|
1811
|
+
conditionParams.push(...labelCondition.params);
|
|
1812
|
+
}
|
|
1813
|
+
for (const [key, value] of Object.entries(props)) {
|
|
1814
|
+
conditions.push(`json_extract(properties, '$.${key}') = ?`);
|
|
1815
|
+
conditionParams.push(value);
|
|
1816
|
+
}
|
|
1817
|
+
// If we have conditions, try to find existing node
|
|
1818
|
+
if (conditions.length > 0) {
|
|
1819
|
+
const findSql = `SELECT id, label, properties FROM nodes WHERE ${conditions.join(" AND ")}`;
|
|
1820
|
+
const findResult = this.db.execute(findSql, conditionParams);
|
|
1821
|
+
if (findResult.rows.length > 0) {
|
|
1822
|
+
const row = findResult.rows[0];
|
|
1823
|
+
return {
|
|
1824
|
+
id: row.id,
|
|
1825
|
+
label: typeof row.label === "string" ? JSON.parse(row.label) : row.label,
|
|
1826
|
+
properties: typeof row.properties === "string" ? JSON.parse(row.properties) : row.properties,
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
// Node doesn't exist - create it
|
|
1831
|
+
const nodeId = crypto.randomUUID();
|
|
1832
|
+
const labelJson = this.normalizeLabelToJson(pattern.label);
|
|
1833
|
+
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [nodeId, labelJson, JSON.stringify(props)]);
|
|
1834
|
+
return {
|
|
1835
|
+
id: nodeId,
|
|
1836
|
+
label: labelJson,
|
|
1837
|
+
properties: props,
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Process a RETURN clause using matched nodes and edges
|
|
1842
|
+
*/
|
|
1843
|
+
processReturnClauseWithEdges(returnClause, matchedNodes, matchedEdges, params) {
|
|
1844
|
+
const results = [];
|
|
1845
|
+
const resultRow = {};
|
|
1846
|
+
for (const item of returnClause.items) {
|
|
1847
|
+
const alias = item.alias || this.getExpressionName(item.expression);
|
|
1848
|
+
if (item.expression.type === "variable") {
|
|
1849
|
+
const varName = item.expression.variable;
|
|
1850
|
+
// Check if it's a node
|
|
1851
|
+
const node = matchedNodes.get(varName);
|
|
1852
|
+
if (node) {
|
|
1853
|
+
// Neo4j 3.5 format: return properties directly
|
|
1854
|
+
resultRow[alias] = node.properties;
|
|
1855
|
+
continue;
|
|
1856
|
+
}
|
|
1857
|
+
// Check if it's an edge
|
|
1858
|
+
const edge = matchedEdges.get(varName);
|
|
1859
|
+
if (edge) {
|
|
1860
|
+
// Neo4j 3.5 format: return properties directly
|
|
1861
|
+
resultRow[alias] = edge.properties;
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
else if (item.expression.type === "property") {
|
|
1866
|
+
const varName = item.expression.variable;
|
|
1867
|
+
const propName = item.expression.property;
|
|
1868
|
+
const node = matchedNodes.get(varName);
|
|
1869
|
+
if (node) {
|
|
1870
|
+
resultRow[alias] = node.properties[propName];
|
|
1871
|
+
continue;
|
|
1872
|
+
}
|
|
1873
|
+
const edge = matchedEdges.get(varName);
|
|
1874
|
+
if (edge) {
|
|
1875
|
+
resultRow[alias] = edge.properties[propName];
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
else if (item.expression.type === "function") {
|
|
1880
|
+
const funcName = item.expression.functionName?.toUpperCase();
|
|
1881
|
+
if (funcName === "TYPE" && item.expression.args?.length === 1) {
|
|
1882
|
+
const arg = item.expression.args[0];
|
|
1883
|
+
if (arg.type === "variable") {
|
|
1884
|
+
const edge = matchedEdges.get(arg.variable);
|
|
1885
|
+
if (edge) {
|
|
1886
|
+
resultRow[alias] = edge.type;
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
else if (funcName === "COUNT") {
|
|
1892
|
+
// count(*) or count(r) - return 1 for MERGE results
|
|
1893
|
+
resultRow[alias] = 1;
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
else if (item.expression.type === "comparison") {
|
|
1898
|
+
// Handle comparison expressions like: l.created_at = $createdAt
|
|
1899
|
+
const left = this.evaluateReturnExpression(item.expression.left, matchedNodes, params);
|
|
1900
|
+
const right = this.evaluateReturnExpression(item.expression.right, matchedNodes, params);
|
|
1901
|
+
const op = item.expression.comparisonOperator;
|
|
1902
|
+
let result;
|
|
1903
|
+
switch (op) {
|
|
1904
|
+
case "=":
|
|
1905
|
+
result = left === right;
|
|
1906
|
+
break;
|
|
1907
|
+
case "<>":
|
|
1908
|
+
result = left !== right;
|
|
1909
|
+
break;
|
|
1910
|
+
case "<":
|
|
1911
|
+
result = left < right;
|
|
1912
|
+
break;
|
|
1913
|
+
case ">":
|
|
1914
|
+
result = left > right;
|
|
1915
|
+
break;
|
|
1916
|
+
case "<=":
|
|
1917
|
+
result = left <= right;
|
|
1918
|
+
break;
|
|
1919
|
+
case ">=":
|
|
1920
|
+
result = left >= right;
|
|
1921
|
+
break;
|
|
1922
|
+
default:
|
|
1923
|
+
result = false;
|
|
1924
|
+
}
|
|
1925
|
+
resultRow[alias] = result;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
results.push(resultRow);
|
|
1929
|
+
return results;
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Process a RETURN clause using matched nodes
|
|
1933
|
+
*/
|
|
1934
|
+
processReturnClause(returnClause, matchedNodes, params) {
|
|
1935
|
+
const results = [];
|
|
1936
|
+
const resultRow = {};
|
|
1937
|
+
for (const item of returnClause.items) {
|
|
1938
|
+
const alias = item.alias || this.getExpressionName(item.expression);
|
|
1939
|
+
if (item.expression.type === "variable") {
|
|
1940
|
+
const node = matchedNodes.get(item.expression.variable);
|
|
1941
|
+
if (node) {
|
|
1942
|
+
// Neo4j 3.5 format: return properties directly
|
|
1943
|
+
resultRow[alias] = node.properties;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
else if (item.expression.type === "property") {
|
|
1947
|
+
const node = matchedNodes.get(item.expression.variable);
|
|
1948
|
+
if (node) {
|
|
1949
|
+
resultRow[alias] = node.properties[item.expression.property];
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
else if (item.expression.type === "function") {
|
|
1953
|
+
// Handle function expressions like count(*), labels(n)
|
|
1954
|
+
const funcName = item.expression.functionName?.toUpperCase();
|
|
1955
|
+
if (funcName === "COUNT") {
|
|
1956
|
+
// count(*) on MERGE results - count the matched/created nodes
|
|
1957
|
+
resultRow[alias] = 1; // MERGE always results in exactly one node
|
|
1958
|
+
}
|
|
1959
|
+
else if (funcName === "LABELS") {
|
|
1960
|
+
// labels(n) function
|
|
1961
|
+
const args = item.expression.args;
|
|
1962
|
+
if (args && args.length > 0 && args[0].type === "variable") {
|
|
1963
|
+
const node = matchedNodes.get(args[0].variable);
|
|
1964
|
+
if (node) {
|
|
1965
|
+
const label = this.normalizeLabelForOutput(node.label);
|
|
1966
|
+
resultRow[alias] = Array.isArray(label) ? label : (label ? [label] : []);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
else if (item.expression.type === "comparison") {
|
|
1972
|
+
// Handle comparison expressions like: l.created_at = $createdAt
|
|
1973
|
+
const left = this.evaluateReturnExpression(item.expression.left, matchedNodes, params);
|
|
1974
|
+
const right = this.evaluateReturnExpression(item.expression.right, matchedNodes, params);
|
|
1975
|
+
const op = item.expression.comparisonOperator;
|
|
1976
|
+
let result;
|
|
1977
|
+
switch (op) {
|
|
1978
|
+
case "=":
|
|
1979
|
+
result = left === right;
|
|
1980
|
+
break;
|
|
1981
|
+
case "<>":
|
|
1982
|
+
result = left !== right;
|
|
1983
|
+
break;
|
|
1984
|
+
case "<":
|
|
1985
|
+
result = left < right;
|
|
1986
|
+
break;
|
|
1987
|
+
case ">":
|
|
1988
|
+
result = left > right;
|
|
1989
|
+
break;
|
|
1990
|
+
case "<=":
|
|
1991
|
+
result = left <= right;
|
|
1992
|
+
break;
|
|
1993
|
+
case ">=":
|
|
1994
|
+
result = left >= right;
|
|
1995
|
+
break;
|
|
1996
|
+
default:
|
|
1997
|
+
result = false;
|
|
1998
|
+
}
|
|
1999
|
+
resultRow[alias] = result;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
results.push(resultRow);
|
|
2003
|
+
return results;
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Evaluate an expression for RETURN clause
|
|
2007
|
+
*/
|
|
2008
|
+
evaluateReturnExpression(expr, matchedNodes, params) {
|
|
2009
|
+
if (expr.type === "property") {
|
|
2010
|
+
const node = matchedNodes.get(expr.variable);
|
|
2011
|
+
if (node) {
|
|
2012
|
+
return node.properties[expr.property];
|
|
2013
|
+
}
|
|
2014
|
+
return null;
|
|
2015
|
+
}
|
|
2016
|
+
else if (expr.type === "parameter") {
|
|
2017
|
+
return params[expr.name];
|
|
2018
|
+
}
|
|
2019
|
+
else if (expr.type === "literal") {
|
|
2020
|
+
return expr.value;
|
|
2021
|
+
}
|
|
2022
|
+
return null;
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Execute a CREATE relationship pattern, tracking created IDs
|
|
2026
|
+
*/
|
|
2027
|
+
executeCreateRelationshipPattern(rel, createdIds, params) {
|
|
2028
|
+
let sourceId;
|
|
2029
|
+
let targetId;
|
|
2030
|
+
// Determine source node ID
|
|
2031
|
+
if (rel.source.variable && createdIds.has(rel.source.variable)) {
|
|
2032
|
+
sourceId = createdIds.get(rel.source.variable);
|
|
2033
|
+
}
|
|
2034
|
+
else if (rel.source.variable && !createdIds.has(rel.source.variable) && !rel.source.label) {
|
|
2035
|
+
// Variable referenced but not found and no label - error
|
|
2036
|
+
throw new Error(`Cannot resolve source node: ${rel.source.variable}`);
|
|
2037
|
+
}
|
|
2038
|
+
else {
|
|
2039
|
+
// Create new source node (with or without label - anonymous nodes are valid)
|
|
2040
|
+
sourceId = crypto.randomUUID();
|
|
2041
|
+
const props = this.resolveProperties(rel.source.properties || {}, params);
|
|
2042
|
+
const labelJson = this.normalizeLabelToJson(rel.source.label);
|
|
2043
|
+
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [sourceId, labelJson, JSON.stringify(props)]);
|
|
2044
|
+
if (rel.source.variable) {
|
|
2045
|
+
createdIds.set(rel.source.variable, sourceId);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
// Determine target node ID
|
|
2049
|
+
if (rel.target.variable && createdIds.has(rel.target.variable)) {
|
|
2050
|
+
targetId = createdIds.get(rel.target.variable);
|
|
2051
|
+
}
|
|
2052
|
+
else if (rel.target.variable && !createdIds.has(rel.target.variable) && !rel.target.label) {
|
|
2053
|
+
// Variable referenced but not found and no label - error
|
|
2054
|
+
throw new Error(`Cannot resolve target node: ${rel.target.variable}`);
|
|
2055
|
+
}
|
|
2056
|
+
else {
|
|
2057
|
+
// Create new target node (with or without label - anonymous nodes are valid)
|
|
2058
|
+
targetId = crypto.randomUUID();
|
|
2059
|
+
const props = this.resolveProperties(rel.target.properties || {}, params);
|
|
2060
|
+
const labelJson = this.normalizeLabelToJson(rel.target.label);
|
|
2061
|
+
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [targetId, labelJson, JSON.stringify(props)]);
|
|
2062
|
+
if (rel.target.variable) {
|
|
2063
|
+
createdIds.set(rel.target.variable, targetId);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
// Swap source/target for left-directed relationships
|
|
2067
|
+
const [actualSource, actualTarget] = rel.edge.direction === "left" ? [targetId, sourceId] : [sourceId, targetId];
|
|
2068
|
+
// Create edge
|
|
2069
|
+
const edgeId = crypto.randomUUID();
|
|
2070
|
+
const edgeType = rel.edge.type || "";
|
|
2071
|
+
const edgeProps = this.resolveProperties(rel.edge.properties || {}, params);
|
|
2072
|
+
this.db.execute("INSERT INTO edges (id, type, source_id, target_id, properties) VALUES (?, ?, ?, ?, ?)", [edgeId, edgeType, actualSource, actualTarget, JSON.stringify(edgeProps)]);
|
|
2073
|
+
if (rel.edge.variable) {
|
|
2074
|
+
createdIds.set(rel.edge.variable, edgeId);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Get a name for an expression (for default aliases)
|
|
2079
|
+
*/
|
|
2080
|
+
getExpressionName(expr) {
|
|
2081
|
+
switch (expr.type) {
|
|
2082
|
+
case "variable":
|
|
2083
|
+
return expr.variable;
|
|
2084
|
+
case "property":
|
|
2085
|
+
return `${expr.variable}_${expr.property}`;
|
|
2086
|
+
case "function": {
|
|
2087
|
+
// Build function expression like count(a) or count(*)
|
|
2088
|
+
const funcName = expr.functionName.toLowerCase();
|
|
2089
|
+
if (expr.args && expr.args.length > 0) {
|
|
2090
|
+
const argNames = expr.args.map(arg => {
|
|
2091
|
+
if (arg.type === "variable")
|
|
2092
|
+
return arg.variable;
|
|
2093
|
+
return "?";
|
|
2094
|
+
});
|
|
2095
|
+
return `${funcName}(${argNames.join(", ")})`;
|
|
2096
|
+
}
|
|
2097
|
+
// Empty args for count(*) or similar
|
|
2098
|
+
return `${funcName}(*)`;
|
|
2099
|
+
}
|
|
2100
|
+
default:
|
|
2101
|
+
return "expr";
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Detect and handle patterns that need multi-phase execution:
|
|
2106
|
+
* - MATCH...CREATE that references matched variables
|
|
2107
|
+
* - MATCH...SET that updates matched nodes/edges via relationships
|
|
2108
|
+
* - MATCH...DELETE that deletes matched nodes/edges via relationships
|
|
2109
|
+
* Returns null if this is not a multi-phase pattern, otherwise returns the result data.
|
|
2110
|
+
*/
|
|
2111
|
+
tryMultiPhaseExecution(query, params) {
|
|
2112
|
+
// Categorize clauses
|
|
2113
|
+
const matchClauses = [];
|
|
2114
|
+
const withClauses = [];
|
|
2115
|
+
const createClauses = [];
|
|
2116
|
+
const setClauses = [];
|
|
2117
|
+
const deleteClauses = [];
|
|
2118
|
+
let returnClause = null;
|
|
2119
|
+
for (const clause of query.clauses) {
|
|
2120
|
+
switch (clause.type) {
|
|
2121
|
+
case "MATCH":
|
|
2122
|
+
matchClauses.push(clause);
|
|
2123
|
+
break;
|
|
2124
|
+
case "WITH":
|
|
2125
|
+
withClauses.push(clause);
|
|
2126
|
+
break;
|
|
2127
|
+
case "CREATE":
|
|
2128
|
+
createClauses.push(clause);
|
|
2129
|
+
break;
|
|
2130
|
+
case "SET":
|
|
2131
|
+
setClauses.push(clause);
|
|
2132
|
+
break;
|
|
2133
|
+
case "DELETE":
|
|
2134
|
+
deleteClauses.push(clause);
|
|
2135
|
+
break;
|
|
2136
|
+
case "RETURN":
|
|
2137
|
+
returnClause = clause;
|
|
2138
|
+
break;
|
|
2139
|
+
default:
|
|
2140
|
+
// MERGE and other clauses - use standard execution
|
|
2141
|
+
return null;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
// Need at least one MATCH clause for multi-phase
|
|
2145
|
+
if (matchClauses.length === 0) {
|
|
2146
|
+
return null;
|
|
2147
|
+
}
|
|
2148
|
+
// Check if any MATCH has relationship patterns (multi-hop)
|
|
2149
|
+
const hasRelationshipPattern = matchClauses.some((m) => m.patterns.some((p) => this.isRelationshipPattern(p)));
|
|
2150
|
+
// Use multi-phase for:
|
|
2151
|
+
// - Relationship patterns (multi-hop) - except simple MATCH...RETURN without mutations
|
|
2152
|
+
// - MATCH...CREATE referencing matched vars
|
|
2153
|
+
// - MATCH...SET (always needs ID resolution)
|
|
2154
|
+
// - MATCH...DELETE (always needs ID resolution)
|
|
2155
|
+
const hasMutations = createClauses.length > 0 || setClauses.length > 0 || deleteClauses.length > 0;
|
|
2156
|
+
const needsMultiPhase = (hasRelationshipPattern && hasMutations) ||
|
|
2157
|
+
createClauses.length > 0 ||
|
|
2158
|
+
setClauses.length > 0 ||
|
|
2159
|
+
deleteClauses.length > 0;
|
|
2160
|
+
if (!needsMultiPhase) {
|
|
2161
|
+
return null;
|
|
2162
|
+
}
|
|
2163
|
+
// Collect all variables defined in MATCH clauses
|
|
2164
|
+
const matchedVariables = new Set();
|
|
2165
|
+
for (const matchClause of matchClauses) {
|
|
2166
|
+
for (const pattern of matchClause.patterns) {
|
|
2167
|
+
this.collectVariablesFromPattern(pattern, matchedVariables);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
// Build alias map from WITH clauses: alias -> original variable
|
|
2171
|
+
// e.g., WITH n AS a creates aliasMap["a"] = "n"
|
|
2172
|
+
// Supports chaining: WITH n AS a, then WITH a AS x -> x points to n
|
|
2173
|
+
const aliasMap = new Map();
|
|
2174
|
+
// Also track property expression aliases: alias -> { variable, property }
|
|
2175
|
+
// e.g., WITH n.num AS num creates propertyAliasMap["num"] = { variable: "n", property: "num" }
|
|
2176
|
+
const propertyAliasMap = new Map();
|
|
2177
|
+
// Track WITH aggregate aliases: alias -> { functionName, argVariable }
|
|
2178
|
+
// e.g., WITH sum(num) AS sum creates withAggregateMap["sum"] = { functionName: "SUM", argVariable: "num" }
|
|
2179
|
+
const withAggregateMap = new Map();
|
|
2180
|
+
for (const withClause of withClauses) {
|
|
2181
|
+
for (const item of withClause.items) {
|
|
2182
|
+
if (item.alias && item.expression.type === "variable" && item.expression.variable) {
|
|
2183
|
+
const original = item.expression.variable;
|
|
2184
|
+
// Track aliases that refer to matched variables OR to existing aliases
|
|
2185
|
+
if (matchedVariables.has(original)) {
|
|
2186
|
+
aliasMap.set(item.alias, original);
|
|
2187
|
+
}
|
|
2188
|
+
else if (aliasMap.has(original)) {
|
|
2189
|
+
// Chained alias: x -> a -> n, resolve the chain
|
|
2190
|
+
aliasMap.set(item.alias, aliasMap.get(original));
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
else if (item.alias && item.expression.type === "property" && item.expression.variable && item.expression.property) {
|
|
2194
|
+
// Track property expression aliases
|
|
2195
|
+
propertyAliasMap.set(item.alias, {
|
|
2196
|
+
variable: item.expression.variable,
|
|
2197
|
+
property: item.expression.property
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
else if (item.alias && item.expression.type === "function") {
|
|
2201
|
+
// Track WITH aggregate aliases like: WITH sum(num) AS sum
|
|
2202
|
+
const funcName = item.expression.functionName?.toUpperCase();
|
|
2203
|
+
const aggregateFunctions = ["SUM", "COUNT", "AVG", "MIN", "MAX", "COLLECT"];
|
|
2204
|
+
if (funcName && aggregateFunctions.includes(funcName) && item.expression.args?.length === 1) {
|
|
2205
|
+
const arg = item.expression.args[0];
|
|
2206
|
+
if (arg.type === "variable" && arg.variable) {
|
|
2207
|
+
withAggregateMap.set(item.alias, {
|
|
2208
|
+
functionName: funcName,
|
|
2209
|
+
argVariable: arg.variable,
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
// Helper to resolve an alias to its original variable (follows chains)
|
|
2217
|
+
const resolveAlias = (varName) => {
|
|
2218
|
+
if (matchedVariables.has(varName))
|
|
2219
|
+
return varName;
|
|
2220
|
+
if (aliasMap.has(varName)) {
|
|
2221
|
+
// The alias map already stores the fully resolved original (no chains)
|
|
2222
|
+
return aliasMap.get(varName);
|
|
2223
|
+
}
|
|
2224
|
+
return null;
|
|
2225
|
+
};
|
|
2226
|
+
// Validate that all variable references in CREATE properties are defined
|
|
2227
|
+
// (either matched variables, aliases, or parameters)
|
|
2228
|
+
this.validateCreatePropertyVariables(createClauses, matchedVariables, aliasMap, params);
|
|
2229
|
+
// Determine which variables need to be resolved for CREATE
|
|
2230
|
+
const referencedInCreate = new Set();
|
|
2231
|
+
for (const createClause of createClauses) {
|
|
2232
|
+
for (const pattern of createClause.patterns) {
|
|
2233
|
+
this.findReferencedVariablesWithAliases(pattern, matchedVariables, aliasMap, referencedInCreate);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
// Determine which variables need to be resolved for SET
|
|
2237
|
+
const referencedInSet = new Set();
|
|
2238
|
+
for (const setClause of setClauses) {
|
|
2239
|
+
for (const assignment of setClause.assignments) {
|
|
2240
|
+
const resolved = resolveAlias(assignment.variable);
|
|
2241
|
+
if (resolved) {
|
|
2242
|
+
referencedInSet.add(resolved);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
// Determine which variables need to be resolved for DELETE
|
|
2247
|
+
const referencedInDelete = new Set();
|
|
2248
|
+
for (const deleteClause of deleteClauses) {
|
|
2249
|
+
for (const variable of deleteClause.variables) {
|
|
2250
|
+
const resolved = resolveAlias(variable);
|
|
2251
|
+
if (resolved) {
|
|
2252
|
+
referencedInDelete.add(resolved);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
// Combine all referenced variables
|
|
2257
|
+
const allReferencedVars = new Set([
|
|
2258
|
+
...referencedInCreate,
|
|
2259
|
+
...referencedInSet,
|
|
2260
|
+
...referencedInDelete,
|
|
2261
|
+
]);
|
|
2262
|
+
// Check if any CREATE/SET/DELETE pattern uses an aliased variable
|
|
2263
|
+
// This is the specific case we need multi-phase for: WITH introduces an alias
|
|
2264
|
+
// that is then used in a mutation clause
|
|
2265
|
+
const aliasesUsedInMutation = new Set();
|
|
2266
|
+
// Check CREATE patterns for aliased variables
|
|
2267
|
+
for (const createClause of createClauses) {
|
|
2268
|
+
for (const pattern of createClause.patterns) {
|
|
2269
|
+
if (this.isRelationshipPattern(pattern)) {
|
|
2270
|
+
if (pattern.source.variable && aliasMap.has(pattern.source.variable)) {
|
|
2271
|
+
aliasesUsedInMutation.add(pattern.source.variable);
|
|
2272
|
+
}
|
|
2273
|
+
if (pattern.target.variable && aliasMap.has(pattern.target.variable)) {
|
|
2274
|
+
aliasesUsedInMutation.add(pattern.target.variable);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
// Check SET assignments for aliased variables
|
|
2280
|
+
for (const setClause of setClauses) {
|
|
2281
|
+
for (const assignment of setClause.assignments) {
|
|
2282
|
+
if (aliasMap.has(assignment.variable)) {
|
|
2283
|
+
aliasesUsedInMutation.add(assignment.variable);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
// Check DELETE for aliased variables
|
|
2288
|
+
for (const deleteClause of deleteClauses) {
|
|
2289
|
+
for (const variable of deleteClause.variables) {
|
|
2290
|
+
if (aliasMap.has(variable)) {
|
|
2291
|
+
aliasesUsedInMutation.add(variable);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
// Only use multi-phase WITH handling if aliases are actually used in mutations
|
|
2296
|
+
const needsWithAliasHandling = aliasesUsedInMutation.size > 0;
|
|
2297
|
+
// Check if RETURN references any property aliases (like `num` from `WITH n.num AS num`)
|
|
2298
|
+
// This requires multi-phase execution because the translator can't handle property aliases
|
|
2299
|
+
let returnUsesPropertyAliases = false;
|
|
2300
|
+
if (returnClause && propertyAliasMap.size > 0) {
|
|
2301
|
+
const returnVars = this.collectReturnVariables(returnClause);
|
|
2302
|
+
returnUsesPropertyAliases = returnVars.some(v => propertyAliasMap.has(v));
|
|
2303
|
+
}
|
|
2304
|
+
// Check if RETURN references any WITH aggregate aliases (like `sum` from `WITH sum(num) AS sum`)
|
|
2305
|
+
let returnUsesWithAggregates = false;
|
|
2306
|
+
if (returnClause && withAggregateMap.size > 0) {
|
|
2307
|
+
const returnVars = this.collectReturnVariables(returnClause);
|
|
2308
|
+
returnUsesWithAggregates = returnVars.some(v => withAggregateMap.has(v));
|
|
2309
|
+
}
|
|
2310
|
+
// DEBUG (disabled)
|
|
2311
|
+
// console.log("tryMultiPhaseExecution debug:", {
|
|
2312
|
+
// withClauses: withClauses.length,
|
|
2313
|
+
// aliasMap: [...aliasMap.entries()],
|
|
2314
|
+
// propertyAliasMap: [...propertyAliasMap.entries()],
|
|
2315
|
+
// withAggregateMap: [...withAggregateMap.entries()],
|
|
2316
|
+
// aliasesUsedInMutation: [...aliasesUsedInMutation],
|
|
2317
|
+
// needsWithAliasHandling,
|
|
2318
|
+
// returnUsesPropertyAliases,
|
|
2319
|
+
// returnUsesWithAggregates,
|
|
2320
|
+
// createClauses: createClauses.length,
|
|
2321
|
+
// });
|
|
2322
|
+
// If WITH clauses are present but no aliases are used in mutations AND no property aliases in RETURN AND no WITH aggregates in RETURN, use standard execution
|
|
2323
|
+
if (withClauses.length > 0 && !needsWithAliasHandling && !returnUsesPropertyAliases && !returnUsesWithAggregates) {
|
|
2324
|
+
return null;
|
|
2325
|
+
}
|
|
2326
|
+
// If no relationship patterns and nothing references matched vars, use standard execution
|
|
2327
|
+
if (!hasRelationshipPattern && allReferencedVars.size === 0 && !needsWithAliasHandling && !returnUsesPropertyAliases && !returnUsesWithAggregates) {
|
|
2328
|
+
return null;
|
|
2329
|
+
}
|
|
2330
|
+
// If WITH aliases are used in mutations, add the original variables to resolution set
|
|
2331
|
+
if (needsWithAliasHandling) {
|
|
2332
|
+
for (const alias of aliasesUsedInMutation) {
|
|
2333
|
+
const original = aliasMap.get(alias);
|
|
2334
|
+
if (original) {
|
|
2335
|
+
allReferencedVars.add(original);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
// For relationship patterns with SET/DELETE, we need to resolve all matched variables
|
|
2340
|
+
if (hasRelationshipPattern && (setClauses.length > 0 || deleteClauses.length > 0)) {
|
|
2341
|
+
// Add all matched variables to the resolution set
|
|
2342
|
+
for (const v of matchedVariables) {
|
|
2343
|
+
allReferencedVars.add(v);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
// Multi-phase execution needed
|
|
2347
|
+
return this.executeMultiPhaseGeneral(matchClauses, withClauses, createClauses, setClauses, deleteClauses, returnClause, allReferencedVars, matchedVariables, aliasMap, propertyAliasMap, withAggregateMap, params);
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Collect variable names from a pattern
|
|
2351
|
+
*/
|
|
2352
|
+
collectVariablesFromPattern(pattern, variables) {
|
|
2353
|
+
if (this.isRelationshipPattern(pattern)) {
|
|
2354
|
+
if (pattern.source.variable)
|
|
2355
|
+
variables.add(pattern.source.variable);
|
|
2356
|
+
if (pattern.target.variable)
|
|
2357
|
+
variables.add(pattern.target.variable);
|
|
2358
|
+
if (pattern.edge.variable)
|
|
2359
|
+
variables.add(pattern.edge.variable);
|
|
2360
|
+
}
|
|
2361
|
+
else {
|
|
2362
|
+
if (pattern.variable)
|
|
2363
|
+
variables.add(pattern.variable);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* Find variables in CREATE that reference MATCH variables
|
|
2368
|
+
*/
|
|
2369
|
+
findReferencedVariables(pattern, matchedVars, referenced) {
|
|
2370
|
+
if (this.isRelationshipPattern(pattern)) {
|
|
2371
|
+
// Source node references a matched variable if it has no label
|
|
2372
|
+
if (pattern.source.variable && !pattern.source.label && matchedVars.has(pattern.source.variable)) {
|
|
2373
|
+
referenced.add(pattern.source.variable);
|
|
2374
|
+
}
|
|
2375
|
+
// Target node references a matched variable if it has no label
|
|
2376
|
+
if (pattern.target.variable && !pattern.target.label && matchedVars.has(pattern.target.variable)) {
|
|
2377
|
+
referenced.add(pattern.target.variable);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* Find variables in CREATE that reference MATCH variables (with alias support)
|
|
2383
|
+
*/
|
|
2384
|
+
findReferencedVariablesWithAliases(pattern, matchedVars, aliasMap, referenced) {
|
|
2385
|
+
const resolveToOriginal = (varName) => {
|
|
2386
|
+
if (!varName)
|
|
2387
|
+
return null;
|
|
2388
|
+
if (matchedVars.has(varName))
|
|
2389
|
+
return varName;
|
|
2390
|
+
if (aliasMap.has(varName))
|
|
2391
|
+
return aliasMap.get(varName);
|
|
2392
|
+
return null;
|
|
2393
|
+
};
|
|
2394
|
+
if (this.isRelationshipPattern(pattern)) {
|
|
2395
|
+
// Source node references a matched variable if it has no label
|
|
2396
|
+
if (pattern.source.variable && !pattern.source.label) {
|
|
2397
|
+
const original = resolveToOriginal(pattern.source.variable);
|
|
2398
|
+
if (original)
|
|
2399
|
+
referenced.add(original);
|
|
2400
|
+
}
|
|
2401
|
+
// Target node references a matched variable if it has no label
|
|
2402
|
+
if (pattern.target.variable && !pattern.target.label) {
|
|
2403
|
+
const original = resolveToOriginal(pattern.target.variable);
|
|
2404
|
+
if (original)
|
|
2405
|
+
referenced.add(original);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
/**
|
|
2410
|
+
* Validate that all variable references in CREATE clause properties are defined.
|
|
2411
|
+
* Throws an error if an undefined variable is referenced.
|
|
2412
|
+
*/
|
|
2413
|
+
validateCreatePropertyVariables(createClauses, matchedVariables, aliasMap, params) {
|
|
2414
|
+
for (const createClause of createClauses) {
|
|
2415
|
+
for (const pattern of createClause.patterns) {
|
|
2416
|
+
// Track variables that will be defined in this pattern (for relationship patterns)
|
|
2417
|
+
const willDefine = new Set();
|
|
2418
|
+
if (this.isRelationshipPattern(pattern)) {
|
|
2419
|
+
// For relationship patterns, check source, edge, and target properties
|
|
2420
|
+
// Source node - its variable will be available for subsequent parts
|
|
2421
|
+
if (pattern.source.variable) {
|
|
2422
|
+
willDefine.add(pattern.source.variable);
|
|
2423
|
+
}
|
|
2424
|
+
this.validatePropertiesForUndefinedVariables(pattern.source.properties || {}, matchedVariables, aliasMap, willDefine, params);
|
|
2425
|
+
// Edge properties
|
|
2426
|
+
if (pattern.edge.variable) {
|
|
2427
|
+
willDefine.add(pattern.edge.variable);
|
|
2428
|
+
}
|
|
2429
|
+
this.validatePropertiesForUndefinedVariables(pattern.edge.properties || {}, matchedVariables, aliasMap, willDefine, params);
|
|
2430
|
+
// Target node properties
|
|
2431
|
+
if (pattern.target.variable) {
|
|
2432
|
+
willDefine.add(pattern.target.variable);
|
|
2433
|
+
}
|
|
2434
|
+
this.validatePropertiesForUndefinedVariables(pattern.target.properties || {}, matchedVariables, aliasMap, willDefine, params);
|
|
2435
|
+
}
|
|
2436
|
+
else {
|
|
2437
|
+
// Simple node pattern
|
|
2438
|
+
if (pattern.variable) {
|
|
2439
|
+
willDefine.add(pattern.variable);
|
|
2440
|
+
}
|
|
2441
|
+
this.validatePropertiesForUndefinedVariables(pattern.properties || {}, matchedVariables, aliasMap, willDefine, params);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Check a properties object for undefined variable references.
|
|
2448
|
+
* Throws an error if found.
|
|
2449
|
+
*/
|
|
2450
|
+
validatePropertiesForUndefinedVariables(props, matchedVariables, aliasMap, willDefine, params) {
|
|
2451
|
+
for (const [_key, value] of Object.entries(props)) {
|
|
2452
|
+
this.validateValueForUndefinedVariables(value, matchedVariables, aliasMap, willDefine, params);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Recursively check a value for undefined variable references.
|
|
2457
|
+
*/
|
|
2458
|
+
validateValueForUndefinedVariables(value, matchedVariables, aliasMap, willDefine, params) {
|
|
2459
|
+
if (typeof value !== "object" || value === null)
|
|
2460
|
+
return;
|
|
2461
|
+
const typedValue = value;
|
|
2462
|
+
if (typedValue.type === "variable" && typedValue.name) {
|
|
2463
|
+
const varName = typedValue.name;
|
|
2464
|
+
// Check if it's a matched variable, an alias, or a parameter
|
|
2465
|
+
const isValidVar = matchedVariables.has(varName) ||
|
|
2466
|
+
aliasMap.has(varName) ||
|
|
2467
|
+
willDefine.has(varName) ||
|
|
2468
|
+
params.hasOwnProperty(varName);
|
|
2469
|
+
if (!isValidVar) {
|
|
2470
|
+
throw new Error(`Variable \`${varName}\` not defined`);
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
else if (typedValue.type === "property" && typedValue.variable) {
|
|
2474
|
+
const varName = typedValue.variable;
|
|
2475
|
+
// Check if it's a matched variable, an alias, or will be defined
|
|
2476
|
+
const isValidVar = matchedVariables.has(varName) ||
|
|
2477
|
+
aliasMap.has(varName) ||
|
|
2478
|
+
willDefine.has(varName);
|
|
2479
|
+
if (!isValidVar) {
|
|
2480
|
+
throw new Error(`Variable \`${varName}\` not defined`);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
else if (typedValue.type === "binary") {
|
|
2484
|
+
// Recursively check left and right operands
|
|
2485
|
+
if (typedValue.left) {
|
|
2486
|
+
this.validateValueForUndefinedVariables(typedValue.left, matchedVariables, aliasMap, willDefine, params);
|
|
2487
|
+
}
|
|
2488
|
+
if (typedValue.right) {
|
|
2489
|
+
this.validateValueForUndefinedVariables(typedValue.right, matchedVariables, aliasMap, willDefine, params);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
// Parameters and literals are always valid, no check needed
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* Execute a complex pattern with MATCH...CREATE/SET/DELETE in multiple phases
|
|
2496
|
+
*/
|
|
2497
|
+
executeMultiPhaseGeneral(matchClauses, withClauses, createClauses, setClauses, deleteClauses, returnClause, referencedVars, allMatchedVars, aliasMap, propertyAliasMap, withAggregateMap, params) {
|
|
2498
|
+
// Phase 1: Execute MATCH to get actual node/edge IDs
|
|
2499
|
+
const varsToResolve = referencedVars.size > 0 ? referencedVars : allMatchedVars;
|
|
2500
|
+
// Also need to include variables referenced by property aliases
|
|
2501
|
+
for (const [_, propInfo] of propertyAliasMap) {
|
|
2502
|
+
if (allMatchedVars.has(propInfo.variable)) {
|
|
2503
|
+
varsToResolve.add(propInfo.variable);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
// Collect deleted variables for edge type capture
|
|
2507
|
+
const deletedVars = new Set();
|
|
2508
|
+
for (const deleteClause of deleteClauses) {
|
|
2509
|
+
for (const variable of deleteClause.variables) {
|
|
2510
|
+
deletedVars.add(variable);
|
|
2511
|
+
// Also add alias resolution
|
|
2512
|
+
if (aliasMap.has(variable)) {
|
|
2513
|
+
deletedVars.add(aliasMap.get(variable));
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
// Detect type(r) in RETURN where r is a deleted variable - need to capture edge type before deletion
|
|
2518
|
+
const edgeTypesToCapture = new Set();
|
|
2519
|
+
if (returnClause && deletedVars.size > 0) {
|
|
2520
|
+
for (const item of returnClause.items) {
|
|
2521
|
+
if (item.expression.type === "function" &&
|
|
2522
|
+
item.expression.functionName?.toUpperCase() === "TYPE" &&
|
|
2523
|
+
item.expression.args?.length === 1 &&
|
|
2524
|
+
item.expression.args[0].type === "variable") {
|
|
2525
|
+
const varName = item.expression.args[0].variable;
|
|
2526
|
+
if (deletedVars.has(varName)) {
|
|
2527
|
+
edgeTypesToCapture.add(varName);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
// Build RETURN items: IDs for all variables + property values for property aliases
|
|
2533
|
+
const returnItems = [];
|
|
2534
|
+
// Add ID lookups for variables
|
|
2535
|
+
for (const v of varsToResolve) {
|
|
2536
|
+
returnItems.push({
|
|
2537
|
+
expression: { type: "function", functionName: "ID", args: [{ type: "variable", variable: v }] },
|
|
2538
|
+
alias: `_id_${v}`,
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
// Add property value lookups for property aliases
|
|
2542
|
+
for (const [alias, propInfo] of propertyAliasMap) {
|
|
2543
|
+
returnItems.push({
|
|
2544
|
+
expression: { type: "property", variable: propInfo.variable, property: propInfo.property },
|
|
2545
|
+
alias: `_prop_${alias}`,
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
// Add edge type lookups for deleted edges used in type() function
|
|
2549
|
+
for (const v of edgeTypesToCapture) {
|
|
2550
|
+
returnItems.push({
|
|
2551
|
+
expression: { type: "function", functionName: "TYPE", args: [{ type: "variable", variable: v }] },
|
|
2552
|
+
alias: `_type_${v}`,
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
const matchQuery = {
|
|
2556
|
+
clauses: [
|
|
2557
|
+
...matchClauses,
|
|
2558
|
+
{
|
|
2559
|
+
type: "RETURN",
|
|
2560
|
+
items: returnItems,
|
|
2561
|
+
},
|
|
2562
|
+
],
|
|
2563
|
+
};
|
|
2564
|
+
const translator = new Translator(params);
|
|
2565
|
+
const matchTranslation = translator.translate(matchQuery);
|
|
2566
|
+
let matchedRows = [];
|
|
2567
|
+
for (const stmt of matchTranslation.statements) {
|
|
2568
|
+
const result = this.db.execute(stmt.sql, stmt.params);
|
|
2569
|
+
if (result.rows.length > 0) {
|
|
2570
|
+
matchedRows = result.rows;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
// If no nodes matched, return empty
|
|
2574
|
+
if (matchedRows.length === 0) {
|
|
2575
|
+
return [];
|
|
2576
|
+
}
|
|
2577
|
+
// Phase 2: Execute CREATE/SET/DELETE for each matched row
|
|
2578
|
+
// Keep track of all resolved IDs (including newly created nodes) for RETURN
|
|
2579
|
+
const allResolvedIds = [];
|
|
2580
|
+
// Also keep track of captured property values (before nodes are deleted)
|
|
2581
|
+
const allCapturedPropertyValues = [];
|
|
2582
|
+
// Keep track of captured edge types (before edges are deleted)
|
|
2583
|
+
const allCapturedEdgeTypes = [];
|
|
2584
|
+
this.db.transaction(() => {
|
|
2585
|
+
for (const row of matchedRows) {
|
|
2586
|
+
// Build a map of variable -> actual node/edge ID
|
|
2587
|
+
const resolvedIds = {};
|
|
2588
|
+
for (const v of varsToResolve) {
|
|
2589
|
+
resolvedIds[v] = row[`_id_${v}`];
|
|
2590
|
+
}
|
|
2591
|
+
// Add aliased variable names pointing to the same IDs
|
|
2592
|
+
// e.g., if WITH n AS a, then resolvedIds["a"] = resolvedIds["n"]
|
|
2593
|
+
for (const [alias, original] of aliasMap.entries()) {
|
|
2594
|
+
if (resolvedIds[original]) {
|
|
2595
|
+
resolvedIds[alias] = resolvedIds[original];
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
// Capture property alias values BEFORE any mutations (especially DELETE)
|
|
2599
|
+
// e.g., WITH n.num AS num -> capturedPropertyValues["num"] = value
|
|
2600
|
+
const capturedPropertyValues = {};
|
|
2601
|
+
for (const [alias, _] of propertyAliasMap) {
|
|
2602
|
+
const rawValue = row[`_prop_${alias}`];
|
|
2603
|
+
// Parse JSON values if they're strings (SQLite returns JSON as strings)
|
|
2604
|
+
capturedPropertyValues[alias] = this.deepParseJson(rawValue);
|
|
2605
|
+
}
|
|
2606
|
+
// Capture edge types BEFORE DELETE for type() function on deleted edges
|
|
2607
|
+
const capturedEdgeTypes = {};
|
|
2608
|
+
for (const v of edgeTypesToCapture) {
|
|
2609
|
+
const edgeType = row[`_type_${v}`];
|
|
2610
|
+
if (typeof edgeType === "string") {
|
|
2611
|
+
capturedEdgeTypes[v] = edgeType;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
// Execute CREATE with resolved IDs (this mutates resolvedIds to include new node IDs)
|
|
2615
|
+
for (const createClause of createClauses) {
|
|
2616
|
+
this.executeCreateWithResolvedIds(createClause, resolvedIds, params);
|
|
2617
|
+
}
|
|
2618
|
+
// Execute SET with resolved IDs
|
|
2619
|
+
for (const setClause of setClauses) {
|
|
2620
|
+
this.executeSetWithResolvedIds(setClause, resolvedIds, params);
|
|
2621
|
+
}
|
|
2622
|
+
// Execute DELETE with resolved IDs
|
|
2623
|
+
for (const deleteClause of deleteClauses) {
|
|
2624
|
+
this.executeDeleteWithResolvedIds(deleteClause, resolvedIds);
|
|
2625
|
+
}
|
|
2626
|
+
// Save the resolved IDs for this row (including newly created nodes)
|
|
2627
|
+
allResolvedIds.push({ ...resolvedIds });
|
|
2628
|
+
// Save the captured property values
|
|
2629
|
+
allCapturedPropertyValues.push(capturedPropertyValues);
|
|
2630
|
+
// Save the captured edge types
|
|
2631
|
+
allCapturedEdgeTypes.push(capturedEdgeTypes);
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
// Phase 3: Execute RETURN if present
|
|
2635
|
+
if (returnClause) {
|
|
2636
|
+
// Check if RETURN references any newly created variables (not in matched vars or aliases)
|
|
2637
|
+
const returnVars = this.collectReturnVariables(returnClause);
|
|
2638
|
+
const referencesCreatedVars = returnVars.some(v => !allMatchedVars.has(v) && !aliasMap.has(v) && !propertyAliasMap.has(v));
|
|
2639
|
+
// Check if RETURN references any property aliases (like `num` from `WITH n.num AS num`)
|
|
2640
|
+
const referencesPropertyAliases = returnVars.some(v => propertyAliasMap.has(v));
|
|
2641
|
+
// If SET or DELETE was executed or WITH clauses are present, we need to use buildReturnResults
|
|
2642
|
+
// because aliased variables need to be resolved from our ID map
|
|
2643
|
+
// For DELETE, nodes are gone so we can't query them - we need to return based on original match count
|
|
2644
|
+
if (referencesCreatedVars || referencesPropertyAliases || setClauses.length > 0 || deleteClauses.length > 0 || withClauses.length > 0) {
|
|
2645
|
+
// Apply WITH clause WHERE filters to captured property values
|
|
2646
|
+
// This handles patterns like: WITH n.num AS num ... DELETE n ... WITH num WHERE num % 2 = 0 ... RETURN num
|
|
2647
|
+
let filteredResolvedIds = allResolvedIds;
|
|
2648
|
+
let filteredPropertyValues = allCapturedPropertyValues;
|
|
2649
|
+
let filteredEdgeTypes = allCapturedEdgeTypes;
|
|
2650
|
+
for (const withClause of withClauses) {
|
|
2651
|
+
if (withClause.where) {
|
|
2652
|
+
// Filter the captured values based on the WITH WHERE condition
|
|
2653
|
+
const filteredPairs = [];
|
|
2654
|
+
for (let i = 0; i < filteredResolvedIds.length; i++) {
|
|
2655
|
+
const resolvedIds = filteredResolvedIds[i];
|
|
2656
|
+
const propertyValues = filteredPropertyValues[i] || {};
|
|
2657
|
+
const edgeTypes = filteredEdgeTypes[i] || {};
|
|
2658
|
+
// Check if this row passes the WITH WHERE filter
|
|
2659
|
+
const passes = this.evaluateWithWhereConditionWithPropertyAliases(withClause.where, resolvedIds, propertyValues, propertyAliasMap, params);
|
|
2660
|
+
if (passes) {
|
|
2661
|
+
filteredPairs.push({ resolvedIds, propertyValues, edgeTypes });
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
filteredResolvedIds = filteredPairs.map(p => p.resolvedIds);
|
|
2665
|
+
filteredPropertyValues = filteredPairs.map(p => p.propertyValues);
|
|
2666
|
+
filteredEdgeTypes = filteredPairs.map(p => p.edgeTypes);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
// RETURN references created nodes, aliased vars, property aliases, or data was modified - use buildReturnResults with resolved IDs
|
|
2670
|
+
return this.buildReturnResults(returnClause, filteredResolvedIds, filteredPropertyValues, propertyAliasMap, withAggregateMap, filteredEdgeTypes);
|
|
2671
|
+
}
|
|
2672
|
+
else {
|
|
2673
|
+
// RETURN only references matched nodes and no mutations - use translator-based approach
|
|
2674
|
+
const fullQuery = {
|
|
2675
|
+
clauses: [...matchClauses, returnClause],
|
|
2676
|
+
};
|
|
2677
|
+
const returnTranslator = new Translator(params);
|
|
2678
|
+
const returnTranslation = returnTranslator.translate(fullQuery);
|
|
2679
|
+
let rows = [];
|
|
2680
|
+
for (const stmt of returnTranslation.statements) {
|
|
2681
|
+
const result = this.db.execute(stmt.sql, stmt.params);
|
|
2682
|
+
if (result.rows.length > 0 || stmt.sql.trim().toUpperCase().startsWith("SELECT")) {
|
|
2683
|
+
rows = result.rows;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
return this.formatResults(rows, returnTranslation.returnColumns);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
return [];
|
|
2690
|
+
}
|
|
2691
|
+
/**
|
|
2692
|
+
* Collect variable names referenced in a RETURN clause
|
|
2693
|
+
*/
|
|
2694
|
+
collectReturnVariables(returnClause) {
|
|
2695
|
+
const vars = [];
|
|
2696
|
+
for (const item of returnClause.items) {
|
|
2697
|
+
this.collectExpressionVariables(item.expression, vars);
|
|
2698
|
+
}
|
|
2699
|
+
return vars;
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Collect variable names from an expression
|
|
2703
|
+
*/
|
|
2704
|
+
collectExpressionVariables(expr, vars) {
|
|
2705
|
+
if (expr.type === "variable" && expr.variable) {
|
|
2706
|
+
vars.push(expr.variable);
|
|
2707
|
+
}
|
|
2708
|
+
else if (expr.type === "property" && expr.variable) {
|
|
2709
|
+
vars.push(expr.variable);
|
|
2710
|
+
}
|
|
2711
|
+
else if (expr.type === "function" && expr.args) {
|
|
2712
|
+
for (const arg of expr.args) {
|
|
2713
|
+
this.collectExpressionVariables(arg, vars);
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Build RETURN results from resolved node/edge IDs
|
|
2719
|
+
*/
|
|
2720
|
+
buildReturnResults(returnClause, allResolvedIds, allCapturedPropertyValues = [], propertyAliasMap = new Map(), withAggregateMap = new Map(), allCapturedEdgeTypes = []) {
|
|
2721
|
+
// Check if all return items are aggregates (like count(*))
|
|
2722
|
+
// If so, we should return a single aggregated row instead of per-row results
|
|
2723
|
+
const allAggregates = returnClause.items.every(item => item.expression.type === "function" &&
|
|
2724
|
+
["COUNT", "SUM", "AVG", "MIN", "MAX", "COLLECT"].includes(item.expression.functionName?.toUpperCase() || ""));
|
|
2725
|
+
// Check if RETURN references a WITH aggregate alias (like `sum` from `WITH sum(num) AS sum`)
|
|
2726
|
+
// If so, we need to compute the aggregate and return it
|
|
2727
|
+
const returnReferencesWithAggregates = returnClause.items.some(item => item.expression.type === "variable" &&
|
|
2728
|
+
item.expression.variable &&
|
|
2729
|
+
withAggregateMap.has(item.expression.variable));
|
|
2730
|
+
// Handle RETURN that references WITH aggregate aliases (like `RETURN sum` where `sum` is from `WITH sum(num) AS sum`)
|
|
2731
|
+
if (returnReferencesWithAggregates) {
|
|
2732
|
+
const resultRow = {};
|
|
2733
|
+
for (const item of returnClause.items) {
|
|
2734
|
+
const alias = item.alias || this.getExpressionName(item.expression);
|
|
2735
|
+
if (item.expression.type === "variable" && item.expression.variable) {
|
|
2736
|
+
const varName = item.expression.variable;
|
|
2737
|
+
// Check if this is a WITH aggregate alias
|
|
2738
|
+
if (withAggregateMap.has(varName)) {
|
|
2739
|
+
const aggInfo = withAggregateMap.get(varName);
|
|
2740
|
+
const { functionName, argVariable } = aggInfo;
|
|
2741
|
+
// Compute the aggregate from captured property values
|
|
2742
|
+
if (propertyAliasMap.has(argVariable)) {
|
|
2743
|
+
// The argument is a property alias - compute aggregate from captured values
|
|
2744
|
+
switch (functionName) {
|
|
2745
|
+
case "SUM": {
|
|
2746
|
+
let sum = 0;
|
|
2747
|
+
for (const capturedValues of allCapturedPropertyValues) {
|
|
2748
|
+
const value = capturedValues[argVariable];
|
|
2749
|
+
if (typeof value === "number") {
|
|
2750
|
+
sum += value;
|
|
2751
|
+
}
|
|
2752
|
+
else if (typeof value === "string") {
|
|
2753
|
+
const parsed = parseFloat(value);
|
|
2754
|
+
if (!isNaN(parsed))
|
|
2755
|
+
sum += parsed;
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
resultRow[alias] = sum;
|
|
2759
|
+
break;
|
|
2760
|
+
}
|
|
2761
|
+
case "COUNT": {
|
|
2762
|
+
resultRow[alias] = allCapturedPropertyValues.length;
|
|
2763
|
+
break;
|
|
2764
|
+
}
|
|
2765
|
+
case "AVG": {
|
|
2766
|
+
let sum = 0;
|
|
2767
|
+
let count = 0;
|
|
2768
|
+
for (const capturedValues of allCapturedPropertyValues) {
|
|
2769
|
+
const value = capturedValues[argVariable];
|
|
2770
|
+
if (typeof value === "number") {
|
|
2771
|
+
sum += value;
|
|
2772
|
+
count++;
|
|
2773
|
+
}
|
|
2774
|
+
else if (typeof value === "string") {
|
|
2775
|
+
const parsed = parseFloat(value);
|
|
2776
|
+
if (!isNaN(parsed)) {
|
|
2777
|
+
sum += parsed;
|
|
2778
|
+
count++;
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
resultRow[alias] = count > 0 ? sum / count : null;
|
|
2783
|
+
break;
|
|
2784
|
+
}
|
|
2785
|
+
case "MIN": {
|
|
2786
|
+
let min = null;
|
|
2787
|
+
for (const capturedValues of allCapturedPropertyValues) {
|
|
2788
|
+
const value = capturedValues[argVariable];
|
|
2789
|
+
const numValue = typeof value === "number" ? value : parseFloat(value);
|
|
2790
|
+
if (!isNaN(numValue)) {
|
|
2791
|
+
min = min === null ? numValue : Math.min(min, numValue);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
resultRow[alias] = min;
|
|
2795
|
+
break;
|
|
2796
|
+
}
|
|
2797
|
+
case "MAX": {
|
|
2798
|
+
let max = null;
|
|
2799
|
+
for (const capturedValues of allCapturedPropertyValues) {
|
|
2800
|
+
const value = capturedValues[argVariable];
|
|
2801
|
+
const numValue = typeof value === "number" ? value : parseFloat(value);
|
|
2802
|
+
if (!isNaN(numValue)) {
|
|
2803
|
+
max = max === null ? numValue : Math.max(max, numValue);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
resultRow[alias] = max;
|
|
2807
|
+
break;
|
|
2808
|
+
}
|
|
2809
|
+
case "COLLECT": {
|
|
2810
|
+
const values = [];
|
|
2811
|
+
for (const capturedValues of allCapturedPropertyValues) {
|
|
2812
|
+
values.push(capturedValues[argVariable]);
|
|
2813
|
+
}
|
|
2814
|
+
resultRow[alias] = values;
|
|
2815
|
+
break;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
return [resultRow];
|
|
2823
|
+
}
|
|
2824
|
+
if (allAggregates && returnClause.items.length > 0) {
|
|
2825
|
+
// Return a single row with aggregated values
|
|
2826
|
+
const resultRow = {};
|
|
2827
|
+
for (const item of returnClause.items) {
|
|
2828
|
+
const alias = item.alias || this.getExpressionName(item.expression);
|
|
2829
|
+
const funcName = item.expression.functionName?.toUpperCase();
|
|
2830
|
+
if (funcName === "COUNT") {
|
|
2831
|
+
resultRow[alias] = allResolvedIds.length;
|
|
2832
|
+
}
|
|
2833
|
+
else if (funcName === "SUM") {
|
|
2834
|
+
// Sum values - may reference property aliases
|
|
2835
|
+
const arg = item.expression.args?.[0];
|
|
2836
|
+
if (arg?.type === "variable" && arg.variable) {
|
|
2837
|
+
const varName = arg.variable;
|
|
2838
|
+
let sum = 0;
|
|
2839
|
+
// Check if this variable is a property alias
|
|
2840
|
+
if (propertyAliasMap.has(varName)) {
|
|
2841
|
+
// Sum the captured property values
|
|
2842
|
+
for (const capturedValues of allCapturedPropertyValues) {
|
|
2843
|
+
const value = capturedValues[varName];
|
|
2844
|
+
if (typeof value === "number") {
|
|
2845
|
+
sum += value;
|
|
2846
|
+
}
|
|
2847
|
+
else if (typeof value === "string") {
|
|
2848
|
+
const parsed = parseFloat(value);
|
|
2849
|
+
if (!isNaN(parsed))
|
|
2850
|
+
sum += parsed;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
resultRow[alias] = sum;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
else if (funcName === "COLLECT") {
|
|
2858
|
+
// Collect all values for the variable
|
|
2859
|
+
const arg = item.expression.args?.[0];
|
|
2860
|
+
if (arg?.type === "variable" && arg.variable) {
|
|
2861
|
+
const values = [];
|
|
2862
|
+
const varName = arg.variable;
|
|
2863
|
+
// Check if this variable is a property alias
|
|
2864
|
+
if (propertyAliasMap.has(varName)) {
|
|
2865
|
+
// Collect the captured property values
|
|
2866
|
+
for (const capturedValues of allCapturedPropertyValues) {
|
|
2867
|
+
values.push(capturedValues[varName]);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
else {
|
|
2871
|
+
// It's a node/edge variable - query from database
|
|
2872
|
+
// Neo4j 3.5 format: collect just properties
|
|
2873
|
+
for (const resolvedIds of allResolvedIds) {
|
|
2874
|
+
const nodeId = resolvedIds[varName];
|
|
2875
|
+
if (nodeId) {
|
|
2876
|
+
const nodeResult = this.db.execute("SELECT properties FROM nodes WHERE id = ?", [nodeId]);
|
|
2877
|
+
if (nodeResult.rows.length > 0) {
|
|
2878
|
+
const row = nodeResult.rows[0];
|
|
2879
|
+
values.push(typeof row.properties === "string" ? JSON.parse(row.properties) : row.properties);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
resultRow[alias] = values;
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
else if (funcName === "AVG") {
|
|
2888
|
+
// Average values - may reference property aliases
|
|
2889
|
+
const arg = item.expression.args?.[0];
|
|
2890
|
+
if (arg?.type === "variable" && arg.variable) {
|
|
2891
|
+
const varName = arg.variable;
|
|
2892
|
+
let sum = 0;
|
|
2893
|
+
let count = 0;
|
|
2894
|
+
// Check if this variable is a property alias
|
|
2895
|
+
if (propertyAliasMap.has(varName)) {
|
|
2896
|
+
// Average the captured property values
|
|
2897
|
+
for (const capturedValues of allCapturedPropertyValues) {
|
|
2898
|
+
const value = capturedValues[varName];
|
|
2899
|
+
if (typeof value === "number") {
|
|
2900
|
+
sum += value;
|
|
2901
|
+
count++;
|
|
2902
|
+
}
|
|
2903
|
+
else if (typeof value === "string") {
|
|
2904
|
+
const parsed = parseFloat(value);
|
|
2905
|
+
if (!isNaN(parsed)) {
|
|
2906
|
+
sum += parsed;
|
|
2907
|
+
count++;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
resultRow[alias] = count > 0 ? sum / count : null;
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
else if (funcName === "MIN") {
|
|
2916
|
+
// Min value - may reference property aliases
|
|
2917
|
+
const arg = item.expression.args?.[0];
|
|
2918
|
+
if (arg?.type === "variable" && arg.variable) {
|
|
2919
|
+
const varName = arg.variable;
|
|
2920
|
+
let min = null;
|
|
2921
|
+
// Check if this variable is a property alias
|
|
2922
|
+
if (propertyAliasMap.has(varName)) {
|
|
2923
|
+
for (const capturedValues of allCapturedPropertyValues) {
|
|
2924
|
+
const value = capturedValues[varName];
|
|
2925
|
+
const numValue = typeof value === "number" ? value : parseFloat(value);
|
|
2926
|
+
if (!isNaN(numValue)) {
|
|
2927
|
+
min = min === null ? numValue : Math.min(min, numValue);
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
resultRow[alias] = min;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
else if (funcName === "MAX") {
|
|
2935
|
+
// Max value - may reference property aliases
|
|
2936
|
+
const arg = item.expression.args?.[0];
|
|
2937
|
+
if (arg?.type === "variable" && arg.variable) {
|
|
2938
|
+
const varName = arg.variable;
|
|
2939
|
+
let max = null;
|
|
2940
|
+
// Check if this variable is a property alias
|
|
2941
|
+
if (propertyAliasMap.has(varName)) {
|
|
2942
|
+
for (const capturedValues of allCapturedPropertyValues) {
|
|
2943
|
+
const value = capturedValues[varName];
|
|
2944
|
+
const numValue = typeof value === "number" ? value : parseFloat(value);
|
|
2945
|
+
if (!isNaN(numValue)) {
|
|
2946
|
+
max = max === null ? numValue : Math.max(max, numValue);
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
resultRow[alias] = max;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
// Add other aggregate handlers as needed
|
|
2954
|
+
}
|
|
2955
|
+
return [resultRow];
|
|
2956
|
+
}
|
|
2957
|
+
const results = [];
|
|
2958
|
+
for (let i = 0; i < allResolvedIds.length; i++) {
|
|
2959
|
+
const resolvedIds = allResolvedIds[i];
|
|
2960
|
+
const capturedValues = allCapturedPropertyValues[i] || {};
|
|
2961
|
+
const capturedEdgeTypes = allCapturedEdgeTypes[i] || {};
|
|
2962
|
+
const resultRow = {};
|
|
2963
|
+
for (const item of returnClause.items) {
|
|
2964
|
+
const alias = item.alias || this.getExpressionName(item.expression);
|
|
2965
|
+
if (item.expression.type === "variable") {
|
|
2966
|
+
const variable = item.expression.variable;
|
|
2967
|
+
// Check if this variable is a property alias
|
|
2968
|
+
if (propertyAliasMap.has(variable)) {
|
|
2969
|
+
// Use the captured property value
|
|
2970
|
+
resultRow[alias] = capturedValues[variable];
|
|
2971
|
+
}
|
|
2972
|
+
else {
|
|
2973
|
+
const nodeId = resolvedIds[variable];
|
|
2974
|
+
if (nodeId) {
|
|
2975
|
+
// Query the node/edge by ID
|
|
2976
|
+
const nodeResult = this.db.execute("SELECT id, label, properties FROM nodes WHERE id = ?", [nodeId]);
|
|
2977
|
+
if (nodeResult.rows.length > 0) {
|
|
2978
|
+
const row = nodeResult.rows[0];
|
|
2979
|
+
// Neo4j 3.5 format: return properties directly
|
|
2980
|
+
resultRow[alias] = typeof row.properties === "string"
|
|
2981
|
+
? JSON.parse(row.properties)
|
|
2982
|
+
: row.properties;
|
|
2983
|
+
}
|
|
2984
|
+
else {
|
|
2985
|
+
// Try edges
|
|
2986
|
+
const edgeResult = this.db.execute("SELECT id, type, source_id, target_id, properties FROM edges WHERE id = ?", [nodeId]);
|
|
2987
|
+
if (edgeResult.rows.length > 0) {
|
|
2988
|
+
const row = edgeResult.rows[0];
|
|
2989
|
+
// Neo4j 3.5 format: return properties directly
|
|
2990
|
+
resultRow[alias] = typeof row.properties === "string"
|
|
2991
|
+
? JSON.parse(row.properties)
|
|
2992
|
+
: row.properties;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
else if (item.expression.type === "property") {
|
|
2999
|
+
const variable = item.expression.variable;
|
|
3000
|
+
const property = item.expression.property;
|
|
3001
|
+
const nodeId = resolvedIds[variable];
|
|
3002
|
+
if (nodeId) {
|
|
3003
|
+
// Try nodes first
|
|
3004
|
+
const nodeResult = this.db.execute(`SELECT json_extract(properties, '$.${property}') as value FROM nodes WHERE id = ?`, [nodeId]);
|
|
3005
|
+
if (nodeResult.rows.length > 0) {
|
|
3006
|
+
resultRow[alias] = this.deepParseJson(nodeResult.rows[0].value);
|
|
3007
|
+
}
|
|
3008
|
+
else {
|
|
3009
|
+
// Try edges
|
|
3010
|
+
const edgeResult = this.db.execute(`SELECT json_extract(properties, '$.${property}') as value FROM edges WHERE id = ?`, [nodeId]);
|
|
3011
|
+
if (edgeResult.rows.length > 0) {
|
|
3012
|
+
resultRow[alias] = this.deepParseJson(edgeResult.rows[0].value);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
else if (item.expression.type === "function" && item.expression.functionName === "ID") {
|
|
3018
|
+
// Handle id(n) function
|
|
3019
|
+
const args = item.expression.args;
|
|
3020
|
+
if (args && args.length > 0 && args[0].type === "variable") {
|
|
3021
|
+
const variable = args[0].variable;
|
|
3022
|
+
const nodeId = resolvedIds[variable];
|
|
3023
|
+
if (nodeId) {
|
|
3024
|
+
resultRow[alias] = nodeId;
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
else if (item.expression.type === "function" && item.expression.functionName?.toUpperCase() === "LABELS") {
|
|
3029
|
+
// Handle labels(n) function
|
|
3030
|
+
const args = item.expression.args;
|
|
3031
|
+
if (args && args.length > 0 && args[0].type === "variable") {
|
|
3032
|
+
const variable = args[0].variable;
|
|
3033
|
+
const nodeId = resolvedIds[variable];
|
|
3034
|
+
if (nodeId) {
|
|
3035
|
+
const nodeResult = this.db.execute("SELECT label FROM nodes WHERE id = ?", [nodeId]);
|
|
3036
|
+
if (nodeResult.rows.length > 0) {
|
|
3037
|
+
const labelValue = nodeResult.rows[0].label;
|
|
3038
|
+
// Parse label - could be a JSON array or a string
|
|
3039
|
+
if (typeof labelValue === "string") {
|
|
3040
|
+
try {
|
|
3041
|
+
const parsed = JSON.parse(labelValue);
|
|
3042
|
+
resultRow[alias] = Array.isArray(parsed) ? parsed : [parsed];
|
|
3043
|
+
}
|
|
3044
|
+
catch {
|
|
3045
|
+
resultRow[alias] = labelValue ? [labelValue] : [];
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
else if (Array.isArray(labelValue)) {
|
|
3049
|
+
resultRow[alias] = labelValue;
|
|
3050
|
+
}
|
|
3051
|
+
else {
|
|
3052
|
+
resultRow[alias] = [];
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
else if (item.expression.type === "function" && item.expression.functionName?.toUpperCase() === "TYPE") {
|
|
3059
|
+
// Handle type(r) function
|
|
3060
|
+
const args = item.expression.args;
|
|
3061
|
+
if (args && args.length > 0 && args[0].type === "variable") {
|
|
3062
|
+
const variable = args[0].variable;
|
|
3063
|
+
// First check if we have a captured edge type (for deleted edges)
|
|
3064
|
+
if (capturedEdgeTypes[variable]) {
|
|
3065
|
+
resultRow[alias] = capturedEdgeTypes[variable];
|
|
3066
|
+
}
|
|
3067
|
+
else {
|
|
3068
|
+
// Fall back to querying the database
|
|
3069
|
+
const edgeId = resolvedIds[variable];
|
|
3070
|
+
if (edgeId) {
|
|
3071
|
+
const edgeResult = this.db.execute("SELECT type FROM edges WHERE id = ?", [edgeId]);
|
|
3072
|
+
if (edgeResult.rows.length > 0) {
|
|
3073
|
+
resultRow[alias] = edgeResult.rows[0].type;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
else if (item.expression.type === "function" && item.expression.functionName?.toUpperCase() === "COUNT") {
|
|
3080
|
+
// Handle count(*) or count(n) - for MATCH+SET+RETURN patterns
|
|
3081
|
+
// If we're in buildReturnResults, return the number of rows we processed
|
|
3082
|
+
resultRow[alias] = allResolvedIds.length;
|
|
3083
|
+
}
|
|
3084
|
+
else if (item.expression.type === "literal") {
|
|
3085
|
+
// Handle literal values like RETURN 42 AS num
|
|
3086
|
+
resultRow[alias] = item.expression.value;
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
if (Object.keys(resultRow).length > 0) {
|
|
3090
|
+
results.push(resultRow);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
// Apply SKIP and LIMIT to the results
|
|
3094
|
+
let finalResults = results;
|
|
3095
|
+
const skip = returnClause.skip ?? 0;
|
|
3096
|
+
const limit = returnClause.limit;
|
|
3097
|
+
if (skip > 0) {
|
|
3098
|
+
finalResults = finalResults.slice(skip);
|
|
3099
|
+
}
|
|
3100
|
+
if (limit !== undefined) {
|
|
3101
|
+
finalResults = finalResults.slice(0, limit);
|
|
3102
|
+
}
|
|
3103
|
+
return finalResults;
|
|
3104
|
+
}
|
|
3105
|
+
/**
|
|
3106
|
+
* Execute a MATCH...CREATE pattern in multiple phases (legacy, for backwards compatibility)
|
|
3107
|
+
*/
|
|
3108
|
+
executeMultiPhase(matchClauses, createClauses, referencedVars, params) {
|
|
3109
|
+
return this.executeMultiPhaseGeneral(matchClauses, [], // no WITH clauses
|
|
3110
|
+
createClauses, [], [], null, referencedVars, referencedVars, new Map(), // no alias map
|
|
3111
|
+
new Map(), // no property alias map
|
|
3112
|
+
new Map(), // no WITH aggregate map
|
|
3113
|
+
params);
|
|
3114
|
+
}
|
|
3115
|
+
/**
|
|
3116
|
+
* Execute SET clause with pre-resolved node IDs
|
|
3117
|
+
*/
|
|
3118
|
+
executeSetWithResolvedIds(setClause, resolvedIds, params) {
|
|
3119
|
+
for (const assignment of setClause.assignments) {
|
|
3120
|
+
const nodeId = resolvedIds[assignment.variable];
|
|
3121
|
+
if (!nodeId) {
|
|
3122
|
+
throw new Error(`Cannot resolve variable for SET: ${assignment.variable}`);
|
|
3123
|
+
}
|
|
3124
|
+
// Handle label assignments
|
|
3125
|
+
if (assignment.labels && assignment.labels.length > 0) {
|
|
3126
|
+
const newLabelsJson = JSON.stringify(assignment.labels);
|
|
3127
|
+
this.db.execute(`UPDATE nodes SET label = (SELECT json_group_array(value) FROM (
|
|
3128
|
+
SELECT DISTINCT value FROM (
|
|
3129
|
+
SELECT value FROM json_each(nodes.label)
|
|
3130
|
+
UNION ALL
|
|
3131
|
+
SELECT value FROM json_each(?)
|
|
3132
|
+
) ORDER BY value
|
|
3133
|
+
)) WHERE id = ?`, [newLabelsJson, nodeId]);
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
// Handle SET n = {props} - replace all properties
|
|
3137
|
+
if (assignment.replaceProps && assignment.value) {
|
|
3138
|
+
const newProps = this.evaluateObjectExpression(assignment.value, params);
|
|
3139
|
+
// Filter out null values (they should be removed)
|
|
3140
|
+
const filteredProps = {};
|
|
3141
|
+
for (const [key, val] of Object.entries(newProps)) {
|
|
3142
|
+
if (val !== null) {
|
|
3143
|
+
filteredProps[key] = val;
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
// Try nodes first, then edges
|
|
3147
|
+
const nodeResult = this.db.execute(`UPDATE nodes SET properties = ? WHERE id = ?`, [JSON.stringify(filteredProps), nodeId]);
|
|
3148
|
+
if (nodeResult.changes === 0) {
|
|
3149
|
+
this.db.execute(`UPDATE edges SET properties = ? WHERE id = ?`, [JSON.stringify(filteredProps), nodeId]);
|
|
3150
|
+
}
|
|
3151
|
+
continue;
|
|
3152
|
+
}
|
|
3153
|
+
// Handle SET n += {props} - merge properties
|
|
3154
|
+
if (assignment.mergeProps && assignment.value) {
|
|
3155
|
+
const newProps = this.evaluateObjectExpression(assignment.value, params);
|
|
3156
|
+
const nullKeys = Object.entries(newProps)
|
|
3157
|
+
.filter(([_, val]) => val === null)
|
|
3158
|
+
.map(([key, _]) => key);
|
|
3159
|
+
const nonNullProps = {};
|
|
3160
|
+
for (const [key, val] of Object.entries(newProps)) {
|
|
3161
|
+
if (val !== null) {
|
|
3162
|
+
nonNullProps[key] = val;
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
if (Object.keys(nonNullProps).length === 0 && nullKeys.length === 0) {
|
|
3166
|
+
// Empty map - no-op
|
|
3167
|
+
continue;
|
|
3168
|
+
}
|
|
3169
|
+
if (nullKeys.length > 0) {
|
|
3170
|
+
// Need to merge non-null props and remove null keys
|
|
3171
|
+
const removePaths = nullKeys.map(k => `'$.${k}'`).join(', ');
|
|
3172
|
+
const nodeResult = this.db.execute(`UPDATE nodes SET properties = json_remove(json_patch(properties, ?), ${removePaths}) WHERE id = ?`, [JSON.stringify(nonNullProps), nodeId]);
|
|
3173
|
+
if (nodeResult.changes === 0) {
|
|
3174
|
+
this.db.execute(`UPDATE edges SET properties = json_remove(json_patch(properties, ?), ${removePaths}) WHERE id = ?`, [JSON.stringify(nonNullProps), nodeId]);
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
else {
|
|
3178
|
+
// Just merge
|
|
3179
|
+
const nodeResult = this.db.execute(`UPDATE nodes SET properties = json_patch(properties, ?) WHERE id = ?`, [JSON.stringify(nonNullProps), nodeId]);
|
|
3180
|
+
if (nodeResult.changes === 0) {
|
|
3181
|
+
this.db.execute(`UPDATE edges SET properties = json_patch(properties, ?) WHERE id = ?`, [JSON.stringify(nonNullProps), nodeId]);
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
continue;
|
|
3185
|
+
}
|
|
3186
|
+
// Handle property assignments
|
|
3187
|
+
if (!assignment.value || !assignment.property) {
|
|
3188
|
+
throw new Error(`Invalid SET assignment for variable: ${assignment.variable}`);
|
|
3189
|
+
}
|
|
3190
|
+
// Use context-aware evaluation for expressions that may reference properties
|
|
3191
|
+
const value = assignment.value.type === "binary" || assignment.value.type === "property"
|
|
3192
|
+
? this.evaluateExpressionWithContext(assignment.value, params, resolvedIds)
|
|
3193
|
+
: this.evaluateExpression(assignment.value, params);
|
|
3194
|
+
// If value is null, remove the property instead of setting it to null
|
|
3195
|
+
if (value === null) {
|
|
3196
|
+
const nodeResult = this.db.execute(`UPDATE nodes SET properties = json_remove(properties, '$.${assignment.property}') WHERE id = ?`, [nodeId]);
|
|
3197
|
+
if (nodeResult.changes === 0) {
|
|
3198
|
+
this.db.execute(`UPDATE edges SET properties = json_remove(properties, '$.${assignment.property}') WHERE id = ?`, [nodeId]);
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
else {
|
|
3202
|
+
// Update the property using json_set
|
|
3203
|
+
// We need to determine if it's a node or edge - for now assume node
|
|
3204
|
+
// Try nodes first, then edges
|
|
3205
|
+
const nodeResult = this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
|
|
3206
|
+
if (nodeResult.changes === 0) {
|
|
3207
|
+
// Try edges
|
|
3208
|
+
this.db.execute(`UPDATE edges SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
/**
|
|
3214
|
+
* Evaluate an object expression to get its key-value pairs
|
|
3215
|
+
*/
|
|
3216
|
+
evaluateObjectExpression(expr, params) {
|
|
3217
|
+
if (expr.type === "object" && expr.properties) {
|
|
3218
|
+
const result = {};
|
|
3219
|
+
for (const prop of expr.properties) {
|
|
3220
|
+
result[prop.key] = this.evaluateExpression(prop.value, params);
|
|
3221
|
+
}
|
|
3222
|
+
return result;
|
|
3223
|
+
}
|
|
3224
|
+
if (expr.type === "parameter") {
|
|
3225
|
+
const paramValue = params[expr.name];
|
|
3226
|
+
if (typeof paramValue === "object" && paramValue !== null) {
|
|
3227
|
+
return paramValue;
|
|
3228
|
+
}
|
|
3229
|
+
throw new Error(`Parameter ${expr.name} is not an object`);
|
|
3230
|
+
}
|
|
3231
|
+
throw new Error(`Expected object expression, got ${expr.type}`);
|
|
3232
|
+
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Execute DELETE clause with pre-resolved node/edge IDs
|
|
3235
|
+
*/
|
|
3236
|
+
executeDeleteWithResolvedIds(deleteClause, resolvedIds) {
|
|
3237
|
+
for (const variable of deleteClause.variables) {
|
|
3238
|
+
const id = resolvedIds[variable];
|
|
3239
|
+
if (!id) {
|
|
3240
|
+
throw new Error(`Cannot resolve variable for DELETE: ${variable}`);
|
|
3241
|
+
}
|
|
3242
|
+
if (deleteClause.detach) {
|
|
3243
|
+
// DETACH DELETE: First delete all edges connected to this node
|
|
3244
|
+
this.db.execute("DELETE FROM edges WHERE source_id = ? OR target_id = ?", [id, id]);
|
|
3245
|
+
}
|
|
3246
|
+
else {
|
|
3247
|
+
// Check if this is a node with connected edges
|
|
3248
|
+
const edgeCheck = this.db.execute("SELECT 1 FROM edges WHERE source_id = ? OR target_id = ? LIMIT 1", [id, id]);
|
|
3249
|
+
if (edgeCheck.rows.length > 0) {
|
|
3250
|
+
throw new Error("Cannot delete node because it still has relationships. To delete this node, you must first delete its relationships, or use DETACH DELETE.");
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
// Try deleting from nodes first
|
|
3254
|
+
const nodeResult = this.db.execute("DELETE FROM nodes WHERE id = ?", [id]);
|
|
3255
|
+
if (nodeResult.changes === 0) {
|
|
3256
|
+
// Try deleting from edges
|
|
3257
|
+
this.db.execute("DELETE FROM edges WHERE id = ?", [id]);
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
/**
|
|
3262
|
+
* Evaluate an expression to get its value
|
|
3263
|
+
* Note: For property and binary expressions that reference nodes, use evaluateExpressionWithContext
|
|
3264
|
+
*/
|
|
3265
|
+
evaluateExpression(expr, params) {
|
|
3266
|
+
switch (expr.type) {
|
|
3267
|
+
case "literal":
|
|
3268
|
+
return expr.value;
|
|
3269
|
+
case "parameter":
|
|
3270
|
+
return params[expr.name];
|
|
3271
|
+
case "function": {
|
|
3272
|
+
// Evaluate function calls (e.g., datetime(), timestamp())
|
|
3273
|
+
const funcName = expr.functionName.toUpperCase();
|
|
3274
|
+
const args = expr.args || [];
|
|
3275
|
+
return this.evaluateFunctionInProperty(funcName, args, params, {});
|
|
3276
|
+
}
|
|
3277
|
+
default:
|
|
3278
|
+
throw new Error(`Cannot evaluate expression of type ${expr.type}`);
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
/**
|
|
3282
|
+
* Evaluate an expression with access to node/edge context for property lookups
|
|
3283
|
+
*/
|
|
3284
|
+
evaluateExpressionWithContext(expr, params, resolvedIds) {
|
|
3285
|
+
switch (expr.type) {
|
|
3286
|
+
case "literal":
|
|
3287
|
+
return expr.value;
|
|
3288
|
+
case "parameter":
|
|
3289
|
+
return params[expr.name];
|
|
3290
|
+
case "property": {
|
|
3291
|
+
// Look up property from node/edge
|
|
3292
|
+
const varName = expr.variable;
|
|
3293
|
+
const propName = expr.property;
|
|
3294
|
+
const entityId = resolvedIds[varName];
|
|
3295
|
+
if (!entityId) {
|
|
3296
|
+
throw new Error(`Unknown variable: ${varName}`);
|
|
3297
|
+
}
|
|
3298
|
+
// Try nodes first
|
|
3299
|
+
const nodeResult = this.db.execute(`SELECT json_extract(properties, '$.${propName}') AS value FROM nodes WHERE id = ?`, [entityId]);
|
|
3300
|
+
if (nodeResult.rows.length > 0) {
|
|
3301
|
+
const value = nodeResult.rows[0].value;
|
|
3302
|
+
// json_extract returns JSON-encoded strings for arrays/objects
|
|
3303
|
+
// Parse if it looks like JSON
|
|
3304
|
+
if (typeof value === "string" && (value.startsWith("[") || value.startsWith("{"))) {
|
|
3305
|
+
try {
|
|
3306
|
+
return JSON.parse(value);
|
|
3307
|
+
}
|
|
3308
|
+
catch {
|
|
3309
|
+
return value;
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
return value;
|
|
3313
|
+
}
|
|
3314
|
+
// Try edges
|
|
3315
|
+
const edgeResult = this.db.execute(`SELECT json_extract(properties, '$.${propName}') AS value FROM edges WHERE id = ?`, [entityId]);
|
|
3316
|
+
if (edgeResult.rows.length > 0) {
|
|
3317
|
+
const value = edgeResult.rows[0].value;
|
|
3318
|
+
if (typeof value === "string" && (value.startsWith("[") || value.startsWith("{"))) {
|
|
3319
|
+
try {
|
|
3320
|
+
return JSON.parse(value);
|
|
3321
|
+
}
|
|
3322
|
+
catch {
|
|
3323
|
+
return value;
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
return value;
|
|
3327
|
+
}
|
|
3328
|
+
return null;
|
|
3329
|
+
}
|
|
3330
|
+
case "binary": {
|
|
3331
|
+
// Evaluate arithmetic expressions
|
|
3332
|
+
const left = this.evaluateExpressionWithContext(expr.left, params, resolvedIds);
|
|
3333
|
+
const right = this.evaluateExpressionWithContext(expr.right, params, resolvedIds);
|
|
3334
|
+
// Handle null values
|
|
3335
|
+
if (left === null || right === null) {
|
|
3336
|
+
return null;
|
|
3337
|
+
}
|
|
3338
|
+
switch (expr.operator) {
|
|
3339
|
+
case "+":
|
|
3340
|
+
// Handle list concatenation
|
|
3341
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
3342
|
+
return [...left, ...right];
|
|
3343
|
+
}
|
|
3344
|
+
if (Array.isArray(left)) {
|
|
3345
|
+
return [...left, right];
|
|
3346
|
+
}
|
|
3347
|
+
if (Array.isArray(right)) {
|
|
3348
|
+
return [left, ...right];
|
|
3349
|
+
}
|
|
3350
|
+
return left + right;
|
|
3351
|
+
case "-":
|
|
3352
|
+
return left - right;
|
|
3353
|
+
case "*":
|
|
3354
|
+
return left * right;
|
|
3355
|
+
case "/":
|
|
3356
|
+
return left / right;
|
|
3357
|
+
case "%":
|
|
3358
|
+
return left % right;
|
|
3359
|
+
case "^":
|
|
3360
|
+
return Math.pow(left, right);
|
|
3361
|
+
default:
|
|
3362
|
+
throw new Error(`Unknown binary operator: ${expr.operator}`);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
case "function": {
|
|
3366
|
+
// Evaluate function calls (e.g., datetime(), timestamp())
|
|
3367
|
+
const funcName = expr.functionName.toUpperCase();
|
|
3368
|
+
const args = expr.args || [];
|
|
3369
|
+
return this.evaluateFunctionInProperty(funcName, args, params, {});
|
|
3370
|
+
}
|
|
3371
|
+
default:
|
|
3372
|
+
throw new Error(`Cannot evaluate expression of type ${expr.type}`);
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
/**
|
|
3376
|
+
* Execute a CREATE clause with pre-resolved node IDs for referenced variables
|
|
3377
|
+
* The resolvedIds map is mutated to include newly created node IDs
|
|
3378
|
+
*/
|
|
3379
|
+
executeCreateWithResolvedIds(createClause, resolvedIds, params) {
|
|
3380
|
+
for (const pattern of createClause.patterns) {
|
|
3381
|
+
if (this.isRelationshipPattern(pattern)) {
|
|
3382
|
+
this.createRelationshipWithResolvedIds(pattern, resolvedIds, params);
|
|
3383
|
+
}
|
|
3384
|
+
else {
|
|
3385
|
+
// Simple node creation - use standard translation
|
|
3386
|
+
const nodeQuery = { clauses: [{ type: "CREATE", patterns: [pattern] }] };
|
|
3387
|
+
const translator = new Translator(params);
|
|
3388
|
+
const translation = translator.translate(nodeQuery);
|
|
3389
|
+
for (const stmt of translation.statements) {
|
|
3390
|
+
this.db.execute(stmt.sql, stmt.params);
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
/**
|
|
3396
|
+
* Create a relationship where some endpoints reference pre-existing nodes.
|
|
3397
|
+
* The resolvedIds map is mutated to include newly created node IDs.
|
|
3398
|
+
*/
|
|
3399
|
+
createRelationshipWithResolvedIds(rel, resolvedIds, params) {
|
|
3400
|
+
let sourceId;
|
|
3401
|
+
let targetId;
|
|
3402
|
+
// Determine source node ID
|
|
3403
|
+
if (rel.source.variable && resolvedIds[rel.source.variable]) {
|
|
3404
|
+
sourceId = resolvedIds[rel.source.variable];
|
|
3405
|
+
}
|
|
3406
|
+
else if (rel.source.variable && !resolvedIds[rel.source.variable] && !rel.source.label) {
|
|
3407
|
+
// Variable referenced but not found and no label - error
|
|
3408
|
+
throw new Error(`Cannot resolve source node: ${rel.source.variable}`);
|
|
3409
|
+
}
|
|
3410
|
+
else {
|
|
3411
|
+
// Create new source node (with or without label - anonymous nodes are valid)
|
|
3412
|
+
sourceId = crypto.randomUUID();
|
|
3413
|
+
const props = this.resolveProperties(rel.source.properties || {}, params);
|
|
3414
|
+
const labelJson = this.normalizeLabelToJson(rel.source.label);
|
|
3415
|
+
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [sourceId, labelJson, JSON.stringify(props)]);
|
|
3416
|
+
// Add to resolvedIds so subsequent patterns can reference it
|
|
3417
|
+
if (rel.source.variable) {
|
|
3418
|
+
resolvedIds[rel.source.variable] = sourceId;
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
// Determine target node ID
|
|
3422
|
+
if (rel.target.variable && resolvedIds[rel.target.variable]) {
|
|
3423
|
+
targetId = resolvedIds[rel.target.variable];
|
|
3424
|
+
}
|
|
3425
|
+
else if (rel.target.variable && !resolvedIds[rel.target.variable] && !rel.target.label) {
|
|
3426
|
+
// Variable referenced but not found and no label - error
|
|
3427
|
+
throw new Error(`Cannot resolve target node: ${rel.target.variable}`);
|
|
3428
|
+
}
|
|
3429
|
+
else {
|
|
3430
|
+
// Create new target node (with or without label - anonymous nodes are valid)
|
|
3431
|
+
targetId = crypto.randomUUID();
|
|
3432
|
+
const props = this.resolveProperties(rel.target.properties || {}, params);
|
|
3433
|
+
const labelJson = this.normalizeLabelToJson(rel.target.label);
|
|
3434
|
+
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [targetId, labelJson, JSON.stringify(props)]);
|
|
3435
|
+
// Add to resolvedIds so subsequent patterns can reference it
|
|
3436
|
+
if (rel.target.variable) {
|
|
3437
|
+
resolvedIds[rel.target.variable] = targetId;
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
// Swap source/target for left-directed relationships
|
|
3441
|
+
const [actualSource, actualTarget] = rel.edge.direction === "left" ? [targetId, sourceId] : [sourceId, targetId];
|
|
3442
|
+
// Create edge
|
|
3443
|
+
const edgeId = crypto.randomUUID();
|
|
3444
|
+
const edgeType = rel.edge.type || "";
|
|
3445
|
+
const edgeProps = this.resolveProperties(rel.edge.properties || {}, params);
|
|
3446
|
+
this.db.execute("INSERT INTO edges (id, type, source_id, target_id, properties) VALUES (?, ?, ?, ?, ?)", [edgeId, edgeType, actualSource, actualTarget, JSON.stringify(edgeProps)]);
|
|
3447
|
+
// Add edge to resolvedIds if it has a variable
|
|
3448
|
+
if (rel.edge.variable) {
|
|
3449
|
+
resolvedIds[rel.edge.variable] = edgeId;
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
/**
|
|
3453
|
+
* Resolve parameter references and binary expressions in properties
|
|
3454
|
+
*/
|
|
3455
|
+
resolveProperties(props, params) {
|
|
3456
|
+
// Use the unwind version with empty context for non-unwind cases
|
|
3457
|
+
return this.resolvePropertiesWithUnwind(props, params, {});
|
|
3458
|
+
}
|
|
3459
|
+
/**
|
|
3460
|
+
* Type guard for relationship patterns
|
|
3461
|
+
*/
|
|
3462
|
+
isRelationshipPattern(pattern) {
|
|
3463
|
+
return "source" in pattern && "edge" in pattern && "target" in pattern;
|
|
3464
|
+
}
|
|
3465
|
+
/**
|
|
3466
|
+
* Format raw database results into a more usable structure
|
|
3467
|
+
*/
|
|
3468
|
+
formatResults(rows, returnColumns) {
|
|
3469
|
+
return rows.map((row) => {
|
|
3470
|
+
const formatted = {};
|
|
3471
|
+
for (const [key, value] of Object.entries(row)) {
|
|
3472
|
+
formatted[key] = this.deepParseJson(value);
|
|
3473
|
+
}
|
|
3474
|
+
return formatted;
|
|
3475
|
+
});
|
|
3476
|
+
}
|
|
3477
|
+
/**
|
|
3478
|
+
* Recursively parse JSON strings in a value
|
|
3479
|
+
* Also normalizes labels (single-element arrays become strings)
|
|
3480
|
+
*/
|
|
3481
|
+
deepParseJson(value, key) {
|
|
3482
|
+
if (typeof value === "string") {
|
|
3483
|
+
try {
|
|
3484
|
+
const parsed = JSON.parse(value);
|
|
3485
|
+
// Recursively process if it's an object or array
|
|
3486
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
3487
|
+
return this.deepParseJson(parsed, key);
|
|
3488
|
+
}
|
|
3489
|
+
return parsed;
|
|
3490
|
+
}
|
|
3491
|
+
catch {
|
|
3492
|
+
// Not valid JSON, return as-is
|
|
3493
|
+
return value;
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
if (Array.isArray(value)) {
|
|
3497
|
+
// If this is a label field, normalize it (single element -> string)
|
|
3498
|
+
if (key === "label") {
|
|
3499
|
+
return value.length === 1 ? value[0] : value;
|
|
3500
|
+
}
|
|
3501
|
+
return value.map((item) => this.deepParseJson(item));
|
|
3502
|
+
}
|
|
3503
|
+
if (typeof value === "object" && value !== null) {
|
|
3504
|
+
const result = {};
|
|
3505
|
+
for (const [k, v] of Object.entries(value)) {
|
|
3506
|
+
result[k] = this.deepParseJson(v, k);
|
|
3507
|
+
}
|
|
3508
|
+
return result;
|
|
3509
|
+
}
|
|
3510
|
+
return value;
|
|
3511
|
+
}
|
|
3512
|
+
/**
|
|
3513
|
+
* Normalize label to JSON string for storage
|
|
3514
|
+
* Handles both single labels and multiple labels
|
|
3515
|
+
*/
|
|
3516
|
+
normalizeLabelToJson(label) {
|
|
3517
|
+
if (!label) {
|
|
3518
|
+
return JSON.stringify([]);
|
|
3519
|
+
}
|
|
3520
|
+
const labelArray = Array.isArray(label) ? label : [label];
|
|
3521
|
+
return JSON.stringify(labelArray);
|
|
3522
|
+
}
|
|
3523
|
+
/**
|
|
3524
|
+
* Normalize label for output (from database JSON to user-friendly format)
|
|
3525
|
+
* Single label: return string, multiple labels: return array
|
|
3526
|
+
*/
|
|
3527
|
+
normalizeLabelForOutput(label) {
|
|
3528
|
+
if (label === null || label === undefined) {
|
|
3529
|
+
return [];
|
|
3530
|
+
}
|
|
3531
|
+
// If it's already an array, normalize it
|
|
3532
|
+
if (Array.isArray(label)) {
|
|
3533
|
+
return label.length === 1 ? label[0] : label;
|
|
3534
|
+
}
|
|
3535
|
+
// If it's a JSON string, parse it
|
|
3536
|
+
if (typeof label === "string") {
|
|
3537
|
+
try {
|
|
3538
|
+
const parsed = JSON.parse(label);
|
|
3539
|
+
if (Array.isArray(parsed)) {
|
|
3540
|
+
return parsed.length === 1 ? parsed[0] : parsed;
|
|
3541
|
+
}
|
|
3542
|
+
return parsed;
|
|
3543
|
+
}
|
|
3544
|
+
catch {
|
|
3545
|
+
// Not valid JSON, return as-is
|
|
3546
|
+
return label;
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
return String(label);
|
|
3550
|
+
}
|
|
3551
|
+
/**
|
|
3552
|
+
* Generate SQL condition for label matching
|
|
3553
|
+
* Supports both single and multiple labels
|
|
3554
|
+
*/
|
|
3555
|
+
generateLabelCondition(label) {
|
|
3556
|
+
const labels = Array.isArray(label) ? label : [label];
|
|
3557
|
+
if (labels.length === 1) {
|
|
3558
|
+
return {
|
|
3559
|
+
sql: `EXISTS (SELECT 1 FROM json_each(label) WHERE value = ?)`,
|
|
3560
|
+
params: [labels[0]]
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3563
|
+
else {
|
|
3564
|
+
// Multiple labels: all must exist
|
|
3565
|
+
const conditions = labels.map(() => `EXISTS (SELECT 1 FROM json_each(label) WHERE value = ?)`);
|
|
3566
|
+
return {
|
|
3567
|
+
sql: conditions.join(" AND "),
|
|
3568
|
+
params: labels
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
// ============================================================================
|
|
3574
|
+
// Convenience function
|
|
3575
|
+
// ============================================================================
|
|
3576
|
+
export function executeQuery(db, cypher, params = {}) {
|
|
3577
|
+
return new Executor(db).execute(cypher, params);
|
|
3578
|
+
}
|
|
3579
|
+
//# sourceMappingURL=executor.js.map
|