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();
