You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@calcite.apache.org by "Vladimir Sitnikov (Jira)" <ji...@apache.org> on 2026/10/14 08:15:00 UTC
[jira] [Created] (CALCITE-7854) FilterProjectTransposeRule throws NullPointerException when the Filter has a distribution trait and the Project's input has none
Vladimir Sitnikov created CALCITE-7854:
------------------------------------------
Summary: FilterProjectTransposeRule throws NullPointerException when the Filter has a distribution trait and the Project's input has none
Key: CALCITE-7854
URL: https://issues.apache.org/jira/browse/CALCITE-7854
Project: Calcite
Issue Type: Bug
Components: core
Reporter: Vladimir Sitnikov
Applying {{FilterProjectTransposeRule}} (for example {{CoreRules.FILTER_PROJECT_TRANSPOSE}}, which copies the Filter) throws {{NullPointerException}} when the Filter's trait set contains a distribution trait and the trait set of the Project's input does not. Trait sets differ in this way when the planner does not register {{RelDistributionTraitDef}} and a distribution is added to the Filter with {{RelTraitSet#plus}}, as a user-defined rule or Filter subclass can do.
Reproducer (for {{RelOptRulesTest}}; {{RelBuilderTest.config()}} uses a planner that does not register {{RelDistributionTraitDef}}):
{code:java}
@Test void testFilterProjectTransposeRuleInputWithoutDistribution() {
final RelBuilder b = RelBuilder.create(RelBuilderTest.config().build());
final Filter filter = (Filter) b
.scan("EMP")
.project(b.field("SAL"))
.filter(b.equals(b.field(0), b.literal(11500)))
.build();
final RelNode in =
filter.copy(filter.getTraitSet().plus(RelDistributions.SINGLETON),
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.getTraitSet().getTrait(RelDistributionTraitDef.INSTANCE),
is(RelDistributions.ANY));
}
{code}
Actual result on main (commit 8f27e24a02; 1.42.0 has the same code):
{noformat}
java.lang.NullPointerException: Cannot invoke "org.apache.calcite.plan.RelTrait.getClass()" because "trait" is null
at org.apache.calcite.plan.RelTraitDef.canonize(RelTraitDef.java:105)
at org.apache.calcite.plan.RelCompositeTrait.of(RelCompositeTrait.java:64)
at org.apache.calcite.plan.RelTraitSet.replaceIfs(RelTraitSet.java:264)
at org.apache.calcite.rel.rules.FilterProjectTransposeRule.onMatch(FilterProjectTransposeRule.java:188)
at org.apache.calcite.plan.AbstractRelOptPlanner.fireRule(AbstractRelOptPlanner.java:380)
at org.apache.calcite.plan.hep.HepPlanner.applyRule(HepPlanner.java:697)
{noformat}
Expected result: the rule pushes the Filter below the Project, and the pushed Filter has the default distribution, {{RelDistributions.ANY}}. That is what {{RelTraitSet#replaceIf}} and {{RelTraitSet#replaceIfs}} produce when their supplier returns null, and the rule already relies on that fallback for the collation trait.
Cause: when {{copyFilter}} is true, the rule copies the input's distribution with {{replaceIfs(RelDistributionTraitDef.INSTANCE, () -> Collections.singletonList(input.getTraitSet().getTrait(RelDistributionTraitDef.INSTANCE)))}}. {{getTrait}} returns null when the input's trait set has no distribution trait, so the supplier returns a list that holds null rather than null itself, and {{RelCompositeTrait.of}} fails on the null element. Calling {{replaceIf}}, which falls back to the default when the supplier returns null, fixes it.
Found while reviewing CALCITE-7736 ([PR #5213|https://github.com/apache/calcite/pull/5213]), which carries this fix inside the nullness migration.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)