Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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 @@ -107,7 +107,7 @@ private RelOptRules() {
PruneEmptyRules.WINDOW_INSTANCE,
PruneEmptyRules.JOIN_LEFT_INSTANCE,
PruneEmptyRules.JOIN_RIGHT_INSTANCE,
PruneEmptyRules.SORT_FETCH_ZERO_INSTANCE,
PruneEmptyRules.SORT_EMPTY_INSTANCE,
PruneEmptyRules.EMPTY_TABLE_INSTANCE,
SingleValuesOptimizationRules.JOIN_LEFT_INSTANCE,
SingleValuesOptimizationRules.JOIN_RIGHT_INSTANCE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,13 @@
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;
import org.apache.calcite.tools.RelBuilderFactory;

import org.immutables.value.Value;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -221,16 +219,34 @@

/**
* 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.
*
* <p>Examples:
*
* <ul>
* <li>Sort[fetch=0] becomes Empty
* <li>Sort(Empty) becomes Empty</li>
* <li>Sort[fetch=0] becomes Empty</li>
* <li>Sort[offset=5](input with at most 2 rows) becomes Empty</li>
* </ul>
*
* <p>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 // to be removed before 2.0
public static final RelOptRule SORT_FETCH_ZERO_INSTANCE =

Check warning on line 248 in core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=apache_calcite&issues=AZ-x7cboPCd1aEIgB3Pt&open=AZ-x7cboPCd1aEIgB3Pt&pullRequest=5132
SortFetchZeroRuleConfig.DEFAULT.toRule();
SortEmptyRuleConfig.DEFAULT.toRule();

/**
* Rule that converts an {@link org.apache.calcite.rel.core.Aggregate}
Expand Down Expand Up @@ -525,20 +541,20 @@
}
}

/** 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");
.withDescription("PruneSortIfEmpty");

@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);
final Sort sort = call.rel(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this is an improvement, but I still don't understand why this cannot be applied to any Rel node.

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.

You're right that isRelDefinitelyEmpty itself is generic.
The reason this rule is specific to Sort is that the replacement logic is not generic: RemoveEmptySingleRule can safely turn any SingleRel into an empty Values, but Join, Union, Intersect, Minus, and Aggregate each need their own logic when they are empty. This rule reuses RemoveEmptySingleRule because Sort is a SingleRel. A fully generic prune anything that is definitely empty rule would need a way to know how to rewrite each operator type correctly. The existing EMPTY_TABLE_INSTANCE follows the same pattern: it uses isRelDefinitelyEmpty but only for TableScan.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What matters is that the operator has a single OUTPUT, and they all do.
Maybe you can find a way to do it, even if it means writing a bit more code to handle operators with more than 1 input.

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.

OK, I generalized ZeroMaxRowsRuleConfig (EMPTY_TABLE_INSTANCE) to match any RelNode that is definitely empty, not just TableScan. It excludes Values and TableModify (the latter may have side effects). This removes the need for a separate Sort-specific rule, so SORT_EMPTY_INSTANCE was deleted and SORT_FETCH_ZERO_INSTANCE is now a deprecated alias. The rule still relies on RelMdUtil.isRelDefinitelyEmpty, so the logic remains simple and covers LIMIT 0, large OFFSET, and any other case where Sort's max row count is zero.

return RelMdUtil.isRelDefinitelyEmpty(call.getMetadataQuery(), sort);
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<BigDecimal> rowCountThreshold = getRowCountThreshold(sort);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down
13 changes: 12 additions & 1 deletion core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5838,7 +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_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_EMPTY_INSTANCE).check();
}

@Test void testEmptyAggregate() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4451,6 +4451,25 @@ LogicalSort(sort0=[$7], dir0=[ASC], fetch=[0])
<Resource name="planAfter">
<![CDATA[
LogicalValues(tuples=[[]])
]]>
</Resource>
</TestCase>
<TestCase name="testEmptySortOffsetGreaterThanMaxRows">
<Resource name="sql">
<![CDATA[select * from (values (1, 2), (3, 4)) as t (a, b)
order by a
offset 5 rows]]>
</Resource>
<Resource name="planBefore">
<![CDATA[
LogicalSort(sort0=[$0], dir0=[ASC], offset=[5])
LogicalProject(A=[$0], B=[$1])
LogicalValues(tuples=[[{ 1, 2 }, { 3, 4 }]])
]]>
</Resource>
<Resource name="planAfter">
<![CDATA[
LogicalValues(tuples=[[]])
]]>
</Resource>
</TestCase>
Expand Down
Loading