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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +417 -0
  3. package/package.json +78 -0
  4. package/packages/nicefox-graphdb/LICENSE +21 -0
  5. package/packages/nicefox-graphdb/README.md +417 -0
  6. package/packages/nicefox-graphdb/dist/auth.d.ts +66 -0
  7. package/packages/nicefox-graphdb/dist/auth.d.ts.map +1 -0
  8. package/packages/nicefox-graphdb/dist/auth.js +148 -0
  9. package/packages/nicefox-graphdb/dist/auth.js.map +1 -0
  10. package/packages/nicefox-graphdb/dist/backup.d.ts +51 -0
  11. package/packages/nicefox-graphdb/dist/backup.d.ts.map +1 -0
  12. package/packages/nicefox-graphdb/dist/backup.js +201 -0
  13. package/packages/nicefox-graphdb/dist/backup.js.map +1 -0
  14. package/packages/nicefox-graphdb/dist/cli-helpers.d.ts +17 -0
  15. package/packages/nicefox-graphdb/dist/cli-helpers.d.ts.map +1 -0
  16. package/packages/nicefox-graphdb/dist/cli-helpers.js +121 -0
  17. package/packages/nicefox-graphdb/dist/cli-helpers.js.map +1 -0
  18. package/packages/nicefox-graphdb/dist/cli.d.ts +3 -0
  19. package/packages/nicefox-graphdb/dist/cli.d.ts.map +1 -0
  20. package/packages/nicefox-graphdb/dist/cli.js +660 -0
  21. package/packages/nicefox-graphdb/dist/cli.js.map +1 -0
  22. package/packages/nicefox-graphdb/dist/db.d.ts +118 -0
  23. package/packages/nicefox-graphdb/dist/db.d.ts.map +1 -0
  24. package/packages/nicefox-graphdb/dist/db.js +245 -0
  25. package/packages/nicefox-graphdb/dist/db.js.map +1 -0
  26. package/packages/nicefox-graphdb/dist/executor.d.ts +272 -0
  27. package/packages/nicefox-graphdb/dist/executor.d.ts.map +1 -0
  28. package/packages/nicefox-graphdb/dist/executor.js +3579 -0
  29. package/packages/nicefox-graphdb/dist/executor.js.map +1 -0
  30. package/packages/nicefox-graphdb/dist/index.d.ts +54 -0
  31. package/packages/nicefox-graphdb/dist/index.d.ts.map +1 -0
  32. package/packages/nicefox-graphdb/dist/index.js +74 -0
  33. package/packages/nicefox-graphdb/dist/index.js.map +1 -0
  34. package/packages/nicefox-graphdb/dist/local.d.ts +7 -0
  35. package/packages/nicefox-graphdb/dist/local.d.ts.map +1 -0
  36. package/packages/nicefox-graphdb/dist/local.js +115 -0
  37. package/packages/nicefox-graphdb/dist/local.js.map +1 -0
  38. package/packages/nicefox-graphdb/dist/parser.d.ts +300 -0
  39. package/packages/nicefox-graphdb/dist/parser.d.ts.map +1 -0
  40. package/packages/nicefox-graphdb/dist/parser.js +1891 -0
  41. package/packages/nicefox-graphdb/dist/parser.js.map +1 -0
  42. package/packages/nicefox-graphdb/dist/remote.d.ts +6 -0
  43. package/packages/nicefox-graphdb/dist/remote.d.ts.map +1 -0
  44. package/packages/nicefox-graphdb/dist/remote.js +87 -0
  45. package/packages/nicefox-graphdb/dist/remote.js.map +1 -0
  46. package/packages/nicefox-graphdb/dist/routes.d.ts +31 -0
  47. package/packages/nicefox-graphdb/dist/routes.d.ts.map +1 -0
  48. package/packages/nicefox-graphdb/dist/routes.js +202 -0
  49. package/packages/nicefox-graphdb/dist/routes.js.map +1 -0
  50. package/packages/nicefox-graphdb/dist/translator.d.ts +136 -0
  51. package/packages/nicefox-graphdb/dist/translator.d.ts.map +1 -0
  52. package/packages/nicefox-graphdb/dist/translator.js +4849 -0
  53. package/packages/nicefox-graphdb/dist/translator.js.map +1 -0
  54. package/packages/nicefox-graphdb/dist/types.d.ts +133 -0
  55. package/packages/nicefox-graphdb/dist/types.d.ts.map +1 -0
  56. package/packages/nicefox-graphdb/dist/types.js +21 -0
  57. 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