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:
- *
- *
- * - Sort[fetch=0] becomes Empty
- *
+ * @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])
+
+
+
+
+
+
+
+
+
+
+