|
28 | 28 |
|
29 | 29 | import java.util.ArrayList; |
30 | 30 | import java.util.Collections; |
| 31 | +import java.util.EnumSet; |
31 | 32 | import java.util.HashSet; |
32 | 33 | import java.util.List; |
33 | 34 | import java.util.Map; |
|
40 | 41 | * @author Luca Garulli (l.garulli--(at)--arcadedata.com) |
41 | 42 | */ |
42 | 43 | public class CypherExecutionPlanner { |
| 44 | + private static final Set<ClauseEntry.ClauseType> MUTATING_CLAUSES = EnumSet.of( |
| 45 | + ClauseEntry.ClauseType.DELETE, |
| 46 | + ClauseEntry.ClauseType.SET, |
| 47 | + ClauseEntry.ClauseType.REMOVE, |
| 48 | + ClauseEntry.ClauseType.CREATE, |
| 49 | + ClauseEntry.ClauseType.MERGE |
| 50 | + ); |
| 51 | + |
43 | 52 | private final DatabaseInternal database; |
44 | 53 | private final CypherStatement statement; |
45 | 54 | private final Map<String, Object> parameters; |
@@ -213,23 +222,48 @@ private boolean shouldUseOptimizer() { |
213 | 222 | } |
214 | 223 | } |
215 | 224 |
|
216 | | - // The optimizer path (buildExecutionStepsWithOptimizer) only supports a fixed clause |
217 | | - // ordering: MATCH(es) → WITH → RETURN. Write clauses (CREATE/SET/DELETE/MERGE) are also handled. |
218 | | - // Disable for queries with clauses that break this assumption. |
| 225 | + // The optimizer path (buildExecutionStepsWithOptimizer) applies clauses in a fixed order |
| 226 | + // (MATCH → CREATE → SET → DELETE → REMOVE → MERGE → UNWIND → WITH → RETURN). |
| 227 | + // This breaks queries where clause ordering matters, e.g. WITH before DELETE or UNWIND before SET. |
| 228 | + // Fall back to ordered execution when write/mutating clauses are interleaved with WITH/UNWIND, |
| 229 | + // or when MATCH appears after WITH (MATCH-WITH-MATCH pattern). |
219 | 230 | if (statement.getClausesInOrder() != null) { |
220 | 231 | int createCount = 0; |
221 | 232 | int mergeCount = 0; |
222 | 233 | int deleteCount = 0; |
| 234 | + boolean seenWith = false; |
| 235 | + boolean seenUnwind = false; |
223 | 236 | for (final ClauseEntry clause : statement.getClausesInOrder()) { |
224 | 237 | final ClauseEntry.ClauseType type = clause.getType(); |
225 | | - if (type == ClauseEntry.ClauseType.FOREACH || type == ClauseEntry.ClauseType.CALL) |
| 238 | + |
| 239 | + // Validate ordering before updating state |
| 240 | + if ((seenWith || seenUnwind) && MUTATING_CLAUSES.contains(type)) |
| 241 | + return false; |
| 242 | + if (seenWith && type == ClauseEntry.ClauseType.MATCH) |
226 | 243 | return false; |
227 | | - if (type == ClauseEntry.ClauseType.CREATE) |
| 244 | + |
| 245 | + switch (type) { |
| 246 | + case FOREACH: |
| 247 | + case CALL: |
| 248 | + return false; |
| 249 | + case WITH: |
| 250 | + seenWith = true; |
| 251 | + break; |
| 252 | + case UNWIND: |
| 253 | + seenUnwind = true; |
| 254 | + break; |
| 255 | + case CREATE: |
228 | 256 | createCount++; |
229 | | - else if (type == ClauseEntry.ClauseType.MERGE) |
| 257 | + break; |
| 258 | + case MERGE: |
230 | 259 | mergeCount++; |
231 | | - else if (type == ClauseEntry.ClauseType.DELETE) |
| 260 | + break; |
| 261 | + case DELETE: |
232 | 262 | deleteCount++; |
| 263 | + break; |
| 264 | + default: |
| 265 | + break; |
| 266 | + } |
233 | 267 | } |
234 | 268 | // Multiple CREATE/MERGE/DELETE clauses not handled by optimizer path |
235 | 269 | if (createCount > 1 || mergeCount > 1 || (deleteCount > 0 && mergeCount > 0)) |
|
0 commit comments