diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptRules.java b/core/src/main/java/org/apache/calcite/plan/RelOptRules.java index 434e278dcb8..16152bc800d 100644 --- a/core/src/main/java/org/apache/calcite/plan/RelOptRules.java +++ b/core/src/main/java/org/apache/calcite/plan/RelOptRules.java @@ -107,7 +107,6 @@ private RelOptRules() { PruneEmptyRules.WINDOW_INSTANCE, PruneEmptyRules.JOIN_LEFT_INSTANCE, PruneEmptyRules.JOIN_RIGHT_INSTANCE, - PruneEmptyRules.SORT_FETCH_ZERO_INSTANCE, PruneEmptyRules.EMPTY_TABLE_INSTANCE, SingleValuesOptimizationRules.JOIN_LEFT_INSTANCE, SingleValuesOptimizationRules.JOIN_RIGHT_INSTANCE, diff --git a/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java b/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java index 02b0bd8af1b..a9b8b0a5b5b 100644 --- a/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java +++ b/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java @@ -35,14 +35,13 @@ import org.apache.calcite.rel.core.Minus; import org.apache.calcite.rel.core.Project; import org.apache.calcite.rel.core.Sort; -import org.apache.calcite.rel.core.TableScan; +import org.apache.calcite.rel.core.TableModify; import org.apache.calcite.rel.core.Union; import org.apache.calcite.rel.core.Values; import org.apache.calcite.rel.core.Window; import org.apache.calcite.rel.logical.LogicalValues; import org.apache.calcite.rel.metadata.RelMdUtil; import org.apache.calcite.rel.type.RelDataType; -import org.apache.calcite.rex.RexDynamicParam; import org.apache.calcite.rex.RexLiteral; import org.apache.calcite.rex.RexNode; import org.apache.calcite.tools.RelBuilder; @@ -50,7 +49,6 @@ import org.immutables.value.Value; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -223,14 +221,13 @@ private static boolean isEmpty(RelNode node) { * Rule that converts a {@link org.apache.calcite.rel.core.Sort} * to empty if it has {@code LIMIT 0}. * - *

Examples: - * - *

+ * @deprecated Use {@link #EMPTY_TABLE_INSTANCE}, which uses + * {@link RelMdUtil#isRelDefinitelyEmpty} to prune any relational expression + * that is definitely empty. */ + @Deprecated // to be removed before 2.0 public static final RelOptRule SORT_FETCH_ZERO_INSTANCE = - SortFetchZeroRuleConfig.DEFAULT.toRule(); + ZeroMaxRowsRuleConfig.DEFAULT.toRule(); /** * Rule that converts an {@link org.apache.calcite.rel.core.Aggregate} @@ -525,25 +522,6 @@ public interface IntersectEmptyPruneRuleConfig extends PruneEmptyRule.Config { } } - /** Configuration for a rule that prunes a Sort if it has limit 0. */ - @Value.Immutable - public interface SortFetchZeroRuleConfig extends PruneEmptyRule.Config { - SortFetchZeroRuleConfig DEFAULT = ImmutableSortFetchZeroRuleConfig.of() - .withOperandSupplier(b -> b.operand(Sort.class).anyInputs()) - .withDescription("PruneSortLimit0"); - - @Override default PruneEmptyRule toRule() { - return new RemoveEmptySingleRule(this) { - @Override public boolean matches(final RelOptRuleCall call) { - Sort sort = call.rel(0); - return sort.fetch != null - && !(sort.fetch instanceof RexDynamicParam) - && RexLiteral.bigDecimalValue(sort.fetch).equals(BigDecimal.ZERO); - } - }; - } - } - /** Configuration for rule that prunes a join it its left input is * empty. */ @Value.Immutable @@ -684,24 +662,38 @@ public interface CorrelateRightEmptyRuleConfig extends PruneEmptyRule.Config { } } - /** Configuration for rule that transforms an empty relational expression into - * an empty values. + /** Configuration for rule that transforms a relational expression into an + * empty values if it is definitely empty. * *

It relies on {@link org.apache.calcite.rel.metadata.RelMdMaxRowCount} to * derive if the relation is empty or not. If the stats are not available then - * the rule is a noop. */ + * the rule is a noop. + * + *

{@link Values} is excluded because it is already a values and does not + * need to be replaced. {@link TableModify} is excluded because it may have + * side effects even when no rows are modified. */ @Value.Immutable public interface ZeroMaxRowsRuleConfig extends PruneEmptyRule.Config { + Predicate CAN_BE_REPLACED_BY_EMPTY_VALUES = rel -> + !(rel instanceof Values) && !(rel instanceof TableModify); + ZeroMaxRowsRuleConfig DEFAULT = ImmutableZeroMaxRowsRuleConfig.of() - .withOperandSupplier(b0 -> b0.operand(TableScan.class).noInputs()) - .withDescription("PruneZeroRowsTable"); + .withOperandSupplier(b0 -> b0.operand(RelNode.class) + .predicate(CAN_BE_REPLACED_BY_EMPTY_VALUES) + .anyInputs()) + .withDescription("PruneZeroRows"); @Override default PruneEmptyRule toRule() { - return new RemoveEmptySingleRule(this) { - @Override public boolean matches(RelOptRuleCall call) { + return new PruneEmptyRule(this) { + @Override public boolean matches(final RelOptRuleCall call) { RelNode node = call.rel(0); return RelMdUtil.isRelDefinitelyEmpty(call.getMetadataQuery(), node); } + + @Override public void onMatch(final RelOptRuleCall call) { + RelNode node = call.rel(0); + call.transformTo(call.builder().push(node).empty().build()); + } }; } } diff --git a/core/src/main/java/org/apache/calcite/rel/rules/SortRemoveRedundantRule.java b/core/src/main/java/org/apache/calcite/rel/rules/SortRemoveRedundantRule.java index 9bcf026fc65..b7450b2c42e 100644 --- a/core/src/main/java/org/apache/calcite/rel/rules/SortRemoveRedundantRule.java +++ b/core/src/main/java/org/apache/calcite/rel/rules/SortRemoveRedundantRule.java @@ -113,7 +113,7 @@ protected SortRemoveRedundantRule(final SortRemoveRedundantRule.Config config) { // If sort is 'order by x' or 'order by x limit n', target threshold is 1. // If sort is pure limit, the target threshold is the limit's fetch. // If the limit's fetch is 0, we could use - // CoreRules.SORT_FETCH_ZERO_INSTANCE to deal with it, so we don't need to + // PruneEmptyRules.EMPTY_TABLE_INSTANCE to deal with it, so we don't need to // deal with it in this rule. final Optional rowCountThreshold = getRowCountThreshold(sort); diff --git a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java index 772c409692b..1daf5168ef7 100644 --- a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java +++ b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java @@ -8068,7 +8068,7 @@ private void checkLiteral2(String expression, String expected) { + "from (values (1, 'a'), (2, 'bb')) as t(x, y)\n" + "limit 0"; final RuleSet rules = - RuleSets.ofList(PruneEmptyRules.SORT_FETCH_ZERO_INSTANCE); + RuleSets.ofList(PruneEmptyRules.EMPTY_TABLE_INSTANCE); final String expectedMysql = "SELECT *\n" + "FROM (SELECT NULL AS `X`, NULL AS `Y`) AS `t`\n" + "WHERE 1 = 0"; @@ -8136,9 +8136,11 @@ private void checkLiteral2(String expression, String expected) { + "limit 0"; final String sql = "SELECT SUBSTRING(y, 1, 1) FROM (" + sql0 + ") t"; final RuleSet rules = - RuleSets.ofList(PruneEmptyRules.SORT_FETCH_ZERO_INSTANCE); - final String expected = "SELECT SUBSTRING(`Y`, 1, 1)\n" - + "FROM (SELECT NULL AS `X`, NULL AS `Y`) AS `t`\n" + RuleSets.ofList(PruneEmptyRules.EMPTY_TABLE_INSTANCE); + // EMPTY_TABLE_INSTANCE is now a general rule that prunes any definitely + // empty expression to an empty Values, so the outer Project is also pruned. + final String expected = "SELECT *\n" + + "FROM (SELECT NULL AS `EXPR$0`) AS `t`\n" + "WHERE 1 = 0"; sql(sql).optimize(rules, null).withMysql().ok(expected); } 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 4ba2f4aa962..1c766bf2619 100644 --- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java +++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java @@ -5838,7 +5838,35 @@ private void checkEmptyJoin(RelOptFixture f) { @Test void testEmptySortLimitZero() { final String sql = "select * from emp order by deptno limit 0"; - sql(sql).withRule(PruneEmptyRules.SORT_FETCH_ZERO_INSTANCE).check(); + sql(sql).withRule(PruneEmptyRules.EMPTY_TABLE_INSTANCE).check(); + } + + /** Tests that a Sort whose OFFSET skips at least as many rows as its input + * can produce is pruned to empty by + * {@link PruneEmptyRules#EMPTY_TABLE_INSTANCE}. */ + @Test void testEmptySortOffsetGreaterThanMaxRows() { + // The input VALUES has at most 2 rows, so 'OFFSET 5' skips them all. + final String sql = "select * from (values (1, 2), (3, 4)) as t (a, b)\n" + + "order by a\n" + + "offset 5 rows"; + sql(sql).withRule(PruneEmptyRules.EMPTY_TABLE_INSTANCE).check(); + } + + /** Tests that {@link PruneEmptyRules#EMPTY_TABLE_INSTANCE} prunes a Filter + * whose condition is always false, even when its input is not an empty + * Values. */ + @Test void testEmptyFilterAlwaysFalse() { + final String sql = "select * from emp where false"; + sql(sql).withRule(PruneEmptyRules.EMPTY_TABLE_INSTANCE).check(); + } + + /** Tests that {@link PruneEmptyRules#EMPTY_TABLE_INSTANCE} does not prune a + * {@code TableModify}, because it may have side effects even when no rows are + * modified. The input Filter is still pruned to empty Values. */ + @Test void testEmptyTableModifyNotPruned() { + final String sql = "insert into sales.dept(deptno, name)\n" + + "select empno, ename from emp where false"; + sql(sql).withRule(PruneEmptyRules.EMPTY_TABLE_INSTANCE).check(); } @Test void testEmptyAggregate() { diff --git a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml index ab26ca8524b..9532c029782 100644 --- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml @@ -4192,6 +4192,23 @@ LogicalProject(EXPR$0=[+(+($0, $1), $0)]) + + + + + + + + + + + @@ -4451,6 +4468,25 @@ LogicalSort(sort0=[$7], dir0=[ASC], fetch=[0]) + + + + + + + + + + + @@ -4468,6 +4504,26 @@ LogicalProject(PRODUCTID=[$0], NAME=[$1], SUPPLIERID=[$2]) + + + + + + + + + + +