This is an automated email from the ASF dual-hosted git repository.
korlov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new 536a5817590 IGNITE-25801 Sql. Use
CoreRules.JOIN_SUB_QUERY_TO_CORRELATE instead of IgniteSubQueryRemoveRule
(#7337)
536a5817590 is described below
commit 536a5817590466c0499c65f8da7ac2951341f5c4
Author: korlov42 <[email protected]>
AuthorDate: Mon Jan 5 11:24:54 2026 +0200
IGNITE-25801 Sql. Use CoreRules.JOIN_SUB_QUERY_TO_CORRELATE instead of
IgniteSubQueryRemoveRule (#7337)
---
.../internal/sql/engine/prepare/PlannerPhase.java | 4 +-
.../rule/logical/IgniteSubQueryRemoveRule.java | 998 ---------------------
2 files changed, 1 insertion(+), 1001 deletions(-)
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerPhase.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerPhase.java
index f7fbe232936..d3c489e56a7 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerPhase.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerPhase.java
@@ -76,7 +76,6 @@ import
org.apache.ignite.internal.sql.engine.rule.logical.FilterScanMergeRule;
import
org.apache.ignite.internal.sql.engine.rule.logical.IgniteJoinConditionPushRule;
import
org.apache.ignite.internal.sql.engine.rule.logical.IgniteMultiJoinOptimizeBushyRule;
import
org.apache.ignite.internal.sql.engine.rule.logical.IgniteProjectCorrelateTransposeRule;
-import
org.apache.ignite.internal.sql.engine.rule.logical.IgniteSubQueryRemoveRule;
import org.apache.ignite.internal.sql.engine.rule.logical.LogicalOrToUnionRule;
import org.apache.ignite.internal.sql.engine.rule.logical.ProjectScanMergeRule;
import org.apache.ignite.internal.sql.engine.util.Commons;
@@ -89,8 +88,7 @@ public enum PlannerPhase {
"Heuristic phase to convert subqueries into correlates",
CoreRules.FILTER_SUB_QUERY_TO_CORRELATE,
CoreRules.PROJECT_SUB_QUERY_TO_CORRELATE,
- // revert into CoreRules.JOIN_SUB_QUERY_TO_CORRELATE after
https://issues.apache.org/jira/browse/IGNITE-25801
- IgniteSubQueryRemoveRule.INSTANCE
+ CoreRules.JOIN_SUB_QUERY_TO_CORRELATE
) {
/** {@inheritDoc} */
@Override
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/IgniteSubQueryRemoveRule.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/IgniteSubQueryRemoveRule.java
deleted file mode 100644
index b1611bf679b..00000000000
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/IgniteSubQueryRemoveRule.java
+++ /dev/null
@@ -1,998 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.internal.sql.engine.rule.logical;
-
-//CHECKSTYLE:OFF
-
-import static java.util.Objects.requireNonNull;
-import static org.apache.calcite.util.Util.last;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-import org.apache.calcite.plan.RelOptRule;
-import org.apache.calcite.plan.RelOptRuleCall;
-import org.apache.calcite.plan.RelOptUtil;
-import org.apache.calcite.plan.RelRule;
-import org.apache.calcite.rel.RelHomogeneousShuttle;
-import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.core.Collect;
-import org.apache.calcite.rel.core.Correlate;
-import org.apache.calcite.rel.core.CorrelationId;
-import org.apache.calcite.rel.core.Join;
-import org.apache.calcite.rel.core.JoinRelType;
-import org.apache.calcite.rel.metadata.RelMdUtil;
-import org.apache.calcite.rel.metadata.RelMetadataQuery;
-import org.apache.calcite.rel.rules.CoreRules;
-import org.apache.calcite.rel.rules.TransformationRule;
-import org.apache.calcite.rex.LogicVisitor;
-import org.apache.calcite.rex.RexBuilder;
-import org.apache.calcite.rex.RexCorrelVariable;
-import org.apache.calcite.rex.RexFieldAccess;
-import org.apache.calcite.rex.RexInputRef;
-import org.apache.calcite.rex.RexLiteral;
-import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.rex.RexShuttle;
-import org.apache.calcite.rex.RexSubQuery;
-import org.apache.calcite.rex.RexUtil;
-import org.apache.calcite.sql.SqlAggFunction;
-import org.apache.calcite.sql.SqlKind;
-import org.apache.calcite.sql.fun.SqlQuantifyOperator;
-import org.apache.calcite.sql.fun.SqlStdOperatorTable;
-import org.apache.calcite.sql2rel.RelDecorrelator;
-import org.apache.calcite.tools.RelBuilder;
-import org.apache.calcite.util.ImmutableBitSet;
-import org.apache.calcite.util.Pair;
-import org.immutables.value.Value;
-
-/**
- * Transform that converts IN, EXISTS and scalar sub-queries into joins.
- *
- * <p>Sub-queries are represented by {@link RexSubQuery} expressions.
- *
- * <p>A sub-query may or may not be correlated. If a sub-query is correlated,
- * the wrapped {@link RelNode} will contain a {@link RexCorrelVariable} before
- * the rewrite, and the product of the rewrite will be a {@link Correlate}.
- * The Correlate can be removed using {@link RelDecorrelator}.
- *
- * @see CoreRules#FILTER_SUB_QUERY_TO_CORRELATE
- * @see CoreRules#PROJECT_SUB_QUERY_TO_CORRELATE
- * @see CoreRules#JOIN_SUB_QUERY_TO_CORRELATE
- *
- * Need to be removed after https://issues.apache.org/jira/browse/IGNITE-25801
- */
[email protected]
-public class IgniteSubQueryRemoveRule
- extends RelRule<IgniteSubQueryRemoveRule.Config>
- implements TransformationRule {
-
- public static final RelOptRule INSTANCE = Config.JOIN.toRule();
-
- /** Creates a SubQueryRemoveRule. */
- protected IgniteSubQueryRemoveRule(Config config) {
- super(config);
- requireNonNull(config.matchHandler());
- }
-
- @Override public void onMatch(RelOptRuleCall call) {
- config.matchHandler().accept(this, call);
- }
-
- protected RexNode apply(RexSubQuery e, Set<CorrelationId> variablesSet,
- RelOptUtil.Logic logic,
- RelBuilder builder, int inputCount, int offset, int subQueryIndex)
{
- switch (e.getKind()) {
- case SCALAR_QUERY:
- return rewriteScalarQuery(e, variablesSet, builder,
inputCount, offset);
- case ARRAY_QUERY_CONSTRUCTOR:
- case MAP_QUERY_CONSTRUCTOR:
- case MULTISET_QUERY_CONSTRUCTOR:
- return rewriteCollection(e, variablesSet, builder,
- inputCount, offset);
- case SOME:
- return rewriteSome(e, variablesSet, builder, subQueryIndex);
- case IN:
- return rewriteIn(e, variablesSet, logic, builder, offset,
subQueryIndex);
- case EXISTS:
- return rewriteExists(e, variablesSet, logic, builder);
- case UNIQUE:
- return rewriteUnique(e, builder);
- default:
- throw new AssertionError(e.getKind());
- }
- }
-
- /**
- * Rewrites a scalar sub-query into an
- * {@link org.apache.calcite.rel.core.Aggregate}.
- *
- * @param e Scalar sub-query to rewrite
- * @param variablesSet A set of variables used by a relational
- * expression of the specified RexSubQuery
- * @param builder Builder
- * @param offset Offset to shift {@link RexInputRef}
- *
- * @return Expression that may be used to replace the RexSubQuery
- */
- private static RexNode rewriteScalarQuery(RexSubQuery e,
Set<CorrelationId> variablesSet,
- RelBuilder builder, int inputCount, int offset) {
- builder.push(e.rel);
- final RelMetadataQuery mq = e.rel.getCluster().getMetadataQuery();
- final Boolean unique =
- mq.areColumnsUnique(builder.peek(), ImmutableBitSet.of());
- if (unique == null || !unique) {
- builder.aggregate(builder.groupKey(),
- builder.aggregateCall(SqlStdOperatorTable.SINGLE_VALUE,
- builder.field(0)));
- }
- builder.join(JoinRelType.LEFT, builder.literal(true), variablesSet);
- return field(builder, inputCount, offset);
- }
-
- /**
- * Rewrites a sub-query into a
- * {@link org.apache.calcite.rel.core.Collect}.
- *
- * @param e Sub-query to rewrite
- * @param variablesSet A set of variables used by a relational
- * expression of the specified RexSubQuery
- * @param builder Builder
- * @param offset Offset to shift {@link RexInputRef}
- * @return Expression that may be used to replace the RexSubQuery
- */
- private static RexNode rewriteCollection(RexSubQuery e,
- Set<CorrelationId> variablesSet, RelBuilder builder,
- int inputCount, int offset) {
- builder.push(e.rel);
- builder.push(
- Collect.create(builder.build(), e.getKind(), "x"));
- builder.join(JoinRelType.INNER, builder.literal(true), variablesSet);
- return field(builder, inputCount, offset);
- }
-
- /**
- * Rewrites a SOME sub-query into a {@link Join}.
- *
- * @param e SOME sub-query to rewrite
- * @param builder Builder
- * @param subQueryIndex sub-query index in multiple sub-queries
- *
- * @return Expression that may be used to replace the RexSubQuery
- */
- private static RexNode rewriteSome(RexSubQuery e, Set<CorrelationId>
variablesSet,
- RelBuilder builder, int subQueryIndex) {
- // If the sub-query is guaranteed empty, just return
- // FALSE.
- final RelMetadataQuery mq = e.rel.getCluster().getMetadataQuery();
- if (RelMdUtil.isRelDefinitelyEmpty(mq, e.rel)) {
- return builder.getRexBuilder().makeLiteral(Boolean.FALSE,
e.getType(), true);
- }
- // Most general case, where the left and right keys might have nulls,
and
- // caller requires 3-valued logic return.
- //
- // select e.deptno, e.deptno < some (select deptno from emp) as v
- // from emp as e
- //
- // becomes
- //
- // select e.deptno,
- // case
- // when q.c = 0 then false // sub-query is empty
- // when (e.deptno < q.m) is true then true
- // when q.c > q.d then unknown // sub-query has at least one null
- // else e.deptno < q.m
- // end as v
- // from emp as e
- // cross join (
- // select max(deptno) as m, count(*) as c, count(deptno) as d
- // from emp) as q
- //
- final SqlQuantifyOperator op = (SqlQuantifyOperator) e.op;
- switch (op.comparisonKind) {
- case GREATER_THAN_OR_EQUAL:
- case LESS_THAN_OR_EQUAL:
- case LESS_THAN:
- case GREATER_THAN:
- case NOT_EQUALS:
- break;
-
- default:
- // "SOME =" should have been rewritten into IN.
- throw new AssertionError("unexpected " + op);
- }
-
- final RexNode caseRexNode;
- final RexNode literalFalse = builder.literal(false);
- final RexNode literalTrue = builder.literal(true);
- final RexLiteral literalUnknown =
-
builder.getRexBuilder().makeNullLiteral(literalFalse.getType());
-
- final SqlAggFunction minMax = op.comparisonKind == SqlKind.GREATER_THAN
- || op.comparisonKind == SqlKind.GREATER_THAN_OR_EQUAL
- ? SqlStdOperatorTable.MIN
- : SqlStdOperatorTable.MAX;
-
- String qAlias = "q";
- if (subQueryIndex != 0) {
- qAlias = "q" + subQueryIndex;
- }
-
- if (variablesSet.isEmpty()) {
- switch (op.comparisonKind) {
- case GREATER_THAN_OR_EQUAL:
- case LESS_THAN_OR_EQUAL:
- case LESS_THAN:
- case GREATER_THAN:
- // for non-correlated case queries such as
- // select e.deptno, e.deptno < some (select deptno from
emp) as v
- // from emp as e
- //
- // becomes
- //
- // select e.deptno,
- // case
- // when q.c = 0 then false // sub-query is empty
- // when (e.deptno < q.m) is true then true
- // when q.c > q.d then unknown // sub-query has at least
one null
- // else e.deptno < q.m
- // end as v
- // from emp as e
- // cross join (
- // select max(deptno) as m, count(*) as c, count(deptno)
as d
- // from emp) as q
- builder.push(e.rel)
- .aggregate(builder.groupKey(),
- builder.aggregateCall(minMax,
builder.field(0)).as("m"),
- builder.count(false, "c"),
- builder.count(false, "d",
builder.field(0)))
- .as(qAlias)
- .join(JoinRelType.INNER);
- caseRexNode =
- builder.call(SqlStdOperatorTable.CASE,
- builder.equals(builder.field(qAlias, "c"),
builder.literal(0)),
- literalFalse,
- builder.call(SqlStdOperatorTable.IS_TRUE,
-
builder.call(RexUtil.op(op.comparisonKind),
- e.operands.get(0),
builder.field(qAlias, "m"))),
- literalTrue,
- builder.greaterThan(builder.field(qAlias,
"c"),
- builder.field(qAlias, "d")),
- literalUnknown,
- builder.call(RexUtil.op(op.comparisonKind),
- e.operands.get(0),
builder.field(qAlias, "m")));
- break;
-
- case NOT_EQUALS:
- // for non-correlated case queries such as
- // select e.deptno, e.deptno <> some (select deptno from
emp) as v
- // from emp as e
- //
- // becomes
- //
- // select e.deptno,
- // case
- // when q.c = 0 then false // sub-query is empty
- // when e.deptno is null then unknown
- // when q.c <> q.d && q.d <= 1 then e.deptno != m ||
unknown
- // when q.d = 1
- // then e.deptno != m // sub-query has the distinct
result
- // else true
- // end as v
- // from emp as e
- // cross join (
- // select count(*) as c, count(deptno) as d, max(deptno)
as m
- // from (select distinct deptno from emp)) as q
- builder.push(e.rel);
- builder.distinct()
- .aggregate(builder.groupKey(),
- builder.count(false, "c"),
- builder.count(false, "d",
builder.field(0)),
- builder.max(builder.field(0)).as("m"))
- .as(qAlias)
- .join(JoinRelType.INNER);
- caseRexNode =
- builder.call(SqlStdOperatorTable.CASE,
- builder.equals(builder.field("c"),
builder.literal(0)),
- literalFalse,
- builder.isNull(e.getOperands().get(0)),
- literalUnknown,
- builder.and(
-
builder.notEquals(builder.field("d"), builder.field("c")),
-
builder.lessThanOrEqual(builder.field("d"),
- builder.literal(1))),
- builder.or(
-
builder.notEquals(e.operands.get(0), builder.field(qAlias, "m")),
- literalUnknown),
- builder.equals(builder.field("d"),
builder.literal(1)),
- builder.notEquals(e.operands.get(0),
builder.field(qAlias, "m")),
- literalTrue);
- break;
-
- default:
- throw new AssertionError("not possible - per above check");
- }
- } else {
- final String indicator = "trueLiteral";
- final List<RexNode> parentQueryFields = new ArrayList<>();
- switch (op.comparisonKind) {
- case GREATER_THAN_OR_EQUAL:
- case LESS_THAN_OR_EQUAL:
- case LESS_THAN:
- case GREATER_THAN:
- // for correlated case queries such as
- //
- // select e.deptno, e.deptno < some (
- // select deptno from emp where emp.name = e.name) as v
- // from emp as e
- //
- // becomes
- //
- // select e.deptno,
- // case
- // when indicator is null then false // sub-query is
empty for corresponding corr value
- // when q.c = 0 then false // sub-query is empty
- // when (e.deptno < q.m) is true then true
- // when q.c > q.d then unknown // sub-query has at least
one null
- // else e.deptno < q.m
- // end as v
- // from emp as e
- // left outer join (
- // select name, max(deptno) as m, count(*) as c,
count(deptno) as d,
- // "alwaysTrue" as indicator
- // from emp group by name) as q on e.name = q.name
- builder.push(e.rel)
- .aggregate(builder.groupKey(),
- builder.aggregateCall(minMax,
builder.field(0)).as("m"),
- builder.count(false, "c"),
- builder.count(false, "d",
builder.field(0)));
-
- parentQueryFields.addAll(builder.fields());
- parentQueryFields.add(builder.alias(literalTrue,
indicator));
- builder.project(parentQueryFields).as(qAlias);
- builder.join(JoinRelType.LEFT, literalTrue, variablesSet);
- caseRexNode =
- builder.call(SqlStdOperatorTable.CASE,
- builder.isNull(builder.field(qAlias,
indicator)),
- literalFalse,
- builder.equals(builder.field(qAlias, "c"),
builder.literal(0)),
- literalFalse,
- builder.call(SqlStdOperatorTable.IS_TRUE,
-
builder.call(RexUtil.op(op.comparisonKind),
- e.operands.get(0),
builder.field(qAlias, "m"))),
- literalTrue,
- builder.greaterThan(builder.field(qAlias,
"c"),
- builder.field(qAlias, "d")),
- literalUnknown,
- builder.call(RexUtil.op(op.comparisonKind),
- e.operands.get(0),
builder.field(qAlias, "m")));
- break;
-
- case NOT_EQUALS:
- // for correlated case queries such as
- //
- // select e.deptno, e.deptno <> some (
- // select deptno from emp where emp.name = e.name) as v
- // from emp as e
- //
- // becomes
- //
- // select e.deptno,
- // case
- // when indicator is null
- // then false // sub-query is empty for corresponding
corr value
- // when q.c = 0 then false // sub-query is empty
- // when e.deptno is null then unknown
- // when q.c <> q.d && q.dd <= 1
- // then e.deptno != m || unknown
- // when q.dd = 1
- // then e.deptno != m // sub-query has the distinct
result
- // else true
- // end as v
- // from emp as e
- // left outer join (
- // select name, count(*) as c, count(deptno) as d,
count(distinct deptno) as dd,
- // max(deptno) as m, "alwaysTrue" as indicator
- // from emp group by name) as q on e.name = q.name
-
- // Additional details on the `q.c <> q.d && q.dd <= 1`
clause:
- // the q.c <> q.d comparison identifies if there are any
null values,
- // since count(*) counts null values and count(deptno)
does not.
- // if there's no null value, c should be equal to d.
- // the q.dd <= 1 part means: true if there is at most one
non-null value
- // so this clause means:
- // "if there are any null values and there is at most one
non-null value".
- builder.push(e.rel)
- .aggregate(builder.groupKey(),
- builder.count(false, "c"),
- builder.count(false, "d",
builder.field(0)),
- builder.count(true, "dd",
builder.field(0)),
- builder.max(builder.field(0)).as("m"));
-
- parentQueryFields.addAll(builder.fields());
- parentQueryFields.add(builder.alias(literalTrue,
indicator));
- builder.project(parentQueryFields).as(qAlias); // TODO use
projectPlus
- builder.join(JoinRelType.LEFT, literalTrue, variablesSet);
- caseRexNode =
- builder.call(SqlStdOperatorTable.CASE,
- builder.isNull(builder.field(qAlias,
indicator)),
- literalFalse,
- builder.equals(builder.field("c"),
builder.literal(0)),
- literalFalse,
- builder.isNull(e.getOperands().get(0)),
- literalUnknown,
- builder.and(
-
builder.notEquals(builder.field("d"), builder.field("c")),
-
builder.lessThanOrEqual(builder.field("dd"),
- builder.literal(1))),
- builder.or(
-
builder.notEquals(e.operands.get(0), builder.field(qAlias, "m")),
- literalUnknown),
- builder.equals(builder.field("dd"),
builder.literal(1)),
- builder.notEquals(e.operands.get(0),
builder.field(qAlias, "m")),
- literalTrue);
- break;
-
- default:
- throw new AssertionError("not possible - per above check");
- }
- }
-
- // CASE statement above is created with nullable boolean type, but it
might
- // not be correct. If the original sub-query node's type is not
nullable it
- // is guaranteed for case statement to not produce NULLs. Therefore to
avoid
- // planner complaining we need to add cast. Note that nullable type is
- // created due to the MIN aggregate call, since there is no GROUP BY.
- if (!e.getType().isNullable()) {
- return builder.cast(caseRexNode, e.getType().getSqlTypeName());
- }
- return caseRexNode;
- }
-
- /**
- * Rewrites an EXISTS RexSubQuery into a {@link Join}.
- *
- * @param e EXISTS sub-query to rewrite
- * @param variablesSet A set of variables used by a relational
- * expression of the specified RexSubQuery
- * @param logic Logic for evaluating
- * @param builder Builder
- *
- * @return Expression that may be used to replace the RexSubQuery
- */
- private static RexNode rewriteExists(RexSubQuery e, Set<CorrelationId>
variablesSet,
- RelOptUtil.Logic logic, RelBuilder builder) {
- // If the sub-query is guaranteed never empty, just return
- // TRUE.
- final RelMetadataQuery mq = e.rel.getCluster().getMetadataQuery();
- if (RelMdUtil.isRelDefinitelyNotEmpty(mq, e.rel)) {
- return builder.literal(true);
- }
- if (RelMdUtil.isRelDefinitelyEmpty(mq, e.rel)) {
- return builder.literal(false);
- }
- builder.push(e.rel);
- builder.project(builder.alias(builder.literal(true), "i"));
- switch (logic) {
- case TRUE:
- // Handles queries with single EXISTS in filter condition:
- // select e.deptno from emp as e
- // where exists (select deptno from emp)
- builder.aggregate(builder.groupKey(0));
- builder.as("dt");
- builder.join(JoinRelType.INNER, builder.literal(true),
variablesSet);
- return builder.literal(true);
- default:
- builder.distinct();
- }
-
- builder.as("dt");
-
- builder.join(JoinRelType.LEFT, builder.literal(true), variablesSet);
-
- return builder.isNotNull(last(builder.fields()));
- }
-
- /**
- * Rewrites a UNIQUE RexSubQuery into an EXISTS RexSubQuery.
- *
- * <p>For example, rewrites the UNIQUE sub-query:
- *
- * <pre>{@code
- * UNIQUE (SELECT PUBLISHED_IN
- * FROM BOOK
- * WHERE AUTHOR_ID = 3)
- * }</pre>
- *
- * <p>to the following EXISTS sub-query:
- *
- * <pre>{@code
- * NOT EXISTS (
- * SELECT * FROM (
- * SELECT PUBLISHED_IN
- * FROM BOOK
- * WHERE AUTHOR_ID = 3
- * ) T
- * WHERE (T.PUBLISHED_IN) IS NOT NULL
- * GROUP BY T.PUBLISHED_IN
- * HAVING COUNT(*) > 1
- * )
- * }</pre>
- *
- * @param e UNIQUE sub-query to rewrite
- * @param builder Builder
- *
- * @return Expression that may be used to replace the RexSubQuery
- */
- private static RexNode rewriteUnique(RexSubQuery e, RelBuilder builder) {
- // if sub-query always return unique value.
- final RelMetadataQuery mq = e.rel.getCluster().getMetadataQuery();
- Boolean isUnique = mq.areRowsUnique(e.rel, true);
- if (isUnique != null && isUnique) {
- return builder.getRexBuilder().makeLiteral(true);
- }
- builder.push(e.rel);
- List<RexNode> notNullCondition =
- builder.fields().stream()
- .map(builder::isNotNull)
- .collect(Collectors.toList());
- builder
- .filter(notNullCondition)
- .aggregate(builder.groupKey(builder.fields()),
builder.countStar("c"))
- .filter(
- builder.greaterThan(last(builder.fields()),
builder.literal(1)));
- RelNode relNode = builder.build();
- return builder.call(SqlStdOperatorTable.NOT,
RexSubQuery.exists(relNode));
- }
-
- /**
- * Rewrites an IN RexSubQuery into a {@link Join}.
- *
- * @param e IN sub-query to rewrite
- * @param variablesSet A set of variables used by a relational
- * expression of the specified RexSubQuery
- * @param logic Logic for evaluating
- * @param builder Builder
- * @param offset Offset to shift {@link RexInputRef}
- * @param subQueryIndex sub-query index in multiple sub-queries
- *
- * @return Expression that may be used to replace the RexSubQuery
- */
- private static RexNode rewriteIn(RexSubQuery e, Set<CorrelationId>
variablesSet,
- RelOptUtil.Logic logic, RelBuilder builder, int offset, int
subQueryIndex) {
- // If the sub-query is guaranteed empty, just return
- // FALSE.
- final RelMetadataQuery mq = e.rel.getCluster().getMetadataQuery();
- if (RelMdUtil.isRelDefinitelyEmpty(mq, e.rel)) {
- return builder.getRexBuilder().makeLiteral(Boolean.FALSE,
e.getType(), true);
- }
- // Most general case, where the left and right keys might have nulls,
and
- // caller requires 3-valued logic return.
- //
- // select e.deptno, e.deptno in (select deptno from emp)
- // from emp as e
- //
- // becomes
- //
- // select e.deptno,
- // case
- // when ct.c = 0 then false
- // when e.deptno is null then null
- // when dt.i is not null then true
- // when ct.ck < ct.c then null
- // else false
- // end
- // from emp as e
- // left join (
- // (select count(*) as c, count(deptno) as ck from emp) as ct
- // cross join (select distinct deptno, true as i from emp)) as dt
- // on e.deptno = dt.deptno
- //
- // If keys are not null we can remove "ct" and simplify to
- //
- // select e.deptno,
- // case
- // when dt.i is not null then true
- // else false
- // end
- // from emp as e
- // left join (select distinct deptno, true as i from emp) as dt
- // on e.deptno = dt.deptno
- //
- // We could further simplify to
- //
- // select e.deptno,
- // dt.i is not null
- // from emp as e
- // left join (select distinct deptno, true as i from emp) as dt
- // on e.deptno = dt.deptno
- //
- // but have not yet.
- //
- // If the logic is TRUE we can just kill the record if the condition
- // evaluates to FALSE or UNKNOWN. Thus the query simplifies to an inner
- // join:
- //
- // select e.deptno,
- // true
- // from emp as e
- // inner join (select distinct deptno from emp) as dt
- // on e.deptno = dt.deptno
- //
-
- builder.push(e.rel);
- final List<RexNode> fields = new ArrayList<>(builder.fields());
-
- // for the case when IN has only literal operands, it may be handled
- // in the simpler way:
- //
- // select e.deptno, 123456 in (select deptno from emp)
- // from emp as e
- //
- // becomes
- //
- // select e.deptno,
- // case
- // when dt.c IS NULL THEN FALSE
- // when e.deptno IS NULL THEN NULL
- // when dt.cs IS FALSE THEN NULL
- // when dt.cs IS NOT NULL THEN TRUE
- // else false
- // end
- // from emp AS e
- // cross join (
- // select distinct deptno is not null as cs, count(*) as c
- // from emp
- // where deptno = 123456 or deptno is null or e.deptno is null
- // order by cs desc limit 1) as dt
- //
-
- String ctAlias = "ct";
- if (subQueryIndex != 0) {
- ctAlias = "ct" + subQueryIndex;
- }
-
- boolean allLiterals = RexUtil.allLiterals(e.getOperands());
- final List<RexNode> expressionOperands = new
ArrayList<>(e.getOperands());
-
- final List<RexNode> keyIsNulls = e.getOperands().stream()
- .filter(operand -> operand.getType().isNullable())
- .map(builder::isNull)
- .collect(Collectors.toList());
-
- final RexLiteral trueLiteral = builder.literal(true);
- final RexLiteral falseLiteral = builder.literal(false);
- final RexLiteral unknownLiteral =
- builder.getRexBuilder().makeNullLiteral(trueLiteral.getType());
- if (allLiterals) {
- final List<RexNode> conditions =
- Pair.zip(expressionOperands, fields).stream()
- .map(pair -> builder.equals(pair.left, pair.right))
- .collect(Collectors.toList());
- switch (logic) {
- case TRUE:
- case TRUE_FALSE:
- builder.filter(conditions);
- builder.project(builder.alias(trueLiteral, "cs"));
- builder.distinct();
- break;
- default:
- List<RexNode> isNullOperands = fields.stream()
- .map(builder::isNull)
- .collect(Collectors.toList());
- // uses keyIsNulls conditions in the filter to avoid empty
results
- isNullOperands.addAll(keyIsNulls);
- builder.filter(
- builder.or(
- builder.and(conditions),
- builder.or(isNullOperands)));
- RexNode project =
- builder.and(
- fields.stream()
- .map(builder::isNotNull)
- .collect(Collectors.toList()));
- builder.project(builder.alias(project, "cs"));
-
- if (variablesSet.isEmpty()) {
-
builder.aggregate(builder.groupKey(builder.field("cs")),
- builder.count(false, "c"));
- } else {
- builder.distinct();
- }
- // sorts input with desc order since we are interested
- // only in the case when one of the values is true.
- // When true value is absent then we are interested
- // only in false value.
- builder.sortLimit(0, 1,
-
ImmutableList.of(builder.desc(builder.field("cs"))));
- }
- // clears expressionOperands and fields lists since
- // all expressions were used in the filter
- expressionOperands.clear();
- fields.clear();
- } else {
- switch (logic) {
- case TRUE:
- builder.aggregate(builder.groupKey(fields));
- break;
- case TRUE_FALSE_UNKNOWN:
- case UNKNOWN_AS_TRUE:
- // Builds the cross join
- builder.aggregate(builder.groupKey(),
- builder.count(false, "c"),
- builder.count(builder.fields()).as("ck"));
- builder.as(ctAlias);
- if (!variablesSet.isEmpty()) {
- builder.join(JoinRelType.LEFT, trueLiteral,
variablesSet);
- } else {
- builder.join(JoinRelType.INNER, trueLiteral,
variablesSet);
- }
- offset += 2;
- builder.push(e.rel);
- // fall through
- default:
- builder.aggregate(builder.groupKey(fields),
- builder.literalAgg(true).as("i"));
- }
- }
-
- String dtAlias = "dt";
- if (subQueryIndex != 0) {
- dtAlias = "dt" + subQueryIndex;
- }
- builder.as(dtAlias);
- int refOffset = offset;
- final List<RexNode> conditions =
- Pair.zip(expressionOperands, builder.fields()).stream()
- .map(pair -> builder.equals(pair.left,
RexUtil.shift(pair.right, refOffset)))
- .collect(Collectors.toList());
- switch (logic) {
- case TRUE:
- builder.join(JoinRelType.INNER, builder.and(conditions),
variablesSet);
- return trueLiteral;
- default:
- break;
- }
- // Now the left join
- builder.join(JoinRelType.LEFT, builder.and(conditions), variablesSet);
-
- final ImmutableList.Builder<RexNode> operands =
ImmutableList.builder();
- RexLiteral b = trueLiteral;
- switch (logic) {
- case TRUE_FALSE_UNKNOWN:
- b = unknownLiteral;
- // fall through
- case UNKNOWN_AS_TRUE:
- if (allLiterals) {
- // Considers case when right side of IN is empty
- // for the case of non-correlated sub-queries
- if (variablesSet.isEmpty()) {
- operands.add(
- builder.isNull(builder.field(dtAlias, "c")),
- falseLiteral);
- }
- operands.add(
- builder.equals(builder.field(dtAlias, "cs"),
falseLiteral),
- b);
- } else {
- operands.add(
- builder.equals(builder.field(ctAlias, "c"),
builder.literal(0)),
- falseLiteral);
- }
- break;
- default:
- break;
- }
-
- if (!keyIsNulls.isEmpty()) {
- operands.add(builder.or(keyIsNulls), unknownLiteral);
- }
-
- if (allLiterals) {
- operands.add(builder.isNotNull(builder.field(dtAlias, "cs")),
- trueLiteral);
- } else {
- operands.add(builder.isNotNull(last(builder.fields())),
- trueLiteral);
- }
-
- if (!allLiterals) {
- switch (logic) {
- case TRUE_FALSE_UNKNOWN:
- case UNKNOWN_AS_TRUE:
- operands.add(
- builder.lessThan(builder.field(ctAlias, "ck"),
- builder.field(ctAlias, "c")),
- b);
- break;
- default:
- break;
- }
- }
- operands.add(falseLiteral);
- return builder.call(SqlStdOperatorTable.CASE, operands.build());
- }
-
- /** Returns a reference to a particular field, by offset, across several
- * inputs on a {@link RelBuilder}'s stack. */
- private static RexInputRef field(RelBuilder builder, int inputCount, int
offset) {
- for (int inputOrdinal = 0;;) {
- final RelNode r = builder.peek(inputCount, inputOrdinal);
- if (offset < r.getRowType().getFieldCount()) {
- return builder.field(inputCount, inputOrdinal, offset);
- }
- ++inputOrdinal;
- offset -= r.getRowType().getFieldCount();
- }
- }
-
- /** Returns a list of expressions that project the first {@code fieldCount}
- * fields of the top input on a {@link RelBuilder}'s stack. */
- private static List<RexNode> fields(RelBuilder builder, int fieldCount) {
- final List<RexNode> projects = new ArrayList<>();
- for (int i = 0; i < fieldCount; i++) {
- projects.add(builder.field(i));
- }
- return projects;
- }
-
- private static void matchJoin(IgniteSubQueryRemoveRule rule,
RelOptRuleCall call) {
- final Join join = call.rel(0);
- final RelBuilder builder = call.builder();
- final RexSubQuery e =
-
requireNonNull(RexUtil.SubQueryFinder.find(join.getCondition()));
- final RelOptUtil.Logic logic =
- LogicVisitor.find(RelOptUtil.Logic.TRUE,
- ImmutableList.of(join.getCondition()), e);
-
- ImmutableBitSet inputSet =
RelOptUtil.InputFinder.bits(e.getOperands(), null);
- int nFieldsLeft = join.getLeft().getRowType().getFieldCount();
- int nFieldsRight = join.getRight().getRowType().getFieldCount();
-
- final Set<CorrelationId> variablesSet =
RelOptUtil.getVariablesUsed(e.rel);
- if (!variablesSet.isEmpty()) {
- final CorrelationId id = Iterables.getOnlyElement(variablesSet);
- final ImmutableBitSet requiredColumns =
RelOptUtil.correlationColumns(id, e.rel);
- inputSet = ImmutableBitSet.union(List.of(requiredColumns,
inputSet));
- }
-
- boolean inputIntersectsRightSide =
- inputSet.intersects(ImmutableBitSet.range(nFieldsLeft,
nFieldsLeft + nFieldsRight));
- boolean inputIntersectsLeftSide =
inputSet.intersects(ImmutableBitSet.range(0, nFieldsLeft));
-
- if (inputIntersectsLeftSide && inputIntersectsRightSide) {
- // The current existential rewrite needs to make join with one
side of the origin join and
- // generate a new condition to replace the on clause. But for
RexNode whose operands are
- // on either side of the join, we can't push them into join. So
this rewriting is not
- // supported.
- return;
- }
-
- if (inputIntersectsLeftSide) {
- builder.push(join.getLeft());
-
- final RexNode target =
- rule.apply(e, variablesSet, logic, builder, 1,
nFieldsLeft, 0);
- final RexShuttle shuttle = new ReplaceSubQueryShuttle(e, target);
-
- final RexNode newCond =
- shuttle.apply(
- RexUtil.shift(join.getCondition(), nFieldsLeft,
- builder.fields().size() - nFieldsLeft));
- builder.push(join.getRight());
- builder.join(join.getJoinType(), newCond);
-
- final int nFields = builder.fields().size();
- ImmutableList<RexNode> fields =
- builder.fields(ImmutableBitSet.range(0, nFieldsLeft)
- .union(ImmutableBitSet.range(nFields -
nFieldsRight, nFields)));
- builder.project(fields);
- } else {
- builder.push(join.getLeft());
- builder.push(join.getRight());
-
- RexSubQuery subQuery = e;
- if (!variablesSet.isEmpty()) {
- // Original correlates reference joint row type, but we are
about to create
- // new join of original right side and correlated sub-query.
Therefore we have
- // to adjust correlated variables int following way:
- // 1) new correlation variable must reference row type of
right side only
- // 2) field index must be shifted on the size of the left
side
-
- CorrelationId id = Iterables.getOnlyElement(variablesSet);
-
- subQuery = e.clone(e.rel.accept(new RelHomogeneousShuttle() {
- private final int offset =
join.getLeft().getRowType().getFieldCount();
- private final RexBuilder rexBuilder =
join.getRight().getCluster().getRexBuilder();
- private final RexShuttle rexShuttle = new RexShuttle() {
- @Override
- public RexNode visitFieldAccess(RexFieldAccess
fieldAccess) {
- if (!(fieldAccess.getReferenceExpr() instanceof
RexCorrelVariable)
- || !((RexCorrelVariable)
fieldAccess.getReferenceExpr()).id.equals(id)) {
- return super.visitFieldAccess(fieldAccess);
- }
-
- RexNode updatedCorrelation =
rexBuilder.makeCorrel(join.getRight().getRowType(), id);
-
- int oldIdx = fieldAccess.getField().getIndex();
- return
rexBuilder.makeFieldAccess(updatedCorrelation, oldIdx - offset);
- }
- };
-
- @Override
- public RelNode visit(RelNode other) {
- RelNode next = super.visit(other);
- return next.accept(rexShuttle);
- }
- }));
- }
-
- final int nFields = join.getRowType().getFieldCount();
- final RexNode target =
- rule.apply(subQuery, variablesSet, logic, builder, 2,
nFields, 0);
- final RexShuttle shuttle = new ReplaceSubQueryShuttle(e, target);
-
- builder.join(join.getJoinType(),
shuttle.apply(join.getCondition()));
- builder.project(fields(builder, nFields));
- }
-
- call.transformTo(builder.build());
- }
-
- /** Shuttle that replaces occurrences of a given
- * {@link org.apache.calcite.rex.RexSubQuery} with a replacement
- * expression. */
- private static class ReplaceSubQueryShuttle extends RexShuttle {
- private final RexSubQuery subQuery;
- private final RexNode replacement;
-
- ReplaceSubQueryShuttle(RexSubQuery subQuery, RexNode replacement) {
- this.subQuery = subQuery;
- this.replacement = replacement;
- }
-
- @Override public RexNode visitSubQuery(RexSubQuery subQuery) {
- return subQuery.equals(this.subQuery) ? replacement : subQuery;
- }
- }
- /** Rule configuration. */
- @Value.Immutable(singleton = false)
- public interface Config extends RelRule.Config {
- Config JOIN = ImmutableIgniteSubQueryRemoveRule.Config.builder()
- .withMatchHandler(IgniteSubQueryRemoveRule::matchJoin)
- .build()
- .withOperandSupplier(b ->
- b.operand(Join.class)
-
.predicate(RexUtil.SubQueryFinder::containsSubQuery)
- .anyInputs())
- .withDescription("SubQueryRemoveRule:Join");
-
- @Override default IgniteSubQueryRemoveRule toRule() {
- return new IgniteSubQueryRemoveRule(this);
- }
-
- /** Forwards a call to {@link #onMatch(RelOptRuleCall)}. */
- MatchHandler<IgniteSubQueryRemoveRule> matchHandler();
-
- /** Sets {@link #matchHandler()}. */
- Config withMatchHandler(MatchHandler<IgniteSubQueryRemoveRule>
matchHandler);
- }
-}
-//CHECKSTYLE:ON