Sean Broeder created CALCITE-7724:
-------------------------------------
Summary: SqlUtil#lookupSubjectRoutines rejects a valid operator
when getFunctionKind() remaps its kind and two candidates share a name
Key: CALCITE-7724
URL: https://issues.apache.org/jira/browse/CALCITE-7724
Project: Calcite
Issue Type: Bug
Components: core
Affects Versions: 1.38.0
Reporter: Sean Broeder
Assignee: Sean Broeder
Summary:
Any call to a function whose SqlKind is remapped by SqlKind.getFunctionKind() —
POSITION and the newly-dedicated CHAR_LENGTH among roughly 90 others — can fail
validation with “No match found for function signature”, even though the exact
operator being called is registered and otherwise perfectly valid. The failure
only appears once a second candidate for the same operator name reaches the
routine-resolution pipeline’s fourth pass, which happens whenever an operator
table (or a chain of them) contributes more than one entry for that name.
Reproduction:
Minimal repro: chain the standard operator table with itself, which is enough
to produce the two-candidate precondition, then validate an ordinary call to
POSITION or CHAR_LENGTH.
{code:java}
// SqlValidatorTest.java
final SqlOperatorTable duplicated =
SqlOperatorTables.chain(SqlStdOperatorTable.instance(),
SqlStdOperatorTable.instance());
expr("position('mouse' in 'house')").withOperatorTable(duplicated).ok();
expr("char_length('string')").withOperatorTable(duplicated).ok();
{code}
Results:
{code:java}
org.opentest4j.AssertionFailedError: Validator threw unexpected exception;
query [values (position('mouse' in 'house'))];
exception [No match found for function signature POSITION(<CHARACTER>,
<CHARACTER>)]
{code}
The same query against a single, non-duplicated operator table validates
without issue — matching the fact that Calcite’s own existing
testPosition()/testCharLength() tests still pass today. The bug is real but
silent until a second contributor for the name is present, which is exactly
what happens in any composite operator table assembled from more than one
source (the situation that surfaced it).
Root Cause:
SqlUtil.lookupSubjectRoutines narrows candidates in four passes: by name, by
parameter count, by parameter type, and finally — only once at least two
candidates remain — by SqlKind:
{code:java}
private static Iterator<SqlOperator> filterOperatorRoutinesByKind(
Iterator<SqlOperator> routines, final SqlKind sqlKind) {
return Iterators.filter(routines,
operator -> requireNonNull(operator, "operator")
.getKind().getFunctionKind() == sqlKind);
}
{code}
The candidate’s kind is passed through getFunctionKind() — a 1.38 addition
whose own doc comment says: “If this kind represents a non-standard function,
return OTHER_FUNCTION, otherwise return this.” The requested kind, sqlKind, is
compared as-is — it is the call’s own, already-bound kind, never run through
the same mapping. For POSITION, that requested kind is SqlKind.POSITION (its x
IN y syntax forces the parser to bind the real operator eagerly, before any
type-driven resolution runs) — so the comparison reduces to OTHER_FUNCTION ==
POSITION. Always false. The operator fails to match itself.
The asymmetry is the whole bug: one side of the comparison is normalized, the
other isn’
Proposed Fix
Map both sides of the comparison through getFunctionKind(). For every kind the
switch doesn’t remap, this is a no-op — identical to today’s behavior and to
every version before 1.38. For the ~90 kinds it does remap, both sides now land
in the same bucket, restoring the ability for an operator to match itself.
{code:java}
@@ SqlUtil.java
private static Iterator<SqlOperator> filterOperatorRoutinesByKind(
Iterator<SqlOperator> routines, final SqlKind sqlKind) {
+ final SqlKind sqlFunctionKind = sqlKind.getFunctionKind();
return Iterators.filter(routines,
- operator -> requireNonNull(operator,
"operator").getKind().getFunctionKind() == sqlKind);
+ operator -> requireNonNull(operator,
"operator").getKind().getFunctionKind() == sqlFunctionKind);
}
{code}
--
This message was sent by Atlassian Jira
(v8.20.10#820010)