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 ee15085f4d [CALCITE-7854] FilterProjectTransposeRule throws
NullPointerException when the Filter has a distribution trait and the Project's
input has none
ee15085f4d is described below
commit ee15085f4d1c848434524d56ed2c2e4e22c53884
Author: Vladimir Sitnikov <[email protected]>
AuthorDate: Wed Oct 14 11:16:04 2026 +0300
[CALCITE-7854] FilterProjectTransposeRule throws NullPointerException when
the Filter has a distribution trait and the Project's input has none
With copyFilter enabled, FilterProjectTransposeRule copied the input's
distribution into the pushed Filter by wrapping
input.getTraitSet().getTrait(RelDistributionTraitDef.INSTANCE) in a
singleton list for RelTraitSet#replaceIfs. When the Filter's trait set
has a distribution trait and the input's trait set has none, getTrait
returns null, and RelCompositeTrait.of fails on the null element:
java.lang.NullPointerException: Cannot invoke
"org.apache.calcite.plan.RelTrait.getClass()" because "trait" is null
This happens, for example, when the planner does not register
RelDistributionTraitDef and a distribution is added to the Filter with
RelTraitSet#plus.
Use RelTraitSet#replaceIf, which takes a single trait and falls back to
the trait's default when the supplier returns null; LogicalFilter.create
sets the distribution the same way. The pushed Filter then has
RelDistributions.ANY, the same fallback the rule already uses for
collation. When the input has a distribution, the pushed Filter still
takes it.
Found while reviewing CALCITE-7736 (PR #5213), which carries the same
fix inside its nullness migration.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../rel/rules/FilterProjectTransposeRule.java | 6 +-
.../org/apache/calcite/test/RelOptRulesTest.java | 64 ++++++++++++++++++++++
2 files changed, 66 insertions(+), 4 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/rel/rules/FilterProjectTransposeRule.java
b/core/src/main/java/org/apache/calcite/rel/rules/FilterProjectTransposeRule.java
index 3c365222cd..7851460550 100644
---
a/core/src/main/java/org/apache/calcite/rel/rules/FilterProjectTransposeRule.java
+++
b/core/src/main/java/org/apache/calcite/rel/rules/FilterProjectTransposeRule.java
@@ -35,7 +35,6 @@
import org.immutables.value.Value;
-import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -185,9 +184,8 @@ protected FilterProjectTransposeRule(
final RelTraitSet traitSet = filter.getTraitSet()
.replaceIfs(RelCollationTraitDef.INSTANCE,
() ->
input.getTraitSet().getTraits(RelCollationTraitDef.INSTANCE))
- .replaceIfs(RelDistributionTraitDef.INSTANCE,
- () -> Collections.singletonList(
-
input.getTraitSet().getTrait(RelDistributionTraitDef.INSTANCE)));
+ .replaceIf(RelDistributionTraitDef.INSTANCE,
+ () ->
input.getTraitSet().getTrait(RelDistributionTraitDef.INSTANCE));
newCondition =
RexUtil.removeNullabilityCast(relBuilder.getTypeFactory(), newCondition);
newFilterRel = filter.copy(traitSet, input, newCondition);
} else {
diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
index 7ca0dc464b..e68f5100fd 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -42,6 +42,7 @@
import org.apache.calcite.rel.RelCollation;
import org.apache.calcite.rel.RelCollationTraitDef;
import org.apache.calcite.rel.RelCollations;
+import org.apache.calcite.rel.RelDistribution;
import org.apache.calcite.rel.RelDistributionTraitDef;
import org.apache.calcite.rel.RelDistributions;
import org.apache.calcite.rel.RelFieldCollation;
@@ -66,6 +67,7 @@
import org.apache.calcite.rel.logical.LogicalMinus;
import org.apache.calcite.rel.logical.LogicalProject;
import org.apache.calcite.rel.logical.LogicalTableModify;
+import org.apache.calcite.rel.logical.LogicalTableScan;
import org.apache.calcite.rel.logical.LogicalUnion;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.calcite.rel.rules.AggregateExpandWithinDistinctRule;
@@ -166,6 +168,7 @@
import static org.apache.calcite.test.SqlToRelTestBase.NL;
+import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -687,6 +690,67 @@ private HepProgram createHypergraphProgram() {
}
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7854">[CALCITE-7854]
+ * FilterProjectTransposeRule throws NullPointerException when the Filter has
+ * a distribution trait and the Project's input has none</a>.
+ *
+ * <p>The pushed Filter gets the default distribution. The rule used to put
+ * a null distribution into the new trait set and failed with
+ * NullPointerException. */
+ @Test void testFilterProjectTransposeRuleInputWithoutDistribution() {
+ assertThat(pushedFilterDistribution(null), is(RelDistributions.ANY));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7854">[CALCITE-7854]
+ * FilterProjectTransposeRule throws NullPointerException when the Filter has
+ * a distribution trait and the Project's input has none</a>.
+ *
+ * <p>The pushed Filter takes the input's distribution, not the one the
+ * original Filter had. */
+ @Test void testFilterProjectTransposeRuleInputWithDistribution() {
+ assertThat(pushedFilterDistribution(RelDistributions.SINGLETON),
+ is(RelDistributions.SINGLETON));
+ }
+
+ /** Applies {@link CoreRules#FILTER_PROJECT_TRANSPOSE} to
+ * Filter(Project(Scan EMP)) and returns the distribution of the pushed
+ * Filter.
+ *
+ * <p>The planner behind {@link RelBuilderTest#config()} does not register
+ * {@link RelDistributionTraitDef}, so distributions are added through
+ * {@link RelTraitSet#plus}: the Filter has {@code hash[0]}, and the scan has
+ * {@code inputDistribution}, or no distribution trait when it is null. */
+ private static @Nullable RelDistribution pushedFilterDistribution(
+ @Nullable RelDistribution inputDistribution) {
+ final RelBuilder b = RelBuilder.create(RelBuilderTest.config().build());
+ final TableScan scan = (TableScan) b.scan("EMP").build();
+ final RelNode input = inputDistribution == null
+ ? scan
+ : new LogicalTableScan(scan.getCluster(),
+ scan.getTraitSet().plus(inputDistribution), scan.getHints(),
+ scan.getTable());
+ final Filter filter = (Filter) b.push(input)
+ .project(b.field("SAL"))
+ .filter(b.equals(b.field(0), b.literal(11500)))
+ .build();
+ final RelNode in =
+ filter.copy(
+
filter.getTraitSet().plus(RelDistributions.hash(ImmutableList.of(0))),
+ filter.getInput(), filter.getCondition());
+ final HepProgram program = new HepProgramBuilder()
+ .addRuleInstance(CoreRules.FILTER_PROJECT_TRANSPOSE)
+ .build();
+ final HepPlanner hepPlanner = new HepPlanner(program);
+ hepPlanner.setRoot(in);
+ final RelNode result = hepPlanner.findBestExp();
+
+ final RelNode pushedFilter = result.getInput(0);
+ assertThat(pushedFilter, instanceOf(Filter.class));
+ return
pushedFilter.getTraitSet().getTrait(RelDistributionTraitDef.INSTANCE);
+ }
+
/** Test case for
* <a
href="https://issues.apache.org/jira/browse/CALCITE-6873">[CALCITE-6873]
* FilterProjectTransposeRule should not push the Filter past the Project