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
The following commit(s) were added to refs/heads/main by this push:
new 9d5e1b28c9 [CALCITE-7640] Enumerable engine should execute aggregates
whose implementor comes from a custom RexImplementorTable
9d5e1b28c9 is described below
commit 9d5e1b28c96b3c87da17f4b0075cd39dd2e1bfca
Author: Darpan Lunagariya <[email protected]>
AuthorDate: Fri Jul 3 21:53:06 2026 +0530
[CALCITE-7640] Enumerable engine should execute aggregates whose
implementor comes from a custom RexImplementorTable
CALCITE-7631 made RexImplementorTable composable for scalar code
generation and constant reduction, but the aggregate path still resolves
implementors through the RexImpTable singleton: EnumerableAggregate's
constructor rejects any aggregate the built-ins do not know, and
AggImpState generates code against the singleton. A custom aggregate
implementor supplied through a chained table is therefore never used.
Resolve aggregate implementors through the injected table, defaulting to
the built-ins. Add RexImplementorTables.of(RelOptCluster) (reads the
planner context, else the built-in table), move the availability check
from the EnumerableAggregate constructor into EnumerableAggregateRule
(keeping only the table-independent structural checks in the node), and
thread the resolved table into AggImpState and its callers. Behaviour is
unchanged when no table is registered.
---
.../calcite/adapter/enumerable/AggImpState.java | 14 +-
.../adapter/enumerable/EnumerableAggregate.java | 10 +-
.../enumerable/EnumerableAggregateRule.java | 8 +
.../enumerable/EnumerableSortedAggregate.java | 4 +-
.../adapter/enumerable/EnumerableWindow.java | 4 +-
.../adapter/enumerable/RexImplementorTables.java | 10 +
.../apache/calcite/interpreter/AggregateNode.java | 5 +-
.../enumerable/EnumerableCustomAggregateTest.java | 214 +++++++++++++++++++++
8 files changed, 258 insertions(+), 11 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/AggImpState.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/AggImpState.java
index 0edc7351f0..a225837788 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/AggImpState.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/AggImpState.java
@@ -35,10 +35,22 @@ public class AggImpState {
public @MonotonicNonNull List<Expression> state;
public @MonotonicNonNull Expression accumulatorAdder;
+ /**
+ * Creates an AggImpState, resolving the implementor from the built-in table.
+ *
+ * @deprecated Use {@link #AggImpState(int, AggregateCall, boolean,
RexImplementorTable)}.
+ */
+ @Deprecated // to be removed before 2.0
public AggImpState(int aggIdx, AggregateCall call, boolean windowContext) {
+ this(aggIdx, call, windowContext, RexImpTable.INSTANCE);
+ }
+
+ public AggImpState(int aggIdx, AggregateCall call, boolean windowContext,
+ RexImplementorTable implementorTable) {
this.aggIdx = aggIdx;
this.call = call;
- AggImplementor implementor =
RexImpTable.INSTANCE.get(call.getAggregation(), windowContext);
+ AggImplementor implementor =
+ implementorTable.get(call.getAggregation(), windowContext);
if (implementor == null) {
throw new IllegalArgumentException(
"Unable to get aggregate implementation for aggregate "
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregate.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregate.java
index d38b26c333..0429168a8f 100644
---
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregate.java
+++
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregate.java
@@ -67,12 +67,6 @@ public EnumerableAggregate(
throw new InvalidRelException(
"within-distinct aggregation not supported");
}
- AggImplementor implementor2 =
- RexImpTable.INSTANCE.get(aggCall.getAggregation(), false);
- if (implementor2 == null) {
- throw new InvalidRelException(
- "aggregation " + aggCall.getAggregation() + " not supported");
- }
}
}
@@ -182,8 +176,10 @@ public EnumerableAggregate(RelOptCluster cluster,
RelTraitSet traitSet,
final int groupCount = getGroupCount();
final List<AggImpState> aggs = new ArrayList<>(aggCalls.size());
+ final RexImplementorTable implementorTable =
+ RexImplementorTables.of(getCluster());
for (Ord<AggregateCall> call : Ord.zip(aggCalls)) {
- aggs.add(new AggImpState(call.i, call.e, false));
+ aggs.add(new AggImpState(call.i, call.e, false, implementorTable));
}
// Function0<Object[]> accumulatorInitializer =
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregateRule.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregateRule.java
index db06fea558..5547408388 100644
---
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregateRule.java
+++
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregateRule.java
@@ -22,6 +22,7 @@
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.convert.ConverterRule;
import org.apache.calcite.rel.core.Aggregate;
+import org.apache.calcite.rel.core.AggregateCall;
import org.apache.calcite.rel.logical.LogicalAggregate;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -48,6 +49,13 @@ protected EnumerableAggregateRule(Config config) {
final Aggregate agg = (Aggregate) rel;
final RelTraitSet traitSet = rel.getCluster()
.traitSet().replace(EnumerableConvention.INSTANCE);
+ final RexImplementorTable implementorTable =
+ RexImplementorTables.of(rel.getCluster());
+ for (AggregateCall aggCall : agg.getAggCallList()) {
+ if (implementorTable.get(aggCall.getAggregation(), false) == null) {
+ return null;
+ }
+ }
try {
return new EnumerableAggregate(
rel.getCluster(),
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortedAggregate.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortedAggregate.java
index 73a4525112..29997dd3b7 100644
---
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortedAggregate.java
+++
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortedAggregate.java
@@ -136,8 +136,10 @@ public EnumerableSortedAggregate(
final int groupCount = getGroupCount();
final List<AggImpState> aggs = new ArrayList<>(aggCalls.size());
+ final RexImplementorTable implementorTable =
+ RexImplementorTables.of(getCluster());
for (Ord<AggregateCall> call : Ord.zip(aggCalls)) {
- aggs.add(new AggImpState(call.i, call.e, false));
+ aggs.add(new AggImpState(call.i, call.e, false, implementorTable));
}
// Function0<Object[]> accumulatorInitializer =
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindow.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindow.java
index 78ecce8821..1b60c56126 100644
---
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindow.java
+++
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindow.java
@@ -224,12 +224,14 @@ private static void
sampleOfTheGeneratedWindowedAggregate() {
List<AggImpState> aggs = new ArrayList<>();
List<AggregateCall> aggregateCalls = group.getAggregateCalls(this);
+ final RexImplementorTable implementorTable =
+ RexImplementorTables.of(getCluster());
for (int aggIdx = 0; aggIdx < aggregateCalls.size(); aggIdx++) {
AggregateCall call = aggregateCalls.get(aggIdx);
if (call.ignoreNulls()) {
throw new UnsupportedOperationException("IGNORE NULLS not
supported");
}
- aggs.add(new AggImpState(aggIdx, call, true));
+ aggs.add(new AggImpState(aggIdx, call, true, implementorTable));
}
// The output from this stage is the input plus the aggregate functions.
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImplementorTables.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImplementorTables.java
index 22b3340ee8..d5ff234a0e 100644
---
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImplementorTables.java
+++
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImplementorTables.java
@@ -17,6 +17,7 @@
package org.apache.calcite.adapter.enumerable;
import org.apache.calcite.adapter.enumerable.RexImpTable.RexCallImplementor;
+import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.sql.SqlAggFunction;
import org.apache.calcite.sql.SqlMatchFunction;
import org.apache.calcite.sql.SqlOperator;
@@ -36,6 +37,15 @@ public abstract class RexImplementorTables {
private RexImplementorTables() {
}
+ /** Returns the implementor table registered on the {@code cluster}'s planner
+ * {@link org.apache.calcite.plan.Context}, or the built-in
+ * {@link RexImpTable#instance()} when none is registered. */
+ public static RexImplementorTable of(RelOptCluster cluster) {
+ return cluster.getPlanner().getContext()
+ .maybeUnwrap(RexImplementorTable.class)
+ .orElse(RexImpTable.instance());
+ }
+
/** Creates a table that consults each of the given tables in turn, returning
* the first non-null implementor.
*
diff --git
a/core/src/main/java/org/apache/calcite/interpreter/AggregateNode.java
b/core/src/main/java/org/apache/calcite/interpreter/AggregateNode.java
index 7c908cc0e8..684baad8d4 100644
--- a/core/src/main/java/org/apache/calcite/interpreter/AggregateNode.java
+++ b/core/src/main/java/org/apache/calcite/interpreter/AggregateNode.java
@@ -22,6 +22,7 @@
import org.apache.calcite.adapter.enumerable.JavaRowFormat;
import org.apache.calcite.adapter.enumerable.PhysType;
import org.apache.calcite.adapter.enumerable.PhysTypeImpl;
+import org.apache.calcite.adapter.enumerable.RexImplementorTables;
import org.apache.calcite.adapter.enumerable.RexToLixTranslator;
import org.apache.calcite.adapter.enumerable.impl.AggAddContextImpl;
import org.apache.calcite.adapter.java.JavaTypeFactory;
@@ -143,7 +144,9 @@ private AccumulatorFactory getAccumulator(Compiler compiler,
final JavaTypeFactory typeFactory =
(JavaTypeFactory) rel.getCluster().getTypeFactory();
int stateOffset = 0;
- final AggImpState agg = new AggImpState(0, call, false);
+ final AggImpState agg =
+ new AggImpState(0, call, false,
+ RexImplementorTables.of(rel.getCluster()));
int stateSize = requireNonNull(agg.state, "agg.state").size();
final BlockBuilder builder2 = new BlockBuilder();
diff --git
a/core/src/test/java/org/apache/calcite/adapter/enumerable/EnumerableCustomAggregateTest.java
b/core/src/test/java/org/apache/calcite/adapter/enumerable/EnumerableCustomAggregateTest.java
new file mode 100644
index 0000000000..8e68b54757
--- /dev/null
+++
b/core/src/test/java/org/apache/calcite/adapter/enumerable/EnumerableCustomAggregateTest.java
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.adapter.enumerable;
+
+import org.apache.calcite.DataContexts;
+import org.apache.calcite.adapter.enumerable.RexImpTable.RexCallImplementor;
+import org.apache.calcite.jdbc.CalcitePrepare;
+import org.apache.calcite.linq4j.Enumerable;
+import org.apache.calcite.linq4j.Enumerator;
+import org.apache.calcite.plan.Contexts;
+import org.apache.calcite.plan.ConventionTraitDef;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRules;
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.plan.volcano.VolcanoPlanner;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelRoot;
+import org.apache.calcite.runtime.Bindable;
+import org.apache.calcite.sql.SqlAggFunction;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlMatchFunction;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.SqlWindowTableFunction;
+import org.apache.calcite.sql.fun.SqlBasicAggFunction;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.parser.SqlParser;
+import org.apache.calcite.sql.test.SqlTestFactory;
+import org.apache.calcite.sql.type.OperandTypes;
+import org.apache.calcite.sql.type.ReturnTypes;
+import org.apache.calcite.sql.util.SqlOperatorTables;
+import org.apache.calcite.sql.validate.SqlValidator;
+import org.apache.calcite.sql2rel.SqlToRelConverter;
+import org.apache.calcite.tools.FrameworkConfig;
+import org.apache.calcite.tools.Frameworks;
+import org.apache.calcite.tools.Planner;
+import org.apache.calcite.tools.Programs;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Tests that an aggregate implementor supplied by a custom
+ * {@link RexImplementorTable} registered on the planner
+ * {@link org.apache.calcite.plan.Context} is used both to plan and to execute
+ * an {@link EnumerableAggregate}.
+ */
+class EnumerableCustomAggregateTest {
+ /** An aggregate function with no built-in implementor. */
+ private static final SqlAggFunction MY_AGG =
+ SqlBasicAggFunction.create("MY_CUSTOM_AGG", SqlKind.OTHER_FUNCTION,
+ ReturnTypes.BIGINT, OperandTypes.ANY);
+
+ private static final String SQL = "SELECT g, MY_CUSTOM_AGG(x) AS c\n"
+ + "FROM (VALUES ('a', 1), ('a', 2), ('b', 3)) AS t (g, x)\n"
+ + "GROUP BY g";
+
+ /** Returns a table that maps {@link #MY_AGG} to the built-in {@code COUNT}
+ * implementor, so the custom aggregate counts its (non-null) input. */
+ private static RexImplementorTable myAggTable() {
+ final AggImplementor countImplementor =
+ requireNonNull(
+ RexImpTable.instance().get(SqlStdOperatorTable.COUNT, false),
+ "COUNT implementor");
+ return new RexImplementorTable() {
+ @Override public @Nullable RexCallImplementor get(SqlOperator operator) {
+ return null;
+ }
+
+ @Override public @Nullable AggImplementor get(SqlAggFunction aggregation,
+ boolean forWindowAggregate) {
+ return aggregation == MY_AGG ? countImplementor : null;
+ }
+
+ @Override public @Nullable MatchImplementor get(SqlMatchFunction
function) {
+ return null;
+ }
+
+ @Override public @Nullable TableFunctionCallImplementor get(
+ SqlWindowTableFunction operator) {
+ return null;
+ }
+ };
+ }
+
+ /** Plans a {@code GROUP BY} query through the {@link Frameworks} API. */
+ private static RelNode planWithFrameworks(
+ @Nullable RexImplementorTable implementorTable) throws Exception {
+ final List<RelOptRule> rules = new ArrayList<>(EnumerableRules.rules());
+ rules.addAll(RelOptRules.CALC_RULES);
+ rules.remove(EnumerableRules.ENUMERABLE_PROJECT_RULE);
+ Frameworks.ConfigBuilder configBuilder = Frameworks.newConfigBuilder()
+ .parserConfig(SqlParser.Config.DEFAULT)
+ .defaultSchema(Frameworks.createRootSchema(true))
+ .operatorTable(
+ SqlOperatorTables.chain(SqlStdOperatorTable.instance(),
+ SqlOperatorTables.of(MY_AGG)))
+ .programs(Programs.ofRules(rules));
+ if (implementorTable != null) {
+ configBuilder = configBuilder.context(Contexts.of(implementorTable));
+ }
+ final FrameworkConfig config = configBuilder.build();
+
+ final Planner planner = Frameworks.getPlanner(config);
+ final SqlNode parse = planner.parse(SQL);
+ final RelNode logical = planner.rel(planner.validate(parse)).project();
+ final RelTraitSet traitSet =
+ logical.getTraitSet().replace(EnumerableConvention.INSTANCE);
+ return planner.transform(0, traitSet, logical);
+ }
+
+ private static EnumerableRel planDirectly(RexImplementorTable
implementorTable)
+ throws Exception {
+ final VolcanoPlanner planner = new
VolcanoPlanner(Contexts.of(implementorTable));
+ planner.addRelTraitDef(ConventionTraitDef.INSTANCE);
+ for (RelOptRule rule : EnumerableRules.rules()) {
+ planner.addRule(rule);
+ }
+ for (RelOptRule rule : RelOptRules.CALC_RULES) {
+ planner.addRule(rule);
+ }
+
+ final SqlTestFactory factory = SqlTestFactory.INSTANCE
+ .withPlannerFactory(context -> planner)
+ .withOperatorTable(opTab ->
+ SqlOperatorTables.chain(opTab, SqlOperatorTables.of(MY_AGG)));
+ final SqlNode parse = factory.createParser(SQL).parseQuery();
+ final SqlToRelConverter converter = factory.createSqlToRelConverter();
+ final SqlValidator validator = converter.validator;
+ final RelRoot root =
+ converter.convertQuery(validator.validate(parse), false, true);
+ final RelTraitSet traitSet =
+ root.rel.getTraitSet().replace(EnumerableConvention.INSTANCE);
+ final RelNode rel = planner.changeTraits(root.rel, traitSet);
+ planner.setRoot(rel);
+ return (EnumerableRel) planner.findBestExp();
+ }
+
+ private static Map<String, Long> execute(EnumerableRel physical) {
+ final Bindable bindable =
+ EnumerableInterpretable.toBindable(Collections.emptyMap(),
+ CalcitePrepare.Dummy.getSparkHandler(false),
+ physical, EnumerableRel.Prefer.ARRAY);
+ final Enumerable<Object[]> result = bindable.bind(DataContexts.EMPTY);
+ final Map<String, Long> actual = new HashMap<>();
+ try (Enumerator<Object[]> enumerator = result.enumerator()) {
+ while (enumerator.moveNext()) {
+ final Object[] row = enumerator.current();
+ actual.put((String) row[0], ((Number) row[1]).longValue());
+ }
+ }
+ return actual;
+ }
+
+ /** With the custom table on the planner context, the aggregate is planned as
+ * an {@link EnumerableAggregate} and executes to the expected values. */
+ @Test void customAggregateImplementorIsUsedEndToEnd() throws Exception {
+ final RelNode physical =
+ planWithFrameworks(
+ RexImplementorTables.chain(myAggTable(), RexImpTable.instance()));
+ assertThat(RelOptUtil.toString(physical),
+ containsString("EnumerableAggregate"));
+
+ final Map<String, Long> actual = execute((EnumerableRel) physical);
+ assertThat(actual, is(ImmutableMap.of("a", 2L, "b", 1L)));
+ }
+
+ @Test void directPlannerUsesCustomTable() throws Exception {
+ final RexImplementorTable implementors =
+ RexImplementorTables.chain(myAggTable(), RexImpTable.instance());
+ final EnumerableRel physical = planDirectly(implementors);
+
+ assertThat(RelOptUtil.toString(physical),
+ containsString("EnumerableAggregate"));
+ assertThat(execute(physical), is(ImmutableMap.of("a", 2L, "b", 1L)));
+ }
+
+ /** Without the custom table the built-in table has no implementor for the
+ * aggregate, so {@link EnumerableAggregateRule} declines and planning
fails. */
+ @Test void unknownAggregateIsRejectedWithoutCustomTable() {
+ assertThrows(RuntimeException.class,
+ () -> planWithFrameworks(null));
+ }
+}