From 960f4b9bb3360b1415e99f45d210844bc7e3c299 Mon Sep 17 00:00:00 2001 From: Sergey Nuyanzin Date: Sun, 2 Aug 2026 23:10:05 +0200 Subject: [PATCH] [CALCITE-7686] Named argument should not be resolved as a column --- .../sql/validate/SqlValidatorImpl.java | 6 +- .../apache/calcite/sql2rel/AggConverter.java | 14 ++++- .../java/org/apache/calcite/test/UdfTest.java | 63 +++++++++++++++++++ .../java/org/apache/calcite/util/Smalls.java | 25 ++++++++ 4 files changed, 105 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index 7021696b679c..1feaf198b955 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -4661,9 +4661,11 @@ private void checkRollUp(@Nullable SqlNode grandParent, @Nullable SqlNode parent checkRollUp(grandParent, parent, stripDot, scope, contextClause); } else if (stripDot.getKind() == SqlKind.CONVERT || stripDot.getKind() == SqlKind.TRANSLATE - || stripDot.getKind() == SqlKind.CONVERT_ORACLE) { + || stripDot.getKind() == SqlKind.CONVERT_ORACLE + || stripDot.getKind() == SqlKind.ARGUMENT_ASSIGNMENT) { // only need to check operand[0] for - // CONVERT, TRANSLATE or CONVERT_ORACLE + // 1. CONVERT, TRANSLATE or CONVERT_ORACLE + // 2. for a named argument "value => name"; operand[1] is the parameter name, not a column SqlNode child = ((SqlCall) stripDot).getOperandList().get(0); checkRollUp(parent, current, child, scope, contextClause); } else if (stripDot.getKind() == SqlKind.LAMBDA) { diff --git a/core/src/main/java/org/apache/calcite/sql2rel/AggConverter.java b/core/src/main/java/org/apache/calcite/sql2rel/AggConverter.java index 65915b69e197..0193b17c3e00 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/AggConverter.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/AggConverter.java @@ -29,6 +29,7 @@ import org.apache.calcite.runtime.PairList; import org.apache.calcite.sql.SqlAggFunction; import org.apache.calcite.sql.SqlCall; +import org.apache.calcite.sql.SqlCallBinding; import org.apache.calcite.sql.SqlDataTypeSpec; import org.apache.calcite.sql.SqlDynamicParam; import org.apache.calcite.sql.SqlIdentifier; @@ -457,7 +458,18 @@ private void translateAgg(SqlCall call, @Nullable SqlNode filter, try { // switch out of agg mode bb.agg = null; - for (SqlNode operand : call.getOperandList()) { + // Permute named arguments ("name => value") into formal parameter order + // and strip the ARGUMENT_ASSIGNMENT wrappers, so that the parameter name + // identifiers are not converted as column references. + final boolean hasNamedArgument = + call.getOperandList().stream() + .anyMatch(node -> node.getKind() == SqlKind.ARGUMENT_ASSIGNMENT); + final List aggOperands = + hasNamedArgument + ? new SqlCallBinding(bb.getValidator(), bb.scope, call) + .permutedCall().getOperandList() + : call.getOperandList(); + for (SqlNode operand : aggOperands) { // special case for COUNT(*): delete the * if (operand instanceof SqlIdentifier) { diff --git a/core/src/test/java/org/apache/calcite/test/UdfTest.java b/core/src/test/java/org/apache/calcite/test/UdfTest.java index 63492af2bcfc..282cc0ae21b1 100644 --- a/core/src/test/java/org/apache/calcite/test/UdfTest.java +++ b/core/src/test/java/org/apache/calcite/test/UdfTest.java @@ -531,6 +531,26 @@ private CalciteAssert.AssertThat withUdf() { .returns("EXPR$0=0\n"); } + /** Test case for + * [CALCITE-7686] + * Named argument should not be resolved as a column. + * + *

The parameter name of a {@code name => value} argument must not be + * resolved as a column reference by the roll-up check. */ + @Test void testUdfArgumentNameInSelectFromTable() { + final CalciteAssert.AssertThat with = withUdf(); + // Named-arg call as a SELECT item over a table scope (unlike VALUES, this + // routes through checkRollUpInSelectList). The parameter names "s" and "n" + // must not be resolved as columns of the FROM source. + with.query("select \"adhoc\".my_left(\"s\" => 'hello', \"n\" => 3) as c\n" + + "from \"adhoc\".\"EMPLOYEES\"") + .returnsCount(4); + // reverse order + with.query("select \"adhoc\".my_left(\"n\" => 3, \"s\" => 'hello') as c\n" + + "from \"adhoc\".\"EMPLOYEES\"") + .returnsCount(4); + } + /** Tests calling a user-defined function some of whose parameters are * optional. */ @Test void testUdfArgumentOptional() { @@ -740,6 +760,49 @@ private CalciteAssert.AssertThat withUdf() { .returns("P=560\n"); } + /** + * Test case for [CALCITE-7686] + * Named argument should not be resolved as a column. + * + *

Tests calling a user-defined aggregate function by named arguments over a + * table scope. The parameter names must not be resolved as columns during + * sql-to-rel conversion. */ + @Test void testUserDefinedAggregateFunctionWithNamedArguments() { + final String empDept = JdbcTest.EmpDeptTableFactory.class.getName(); + final String namedSum = Smalls.MyNamedSumFunction.class.getName(); + final CalciteAssert.AssertThat with = CalciteAssert.model("{\n" + + " version: '1.0',\n" + + " schemas: [\n" + + " {\n" + + " name: 'adhoc',\n" + + " tables: [\n" + + " {\n" + + " name: 'EMPLOYEES',\n" + + " type: 'custom',\n" + + " factory: '" + empDept + "',\n" + + " operand: {'foo': true, 'bar': 345}\n" + + " }\n" + + " ],\n" + + " functions: [\n" + + " {\n" + + " name: 'MY_NAMED_SUM',\n" + + " className: '" + namedSum + "'\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}") + .withDefaultSchema("adhoc"); + // named arguments in physical order + with.query("select \"adhoc\".my_named_sum(\"v1\" => \"commission\", \"v2\" => 250) as p\n" + + "from \"adhoc\".EMPLOYEES\n") + .returns("P=1500\n"); + // named arguments in reverse order + with.query("select \"adhoc\".my_named_sum(\"v2\" => 250, \"v1\" => \"commission\") as p\n" + + "from \"adhoc\".EMPLOYEES\n") + .returns("P=1500\n"); + } + /** Test for * {@link org.apache.calcite.runtime.CalciteResource#firstParameterOfAdd(String)}. */ @Test void testUserDefinedAggregateFunction3() { diff --git a/testkit/src/main/java/org/apache/calcite/util/Smalls.java b/testkit/src/main/java/org/apache/calcite/util/Smalls.java index 6f33f00669da..b1748cb66cf6 100644 --- a/testkit/src/main/java/org/apache/calcite/util/Smalls.java +++ b/testkit/src/main/java/org/apache/calcite/util/Smalls.java @@ -909,6 +909,31 @@ public static long result(long accumulator) { } } + /** Example of a UDAF with two named parameters (via {@code @Parameter}), + * so it can be called using named-argument notation. It sums {@code v1} + * for rows where {@code v1 > v2}. */ + public static class MyNamedSumFunction { + public MyNamedSumFunction() { + } + public int init() { + return 0; + } + public int add(int accumulator, + @Parameter(name = "v1") int v1, + @Parameter(name = "v2") int v2) { + if (v1 > v2) { + return accumulator + v1; + } + return accumulator; + } + public int merge(int accumulator0, int accumulator1) { + return accumulator0 + accumulator1; + } + public int result(int accumulator) { + return accumulator; + } + } + /** Example of a user-defined aggregate function (UDAF) with two parameters. * The constructor has an initialization parameter. */ public static class MyTwoParamsSumFunctionFilter1 {