* Process a query using the legacy flat structure (backward compatibility).
(query: Query, steps: Step<any>[])
| 324 | * Process a query using the legacy flat structure (backward compatibility). |
| 325 | */ |
| 326 | function processLegacyQuery(query: Query, steps: Step<any>[]): void { |
| 327 | // 1. Convert MATCH clauses patterns to steps |
| 328 | // Process regular MATCH clauses first, then OPTIONAL MATCH clauses |
| 329 | // If the first MATCH is OPTIONAL, we need a StartStep to provide an initial path |
| 330 | // so that OptionalMatchStep has an input to extend with null bindings |
| 331 | const firstMatchIsOptional = query.matches.length > 0 && query.matches[0]!.optional; |
| 332 | if (firstMatchIsOptional) { |
| 333 | steps.push(new StartStep({})); |
| 334 | } |
| 335 | |
| 336 | let hasSeenPreviousMatch = false; |
| 337 | for (const matchClause of query.matches) { |
| 338 | // Convert the pattern to steps |
| 339 | let patternSteps: Step<any>[]; |
| 340 | |
| 341 | if (matchClause.pattern.type === "ShortestPathPattern") { |
| 342 | patternSteps = convertShortestPathPattern( |
| 343 | matchClause.pattern as ShortestPathPattern, |
| 344 | matchClause.where, |
| 345 | ); |
| 346 | } else if (matchClause.pattern.type === "MultiPattern") { |
| 347 | // Handle comma-separated patterns like MATCH (a), (b) |
| 348 | patternSteps = convertMultiPattern(matchClause.pattern as MultiPattern, matchClause.where); |
| 349 | } else { |
| 350 | // For OPTIONAL MATCH after a previous MATCH, the first node may reference |
| 351 | // a previously bound variable. We enable anchor detection (anchoredToInput: true) |
| 352 | // when there are previous steps in the query that may have bound variables. |
| 353 | // The anchor heuristic will only trigger if the first node has no labels. |
| 354 | const isAnchoredContext = matchClause.optional && hasSeenPreviousMatch; |
| 355 | patternSteps = convertPattern( |
| 356 | matchClause.pattern as Pattern, |
| 357 | matchClause.where, |
| 358 | isAnchoredContext, |
| 359 | ); |
| 360 | } |
| 361 | |
| 362 | if (matchClause.optional) { |
| 363 | // Wrap in OptionalMatchStep for OPTIONAL MATCH |
| 364 | // skipAnchor: Only skip anchor if there was a previous MATCH that bound variables |
| 365 | const skipAnchor = hasSeenPreviousMatch; |
| 366 | const variables = extractPatternVariables(matchClause.pattern, skipAnchor); |
| 367 | steps.push(new OptionalMatchStep({ variables }, patternSteps as Step<any>[])); |
| 368 | } else { |
| 369 | steps.push(...patternSteps); |
| 370 | } |
| 371 | hasSeenPreviousMatch = true; |
| 372 | } |
| 373 | |
| 374 | // 1b. If no MATCH clauses but there are mutations or list operations, add a StartStep |
| 375 | // to provide an initial empty path for CREATE/MERGE/UNWIND to work with |
| 376 | const hasMutations = |
| 377 | (query.merge && query.merge.length > 0) || |
| 378 | query.create || |
| 379 | query.set || |
| 380 | query.remove || |
| 381 | query.delete || |
| 382 | (query.foreach && query.foreach.length > 0); |
| 383 |
no test coverage detected