Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@
import io.trino.sql.planner.iterative.rule.RewriteTableFunctionToTableScan;
import io.trino.sql.planner.iterative.rule.SimplifyCountOverConstant;
import io.trino.sql.planner.iterative.rule.SimplifyExpressions;
import io.trino.sql.planner.iterative.rule.SimplifyFalseConditions;
import io.trino.sql.planner.iterative.rule.SimplifyFilterPredicate;
import io.trino.sql.planner.iterative.rule.SingleDistinctAggregationToGroupBy;
import io.trino.sql.planner.iterative.rule.TransformCorrelatedDistinctAggregationWithProjection;
Expand Down Expand Up @@ -374,6 +375,7 @@ public PlanOptimizers(
.addAll(new CanonicalizeExpressions(plannerContext, typeAnalyzer).rules())
.addAll(new RemoveRedundantDateTrunc(plannerContext, typeAnalyzer).rules())
.addAll(new ArraySortAfterArrayDistinct(plannerContext).rules())
.addAll(new SimplifyFalseConditions(plannerContext).rules())
.add(new RemoveTrivialFilters())
.build();
IterativeOptimizer simplifyOptimizer = new IterativeOptimizer(
Expand Down Expand Up @@ -420,6 +422,7 @@ public PlanOptimizers(
.addAll(limitPushdownRules)
.addAll(new UnwrapRowSubscript().rules())
.addAll(new PushCastIntoRow().rules())
.addAll(new SimplifyFalseConditions(plannerContext).rules())
.addAll(ImmutableSet.of(
new ImplementTableFunctionSource(metadata),
new UnwrapSingleColumnRowInApply(typeAnalyzer),
Expand Down Expand Up @@ -579,6 +582,8 @@ public PlanOptimizers(
.addAll(columnPruningRules)
.add(new InlineProjections(plannerContext, typeAnalyzer))
.addAll(new PushFilterThroughCountAggregation(plannerContext).rules()) // must run after PredicatePushDown and after TransformFilteringSemiJoinToInnerJoin
.addAll(new SimplifyFalseConditions(plannerContext).rules())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's already a rule for this: RemoveTrivialFilters. The new logic should be implemented within that rule.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it became a separate rule following #15558 (comment)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was mainly because of @martint's comment from 4 years ago:

// TODO: DomainTranslator.fromPredicate can infer that the expression is "false" in some cases (TupleDomain.none()).
// This should move to another rule that simplifies the filter using that logic and then rely on RemoveTrivialFilters
// to turn the subtree into a Values node

But I don't object to merging these two, they seem to go together anyway + that will turn RemoveTrivialFilters into an ExpressionRewriteRuleSet so maybe it will also benefit the join case (I didn't check).
I'm not sure the term "trivial" will still be accurate though.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok to have a rule that simplifies expressions (we already have one) and one that removes filters where the expression is known to filter everything or nothing (we already have one, too). But we should not add yet another rule that mixes both concerns.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took another look, RemoveTrivialFilters can't become an ExpressionRewriteRuleSet because ExpressionRewriteRuleSet doesn't replace a node of one type with a node of another type.
The new rule's logic can't be moved into SimplifyExpressions because not all node types are supported (see #16206 (comment)).
I can make RemoveTrivialFilters return 2 rules, but I failed to prove the join case (couldn't find an actual query that has a final plan that contains a JoinNode with a filter that can be simplified to FALSE \ TRUE).
@findepi \ @martint WDYT should be the next step?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the join case (couldn't find an actual query that has a final plan that contains a JoinNode with a filter that can be simplified to FALSE \ TRUE).

I guess that's because PPD pushes such filter under the join and then it gets simplified/eliminated there

But we should not add yet another rule that mixes both concerns.

I read it as implying RemoveTrivialFilters shouldn't call DomainTranslator for help.

We tried to introduce a generic rule ExpressionRewriteRuleSet-based to simplify boolean conditions that can be replaced with constant false. I know what problems this run into (distinguishing context: eg between projected).
Did it result in more test coverage? like cases mentioned in #16206 (comment) ?

Copy link
Copy Markdown
Member Author

@assaf2 assaf2 Apr 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read it as implying RemoveTrivialFilters shouldn't call DomainTranslator for help.

Please see #16206 (comment). Note that adding this logic into RemoveTrivialFilters will address this concern you raised #16206 (comment).

Did it result in more test coverage? like cases mentioned in #16206 (comment) ?

I didn't add new tests for those cases.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't add new tests for those cases.

Can you please do?
These are interesting test cases to have, regardless of the implementation choices we end up doing in this PR

.add(new RemoveTrivialFilters())
.build()));

// Perform redirection before CBO rules to ensure stats from destination connector are used
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,7 @@ public static Optional<PlanNode> pushFilterIntoTableScan(
}

if (newDomain.isNone()) {
// TODO: DomainTranslator.fromPredicate can infer that the expression is "false" in some cases (TupleDomain.none()).
// This should move to another rule that simplifies the filter using that logic and then rely on RemoveTrivialFilters
// to turn the subtree into a Values node
// This is just for extra safety, SimplifyFalseConditions is responsible for eliminating such filters
return Optional.of(new ValuesNode(node.getId(), node.getOutputSymbols(), ImmutableList.of()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
* - Exchange (optional)
* - MergeWriter
* - Exchange
* - Project
* - Project (optional)
* - empty Values
* </pre>
* into
Expand All @@ -61,10 +61,20 @@ public static Set<Rule<?>> rules()
{
return ImmutableSet.of(
removeEmptyMergeWriterRule(),
removeEmptyMergeWriterWithExchangeRule());
removeEmptyMergeWriterWithProjectRule(),
removeEmptyMergeWriterWithExchangeRule(),
removeEmptyMergeWriterWithProjectAndExchangeRule());
}

static Rule<TableFinishNode> removeEmptyMergeWriterRule()
{
return new RemoveEmptyMergeWriter(tableFinish()
.with(source().matching(mergeWriter()
.with(source().matching(exchange()
.with(source().matching(emptyValues())))))));
}

static Rule<TableFinishNode> removeEmptyMergeWriterWithProjectRule()
{
return new RemoveEmptyMergeWriter(tableFinish()
.with(source().matching(mergeWriter()
Expand All @@ -74,6 +84,15 @@ static Rule<TableFinishNode> removeEmptyMergeWriterRule()
}

static Rule<TableFinishNode> removeEmptyMergeWriterWithExchangeRule()
{
return new RemoveEmptyMergeWriter(tableFinish()
.with(source().matching(exchange()
.with(source().matching(mergeWriter()
.with(source().matching(exchange()
.with(source().matching(emptyValues())))))))));
}

static Rule<TableFinishNode> removeEmptyMergeWriterWithProjectAndExchangeRule()
{
return new RemoveEmptyMergeWriter(tableFinish()
.with(source().matching(exchange()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,7 @@ public Result apply(FilterNode filterNode, Captures captures, Context context)
.transformKeys(node.getAssignments()::get);

if (predicateDomain.isNone()) {
// TODO: DomainTranslator.fromPredicate can infer that the expression is "false" in some cases (TupleDomain.none()).
// This should move to another rule that simplifies the filter using that logic and then rely on RemoveTrivialFilters
// to turn the subtree into a Values node
// This is just for extra safety, SimplifyFalseConditions is responsible for eliminating such filters
return Result.ofPlanNode(new ValuesNode(node.getId(), node.getOutputSymbols(), ImmutableList.of()));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Licensed 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 io.trino.sql.planner.iterative.rule;

import com.google.common.collect.ImmutableSet;
import io.trino.Session;
import io.trino.metadata.Metadata;
import io.trino.sql.PlannerContext;
import io.trino.sql.planner.DomainTranslator;
import io.trino.sql.planner.TypeProvider;
import io.trino.sql.planner.iterative.Rule;
import io.trino.sql.tree.Expression;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import static io.trino.sql.ExpressionUtils.combineConjuncts;
import static io.trino.sql.ExpressionUtils.extractConjuncts;
import static io.trino.sql.planner.DeterminismEvaluator.isDeterministic;
import static io.trino.sql.tree.BooleanLiteral.FALSE_LITERAL;
import static java.util.Objects.requireNonNull;

/**
* Uses DomainTranslator#getExtractionResult to infer that the expression is "false" in some cases (TupleDomain.none()).
*/
public class SimplifyFalseConditions
extends ExpressionRewriteRuleSet
{
public SimplifyFalseConditions(PlannerContext plannerContext)
{
super(createRewrite(plannerContext));
}

@Override
public Set<Rule<?>> rules()
{
// Can only apply on expressions that are used for filtering, otherwise TupleDomain.none() might represent a NULL value
return ImmutableSet.of(
filterExpressionRewrite(),
joinExpressionRewrite());
}

private static ExpressionRewriter createRewrite(PlannerContext plannerContext)
{
requireNonNull(plannerContext, "plannerContext is null");

return (expression, context) -> rewrite(context.getSession(), plannerContext, context.getSymbolAllocator().getTypes(), expression);
}

public static Expression rewrite(Session session, PlannerContext plannerContext, TypeProvider types, Expression expression)
{
Metadata metadata = plannerContext.getMetadata();
List<Expression> deterministicPredicates = new ArrayList<>();
for (Expression conjunct : extractConjuncts(expression)) {
try {
if (isDeterministic(conjunct, metadata)) {
deterministicPredicates.add(conjunct);
}
}
catch (IllegalArgumentException ignored) {
// Might fail in case of an unresolved function
}
}

DomainTranslator.ExtractionResult decomposedPredicate = DomainTranslator.getExtractionResult(
plannerContext,
session,
combineConjuncts(metadata, deterministicPredicates),
types);

if (decomposedPredicate.getTupleDomain().isNone()) {
return FALSE_LITERAL;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if the expression was already a false_literal? do we need to return expression in such case (same, not just equal)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a reason for that, do you have something in mind?

}

return expression;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import io.trino.sql.planner.plan.Assignments;
import io.trino.sql.planner.plan.ExchangeNode;
import io.trino.sql.planner.plan.PlanNode;
import io.trino.sql.planner.plan.ProjectNode;
import io.trino.sql.planner.plan.TableFinishNode;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
Expand All @@ -34,6 +35,8 @@
import static io.trino.sql.planner.assertions.PlanMatchPattern.values;
import static io.trino.sql.planner.iterative.rule.RemoveEmptyMergeWriterRuleSet.removeEmptyMergeWriterRule;
import static io.trino.sql.planner.iterative.rule.RemoveEmptyMergeWriterRuleSet.removeEmptyMergeWriterWithExchangeRule;
import static io.trino.sql.planner.iterative.rule.RemoveEmptyMergeWriterRuleSet.removeEmptyMergeWriterWithProjectAndExchangeRule;
import static io.trino.sql.planner.iterative.rule.RemoveEmptyMergeWriterRuleSet.removeEmptyMergeWriterWithProjectRule;

public class TestRemoveEmptyMergeWriterRuleSet
extends BaseRuleTest
Expand All @@ -51,34 +54,40 @@ public void setup()
@Test
public void testRemoveEmptyMergeRewrite()
{
testRemoveEmptyMergeRewrite(removeEmptyMergeWriterRule(), false);
testRemoveEmptyMergeRewrite(removeEmptyMergeWriterRule(), false, false);
}

@Test
public void testRemoveEmptyMergeRewriteWithProject()
{
testRemoveEmptyMergeRewrite(removeEmptyMergeWriterWithProjectRule(), true, false);
}

@Test
public void testRemoveEmptyMergeRewriteWithExchange()
{
testRemoveEmptyMergeRewrite(removeEmptyMergeWriterWithExchangeRule(), true);
testRemoveEmptyMergeRewrite(removeEmptyMergeWriterWithExchangeRule(), false, true);
}

private void testRemoveEmptyMergeRewrite(Rule<TableFinishNode> rule, boolean planWithExchange)
@Test
public void testRemoveEmptyMergeRewriteWithProjectAndExchange()
{
testRemoveEmptyMergeRewrite(removeEmptyMergeWriterWithProjectAndExchangeRule(), true, true);
}

private void testRemoveEmptyMergeRewrite(Rule<TableFinishNode> rule, boolean planWithProject, boolean planWithExchange)
{
tester().assertThat(rule)
.on(p -> {
Symbol mergeRow = p.symbol("merge_row");
Symbol rowId = p.symbol("row_id");
Symbol rowCount = p.symbol("row_count");

PlanNode values = p.values(mergeRow, rowId, rowCount);
PlanNode merge = p.merge(
schemaTableName,
p.exchange(e -> e
.addSource(
p.project(
Assignments.builder()
.putIdentity(mergeRow)
.putIdentity(rowId)
.putIdentity(rowCount)
.build(),
p.values(mergeRow, rowId, rowCount)))
.addSource(planWithProject ? withProject(p, values) : values)
.addInputsSet(mergeRow, rowId, rowCount)
.partitioningScheme(
new PartitioningScheme(
Expand All @@ -95,6 +104,13 @@ private void testRemoveEmptyMergeRewrite(Rule<TableFinishNode> rule, boolean pla
.matches(values("A"));
}

private ProjectNode withProject(PlanBuilder planBuilder, PlanNode values)
{
Assignments.Builder assignments = Assignments.builder();
values.getOutputSymbols().forEach(assignments::putIdentity);
return planBuilder.project(assignments.build(), values);
}

private ExchangeNode withExchange(PlanBuilder planBuilder, PlanNode source, Symbol symbol)
{
return planBuilder.exchange(e -> e
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Licensed 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 io.trino.sql.planner.iterative.rule;

import io.trino.sql.planner.assertions.BasePlanTest;
import org.testng.annotations.Test;

import static io.trino.sql.planner.assertions.PlanMatchPattern.output;
import static io.trino.sql.planner.assertions.PlanMatchPattern.values;
import static java.lang.String.format;

public class TestSimplifyFalseConditions
extends BasePlanTest
{
@Test
public void testPredicateIsFalse()
{
testRewrite("FALSE");
testRewrite("NOT (TRUE)");
}

@Test
public void testCastNullToBoolean()
{
testRewriteWithAndWithoutNegation("CAST(null AS boolean)");
}

@Test
public void testLogicalExpression()
{
testRewrite("TRUE AND CAST(null AS boolean)");
testRewrite("col = BIGINT '44' AND CAST(null AS boolean)");
testRewrite("col = BIGINT '44' AND FALSE");
}

@Test
public void testComparison()
{
testRewriteWithAndWithoutNegation("col > CAST(null AS BIGINT)");
testRewriteWithAndWithoutNegation("col >= CAST(null AS BIGINT)");
testRewriteWithAndWithoutNegation("col < CAST(null AS BIGINT)");
testRewriteWithAndWithoutNegation("col <= CAST(null AS BIGINT)");
testRewriteWithAndWithoutNegation("col = CAST(null AS BIGINT)");
testRewriteWithAndWithoutNegation("col != CAST(null AS BIGINT)");
}

@Test
public void testIn()
{
testRewriteWithAndWithoutNegation("col IN (CAST(null AS BIGINT))");
}

@Test
public void testBetween()
{
testRewrite("col BETWEEN (CAST(null AS BIGINT)) AND BIGINT '44'");
testRewrite("col BETWEEN BIGINT '44' AND (CAST(null AS BIGINT))");
}

private void testRewriteWithAndWithoutNegation(String inputPredicate)
{
testRewrite(inputPredicate);
testRewrite(format("NOT (%s)", inputPredicate));
}

private void testRewrite(String inputPredicate)
{
assertPlan(String.format("SELECT * FROM (VALUES BIGINT '1') t(col) WHERE %s", inputPredicate),
output(
values("a")));
}
}