leangraph 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.js +2 -2
- package/dist/auth.js.map +1 -1
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +56 -9
- package/dist/db.js.map +1 -1
- package/dist/executor.d.ts +108 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +1160 -323
- package/dist/executor.js.map +1 -1
- package/dist/parser.d.ts +15 -3
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +231 -42
- package/dist/parser.js.map +1 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +3 -0
- package/dist/routes.js.map +1 -1
- package/dist/translator.d.ts +36 -0
- package/dist/translator.d.ts.map +1 -1
- package/dist/translator.js +549 -104
- package/dist/translator.js.map +1 -1
- package/package.json +1 -1
package/dist/executor.js
CHANGED
|
@@ -77,6 +77,9 @@ function cloneContext(ctx, cloneRows = true) {
|
|
|
77
77
|
export class Executor {
|
|
78
78
|
db;
|
|
79
79
|
propertyCache = new Map();
|
|
80
|
+
edgePropertyCache = new Map();
|
|
81
|
+
// Cache for full edge info (type, source_id, target_id) - populated by batchGetEdgeInfo
|
|
82
|
+
edgeInfoCache = new Map();
|
|
80
83
|
constructor(db) {
|
|
81
84
|
this.db = db;
|
|
82
85
|
}
|
|
@@ -97,13 +100,89 @@ export class Executor {
|
|
|
97
100
|
}
|
|
98
101
|
return props || {};
|
|
99
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Get edge properties from cache or parse from JSON string and cache them
|
|
105
|
+
*/
|
|
106
|
+
getEdgeProperties(edgeId, propsJson) {
|
|
107
|
+
let props = this.edgePropertyCache.get(edgeId);
|
|
108
|
+
if (!props) {
|
|
109
|
+
props = typeof propsJson === "string" ? JSON.parse(propsJson) : propsJson;
|
|
110
|
+
if (props && typeof props === "object" && !Array.isArray(props)) {
|
|
111
|
+
this.edgePropertyCache.set(edgeId, props);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// Fallback for invalid data
|
|
115
|
+
props = {};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return props || {};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Invalidate cached properties for a node or edge after it has been updated
|
|
122
|
+
*/
|
|
123
|
+
invalidatePropertyCache(id) {
|
|
124
|
+
this.propertyCache.delete(id);
|
|
125
|
+
this.edgePropertyCache.delete(id);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Extract edge ID from various representations (object with _nf_id/id, JSON string, or raw ID)
|
|
129
|
+
*/
|
|
130
|
+
extractEdgeId(rel) {
|
|
131
|
+
if (typeof rel === "object" && rel !== null) {
|
|
132
|
+
const relObj = rel;
|
|
133
|
+
return (relObj._nf_id || relObj.id);
|
|
134
|
+
}
|
|
135
|
+
else if (typeof rel === "string") {
|
|
136
|
+
try {
|
|
137
|
+
const parsed = JSON.parse(rel);
|
|
138
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
139
|
+
return (parsed._nf_id || parsed.id);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Not JSON, assume it's a raw ID
|
|
144
|
+
return rel;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Batch fetch edge info for multiple edge IDs in a single query
|
|
151
|
+
* Returns a Map from edge ID to edge info (id, type, source_id, target_id, properties)
|
|
152
|
+
*/
|
|
153
|
+
batchGetEdgeInfo(edgeIds) {
|
|
154
|
+
const result = new Map();
|
|
155
|
+
if (edgeIds.length === 0)
|
|
156
|
+
return result;
|
|
157
|
+
// Batch query with IN clause
|
|
158
|
+
const placeholders = edgeIds.map(() => '?').join(',');
|
|
159
|
+
const queryResult = this.db.execute(`SELECT id, type, source_id, target_id, properties FROM edges WHERE id IN (${placeholders})`, edgeIds);
|
|
160
|
+
for (const row of queryResult.rows) {
|
|
161
|
+
const id = row.id;
|
|
162
|
+
const type = row.type;
|
|
163
|
+
const source_id = row.source_id;
|
|
164
|
+
const target_id = row.target_id;
|
|
165
|
+
result.set(id, {
|
|
166
|
+
id,
|
|
167
|
+
type,
|
|
168
|
+
source_id,
|
|
169
|
+
target_id,
|
|
170
|
+
properties: this.getEdgeProperties(id, row.properties)
|
|
171
|
+
});
|
|
172
|
+
// Also populate edge info cache for type()/startNode()/endNode() lookups
|
|
173
|
+
this.edgeInfoCache.set(id, { type, source_id, target_id });
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
100
177
|
/**
|
|
101
178
|
* Execute a Cypher query and return formatted results
|
|
102
179
|
*/
|
|
103
180
|
execute(cypher, params = {}) {
|
|
104
181
|
const startTime = performance.now();
|
|
105
|
-
// Clear property
|
|
182
|
+
// Clear property caches at start of each query execution
|
|
106
183
|
this.propertyCache.clear();
|
|
184
|
+
this.edgePropertyCache.clear();
|
|
185
|
+
this.edgeInfoCache.clear();
|
|
107
186
|
try {
|
|
108
187
|
// 1. Parse the Cypher query
|
|
109
188
|
const parseResult = parse(cypher);
|
|
@@ -118,124 +197,94 @@ export class Executor {
|
|
|
118
197
|
},
|
|
119
198
|
};
|
|
120
199
|
}
|
|
121
|
-
// 2.
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
data: phasedResult,
|
|
128
|
-
meta: {
|
|
129
|
-
count: phasedResult.length,
|
|
130
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
// 2.1. Check for UNWIND with CREATE pattern (needs special handling)
|
|
135
|
-
const unwindCreateResult = this.tryUnwindCreateExecution(parseResult.query, params);
|
|
136
|
-
if (unwindCreateResult !== null) {
|
|
137
|
-
const endTime = performance.now();
|
|
138
|
-
return {
|
|
139
|
-
success: true,
|
|
140
|
-
data: unwindCreateResult,
|
|
141
|
-
meta: {
|
|
142
|
-
count: unwindCreateResult.length,
|
|
143
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
// 2.2. Check for UNWIND with MERGE pattern (needs special handling)
|
|
148
|
-
const unwindMergeResult = this.tryUnwindMergeExecution(parseResult.query, params);
|
|
149
|
-
if (unwindMergeResult !== null) {
|
|
150
|
-
const endTime = performance.now();
|
|
151
|
-
return {
|
|
152
|
-
success: true,
|
|
153
|
-
data: unwindMergeResult,
|
|
154
|
-
meta: {
|
|
155
|
-
count: unwindMergeResult.length,
|
|
156
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
// 2.3. Check for MATCH+WITH(COLLECT)+UNWIND+RETURN pattern (needs subquery for aggregates)
|
|
161
|
-
const collectUnwindResult = this.tryCollectUnwindExecution(parseResult.query, params);
|
|
162
|
-
if (collectUnwindResult !== null) {
|
|
163
|
-
const endTime = performance.now();
|
|
164
|
-
return {
|
|
165
|
-
success: true,
|
|
166
|
-
data: collectUnwindResult,
|
|
167
|
-
meta: {
|
|
168
|
-
count: collectUnwindResult.length,
|
|
169
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
// 2.4. Check for MATCH+WITH(COLLECT)+DELETE[expr] pattern
|
|
174
|
-
const collectDeleteResult = this.tryCollectDeleteExecution(parseResult.query, params);
|
|
175
|
-
if (collectDeleteResult !== null) {
|
|
176
|
-
const endTime = performance.now();
|
|
177
|
-
return {
|
|
178
|
-
success: true,
|
|
179
|
-
data: collectDeleteResult,
|
|
180
|
-
meta: {
|
|
181
|
-
count: collectDeleteResult.length,
|
|
182
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
183
|
-
},
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
// 2.5. Check for CREATE...RETURN pattern (needs special handling)
|
|
187
|
-
const createReturnResult = this.tryCreateReturnExecution(parseResult.query, params);
|
|
188
|
-
if (createReturnResult !== null) {
|
|
189
|
-
const endTime = performance.now();
|
|
190
|
-
return {
|
|
191
|
-
success: true,
|
|
192
|
-
data: createReturnResult,
|
|
193
|
-
meta: {
|
|
194
|
-
count: createReturnResult.length,
|
|
195
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
196
|
-
},
|
|
197
|
-
};
|
|
200
|
+
// 2. Classify query with single-pass and dispatch to appropriate handler
|
|
201
|
+
const { pattern, flags } = this.classifyQuery(parseResult.query);
|
|
202
|
+
// 3. Run semantic validations only when relevant clauses are present
|
|
203
|
+
// (avoids extra iteration overhead for simple queries)
|
|
204
|
+
if (flags.hasMerge) {
|
|
205
|
+
this.validateMergeVariables(parseResult.query);
|
|
198
206
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (mergeResult !== null) {
|
|
202
|
-
const endTime = performance.now();
|
|
203
|
-
return {
|
|
204
|
-
success: true,
|
|
205
|
-
data: mergeResult,
|
|
206
|
-
meta: {
|
|
207
|
-
count: mergeResult.length,
|
|
208
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
209
|
-
},
|
|
210
|
-
};
|
|
207
|
+
if (flags.hasSet) {
|
|
208
|
+
this.validateSetClauseValueVariables(parseResult.query, params);
|
|
211
209
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const boundRelListResult = this.tryBoundRelationshipListExecution(parseResult.query, params);
|
|
215
|
-
if (boundRelListResult !== null) {
|
|
216
|
-
const endTime = performance.now();
|
|
217
|
-
return {
|
|
218
|
-
success: true,
|
|
219
|
-
data: boundRelListResult,
|
|
220
|
-
meta: {
|
|
221
|
-
count: boundRelListResult.length,
|
|
222
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
223
|
-
},
|
|
224
|
-
};
|
|
210
|
+
if (flags.hasWith || flags.hasReturn) {
|
|
211
|
+
this.validateOrderByVariables(parseResult.query, params);
|
|
225
212
|
}
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
const multiPhaseResult = this.tryMultiPhaseExecution(parseResult.query, params);
|
|
229
|
-
if (multiPhaseResult !== null) {
|
|
213
|
+
// Helper to return successful result
|
|
214
|
+
const makeResult = (data) => {
|
|
230
215
|
const endTime = performance.now();
|
|
231
216
|
return {
|
|
232
217
|
success: true,
|
|
233
|
-
data
|
|
218
|
+
data,
|
|
234
219
|
meta: {
|
|
235
|
-
count:
|
|
220
|
+
count: data.length,
|
|
236
221
|
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
237
222
|
},
|
|
238
223
|
};
|
|
224
|
+
};
|
|
225
|
+
// Dispatch based on pattern (each try* method still validates and may return null)
|
|
226
|
+
switch (pattern) {
|
|
227
|
+
case "PHASED": {
|
|
228
|
+
const result = this.tryPhasedExecution(parseResult.query, params);
|
|
229
|
+
if (result !== null)
|
|
230
|
+
return makeResult(result);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case "UNWIND_CREATE": {
|
|
234
|
+
const result = this.tryUnwindCreateExecution(parseResult.query, params);
|
|
235
|
+
if (result !== null)
|
|
236
|
+
return makeResult(result);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case "UNWIND_MERGE": {
|
|
240
|
+
const result = this.tryUnwindMergeExecution(parseResult.query, params);
|
|
241
|
+
if (result !== null)
|
|
242
|
+
return makeResult(result);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case "COLLECT_UNWIND": {
|
|
246
|
+
const result = this.tryCollectUnwindExecution(parseResult.query, params);
|
|
247
|
+
if (result !== null)
|
|
248
|
+
return makeResult(result);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case "COLLECT_DELETE": {
|
|
252
|
+
const result = this.tryCollectDeleteExecution(parseResult.query, params);
|
|
253
|
+
if (result !== null)
|
|
254
|
+
return makeResult(result);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case "CREATE_RETURN": {
|
|
258
|
+
const result = this.tryCreateReturnExecution(parseResult.query, params);
|
|
259
|
+
if (result !== null)
|
|
260
|
+
return makeResult(result);
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case "BOUND_REL_LIST": {
|
|
264
|
+
const result = this.tryBoundRelationshipListExecution(parseResult.query, params);
|
|
265
|
+
if (result !== null)
|
|
266
|
+
return makeResult(result);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case "MERGE": {
|
|
270
|
+
const result = this.tryMergeExecution(parseResult.query, params);
|
|
271
|
+
if (result !== null)
|
|
272
|
+
return makeResult(result);
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
case "MULTI_PHASE": {
|
|
276
|
+
const result = this.tryMultiPhaseExecution(parseResult.query, params);
|
|
277
|
+
if (result !== null)
|
|
278
|
+
return makeResult(result);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case "FOREACH": {
|
|
282
|
+
const result = this.tryForeachExecution(parseResult.query, params);
|
|
283
|
+
if (result !== null)
|
|
284
|
+
return makeResult(result);
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
// STANDARD falls through to SQL translation below
|
|
239
288
|
}
|
|
240
289
|
// 3. Standard single-phase execution: Translate to SQL
|
|
241
290
|
const translator = new Translator(params);
|
|
@@ -274,6 +323,300 @@ export class Executor {
|
|
|
274
323
|
}
|
|
275
324
|
}
|
|
276
325
|
// ============================================================================
|
|
326
|
+
// Query Classification (Single-Pass)
|
|
327
|
+
// ============================================================================
|
|
328
|
+
/**
|
|
329
|
+
* Single-pass query classifier - scans clauses once to determine execution pattern
|
|
330
|
+
*
|
|
331
|
+
* This replaces the sequential try*Execution() calls that each scanned all clauses.
|
|
332
|
+
* By doing one pass and collecting all needed flags, we avoid O(n*m) complexity
|
|
333
|
+
* where n is number of clauses and m is number of try* methods.
|
|
334
|
+
*/
|
|
335
|
+
classifyQuery(query) {
|
|
336
|
+
const flags = {
|
|
337
|
+
// Clause presence
|
|
338
|
+
hasMatch: false,
|
|
339
|
+
hasOptionalMatch: false,
|
|
340
|
+
hasCreate: false,
|
|
341
|
+
hasMerge: false,
|
|
342
|
+
hasSet: false,
|
|
343
|
+
hasDelete: false,
|
|
344
|
+
hasUnwind: false,
|
|
345
|
+
hasWith: false,
|
|
346
|
+
hasReturn: false,
|
|
347
|
+
hasForeach: false,
|
|
348
|
+
// Detailed flags
|
|
349
|
+
mergeHasSetClauses: false,
|
|
350
|
+
mergeHasRelationshipPattern: false,
|
|
351
|
+
mergeHasPathExpressions: false,
|
|
352
|
+
withHasCollect: false,
|
|
353
|
+
matchHasRelationshipPattern: false,
|
|
354
|
+
hasMutations: false,
|
|
355
|
+
// Counts
|
|
356
|
+
mergeCount: 0,
|
|
357
|
+
withCount: 0,
|
|
358
|
+
matchCount: 0,
|
|
359
|
+
// Pre-categorized clauses
|
|
360
|
+
matchClauses: [],
|
|
361
|
+
createClauses: [],
|
|
362
|
+
mergeClauses: [],
|
|
363
|
+
setClauses: [],
|
|
364
|
+
deleteClauses: [],
|
|
365
|
+
withClauses: [],
|
|
366
|
+
unwindClauses: [],
|
|
367
|
+
returnClause: null,
|
|
368
|
+
// Merge details
|
|
369
|
+
firstMergeClause: null,
|
|
370
|
+
// WITH COLLECT detection
|
|
371
|
+
singleWithCollectAlias: null,
|
|
372
|
+
// Bound list detection
|
|
373
|
+
boundListVar: null,
|
|
374
|
+
boundListExprVars: [],
|
|
375
|
+
};
|
|
376
|
+
// Single pass through all clauses
|
|
377
|
+
for (const clause of query.clauses) {
|
|
378
|
+
switch (clause.type) {
|
|
379
|
+
case "MATCH":
|
|
380
|
+
flags.hasMatch = true;
|
|
381
|
+
flags.matchCount++;
|
|
382
|
+
flags.matchClauses.push(clause);
|
|
383
|
+
// Check for relationship patterns
|
|
384
|
+
if (clause.patterns.some(p => this.isRelationshipPattern(p))) {
|
|
385
|
+
flags.matchHasRelationshipPattern = true;
|
|
386
|
+
}
|
|
387
|
+
break;
|
|
388
|
+
case "OPTIONAL_MATCH":
|
|
389
|
+
flags.hasOptionalMatch = true;
|
|
390
|
+
flags.matchCount++;
|
|
391
|
+
// OPTIONAL_MATCH is stored as MatchClause with isOptional flag
|
|
392
|
+
flags.matchClauses.push(clause);
|
|
393
|
+
break;
|
|
394
|
+
case "CREATE":
|
|
395
|
+
flags.hasCreate = true;
|
|
396
|
+
flags.createClauses.push(clause);
|
|
397
|
+
break;
|
|
398
|
+
case "MERGE": {
|
|
399
|
+
flags.hasMerge = true;
|
|
400
|
+
flags.mergeCount++;
|
|
401
|
+
flags.mergeClauses.push(clause);
|
|
402
|
+
if (!flags.firstMergeClause) {
|
|
403
|
+
flags.firstMergeClause = clause;
|
|
404
|
+
}
|
|
405
|
+
// Check for ON CREATE SET / ON MATCH SET
|
|
406
|
+
if (clause.onCreateSet || clause.onMatchSet) {
|
|
407
|
+
flags.mergeHasSetClauses = true;
|
|
408
|
+
}
|
|
409
|
+
// Check for relationship patterns
|
|
410
|
+
if (clause.patterns.some(p => this.isRelationshipPattern(p))) {
|
|
411
|
+
flags.mergeHasRelationshipPattern = true;
|
|
412
|
+
}
|
|
413
|
+
// Check for path expressions
|
|
414
|
+
if (clause.pathExpressions && clause.pathExpressions.length > 0) {
|
|
415
|
+
flags.mergeHasPathExpressions = true;
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
case "SET":
|
|
420
|
+
flags.hasSet = true;
|
|
421
|
+
flags.setClauses.push(clause);
|
|
422
|
+
break;
|
|
423
|
+
case "DELETE":
|
|
424
|
+
flags.hasDelete = true;
|
|
425
|
+
flags.deleteClauses.push(clause);
|
|
426
|
+
break;
|
|
427
|
+
case "UNWIND":
|
|
428
|
+
flags.hasUnwind = true;
|
|
429
|
+
flags.unwindClauses.push(clause);
|
|
430
|
+
break;
|
|
431
|
+
case "WITH": {
|
|
432
|
+
flags.hasWith = true;
|
|
433
|
+
flags.withCount++;
|
|
434
|
+
flags.withClauses.push(clause);
|
|
435
|
+
// Check for COLLECT function
|
|
436
|
+
if (clause.items.length === 1) {
|
|
437
|
+
const item = clause.items[0];
|
|
438
|
+
if (item.expression.type === "function" &&
|
|
439
|
+
item.expression.functionName?.toUpperCase() === "COLLECT" &&
|
|
440
|
+
item.alias) {
|
|
441
|
+
flags.withHasCollect = true;
|
|
442
|
+
flags.singleWithCollectAlias = item.alias;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Check for list literal pattern [r1, r2] for bound relationship list detection
|
|
446
|
+
for (const item of clause.items) {
|
|
447
|
+
if (item.alias && item.expression.type === "function" &&
|
|
448
|
+
item.expression.functionName === "LIST" && item.expression.args) {
|
|
449
|
+
flags.boundListVar = item.alias;
|
|
450
|
+
flags.boundListExprVars = [];
|
|
451
|
+
for (const elem of item.expression.args) {
|
|
452
|
+
if (elem.type === "variable" && elem.variable) {
|
|
453
|
+
flags.boundListExprVars.push(elem.variable);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
case "RETURN":
|
|
461
|
+
flags.hasReturn = true;
|
|
462
|
+
flags.returnClause = clause;
|
|
463
|
+
break;
|
|
464
|
+
case "FOREACH":
|
|
465
|
+
flags.hasForeach = true;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Derived flags
|
|
470
|
+
flags.hasMutations = flags.hasCreate || flags.hasSet || flags.hasDelete;
|
|
471
|
+
// Determine pattern using the collected flags
|
|
472
|
+
const pattern = this.determineQueryPattern(query, flags);
|
|
473
|
+
return { pattern, flags };
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Determine execution pattern from collected flags
|
|
477
|
+
* Order matters - patterns are checked in priority order
|
|
478
|
+
*/
|
|
479
|
+
determineQueryPattern(query, flags) {
|
|
480
|
+
// 0. FOREACH - Queries with FOREACH clause (checked first, high priority)
|
|
481
|
+
if (flags.hasForeach) {
|
|
482
|
+
return "FOREACH";
|
|
483
|
+
}
|
|
484
|
+
// 1. PHASED - Complex multi-phase queries with phase boundaries
|
|
485
|
+
const phases = this.detectPhases(query);
|
|
486
|
+
const needsMergePhasedExecution = (flags.hasMatch || flags.hasCreate) &&
|
|
487
|
+
flags.hasMerge &&
|
|
488
|
+
!flags.mergeHasSetClauses;
|
|
489
|
+
const needsCreateWithPhasedExecution = flags.hasCreate && flags.hasWith && !flags.hasMatch;
|
|
490
|
+
if (phases.length > 1 || needsMergePhasedExecution || needsCreateWithPhasedExecution) {
|
|
491
|
+
return "PHASED";
|
|
492
|
+
}
|
|
493
|
+
// 2. UNWIND_CREATE - UNWIND + CREATE without MATCH
|
|
494
|
+
if (flags.hasUnwind && flags.hasCreate && !flags.hasMatch) {
|
|
495
|
+
return "UNWIND_CREATE";
|
|
496
|
+
}
|
|
497
|
+
// 3. UNWIND_MERGE - UNWIND + MERGE without MATCH or CREATE
|
|
498
|
+
if (flags.hasUnwind && flags.hasMerge && !flags.hasMatch && !flags.hasCreate) {
|
|
499
|
+
return "UNWIND_MERGE";
|
|
500
|
+
}
|
|
501
|
+
// 4. COLLECT_UNWIND - MATCH + WITH(COLLECT) + UNWIND + RETURN
|
|
502
|
+
if (flags.hasMatch && flags.withHasCollect && flags.hasUnwind && flags.hasReturn) {
|
|
503
|
+
// Additional validation: WITH must have exactly one COLLECT item
|
|
504
|
+
// and UNWIND must reference the COLLECT alias
|
|
505
|
+
const lastUnwind = flags.unwindClauses[flags.unwindClauses.length - 1];
|
|
506
|
+
if (lastUnwind &&
|
|
507
|
+
lastUnwind.expression.type === "variable" &&
|
|
508
|
+
lastUnwind.expression.variable === flags.singleWithCollectAlias) {
|
|
509
|
+
return "COLLECT_UNWIND";
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// 5. COLLECT_DELETE - MATCH + WITH(COLLECT) + DELETE
|
|
513
|
+
if (flags.hasMatch && flags.withHasCollect && flags.hasDelete &&
|
|
514
|
+
flags.deleteClauses[0]?.expressions && flags.deleteClauses[0].expressions.length > 0) {
|
|
515
|
+
return "COLLECT_DELETE";
|
|
516
|
+
}
|
|
517
|
+
// 6. CREATE_RETURN - CREATE + RETURN without MATCH or MERGE
|
|
518
|
+
if (flags.hasCreate && flags.hasReturn && !flags.hasMatch && !flags.hasMerge) {
|
|
519
|
+
return "CREATE_RETURN";
|
|
520
|
+
}
|
|
521
|
+
// 7. BOUND_REL_LIST - MATCH + WITH + MATCH with bound list pattern
|
|
522
|
+
if (flags.matchCount >= 2 && flags.hasWith && flags.boundListVar &&
|
|
523
|
+
flags.boundListExprVars.length > 0 && flags.hasReturn) {
|
|
524
|
+
return "BOUND_REL_LIST";
|
|
525
|
+
}
|
|
526
|
+
// 8. MERGE - MERGE with special handling needs
|
|
527
|
+
if (flags.hasMerge) {
|
|
528
|
+
const hasSpecialMerge = flags.mergeHasRelationshipPattern ||
|
|
529
|
+
flags.mergeHasSetClauses ||
|
|
530
|
+
flags.mergeHasPathExpressions ||
|
|
531
|
+
flags.hasReturn ||
|
|
532
|
+
flags.mergeCount > 1;
|
|
533
|
+
if (hasSpecialMerge) {
|
|
534
|
+
return "MERGE";
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// 10. MULTI_PHASE - MATCH with mutations (CREATE/SET/DELETE)
|
|
538
|
+
if (flags.hasMatch && flags.hasMutations) {
|
|
539
|
+
return "MULTI_PHASE";
|
|
540
|
+
}
|
|
541
|
+
// 10. UNWIND + MATCH where MATCH references UNWIND variables
|
|
542
|
+
// This handles: UNWIND list AS x MATCH (n {prop: x}) RETURN n
|
|
543
|
+
// The MATCH property constraints reference UNWIND variables
|
|
544
|
+
if (flags.hasUnwind && flags.hasMatch) {
|
|
545
|
+
const unwindAliases = new Set();
|
|
546
|
+
for (const clause of query.clauses) {
|
|
547
|
+
if (clause.type === "UNWIND") {
|
|
548
|
+
unwindAliases.add(clause.alias);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// Check if any MATCH pattern references UNWIND variables
|
|
552
|
+
for (const clause of query.clauses) {
|
|
553
|
+
if (clause.type === "MATCH" || clause.type === "OPTIONAL_MATCH") {
|
|
554
|
+
for (const pattern of clause.patterns) {
|
|
555
|
+
if (this.patternReferencesVariables(pattern, unwindAliases)) {
|
|
556
|
+
return "PHASED";
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// 11. STANDARD - Default SQL translation
|
|
563
|
+
return "STANDARD";
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Check if a pattern references any of the given variables
|
|
567
|
+
*/
|
|
568
|
+
patternReferencesVariables(pattern, variables) {
|
|
569
|
+
// Check if this is a relationship pattern
|
|
570
|
+
if ('source' in pattern && 'edge' in pattern && 'target' in pattern) {
|
|
571
|
+
const relPattern = pattern;
|
|
572
|
+
return this.nodePropertiesReferenceVariables(relPattern.source, variables) ||
|
|
573
|
+
this.nodePropertiesReferenceVariables(relPattern.target, variables) ||
|
|
574
|
+
this.edgePropertiesReferenceVariables(relPattern.edge, variables);
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
return this.nodePropertiesReferenceVariables(pattern, variables);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Check if node properties reference any of the given variables
|
|
582
|
+
*/
|
|
583
|
+
nodePropertiesReferenceVariables(node, variables) {
|
|
584
|
+
if (!node.properties)
|
|
585
|
+
return false;
|
|
586
|
+
for (const value of Object.values(node.properties)) {
|
|
587
|
+
if (this.propertyValueReferencesVariables(value, variables)) {
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Check if edge properties reference any of the given variables
|
|
595
|
+
*/
|
|
596
|
+
edgePropertiesReferenceVariables(edge, variables) {
|
|
597
|
+
if (!edge.properties)
|
|
598
|
+
return false;
|
|
599
|
+
for (const value of Object.values(edge.properties)) {
|
|
600
|
+
if (this.propertyValueReferencesVariables(value, variables)) {
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Check if a property value references any of the given variables
|
|
608
|
+
*/
|
|
609
|
+
propertyValueReferencesVariables(value, variables) {
|
|
610
|
+
if (value === null || typeof value !== 'object')
|
|
611
|
+
return false;
|
|
612
|
+
if ('type' in value) {
|
|
613
|
+
if (value.type === 'variable' && 'name' in value && typeof value.name === 'string') {
|
|
614
|
+
return variables.has(value.name);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
// ============================================================================
|
|
277
620
|
// Phase-Based Execution
|
|
278
621
|
// ============================================================================
|
|
279
622
|
/**
|
|
@@ -288,12 +631,8 @@ export class Executor {
|
|
|
288
631
|
*/
|
|
289
632
|
tryPhasedExecution(query, params) {
|
|
290
633
|
const phases = this.detectPhases(query);
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
// Semantic validation: SET expressions cannot reference undefined variables
|
|
294
|
-
this.validateSetClauseValueVariables(query, params);
|
|
295
|
-
// Semantic validation: ORDER BY expressions cannot reference undefined or out-of-scope variables
|
|
296
|
-
this.validateOrderByVariables(query, params);
|
|
634
|
+
// Note: Semantic validations (validateMergeVariables, validateSetClauseValueVariables,
|
|
635
|
+
// validateOrderByVariables) are now called in execute() before classification
|
|
297
636
|
// Check if we need phased execution for MATCH + MERGE combinations
|
|
298
637
|
// These need special handling for proper Cartesian product semantics
|
|
299
638
|
// BUT: If MERGE has ON CREATE SET or ON MATCH SET, let tryMergeExecution handle it
|
|
@@ -785,6 +1124,30 @@ export class Executor {
|
|
|
785
1124
|
break;
|
|
786
1125
|
}
|
|
787
1126
|
}
|
|
1127
|
+
// Track UNWIND alias as a known variable
|
|
1128
|
+
knownVariables.add(clause.alias);
|
|
1129
|
+
}
|
|
1130
|
+
// Check if MATCH pattern properties reference UNWIND variables - needs phase boundary
|
|
1131
|
+
// This handles: UNWIND list AS x MATCH (n {prop: x}) RETURN n
|
|
1132
|
+
// The MATCH needs to be executed for each UNWIND row, which requires phased execution
|
|
1133
|
+
if ((clause.type === "MATCH" || clause.type === "OPTIONAL_MATCH") && i > 0) {
|
|
1134
|
+
// Collect all UNWIND aliases from previous clauses
|
|
1135
|
+
const unwindAliases = new Set();
|
|
1136
|
+
for (let j = 0; j < i; j++) {
|
|
1137
|
+
const prevClause = clauses[j];
|
|
1138
|
+
if (prevClause.type === "UNWIND") {
|
|
1139
|
+
unwindAliases.add(prevClause.alias);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
// Check if MATCH pattern properties reference any UNWIND variables
|
|
1143
|
+
if (unwindAliases.size > 0) {
|
|
1144
|
+
for (const pattern of clause.patterns) {
|
|
1145
|
+
if (this.patternReferencesVariables(pattern, unwindAliases)) {
|
|
1146
|
+
needsNewPhase = true;
|
|
1147
|
+
break;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
788
1151
|
}
|
|
789
1152
|
// Check if MATCH after a WITH with aggregates - needs phase boundary
|
|
790
1153
|
// This handles patterns like:
|
|
@@ -1457,12 +1820,12 @@ export class Executor {
|
|
|
1457
1820
|
// Build query to find existing matching nodes
|
|
1458
1821
|
const conditions = [];
|
|
1459
1822
|
const conditionParams = [];
|
|
1460
|
-
// Label condition
|
|
1823
|
+
// Label condition (uses primary label index with fallback for secondary labels)
|
|
1461
1824
|
if (pattern.label) {
|
|
1462
1825
|
const labels = Array.isArray(pattern.label) ? pattern.label : [pattern.label];
|
|
1463
1826
|
for (const label of labels) {
|
|
1464
|
-
conditions.push(`EXISTS (SELECT 1 FROM json_each(label) WHERE value = ?)`);
|
|
1465
|
-
conditionParams.push(label);
|
|
1827
|
+
conditions.push(`(json_extract(label, '$[0]') = ? OR EXISTS (SELECT 1 FROM json_each(label) WHERE value = ? AND json_extract(label, '$[0]') != ?))`);
|
|
1828
|
+
conditionParams.push(label, label, label);
|
|
1466
1829
|
}
|
|
1467
1830
|
}
|
|
1468
1831
|
// Property conditions
|
|
@@ -1575,9 +1938,7 @@ export class Executor {
|
|
|
1575
1938
|
for (const edgeRow of findResult.rows) {
|
|
1576
1939
|
const outputRow = new Map(inputRow);
|
|
1577
1940
|
if (pattern.edge.variable) {
|
|
1578
|
-
const props =
|
|
1579
|
-
? JSON.parse(edgeRow.properties)
|
|
1580
|
-
: edgeRow.properties;
|
|
1941
|
+
const props = this.getEdgeProperties(edgeRow.id, edgeRow.properties);
|
|
1581
1942
|
// Include _nf_start and _nf_end for startNode() and endNode() functions
|
|
1582
1943
|
// Use the actual source/target from the found edge (may be reversed for undirected)
|
|
1583
1944
|
outputRow.set(pattern.edge.variable, {
|
|
@@ -2014,7 +2375,9 @@ export class Executor {
|
|
|
2014
2375
|
// Get variables referenced in WHERE that are context variables
|
|
2015
2376
|
const whereReferencesContext = clause.where ?
|
|
2016
2377
|
this.whereReferencesContextVars(clause.where, contextVarNames, introducedVars) : false;
|
|
2017
|
-
if (
|
|
2378
|
+
// Check if pattern properties reference context variables (e.g., MATCH (n {prop: x}) where x is from UNWIND)
|
|
2379
|
+
const patternPropertiesReferenceContext = clause.patterns.some(pattern => this.patternReferencesVariables(pattern, contextVarNames));
|
|
2380
|
+
if (boundVars.size > 0 || whereReferencesContext || patternPropertiesReferenceContext) {
|
|
2018
2381
|
// For complex patterns (multi-hop, anonymous nodes), use SQL translation with
|
|
2019
2382
|
// constraints for bound variables. This handles patterns like:
|
|
2020
2383
|
// WITH me, you MATCH (me)-[r1:ATE]->()<-[r2:ATE]-(you)
|
|
@@ -2153,9 +2516,7 @@ export class Executor {
|
|
|
2153
2516
|
for (const row of edgeResult.rows) {
|
|
2154
2517
|
const matchRow = new Map();
|
|
2155
2518
|
if (edgeVar) {
|
|
2156
|
-
const edgeProps =
|
|
2157
|
-
? JSON.parse(row.properties)
|
|
2158
|
-
: row.properties;
|
|
2519
|
+
const edgeProps = this.getEdgeProperties(row.id, row.properties);
|
|
2159
2520
|
matchRow.set(edgeVar, { ...edgeProps, _nf_id: row.id });
|
|
2160
2521
|
}
|
|
2161
2522
|
results.push(matchRow);
|
|
@@ -2190,9 +2551,55 @@ export class Executor {
|
|
|
2190
2551
|
// Transform the clause to substitute context variable references with parameters
|
|
2191
2552
|
// This handles cases like: WITH x AS foo MATCH (n) WHERE n.id = foo
|
|
2192
2553
|
const transformedClause = this.transformClauseForContext(clause, contextVarNames);
|
|
2554
|
+
// For OPTIONAL MATCH with bound source variable BUT new target variable, we need
|
|
2555
|
+
// to prepend a regular MATCH clause to bind the source first. This ensures the
|
|
2556
|
+
// translator includes the source node in the FROM clause.
|
|
2557
|
+
// e.g., "OPTIONAL MATCH (n)-[r]->() WHERE id(n) = ?" becomes
|
|
2558
|
+
// "MATCH (n) WHERE id(n) = ? OPTIONAL MATCH (n)-[r]->()"
|
|
2559
|
+
// Note: When BOTH source and target are bound, we don't need prefix MATCHes - the
|
|
2560
|
+
// WHERE constraints added by transformClauseForContext are sufficient.
|
|
2561
|
+
const prefixMatchClauses = [];
|
|
2562
|
+
if (clause.type === "OPTIONAL_MATCH") {
|
|
2563
|
+
for (const pattern of clause.patterns) {
|
|
2564
|
+
if ('source' in pattern && 'edge' in pattern && 'target' in pattern) {
|
|
2565
|
+
const relPattern = pattern;
|
|
2566
|
+
const sourceVar = relPattern.source?.variable;
|
|
2567
|
+
const targetVar = relPattern.target?.variable;
|
|
2568
|
+
const sourceIsBound = sourceVar && contextVarNames.has(sourceVar);
|
|
2569
|
+
const targetIsBound = targetVar && contextVarNames.has(targetVar);
|
|
2570
|
+
// Only add prefix MATCH when source is bound but target is new
|
|
2571
|
+
// This handles: WITH n OPTIONAL MATCH (n)-[r]->() where n is bound, target is new
|
|
2572
|
+
if (sourceIsBound && !targetIsBound) {
|
|
2573
|
+
const nodeId = this.extractNodeId(inputRow.get(sourceVar));
|
|
2574
|
+
if (nodeId) {
|
|
2575
|
+
prefixMatchClauses.push({
|
|
2576
|
+
type: "MATCH",
|
|
2577
|
+
patterns: [{
|
|
2578
|
+
variable: sourceVar
|
|
2579
|
+
}],
|
|
2580
|
+
where: {
|
|
2581
|
+
type: "comparison",
|
|
2582
|
+
left: {
|
|
2583
|
+
type: "function",
|
|
2584
|
+
functionName: "ID",
|
|
2585
|
+
args: [{ type: "variable", variable: sourceVar }]
|
|
2586
|
+
},
|
|
2587
|
+
operator: "=",
|
|
2588
|
+
right: {
|
|
2589
|
+
type: "parameter",
|
|
2590
|
+
name: `_ctx_${sourceVar}`
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2193
2599
|
// Build a MATCH + RETURN query for all pattern variables
|
|
2194
2600
|
const matchQuery = {
|
|
2195
2601
|
clauses: [
|
|
2602
|
+
...prefixMatchClauses,
|
|
2196
2603
|
transformedClause,
|
|
2197
2604
|
{
|
|
2198
2605
|
type: "RETURN",
|
|
@@ -2201,9 +2608,17 @@ export class Executor {
|
|
|
2201
2608
|
],
|
|
2202
2609
|
};
|
|
2203
2610
|
// Create params with context values prefixed with _ctx_
|
|
2611
|
+
// For node variables, extract the node ID for use in id() comparisons
|
|
2204
2612
|
const mergedParams = { ...params };
|
|
2205
2613
|
for (const [key, value] of inputRow) {
|
|
2206
|
-
|
|
2614
|
+
// If value is a node object, extract its ID for the parameter
|
|
2615
|
+
const nodeId = this.extractNodeId(value);
|
|
2616
|
+
if (nodeId) {
|
|
2617
|
+
mergedParams[`_ctx_${key}`] = nodeId;
|
|
2618
|
+
}
|
|
2619
|
+
else {
|
|
2620
|
+
mergedParams[`_ctx_${key}`] = value;
|
|
2621
|
+
}
|
|
2207
2622
|
}
|
|
2208
2623
|
// Translate to SQL with merged parameters
|
|
2209
2624
|
const translator = new Translator(mergedParams);
|
|
@@ -2268,19 +2683,126 @@ export class Executor {
|
|
|
2268
2683
|
* Transform a MATCH clause to substitute context variable references with parameter references.
|
|
2269
2684
|
* This converts WHERE conditions like `n.id = foo` (where foo is from context)
|
|
2270
2685
|
* to `n.id = $_ctx_foo` so the translator can handle it.
|
|
2686
|
+
* Also transforms pattern properties like {name: foo} to use context parameters.
|
|
2687
|
+
*
|
|
2688
|
+
* For bound variables that appear as sources in relationship patterns, we add
|
|
2689
|
+
* WHERE constraints like `id(n) = $_ctx_n` so the translator knows to include
|
|
2690
|
+
* the node in the FROM clause.
|
|
2271
2691
|
*/
|
|
2272
2692
|
transformClauseForContext(clause, contextVars) {
|
|
2273
|
-
if (!clause.where) {
|
|
2274
|
-
return clause;
|
|
2275
|
-
}
|
|
2276
2693
|
// Deep clone the clause
|
|
2277
2694
|
const transformed = JSON.parse(JSON.stringify(clause));
|
|
2695
|
+
// Transform pattern properties to use parameter references for context variables
|
|
2696
|
+
// This handles MATCH (n:Label {prop: contextVar}) from UNWIND
|
|
2697
|
+
for (const pattern of transformed.patterns) {
|
|
2698
|
+
this.transformPatternPropertiesForContext(pattern, contextVars);
|
|
2699
|
+
}
|
|
2278
2700
|
// Transform WHERE condition to use parameter references for context variables
|
|
2279
2701
|
if (transformed.where) {
|
|
2280
2702
|
transformed.where = this.transformWhereForContext(transformed.where, contextVars);
|
|
2281
2703
|
}
|
|
2704
|
+
// For OPTIONAL MATCH with bound source but new target, add WHERE constraints
|
|
2705
|
+
// to ensure the source node is properly included in the SQL FROM clause.
|
|
2706
|
+
// This handles: WITH n OPTIONAL MATCH (n)-[r:REL]->() where n is bound but target is new
|
|
2707
|
+
// Note: When BOTH source and target are bound, we don't add WHERE constraints here
|
|
2708
|
+
// because the translator can handle that case and adding constraints causes bad SQL
|
|
2709
|
+
// (referencing tables before they're joined).
|
|
2710
|
+
const boundSourceVars = new Set();
|
|
2711
|
+
for (const pattern of transformed.patterns) {
|
|
2712
|
+
if ('source' in pattern && 'edge' in pattern && 'target' in pattern) {
|
|
2713
|
+
const relPattern = pattern;
|
|
2714
|
+
const sourceVar = relPattern.source?.variable;
|
|
2715
|
+
const targetVar = relPattern.target?.variable;
|
|
2716
|
+
const sourceIsBound = sourceVar && contextVars.has(sourceVar);
|
|
2717
|
+
const targetIsBound = targetVar && contextVars.has(targetVar);
|
|
2718
|
+
// Only add constraint when source is bound but target is new
|
|
2719
|
+
if (sourceIsBound && !targetIsBound) {
|
|
2720
|
+
boundSourceVars.add(sourceVar);
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
// Add WHERE constraints for bound source variables: id(varName) = $_ctx_varName
|
|
2725
|
+
if (boundSourceVars.size > 0) {
|
|
2726
|
+
const idConditions = [];
|
|
2727
|
+
for (const varName of boundSourceVars) {
|
|
2728
|
+
idConditions.push({
|
|
2729
|
+
type: "comparison",
|
|
2730
|
+
left: {
|
|
2731
|
+
type: "function",
|
|
2732
|
+
functionName: "ID",
|
|
2733
|
+
args: [{ type: "variable", variable: varName }]
|
|
2734
|
+
},
|
|
2735
|
+
operator: "=",
|
|
2736
|
+
right: {
|
|
2737
|
+
type: "parameter",
|
|
2738
|
+
name: `_ctx_${varName}`
|
|
2739
|
+
}
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
// Combine with existing WHERE using conditions array
|
|
2743
|
+
if (transformed.where) {
|
|
2744
|
+
// Combine all conditions into a single AND clause
|
|
2745
|
+
transformed.where = {
|
|
2746
|
+
type: "and",
|
|
2747
|
+
conditions: [...idConditions, transformed.where]
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
else {
|
|
2751
|
+
// Just use the ID conditions
|
|
2752
|
+
if (idConditions.length === 1) {
|
|
2753
|
+
transformed.where = idConditions[0];
|
|
2754
|
+
}
|
|
2755
|
+
else {
|
|
2756
|
+
transformed.where = {
|
|
2757
|
+
type: "and",
|
|
2758
|
+
conditions: idConditions
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2282
2763
|
return transformed;
|
|
2283
2764
|
}
|
|
2765
|
+
/**
|
|
2766
|
+
* Transform pattern properties to use context parameter references
|
|
2767
|
+
*/
|
|
2768
|
+
transformPatternPropertiesForContext(pattern, contextVars) {
|
|
2769
|
+
// Check if this is a relationship pattern with source/edge/target
|
|
2770
|
+
if ('source' in pattern && 'edge' in pattern && 'target' in pattern) {
|
|
2771
|
+
this.transformNodePropertiesForContext(pattern.source, contextVars);
|
|
2772
|
+
this.transformNodePropertiesForContext(pattern.target, contextVars);
|
|
2773
|
+
if (pattern.edge.properties) {
|
|
2774
|
+
this.transformPropertiesMapForContext(pattern.edge.properties, contextVars);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
else {
|
|
2778
|
+
// Simple node pattern
|
|
2779
|
+
this.transformNodePropertiesForContext(pattern, contextVars);
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
/**
|
|
2783
|
+
* Transform node properties to use context parameter references
|
|
2784
|
+
*/
|
|
2785
|
+
transformNodePropertiesForContext(node, contextVars) {
|
|
2786
|
+
if (node.properties) {
|
|
2787
|
+
this.transformPropertiesMapForContext(node.properties, contextVars);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
/**
|
|
2791
|
+
* Transform a properties map to use context parameter references
|
|
2792
|
+
*/
|
|
2793
|
+
transformPropertiesMapForContext(properties, contextVars) {
|
|
2794
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
2795
|
+
if (value && typeof value === 'object' && 'type' in value &&
|
|
2796
|
+
value.type === "variable" && 'name' in value &&
|
|
2797
|
+
typeof value.name === 'string' && contextVars.has(value.name)) {
|
|
2798
|
+
// Replace with parameter reference
|
|
2799
|
+
properties[key] = {
|
|
2800
|
+
type: "parameter",
|
|
2801
|
+
name: `_ctx_${value.name}`,
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2284
2806
|
/**
|
|
2285
2807
|
* Transform a WHERE condition to substitute context variable references with parameter references
|
|
2286
2808
|
*/
|
|
@@ -2437,56 +2959,24 @@ export class Executor {
|
|
|
2437
2959
|
}
|
|
2438
2960
|
// Follow the sequence of relationships to find the path endpoints
|
|
2439
2961
|
// Each relationship in the list is either an edge object or edge ID
|
|
2962
|
+
// Batch collect all edge IDs first
|
|
2963
|
+
const edgeIds = [];
|
|
2964
|
+
for (const rel of relList) {
|
|
2965
|
+
const edgeId = this.extractEdgeId(rel);
|
|
2966
|
+
if (edgeId)
|
|
2967
|
+
edgeIds.push(edgeId);
|
|
2968
|
+
}
|
|
2969
|
+
// Batch fetch all edge info in a single query
|
|
2970
|
+
const edgeInfoMap = this.batchGetEdgeInfo(edgeIds);
|
|
2440
2971
|
let currentNodeId = null;
|
|
2441
2972
|
let firstNodeId = null;
|
|
2442
2973
|
let lastNodeId = null;
|
|
2443
2974
|
let valid = true;
|
|
2444
2975
|
for (let i = 0; i < relList.length; i++) {
|
|
2445
2976
|
const rel = relList[i];
|
|
2446
|
-
// Extract edge
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
// Edge object from MATCH - may have _nf_id instead of id
|
|
2450
|
-
const relObj = rel;
|
|
2451
|
-
const edgeId = (relObj._nf_id || relObj.id);
|
|
2452
|
-
if (edgeId) {
|
|
2453
|
-
// Look up the edge to get source/target
|
|
2454
|
-
const edgeResult = this.db.execute("SELECT id, source_id, target_id FROM edges WHERE id = ?", [edgeId]);
|
|
2455
|
-
if (edgeResult.rows.length > 0) {
|
|
2456
|
-
const row = edgeResult.rows[0];
|
|
2457
|
-
edgeInfo = {
|
|
2458
|
-
id: row.id,
|
|
2459
|
-
source_id: row.source_id,
|
|
2460
|
-
target_id: row.target_id
|
|
2461
|
-
};
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2464
|
-
}
|
|
2465
|
-
else if (typeof rel === "string") {
|
|
2466
|
-
// Could be a JSON string like '{"_nf_id":"uuid"}' or a raw UUID string
|
|
2467
|
-
let edgeId = null;
|
|
2468
|
-
try {
|
|
2469
|
-
const parsed = JSON.parse(rel);
|
|
2470
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
2471
|
-
edgeId = (parsed._nf_id || parsed.id);
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
catch {
|
|
2475
|
-
// Not JSON, assume it's a raw ID
|
|
2476
|
-
edgeId = rel;
|
|
2477
|
-
}
|
|
2478
|
-
if (edgeId) {
|
|
2479
|
-
const edgeResult = this.db.execute("SELECT id, source_id, target_id FROM edges WHERE id = ?", [edgeId]);
|
|
2480
|
-
if (edgeResult.rows.length > 0) {
|
|
2481
|
-
const row = edgeResult.rows[0];
|
|
2482
|
-
edgeInfo = {
|
|
2483
|
-
id: row.id,
|
|
2484
|
-
source_id: row.source_id,
|
|
2485
|
-
target_id: row.target_id
|
|
2486
|
-
};
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
}
|
|
2977
|
+
// Extract edge ID and get info from batch result
|
|
2978
|
+
const edgeId = this.extractEdgeId(rel);
|
|
2979
|
+
const edgeInfo = edgeId ? edgeInfoMap.get(edgeId) : null;
|
|
2490
2980
|
if (!edgeInfo) {
|
|
2491
2981
|
valid = false;
|
|
2492
2982
|
break;
|
|
@@ -2564,12 +3054,8 @@ export class Executor {
|
|
|
2564
3054
|
const firstNode = firstNodeResult.rows[0];
|
|
2565
3055
|
const lastNode = lastNodeResult.rows[0];
|
|
2566
3056
|
// Format nodes like the translator does (with _nf_id embedded)
|
|
2567
|
-
const firstProps =
|
|
2568
|
-
|
|
2569
|
-
: firstNode.properties;
|
|
2570
|
-
const lastProps = typeof lastNode.properties === "string"
|
|
2571
|
-
? JSON.parse(lastNode.properties)
|
|
2572
|
-
: lastNode.properties;
|
|
3057
|
+
const firstProps = this.getNodeProperties(firstNode.id, firstNode.properties);
|
|
3058
|
+
const lastProps = this.getNodeProperties(lastNode.id, lastNode.properties);
|
|
2573
3059
|
outputRow.set(boundInfo.sourceVar, { ...firstProps, _nf_id: firstNode.id });
|
|
2574
3060
|
outputRow.set(boundInfo.targetVar, { ...lastProps, _nf_id: lastNode.id });
|
|
2575
3061
|
newRows.push(outputRow);
|
|
@@ -2743,6 +3229,8 @@ export class Executor {
|
|
|
2743
3229
|
if (assignment.property && assignment.value) {
|
|
2744
3230
|
const value = this.evaluateExpressionInRow(assignment.value, row, params);
|
|
2745
3231
|
this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
|
|
3232
|
+
// Invalidate cache after UPDATE
|
|
3233
|
+
this.invalidatePropertyCache(nodeId);
|
|
2746
3234
|
}
|
|
2747
3235
|
}
|
|
2748
3236
|
}
|
|
@@ -2754,7 +3242,9 @@ export class Executor {
|
|
|
2754
3242
|
executeDeleteClause(clause, context, params) {
|
|
2755
3243
|
for (const row of context.rows) {
|
|
2756
3244
|
for (const variable of clause.variables) {
|
|
2757
|
-
const
|
|
3245
|
+
const value = row.get(variable);
|
|
3246
|
+
// Extract node/edge ID from the value (could be object with _nf_id or string ID)
|
|
3247
|
+
const id = this.extractNodeId(value);
|
|
2758
3248
|
if (!id)
|
|
2759
3249
|
continue;
|
|
2760
3250
|
if (clause.detach) {
|
|
@@ -2930,6 +3420,27 @@ export class Executor {
|
|
|
2930
3420
|
return null;
|
|
2931
3421
|
}
|
|
2932
3422
|
}
|
|
3423
|
+
case "reduce": {
|
|
3424
|
+
// Evaluate reduce expression: reduce(acc = init, x IN list | expr)
|
|
3425
|
+
const listValue = this.evaluateExpressionInRow(expr.listExpr, row, params);
|
|
3426
|
+
if (!Array.isArray(listValue))
|
|
3427
|
+
return null;
|
|
3428
|
+
// Start with the initial value
|
|
3429
|
+
let accumulator = this.evaluateExpressionInRow(expr.initialValue, row, params);
|
|
3430
|
+
for (const item of listValue) {
|
|
3431
|
+
// Create a new row with both the accumulator and loop variable
|
|
3432
|
+
const itemRow = new Map(row);
|
|
3433
|
+
if (expr.accumulator) {
|
|
3434
|
+
itemRow.set(expr.accumulator, accumulator);
|
|
3435
|
+
}
|
|
3436
|
+
if (expr.variable) {
|
|
3437
|
+
itemRow.set(expr.variable, item);
|
|
3438
|
+
}
|
|
3439
|
+
// Evaluate the reduce expression to get the new accumulator value
|
|
3440
|
+
accumulator = this.evaluateExpressionInRow(expr.reduceExpr, itemRow, params);
|
|
3441
|
+
}
|
|
3442
|
+
return accumulator;
|
|
3443
|
+
}
|
|
2933
3444
|
case "comparison": {
|
|
2934
3445
|
// Evaluate comparison expression: left op right
|
|
2935
3446
|
const left = this.evaluateExpressionInRow(expr.left, row, params);
|
|
@@ -3038,7 +3549,12 @@ export class Executor {
|
|
|
3038
3549
|
}
|
|
3039
3550
|
if (!edgeId)
|
|
3040
3551
|
return null;
|
|
3041
|
-
//
|
|
3552
|
+
// Check edge info cache first (populated by batchGetEdgeInfo)
|
|
3553
|
+
const cachedInfo = this.edgeInfoCache.get(edgeId);
|
|
3554
|
+
if (cachedInfo) {
|
|
3555
|
+
return cachedInfo.type;
|
|
3556
|
+
}
|
|
3557
|
+
// Fall back to database lookup
|
|
3042
3558
|
const result = this.db.execute("SELECT type FROM edges WHERE id = ?", [edgeId]);
|
|
3043
3559
|
if (result.rows.length > 0) {
|
|
3044
3560
|
return result.rows[0].type;
|
|
@@ -3057,10 +3573,17 @@ export class Executor {
|
|
|
3057
3573
|
startNodeId = edgeObj._nf_start;
|
|
3058
3574
|
}
|
|
3059
3575
|
else if ("_nf_id" in edgeObj) {
|
|
3060
|
-
//
|
|
3061
|
-
const
|
|
3062
|
-
if (
|
|
3063
|
-
startNodeId =
|
|
3576
|
+
// Check edge info cache first (populated by batchGetEdgeInfo)
|
|
3577
|
+
const cachedInfo = this.edgeInfoCache.get(edgeObj._nf_id);
|
|
3578
|
+
if (cachedInfo) {
|
|
3579
|
+
startNodeId = cachedInfo.source_id;
|
|
3580
|
+
}
|
|
3581
|
+
else {
|
|
3582
|
+
// Fall back to database lookup
|
|
3583
|
+
const edgeResult = this.db.execute("SELECT source_id FROM edges WHERE id = ?", [edgeObj._nf_id]);
|
|
3584
|
+
if (edgeResult.rows.length > 0) {
|
|
3585
|
+
startNodeId = edgeResult.rows[0].source_id;
|
|
3586
|
+
}
|
|
3064
3587
|
}
|
|
3065
3588
|
}
|
|
3066
3589
|
}
|
|
@@ -3069,9 +3592,7 @@ export class Executor {
|
|
|
3069
3592
|
// Look up node from database
|
|
3070
3593
|
const nodeResult = this.db.execute("SELECT id, properties FROM nodes WHERE id = ?", [startNodeId]);
|
|
3071
3594
|
if (nodeResult.rows.length > 0) {
|
|
3072
|
-
const nodeProps =
|
|
3073
|
-
? JSON.parse(nodeResult.rows[0].properties)
|
|
3074
|
-
: nodeResult.rows[0].properties;
|
|
3595
|
+
const nodeProps = this.getNodeProperties(nodeResult.rows[0].id, nodeResult.rows[0].properties);
|
|
3075
3596
|
return { ...nodeProps, _nf_id: nodeResult.rows[0].id };
|
|
3076
3597
|
}
|
|
3077
3598
|
return null;
|
|
@@ -3088,10 +3609,17 @@ export class Executor {
|
|
|
3088
3609
|
endNodeId = edgeObj._nf_end;
|
|
3089
3610
|
}
|
|
3090
3611
|
else if ("_nf_id" in edgeObj) {
|
|
3091
|
-
//
|
|
3092
|
-
const
|
|
3093
|
-
if (
|
|
3094
|
-
endNodeId =
|
|
3612
|
+
// Check edge info cache first (populated by batchGetEdgeInfo)
|
|
3613
|
+
const cachedInfo = this.edgeInfoCache.get(edgeObj._nf_id);
|
|
3614
|
+
if (cachedInfo) {
|
|
3615
|
+
endNodeId = cachedInfo.target_id;
|
|
3616
|
+
}
|
|
3617
|
+
else {
|
|
3618
|
+
// Fall back to database lookup
|
|
3619
|
+
const edgeResult = this.db.execute("SELECT target_id FROM edges WHERE id = ?", [edgeObj._nf_id]);
|
|
3620
|
+
if (edgeResult.rows.length > 0) {
|
|
3621
|
+
endNodeId = edgeResult.rows[0].target_id;
|
|
3622
|
+
}
|
|
3095
3623
|
}
|
|
3096
3624
|
}
|
|
3097
3625
|
}
|
|
@@ -3100,9 +3628,7 @@ export class Executor {
|
|
|
3100
3628
|
// Look up node from database
|
|
3101
3629
|
const nodeResult = this.db.execute("SELECT id, properties FROM nodes WHERE id = ?", [endNodeId]);
|
|
3102
3630
|
if (nodeResult.rows.length > 0) {
|
|
3103
|
-
const nodeProps =
|
|
3104
|
-
? JSON.parse(nodeResult.rows[0].properties)
|
|
3105
|
-
: nodeResult.rows[0].properties;
|
|
3631
|
+
const nodeProps = this.getNodeProperties(nodeResult.rows[0].id, nodeResult.rows[0].properties);
|
|
3106
3632
|
return { ...nodeProps, _nf_id: nodeResult.rows[0].id };
|
|
3107
3633
|
}
|
|
3108
3634
|
return null;
|
|
@@ -3591,6 +4117,10 @@ export class Executor {
|
|
|
3591
4117
|
this.db.transaction(() => {
|
|
3592
4118
|
// Collect all node inserts for batching
|
|
3593
4119
|
const nodeInserts = [];
|
|
4120
|
+
// Collect all edge inserts for batching
|
|
4121
|
+
const edgeInserts = [];
|
|
4122
|
+
// Cache edge info for RETURN lookups before batch insert
|
|
4123
|
+
const pendingEdgeInfo = new Map();
|
|
3594
4124
|
for (let comboIndex = 0; comboIndex < combinations.length; comboIndex++) {
|
|
3595
4125
|
const combination = combinations[comboIndex];
|
|
3596
4126
|
// Build a map of unwind variable -> current value
|
|
@@ -3600,12 +4130,8 @@ export class Executor {
|
|
|
3600
4130
|
}
|
|
3601
4131
|
for (const createClause of createClauses) {
|
|
3602
4132
|
for (const pattern of createClause.patterns) {
|
|
3603
|
-
if (this.isRelationshipPattern(pattern)) {
|
|
3604
|
-
//
|
|
3605
|
-
const createdIds = new Map();
|
|
3606
|
-
this.executeCreateRelationshipPatternWithUnwind(pattern, createdIds, params, unwindContext);
|
|
3607
|
-
}
|
|
3608
|
-
else {
|
|
4133
|
+
if (!this.isRelationshipPattern(pattern)) {
|
|
4134
|
+
// Only collect node patterns in this pass; relationships handled after nodes are inserted
|
|
3609
4135
|
const id = crypto.randomUUID();
|
|
3610
4136
|
const labelJson = this.normalizeLabelToJson(pattern.label);
|
|
3611
4137
|
const props = this.resolvePropertiesWithUnwind(pattern.properties || {}, params, unwindContext);
|
|
@@ -3648,7 +4174,7 @@ export class Executor {
|
|
|
3648
4174
|
for (const createClause of createClauses) {
|
|
3649
4175
|
for (const pattern of createClause.patterns) {
|
|
3650
4176
|
if (this.isRelationshipPattern(pattern)) {
|
|
3651
|
-
this.executeCreateRelationshipPatternWithUnwind(pattern, createdIds, params, unwindContext);
|
|
4177
|
+
this.executeCreateRelationshipPatternWithUnwind(pattern, createdIds, params, unwindContext, edgeInserts, pendingEdgeInfo);
|
|
3652
4178
|
}
|
|
3653
4179
|
}
|
|
3654
4180
|
}
|
|
@@ -3672,23 +4198,37 @@ export class Executor {
|
|
|
3672
4198
|
let value;
|
|
3673
4199
|
const id = createdIds.get(aggInfo.argVariable);
|
|
3674
4200
|
if (id) {
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
if (result.rows.length > 0) {
|
|
3680
|
-
const props = typeof result.rows[0].properties === "string"
|
|
3681
|
-
? JSON.parse(result.rows[0].properties)
|
|
3682
|
-
: result.rows[0].properties;
|
|
4201
|
+
// Check pendingEdgeInfo first (edges not yet batch-inserted)
|
|
4202
|
+
const pendingEdge = pendingEdgeInfo.get(id);
|
|
4203
|
+
if (pendingEdge) {
|
|
4204
|
+
const props = pendingEdge.properties;
|
|
3683
4205
|
if (aggInfo.argProperty) {
|
|
3684
|
-
// Collect property value
|
|
3685
4206
|
value = props[aggInfo.argProperty];
|
|
3686
4207
|
}
|
|
3687
4208
|
else {
|
|
3688
|
-
// Collect the whole node object (for collect(n))
|
|
3689
4209
|
value = props;
|
|
3690
4210
|
}
|
|
3691
4211
|
}
|
|
4212
|
+
else {
|
|
4213
|
+
let result = this.db.execute("SELECT id, properties FROM nodes WHERE id = ?", [id]);
|
|
4214
|
+
let isNode = result.rows.length > 0;
|
|
4215
|
+
if (!isNode) {
|
|
4216
|
+
result = this.db.execute("SELECT id, properties FROM edges WHERE id = ?", [id]);
|
|
4217
|
+
}
|
|
4218
|
+
if (result.rows.length > 0) {
|
|
4219
|
+
const props = isNode
|
|
4220
|
+
? this.getNodeProperties(result.rows[0].id, result.rows[0].properties)
|
|
4221
|
+
: this.getEdgeProperties(result.rows[0].id, result.rows[0].properties);
|
|
4222
|
+
if (aggInfo.argProperty) {
|
|
4223
|
+
// Collect property value
|
|
4224
|
+
value = props[aggInfo.argProperty];
|
|
4225
|
+
}
|
|
4226
|
+
else {
|
|
4227
|
+
// Collect the whole node object (for collect(n))
|
|
4228
|
+
value = props;
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
3692
4232
|
}
|
|
3693
4233
|
if (value !== undefined) {
|
|
3694
4234
|
if (!withAggregateValues.has(alias)) {
|
|
@@ -3713,17 +4253,25 @@ export class Executor {
|
|
|
3713
4253
|
const property = arg.property;
|
|
3714
4254
|
const id = createdIds.get(variable);
|
|
3715
4255
|
if (id) {
|
|
3716
|
-
//
|
|
3717
|
-
|
|
3718
|
-
if (
|
|
3719
|
-
|
|
3720
|
-
result = this.db.execute("SELECT properties FROM edges WHERE id = ?", [id]);
|
|
4256
|
+
// Check pendingEdgeInfo first (edges not yet batch-inserted)
|
|
4257
|
+
const pendingEdge = pendingEdgeInfo.get(id);
|
|
4258
|
+
if (pendingEdge) {
|
|
4259
|
+
value = pendingEdge.properties[property];
|
|
3721
4260
|
}
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
4261
|
+
else {
|
|
4262
|
+
// Try nodes first, then edges
|
|
4263
|
+
let result = this.db.execute("SELECT id, properties FROM nodes WHERE id = ?", [id]);
|
|
4264
|
+
let isNode = result.rows.length > 0;
|
|
4265
|
+
if (!isNode) {
|
|
4266
|
+
// Try edges table
|
|
4267
|
+
result = this.db.execute("SELECT id, properties FROM edges WHERE id = ?", [id]);
|
|
4268
|
+
}
|
|
4269
|
+
if (result.rows.length > 0) {
|
|
4270
|
+
const props = isNode
|
|
4271
|
+
? this.getNodeProperties(result.rows[0].id, result.rows[0].properties)
|
|
4272
|
+
: this.getEdgeProperties(result.rows[0].id, result.rows[0].properties);
|
|
4273
|
+
value = props[property];
|
|
4274
|
+
}
|
|
3727
4275
|
}
|
|
3728
4276
|
}
|
|
3729
4277
|
}
|
|
@@ -3758,9 +4306,7 @@ export class Executor {
|
|
|
3758
4306
|
if (nodeResult.rows.length > 0) {
|
|
3759
4307
|
const row = nodeResult.rows[0];
|
|
3760
4308
|
// Neo4j 3.5 format: return properties directly
|
|
3761
|
-
resultRow[alias] =
|
|
3762
|
-
? JSON.parse(row.properties)
|
|
3763
|
-
: row.properties;
|
|
4309
|
+
resultRow[alias] = this.getNodeProperties(row.id, row.properties);
|
|
3764
4310
|
}
|
|
3765
4311
|
}
|
|
3766
4312
|
}
|
|
@@ -3770,17 +4316,25 @@ export class Executor {
|
|
|
3770
4316
|
const property = item.expression.property;
|
|
3771
4317
|
const id = createdIds.get(variable);
|
|
3772
4318
|
if (id) {
|
|
3773
|
-
//
|
|
3774
|
-
|
|
3775
|
-
if (
|
|
3776
|
-
|
|
3777
|
-
nodeResult = this.db.execute("SELECT properties FROM edges WHERE id = ?", [id]);
|
|
4319
|
+
// Check pendingEdgeInfo first (edges not yet batch-inserted)
|
|
4320
|
+
const pendingEdge = pendingEdgeInfo.get(id);
|
|
4321
|
+
if (pendingEdge) {
|
|
4322
|
+
resultRow[alias] = pendingEdge.properties[property];
|
|
3778
4323
|
}
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
4324
|
+
else {
|
|
4325
|
+
// Try nodes first, then edges
|
|
4326
|
+
let nodeResult = this.db.execute("SELECT id, properties FROM nodes WHERE id = ?", [id]);
|
|
4327
|
+
let isNode = nodeResult.rows.length > 0;
|
|
4328
|
+
if (!isNode) {
|
|
4329
|
+
// Try edges table
|
|
4330
|
+
nodeResult = this.db.execute("SELECT id, properties FROM edges WHERE id = ?", [id]);
|
|
4331
|
+
}
|
|
4332
|
+
if (nodeResult.rows.length > 0) {
|
|
4333
|
+
const props = isNode
|
|
4334
|
+
? this.getNodeProperties(nodeResult.rows[0].id, nodeResult.rows[0].properties)
|
|
4335
|
+
: this.getEdgeProperties(nodeResult.rows[0].id, nodeResult.rows[0].properties);
|
|
4336
|
+
resultRow[alias] = props[property];
|
|
4337
|
+
}
|
|
3784
4338
|
}
|
|
3785
4339
|
}
|
|
3786
4340
|
}
|
|
@@ -3791,6 +4345,14 @@ export class Executor {
|
|
|
3791
4345
|
}
|
|
3792
4346
|
}
|
|
3793
4347
|
}
|
|
4348
|
+
// Batch insert edges (cap at 500 rows per statement)
|
|
4349
|
+
const EDGE_BATCH_SIZE = 500;
|
|
4350
|
+
for (let i = 0; i < edgeInserts.length; i += EDGE_BATCH_SIZE) {
|
|
4351
|
+
const batch = edgeInserts.slice(i, i + EDGE_BATCH_SIZE);
|
|
4352
|
+
const placeholders = batch.map(() => '(?, ?, ?, ?, ?)').join(',');
|
|
4353
|
+
const values = batch.flatMap(insert => [insert.id, insert.type, insert.sourceId, insert.targetId, insert.propsJson]);
|
|
4354
|
+
this.db.execute(`INSERT INTO edges (id, type, source_id, target_id, properties) VALUES ${placeholders}`, values);
|
|
4355
|
+
}
|
|
3794
4356
|
});
|
|
3795
4357
|
// Compute WITH aggregate results if RETURN references them
|
|
3796
4358
|
if (returnsWithAggregateAliases && returnClause) {
|
|
@@ -3969,14 +4531,15 @@ export class Executor {
|
|
|
3969
4531
|
const id = resolvedIds[variable];
|
|
3970
4532
|
if (id) {
|
|
3971
4533
|
// Try nodes first, then edges
|
|
3972
|
-
let result = this.db.execute("SELECT properties FROM nodes WHERE id = ?", [id]);
|
|
3973
|
-
|
|
3974
|
-
|
|
4534
|
+
let result = this.db.execute("SELECT id, properties FROM nodes WHERE id = ?", [id]);
|
|
4535
|
+
let isNode = result.rows.length > 0;
|
|
4536
|
+
if (!isNode) {
|
|
4537
|
+
result = this.db.execute("SELECT id, properties FROM edges WHERE id = ?", [id]);
|
|
3975
4538
|
}
|
|
3976
4539
|
if (result.rows.length > 0) {
|
|
3977
|
-
const props =
|
|
3978
|
-
?
|
|
3979
|
-
: result.rows[0].properties;
|
|
4540
|
+
const props = isNode
|
|
4541
|
+
? this.getNodeProperties(result.rows[0].id, result.rows[0].properties)
|
|
4542
|
+
: this.getEdgeProperties(result.rows[0].id, result.rows[0].properties);
|
|
3980
4543
|
return props[property];
|
|
3981
4544
|
}
|
|
3982
4545
|
}
|
|
@@ -4012,15 +4575,16 @@ export class Executor {
|
|
|
4012
4575
|
const id = createdIds.get(variable);
|
|
4013
4576
|
if (id) {
|
|
4014
4577
|
// Try nodes first, then edges
|
|
4015
|
-
let result = this.db.execute("SELECT properties FROM nodes WHERE id = ?", [id]);
|
|
4016
|
-
|
|
4578
|
+
let result = this.db.execute("SELECT id, properties FROM nodes WHERE id = ?", [id]);
|
|
4579
|
+
let isNode = result.rows.length > 0;
|
|
4580
|
+
if (!isNode) {
|
|
4017
4581
|
// Try edges table
|
|
4018
|
-
result = this.db.execute("SELECT properties FROM edges WHERE id = ?", [id]);
|
|
4582
|
+
result = this.db.execute("SELECT id, properties FROM edges WHERE id = ?", [id]);
|
|
4019
4583
|
}
|
|
4020
4584
|
if (result.rows.length > 0) {
|
|
4021
|
-
const props =
|
|
4022
|
-
?
|
|
4023
|
-
: result.rows[0].properties;
|
|
4585
|
+
const props = isNode
|
|
4586
|
+
? this.getNodeProperties(result.rows[0].id, result.rows[0].properties)
|
|
4587
|
+
: this.getEdgeProperties(result.rows[0].id, result.rows[0].properties);
|
|
4024
4588
|
return props[property];
|
|
4025
4589
|
}
|
|
4026
4590
|
}
|
|
@@ -4108,8 +4672,9 @@ export class Executor {
|
|
|
4108
4672
|
let whereConditions = [];
|
|
4109
4673
|
let whereParams = [];
|
|
4110
4674
|
if (nodePattern.label) {
|
|
4111
|
-
|
|
4112
|
-
|
|
4675
|
+
// Use primary label index with fallback for secondary labels
|
|
4676
|
+
whereConditions.push("(json_extract(label, '$[0]') = ? OR EXISTS (SELECT 1 FROM json_each(label) WHERE value = ? AND json_extract(label, '$[0]') != ?))");
|
|
4677
|
+
whereParams.push(nodePattern.label, nodePattern.label, nodePattern.label);
|
|
4113
4678
|
}
|
|
4114
4679
|
for (const [key, value] of Object.entries(props)) {
|
|
4115
4680
|
whereConditions.push(`json_extract(properties, '$.${key}') = ?`);
|
|
@@ -5059,7 +5624,7 @@ export class Executor {
|
|
|
5059
5624
|
/**
|
|
5060
5625
|
* Execute CREATE relationship pattern with unwind context
|
|
5061
5626
|
*/
|
|
5062
|
-
executeCreateRelationshipPatternWithUnwind(rel, createdIds, params, unwindContext) {
|
|
5627
|
+
executeCreateRelationshipPatternWithUnwind(rel, createdIds, params, unwindContext, edgeInserts, pendingEdgeInfo) {
|
|
5063
5628
|
let sourceId;
|
|
5064
5629
|
let targetId;
|
|
5065
5630
|
// Determine source node ID
|
|
@@ -5102,7 +5667,30 @@ export class Executor {
|
|
|
5102
5667
|
const edgeId = crypto.randomUUID();
|
|
5103
5668
|
const edgeType = rel.edge.type || "";
|
|
5104
5669
|
const edgeProps = this.resolvePropertiesWithUnwind(rel.edge.properties || {}, params, unwindContext);
|
|
5105
|
-
|
|
5670
|
+
if (edgeInserts) {
|
|
5671
|
+
// Collect for batch insert
|
|
5672
|
+
edgeInserts.push({
|
|
5673
|
+
id: edgeId,
|
|
5674
|
+
type: edgeType,
|
|
5675
|
+
sourceId: actualSource,
|
|
5676
|
+
targetId: actualTarget,
|
|
5677
|
+
propsJson: JSON.stringify(edgeProps),
|
|
5678
|
+
variable: rel.edge.variable
|
|
5679
|
+
});
|
|
5680
|
+
// Cache for RETURN lookups before batch insert
|
|
5681
|
+
if (pendingEdgeInfo) {
|
|
5682
|
+
pendingEdgeInfo.set(edgeId, {
|
|
5683
|
+
type: edgeType,
|
|
5684
|
+
sourceId: actualSource,
|
|
5685
|
+
targetId: actualTarget,
|
|
5686
|
+
properties: edgeProps
|
|
5687
|
+
});
|
|
5688
|
+
}
|
|
5689
|
+
}
|
|
5690
|
+
else {
|
|
5691
|
+
// Direct insert (for non-batched callers)
|
|
5692
|
+
this.db.execute("INSERT INTO edges (id, type, source_id, target_id, properties) VALUES (?, ?, ?, ?, ?)", [edgeId, edgeType, actualSource, actualTarget, JSON.stringify(edgeProps)]);
|
|
5693
|
+
}
|
|
5106
5694
|
if (rel.edge.variable) {
|
|
5107
5695
|
createdIds.set(rel.edge.variable, edgeId);
|
|
5108
5696
|
}
|
|
@@ -5186,9 +5774,7 @@ export class Executor {
|
|
|
5186
5774
|
if (nodeResult.rows.length > 0) {
|
|
5187
5775
|
const row = nodeResult.rows[0];
|
|
5188
5776
|
// Neo4j 3.5 format: return properties directly
|
|
5189
|
-
resultRow[alias] =
|
|
5190
|
-
? JSON.parse(row.properties)
|
|
5191
|
-
: row.properties;
|
|
5777
|
+
resultRow[alias] = this.getNodeProperties(row.id, row.properties);
|
|
5192
5778
|
}
|
|
5193
5779
|
}
|
|
5194
5780
|
}
|
|
@@ -5428,6 +6014,15 @@ export class Executor {
|
|
|
5428
6014
|
}
|
|
5429
6015
|
if (relList.length === 0)
|
|
5430
6016
|
continue;
|
|
6017
|
+
// Batch collect all edge IDs first
|
|
6018
|
+
const edgeIds = [];
|
|
6019
|
+
for (const rel of relList) {
|
|
6020
|
+
const edgeId = this.extractEdgeId(rel);
|
|
6021
|
+
if (edgeId)
|
|
6022
|
+
edgeIds.push(edgeId);
|
|
6023
|
+
}
|
|
6024
|
+
// Batch fetch all edge info in a single query
|
|
6025
|
+
const edgeInfoMap = this.batchGetEdgeInfo(edgeIds);
|
|
5431
6026
|
// Follow the sequence of relationships to find endpoints
|
|
5432
6027
|
let currentNodeId = null;
|
|
5433
6028
|
let firstNodeId = null;
|
|
@@ -5435,45 +6030,9 @@ export class Executor {
|
|
|
5435
6030
|
let valid = true;
|
|
5436
6031
|
for (let i = 0; i < relList.length; i++) {
|
|
5437
6032
|
const rel = relList[i];
|
|
5438
|
-
// Extract edge info
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
const relObj = rel;
|
|
5442
|
-
const edgeId = (relObj._nf_id || relObj.id);
|
|
5443
|
-
if (edgeId) {
|
|
5444
|
-
const edgeResult = this.db.execute("SELECT id, source_id, target_id FROM edges WHERE id = ?", [edgeId]);
|
|
5445
|
-
if (edgeResult.rows.length > 0) {
|
|
5446
|
-
const r = edgeResult.rows[0];
|
|
5447
|
-
edgeInfo = {
|
|
5448
|
-
id: r.id,
|
|
5449
|
-
source_id: r.source_id,
|
|
5450
|
-
target_id: r.target_id
|
|
5451
|
-
};
|
|
5452
|
-
}
|
|
5453
|
-
}
|
|
5454
|
-
}
|
|
5455
|
-
else if (typeof rel === "string") {
|
|
5456
|
-
// Could be a JSON string like '{"_nf_id":"uuid"}' or a raw edge ID
|
|
5457
|
-
let edgeId = rel;
|
|
5458
|
-
try {
|
|
5459
|
-
const parsed = JSON.parse(rel);
|
|
5460
|
-
if (typeof parsed === "object" && parsed !== null && (parsed._nf_id || parsed.id)) {
|
|
5461
|
-
edgeId = (parsed._nf_id || parsed.id);
|
|
5462
|
-
}
|
|
5463
|
-
}
|
|
5464
|
-
catch {
|
|
5465
|
-
// Not JSON, use as-is
|
|
5466
|
-
}
|
|
5467
|
-
const edgeResult = this.db.execute("SELECT id, source_id, target_id FROM edges WHERE id = ?", [edgeId]);
|
|
5468
|
-
if (edgeResult.rows.length > 0) {
|
|
5469
|
-
const r = edgeResult.rows[0];
|
|
5470
|
-
edgeInfo = {
|
|
5471
|
-
id: r.id,
|
|
5472
|
-
source_id: r.source_id,
|
|
5473
|
-
target_id: r.target_id
|
|
5474
|
-
};
|
|
5475
|
-
}
|
|
5476
|
-
}
|
|
6033
|
+
// Extract edge ID and get info from batch result
|
|
6034
|
+
const edgeId = this.extractEdgeId(rel);
|
|
6035
|
+
const edgeInfo = edgeId ? edgeInfoMap.get(edgeId) : null;
|
|
5477
6036
|
if (!edgeInfo) {
|
|
5478
6037
|
valid = false;
|
|
5479
6038
|
break;
|
|
@@ -5524,12 +6083,8 @@ export class Executor {
|
|
|
5524
6083
|
}
|
|
5525
6084
|
const firstNode = firstNodeResult.rows[0];
|
|
5526
6085
|
const lastNode = lastNodeResult.rows[0];
|
|
5527
|
-
const firstProps =
|
|
5528
|
-
|
|
5529
|
-
: firstNode.properties;
|
|
5530
|
-
const lastProps = typeof lastNode.properties === "string"
|
|
5531
|
-
? JSON.parse(lastNode.properties)
|
|
5532
|
-
: lastNode.properties;
|
|
6086
|
+
const firstProps = this.getNodeProperties(firstNode.id, firstNode.properties);
|
|
6087
|
+
const lastProps = this.getNodeProperties(lastNode.id, lastNode.properties);
|
|
5533
6088
|
// Build result based on RETURN items
|
|
5534
6089
|
const resultRow = {};
|
|
5535
6090
|
for (const item of returnClause.items) {
|
|
@@ -5760,6 +6315,8 @@ export class Executor {
|
|
|
5760
6315
|
continue;
|
|
5761
6316
|
const value = this.evaluateExpressionWithMatchedNodes(assignment.value, params, matchedNodes);
|
|
5762
6317
|
this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
|
|
6318
|
+
// Invalidate cache after UPDATE
|
|
6319
|
+
this.invalidatePropertyCache(nodeId);
|
|
5763
6320
|
}
|
|
5764
6321
|
}
|
|
5765
6322
|
}
|
|
@@ -5771,7 +6328,7 @@ export class Executor {
|
|
|
5771
6328
|
matchedNodes.set(pattern.variable, {
|
|
5772
6329
|
id: row.id,
|
|
5773
6330
|
label: row.label,
|
|
5774
|
-
properties:
|
|
6331
|
+
properties: this.getNodeProperties(row.id, row.properties),
|
|
5775
6332
|
});
|
|
5776
6333
|
}
|
|
5777
6334
|
}
|
|
@@ -5836,7 +6393,7 @@ export class Executor {
|
|
|
5836
6393
|
type: row.type,
|
|
5837
6394
|
source_id: row.source_id,
|
|
5838
6395
|
target_id: row.target_id,
|
|
5839
|
-
properties:
|
|
6396
|
+
properties: this.getEdgeProperties(row.id, row.properties),
|
|
5840
6397
|
});
|
|
5841
6398
|
}
|
|
5842
6399
|
}
|
|
@@ -6109,7 +6666,7 @@ export class Executor {
|
|
|
6109
6666
|
newRow.set(nodePattern.variable, {
|
|
6110
6667
|
id: sqlRow.id,
|
|
6111
6668
|
label: labelValue,
|
|
6112
|
-
properties:
|
|
6669
|
+
properties: this.getNodeProperties(sqlRow.id, sqlRow.properties),
|
|
6113
6670
|
});
|
|
6114
6671
|
}
|
|
6115
6672
|
newMatchRows.push(newRow);
|
|
@@ -6258,6 +6815,8 @@ export class Executor {
|
|
|
6258
6815
|
continue;
|
|
6259
6816
|
const value = this.evaluateExpressionWithMatchedNodes(assignment.value, params, matchedNodes);
|
|
6260
6817
|
this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
|
|
6818
|
+
// Invalidate cache after UPDATE
|
|
6819
|
+
this.invalidatePropertyCache(nodeId);
|
|
6261
6820
|
}
|
|
6262
6821
|
}
|
|
6263
6822
|
}
|
|
@@ -6269,7 +6828,7 @@ export class Executor {
|
|
|
6269
6828
|
matchedNodes.set(pattern.variable, {
|
|
6270
6829
|
id: row.id,
|
|
6271
6830
|
label: row.label,
|
|
6272
|
-
properties:
|
|
6831
|
+
properties: this.getNodeProperties(row.id, row.properties),
|
|
6273
6832
|
});
|
|
6274
6833
|
}
|
|
6275
6834
|
}
|
|
@@ -6392,7 +6951,7 @@ export class Executor {
|
|
|
6392
6951
|
type: row.type,
|
|
6393
6952
|
source_id: row.source_id,
|
|
6394
6953
|
target_id: row.target_id,
|
|
6395
|
-
properties:
|
|
6954
|
+
properties: this.getEdgeProperties(row.id, row.properties),
|
|
6396
6955
|
});
|
|
6397
6956
|
}
|
|
6398
6957
|
}
|
|
@@ -6596,6 +7155,8 @@ export class Executor {
|
|
|
6596
7155
|
? this.evaluateExpressionWithContext(assignment.value, params, resolvedIds)
|
|
6597
7156
|
: this.evaluateExpression(assignment.value, params);
|
|
6598
7157
|
this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
|
|
7158
|
+
// Invalidate cache after UPDATE
|
|
7159
|
+
this.invalidatePropertyCache(nodeId);
|
|
6599
7160
|
}
|
|
6600
7161
|
}
|
|
6601
7162
|
}
|
|
@@ -6607,7 +7168,7 @@ export class Executor {
|
|
|
6607
7168
|
matchedNodes.set(pattern.variable, {
|
|
6608
7169
|
id: row.id,
|
|
6609
7170
|
label: row.label,
|
|
6610
|
-
properties:
|
|
7171
|
+
properties: this.getNodeProperties(row.id, row.properties),
|
|
6611
7172
|
});
|
|
6612
7173
|
}
|
|
6613
7174
|
}
|
|
@@ -6705,6 +7266,8 @@ export class Executor {
|
|
|
6705
7266
|
const value = this.evaluateExpression(assignment.value, params);
|
|
6706
7267
|
// Update target node with ON CREATE SET
|
|
6707
7268
|
this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), targetNodeId]);
|
|
7269
|
+
// Invalidate cache after UPDATE
|
|
7270
|
+
this.invalidatePropertyCache(targetNodeId);
|
|
6708
7271
|
}
|
|
6709
7272
|
}
|
|
6710
7273
|
this.db.execute("INSERT INTO edges (id, type, source_id, target_id, properties) VALUES (?, ?, ?, ?, ?)", [edgeId, edgeType, sourceNodeId, targetNodeId, JSON.stringify(finalEdgeProps)]);
|
|
@@ -6722,6 +7285,8 @@ export class Executor {
|
|
|
6722
7285
|
const value = this.evaluateExpression(assignment.value, params);
|
|
6723
7286
|
// Update target node with ON MATCH SET
|
|
6724
7287
|
this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), targetNodeId]);
|
|
7288
|
+
// Invalidate cache after UPDATE
|
|
7289
|
+
this.invalidatePropertyCache(targetNodeId);
|
|
6725
7290
|
}
|
|
6726
7291
|
}
|
|
6727
7292
|
}
|
|
@@ -6735,7 +7300,7 @@ export class Executor {
|
|
|
6735
7300
|
type: row.type,
|
|
6736
7301
|
source_id: row.source_id,
|
|
6737
7302
|
target_id: row.target_id,
|
|
6738
|
-
properties:
|
|
7303
|
+
properties: this.getEdgeProperties(row.id, row.properties),
|
|
6739
7304
|
});
|
|
6740
7305
|
}
|
|
6741
7306
|
}
|
|
@@ -6747,7 +7312,7 @@ export class Executor {
|
|
|
6747
7312
|
matchedNodes.set(targetVar, {
|
|
6748
7313
|
id: row.id,
|
|
6749
7314
|
label: row.label,
|
|
6750
|
-
properties:
|
|
7315
|
+
properties: this.getNodeProperties(row.id, row.properties),
|
|
6751
7316
|
});
|
|
6752
7317
|
}
|
|
6753
7318
|
}
|
|
@@ -6782,7 +7347,7 @@ export class Executor {
|
|
|
6782
7347
|
return {
|
|
6783
7348
|
id: row.id,
|
|
6784
7349
|
label: typeof row.label === "string" ? JSON.parse(row.label) : row.label,
|
|
6785
|
-
properties:
|
|
7350
|
+
properties: this.getNodeProperties(row.id, row.properties),
|
|
6786
7351
|
};
|
|
6787
7352
|
}
|
|
6788
7353
|
}
|
|
@@ -7041,7 +7606,7 @@ export class Executor {
|
|
|
7041
7606
|
case "variable":
|
|
7042
7607
|
return expr.variable;
|
|
7043
7608
|
case "property":
|
|
7044
|
-
return `${expr.variable}
|
|
7609
|
+
return `${expr.variable}.${expr.property}`;
|
|
7045
7610
|
case "function": {
|
|
7046
7611
|
// Build function expression like count(a) or count(*)
|
|
7047
7612
|
const funcName = expr.functionName.toLowerCase();
|
|
@@ -7080,6 +7645,236 @@ export class Executor {
|
|
|
7080
7645
|
return "expr";
|
|
7081
7646
|
}
|
|
7082
7647
|
}
|
|
7648
|
+
/**
|
|
7649
|
+
* Execute queries with FOREACH clause.
|
|
7650
|
+
* FOREACH iterates over a list and executes mutation clauses for each item.
|
|
7651
|
+
* Pattern: ... FOREACH (var IN list | SET/CREATE/DELETE...) ...
|
|
7652
|
+
*/
|
|
7653
|
+
tryForeachExecution(query, params) {
|
|
7654
|
+
// Execute query with phased approach using PhaseContext
|
|
7655
|
+
let context = createEmptyContext();
|
|
7656
|
+
this.db.transaction(() => {
|
|
7657
|
+
for (const clause of query.clauses) {
|
|
7658
|
+
if (clause.type === "FOREACH") {
|
|
7659
|
+
// Execute FOREACH for the current context
|
|
7660
|
+
this.executeForeachClause(clause, context, params);
|
|
7661
|
+
}
|
|
7662
|
+
else if (clause.type === "RETURN") {
|
|
7663
|
+
// RETURN is handled after transaction
|
|
7664
|
+
}
|
|
7665
|
+
else if (clause.type === "CREATE") {
|
|
7666
|
+
context = this.executeCreateClause(clause, context, params);
|
|
7667
|
+
}
|
|
7668
|
+
else if (clause.type === "WITH") {
|
|
7669
|
+
context = this.executeWithClause(clause, context, params);
|
|
7670
|
+
}
|
|
7671
|
+
else if (clause.type === "SET") {
|
|
7672
|
+
this.executeForeachSetClause(clause, context, params);
|
|
7673
|
+
}
|
|
7674
|
+
// Other clauses could be added as needed
|
|
7675
|
+
}
|
|
7676
|
+
});
|
|
7677
|
+
// Handle RETURN clause if present
|
|
7678
|
+
const returnClause = query.clauses.find((c) => c.type === "RETURN");
|
|
7679
|
+
if (returnClause) {
|
|
7680
|
+
return this.executeForeachReturn(returnClause, context, params);
|
|
7681
|
+
}
|
|
7682
|
+
return [];
|
|
7683
|
+
}
|
|
7684
|
+
/**
|
|
7685
|
+
* Execute a FOREACH clause within PhaseContext.
|
|
7686
|
+
*/
|
|
7687
|
+
executeForeachClause(foreachClause, context, params) {
|
|
7688
|
+
for (const row of context.rows) {
|
|
7689
|
+
// Evaluate the list expression in the current row context
|
|
7690
|
+
const listValue = this.evaluateExpressionInRow(foreachClause.expression, row, params);
|
|
7691
|
+
if (!Array.isArray(listValue)) {
|
|
7692
|
+
continue;
|
|
7693
|
+
}
|
|
7694
|
+
// For each item in the list, execute the body clauses
|
|
7695
|
+
for (const item of listValue) {
|
|
7696
|
+
// Create a row context with the iteration variable bound
|
|
7697
|
+
const iterationRow = new Map(row);
|
|
7698
|
+
iterationRow.set(foreachClause.variable, item);
|
|
7699
|
+
// Execute each body clause
|
|
7700
|
+
for (const bodyClause of foreachClause.body) {
|
|
7701
|
+
this.executeForeachBodyClause(bodyClause, iterationRow, context, params);
|
|
7702
|
+
}
|
|
7703
|
+
}
|
|
7704
|
+
}
|
|
7705
|
+
}
|
|
7706
|
+
/**
|
|
7707
|
+
* Execute a single clause within FOREACH body.
|
|
7708
|
+
*/
|
|
7709
|
+
executeForeachBodyClause(clause, row, context, params) {
|
|
7710
|
+
switch (clause.type) {
|
|
7711
|
+
case "SET":
|
|
7712
|
+
this.executeForeachSetInRow(clause, row, params);
|
|
7713
|
+
break;
|
|
7714
|
+
case "CREATE":
|
|
7715
|
+
this.executeForeachCreateInRow(clause, row, params);
|
|
7716
|
+
break;
|
|
7717
|
+
case "DELETE":
|
|
7718
|
+
this.executeForeachDeleteInRow(clause, row);
|
|
7719
|
+
break;
|
|
7720
|
+
case "FOREACH":
|
|
7721
|
+
// Nested FOREACH - create temporary context with single row
|
|
7722
|
+
const tempContext = {
|
|
7723
|
+
nodeIds: new Map(context.nodeIds),
|
|
7724
|
+
edgeIds: new Map(context.edgeIds),
|
|
7725
|
+
values: new Map(context.values),
|
|
7726
|
+
rows: [row],
|
|
7727
|
+
};
|
|
7728
|
+
this.executeForeachClause(clause, tempContext, params);
|
|
7729
|
+
break;
|
|
7730
|
+
default:
|
|
7731
|
+
throw new Error(`Unsupported clause type '${clause.type}' in FOREACH body`);
|
|
7732
|
+
}
|
|
7733
|
+
}
|
|
7734
|
+
/**
|
|
7735
|
+
* Execute SET within FOREACH context using Map-based row.
|
|
7736
|
+
*/
|
|
7737
|
+
executeForeachSetInRow(clause, row, params) {
|
|
7738
|
+
for (const assignment of clause.assignments) {
|
|
7739
|
+
const targetVar = assignment.variable;
|
|
7740
|
+
const targetObj = row.get(targetVar);
|
|
7741
|
+
if (!targetObj || typeof targetObj !== "object") {
|
|
7742
|
+
continue;
|
|
7743
|
+
}
|
|
7744
|
+
const target = targetObj;
|
|
7745
|
+
// Support both _nf_id (node) and _id (edge)
|
|
7746
|
+
const nodeId = target._nf_id || target._id;
|
|
7747
|
+
if (!nodeId) {
|
|
7748
|
+
continue;
|
|
7749
|
+
}
|
|
7750
|
+
// Determine if this is a node or edge based on which id field exists
|
|
7751
|
+
const isNode = "_nf_id" in target;
|
|
7752
|
+
const table = isNode ? "nodes" : "edges";
|
|
7753
|
+
if (assignment.value) {
|
|
7754
|
+
const newValue = this.evaluateExpressionInRow(assignment.value, row, params);
|
|
7755
|
+
if (assignment.property) {
|
|
7756
|
+
// SET n.prop = value
|
|
7757
|
+
const propName = assignment.property;
|
|
7758
|
+
const sql = `UPDATE ${table} SET properties = json_set(properties, '$.${propName}', json(?)) WHERE id = ?`;
|
|
7759
|
+
this.db.execute(sql, [JSON.stringify(newValue), nodeId]);
|
|
7760
|
+
// Update the row object so subsequent iterations see the new value
|
|
7761
|
+
target[propName] = newValue;
|
|
7762
|
+
}
|
|
7763
|
+
else if (assignment.mergeProps) {
|
|
7764
|
+
// SET n += {...}
|
|
7765
|
+
if (typeof newValue === "object" && newValue !== null) {
|
|
7766
|
+
const propsJson = JSON.stringify(newValue);
|
|
7767
|
+
const sql = `UPDATE ${table} SET properties = json_patch(properties, ?) WHERE id = ?`;
|
|
7768
|
+
this.db.execute(sql, [propsJson, nodeId]);
|
|
7769
|
+
}
|
|
7770
|
+
}
|
|
7771
|
+
else if (assignment.replaceProps) {
|
|
7772
|
+
// SET n = {...}
|
|
7773
|
+
if (typeof newValue === "object" && newValue !== null) {
|
|
7774
|
+
const propsJson = JSON.stringify(newValue);
|
|
7775
|
+
const sql = `UPDATE ${table} SET properties = ? WHERE id = ?`;
|
|
7776
|
+
this.db.execute(sql, [propsJson, nodeId]);
|
|
7777
|
+
}
|
|
7778
|
+
}
|
|
7779
|
+
}
|
|
7780
|
+
}
|
|
7781
|
+
}
|
|
7782
|
+
/**
|
|
7783
|
+
* Execute CREATE within FOREACH context using Map-based row.
|
|
7784
|
+
*/
|
|
7785
|
+
executeForeachCreateInRow(clause, row, _params) {
|
|
7786
|
+
for (const pattern of clause.patterns) {
|
|
7787
|
+
if (this.isRelationshipPattern(pattern)) {
|
|
7788
|
+
const relPattern = pattern;
|
|
7789
|
+
const sourceNode = row.get(relPattern.source.variable ?? "");
|
|
7790
|
+
const targetNode = row.get(relPattern.target.variable ?? "");
|
|
7791
|
+
if (sourceNode?._id && targetNode?._id && relPattern.edge.type) {
|
|
7792
|
+
const props = relPattern.edge.properties || {};
|
|
7793
|
+
const propsJson = JSON.stringify(props);
|
|
7794
|
+
this.db.execute("INSERT INTO edges (source, target, type, properties) VALUES (?, ?, ?, ?)", [sourceNode._id, targetNode._id, relPattern.edge.type, propsJson]);
|
|
7795
|
+
}
|
|
7796
|
+
}
|
|
7797
|
+
else {
|
|
7798
|
+
const nodePattern = pattern;
|
|
7799
|
+
const label = nodePattern.label;
|
|
7800
|
+
const labels = Array.isArray(label) ? label : (label ? [label] : []);
|
|
7801
|
+
const props = nodePattern.properties || {};
|
|
7802
|
+
const propsJson = JSON.stringify(props);
|
|
7803
|
+
const labelsJson = JSON.stringify(labels);
|
|
7804
|
+
const result = this.db.execute("INSERT INTO nodes (labels, properties) VALUES (?, ?)", [labelsJson, propsJson]);
|
|
7805
|
+
if (nodePattern.variable && result.lastInsertRowid) {
|
|
7806
|
+
row.set(nodePattern.variable, {
|
|
7807
|
+
_id: result.lastInsertRowid,
|
|
7808
|
+
_labels: labels,
|
|
7809
|
+
...props,
|
|
7810
|
+
});
|
|
7811
|
+
}
|
|
7812
|
+
}
|
|
7813
|
+
}
|
|
7814
|
+
}
|
|
7815
|
+
/**
|
|
7816
|
+
* Execute DELETE within FOREACH context using Map-based row.
|
|
7817
|
+
*/
|
|
7818
|
+
executeForeachDeleteInRow(clause, row) {
|
|
7819
|
+
if (!clause.expressions)
|
|
7820
|
+
return;
|
|
7821
|
+
for (const expr of clause.expressions) {
|
|
7822
|
+
if (expr.type === "variable" && expr.variable) {
|
|
7823
|
+
const target = row.get(expr.variable);
|
|
7824
|
+
if (target?._id !== undefined) {
|
|
7825
|
+
const isNode = "_labels" in target;
|
|
7826
|
+
if (isNode) {
|
|
7827
|
+
if (clause.detach) {
|
|
7828
|
+
this.db.execute("DELETE FROM edges WHERE source = ? OR target = ?", [target._id, target._id]);
|
|
7829
|
+
}
|
|
7830
|
+
this.db.execute("DELETE FROM nodes WHERE id = ?", [target._id]);
|
|
7831
|
+
}
|
|
7832
|
+
else {
|
|
7833
|
+
this.db.execute("DELETE FROM edges WHERE id = ?", [target._id]);
|
|
7834
|
+
}
|
|
7835
|
+
}
|
|
7836
|
+
}
|
|
7837
|
+
}
|
|
7838
|
+
}
|
|
7839
|
+
/**
|
|
7840
|
+
* Execute SET clause within PhaseContext (not inside FOREACH iteration).
|
|
7841
|
+
*/
|
|
7842
|
+
executeForeachSetClause(clause, context, params) {
|
|
7843
|
+
for (const row of context.rows) {
|
|
7844
|
+
this.executeForeachSetInRow(clause, row, params);
|
|
7845
|
+
}
|
|
7846
|
+
}
|
|
7847
|
+
/**
|
|
7848
|
+
* Execute RETURN clause for FOREACH execution path.
|
|
7849
|
+
*/
|
|
7850
|
+
executeForeachReturn(clause, context, params) {
|
|
7851
|
+
const resultRows = [];
|
|
7852
|
+
for (const row of context.rows) {
|
|
7853
|
+
const resultRow = {};
|
|
7854
|
+
for (const item of clause.items) {
|
|
7855
|
+
const alias = item.alias || this.getForeachExpressionName(item.expression);
|
|
7856
|
+
const value = this.evaluateExpressionInRow(item.expression, row, params);
|
|
7857
|
+
resultRow[alias] = value;
|
|
7858
|
+
}
|
|
7859
|
+
resultRows.push(resultRow);
|
|
7860
|
+
}
|
|
7861
|
+
return resultRows;
|
|
7862
|
+
}
|
|
7863
|
+
/**
|
|
7864
|
+
* Get expression name for FOREACH return aliases.
|
|
7865
|
+
*/
|
|
7866
|
+
getForeachExpressionName(expr) {
|
|
7867
|
+
switch (expr.type) {
|
|
7868
|
+
case "variable":
|
|
7869
|
+
return expr.variable || "value";
|
|
7870
|
+
case "property":
|
|
7871
|
+
return `${expr.variable}.${expr.property}`;
|
|
7872
|
+
case "function":
|
|
7873
|
+
return expr.functionName?.toLowerCase() || "value";
|
|
7874
|
+
default:
|
|
7875
|
+
return "value";
|
|
7876
|
+
}
|
|
7877
|
+
}
|
|
7083
7878
|
/**
|
|
7084
7879
|
* Detect and handle patterns that need multi-phase execution:
|
|
7085
7880
|
* - MATCH...CREATE that references matched variables
|
|
@@ -7840,10 +8635,10 @@ export class Executor {
|
|
|
7840
8635
|
for (const resolvedIds of allResolvedIds) {
|
|
7841
8636
|
const nodeId = resolvedIds[varName];
|
|
7842
8637
|
if (nodeId) {
|
|
7843
|
-
const nodeResult = this.db.execute("SELECT properties FROM nodes WHERE id = ?", [nodeId]);
|
|
8638
|
+
const nodeResult = this.db.execute("SELECT id, properties FROM nodes WHERE id = ?", [nodeId]);
|
|
7844
8639
|
if (nodeResult.rows.length > 0) {
|
|
7845
8640
|
const row = nodeResult.rows[0];
|
|
7846
|
-
values.push(
|
|
8641
|
+
values.push(this.getNodeProperties(row.id, row.properties));
|
|
7847
8642
|
}
|
|
7848
8643
|
}
|
|
7849
8644
|
}
|
|
@@ -7949,9 +8744,7 @@ export class Executor {
|
|
|
7949
8744
|
if (nodeResult.rows.length > 0) {
|
|
7950
8745
|
const row = nodeResult.rows[0];
|
|
7951
8746
|
// Neo4j 3.5 format: return properties directly
|
|
7952
|
-
resultRow[alias] =
|
|
7953
|
-
? JSON.parse(row.properties)
|
|
7954
|
-
: row.properties;
|
|
8747
|
+
resultRow[alias] = this.getNodeProperties(row.id, row.properties);
|
|
7955
8748
|
}
|
|
7956
8749
|
else {
|
|
7957
8750
|
// Try edges
|
|
@@ -7959,9 +8752,7 @@ export class Executor {
|
|
|
7959
8752
|
if (edgeResult.rows.length > 0) {
|
|
7960
8753
|
const row = edgeResult.rows[0];
|
|
7961
8754
|
// Neo4j 3.5 format: return properties directly
|
|
7962
|
-
resultRow[alias] =
|
|
7963
|
-
? JSON.parse(row.properties)
|
|
7964
|
-
: row.properties;
|
|
8755
|
+
resultRow[alias] = this.getEdgeProperties(row.id, row.properties);
|
|
7965
8756
|
}
|
|
7966
8757
|
}
|
|
7967
8758
|
}
|
|
@@ -8174,6 +8965,8 @@ export class Executor {
|
|
|
8174
8965
|
if (nodeResult.changes === 0) {
|
|
8175
8966
|
this.db.execute(`UPDATE edges SET properties = ? WHERE id = ?`, [JSON.stringify(filteredProps), nodeId]);
|
|
8176
8967
|
}
|
|
8968
|
+
// Invalidate cache after UPDATE
|
|
8969
|
+
this.invalidatePropertyCache(nodeId);
|
|
8177
8970
|
continue;
|
|
8178
8971
|
}
|
|
8179
8972
|
// Handle SET n += {props} - merge properties
|
|
@@ -8207,6 +9000,8 @@ export class Executor {
|
|
|
8207
9000
|
this.db.execute(`UPDATE edges SET properties = json_patch(properties, ?) WHERE id = ?`, [JSON.stringify(nonNullProps), nodeId]);
|
|
8208
9001
|
}
|
|
8209
9002
|
}
|
|
9003
|
+
// Invalidate cache after UPDATE
|
|
9004
|
+
this.invalidatePropertyCache(nodeId);
|
|
8210
9005
|
continue;
|
|
8211
9006
|
}
|
|
8212
9007
|
// Handle property assignments
|
|
@@ -8214,7 +9009,7 @@ export class Executor {
|
|
|
8214
9009
|
throw new Error(`Invalid SET assignment for variable: ${assignment.variable}`);
|
|
8215
9010
|
}
|
|
8216
9011
|
// Use context-aware evaluation for expressions that may reference properties
|
|
8217
|
-
const value = assignment.value.type === "binary" || assignment.value.type === "property"
|
|
9012
|
+
const value = assignment.value.type === "binary" || assignment.value.type === "property" || assignment.value.type === "case"
|
|
8218
9013
|
? this.evaluateExpressionWithContext(assignment.value, params, resolvedIds)
|
|
8219
9014
|
: this.evaluateExpression(assignment.value, params);
|
|
8220
9015
|
// If value is null, remove the property instead of setting it to null
|
|
@@ -8234,6 +9029,8 @@ export class Executor {
|
|
|
8234
9029
|
this.db.execute(`UPDATE edges SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
|
|
8235
9030
|
}
|
|
8236
9031
|
}
|
|
9032
|
+
// Invalidate cache after UPDATE
|
|
9033
|
+
this.invalidatePropertyCache(nodeId);
|
|
8237
9034
|
}
|
|
8238
9035
|
}
|
|
8239
9036
|
/**
|
|
@@ -8289,6 +9086,28 @@ export class Executor {
|
|
|
8289
9086
|
}
|
|
8290
9087
|
return deletedVariables;
|
|
8291
9088
|
}
|
|
9089
|
+
/**
|
|
9090
|
+
* Build a row context from resolved node/edge IDs for condition evaluation
|
|
9091
|
+
*/
|
|
9092
|
+
buildRowFromResolvedIds(resolvedIds) {
|
|
9093
|
+
const row = {};
|
|
9094
|
+
for (const [varName, entityId] of Object.entries(resolvedIds)) {
|
|
9095
|
+
// Try to get properties from nodes first
|
|
9096
|
+
const nodeResult = this.db.execute(`SELECT properties FROM nodes WHERE id = ?`, [entityId]);
|
|
9097
|
+
if (nodeResult.rows.length > 0) {
|
|
9098
|
+
const props = JSON.parse(nodeResult.rows[0].properties || "{}");
|
|
9099
|
+
row[varName] = props;
|
|
9100
|
+
continue;
|
|
9101
|
+
}
|
|
9102
|
+
// Try edges
|
|
9103
|
+
const edgeResult = this.db.execute(`SELECT properties FROM edges WHERE id = ?`, [entityId]);
|
|
9104
|
+
if (edgeResult.rows.length > 0) {
|
|
9105
|
+
const props = JSON.parse(edgeResult.rows[0].properties || "{}");
|
|
9106
|
+
row[varName] = props;
|
|
9107
|
+
}
|
|
9108
|
+
}
|
|
9109
|
+
return row;
|
|
9110
|
+
}
|
|
8292
9111
|
/**
|
|
8293
9112
|
* Evaluate an expression to get its value
|
|
8294
9113
|
* Note: For property and binary expressions that reference nodes, use evaluateExpressionWithContext
|
|
@@ -8399,6 +9218,19 @@ export class Executor {
|
|
|
8399
9218
|
const args = expr.args || [];
|
|
8400
9219
|
return this.evaluateFunctionInProperty(funcName, args, params, {});
|
|
8401
9220
|
}
|
|
9221
|
+
case "case": {
|
|
9222
|
+
// Evaluate CASE WHEN ... THEN ... ELSE ... END
|
|
9223
|
+
// Build a row context from resolvedIds for condition evaluation
|
|
9224
|
+
const row = this.buildRowFromResolvedIds(resolvedIds);
|
|
9225
|
+
if (expr.whens) {
|
|
9226
|
+
for (const when of expr.whens) {
|
|
9227
|
+
if (this.evaluateWhereConditionOnRow(when.condition, row, params)) {
|
|
9228
|
+
return this.evaluateExpressionWithContext(when.result, params, resolvedIds);
|
|
9229
|
+
}
|
|
9230
|
+
}
|
|
9231
|
+
}
|
|
9232
|
+
return expr.elseExpr ? this.evaluateExpressionWithContext(expr.elseExpr, params, resolvedIds) : null;
|
|
9233
|
+
}
|
|
8402
9234
|
default:
|
|
8403
9235
|
throw new Error(`Cannot evaluate expression of type ${expr.type}`);
|
|
8404
9236
|
}
|
|
@@ -8588,21 +9420,26 @@ export class Executor {
|
|
|
8588
9420
|
/**
|
|
8589
9421
|
* Generate SQL condition for label matching
|
|
8590
9422
|
* Supports both single and multiple labels
|
|
9423
|
+
* Uses primary label index with fallback for secondary labels
|
|
8591
9424
|
*/
|
|
8592
9425
|
generateLabelCondition(label) {
|
|
8593
9426
|
const labels = Array.isArray(label) ? label : [label];
|
|
8594
9427
|
if (labels.length === 1) {
|
|
8595
9428
|
return {
|
|
8596
|
-
sql: `EXISTS (SELECT 1 FROM json_each(label) WHERE value = ?)`,
|
|
8597
|
-
params: [labels[0]]
|
|
9429
|
+
sql: `(json_extract(label, '$[0]') = ? OR EXISTS (SELECT 1 FROM json_each(label) WHERE value = ? AND json_extract(label, '$[0]') != ?))`,
|
|
9430
|
+
params: [labels[0], labels[0], labels[0]]
|
|
8598
9431
|
};
|
|
8599
9432
|
}
|
|
8600
9433
|
else {
|
|
8601
|
-
// Multiple labels: all must exist
|
|
8602
|
-
const conditions = labels.map(() => `EXISTS (SELECT 1 FROM json_each(label) WHERE value = ?)`);
|
|
9434
|
+
// Multiple labels: all must exist (each uses indexed primary label check with fallback)
|
|
9435
|
+
const conditions = labels.map(() => `(json_extract(label, '$[0]') = ? OR EXISTS (SELECT 1 FROM json_each(label) WHERE value = ? AND json_extract(label, '$[0]') != ?))`);
|
|
9436
|
+
const params = [];
|
|
9437
|
+
for (const l of labels) {
|
|
9438
|
+
params.push(l, l, l);
|
|
9439
|
+
}
|
|
8603
9440
|
return {
|
|
8604
9441
|
sql: conditions.join(" AND "),
|
|
8605
|
-
params
|
|
9442
|
+
params
|
|
8606
9443
|
};
|
|
8607
9444
|
}
|
|
8608
9445
|
}
|