From 94698b9d5f8df3561f53e5751a6aeaa734b5b6d1 Mon Sep 17 00:00:00 2001
From: xuzifu666 <1206332514@qq.com>
Date: Wed, 29 Jul 2026 19:45:58 +0800
Subject: [PATCH 1/6] [CALCITE-7680] PruneEmptyRules should prune Sort when
OFFSET is greater than or equal to max input rows
---
.../org/apache/calcite/plan/RelOptRules.java | 1 +
.../calcite/rel/rules/PruneEmptyRules.java | 42 +++++++++++++++++++
.../apache/calcite/test/RelOptRulesTest.java | 11 +++++
.../apache/calcite/test/RelOptRulesTest.xml | 19 +++++++++
4 files changed, 73 insertions(+)
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..e4c8f3b28ca 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptRules.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptRules.java
@@ -108,6 +108,7 @@ private RelOptRules() {
PruneEmptyRules.JOIN_LEFT_INSTANCE,
PruneEmptyRules.JOIN_RIGHT_INSTANCE,
PruneEmptyRules.SORT_FETCH_ZERO_INSTANCE,
+ PruneEmptyRules.SORT_OFFSET_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..ce028b0b41d 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
@@ -232,6 +232,24 @@ private static boolean isEmpty(RelNode node) {
public static final RelOptRule SORT_FETCH_ZERO_INSTANCE =
SortFetchZeroRuleConfig.DEFAULT.toRule();
+ /**
+ * Rule that converts a {@link org.apache.calcite.rel.core.Sort}
+ * to empty if its {@code OFFSET} is greater than or equal to the maximum
+ * number of rows its input can produce, so that all rows are skipped.
+ *
+ *
Examples:
+ *
+ *
+ * - Sort[offset=5](input with at most 2 rows) becomes Empty
+ *
+ *
+ * It relies on {@link org.apache.calcite.rel.metadata.RelMdMaxRowCount}
+ * to derive the input row count. If the stats are not available then the rule
+ * is a noop.
+ */
+ public static final RelOptRule SORT_OFFSET_INSTANCE =
+ SortOffsetGreaterThanMaxRowsRuleConfig.DEFAULT.toRule();
+
/**
* Rule that converts an {@link org.apache.calcite.rel.core.Aggregate}
* to empty if its child is empty.
@@ -544,6 +562,30 @@ public interface SortFetchZeroRuleConfig extends PruneEmptyRule.Config {
}
}
+ /** Configuration for a rule that prunes a Sort if its {@code OFFSET} skips at
+ * least as many rows as its input can ever produce. */
+ @Value.Immutable
+ public interface SortOffsetGreaterThanMaxRowsRuleConfig extends PruneEmptyRule.Config {
+ SortOffsetGreaterThanMaxRowsRuleConfig DEFAULT =
+ ImmutableSortOffsetGreaterThanMaxRowsRuleConfig.of()
+ .withOperandSupplier(b -> b.operand(Sort.class).anyInputs())
+ .withDescription("PruneSortOffsetGreaterThanMaxRows");
+
+ @Override default PruneEmptyRule toRule() {
+ return new RemoveEmptySingleRule(this) {
+ @Override public boolean matches(final RelOptRuleCall call) {
+ final Sort sort = call.rel(0);
+ // Only consider a static (non-dynamic) OFFSET. If the offset skips at
+ // least as many rows as the input can ever produce, the Sort returns
+ // no rows. RelMdMaxRowCount#getMaxRowCount(Sort) already subtracts the
+ // offset from the input row count, so the Sort is definitely empty.
+ return sort.offset instanceof RexLiteral
+ && RelMdUtil.isRelDefinitelyEmpty(call.getMetadataQuery(), sort);
+ }
+ };
+ }
+ }
+
/** Configuration for rule that prunes a join it its left input is
* empty. */
@Value.Immutable
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..f5500802a04 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -5841,6 +5841,17 @@ private void checkEmptyJoin(RelOptFixture f) {
sql(sql).withRule(PruneEmptyRules.SORT_FETCH_ZERO_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#SORT_OFFSET_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.SORT_OFFSET_INSTANCE).check();
+ }
+
@Test void testEmptyAggregate() {
final String sql = "select sum(empno) from emp where false group by deptno";
sql(sql)
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..45dfc00ba42 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -4451,6 +4451,25 @@ LogicalSort(sort0=[$7], dir0=[ASC], fetch=[0])
+
+
+
+
+
+
+
+
+
+
+
From 659ee7a1d7f84c10989917814913a85189e14724 Mon Sep 17 00:00:00 2001
From: xuzifu666 <1206332514@qq.com>
Date: Thu, 30 Jul 2026 10:14:46 +0800
Subject: [PATCH 2/6] Addressed
---
.../java/org/apache/calcite/rel/rules/PruneEmptyRules.java | 4 ----
1 file changed, 4 deletions(-)
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 ce028b0b41d..4fd9a5dc604 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
@@ -242,10 +242,6 @@ private static boolean isEmpty(RelNode node) {
*
* - Sort[offset=5](input with at most 2 rows) becomes Empty
*
- *
- * It relies on {@link org.apache.calcite.rel.metadata.RelMdMaxRowCount}
- * to derive the input row count. If the stats are not available then the rule
- * is a noop.
*/
public static final RelOptRule SORT_OFFSET_INSTANCE =
SortOffsetGreaterThanMaxRowsRuleConfig.DEFAULT.toRule();
From a4276c88d272979ad3b9577a3687fd017c38a4fd Mon Sep 17 00:00:00 2001
From: xuzifu666 <1206332514@qq.com>
Date: Thu, 30 Jul 2026 14:52:41 +0800
Subject: [PATCH 3/6] Addressed
---
.../org/apache/calcite/plan/RelOptRules.java | 3 +-
.../calcite/rel/rules/PruneEmptyRules.java | 69 +++++++------------
.../rel/rules/SortRemoveRedundantRule.java | 2 +-
.../rel/rel2sql/RelToSqlConverterTest.java | 4 +-
.../apache/calcite/test/RelOptRulesTest.java | 6 +-
5 files changed, 31 insertions(+), 53 deletions(-)
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 e4c8f3b28ca..abd195cdbc2 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptRules.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptRules.java
@@ -107,8 +107,7 @@ private RelOptRules() {
PruneEmptyRules.WINDOW_INSTANCE,
PruneEmptyRules.JOIN_LEFT_INSTANCE,
PruneEmptyRules.JOIN_RIGHT_INSTANCE,
- PruneEmptyRules.SORT_FETCH_ZERO_INSTANCE,
- PruneEmptyRules.SORT_OFFSET_INSTANCE,
+ PruneEmptyRules.SORT_EMPTY_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 4fd9a5dc604..a84a638e461 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
@@ -42,7 +42,6 @@
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;
@@ -221,30 +220,34 @@ 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}.
+ * to empty if it is definitely empty, for example because its child is empty,
+ * it has {@code LIMIT 0}, or its {@code OFFSET} is greater than or equal to
+ * the maximum number of rows its input can produce.
*
*
Examples:
*
*
- * - Sort[fetch=0] becomes Empty
+ *
- Sort(Empty) becomes Empty
+ * - Sort[fetch=0] becomes Empty
+ * - Sort[offset=5](input with at most 2 rows) becomes Empty
*
+ *
+ * It relies on {@link org.apache.calcite.rel.metadata.RelMdMaxRowCount}
+ * to derive whether the Sort is definitely empty.
*/
- public static final RelOptRule SORT_FETCH_ZERO_INSTANCE =
- SortFetchZeroRuleConfig.DEFAULT.toRule();
+ public static final RelOptRule SORT_EMPTY_INSTANCE =
+ SortEmptyRuleConfig.DEFAULT.toRule();
/**
* Rule that converts a {@link org.apache.calcite.rel.core.Sort}
- * to empty if its {@code OFFSET} is greater than or equal to the maximum
- * number of rows its input can produce, so that all rows are skipped.
- *
- *
Examples:
+ * to empty if it has {@code LIMIT 0}.
*
- *
- * - Sort[offset=5](input with at most 2 rows) becomes Empty
- *
+ * @deprecated Use {@link #SORT_EMPTY_INSTANCE}, which covers this case and
+ * also Sort nodes that are empty for other reasons (e.g. a large OFFSET).
*/
- public static final RelOptRule SORT_OFFSET_INSTANCE =
- SortOffsetGreaterThanMaxRowsRuleConfig.DEFAULT.toRule();
+ @Deprecated // to be removed before 2.0
+ public static final RelOptRule SORT_FETCH_ZERO_INSTANCE =
+ SortEmptyRuleConfig.DEFAULT.toRule();
/**
* Rule that converts an {@link org.apache.calcite.rel.core.Aggregate}
@@ -539,44 +542,20 @@ public interface IntersectEmptyPruneRuleConfig extends PruneEmptyRule.Config {
}
}
- /** Configuration for a rule that prunes a Sort if it has limit 0. */
+ /** Configuration for a rule that prunes a Sort if it is definitely empty,
+ * for example because its input is empty, it has {@code LIMIT 0}, or its
+ * {@code OFFSET} skips more rows than the input can produce. */
@Value.Immutable
- public interface SortFetchZeroRuleConfig extends PruneEmptyRule.Config {
- SortFetchZeroRuleConfig DEFAULT = ImmutableSortFetchZeroRuleConfig.of()
+ public interface SortEmptyRuleConfig extends PruneEmptyRule.Config {
+ SortEmptyRuleConfig DEFAULT = ImmutableSortEmptyRuleConfig.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 a rule that prunes a Sort if its {@code OFFSET} skips at
- * least as many rows as its input can ever produce. */
- @Value.Immutable
- public interface SortOffsetGreaterThanMaxRowsRuleConfig extends PruneEmptyRule.Config {
- SortOffsetGreaterThanMaxRowsRuleConfig DEFAULT =
- ImmutableSortOffsetGreaterThanMaxRowsRuleConfig.of()
- .withOperandSupplier(b -> b.operand(Sort.class).anyInputs())
- .withDescription("PruneSortOffsetGreaterThanMaxRows");
+ .withDescription("PruneSortIfEmpty");
@Override default PruneEmptyRule toRule() {
return new RemoveEmptySingleRule(this) {
@Override public boolean matches(final RelOptRuleCall call) {
final Sort sort = call.rel(0);
- // Only consider a static (non-dynamic) OFFSET. If the offset skips at
- // least as many rows as the input can ever produce, the Sort returns
- // no rows. RelMdMaxRowCount#getMaxRowCount(Sort) already subtracts the
- // offset from the input row count, so the Sort is definitely empty.
- return sort.offset instanceof RexLiteral
- && RelMdUtil.isRelDefinitelyEmpty(call.getMetadataQuery(), sort);
+ return RelMdUtil.isRelDefinitelyEmpty(call.getMetadataQuery(), sort);
}
};
}
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..b88773774f2 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.SORT_EMPTY_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..0d5aee3c1d7 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.SORT_EMPTY_INSTANCE);
final String expectedMysql = "SELECT *\n"
+ "FROM (SELECT NULL AS `X`, NULL AS `Y`) AS `t`\n"
+ "WHERE 1 = 0";
@@ -8136,7 +8136,7 @@ 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);
+ RuleSets.ofList(PruneEmptyRules.SORT_EMPTY_INSTANCE);
final String expected = "SELECT SUBSTRING(`Y`, 1, 1)\n"
+ "FROM (SELECT NULL AS `X`, NULL AS `Y`) AS `t`\n"
+ "WHERE 1 = 0";
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 f5500802a04..16bf4baecde 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -5838,18 +5838,18 @@ 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.SORT_EMPTY_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#SORT_OFFSET_INSTANCE}. */
+ * {@link PruneEmptyRules#SORT_EMPTY_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.SORT_OFFSET_INSTANCE).check();
+ sql(sql).withRule(PruneEmptyRules.SORT_EMPTY_INSTANCE).check();
}
@Test void testEmptyAggregate() {
From 4dd4d6c6023b88d921580a656c24ea890eadec9e Mon Sep 17 00:00:00 2001
From: xuzifu666 <1206332514@qq.com>
Date: Thu, 30 Jul 2026 15:17:30 +0800
Subject: [PATCH 4/6] Addressed
---
.../main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java | 1 -
1 file changed, 1 deletion(-)
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 a84a638e461..6972e0f4bd1 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
@@ -49,7 +49,6 @@
import org.immutables.value.Value;
-import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
From 16d480b42de2b2ca6f420ef951422f17954b935b Mon Sep 17 00:00:00 2001
From: xuzifu666 <1206332514@qq.com>
Date: Sat, 1 Aug 2026 15:25:59 +0800
Subject: [PATCH 5/6] Addressed
---
.../org/apache/calcite/plan/RelOptRules.java | 1 -
.../calcite/rel/rules/PruneEmptyRules.java | 76 +++++++------------
.../rel/rules/SortRemoveRedundantRule.java | 2 +-
.../rel/rel2sql/RelToSqlConverterTest.java | 10 ++-
.../apache/calcite/test/RelOptRulesTest.java | 6 +-
5 files changed, 36 insertions(+), 59 deletions(-)
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 abd195cdbc2..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_EMPTY_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 6972e0f4bd1..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,7 +35,7 @@
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;
@@ -217,36 +217,17 @@ private static boolean isEmpty(RelNode node) {
public static final RelOptRule SORT_INSTANCE =
RemoveEmptySingleRule.RemoveEmptySingleRuleConfig.SORT.toRule();
- /**
- * Rule that converts a {@link org.apache.calcite.rel.core.Sort}
- * to empty if it is definitely empty, for example because its child is empty,
- * it has {@code LIMIT 0}, or its {@code OFFSET} is greater than or equal to
- * the maximum number of rows its input can produce.
- *
- * Examples:
- *
- *
- * - Sort(Empty) becomes Empty
- * - Sort[fetch=0] becomes Empty
- * - Sort[offset=5](input with at most 2 rows) becomes Empty
- *
- *
- * It relies on {@link org.apache.calcite.rel.metadata.RelMdMaxRowCount}
- * to derive whether the Sort is definitely empty.
- */
- public static final RelOptRule SORT_EMPTY_INSTANCE =
- SortEmptyRuleConfig.DEFAULT.toRule();
-
/**
* Rule that converts a {@link org.apache.calcite.rel.core.Sort}
* to empty if it has {@code LIMIT 0}.
*
- * @deprecated Use {@link #SORT_EMPTY_INSTANCE}, which covers this case and
- * also Sort nodes that are empty for other reasons (e.g. a large OFFSET).
+ * @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 =
- SortEmptyRuleConfig.DEFAULT.toRule();
+ ZeroMaxRowsRuleConfig.DEFAULT.toRule();
/**
* Rule that converts an {@link org.apache.calcite.rel.core.Aggregate}
@@ -541,25 +522,6 @@ public interface IntersectEmptyPruneRuleConfig extends PruneEmptyRule.Config {
}
}
- /** Configuration for a rule that prunes a Sort if it is definitely empty,
- * for example because its input is empty, it has {@code LIMIT 0}, or its
- * {@code OFFSET} skips more rows than the input can produce. */
- @Value.Immutable
- public interface SortEmptyRuleConfig extends PruneEmptyRule.Config {
- SortEmptyRuleConfig DEFAULT = ImmutableSortEmptyRuleConfig.of()
- .withOperandSupplier(b -> b.operand(Sort.class).anyInputs())
- .withDescription("PruneSortIfEmpty");
-
- @Override default PruneEmptyRule toRule() {
- return new RemoveEmptySingleRule(this) {
- @Override public boolean matches(final RelOptRuleCall call) {
- final Sort sort = call.rel(0);
- return RelMdUtil.isRelDefinitelyEmpty(call.getMetadataQuery(), sort);
- }
- };
- }
- }
-
/** Configuration for rule that prunes a join it its left input is
* empty. */
@Value.Immutable
@@ -700,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 b88773774f2..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
- // PruneEmptyRules.SORT_EMPTY_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 0d5aee3c1d7..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_EMPTY_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_EMPTY_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 16bf4baecde..5e284ab7280 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -5838,18 +5838,18 @@ private void checkEmptyJoin(RelOptFixture f) {
@Test void testEmptySortLimitZero() {
final String sql = "select * from emp order by deptno limit 0";
- sql(sql).withRule(PruneEmptyRules.SORT_EMPTY_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#SORT_EMPTY_INSTANCE}. */
+ * {@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.SORT_EMPTY_INSTANCE).check();
+ sql(sql).withRule(PruneEmptyRules.EMPTY_TABLE_INSTANCE).check();
}
@Test void testEmptyAggregate() {
From 6d0064e8e7f33cf02547bd069b9170dc43a62423 Mon Sep 17 00:00:00 2001
From: xuzifu666 <1206332514@qq.com>
Date: Sat, 1 Aug 2026 15:57:08 +0800
Subject: [PATCH 6/6] Addressed
---
.../apache/calcite/test/RelOptRulesTest.java | 17 +++++++++
.../apache/calcite/test/RelOptRulesTest.xml | 37 +++++++++++++++++++
2 files changed, 54 insertions(+)
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 5e284ab7280..1c766bf2619 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -5852,6 +5852,23 @@ private void checkEmptyJoin(RelOptFixture f) {
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() {
final String sql = "select sum(empno) from emp where false group by deptno";
sql(sql)
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 45dfc00ba42..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)])
+
+
+
+
+
+
+
+
+
+
+
@@ -4487,6 +4504,26 @@ LogicalProject(PRODUCTID=[$0], NAME=[$1], SUPPLIERID=[$2])
+
+
+
+
+
+
+
+
+
+
+