From 7a553f387eaa92e6b3fe467345f3124445a0cd0f Mon Sep 17 00:00:00 2001 From: Sergey Nuyanzin Date: Wed, 15 Apr 2026 21:55:35 +0200 Subject: [PATCH 1/3] [CALCITE-7474] `LAST` in `MATCH_RECOGNIZE` might return wrong result --- .../adapter/enumerable/MatchUtils.java | 11 ++--- .../adapter/enumerable/RexImpTable.java | 45 ++++++------------- .../apache/calcite/util/BuiltInMethod.java | 4 +- core/src/test/resources/sql/match.iq | 15 +++++++ 4 files changed, 37 insertions(+), 38 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/MatchUtils.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/MatchUtils.java index 254ffc289aa6..6306ee064872 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/MatchUtils.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/MatchUtils.java @@ -27,21 +27,22 @@ private MatchUtils() { } /** - * Returns the row with the highest index whose corresponding symbol matches, null otherwise. + * Returns the row with the highest index whose corresponding symbol matches, + * row with defaultIndex otherwise. * * @param symbol Target Symbol * @param rows List of passed rows * @param symbols Corresponding symbols to rows - * @return index or -1 + * @return index or defaultIndex */ - public static int lastWithSymbol(String symbol, List rows, List symbols, - int startIndex) { + public static int lastWithSymbolOrDefault(String symbol, List rows, List symbols, + int startIndex, int defaultIndex) { for (int i = startIndex; i >= 0; i--) { if (symbol.equals(symbols.get(i))) { return i; } } - return -1; + return defaultIndex; } public static void print(int s) { diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java index 42afb720fce6..eec14352db90 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java @@ -22,7 +22,6 @@ import org.apache.calcite.avatica.util.DateTimeUtils; import org.apache.calcite.avatica.util.TimeUnit; import org.apache.calcite.avatica.util.TimeUnitRange; -import org.apache.calcite.linq4j.tree.BinaryExpression; import org.apache.calcite.linq4j.tree.BlockBuilder; import org.apache.calcite.linq4j.tree.BlockStatement; import org.apache.calcite.linq4j.tree.ConstantExpression; @@ -4173,43 +4172,27 @@ private static class LastImplementor implements MatchImplementor { final String alpha = ((RexPatternFieldRef) call.getOperands().get(0)).getAlpha(); - // TODO: verify if the variable is needed - @SuppressWarnings("unused") - final BinaryExpression lastIndex = - Expressions.subtract( - Expressions.call(rows, BuiltInMethod.COLLECTION_SIZE.method), - Expressions.constant(1)); - // Just take the last one, if exists if ("*".equals(alpha)) { setInputGetterIndex(translator, i); - // Important, unbox the node / expression to avoid NullAs.NOT_POSSIBLE - final RexPatternFieldRef ref = (RexPatternFieldRef) node; - final RexPatternFieldRef newRef = - new RexPatternFieldRef(ref.getAlpha(), - ref.getIndex(), - translator.typeFactory.createTypeWithNullability(ref.getType(), - true)); - final Expression expression = translator.translate(newRef, NullAs.NULL); - setInputGetterIndex(translator, null); - return expression; } else { // Alpha != "*" so we have to search for a specific one to find and use that, if found + // otherwise pick the last one setInputGetterIndex(translator, - Expressions.call(BuiltInMethod.MATCH_UTILS_LAST_WITH_SYMBOL.method, - Expressions.constant(alpha), rows, symbols, i)); - - // Important, unbox the node / expression to avoid NullAs.NOT_POSSIBLE - final RexPatternFieldRef ref = (RexPatternFieldRef) node; - final RexPatternFieldRef newRef = - new RexPatternFieldRef(ref.getAlpha(), - ref.getIndex(), - translator.typeFactory.createTypeWithNullability(ref.getType(), - true)); - final Expression expression = translator.translate(newRef, NullAs.NULL); - setInputGetterIndex(translator, null); - return expression; + Expressions.call(BuiltInMethod.MATCH_UTILS_LAST_WITH_SYMBOL_OR_DEFAULT.method, + Expressions.constant(alpha), rows, symbols, i, i)); } + + // Important, unbox the node / expression to avoid NullAs.NOT_POSSIBLE + final RexPatternFieldRef ref = (RexPatternFieldRef) node; + final RexPatternFieldRef newRef = + new RexPatternFieldRef(ref.getAlpha(), + ref.getIndex(), + translator.typeFactory.createTypeWithNullability(ref.getType(), + true)); + final Expression expression = translator.translate(newRef, NullAs.NULL); + setInputGetterIndex(translator, null); + return expression; } private static void setInputGetterIndex(RexToLixTranslator translator, @Nullable Expression o) { diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java index 61aebe15f51e..bd270c4494b9 100644 --- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java +++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java @@ -243,8 +243,8 @@ public enum BuiltInMethod { MATCHER_BUILDER_ADD(Matcher.Builder.class, "add", String.class, Predicate.class), MATCHER_BUILDER_BUILD(Matcher.Builder.class, "build"), - MATCH_UTILS_LAST_WITH_SYMBOL(MatchUtils.class, "lastWithSymbol", String.class, - List.class, List.class, int.class), + MATCH_UTILS_LAST_WITH_SYMBOL_OR_DEFAULT(MatchUtils.class, "lastWithSymbolOrDefault", String.class, + List.class, List.class, int.class, int.class), EMITTER_EMIT(Enumerables.Emitter.class, "emit", List.class, List.class, List.class, int.class, Consumer.class), MERGE_JOIN(EnumerableDefaults.class, "mergeJoin", Enumerable.class, diff --git a/core/src/test/resources/sql/match.iq b/core/src/test/resources/sql/match.iq index 887c781c15ec..684195f90fe0 100644 --- a/core/src/test/resources/sql/match.iq +++ b/core/src/test/resources/sql/match.iq @@ -140,6 +140,21 @@ C EMPID !ok +# Test Simple LAST with expanded column name +select * +from "hr"."emps" match_recognize ( + order by "empid" desc + measures "commission" as c, + LAST("hr"."emps"."empid") as empid + pattern (s up) + define up as up."commission" < prev(up."commission")); +C EMPID +---- ----- +1000 100 + 500 200 + +!ok + # Test LAST with Classifier select * from "hr"."emps" match_recognize ( From bb70c60e52f0c74a5948f47aa5ee07096f83d02e Mon Sep 17 00:00:00 2001 From: Sergey Nuyanzin Date: Wed, 15 Apr 2026 23:47:37 +0200 Subject: [PATCH 2/3] [CALCITE-7474] Address feedback --- .../adapter/enumerable/MatchUtils.java | 29 +++++++++++++++---- .../adapter/enumerable/RexImpTable.java | 4 +-- .../apache/calcite/util/BuiltInMethod.java | 6 ++-- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/MatchUtils.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/MatchUtils.java index 6306ee064872..b082c6624865 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/MatchUtils.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/MatchUtils.java @@ -27,22 +27,39 @@ private MatchUtils() { } /** - * Returns the row with the highest index whose corresponding symbol matches, - * row with defaultIndex otherwise. + * Returns the row with the highest index whose corresponding symbol matches, null otherwise. * * @param symbol Target Symbol * @param rows List of passed rows * @param symbols Corresponding symbols to rows - * @return index or defaultIndex + * @return index or -1 + */ + public static int lastWithSymbol(String symbol, List rows, List symbols, + int startIndex) { + for (int i = startIndex; i >= 0; i--) { + if (symbol.equals(symbols.get(i))) { + return i; + } + } + return -1; + } + + /** + * Returns the row with the highest index whose corresponding symbol matches, + * row with startIndex otherwise. + * + * @param symbol Target Symbol + * @param symbols Corresponding symbols to rows + * @return index or startIndex */ - public static int lastWithSymbolOrDefault(String symbol, List rows, List symbols, - int startIndex, int defaultIndex) { + public static int lastWithSymbolOrLast(String symbol, List symbols, + int startIndex) { for (int i = startIndex; i >= 0; i--) { if (symbol.equals(symbols.get(i))) { return i; } } - return defaultIndex; + return startIndex; } public static void print(int s) { diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java index eec14352db90..549bddbf724d 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java @@ -4179,8 +4179,8 @@ private static class LastImplementor implements MatchImplementor { // Alpha != "*" so we have to search for a specific one to find and use that, if found // otherwise pick the last one setInputGetterIndex(translator, - Expressions.call(BuiltInMethod.MATCH_UTILS_LAST_WITH_SYMBOL_OR_DEFAULT.method, - Expressions.constant(alpha), rows, symbols, i, i)); + Expressions.call(BuiltInMethod.MATCH_UTILS_LAST_WITH_SYMBOL_OR_LAST.method, + Expressions.constant(alpha), symbols, i)); } // Important, unbox the node / expression to avoid NullAs.NOT_POSSIBLE diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java index bd270c4494b9..98b67a02bbf3 100644 --- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java +++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java @@ -243,8 +243,10 @@ public enum BuiltInMethod { MATCHER_BUILDER_ADD(Matcher.Builder.class, "add", String.class, Predicate.class), MATCHER_BUILDER_BUILD(Matcher.Builder.class, "build"), - MATCH_UTILS_LAST_WITH_SYMBOL_OR_DEFAULT(MatchUtils.class, "lastWithSymbolOrDefault", String.class, - List.class, List.class, int.class, int.class), + MATCH_UTILS_LAST_WITH_SYMBOL(MatchUtils.class, "lastWithSymbol", String.class, + List.class, List.class, int.class), + MATCH_UTILS_LAST_WITH_SYMBOL_OR_LAST(MatchUtils.class, "lastWithSymbolOrLast", String.class, + List.class, int.class), EMITTER_EMIT(Enumerables.Emitter.class, "emit", List.class, List.class, List.class, int.class, Consumer.class), MERGE_JOIN(EnumerableDefaults.class, "mergeJoin", Enumerable.class, From 1c0db251c909e8863a96f786ce0d082034a0b13a Mon Sep 17 00:00:00 2001 From: Sergey Nuyanzin Date: Thu, 16 Apr 2026 00:19:22 +0200 Subject: [PATCH 3/3] [CALCITE-7474] Address feedback --- .../org/apache/calcite/adapter/enumerable/MatchUtils.java | 7 +++---- core/src/test/resources/sql/match.iq | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/MatchUtils.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/MatchUtils.java index b082c6624865..d7096f0fb1c9 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/MatchUtils.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/MatchUtils.java @@ -27,7 +27,7 @@ private MatchUtils() { } /** - * Returns the row with the highest index whose corresponding symbol matches, null otherwise. + * Returns the highest index whose corresponding symbol matches, -1 otherwise. * * @param symbol Target Symbol * @param rows List of passed rows @@ -45,14 +45,13 @@ public static int lastWithSymbol(String symbol, List rows, List s } /** - * Returns the row with the highest index whose corresponding symbol matches, - * row with startIndex otherwise. + * Returns the highest index whose corresponding symbol matches, startIndex otherwise. * * @param symbol Target Symbol * @param symbols Corresponding symbols to rows * @return index or startIndex */ - public static int lastWithSymbolOrLast(String symbol, List symbols, + public static int lastWithSymbolOrLast(String symbol, List symbols, int startIndex) { for (int i = startIndex; i >= 0; i--) { if (symbol.equals(symbols.get(i))) { diff --git a/core/src/test/resources/sql/match.iq b/core/src/test/resources/sql/match.iq index 684195f90fe0..8c37204d90bc 100644 --- a/core/src/test/resources/sql/match.iq +++ b/core/src/test/resources/sql/match.iq @@ -141,6 +141,7 @@ C EMPID !ok # Test Simple LAST with expanded column name +# Test case for CALCITE-7474 select * from "hr"."emps" match_recognize ( order by "empid" desc