@@ -576,6 +576,10 @@ private AbstractExecutionStep buildExecutionStepsWithOrder(final CommandContext
576576 // Get function factory from evaluator for steps that need it
577577 final CypherFunctionFactory functionFactory = expressionEvaluator != null ? expressionEvaluator .getFunctionFactory () : null ;
578578
579+ // Track variables bound across MATCH clauses so subsequent MATCHes
580+ // can detect already-bound variables and avoid Cartesian products
581+ final Set <String > boundVariables = new HashSet <>();
582+
579583 // OPTIMIZATION: Check for simple COUNT(*) pattern that can use Type.count() O(1) operation
580584 // Pattern: MATCH (a:TypeName) RETURN COUNT(a) as alias
581585 final AbstractExecutionStep typeCountStep = tryCreateTypeCountOptimization (context );
@@ -625,7 +629,7 @@ public String prettyPrint(final int depth, final int indent) {
625629
626630 case MATCH :
627631 final MatchClause matchClause = entry .getTypedClause ();
628- currentStep = buildMatchStep (matchClause , currentStep , context );
632+ currentStep = buildMatchStep (matchClause , currentStep , context , boundVariables );
629633 break ;
630634
631635 case WITH :
@@ -801,9 +805,23 @@ private AbstractExecutionStep buildWithStep(final WithClause withClause,
801805
802806 /**
803807 * Builds execution step for a MATCH clause.
808+ * Backward-compatible overload without bound variable tracking.
804809 */
805810 private AbstractExecutionStep buildMatchStep (final MatchClause matchClause , AbstractExecutionStep currentStep ,
806811 final CommandContext context ) {
812+ return buildMatchStep (matchClause , currentStep , context , new HashSet <>());
813+ }
814+
815+ /**
816+ * Builds execution step for a MATCH clause with bound variable tracking.
817+ *
818+ * @param matchClause the MATCH clause to build
819+ * @param currentStep current step in the execution chain
820+ * @param context command context
821+ * @param boundVariables set of variable names already bound in previous steps (updated in-place)
822+ */
823+ private AbstractExecutionStep buildMatchStep (final MatchClause matchClause , AbstractExecutionStep currentStep ,
824+ final CommandContext context , final Set <String > boundVariables ) {
807825 if (!matchClause .hasPathPatterns ()) {
808826 return currentStep ;
809827 }
@@ -826,6 +844,13 @@ private AbstractExecutionStep buildMatchStep(final MatchClause matchClause, Abst
826844 final String variable = nodePattern .getVariable () != null ? nodePattern .getVariable () : ("n" + patternIndex );
827845 matchVariables .add (variable );
828846
847+ // Check if this variable was already bound in a previous MATCH clause
848+ if (boundVariables .contains (variable )) {
849+ // Variable already bound - skip creating a new MatchNodeStep
850+ // The bound value will be used from the input result
851+ continue ;
852+ }
853+
829854 // OPTIMIZATION: Extract ID filter for this variable to avoid Cartesian product
830855 final String idFilter = extractIdFilter (whereClause , variable );
831856 final MatchNodeStep matchStep = new MatchNodeStep (variable , nodePattern , context , idFilter );
@@ -848,8 +873,11 @@ private AbstractExecutionStep buildMatchStep(final MatchClause matchClause, Abst
848873 final NodePattern sourceNode = pathPattern .getFirstNode ();
849874 final String sourceVar = sourceNode .getVariable () != null ? sourceNode .getVariable () : "a" ;
850875
876+ // Check if source node variable is already bound (either from previous MATCH or
877+ // from being in the boundVariables set). Previously this only checked for
878+ // unlabeled/unpropertied nodes, which broke when labels were repeated.
851879 final boolean sourceAlreadyBound = stepBeforeMatch != null &&
852- !sourceNode .hasLabels () && !sourceNode .hasProperties ();
880+ ( boundVariables . contains ( sourceVar ) || ( !sourceNode .hasLabels () && !sourceNode .hasProperties ()) );
853881
854882 if (!sourceAlreadyBound ) {
855883 matchVariables .add (sourceVar );
@@ -898,7 +926,10 @@ private AbstractExecutionStep buildMatchStep(final MatchClause matchClause, Abst
898926 if (relPattern .isVariableLength ()) {
899927 nextStep = new ExpandPathStep (sourceVar , pathVariable , targetVar , relPattern , context );
900928 } else {
901- nextStep = new MatchRelationshipStep (sourceVar , relVar , targetVar , relPattern , pathVariable , context );
929+ // Pass target node pattern for label filtering and bound variables
930+ // for identity checking on already-bound target variables
931+ nextStep = new MatchRelationshipStep (sourceVar , relVar , targetVar , relPattern , pathVariable ,
932+ targetNode , boundVariables , context );
902933 }
903934
904935 if (isOptional && matchChainStart == null ) {
@@ -942,6 +973,9 @@ private AbstractExecutionStep buildMatchStep(final MatchClause matchClause, Abst
942973 currentStep = optionalStep ;
943974 }
944975
976+ // Update bound variables with newly bound variables from this MATCH
977+ boundVariables .addAll (matchVariables );
978+
945979 return currentStep ;
946980 }
947981
@@ -988,6 +1022,10 @@ public String prettyPrint(final int depth, final int indent) {
9881022 };
9891023 }
9901024
1025+ // Track variables bound across MATCH clauses so subsequent MATCHes
1026+ // can detect already-bound variables and avoid Cartesian products
1027+ final Set <String > legacyBoundVariables = new HashSet <>();
1028+
9911029 // Step 1: MATCH clauses - fetch nodes
9921030 // Process ALL MATCH clauses (not just the first)
9931031 if (!statement .getMatchClauses ().isEmpty ()) {
@@ -1015,6 +1053,12 @@ public String prettyPrint(final int depth, final int indent) {
10151053 final String variable = nodePattern .getVariable () != null ? nodePattern .getVariable () : ("n" + patternIndex );
10161054 matchVariables .add (variable ); // Track variable for OPTIONAL MATCH
10171055
1056+ // Check if this variable was already bound in a previous MATCH clause
1057+ if (legacyBoundVariables .contains (variable )) {
1058+ // Variable already bound - skip creating a new MatchNodeStep
1059+ continue ;
1060+ }
1061+
10181062 // OPTIMIZATION: Extract ID filter from WHERE clause (if present) for pushdown
10191063 final WhereClause matchWhere = matchClause .hasWhereClause () ? matchClause .getWhereClause () : statement .getWhereClause ();
10201064 final String idFilter = extractIdFilter (matchWhere , variable );
@@ -1042,10 +1086,9 @@ public String prettyPrint(final int depth, final int indent) {
10421086 final String sourceVar = sourceNode .getVariable () != null ? sourceNode .getVariable () : "a" ;
10431087
10441088 // Check if source node is already bound (for multiple MATCH clauses or OPTIONAL MATCH)
1045- // If the source node has no labels/properties and there's a previous step,
1046- // it's likely referring to an already-bound variable - skip creating MatchNodeStep
1089+ // Check both legacy bound variables AND the old heuristic (no labels/properties)
10471090 final boolean sourceAlreadyBound = stepBeforeMatch != null &&
1048- !sourceNode .hasLabels () && !sourceNode .hasProperties ();
1091+ ( legacyBoundVariables . contains ( sourceVar ) || ( !sourceNode .hasLabels () && !sourceNode .hasProperties ()) );
10491092
10501093 if (!sourceAlreadyBound ) {
10511094 // Only track the source variable if we're creating a new binding for it
@@ -1109,8 +1152,9 @@ public String prettyPrint(final int depth, final int indent) {
11091152 // Variable-length path - pass path variable for named path support
11101153 nextStep = new ExpandPathStep (sourceVar , pathVariable , targetVar , relPattern , context );
11111154 } else {
1112- // Fixed-length relationship - pass path variable
1113- nextStep = new MatchRelationshipStep (sourceVar , relVar , targetVar , relPattern , pathVariable , context );
1155+ // Fixed-length relationship - pass path variable, target node pattern, and bound variables
1156+ nextStep = new MatchRelationshipStep (sourceVar , relVar , targetVar , relPattern , pathVariable ,
1157+ targetNode , legacyBoundVariables , context );
11141158 }
11151159
11161160 // Chain the relationship step
@@ -1168,6 +1212,9 @@ public String prettyPrint(final int depth, final int indent) {
11681212 // The output of OptionalMatchStep becomes currentStep
11691213 currentStep = optionalStep ;
11701214 }
1215+
1216+ // Update bound variables with newly bound variables from this MATCH
1217+ legacyBoundVariables .addAll (matchVariables );
11711218 } else {
11721219 // Phase 1: Use raw pattern string - create a simple stub
11731220 final ResultInternal stubResult = new ResultInternal ();
0 commit comments