From 34645755cb39148bdeeeee1722459cbe45cfdc77 Mon Sep 17 00:00:00 2001 From: CNE Pierre FICHEPOIL Date: Wed, 15 Apr 2026 11:54:04 +0000 Subject: [PATCH 1/2] fix(opencypher): MATCH WHERE ID(n) = falls back to full scan when expr is dynamic --- .../executor/steps/MatchNodeStep.java | 77 ++++++++++++++++--- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MatchNodeStep.java b/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MatchNodeStep.java index e981301c03..527f96064a 100644 --- a/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MatchNodeStep.java +++ b/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MatchNodeStep.java @@ -28,6 +28,7 @@ import com.arcadedb.query.opencypher.ast.BooleanExpression; import com.arcadedb.query.opencypher.ast.ComparisonExpression; import com.arcadedb.query.opencypher.ast.Expression; +import com.arcadedb.query.opencypher.ast.FunctionCallExpression; import com.arcadedb.query.opencypher.ast.LogicalExpression; import com.arcadedb.query.opencypher.ast.NodePattern; import com.arcadedb.query.opencypher.ast.PropertyAccessExpression; @@ -65,11 +66,12 @@ * predicates during scanning rather than in a separate FilterPropertiesStep. */ public class MatchNodeStep extends AbstractExecutionStep { - private final String variable; - private final NodePattern pattern; - private final String idFilter; // Optional ID filter to apply (e.g., "#1:0") - private final BooleanExpression whereFilter; // Optional inline WHERE predicate (pushdown) - private String usedIndexName; // Track which index was used (if any) + private final String variable; + private final NodePattern pattern; + private final String idFilter; // Optional ID filter to apply (e.g., "#1:0") + private final BooleanExpression whereFilter; // Optional inline WHERE predicate (pushdown) + private final ExpressionEvaluator evaluator; // Shared evaluator for WHERE/ID expression resolution + private String usedIndexName; // Track which index was used (if any) /** * Creates a match node step. @@ -111,6 +113,7 @@ public MatchNodeStep(final String variable, final NodePattern pattern, final Com this.pattern = pattern; this.idFilter = idFilter; this.whereFilter = whereFilter; + this.evaluator = new ExpressionEvaluator(new CypherFunctionFactory(DefaultSQLFunctionFactory.getInstance())); } @Override @@ -291,13 +294,19 @@ private Iterator getVertexIterator() { } private Iterator getVertexIterator(final Result currentInputResult) { + // Check for dynamic ID filter from WHERE clause if static idFilter is not present + String effectiveIdFilter = this.idFilter; + if ((effectiveIdFilter == null || effectiveIdFilter.isEmpty()) && whereFilter != null) { + effectiveIdFilter = extractDynamicIdFilter(whereFilter, currentInputResult); + } + // OPTIMIZATION: If ID filter is present, look up the specific vertex by ID // This is critical for performance when matching by ID (e.g., WHERE ID(a) = "#1:0") // Without this optimization, MATCH (a),(b) WHERE ID(a) = x AND ID(b) = y // would create a Cartesian product of ALL vertices before filtering - if (idFilter != null && !idFilter.isEmpty()) { + if (effectiveIdFilter != null && !effectiveIdFilter.isEmpty()) { try { - final RID rid = new RID(context.getDatabase(), idFilter); + final RID rid = new RID(context.getDatabase(), effectiveIdFilter); final Identifiable vertex = context.getDatabase().lookupByRID(rid, true); // Return single-element iterator for the matched vertex return List.of(vertex).iterator(); @@ -570,6 +579,58 @@ private Iterator tryFindAndUseIndexFromWhere(final DocumentType ty return null; } + /** + * Extracts ID filter from a boolean expression, resolving dynamic values against the current input result. + * This handles UNWIND...MATCH...WHERE patterns where ID is filtered dynamically. + */ + private String extractDynamicIdFilter(final BooleanExpression expr, final Result currentInputResult) { + if (expr instanceof ComparisonExpression) { + final ComparisonExpression comp = (ComparisonExpression) expr; + if (comp.getOperator() != ComparisonExpression.Operator.EQUALS) + return null; + + // Check for pattern: ID(variable) = + if (comp.getLeft() instanceof FunctionCallExpression) { + final FunctionCallExpression func = (FunctionCallExpression) comp.getLeft(); + if ("id".equalsIgnoreCase(func.getFunctionName()) && func.getArguments().size() == 1) { + final Expression arg = func.getArguments().get(0); + if (arg instanceof VariableExpression && variable.equals(((VariableExpression) arg).getVariableName())) { + return evaluateToId(comp.getRight(), currentInputResult); + } + } + } + + // Check for reversed: = ID(variable) + if (comp.getRight() instanceof FunctionCallExpression) { + final FunctionCallExpression func = (FunctionCallExpression) comp.getRight(); + if ("id".equalsIgnoreCase(func.getFunctionName()) && func.getArguments().size() == 1) { + final Expression arg = func.getArguments().get(0); + if (arg instanceof VariableExpression && variable.equals(((VariableExpression) arg).getVariableName())) { + return evaluateToId(comp.getLeft(), currentInputResult); + } + } + } + } else if (expr instanceof LogicalExpression) { + final LogicalExpression logical = (LogicalExpression) expr; + if (logical.getOperator() == LogicalExpression.Operator.AND) { + final String leftId = extractDynamicIdFilter(logical.getLeft(), currentInputResult); + if (leftId != null) return leftId; + return extractDynamicIdFilter(logical.getRight(), currentInputResult); + } + } + return null; + } + + private String evaluateToId(final Expression expr, final Result currentInputResult) { + final Object resolvedValue = evaluator.evaluate(expr, currentInputResult, context); + if (resolvedValue != null) { + if (resolvedValue instanceof Identifiable) + return ((Identifiable) resolvedValue).getIdentity().toString(); + return resolvedValue.toString(); + } + return null; + } + /** * Extracts equality predicates of the form variable.property = value from a boolean expression. * Supports AND conjunctions. Resolves dynamic expressions against the current input result. @@ -603,8 +664,6 @@ private void extractEqualityPredicates(final BooleanExpression expr, if (propertyName != null && valueExpr != null) { // Resolve the value expression - final ExpressionEvaluator evaluator = new ExpressionEvaluator( - new CypherFunctionFactory(DefaultSQLFunctionFactory.getInstance())); final Object resolvedValue = evaluator.evaluate(valueExpr, currentInputResult, context); if (resolvedValue != null) predicates.put(propertyName, resolvedValue); From 801337dc5dd95a553c1352648cf25585268cdf97 Mon Sep 17 00:00:00 2001 From: CNE Pierre FICHEPOIL Date: Wed, 15 Apr 2026 13:39:49 +0000 Subject: [PATCH 2/2] more optimizations by claude --- .../opencypher/executor/steps/MergeStep.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MergeStep.java b/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MergeStep.java index 1eee13871a..dcdd8d01b0 100644 --- a/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MergeStep.java +++ b/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MergeStep.java @@ -42,6 +42,7 @@ import com.arcadedb.query.sql.executor.Result; import com.arcadedb.query.sql.executor.ResultInternal; import com.arcadedb.query.sql.executor.ResultSet; +import com.arcadedb.index.TypeIndex; import com.arcadedb.schema.DocumentType; import com.arcadedb.schema.VertexType; @@ -381,6 +382,51 @@ private ResultInternal copyResult(final Result source) { return copy; } + /** + * Tries to find and use an index for the evaluated property constraints. + * Returns an iterator of matching identifiables, or null if no suitable index found. + * Applies leftmost-prefix matching for composite indexes. + */ + private Iterator tryFindByIndex(final DocumentType type, final String label, + final Map evaluatedProperties) { + TypeIndex bestIndex = null; + int bestMatchCount = 0; + List bestMatchedProperties = null; + + for (final TypeIndex index : type.getAllIndexes(false)) { + final List indexProperties = index.getPropertyNames(); + int matchCount = 0; + final List matchedProperties = new ArrayList<>(); + + for (final String indexProp : indexProperties) { + if (evaluatedProperties.containsKey(indexProp)) { + matchCount++; + matchedProperties.add(indexProp); + } else + break; // Leftmost prefix only + } + + if (matchCount > 0 && matchCount > bestMatchCount) { + bestMatchCount = matchCount; + bestIndex = index; + bestMatchedProperties = matchedProperties; + } + } + + if (bestIndex == null || bestMatchedProperties == null || bestMatchedProperties.isEmpty()) + return null; + + final String[] propertyNames = bestMatchedProperties.toArray(new String[0]); + final Object[] propertyValues = new Object[propertyNames.length]; + for (int i = 0; i < propertyNames.length; i++) + propertyValues[i] = evaluatedProperties.get(propertyNames[i]); + + @SuppressWarnings("unchecked") + final Iterator iter = (Iterator) (Object) + context.getDatabase().lookupByKey(label, propertyNames, propertyValues); + return iter; + } + /** * Finds a node matching the pattern. * @@ -444,6 +490,22 @@ private Vertex findNode(final NodePattern nodePattern, final Result result) { if (!context.getDatabase().getSchema().existsType(label)) return null; + // OPTIMIZATION: try index before full scan + if (evaluatedProperties != null && !evaluatedProperties.isEmpty()) { + final DocumentType type = context.getDatabase().getSchema().getType(label); + if (type != null) { + final Iterator indexIter = tryFindByIndex(type, label, evaluatedProperties); + if (indexIter != null) { + while (indexIter.hasNext()) { + final Identifiable identifiable = indexIter.next(); + if (identifiable instanceof Vertex vertex && matchesProperties(vertex, evaluatedProperties)) + return vertex; + } + return null; + } + } + } + @SuppressWarnings("unchecked") final Iterator iterator = (Iterator) (Object) context.getDatabase().iterateType(label, true); @@ -516,6 +578,23 @@ private List findAllNodes(final NodePattern nodePattern, final Result re final String label = labels.get(0); if (!context.getDatabase().getSchema().existsType(label)) return matches; + + // OPTIMIZATION: try index before full scan + if (evaluatedProperties != null && !evaluatedProperties.isEmpty()) { + final DocumentType type = context.getDatabase().getSchema().getType(label); + if (type != null) { + final Iterator indexIter = tryFindByIndex(type, label, evaluatedProperties); + if (indexIter != null) { + while (indexIter.hasNext()) { + final Identifiable identifiable = indexIter.next(); + if (identifiable instanceof Vertex vertex && matchesProperties(vertex, evaluatedProperties)) + matches.add(vertex); + } + return matches; + } + } + } + @SuppressWarnings("unchecked") final Iterator iterator = (Iterator) (Object) context.getDatabase().iterateType(label, true); while (iterator.hasNext()) {