Skip to content
Merged
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
49 changes: 49 additions & 0 deletions core/src/main/java/org/apache/calcite/rex/RexSimplify.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@

private static final Strong STRONG = new Strong();

/** Maximum number of terms for which to apply the absorption law. */
private static final int MAX_TERMS_FOR_ABSORPTION = 20;

/**
* Creates a RexSimplify.
*
Expand Down Expand Up @@ -661,7 +664,7 @@
}

// e must be a comparison (=, >, >=, <, <=, !=)
private <C extends Comparable<C>> RexNode simplifyComparison(RexCall e,

Check warning on line 667 in core/src/main/java/org/apache/calcite/rex/RexSimplify.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

A "Brain Method" was detected. Refactor it to reduce at least one of the following metrics: LOC from 119 to 64, Complexity from 43 to 14, Nesting Level from 5 to 2, Number of Variables from 13 to 6.

See more on https://sonarcloud.io/project/issues?id=apache_calcite&issues=AZ-DrgziV8Z-4xnyBeON&open=AZ-DrgziV8Z-4xnyBeON&pullRequest=5110
RexUnknownAs unknownAs, Class<C> clazz) {
final List<RexNode> operands = new ArrayList<>(e.operands);
// UNKNOWN mode is warranted: false = null
Expand Down Expand Up @@ -1843,6 +1846,9 @@
SqlStdOperatorTable.IS_NULL, notSatisfiableNullable), UNKNOWN));
}
}
// Absorption law: a AND (a OR b) => a
absorb(terms, SqlKind.OR);

// Add the NOT disjunctions back in.
for (RexNode notDisjunction : notTerms) {
terms.add(simplify(not(notDisjunction), UNKNOWN));
Expand Down Expand Up @@ -2085,6 +2091,9 @@
if (!Collections.disjoint(nullOperands, strongOperands)) {
return rexBuilder.makeLiteral(false);
}
// Absorption law: a AND (a OR b) => a
absorb(terms, SqlKind.OR);

// Remove not necessary IS NOT NULL expressions.
// Example. IS NOT NULL(x) AND x < 5 : x < 5
for (RexNode operand : notNullOperands) {
Expand Down Expand Up @@ -2251,7 +2260,7 @@

/** Simplifies a list of terms and combines them into an OR.
* Modifies the list in place. */
private RexNode simplifyOrs(List<RexNode> terms, RexUnknownAs unknownAs) {

Check warning on line 2263 in core/src/main/java/org/apache/calcite/rex/RexSimplify.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

A "Brain Method" was detected. Refactor it to reduce at least one of the following metrics: LOC from 97 to 64, Complexity from 23 to 14, Nesting Level from 7 to 2, Number of Variables from 21 to 6.

See more on https://sonarcloud.io/project/issues?id=apache_calcite&issues=AZ-DrgziV8Z-4xnyBeOM&open=AZ-DrgziV8Z-4xnyBeOM&pullRequest=5110
final SargCollector sargCollector = new SargCollector(rexBuilder, false);
final List<RexNode> newTerms = new ArrayList<>();
terms.forEach(t -> sargCollector.accept(t, newTerms));
Expand Down Expand Up @@ -2358,9 +2367,49 @@
break;
}
}

// Absorption law: a OR (a AND b) => a
absorb(terms, SqlKind.AND);

return RexUtil.composeDisjunction(rexBuilder, terms);
}

/**
* Applies the absorption law to a list of terms, removing any composite term
* that is absorbed by a sibling term.
*
* <p>When {@code compositeKind} is {@link SqlKind#OR}, removes any
* {@code (a OR b)} term whose disjunctions contain a sibling {@code a}, so
* {@code a AND (a OR b) => a}. When it is {@link SqlKind#AND}, removes any
* {@code (a AND b)} term whose conjunctions contain a sibling {@code a}, so
* {@code a OR (a AND b) => a}.
*
* <p>The absorbing sibling {@code a} must be deterministic; otherwise its two
* occurrences might evaluate differently and the rewrite would not be
* equivalence-preserving.
*/
private static void absorb(List<RexNode> terms, SqlKind compositeKind) {
if (terms.size() > MAX_TERMS_FOR_ABSORPTION) {
return;
}
for (int i = 0; i < terms.size(); i++) {
final RexNode term = terms.get(i);
if (term.getKind() == compositeKind) {
final List<RexNode> components = compositeKind == SqlKind.OR
? RelOptUtil.disjunctions(term)

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.

This looks useful, but these two function calls are not cheap.
I hope that this does not matter in practice, but this could be expensive for some very complex boolean expressions.

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 are right that for each term we may call RelOptUtil.disjunctions / RelOptUtil.conjunctions, and then scan the sibling terms to check containment. In the worst case this is O(n²·m) where n is the number of terms and m is the size of the disjunction/conjunction.

In practice, however, the number of top-level conjuncts/disjuncts in a WHERE/FILTER predicate is usually small, so the cost should be bounded.

But I agree we should not rely on that assumption, I added a guard to skip absorption when the term list is large(The threshold is 20,referenced the threshold value for the IN-to-OR transition), avoid the performance impact associated with complex situations.

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 are more and more machine/AI generated queries, what used to be reasonable it's not necessarily the same today, so I agree on being conservative here with a bloat parameter

: RelOptUtil.conjunctions(term);
for (RexNode other : terms) {
if (other != term && components.contains(other)
&& RexUtil.isDeterministic(other)) {
terms.remove(i);
i--;

Check warning on line 2405 in core/src/main/java/org/apache/calcite/rex/RexSimplify.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor the code in order to not assign to this loop counter from within the loop body.

See more on https://sonarcloud.io/project/issues?id=apache_calcite&issues=AZ95pAifDeeK920-n2Fx&open=AZ95pAifDeeK920-n2Fx&pullRequest=5110
break;
}
}
}
}
}

private Pair<Comparable, RuntimeException> evaluate(RexNode e, Map<RexNode, Comparable> map) {
Comparable c = null;
RuntimeException ex = null;
Expand Down
48 changes: 43 additions & 5 deletions core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,45 @@ private RexProgramBuilder createProg(int variant) {
"false");
}

/** Test case for
* <a href="https://issues.apache.org/jira/browse/CALCITE-7657">[CALCITE-7657]
* Apply the absorption law to simplify boolean expressions</a>. */
@Test void testAbsorptionLaw() {
// AND absorption: a AND (a OR b) => a
checkSimplify(and(vBool(), or(vBool(), vBool(1))), "?0.bool0");
checkSimplify(and(or(vBool(), vBool(1)), vBool()), "?0.bool0");

// OR absorption: a OR (a AND b) => a
checkSimplify(or(vBool(), and(vBool(), vBool(1))), "?0.bool0");
checkSimplify(or(and(vBool(), vBool(1)), vBool()), "?0.bool0");

// with not-null booleans
checkSimplify(and(vBoolNotNull(), or(vBoolNotNull(), vBoolNotNull(1))), "?0.notNullBool0");
checkSimplify(or(vBoolNotNull(), and(vBoolNotNull(), vBoolNotNull(1))), "?0.notNullBool0");

// filter mode (unknownAsFalse)
checkSimplifyFilter(and(vBool(), or(vBool(), vBool(1))), "?0.bool0");
checkSimplifyFilter(or(vBool(), and(vBool(), vBool(1))), "?0.bool0");
}

@Test void testAbsorptionLawWithNonDeterministic() {
// a is a non-deterministic boolean ("NDC()")
final SqlOperator ndc = getNoDeterministicOperator();
final RexNode a = rexBuilder.makeCall(ndc);
final RexNode b = gt(vInt(1), literal(1));

// a AND (a OR b) must NOT be simplified to a
checkSimplifyUnchanged(and(a, or(a, b)));
// a OR (a AND b) must NOT be simplified to a
checkSimplifyUnchanged(or(a, and(a, b)));

// Sanity check: when a is deterministic, absorption does apply.
final SqlOperator dc = getDeterministicOperator();
final RexNode da = rexBuilder.makeCall(dc);
checkSimplify(and(da, or(da, b)), "DC()");
checkSimplify(or(da, and(da, b)), "DC()");
}

@Disabled("CALCITE-3457: AssertionError in RexSimplify.validateStrongPolicy")
@Test void reproducerFor3457() {
// Identified with RexProgramFuzzyTest#testFuzzy, seed=4887662474363391810L
Expand Down Expand Up @@ -3034,10 +3073,9 @@ trueLiteral, literal(1),
// ==>
// "A IS NOT NULL"
SqlOperator dc = getDeterministicOperator();
checkSimplify2(
checkSimplify(
and(or(isNotNull(rexBuilder.makeCall(dc)), gt(vInt(2), literal(2))),
isNotNull(rexBuilder.makeCall(dc))),
"AND(OR(IS NOT NULL(DC()), >(?0.int2, 2)), IS NOT NULL(DC()))",
"IS NOT NULL(DC())");
}

Expand Down Expand Up @@ -3903,15 +3941,15 @@ private static String getString(Map<RexNode, RexNode> map) {
// -> "x = x AND y < y" (treating unknown as unknown)
// -> false (treating unknown as false)
checkSimplify3(and(eq(vInt(1), vInt(1)), not(ge(vInt(2), vInt(2)))),
"AND(OR(null, IS NOT NULL(?0.int1)), null, IS NULL(?0.int2))",
"AND(null, IS NULL(?0.int2))",
"false",
"IS NULL(?0.int2)");

// "NOT(x = x AND NOT (y = y))"
// -> "OR(x <> x, y >= y)" (treating unknown as unknown)
// -> "y IS NOT NULL" (treating unknown as false)
checkSimplify3(not(and(eq(vInt(1), vInt(1)), not(ge(vInt(2), vInt(2))))),
"OR(AND(null, IS NULL(?0.int1)), null, IS NOT NULL(?0.int2))",
"OR(null, IS NOT NULL(?0.int2))",
"IS NOT NULL(?0.int2)",
"true");
}
Expand Down Expand Up @@ -3969,7 +4007,7 @@ private static String getString(Map<RexNode, RexNode> map) {
// -> "AND(x <> x, y >= y)" (treating unknown as unknown)
// -> "FALSE" (treating unknown as false)
checkSimplify3(not(or(eq(vInt(1), vInt(1)), not(ge(vInt(2), vInt(2))))),
"AND(null, IS NULL(?0.int1), OR(null, IS NOT NULL(?0.int2)))",
"AND(null, IS NULL(?0.int1))",
"false",
"IS NULL(?0.int1)");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,9 +322,8 @@ class JdbcAdapterTest {
+ " JdbcProject($f2=[OR(IS NOT NULL($7), IS NOT NULL($1))])\n"
+ " JdbcTableScan(table=[[SCOTT, EMP]])\n"
+ " JdbcToEnumerableConverter\n"
+ " JdbcAggregate(group=[{0, 1}], i=[LITERAL_AGG(true)], em=[MAX($2)])\n"
+ " JdbcProject(DEPTNO=[$7], ENAME=[CAST($1):VARCHAR(14)],"
+ " $f2=[AND(IS NOT NULL($7), IS NOT NULL($1))])\n"
+ " JdbcAggregate(group=[{0, 1}], i=[LITERAL_AGG(true)])\n"

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.

The intermediate variable $f2 is no longer needed in the condition; OR(IS NULL($7), IS NULL($1)) suffices. Consequently:
$f2=[AND(IS NOT NULL($7), IS NOT NULL($1))] is removed from JdbcProject;
em=[MAX($2)] is removed from JdbcAggregate;
At the same time, JdbcFilter(condition=[OR(IS NOT NULL($7), IS NULL($1))]) is added to the subquery side to filter out rows where both columns are NULL at an earlier stage.

+ " JdbcProject(DEPTNO=[$7], ENAME=[CAST($1):VARCHAR(14)])\n"
+ " JdbcFilter(condition=[OR(IS NOT NULL($7), IS NOT NULL($1))])\n"
+ " JdbcTableScan(table=[[SCOTT, EMP]])\n\n");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1653,7 +1653,7 @@ protected final MaterializedViewFixture sql(String materialize,
SqlStdOperatorTable.NOT,
i4))));
f.checkSatisfiable(e8,
"AND(=($0, 0), $2, $3, OR(NOT($2), NOT($3), NOT($4)), NOT($4))");
"AND(=($0, 0), $2, $3, NOT($4))");
}

@Test void testSplitFilter() {
Expand Down
70 changes: 70 additions & 0 deletions core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,76 @@ private void checkSimplify(UnaryOperator<RelBuilder.Config> transform,
assertThat(f.apply(createBuilder()), hasTree(expected));
}

/** Test case for
* <a href="https://issues.apache.org/jira/browse/CALCITE-7657">[CALCITE-7657]
* Apply the absorption law to simplify boolean expressions</a>. */
@Test void testFilterAndAbsorptionLaw() {
// Equivalent SQL:
// SELECT *
// FROM emp
// WHERE deptno = 10 AND (deptno = 10 OR sal > 100)
// Should be simplified to:
// SELECT *
// FROM emp
// WHERE deptno = 10
final Function<RelBuilder, RelNode> f = b ->
b.scan("EMP")
.filter(
b.and(
b.equals(b.field("DEPTNO"), b.literal(10)),
b.or(
b.equals(b.field("DEPTNO"), b.literal(10)),
b.greaterThan(b.field("SAL"), b.literal(100)))))
.build();

final String expected = "LogicalFilter(condition=[=($7, 10)])\n"
+ " LogicalTableScan(table=[[scott, EMP]])\n";
assertThat(f.apply(createBuilder()), hasTree(expected));
}

@Test void testFilterOrAbsorptionLaw() {
// Equivalent SQL:
// SELECT *
// FROM emp
// WHERE deptno = 10 OR (deptno = 10 AND sal > 100)
// Should be simplified to:
// SELECT *
// FROM emp
// WHERE deptno = 10
final Function<RelBuilder, RelNode> f = b ->
b.scan("EMP")
.filter(
b.or(
b.equals(b.field("DEPTNO"), b.literal(10)),
b.and(
b.equals(b.field("DEPTNO"), b.literal(10)),
b.greaterThan(b.field("SAL"), b.literal(100)))))
.build();

final String expected = "LogicalFilter(condition=[=($7, 10)])\n"
+ " LogicalTableScan(table=[[scott, EMP]])\n";
assertThat(f.apply(createBuilder()), hasTree(expected));
}

@Test void testFilterAbsorptionLawWithNonDeterministic() {
final Function<RelBuilder, RelNode> f = b -> {
final RexNode rand =
b.greaterThan(
b.call(SqlStdOperatorTable.RAND), b.literal(0.5));
return b.scan("EMP")
.filter(
b.and(rand,
b.or(rand,
b.greaterThan(b.field("SAL"), b.literal(100)))))
.build();
};

final String expected = "LogicalFilter(condition=[AND(>(RAND(), 0.5E0),"
+ " OR(>(RAND(), 0.5E0), >($5, 100)))])\n"
+ " LogicalTableScan(table=[[scott, EMP]])\n";
assertThat(f.apply(createBuilder()), hasTree(expected));
}

@Test void testBadFieldName() {
final RelBuilder builder = RelBuilder.create(config().build());
try {
Expand Down
12 changes: 6 additions & 6 deletions core/src/test/resources/sql/sub-query.iq
Original file line number Diff line number Diff line change
Expand Up @@ -4315,7 +4315,7 @@ select * from "scott".emp where (empno, deptno) not in ((1, 2), (3, null));

!ok
!if (use_old_decorr) {
EnumerableCalc(expr#0..14=[{inputs}], expr#15=[0], expr#16=[=($t8, $t15)], expr#17=[IS NULL($t7)], expr#18=[IS NOT NULL($t13)], expr#19=[AND($t14, $t18)], expr#20=[<($t9, $t8)], expr#21=[OR($t17, $t19, $t18, $t20)], expr#22=[IS NOT TRUE($t21)], expr#23=[OR($t16, $t22)], proj#0..7=[{exprs}], $condition=[$t23])
EnumerableCalc(expr#0..13=[{inputs}], expr#14=[0], expr#15=[=($t8, $t14)], expr#16=[IS NULL($t13)], expr#17=[>=($t9, $t8)], expr#18=[IS NOT NULL($t7)], expr#19=[AND($t16, $t17, $t18)], expr#20=[OR($t15, $t19)], proj#0..7=[{exprs}], $condition=[$t20])

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.

Since IS NOT NULL($t13) is already part of the OR clause and is subsumed by AND($t14, IS NOT NULL($t13)), the term AND($t14, IS NOT NULL($t13)) is absorbed by IS NOT NULL($t13). After converting NOT(<) to >= and reordering the variables, we obtain:

OR(=($t8, 0), AND(IS NULL($t13), >=($t9, $t8), IS NOT NULL($t7)))

EnumerableMergeJoin(condition=[AND(=($10, $11), OR(IS NULL($12), =(CAST($7):INTEGER, $12)))], joinType=[left])
EnumerableSort(sort0=[$10], dir0=[ASC])
EnumerableCalc(expr#0..9=[{inputs}], expr#10=[CAST($t0):INTEGER NOT NULL], proj#0..10=[{exprs}])
Expand All @@ -4324,7 +4324,7 @@ EnumerableCalc(expr#0..14=[{inputs}], expr#15=[0], expr#16=[=($t8, $t15)], expr#
EnumerableAggregate(group=[{}], c=[COUNT()], ck=[COUNT() FILTER $0])
EnumerableValues(tuples=[[{ true }, { true }]])
EnumerableSort(sort0=[$0], dir0=[ASC])
EnumerableCalc(expr#0..1=[{inputs}], expr#2=[true], expr#3=[IS NOT NULL($t1)], proj#0..3=[{exprs}])
EnumerableCalc(expr#0..1=[{inputs}], expr#2=[true], proj#0..2=[{exprs}])

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.

After the main Calc node is simplified, the subquery no longer needs to additionally output the IS NOT NULL($t1) column, resulting in this structure.

EnumerableValues(tuples=[[{ 3, null }, { 1, 2 }]])
!plan
!}
Expand All @@ -4339,14 +4339,14 @@ select * from "scott".emp where (mgr, deptno) not in ((1, 2), (3, null), (cast(n

!ok
!if (use_old_decorr) {
EnumerableCalc(expr#0..13=[{inputs}], expr#14=[0], expr#15=[=($t8, $t14)], expr#16=[IS NULL($t3)], expr#17=[IS NULL($t7)], expr#18=[IS NOT NULL($t12)], expr#19=[AND($t13, $t18)], expr#20=[<($t9, $t8)], expr#21=[OR($t16, $t17, $t19, $t18, $t20)], expr#22=[IS NOT TRUE($t21)], expr#23=[OR($t15, $t22)], proj#0..7=[{exprs}], $condition=[$t23])
EnumerableCalc(expr#0..12=[{inputs}], expr#13=[0], expr#14=[=($t8, $t13)], expr#15=[IS NULL($t12)], expr#16=[>=($t9, $t8)], expr#17=[IS NOT NULL($t3)], expr#18=[IS NOT NULL($t7)], expr#19=[AND($t15, $t16, $t17, $t18)], expr#20=[OR($t14, $t19)], proj#0..7=[{exprs}], $condition=[$t20])

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.

Redundant terms within OR(...) are absorbed by IS NOT NULL/IS NULL-related terms, shortening the expression.

EnumerableNestedLoopJoin(condition=[AND(OR(IS NULL($10), =(CAST($3):INTEGER, $10)), OR(IS NULL($11), =(CAST($7):INTEGER, $11)))], joinType=[left])
EnumerableNestedLoopJoin(condition=[true], joinType=[inner])
EnumerableTableScan(table=[[scott, EMP]])
EnumerableAggregate(group=[{}], c=[COUNT()], ck=[COUNT() FILTER $0])
EnumerableCalc(expr#0..1=[{inputs}], expr#2=[IS NOT NULL($t0)], expr#3=[IS NOT NULL($t1)], expr#4=[OR($t2, $t3)], $f2=[$t4])
EnumerableValues(tuples=[[{ 3, null }, { null, null }, { 1, 2 }]])
EnumerableCalc(expr#0..1=[{inputs}], expr#2=[true], expr#3=[IS NOT NULL($t0)], expr#4=[IS NOT NULL($t1)], expr#5=[AND($t3, $t4)], expr#6=[OR($t3, $t4)], proj#0..2=[{exprs}], $f20=[$t5], $condition=[$t6])
EnumerableCalc(expr#0..1=[{inputs}], expr#2=[true], expr#3=[IS NOT NULL($t0)], expr#4=[IS NOT NULL($t1)], expr#5=[OR($t3, $t4)], proj#0..2=[{exprs}], $condition=[$t5])

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.

The original plan involved calculating two columns simultaneously:

expr#3=[IS NOT NULL($t0)]
expr#4=[IS NOT NULL($t1)]
expr#5=[AND($t3, $t4)]       -- Neither column is empty
expr#6=[OR($t3, $t4)]        -- At least one non-empty column

After simplification, the upper-level condition only requires "at least one non-null column" to handle the NULL semantics of NOT IN; AND($t3, $t4) has been absorbed:

expr#3=[IS NOT NULL($t0)]
expr#4=[IS NOT NULL($t1)]
expr#5=[OR($t3, $t4)]

EnumerableValues(tuples=[[{ 3, null }, { null, null }, { 1, 2 }]])
!plan
!}
Expand Down Expand Up @@ -4381,7 +4381,7 @@ select * from "scott".emp where (empno, deptno) not in ((7369, 20), (7499, 30));

!ok
!if (use_old_decorr) {
EnumerableCalc(expr#0..15=[{inputs}], expr#16=[0], expr#17=[=($t8, $t16)], expr#18=[IS NULL($t7)], expr#19=[IS NOT NULL($t14)], expr#20=[AND($t15, $t19)], expr#21=[<($t9, $t8)], expr#22=[OR($t18, $t20, $t19, $t21)], expr#23=[IS NOT TRUE($t22)], expr#24=[OR($t17, $t23)], proj#0..7=[{exprs}], $condition=[$t24])
EnumerableCalc(expr#0..14=[{inputs}], expr#15=[0], expr#16=[=($t8, $t15)], expr#17=[IS NULL($t14)], expr#18=[>=($t9, $t8)], expr#19=[IS NOT NULL($t7)], expr#20=[AND($t17, $t18, $t19)], expr#21=[OR($t16, $t20)], proj#0..7=[{exprs}], $condition=[$t21])

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.

Similarly, the expression is compressed because the OR(...) clause resulting from the expansion of NOT IN contains redundant terms that can be absorbed.

EnumerableMergeJoin(condition=[AND(=($10, $12), =($11, $13))], joinType=[left])
EnumerableSort(sort0=[$10], sort1=[$11], dir0=[ASC], dir1=[ASC])
EnumerableCalc(expr#0..9=[{inputs}], expr#10=[CAST($t0):INTEGER NOT NULL], expr#11=[CAST($t7):INTEGER], proj#0..11=[{exprs}])
Expand All @@ -4391,7 +4391,7 @@ EnumerableCalc(expr#0..15=[{inputs}], expr#16=[0], expr#17=[=($t8, $t16)], expr#
EnumerableCalc(expr#0..1=[{inputs}], expr#2=[true], $f2=[$t2])
EnumerableValues(tuples=[[{ 7369, 20 }, { 7499, 30 }]])
EnumerableSort(sort0=[$0], sort1=[$1], dir0=[ASC], dir1=[ASC])
EnumerableCalc(expr#0..1=[{inputs}], expr#2=[true], proj#0..2=[{exprs}], $f20=[$t2])
EnumerableCalc(expr#0..1=[{inputs}], expr#2=[true], proj#0..2=[{exprs}])

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.

After simplification in Calc, the helper column $f20 is no longer needed.

EnumerableValues(tuples=[[{ 7369, 20 }, { 7499, 30 }]])
!plan
!}
Expand Down
Loading