leangraph 1.0.1 → 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/README.md +198 -111
- package/dist/auth.d.ts +1 -4
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +5 -15
- package/dist/auth.js.map +1 -1
- package/dist/backup.d.ts +1 -3
- package/dist/backup.d.ts.map +1 -1
- package/dist/backup.js +10 -15
- package/dist/backup.js.map +1 -1
- package/dist/cli-helpers.d.ts +2 -3
- package/dist/cli-helpers.d.ts.map +1 -1
- package/dist/cli-helpers.js +11 -30
- package/dist/cli-helpers.js.map +1 -1
- package/dist/cli.js +82 -129
- package/dist/cli.js.map +1 -1
- package/dist/db.d.ts +9 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +82 -9
- package/dist/db.js.map +1 -1
- package/dist/executor.d.ts +114 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +1248 -341
- package/dist/executor.js.map +1 -1
- package/dist/index.d.ts +8 -34
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -38
- package/dist/index.js.map +1 -1
- package/dist/local.d.ts +3 -3
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +13 -15
- package/dist/local.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/remote.d.ts +3 -3
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +8 -10
- package/dist/remote.js.map +1 -1
- package/dist/routes.d.ts +0 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +15 -39
- package/dist/routes.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/translator.d.ts +36 -0
- package/dist/translator.d.ts.map +1 -1
- package/dist/translator.js +634 -136
- package/dist/translator.js.map +1 -1
- package/dist/types.d.ts +24 -26
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/types.js.map +1 -1
- package/package.json +8 -2
package/dist/executor.js
CHANGED
|
@@ -59,12 +59,16 @@ function createEmptyContext() {
|
|
|
59
59
|
/**
|
|
60
60
|
* Clone a phase context
|
|
61
61
|
*/
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Clone a phase context with optimized row cloning
|
|
64
|
+
* For read-only operations, we can avoid the expensive row cloning
|
|
65
|
+
*/
|
|
66
|
+
function cloneContext(ctx, cloneRows = true) {
|
|
63
67
|
return {
|
|
64
68
|
nodeIds: new Map(ctx.nodeIds),
|
|
65
69
|
edgeIds: new Map(ctx.edgeIds),
|
|
66
70
|
values: new Map(ctx.values),
|
|
67
|
-
rows: ctx.rows.map(row => new Map(row)),
|
|
71
|
+
rows: cloneRows ? ctx.rows.map(row => new Map(row)) : ctx.rows,
|
|
68
72
|
};
|
|
69
73
|
}
|
|
70
74
|
// ============================================================================
|
|
@@ -72,14 +76,113 @@ function cloneContext(ctx) {
|
|
|
72
76
|
// ============================================================================
|
|
73
77
|
export class Executor {
|
|
74
78
|
db;
|
|
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();
|
|
75
83
|
constructor(db) {
|
|
76
84
|
this.db = db;
|
|
77
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Get node properties from cache or parse from JSON string and cache them
|
|
88
|
+
*/
|
|
89
|
+
getNodeProperties(nodeId, propsJson) {
|
|
90
|
+
let props = this.propertyCache.get(nodeId);
|
|
91
|
+
if (!props) {
|
|
92
|
+
props = typeof propsJson === "string" ? JSON.parse(propsJson) : propsJson;
|
|
93
|
+
if (props && typeof props === "object" && !Array.isArray(props)) {
|
|
94
|
+
this.propertyCache.set(nodeId, props);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Fallback for invalid data
|
|
98
|
+
props = {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return props || {};
|
|
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
|
+
}
|
|
78
177
|
/**
|
|
79
178
|
* Execute a Cypher query and return formatted results
|
|
80
179
|
*/
|
|
81
180
|
execute(cypher, params = {}) {
|
|
82
181
|
const startTime = performance.now();
|
|
182
|
+
// Clear property caches at start of each query execution
|
|
183
|
+
this.propertyCache.clear();
|
|
184
|
+
this.edgePropertyCache.clear();
|
|
185
|
+
this.edgeInfoCache.clear();
|
|
83
186
|
try {
|
|
84
187
|
// 1. Parse the Cypher query
|
|
85
188
|
const parseResult = parse(cypher);
|
|
@@ -94,124 +197,94 @@ export class Executor {
|
|
|
94
197
|
},
|
|
95
198
|
};
|
|
96
199
|
}
|
|
97
|
-
// 2.
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
data: phasedResult,
|
|
104
|
-
meta: {
|
|
105
|
-
count: phasedResult.length,
|
|
106
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
// 2.1. Check for UNWIND with CREATE pattern (needs special handling)
|
|
111
|
-
const unwindCreateResult = this.tryUnwindCreateExecution(parseResult.query, params);
|
|
112
|
-
if (unwindCreateResult !== null) {
|
|
113
|
-
const endTime = performance.now();
|
|
114
|
-
return {
|
|
115
|
-
success: true,
|
|
116
|
-
data: unwindCreateResult,
|
|
117
|
-
meta: {
|
|
118
|
-
count: unwindCreateResult.length,
|
|
119
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
// 2.2. Check for UNWIND with MERGE pattern (needs special handling)
|
|
124
|
-
const unwindMergeResult = this.tryUnwindMergeExecution(parseResult.query, params);
|
|
125
|
-
if (unwindMergeResult !== null) {
|
|
126
|
-
const endTime = performance.now();
|
|
127
|
-
return {
|
|
128
|
-
success: true,
|
|
129
|
-
data: unwindMergeResult,
|
|
130
|
-
meta: {
|
|
131
|
-
count: unwindMergeResult.length,
|
|
132
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
// 2.3. Check for MATCH+WITH(COLLECT)+UNWIND+RETURN pattern (needs subquery for aggregates)
|
|
137
|
-
const collectUnwindResult = this.tryCollectUnwindExecution(parseResult.query, params);
|
|
138
|
-
if (collectUnwindResult !== null) {
|
|
139
|
-
const endTime = performance.now();
|
|
140
|
-
return {
|
|
141
|
-
success: true,
|
|
142
|
-
data: collectUnwindResult,
|
|
143
|
-
meta: {
|
|
144
|
-
count: collectUnwindResult.length,
|
|
145
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
146
|
-
},
|
|
147
|
-
};
|
|
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);
|
|
148
206
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (collectDeleteResult !== null) {
|
|
152
|
-
const endTime = performance.now();
|
|
153
|
-
return {
|
|
154
|
-
success: true,
|
|
155
|
-
data: collectDeleteResult,
|
|
156
|
-
meta: {
|
|
157
|
-
count: collectDeleteResult.length,
|
|
158
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
159
|
-
},
|
|
160
|
-
};
|
|
207
|
+
if (flags.hasSet) {
|
|
208
|
+
this.validateSetClauseValueVariables(parseResult.query, params);
|
|
161
209
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (createReturnResult !== null) {
|
|
165
|
-
const endTime = performance.now();
|
|
166
|
-
return {
|
|
167
|
-
success: true,
|
|
168
|
-
data: createReturnResult,
|
|
169
|
-
meta: {
|
|
170
|
-
count: createReturnResult.length,
|
|
171
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
172
|
-
},
|
|
173
|
-
};
|
|
210
|
+
if (flags.hasWith || flags.hasReturn) {
|
|
211
|
+
this.validateOrderByVariables(parseResult.query, params);
|
|
174
212
|
}
|
|
175
|
-
//
|
|
176
|
-
const
|
|
177
|
-
if (mergeResult !== null) {
|
|
213
|
+
// Helper to return successful result
|
|
214
|
+
const makeResult = (data) => {
|
|
178
215
|
const endTime = performance.now();
|
|
179
216
|
return {
|
|
180
217
|
success: true,
|
|
181
|
-
data
|
|
218
|
+
data,
|
|
182
219
|
meta: {
|
|
183
|
-
count:
|
|
184
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
185
|
-
},
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
// 2.6. Check for MATCH...WITH...MATCH pattern with bound relationship list
|
|
189
|
-
// e.g., MATCH ()-[r1]->()-[r2]->() WITH [r1, r2] AS rs MATCH (a)-[rs*]->(b) RETURN a, b
|
|
190
|
-
const boundRelListResult = this.tryBoundRelationshipListExecution(parseResult.query, params);
|
|
191
|
-
if (boundRelListResult !== null) {
|
|
192
|
-
const endTime = performance.now();
|
|
193
|
-
return {
|
|
194
|
-
success: true,
|
|
195
|
-
data: boundRelListResult,
|
|
196
|
-
meta: {
|
|
197
|
-
count: boundRelListResult.length,
|
|
198
|
-
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
199
|
-
},
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
// 3. Check if this is a pattern that needs multi-phase execution
|
|
203
|
-
// (MATCH...CREATE, MATCH...SET, MATCH...DELETE with relationship patterns)
|
|
204
|
-
const multiPhaseResult = this.tryMultiPhaseExecution(parseResult.query, params);
|
|
205
|
-
if (multiPhaseResult !== null) {
|
|
206
|
-
const endTime = performance.now();
|
|
207
|
-
return {
|
|
208
|
-
success: true,
|
|
209
|
-
data: multiPhaseResult,
|
|
210
|
-
meta: {
|
|
211
|
-
count: multiPhaseResult.length,
|
|
220
|
+
count: data.length,
|
|
212
221
|
time_ms: Math.round((endTime - startTime) * 100) / 100,
|
|
213
222
|
},
|
|
214
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
|
|
215
288
|
}
|
|
216
289
|
// 3. Standard single-phase execution: Translate to SQL
|
|
217
290
|
const translator = new Translator(params);
|
|
@@ -250,6 +323,300 @@ export class Executor {
|
|
|
250
323
|
}
|
|
251
324
|
}
|
|
252
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
|
+
// ============================================================================
|
|
253
620
|
// Phase-Based Execution
|
|
254
621
|
// ============================================================================
|
|
255
622
|
/**
|
|
@@ -264,12 +631,8 @@ export class Executor {
|
|
|
264
631
|
*/
|
|
265
632
|
tryPhasedExecution(query, params) {
|
|
266
633
|
const phases = this.detectPhases(query);
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
// Semantic validation: SET expressions cannot reference undefined variables
|
|
270
|
-
this.validateSetClauseValueVariables(query, params);
|
|
271
|
-
// Semantic validation: ORDER BY expressions cannot reference undefined or out-of-scope variables
|
|
272
|
-
this.validateOrderByVariables(query, params);
|
|
634
|
+
// Note: Semantic validations (validateMergeVariables, validateSetClauseValueVariables,
|
|
635
|
+
// validateOrderByVariables) are now called in execute() before classification
|
|
273
636
|
// Check if we need phased execution for MATCH + MERGE combinations
|
|
274
637
|
// These need special handling for proper Cartesian product semantics
|
|
275
638
|
// BUT: If MERGE has ON CREATE SET or ON MATCH SET, let tryMergeExecution handle it
|
|
@@ -761,6 +1124,30 @@ export class Executor {
|
|
|
761
1124
|
break;
|
|
762
1125
|
}
|
|
763
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
|
+
}
|
|
764
1151
|
}
|
|
765
1152
|
// Check if MATCH after a WITH with aggregates - needs phase boundary
|
|
766
1153
|
// This handles patterns like:
|
|
@@ -1269,7 +1656,9 @@ export class Executor {
|
|
|
1269
1656
|
* Execute a single phase with the given context
|
|
1270
1657
|
*/
|
|
1271
1658
|
executePhase(clauses, inputContext, params, isLastPhase) {
|
|
1272
|
-
|
|
1659
|
+
// Check if this phase contains only read-only clauses
|
|
1660
|
+
const hasWriteClauses = clauses.some(clause => !this.isReadOnlyClause(clause));
|
|
1661
|
+
let context = cloneContext(inputContext, hasWriteClauses);
|
|
1273
1662
|
for (const clause of clauses) {
|
|
1274
1663
|
context = this.executeClause(clause, context, params);
|
|
1275
1664
|
}
|
|
@@ -1278,25 +1667,32 @@ export class Executor {
|
|
|
1278
1667
|
/**
|
|
1279
1668
|
* Execute a single clause and update context
|
|
1280
1669
|
*/
|
|
1670
|
+
isReadOnlyClause(clause) {
|
|
1671
|
+
// These clauses are read-only and don't modify the context structure
|
|
1672
|
+
return clause.type === "MATCH" || clause.type === "OPTIONAL_MATCH" || clause.type === "RETURN";
|
|
1673
|
+
}
|
|
1281
1674
|
executeClause(clause, context, params) {
|
|
1675
|
+
// For read-only clauses, avoid the expensive row cloning operation
|
|
1676
|
+
const cloneRows = !this.isReadOnlyClause(clause);
|
|
1677
|
+
const newContext = cloneContext(context, cloneRows);
|
|
1282
1678
|
switch (clause.type) {
|
|
1283
1679
|
case "CREATE":
|
|
1284
|
-
return this.executeCreateClause(clause,
|
|
1680
|
+
return this.executeCreateClause(clause, newContext, params);
|
|
1285
1681
|
case "MERGE":
|
|
1286
|
-
return this.executeMergeClause(clause,
|
|
1682
|
+
return this.executeMergeClause(clause, newContext, params);
|
|
1287
1683
|
case "WITH":
|
|
1288
|
-
return this.executeWithClause(clause,
|
|
1684
|
+
return this.executeWithClause(clause, newContext, params);
|
|
1289
1685
|
case "UNWIND":
|
|
1290
|
-
return this.executeUnwindClause(clause,
|
|
1686
|
+
return this.executeUnwindClause(clause, newContext, params);
|
|
1291
1687
|
case "MATCH":
|
|
1292
1688
|
case "OPTIONAL_MATCH":
|
|
1293
|
-
return this.executeMatchClause(clause,
|
|
1689
|
+
return this.executeMatchClause(clause, newContext, params);
|
|
1294
1690
|
case "RETURN":
|
|
1295
|
-
return this.executeReturnClause(clause,
|
|
1691
|
+
return this.executeReturnClause(clause, newContext, params);
|
|
1296
1692
|
case "SET":
|
|
1297
|
-
return this.executeSetClause(clause,
|
|
1693
|
+
return this.executeSetClause(clause, newContext, params);
|
|
1298
1694
|
case "DELETE":
|
|
1299
|
-
return this.executeDeleteClause(clause,
|
|
1695
|
+
return this.executeDeleteClause(clause, newContext, params);
|
|
1300
1696
|
default:
|
|
1301
1697
|
// For unsupported clause types, return context unchanged
|
|
1302
1698
|
return context;
|
|
@@ -1424,12 +1820,12 @@ export class Executor {
|
|
|
1424
1820
|
// Build query to find existing matching nodes
|
|
1425
1821
|
const conditions = [];
|
|
1426
1822
|
const conditionParams = [];
|
|
1427
|
-
// Label condition
|
|
1823
|
+
// Label condition (uses primary label index with fallback for secondary labels)
|
|
1428
1824
|
if (pattern.label) {
|
|
1429
1825
|
const labels = Array.isArray(pattern.label) ? pattern.label : [pattern.label];
|
|
1430
1826
|
for (const label of labels) {
|
|
1431
|
-
conditions.push(`EXISTS (SELECT 1 FROM json_each(label) WHERE value = ?)`);
|
|
1432
|
-
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);
|
|
1433
1829
|
}
|
|
1434
1830
|
}
|
|
1435
1831
|
// Property conditions
|
|
@@ -1448,9 +1844,7 @@ export class Executor {
|
|
|
1448
1844
|
for (const row of findResult.rows) {
|
|
1449
1845
|
const outputRow = new Map(inputRow);
|
|
1450
1846
|
if (pattern.variable) {
|
|
1451
|
-
const nodeProps = typeof row.properties === "string"
|
|
1452
|
-
? JSON.parse(row.properties)
|
|
1453
|
-
: row.properties;
|
|
1847
|
+
const nodeProps = this.getNodeProperties(typeof row.id === "string" ? row.id : "", typeof row.properties === "string" || (typeof row.properties === "object" && row.properties !== null) ? row.properties : "{}");
|
|
1454
1848
|
const nodeObj = { ...nodeProps, _nf_id: row.id };
|
|
1455
1849
|
outputRow.set(pattern.variable, nodeObj);
|
|
1456
1850
|
}
|
|
@@ -1544,9 +1938,7 @@ export class Executor {
|
|
|
1544
1938
|
for (const edgeRow of findResult.rows) {
|
|
1545
1939
|
const outputRow = new Map(inputRow);
|
|
1546
1940
|
if (pattern.edge.variable) {
|
|
1547
|
-
const props =
|
|
1548
|
-
? JSON.parse(edgeRow.properties)
|
|
1549
|
-
: edgeRow.properties;
|
|
1941
|
+
const props = this.getEdgeProperties(edgeRow.id, edgeRow.properties);
|
|
1550
1942
|
// Include _nf_start and _nf_end for startNode() and endNode() functions
|
|
1551
1943
|
// Use the actual source/target from the found edge (may be reversed for undirected)
|
|
1552
1944
|
outputRow.set(pattern.edge.variable, {
|
|
@@ -1983,7 +2375,9 @@ export class Executor {
|
|
|
1983
2375
|
// Get variables referenced in WHERE that are context variables
|
|
1984
2376
|
const whereReferencesContext = clause.where ?
|
|
1985
2377
|
this.whereReferencesContextVars(clause.where, contextVarNames, introducedVars) : false;
|
|
1986
|
-
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) {
|
|
1987
2381
|
// For complex patterns (multi-hop, anonymous nodes), use SQL translation with
|
|
1988
2382
|
// constraints for bound variables. This handles patterns like:
|
|
1989
2383
|
// WITH me, you MATCH (me)-[r1:ATE]->()<-[r2:ATE]-(you)
|
|
@@ -2122,9 +2516,7 @@ export class Executor {
|
|
|
2122
2516
|
for (const row of edgeResult.rows) {
|
|
2123
2517
|
const matchRow = new Map();
|
|
2124
2518
|
if (edgeVar) {
|
|
2125
|
-
const edgeProps =
|
|
2126
|
-
? JSON.parse(row.properties)
|
|
2127
|
-
: row.properties;
|
|
2519
|
+
const edgeProps = this.getEdgeProperties(row.id, row.properties);
|
|
2128
2520
|
matchRow.set(edgeVar, { ...edgeProps, _nf_id: row.id });
|
|
2129
2521
|
}
|
|
2130
2522
|
results.push(matchRow);
|
|
@@ -2159,9 +2551,55 @@ export class Executor {
|
|
|
2159
2551
|
// Transform the clause to substitute context variable references with parameters
|
|
2160
2552
|
// This handles cases like: WITH x AS foo MATCH (n) WHERE n.id = foo
|
|
2161
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
|
+
}
|
|
2162
2599
|
// Build a MATCH + RETURN query for all pattern variables
|
|
2163
2600
|
const matchQuery = {
|
|
2164
2601
|
clauses: [
|
|
2602
|
+
...prefixMatchClauses,
|
|
2165
2603
|
transformedClause,
|
|
2166
2604
|
{
|
|
2167
2605
|
type: "RETURN",
|
|
@@ -2170,9 +2608,17 @@ export class Executor {
|
|
|
2170
2608
|
],
|
|
2171
2609
|
};
|
|
2172
2610
|
// Create params with context values prefixed with _ctx_
|
|
2611
|
+
// For node variables, extract the node ID for use in id() comparisons
|
|
2173
2612
|
const mergedParams = { ...params };
|
|
2174
2613
|
for (const [key, value] of inputRow) {
|
|
2175
|
-
|
|
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
|
+
}
|
|
2176
2622
|
}
|
|
2177
2623
|
// Translate to SQL with merged parameters
|
|
2178
2624
|
const translator = new Translator(mergedParams);
|
|
@@ -2237,19 +2683,126 @@ export class Executor {
|
|
|
2237
2683
|
* Transform a MATCH clause to substitute context variable references with parameter references.
|
|
2238
2684
|
* This converts WHERE conditions like `n.id = foo` (where foo is from context)
|
|
2239
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.
|
|
2240
2691
|
*/
|
|
2241
2692
|
transformClauseForContext(clause, contextVars) {
|
|
2242
|
-
if (!clause.where) {
|
|
2243
|
-
return clause;
|
|
2244
|
-
}
|
|
2245
2693
|
// Deep clone the clause
|
|
2246
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
|
+
}
|
|
2247
2700
|
// Transform WHERE condition to use parameter references for context variables
|
|
2248
2701
|
if (transformed.where) {
|
|
2249
2702
|
transformed.where = this.transformWhereForContext(transformed.where, contextVars);
|
|
2250
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
|
+
}
|
|
2251
2763
|
return transformed;
|
|
2252
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
|
+
}
|
|
2253
2806
|
/**
|
|
2254
2807
|
* Transform a WHERE condition to substitute context variable references with parameter references
|
|
2255
2808
|
*/
|
|
@@ -2406,56 +2959,24 @@ export class Executor {
|
|
|
2406
2959
|
}
|
|
2407
2960
|
// Follow the sequence of relationships to find the path endpoints
|
|
2408
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);
|
|
2409
2971
|
let currentNodeId = null;
|
|
2410
2972
|
let firstNodeId = null;
|
|
2411
2973
|
let lastNodeId = null;
|
|
2412
2974
|
let valid = true;
|
|
2413
2975
|
for (let i = 0; i < relList.length; i++) {
|
|
2414
2976
|
const rel = relList[i];
|
|
2415
|
-
// Extract edge
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
// Edge object from MATCH - may have _nf_id instead of id
|
|
2419
|
-
const relObj = rel;
|
|
2420
|
-
const edgeId = (relObj._nf_id || relObj.id);
|
|
2421
|
-
if (edgeId) {
|
|
2422
|
-
// Look up the edge to get source/target
|
|
2423
|
-
const edgeResult = this.db.execute("SELECT id, source_id, target_id FROM edges WHERE id = ?", [edgeId]);
|
|
2424
|
-
if (edgeResult.rows.length > 0) {
|
|
2425
|
-
const row = edgeResult.rows[0];
|
|
2426
|
-
edgeInfo = {
|
|
2427
|
-
id: row.id,
|
|
2428
|
-
source_id: row.source_id,
|
|
2429
|
-
target_id: row.target_id
|
|
2430
|
-
};
|
|
2431
|
-
}
|
|
2432
|
-
}
|
|
2433
|
-
}
|
|
2434
|
-
else if (typeof rel === "string") {
|
|
2435
|
-
// Could be a JSON string like '{"_nf_id":"uuid"}' or a raw UUID string
|
|
2436
|
-
let edgeId = null;
|
|
2437
|
-
try {
|
|
2438
|
-
const parsed = JSON.parse(rel);
|
|
2439
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
2440
|
-
edgeId = (parsed._nf_id || parsed.id);
|
|
2441
|
-
}
|
|
2442
|
-
}
|
|
2443
|
-
catch {
|
|
2444
|
-
// Not JSON, assume it's a raw ID
|
|
2445
|
-
edgeId = rel;
|
|
2446
|
-
}
|
|
2447
|
-
if (edgeId) {
|
|
2448
|
-
const edgeResult = this.db.execute("SELECT id, source_id, target_id FROM edges WHERE id = ?", [edgeId]);
|
|
2449
|
-
if (edgeResult.rows.length > 0) {
|
|
2450
|
-
const row = edgeResult.rows[0];
|
|
2451
|
-
edgeInfo = {
|
|
2452
|
-
id: row.id,
|
|
2453
|
-
source_id: row.source_id,
|
|
2454
|
-
target_id: row.target_id
|
|
2455
|
-
};
|
|
2456
|
-
}
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2977
|
+
// Extract edge ID and get info from batch result
|
|
2978
|
+
const edgeId = this.extractEdgeId(rel);
|
|
2979
|
+
const edgeInfo = edgeId ? edgeInfoMap.get(edgeId) : null;
|
|
2459
2980
|
if (!edgeInfo) {
|
|
2460
2981
|
valid = false;
|
|
2461
2982
|
break;
|
|
@@ -2533,12 +3054,8 @@ export class Executor {
|
|
|
2533
3054
|
const firstNode = firstNodeResult.rows[0];
|
|
2534
3055
|
const lastNode = lastNodeResult.rows[0];
|
|
2535
3056
|
// Format nodes like the translator does (with _nf_id embedded)
|
|
2536
|
-
const firstProps =
|
|
2537
|
-
|
|
2538
|
-
: firstNode.properties;
|
|
2539
|
-
const lastProps = typeof lastNode.properties === "string"
|
|
2540
|
-
? JSON.parse(lastNode.properties)
|
|
2541
|
-
: lastNode.properties;
|
|
3057
|
+
const firstProps = this.getNodeProperties(firstNode.id, firstNode.properties);
|
|
3058
|
+
const lastProps = this.getNodeProperties(lastNode.id, lastNode.properties);
|
|
2542
3059
|
outputRow.set(boundInfo.sourceVar, { ...firstProps, _nf_id: firstNode.id });
|
|
2543
3060
|
outputRow.set(boundInfo.targetVar, { ...lastProps, _nf_id: lastNode.id });
|
|
2544
3061
|
newRows.push(outputRow);
|
|
@@ -2712,6 +3229,8 @@ export class Executor {
|
|
|
2712
3229
|
if (assignment.property && assignment.value) {
|
|
2713
3230
|
const value = this.evaluateExpressionInRow(assignment.value, row, params);
|
|
2714
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);
|
|
2715
3234
|
}
|
|
2716
3235
|
}
|
|
2717
3236
|
}
|
|
@@ -2723,7 +3242,9 @@ export class Executor {
|
|
|
2723
3242
|
executeDeleteClause(clause, context, params) {
|
|
2724
3243
|
for (const row of context.rows) {
|
|
2725
3244
|
for (const variable of clause.variables) {
|
|
2726
|
-
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);
|
|
2727
3248
|
if (!id)
|
|
2728
3249
|
continue;
|
|
2729
3250
|
if (clause.detach) {
|
|
@@ -2899,6 +3420,27 @@ export class Executor {
|
|
|
2899
3420
|
return null;
|
|
2900
3421
|
}
|
|
2901
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
|
+
}
|
|
2902
3444
|
case "comparison": {
|
|
2903
3445
|
// Evaluate comparison expression: left op right
|
|
2904
3446
|
const left = this.evaluateExpressionInRow(expr.left, row, params);
|
|
@@ -3007,7 +3549,12 @@ export class Executor {
|
|
|
3007
3549
|
}
|
|
3008
3550
|
if (!edgeId)
|
|
3009
3551
|
return null;
|
|
3010
|
-
//
|
|
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
|
|
3011
3558
|
const result = this.db.execute("SELECT type FROM edges WHERE id = ?", [edgeId]);
|
|
3012
3559
|
if (result.rows.length > 0) {
|
|
3013
3560
|
return result.rows[0].type;
|
|
@@ -3026,10 +3573,17 @@ export class Executor {
|
|
|
3026
3573
|
startNodeId = edgeObj._nf_start;
|
|
3027
3574
|
}
|
|
3028
3575
|
else if ("_nf_id" in edgeObj) {
|
|
3029
|
-
//
|
|
3030
|
-
const
|
|
3031
|
-
if (
|
|
3032
|
-
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
|
+
}
|
|
3033
3587
|
}
|
|
3034
3588
|
}
|
|
3035
3589
|
}
|
|
@@ -3038,9 +3592,7 @@ export class Executor {
|
|
|
3038
3592
|
// Look up node from database
|
|
3039
3593
|
const nodeResult = this.db.execute("SELECT id, properties FROM nodes WHERE id = ?", [startNodeId]);
|
|
3040
3594
|
if (nodeResult.rows.length > 0) {
|
|
3041
|
-
const nodeProps =
|
|
3042
|
-
? JSON.parse(nodeResult.rows[0].properties)
|
|
3043
|
-
: nodeResult.rows[0].properties;
|
|
3595
|
+
const nodeProps = this.getNodeProperties(nodeResult.rows[0].id, nodeResult.rows[0].properties);
|
|
3044
3596
|
return { ...nodeProps, _nf_id: nodeResult.rows[0].id };
|
|
3045
3597
|
}
|
|
3046
3598
|
return null;
|
|
@@ -3057,10 +3609,17 @@ export class Executor {
|
|
|
3057
3609
|
endNodeId = edgeObj._nf_end;
|
|
3058
3610
|
}
|
|
3059
3611
|
else if ("_nf_id" in edgeObj) {
|
|
3060
|
-
//
|
|
3061
|
-
const
|
|
3062
|
-
if (
|
|
3063
|
-
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
|
+
}
|
|
3064
3623
|
}
|
|
3065
3624
|
}
|
|
3066
3625
|
}
|
|
@@ -3069,9 +3628,7 @@ export class Executor {
|
|
|
3069
3628
|
// Look up node from database
|
|
3070
3629
|
const nodeResult = this.db.execute("SELECT id, properties FROM nodes WHERE id = ?", [endNodeId]);
|
|
3071
3630
|
if (nodeResult.rows.length > 0) {
|
|
3072
|
-
const nodeProps =
|
|
3073
|
-
? JSON.parse(nodeResult.rows[0].properties)
|
|
3074
|
-
: nodeResult.rows[0].properties;
|
|
3631
|
+
const nodeProps = this.getNodeProperties(nodeResult.rows[0].id, nodeResult.rows[0].properties);
|
|
3075
3632
|
return { ...nodeProps, _nf_id: nodeResult.rows[0].id };
|
|
3076
3633
|
}
|
|
3077
3634
|
return null;
|
|
@@ -3558,27 +4115,66 @@ export class Executor {
|
|
|
3558
4115
|
// Also collect values for WITH aggregates (can be numbers or objects for collect)
|
|
3559
4116
|
const withAggregateValues = new Map();
|
|
3560
4117
|
this.db.transaction(() => {
|
|
3561
|
-
|
|
4118
|
+
// Collect all node inserts for batching
|
|
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();
|
|
4124
|
+
for (let comboIndex = 0; comboIndex < combinations.length; comboIndex++) {
|
|
4125
|
+
const combination = combinations[comboIndex];
|
|
3562
4126
|
// Build a map of unwind variable -> current value
|
|
3563
4127
|
const unwindContext = {};
|
|
3564
4128
|
for (let i = 0; i < unwindClauses.length; i++) {
|
|
3565
4129
|
unwindContext[unwindClauses[i].alias] = combination[i];
|
|
3566
4130
|
}
|
|
3567
|
-
// Execute CREATE with the unwind context
|
|
3568
|
-
const createdIds = new Map();
|
|
3569
4131
|
for (const createClause of createClauses) {
|
|
3570
4132
|
for (const pattern of createClause.patterns) {
|
|
3571
|
-
if (this.isRelationshipPattern(pattern)) {
|
|
3572
|
-
this
|
|
3573
|
-
}
|
|
3574
|
-
else {
|
|
4133
|
+
if (!this.isRelationshipPattern(pattern)) {
|
|
4134
|
+
// Only collect node patterns in this pass; relationships handled after nodes are inserted
|
|
3575
4135
|
const id = crypto.randomUUID();
|
|
3576
4136
|
const labelJson = this.normalizeLabelToJson(pattern.label);
|
|
3577
4137
|
const props = this.resolvePropertiesWithUnwind(pattern.properties || {}, params, unwindContext);
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
4138
|
+
nodeInserts.push({
|
|
4139
|
+
id,
|
|
4140
|
+
labelJson,
|
|
4141
|
+
propsJson: JSON.stringify(props),
|
|
4142
|
+
variable: pattern.variable,
|
|
4143
|
+
combinationIndex: comboIndex
|
|
4144
|
+
});
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
// Batch insert nodes (cap at 500 rows per statement)
|
|
4150
|
+
const BATCH_SIZE = 500;
|
|
4151
|
+
for (let i = 0; i < nodeInserts.length; i += BATCH_SIZE) {
|
|
4152
|
+
const batch = nodeInserts.slice(i, i + BATCH_SIZE);
|
|
4153
|
+
const placeholders = batch.map(() => '(?, ?, ?)').join(',');
|
|
4154
|
+
const values = batch.flatMap(insert => [insert.id, insert.labelJson, insert.propsJson]);
|
|
4155
|
+
this.db.execute(`INSERT INTO nodes (id, label, properties) VALUES ${placeholders}`, values);
|
|
4156
|
+
}
|
|
4157
|
+
// Process each combination with batched inserts
|
|
4158
|
+
for (let comboIndex = 0; comboIndex < combinations.length; comboIndex++) {
|
|
4159
|
+
const combination = combinations[comboIndex];
|
|
4160
|
+
// Build a map of unwind variable -> current value
|
|
4161
|
+
const unwindContext = {};
|
|
4162
|
+
for (let i = 0; i < unwindClauses.length; i++) {
|
|
4163
|
+
unwindContext[unwindClauses[i].alias] = combination[i];
|
|
4164
|
+
}
|
|
4165
|
+
// Execute CREATE with the unwind context
|
|
4166
|
+
const createdIds = new Map();
|
|
4167
|
+
// Add nodes from batch insert to createdIds
|
|
4168
|
+
for (const insert of nodeInserts) {
|
|
4169
|
+
if (insert.combinationIndex === comboIndex && insert.variable) {
|
|
4170
|
+
createdIds.set(insert.variable, insert.id);
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
// Handle relationships (they may reference nodes created in this combination)
|
|
4174
|
+
for (const createClause of createClauses) {
|
|
4175
|
+
for (const pattern of createClause.patterns) {
|
|
4176
|
+
if (this.isRelationshipPattern(pattern)) {
|
|
4177
|
+
this.executeCreateRelationshipPatternWithUnwind(pattern, createdIds, params, unwindContext, edgeInserts, pendingEdgeInfo);
|
|
3582
4178
|
}
|
|
3583
4179
|
}
|
|
3584
4180
|
}
|
|
@@ -3602,23 +4198,37 @@ export class Executor {
|
|
|
3602
4198
|
let value;
|
|
3603
4199
|
const id = createdIds.get(aggInfo.argVariable);
|
|
3604
4200
|
if (id) {
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
if (result.rows.length > 0) {
|
|
3610
|
-
const props = typeof result.rows[0].properties === "string"
|
|
3611
|
-
? JSON.parse(result.rows[0].properties)
|
|
3612
|
-
: 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;
|
|
3613
4205
|
if (aggInfo.argProperty) {
|
|
3614
|
-
// Collect property value
|
|
3615
4206
|
value = props[aggInfo.argProperty];
|
|
3616
4207
|
}
|
|
3617
4208
|
else {
|
|
3618
|
-
// Collect the whole node object (for collect(n))
|
|
3619
4209
|
value = props;
|
|
3620
4210
|
}
|
|
3621
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
|
+
}
|
|
3622
4232
|
}
|
|
3623
4233
|
if (value !== undefined) {
|
|
3624
4234
|
if (!withAggregateValues.has(alias)) {
|
|
@@ -3643,17 +4253,25 @@ export class Executor {
|
|
|
3643
4253
|
const property = arg.property;
|
|
3644
4254
|
const id = createdIds.get(variable);
|
|
3645
4255
|
if (id) {
|
|
3646
|
-
//
|
|
3647
|
-
|
|
3648
|
-
if (
|
|
3649
|
-
|
|
3650
|
-
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];
|
|
3651
4260
|
}
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
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
|
+
}
|
|
3657
4275
|
}
|
|
3658
4276
|
}
|
|
3659
4277
|
}
|
|
@@ -3688,9 +4306,7 @@ export class Executor {
|
|
|
3688
4306
|
if (nodeResult.rows.length > 0) {
|
|
3689
4307
|
const row = nodeResult.rows[0];
|
|
3690
4308
|
// Neo4j 3.5 format: return properties directly
|
|
3691
|
-
resultRow[alias] =
|
|
3692
|
-
? JSON.parse(row.properties)
|
|
3693
|
-
: row.properties;
|
|
4309
|
+
resultRow[alias] = this.getNodeProperties(row.id, row.properties);
|
|
3694
4310
|
}
|
|
3695
4311
|
}
|
|
3696
4312
|
}
|
|
@@ -3700,17 +4316,25 @@ export class Executor {
|
|
|
3700
4316
|
const property = item.expression.property;
|
|
3701
4317
|
const id = createdIds.get(variable);
|
|
3702
4318
|
if (id) {
|
|
3703
|
-
//
|
|
3704
|
-
|
|
3705
|
-
if (
|
|
3706
|
-
|
|
3707
|
-
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];
|
|
3708
4323
|
}
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
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
|
+
}
|
|
3714
4338
|
}
|
|
3715
4339
|
}
|
|
3716
4340
|
}
|
|
@@ -3721,6 +4345,14 @@ export class Executor {
|
|
|
3721
4345
|
}
|
|
3722
4346
|
}
|
|
3723
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
|
+
}
|
|
3724
4356
|
});
|
|
3725
4357
|
// Compute WITH aggregate results if RETURN references them
|
|
3726
4358
|
if (returnsWithAggregateAliases && returnClause) {
|
|
@@ -3899,14 +4531,15 @@ export class Executor {
|
|
|
3899
4531
|
const id = resolvedIds[variable];
|
|
3900
4532
|
if (id) {
|
|
3901
4533
|
// Try nodes first, then edges
|
|
3902
|
-
let result = this.db.execute("SELECT properties FROM nodes WHERE id = ?", [id]);
|
|
3903
|
-
|
|
3904
|
-
|
|
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]);
|
|
3905
4538
|
}
|
|
3906
4539
|
if (result.rows.length > 0) {
|
|
3907
|
-
const props =
|
|
3908
|
-
?
|
|
3909
|
-
: 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);
|
|
3910
4543
|
return props[property];
|
|
3911
4544
|
}
|
|
3912
4545
|
}
|
|
@@ -3942,15 +4575,16 @@ export class Executor {
|
|
|
3942
4575
|
const id = createdIds.get(variable);
|
|
3943
4576
|
if (id) {
|
|
3944
4577
|
// Try nodes first, then edges
|
|
3945
|
-
let result = this.db.execute("SELECT properties FROM nodes WHERE id = ?", [id]);
|
|
3946
|
-
|
|
4578
|
+
let result = this.db.execute("SELECT id, properties FROM nodes WHERE id = ?", [id]);
|
|
4579
|
+
let isNode = result.rows.length > 0;
|
|
4580
|
+
if (!isNode) {
|
|
3947
4581
|
// Try edges table
|
|
3948
|
-
result = this.db.execute("SELECT properties FROM edges WHERE id = ?", [id]);
|
|
4582
|
+
result = this.db.execute("SELECT id, properties FROM edges WHERE id = ?", [id]);
|
|
3949
4583
|
}
|
|
3950
4584
|
if (result.rows.length > 0) {
|
|
3951
|
-
const props =
|
|
3952
|
-
?
|
|
3953
|
-
: 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);
|
|
3954
4588
|
return props[property];
|
|
3955
4589
|
}
|
|
3956
4590
|
}
|
|
@@ -4038,8 +4672,9 @@ export class Executor {
|
|
|
4038
4672
|
let whereConditions = [];
|
|
4039
4673
|
let whereParams = [];
|
|
4040
4674
|
if (nodePattern.label) {
|
|
4041
|
-
|
|
4042
|
-
|
|
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);
|
|
4043
4678
|
}
|
|
4044
4679
|
for (const [key, value] of Object.entries(props)) {
|
|
4045
4680
|
whereConditions.push(`json_extract(properties, '$.${key}') = ?`);
|
|
@@ -4989,7 +5624,7 @@ export class Executor {
|
|
|
4989
5624
|
/**
|
|
4990
5625
|
* Execute CREATE relationship pattern with unwind context
|
|
4991
5626
|
*/
|
|
4992
|
-
executeCreateRelationshipPatternWithUnwind(rel, createdIds, params, unwindContext) {
|
|
5627
|
+
executeCreateRelationshipPatternWithUnwind(rel, createdIds, params, unwindContext, edgeInserts, pendingEdgeInfo) {
|
|
4993
5628
|
let sourceId;
|
|
4994
5629
|
let targetId;
|
|
4995
5630
|
// Determine source node ID
|
|
@@ -5032,7 +5667,30 @@ export class Executor {
|
|
|
5032
5667
|
const edgeId = crypto.randomUUID();
|
|
5033
5668
|
const edgeType = rel.edge.type || "";
|
|
5034
5669
|
const edgeProps = this.resolvePropertiesWithUnwind(rel.edge.properties || {}, params, unwindContext);
|
|
5035
|
-
|
|
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
|
+
}
|
|
5036
5694
|
if (rel.edge.variable) {
|
|
5037
5695
|
createdIds.set(rel.edge.variable, edgeId);
|
|
5038
5696
|
}
|
|
@@ -5116,9 +5774,7 @@ export class Executor {
|
|
|
5116
5774
|
if (nodeResult.rows.length > 0) {
|
|
5117
5775
|
const row = nodeResult.rows[0];
|
|
5118
5776
|
// Neo4j 3.5 format: return properties directly
|
|
5119
|
-
resultRow[alias] =
|
|
5120
|
-
? JSON.parse(row.properties)
|
|
5121
|
-
: row.properties;
|
|
5777
|
+
resultRow[alias] = this.getNodeProperties(row.id, row.properties);
|
|
5122
5778
|
}
|
|
5123
5779
|
}
|
|
5124
5780
|
}
|
|
@@ -5358,6 +6014,15 @@ export class Executor {
|
|
|
5358
6014
|
}
|
|
5359
6015
|
if (relList.length === 0)
|
|
5360
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);
|
|
5361
6026
|
// Follow the sequence of relationships to find endpoints
|
|
5362
6027
|
let currentNodeId = null;
|
|
5363
6028
|
let firstNodeId = null;
|
|
@@ -5365,45 +6030,9 @@ export class Executor {
|
|
|
5365
6030
|
let valid = true;
|
|
5366
6031
|
for (let i = 0; i < relList.length; i++) {
|
|
5367
6032
|
const rel = relList[i];
|
|
5368
|
-
// Extract edge info
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
const relObj = rel;
|
|
5372
|
-
const edgeId = (relObj._nf_id || relObj.id);
|
|
5373
|
-
if (edgeId) {
|
|
5374
|
-
const edgeResult = this.db.execute("SELECT id, source_id, target_id FROM edges WHERE id = ?", [edgeId]);
|
|
5375
|
-
if (edgeResult.rows.length > 0) {
|
|
5376
|
-
const r = edgeResult.rows[0];
|
|
5377
|
-
edgeInfo = {
|
|
5378
|
-
id: r.id,
|
|
5379
|
-
source_id: r.source_id,
|
|
5380
|
-
target_id: r.target_id
|
|
5381
|
-
};
|
|
5382
|
-
}
|
|
5383
|
-
}
|
|
5384
|
-
}
|
|
5385
|
-
else if (typeof rel === "string") {
|
|
5386
|
-
// Could be a JSON string like '{"_nf_id":"uuid"}' or a raw edge ID
|
|
5387
|
-
let edgeId = rel;
|
|
5388
|
-
try {
|
|
5389
|
-
const parsed = JSON.parse(rel);
|
|
5390
|
-
if (typeof parsed === "object" && parsed !== null && (parsed._nf_id || parsed.id)) {
|
|
5391
|
-
edgeId = (parsed._nf_id || parsed.id);
|
|
5392
|
-
}
|
|
5393
|
-
}
|
|
5394
|
-
catch {
|
|
5395
|
-
// Not JSON, use as-is
|
|
5396
|
-
}
|
|
5397
|
-
const edgeResult = this.db.execute("SELECT id, source_id, target_id FROM edges WHERE id = ?", [edgeId]);
|
|
5398
|
-
if (edgeResult.rows.length > 0) {
|
|
5399
|
-
const r = edgeResult.rows[0];
|
|
5400
|
-
edgeInfo = {
|
|
5401
|
-
id: r.id,
|
|
5402
|
-
source_id: r.source_id,
|
|
5403
|
-
target_id: r.target_id
|
|
5404
|
-
};
|
|
5405
|
-
}
|
|
5406
|
-
}
|
|
6033
|
+
// Extract edge ID and get info from batch result
|
|
6034
|
+
const edgeId = this.extractEdgeId(rel);
|
|
6035
|
+
const edgeInfo = edgeId ? edgeInfoMap.get(edgeId) : null;
|
|
5407
6036
|
if (!edgeInfo) {
|
|
5408
6037
|
valid = false;
|
|
5409
6038
|
break;
|
|
@@ -5454,12 +6083,8 @@ export class Executor {
|
|
|
5454
6083
|
}
|
|
5455
6084
|
const firstNode = firstNodeResult.rows[0];
|
|
5456
6085
|
const lastNode = lastNodeResult.rows[0];
|
|
5457
|
-
const firstProps =
|
|
5458
|
-
|
|
5459
|
-
: firstNode.properties;
|
|
5460
|
-
const lastProps = typeof lastNode.properties === "string"
|
|
5461
|
-
? JSON.parse(lastNode.properties)
|
|
5462
|
-
: lastNode.properties;
|
|
6086
|
+
const firstProps = this.getNodeProperties(firstNode.id, firstNode.properties);
|
|
6087
|
+
const lastProps = this.getNodeProperties(lastNode.id, lastNode.properties);
|
|
5463
6088
|
// Build result based on RETURN items
|
|
5464
6089
|
const resultRow = {};
|
|
5465
6090
|
for (const item of returnClause.items) {
|
|
@@ -5575,7 +6200,7 @@ export class Executor {
|
|
|
5575
6200
|
matchedNodes.set(nodePattern.variable, {
|
|
5576
6201
|
id: row.id,
|
|
5577
6202
|
label: row.label,
|
|
5578
|
-
properties: typeof row.
|
|
6203
|
+
properties: this.getNodeProperties(typeof row.id === "string" ? row.id : "", typeof row.properties === "string" || (typeof row.properties === "object" && row.properties !== null) ? row.properties : "{}"),
|
|
5579
6204
|
});
|
|
5580
6205
|
}
|
|
5581
6206
|
}
|
|
@@ -5690,6 +6315,8 @@ export class Executor {
|
|
|
5690
6315
|
continue;
|
|
5691
6316
|
const value = this.evaluateExpressionWithMatchedNodes(assignment.value, params, matchedNodes);
|
|
5692
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);
|
|
5693
6320
|
}
|
|
5694
6321
|
}
|
|
5695
6322
|
}
|
|
@@ -5701,7 +6328,7 @@ export class Executor {
|
|
|
5701
6328
|
matchedNodes.set(pattern.variable, {
|
|
5702
6329
|
id: row.id,
|
|
5703
6330
|
label: row.label,
|
|
5704
|
-
properties:
|
|
6331
|
+
properties: this.getNodeProperties(row.id, row.properties),
|
|
5705
6332
|
});
|
|
5706
6333
|
}
|
|
5707
6334
|
}
|
|
@@ -5766,7 +6393,7 @@ export class Executor {
|
|
|
5766
6393
|
type: row.type,
|
|
5767
6394
|
source_id: row.source_id,
|
|
5768
6395
|
target_id: row.target_id,
|
|
5769
|
-
properties:
|
|
6396
|
+
properties: this.getEdgeProperties(row.id, row.properties),
|
|
5770
6397
|
});
|
|
5771
6398
|
}
|
|
5772
6399
|
}
|
|
@@ -6039,7 +6666,7 @@ export class Executor {
|
|
|
6039
6666
|
newRow.set(nodePattern.variable, {
|
|
6040
6667
|
id: sqlRow.id,
|
|
6041
6668
|
label: labelValue,
|
|
6042
|
-
properties:
|
|
6669
|
+
properties: this.getNodeProperties(sqlRow.id, sqlRow.properties),
|
|
6043
6670
|
});
|
|
6044
6671
|
}
|
|
6045
6672
|
newMatchRows.push(newRow);
|
|
@@ -6188,6 +6815,8 @@ export class Executor {
|
|
|
6188
6815
|
continue;
|
|
6189
6816
|
const value = this.evaluateExpressionWithMatchedNodes(assignment.value, params, matchedNodes);
|
|
6190
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);
|
|
6191
6820
|
}
|
|
6192
6821
|
}
|
|
6193
6822
|
}
|
|
@@ -6199,7 +6828,7 @@ export class Executor {
|
|
|
6199
6828
|
matchedNodes.set(pattern.variable, {
|
|
6200
6829
|
id: row.id,
|
|
6201
6830
|
label: row.label,
|
|
6202
|
-
properties:
|
|
6831
|
+
properties: this.getNodeProperties(row.id, row.properties),
|
|
6203
6832
|
});
|
|
6204
6833
|
}
|
|
6205
6834
|
}
|
|
@@ -6322,7 +6951,7 @@ export class Executor {
|
|
|
6322
6951
|
type: row.type,
|
|
6323
6952
|
source_id: row.source_id,
|
|
6324
6953
|
target_id: row.target_id,
|
|
6325
|
-
properties:
|
|
6954
|
+
properties: this.getEdgeProperties(row.id, row.properties),
|
|
6326
6955
|
});
|
|
6327
6956
|
}
|
|
6328
6957
|
}
|
|
@@ -6526,6 +7155,8 @@ export class Executor {
|
|
|
6526
7155
|
? this.evaluateExpressionWithContext(assignment.value, params, resolvedIds)
|
|
6527
7156
|
: this.evaluateExpression(assignment.value, params);
|
|
6528
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);
|
|
6529
7160
|
}
|
|
6530
7161
|
}
|
|
6531
7162
|
}
|
|
@@ -6537,7 +7168,7 @@ export class Executor {
|
|
|
6537
7168
|
matchedNodes.set(pattern.variable, {
|
|
6538
7169
|
id: row.id,
|
|
6539
7170
|
label: row.label,
|
|
6540
|
-
properties:
|
|
7171
|
+
properties: this.getNodeProperties(row.id, row.properties),
|
|
6541
7172
|
});
|
|
6542
7173
|
}
|
|
6543
7174
|
}
|
|
@@ -6635,6 +7266,8 @@ export class Executor {
|
|
|
6635
7266
|
const value = this.evaluateExpression(assignment.value, params);
|
|
6636
7267
|
// Update target node with ON CREATE SET
|
|
6637
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);
|
|
6638
7271
|
}
|
|
6639
7272
|
}
|
|
6640
7273
|
this.db.execute("INSERT INTO edges (id, type, source_id, target_id, properties) VALUES (?, ?, ?, ?, ?)", [edgeId, edgeType, sourceNodeId, targetNodeId, JSON.stringify(finalEdgeProps)]);
|
|
@@ -6652,6 +7285,8 @@ export class Executor {
|
|
|
6652
7285
|
const value = this.evaluateExpression(assignment.value, params);
|
|
6653
7286
|
// Update target node with ON MATCH SET
|
|
6654
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);
|
|
6655
7290
|
}
|
|
6656
7291
|
}
|
|
6657
7292
|
}
|
|
@@ -6665,7 +7300,7 @@ export class Executor {
|
|
|
6665
7300
|
type: row.type,
|
|
6666
7301
|
source_id: row.source_id,
|
|
6667
7302
|
target_id: row.target_id,
|
|
6668
|
-
properties:
|
|
7303
|
+
properties: this.getEdgeProperties(row.id, row.properties),
|
|
6669
7304
|
});
|
|
6670
7305
|
}
|
|
6671
7306
|
}
|
|
@@ -6677,7 +7312,7 @@ export class Executor {
|
|
|
6677
7312
|
matchedNodes.set(targetVar, {
|
|
6678
7313
|
id: row.id,
|
|
6679
7314
|
label: row.label,
|
|
6680
|
-
properties:
|
|
7315
|
+
properties: this.getNodeProperties(row.id, row.properties),
|
|
6681
7316
|
});
|
|
6682
7317
|
}
|
|
6683
7318
|
}
|
|
@@ -6712,7 +7347,7 @@ export class Executor {
|
|
|
6712
7347
|
return {
|
|
6713
7348
|
id: row.id,
|
|
6714
7349
|
label: typeof row.label === "string" ? JSON.parse(row.label) : row.label,
|
|
6715
|
-
properties:
|
|
7350
|
+
properties: this.getNodeProperties(row.id, row.properties),
|
|
6716
7351
|
};
|
|
6717
7352
|
}
|
|
6718
7353
|
}
|
|
@@ -6971,7 +7606,7 @@ export class Executor {
|
|
|
6971
7606
|
case "variable":
|
|
6972
7607
|
return expr.variable;
|
|
6973
7608
|
case "property":
|
|
6974
|
-
return `${expr.variable}
|
|
7609
|
+
return `${expr.variable}.${expr.property}`;
|
|
6975
7610
|
case "function": {
|
|
6976
7611
|
// Build function expression like count(a) or count(*)
|
|
6977
7612
|
const funcName = expr.functionName.toLowerCase();
|
|
@@ -7010,6 +7645,236 @@ export class Executor {
|
|
|
7010
7645
|
return "expr";
|
|
7011
7646
|
}
|
|
7012
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
|
+
}
|
|
7013
7878
|
/**
|
|
7014
7879
|
* Detect and handle patterns that need multi-phase execution:
|
|
7015
7880
|
* - MATCH...CREATE that references matched variables
|
|
@@ -7770,10 +8635,10 @@ export class Executor {
|
|
|
7770
8635
|
for (const resolvedIds of allResolvedIds) {
|
|
7771
8636
|
const nodeId = resolvedIds[varName];
|
|
7772
8637
|
if (nodeId) {
|
|
7773
|
-
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]);
|
|
7774
8639
|
if (nodeResult.rows.length > 0) {
|
|
7775
8640
|
const row = nodeResult.rows[0];
|
|
7776
|
-
values.push(
|
|
8641
|
+
values.push(this.getNodeProperties(row.id, row.properties));
|
|
7777
8642
|
}
|
|
7778
8643
|
}
|
|
7779
8644
|
}
|
|
@@ -7879,9 +8744,7 @@ export class Executor {
|
|
|
7879
8744
|
if (nodeResult.rows.length > 0) {
|
|
7880
8745
|
const row = nodeResult.rows[0];
|
|
7881
8746
|
// Neo4j 3.5 format: return properties directly
|
|
7882
|
-
resultRow[alias] =
|
|
7883
|
-
? JSON.parse(row.properties)
|
|
7884
|
-
: row.properties;
|
|
8747
|
+
resultRow[alias] = this.getNodeProperties(row.id, row.properties);
|
|
7885
8748
|
}
|
|
7886
8749
|
else {
|
|
7887
8750
|
// Try edges
|
|
@@ -7889,9 +8752,7 @@ export class Executor {
|
|
|
7889
8752
|
if (edgeResult.rows.length > 0) {
|
|
7890
8753
|
const row = edgeResult.rows[0];
|
|
7891
8754
|
// Neo4j 3.5 format: return properties directly
|
|
7892
|
-
resultRow[alias] =
|
|
7893
|
-
? JSON.parse(row.properties)
|
|
7894
|
-
: row.properties;
|
|
8755
|
+
resultRow[alias] = this.getEdgeProperties(row.id, row.properties);
|
|
7895
8756
|
}
|
|
7896
8757
|
}
|
|
7897
8758
|
}
|
|
@@ -8104,6 +8965,8 @@ export class Executor {
|
|
|
8104
8965
|
if (nodeResult.changes === 0) {
|
|
8105
8966
|
this.db.execute(`UPDATE edges SET properties = ? WHERE id = ?`, [JSON.stringify(filteredProps), nodeId]);
|
|
8106
8967
|
}
|
|
8968
|
+
// Invalidate cache after UPDATE
|
|
8969
|
+
this.invalidatePropertyCache(nodeId);
|
|
8107
8970
|
continue;
|
|
8108
8971
|
}
|
|
8109
8972
|
// Handle SET n += {props} - merge properties
|
|
@@ -8137,6 +9000,8 @@ export class Executor {
|
|
|
8137
9000
|
this.db.execute(`UPDATE edges SET properties = json_patch(properties, ?) WHERE id = ?`, [JSON.stringify(nonNullProps), nodeId]);
|
|
8138
9001
|
}
|
|
8139
9002
|
}
|
|
9003
|
+
// Invalidate cache after UPDATE
|
|
9004
|
+
this.invalidatePropertyCache(nodeId);
|
|
8140
9005
|
continue;
|
|
8141
9006
|
}
|
|
8142
9007
|
// Handle property assignments
|
|
@@ -8144,7 +9009,7 @@ export class Executor {
|
|
|
8144
9009
|
throw new Error(`Invalid SET assignment for variable: ${assignment.variable}`);
|
|
8145
9010
|
}
|
|
8146
9011
|
// Use context-aware evaluation for expressions that may reference properties
|
|
8147
|
-
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"
|
|
8148
9013
|
? this.evaluateExpressionWithContext(assignment.value, params, resolvedIds)
|
|
8149
9014
|
: this.evaluateExpression(assignment.value, params);
|
|
8150
9015
|
// If value is null, remove the property instead of setting it to null
|
|
@@ -8164,6 +9029,8 @@ export class Executor {
|
|
|
8164
9029
|
this.db.execute(`UPDATE edges SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
|
|
8165
9030
|
}
|
|
8166
9031
|
}
|
|
9032
|
+
// Invalidate cache after UPDATE
|
|
9033
|
+
this.invalidatePropertyCache(nodeId);
|
|
8167
9034
|
}
|
|
8168
9035
|
}
|
|
8169
9036
|
/**
|
|
@@ -8219,6 +9086,28 @@ export class Executor {
|
|
|
8219
9086
|
}
|
|
8220
9087
|
return deletedVariables;
|
|
8221
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
|
+
}
|
|
8222
9111
|
/**
|
|
8223
9112
|
* Evaluate an expression to get its value
|
|
8224
9113
|
* Note: For property and binary expressions that reference nodes, use evaluateExpressionWithContext
|
|
@@ -8329,6 +9218,19 @@ export class Executor {
|
|
|
8329
9218
|
const args = expr.args || [];
|
|
8330
9219
|
return this.evaluateFunctionInProperty(funcName, args, params, {});
|
|
8331
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
|
+
}
|
|
8332
9234
|
default:
|
|
8333
9235
|
throw new Error(`Cannot evaluate expression of type ${expr.type}`);
|
|
8334
9236
|
}
|
|
@@ -8518,21 +9420,26 @@ export class Executor {
|
|
|
8518
9420
|
/**
|
|
8519
9421
|
* Generate SQL condition for label matching
|
|
8520
9422
|
* Supports both single and multiple labels
|
|
9423
|
+
* Uses primary label index with fallback for secondary labels
|
|
8521
9424
|
*/
|
|
8522
9425
|
generateLabelCondition(label) {
|
|
8523
9426
|
const labels = Array.isArray(label) ? label : [label];
|
|
8524
9427
|
if (labels.length === 1) {
|
|
8525
9428
|
return {
|
|
8526
|
-
sql: `EXISTS (SELECT 1 FROM json_each(label) WHERE value = ?)`,
|
|
8527
|
-
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]]
|
|
8528
9431
|
};
|
|
8529
9432
|
}
|
|
8530
9433
|
else {
|
|
8531
|
-
// Multiple labels: all must exist
|
|
8532
|
-
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
|
+
}
|
|
8533
9440
|
return {
|
|
8534
9441
|
sql: conditions.join(" AND "),
|
|
8535
|
-
params
|
|
9442
|
+
params
|
|
8536
9443
|
};
|
|
8537
9444
|
}
|
|
8538
9445
|
}
|