This is an automated email from the ASF dual-hosted git repository.

mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git

commit e297ad4fc72876526fc7ba720ed99201d613498b
Author: Sean Broeder <[email protected]>
AuthorDate: Mon Aug 17 16:48:15 2026 -0700

    [CALCITE-7724] SqlUtil#lookupSubjectRoutines rejects a valid operator when 
getFunctionKind() remaps its kind and 2+ candidates share a name
    
    filterOperatorRoutinesByKind's "fourth pass" only runs once at least
    two candidates survive the earlier passes (a single surviving
    candidate short-circuits before this pass). It compares
    candidate.getKind().getFunctionKind() against the call's own,
    already-bound SqlKind - but that comparison applies getFunctionKind()'s
    remapping (introduced for this method) to the candidate side only, not
    to the requested side.
    
    For any SqlKind that getFunctionKind() maps to something else -
    POSITION and the newly-dedicated CHAR_LENGTH both map to
    OTHER_FUNCTION, along with ~90 others in that switch - this asymmetry
    means an operator can fail to match even itself, once a second
    candidate for the same name is present (e.g. because two chained
    operator tables both contribute an entry for it). The call is rejected
    outright with "No match found for function signature ...", even though
    exactly the intended operator was available.
    
    Fix: map both sides of the comparison through getFunctionKind() before
    comparing, matching normal-case comparisons for kinds it doesn't remap.
    
    Added a regression test (chaining the standard operator table with
    itself to force the two-candidate precondition, the minimal way to
    reach the buggy pass) and verified the entire SqlValidatorTest suite
    (589 tests) still passes with the fix.
---
 .../main/java/org/apache/calcite/sql/SqlUtil.java  |  9 +++++-
 .../org/apache/calcite/test/SqlValidatorTest.java  | 37 ++++++++++++++++++++++
 2 files changed, 45 insertions(+), 1 deletion(-)

diff --git a/core/src/main/java/org/apache/calcite/sql/SqlUtil.java 
b/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
index b5c91a1c7a..7bc4c2a55d 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
@@ -558,9 +558,16 @@ public static SqlLiteral 
concatenateLiterals(List<SqlLiteral> lits) {
 
   private static Iterator<SqlOperator> filterOperatorRoutinesByKind(
       Iterator<SqlOperator> routines, final SqlKind sqlKind) {
+    // Map both sides through getFunctionKind() so a candidate can still match 
a call
+    // that is already bound to that very candidate (or an operator with the 
same
+    // requested kind) even when getFunctionKind() maps that kind to something 
else,
+    // e.g. SqlKind.POSITION/CHAR_LENGTH both map to OTHER_FUNCTION. Comparing 
the
+    // candidate's mapped kind against the call's raw, unmapped kind is 
asymmetric and
+    // can spuriously reject every candidate, including the operator being 
called.
+    final SqlKind sqlFunctionKind = sqlKind.getFunctionKind();
     return Iterators.filter(routines,
         operator -> requireNonNull(operator, "operator")
-            .getKind().getFunctionKind() == sqlKind);
+            .getKind().getFunctionKind() == sqlFunctionKind);
   }
 
   /**
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 38f883743e..37c595c732 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -1043,6 +1043,43 @@ void testDyadicCollateOperator() {
         .fails("Parameters must be of the same type");
   }
 
+  /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7724";>
+   * [CALCITE-7724] SqlUtil#lookupSubjectRoutines rejects a valid operator 
when two
+   * operator-table entries resolve to the same operator and that operator's
+   * {@link SqlKind} is remapped by {@link SqlKind#getFunctionKind()}</a>.
+   *
+   * <p>{@link SqlUtil#lookupSubjectRoutines} only reaches its "fourth pass"
+   * ({@code filterOperatorRoutinesByKind}) once at least two candidate 
operators survive
+   * the earlier passes - which happens whenever an operator table (or a chain 
of them)
+   * contains more than one entry for the same operator name, arity and 
category, e.g.
+   * because it is registered in two different operator tables that get 
chained together.
+   * That pass compares {@code candidate.getKind().getFunctionKind()} (mapped) 
against the
+   * call's already-bound, unmapped {@code SqlKind} - for any {@link SqlKind} 
that
+   * {@code getFunctionKind()} maps to something else (such as {@link 
SqlKind#POSITION} or
+   * the now-dedicated {@link SqlKind#CHAR_LENGTH}, both mapped to
+   * {@link SqlKind#OTHER_FUNCTION}), this comparison fails even when the 
candidate is the
+   * operator the call is already bound to - eliminating every candidate and 
causing a
+   * spurious "No match found for function signature" validation error for an 
otherwise
+   * perfectly valid call. */
+  @Test void testFunctionKindMismatchWithDuplicateOperatorTableEntry() {
+    // Chaining the standard operator table with itself is a minimal way to 
force two
+    // candidates for the same operator to reach the fourth pass; in practice 
this also
+    // happens with any two chained operator tables that both contribute an 
entry for the
+    // same builtin operator (which is how this was found - via a composite 
operator table
+    // with more than one contributor).
+    final SqlOperatorTable duplicated =
+        SqlOperatorTables.chain(SqlStdOperatorTable.instance(), 
SqlStdOperatorTable.instance());
+    expr("position('mouse' in 'house')")
+        .withOperatorTable(duplicated)
+        .ok();
+    expr("char_length('string')")
+        .withOperatorTable(duplicated)
+        .ok();
+    expr("character_length('string')")
+        .withOperatorTable(duplicated)
+        .ok();
+  }
+
   @Test void testTrim() {
     expr("trim('mustache' FROM 'beard')").ok();
     expr("trim(both 'mustache' FROM 'beard')").ok();

Reply via email to