This is an automated email from the ASF dual-hosted git repository.

alexpl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new 79b9a4758ad IGNITE-20255 SQL Calcite: Add ENFORCE_JOIN_ORDER hint - 
Fixes #10910.
79b9a4758ad is described below

commit 79b9a4758ad3cb8d85fb129a6968340b53c3e8f9
Author: Vladimir Steshin <[email protected]>
AuthorDate: Thu Oct 26 20:45:50 2023 +0300

    IGNITE-20255 SQL Calcite: Add ENFORCE_JOIN_ORDER hint - Fixes #10910.
    
    Signed-off-by: Aleksey Plekhanov <[email protected]>
---
 docs/_docs/SQL/sql-calcite.adoc                    |   6 +-
 .../query/calcite/CalciteQueryProcessor.java       |   3 +-
 .../processors/query/calcite/RootQuery.java        |   6 +-
 .../query/calcite/hint/HintDefinition.java         |  32 +++
 .../processors/query/calcite/hint/HintUtils.java   |  14 +-
 .../processors/query/calcite/hint/HintsConfig.java |  35 ++-
 .../query/calcite/prepare/BaseQueryContext.java    |  27 +-
 .../processors/query/calcite/prepare/CacheKey.java |   2 +-
 .../query/calcite/prepare/PlannerHelper.java       |  35 ++-
 .../query/calcite/prepare/PlannerPhase.java        |   2 +-
 .../query/calcite/jdbc/JdbcQueryTest.java          |  65 ++++-
 .../calcite/planner/hints/HintsTestSuite.java      |   3 +-
 .../planner/hints/JoinOrderHintsPlannerTest.java   | 279 +++++++++++++++++++++
 13 files changed, 491 insertions(+), 18 deletions(-)

diff --git a/docs/_docs/SQL/sql-calcite.adoc b/docs/_docs/SQL/sql-calcite.adoc
index a35689bc0ea..63e65244a2f 100644
--- a/docs/_docs/SQL/sql-calcite.adoc
+++ b/docs/_docs/SQL/sql-calcite.adoc
@@ -331,15 +331,15 @@ SELECT /*+ NO_INDEX */ T1.* FROM TBL1 T1 WHERE T1.V1 = 
T2.V1 AND T1.V2 > ?;
 SELECT /*+ NO_INDEX(TBL1_IDX2, TBL2_IDX1) */ T1.V1, T2.V1 FROM TBL1 T1, TBL2 
T2 WHERE T1.V1 = T2.V1 AND T1.V2 > ? AND T2.V2 > ?;
 ----
 
-==== ORDERED_JOINS
+==== ENFORCE_JOIN_ORDER
 Forces join order as appears in a query. Fastens building of joins plan.
 
 ===== Example:
 [source, SQL]
 ----
-SELECT /*+ ORDERED_JOINS */ T1.V1, T2.V1, T2.V2, T3.V1, T3.V2, T3.V3 FROM TBL1 
T1 JOIN TBL2 T2 ON T1.V3=T2.V1 JOIN TBL3 T3 ON T2.V3=T3.V1 AND T2.V2=T3.V2
+SELECT /*+ ENFORCE_JOIN_ORDER */ T1.V1, T2.V1, T2.V2, T3.V1, T3.V2, T3.V3 FROM 
TBL1 T1 JOIN TBL2 T2 ON T1.V3=T2.V1 JOIN TBL3 T3 ON T2.V3=T3.V1 AND T2.V2=T3.V2
 
-SELECT t1.v1, t3.v2 FROM TBL1 t1 JOIN TBL3 t3 on t1.v3=t3.v3 WHERE t1.v2 in 
(SELECT /*+ ORDERED_JOINS */ t2.v2 FROM TBL2 t2 JOIN TBL3 t3 ON t2.v1=t3.v1)
+SELECT t1.v1, t3.v2 FROM TBL1 t1 JOIN TBL3 t3 on t1.v3=t3.v3 WHERE t1.v2 in 
(SELECT /*+ ENFORCE_JOIN_ORDER */ t2.v2 FROM TBL2 t2 JOIN TBL3 t3 ON 
t2.v1=t3.v1)
 ----
 
 ==== EXPAND_DISTINCT_AGG
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java
index 5e60cd44b17..49720a4a242 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java
@@ -563,7 +563,7 @@ public class CalciteQueryProcessor extends 
GridProcessorAdapter implements Query
 
         SqlFieldsQuery sqlFieldsQry = qryCtx.unwrap(SqlFieldsQuery.class);
 
-        return sqlFieldsQry != null ? sqlFieldsQry.isLocal() : null;
+        return sqlFieldsQry != null ? F.asList(sqlFieldsQry.isLocal(), 
sqlFieldsQry.isEnforceJoinOrder()) : null;
     }
 
     /** */
@@ -588,6 +588,7 @@ public class CalciteQueryProcessor extends 
GridProcessorAdapter implements Query
             params,
             qryCtx,
             fldsQry != null && fldsQry.isLocal(),
+            fldsQry != null && fldsQry.isEnforceJoinOrder(),
             fldsQry != null ? fldsQry.getPartitions() : null,
             exchangeSvc,
             (q, ex) -> qryReg.unregister(q.id(), ex),
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/RootQuery.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/RootQuery.java
index 8a1f1c20555..31fafc96eee 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/RootQuery.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/RootQuery.java
@@ -108,6 +108,7 @@ public class RootQuery<RowT> extends Query<RowT> implements 
TrackableQuery {
         Object[] params,
         QueryContext qryCtx,
         boolean isLocal,
+        boolean forcedJoinOrder,
         int[] parts,
         ExchangeService exch,
         BiConsumer<Query<RowT>, Throwable> unregister,
@@ -146,6 +147,7 @@ public class RootQuery<RowT> extends Query<RowT> implements 
TrackableQuery {
                     .build()
             )
             .local(isLocal)
+            .forcedJoinOrder(forcedJoinOrder)
             .partitions(parts)
             .logger(log)
             .build();
@@ -160,8 +162,8 @@ public class RootQuery<RowT> extends Query<RowT> implements 
TrackableQuery {
      * @param schema new schema.
      */
     public RootQuery<RowT> childQuery(SchemaPlus schema) {
-        return new RootQuery<>(sql, schema, params, QueryContext.of(cancel), 
ctx.isLocal(), ctx.partitions(), exch, unregister, log,
-            plannerTimeout, totalTimeout);
+        return new RootQuery<>(sql, schema, params, QueryContext.of(cancel), 
ctx.isLocal(), ctx.isForcedJoinOrder(),
+            ctx.partitions(), exch, unregister, log, plannerTimeout, 
totalTimeout);
     }
 
     /** */
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintDefinition.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintDefinition.java
index 748bbc59913..a20e2675282 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintDefinition.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintDefinition.java
@@ -17,8 +17,14 @@
 
 package org.apache.ignite.internal.processors.query.calcite.hint;
 
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.rel.hint.HintPredicate;
 import org.apache.calcite.rel.hint.HintPredicates;
+import org.apache.calcite.rel.rules.CoreRules;
+import org.apache.calcite.rel.rules.JoinPushThroughJoinRule;
 import 
org.apache.ignite.internal.processors.query.calcite.rel.logical.IgniteLogicalTableScan;
 
 /**
@@ -47,6 +53,25 @@ public enum HintDefinition {
         }
     },
 
+    /** Forces join order as appears in query. Fastens building of joins plan. 
*/
+    ENFORCE_JOIN_ORDER {
+        /** {@inheritDoc} */
+        @Override public HintPredicate predicate() {
+            return HintPredicates.JOIN;
+        }
+
+        /** {@inheritDoc} */
+        @Override public HintOptionsChecker optionsChecker() {
+            return HintsConfig.OPTS_CHECK_EMPTY;
+        }
+
+        /** {@inheritDoc} */
+        @Override public Collection<RelOptRule> disabledRules() {
+            // CoreRules#JOIN_COMMUTE also disables 
CoreRules.JOIN_COMMUTE_OUTER.
+            return Arrays.asList(CoreRules.JOIN_COMMUTE, 
JoinPushThroughJoinRule.LEFT, JoinPushThroughJoinRule.RIGHT);
+        }
+    },
+
     /** Disables indexes. */
     NO_INDEX {
         /** {@inheritDoc} */
@@ -86,4 +111,11 @@ public enum HintDefinition {
     HintOptionsChecker optionsChecker() {
         return HintsConfig.OPTS_CHECK_PLAIN;
     }
+
+    /**
+     * @return Rules to excluded by current hint.
+     */
+    public Collection<RelOptRule> disabledRules() {
+        return Collections.emptyList();
+    }
 }
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintUtils.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintUtils.java
index dc3da9a7227..7cc01556719 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintUtils.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintUtils.java
@@ -138,13 +138,20 @@ public final class HintUtils {
                 + ' ';
 
             if (!relNode.getInputs().isEmpty())
-                relNode = new NoInputsRelNodeWrap(relNode);
+                relNode = noInputsRelWrap(relNode);
 
             log.debug(String.format("Skipped hint '%s' %sfor relation operator 
'%s'. %s", hint.hintName,
                 hintOptions, RelOptUtil.toString(relNode, 
SqlExplainLevel.EXPPLAN_ATTRIBUTES).trim(), reason));
         }
     }
 
+    /**
+     * @return A RelNode witout any inputs. For logging purposes.
+     */
+    public static RelNode noInputsRelWrap(RelNode rel) {
+        return new NoInputsRelNodeWrap(rel);
+    }
+
     /** */
     private static final class NoInputsRelNodeWrap extends AbstractRelNode {
         /** Original rel. */
@@ -162,6 +169,11 @@ public final class HintUtils {
             return Collections.emptyList();
         }
 
+        /** {@inheritDoc} */
+        @Override public RelNode getInput(int i) {
+            throw new UnsupportedOperationException("Failed to pass any node 
input. This a no-inputs node.");
+        }
+
         /** {@inheritDoc} */
         @Override protected RelDataType deriveRowType() {
             return rel.getRowType();
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintsConfig.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintsConfig.java
index 2f4e902ef9d..a3471845169 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintsConfig.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintsConfig.java
@@ -18,10 +18,14 @@
 package org.apache.ignite.internal.processors.query.calcite.hint;
 
 import java.util.Arrays;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.hint.HintPredicate;
 import org.apache.calcite.rel.hint.HintStrategy;
 import org.apache.calcite.rel.hint.HintStrategyTable;
 import org.apache.calcite.rel.hint.RelHint;
 import org.apache.calcite.util.Litmus;
+import org.apache.ignite.internal.util.typedef.F;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -77,9 +81,38 @@ public final class HintsConfig {
     public static HintStrategyTable buildHintTable() {
         HintStrategyTable.Builder b = 
HintStrategyTable.builder().errorHandler(Litmus.IGNORE);
 
+        RelOptRule[] disabledRulesTpl = new RelOptRule[0];
+
         Arrays.stream(HintDefinition.values()).forEach(hintDef ->
-            b.hintStrategy(hintDef.name(), 
HintStrategy.builder(hintDef.predicate()).build()));
+            b.hintStrategy(hintDef.name(), 
HintStrategy.builder(hintPredicate(hintDef))
+                
.excludedRules(hintDef.disabledRules().toArray(disabledRulesTpl)).build()));
 
         return b.build();
     }
+
+    /**
+     * Adds hint options checker to {@link HintPredicate} if {@code hintDef} 
has rules to exclude.
+     *
+     * @return Hint predicate.
+     */
+    private static HintPredicate hintPredicate(HintDefinition hintDef) {
+        if (F.isEmpty(hintDef.disabledRules()))
+            return hintDef.predicate();
+
+        return new HintPredicate() {
+            @Override public boolean apply(RelHint hint, RelNode rel) {
+                if (!hintDef.predicate().apply(hint, rel))
+                    return false;
+
+                String optsErrMsg = hintDef.optionsChecker().apply(hint);
+
+                if (F.isEmpty(optsErrMsg))
+                    return true;
+
+                HintUtils.skippedHint(rel, hint, optsErrMsg);
+
+                return false;
+            }
+        };
+    }
 }
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/BaseQueryContext.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/BaseQueryContext.java
index e5f81d91d08..2f1e1bb5845 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/BaseQueryContext.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/BaseQueryContext.java
@@ -160,6 +160,9 @@ public final class BaseQueryContext extends 
AbstractQueryContext {
     /** */
     private final boolean isLocal;
 
+    /** */
+    private final boolean forcedJoinOrder;
+
     /** */
     private final int[] parts;
 
@@ -171,6 +174,7 @@ public final class BaseQueryContext extends 
AbstractQueryContext {
         Context parentCtx,
         IgniteLogger log,
         boolean isLocal,
+        boolean forcedJoinOrder,
         int[] parts
     ) {
         super(Contexts.chain(parentCtx, cfg.getContext()));
@@ -182,6 +186,8 @@ public final class BaseQueryContext extends 
AbstractQueryContext {
 
         this.isLocal = isLocal;
 
+        this.forcedJoinOrder = forcedJoinOrder;
+
         this.parts = parts;
 
         qryCancel = unwrap(GridQueryCancel.class);
@@ -283,6 +289,11 @@ public final class BaseQueryContext extends 
AbstractQueryContext {
         return isLocal;
     }
 
+    /** */
+    public boolean isForcedJoinOrder() {
+        return forcedJoinOrder;
+    }
+
     /** */
     public int[] partitions() {
         if (parts != null)
@@ -312,7 +323,10 @@ public final class BaseQueryContext extends 
AbstractQueryContext {
         private IgniteLogger log = new NullLogger();
 
         /** */
-        private boolean isLocal = false;
+        private boolean isLocal;
+
+        /** */
+        private boolean forcedJoinOrder;
 
         /** */
         private int[] parts = null;
@@ -353,6 +367,15 @@ public final class BaseQueryContext extends 
AbstractQueryContext {
             return this;
         }
 
+        /**
+         * @param forcedJoinOrder Forced join orders flag.
+         * @return Builder for chaining.
+         */
+        public Builder forcedJoinOrder(boolean forcedJoinOrder) {
+            this.forcedJoinOrder = forcedJoinOrder;
+            return this;
+        }
+
         /**
          * @param parts Array of partitions' numbers.
          * @return Builder for chaining.
@@ -370,7 +393,7 @@ public final class BaseQueryContext extends 
AbstractQueryContext {
          * @return Planner context.
          */
         public BaseQueryContext build() {
-            return new BaseQueryContext(frameworkCfg, parentCtx, log, isLocal, 
parts);
+            return new BaseQueryContext(frameworkCfg, parentCtx, log, isLocal, 
forcedJoinOrder, parts);
         }
     }
 }
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/CacheKey.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/CacheKey.java
index 07cedd66a3c..f7aef65df62 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/CacheKey.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/CacheKey.java
@@ -49,7 +49,7 @@ public class CacheKey {
         this.query = query;
         this.contextKey = contextKey;
         paramTypes = params.length == 0 ? null :
-            Arrays.stream(params).map(p -> (p != null) ? p.getClass() : 
Void.class).toArray(Class[]::new);;
+            Arrays.stream(params).map(p -> (p != null) ? p.getClass() : 
Void.class).toArray(Class[]::new);
     }
 
     /**
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerHelper.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerHelper.java
index b979c7997c6..7618793255f 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerHelper.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerHelper.java
@@ -22,6 +22,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import com.google.common.collect.ImmutableSet;
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.plan.RelTraitSet;
@@ -31,6 +32,7 @@ import org.apache.calcite.rel.RelRoot;
 import org.apache.calcite.rel.core.SetOp;
 import org.apache.calcite.rel.core.Spool;
 import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.hint.Hintable;
 import org.apache.calcite.rel.hint.RelHint;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexNode;
@@ -73,8 +75,9 @@ public class PlannerHelper {
             // Convert to Relational operators graph.
             RelRoot root = planner.rel(sqlNode);
 
-            planner.setDisabledRules(HintUtils.options(root.rel, 
extractRootHints(root.rel),
-                HintDefinition.DISABLE_RULE));
+            root = addExternalOptions(root);
+
+            planner.setDisabledRules(HintUtils.options(root.rel, 
extractRootHints(root.rel), HintDefinition.DISABLE_RULE));
 
             RelNode rel = root.rel;
 
@@ -126,6 +129,34 @@ public class PlannerHelper {
         }
     }
 
+    /**
+     * Add external options as hints to {@code root.rel}.
+     *
+     * @return New or old root node.
+     */
+    private static RelRoot addExternalOptions(RelRoot root) {
+        if (!Commons.context(root.rel).isForcedJoinOrder())
+            return root;
+
+        if (!(root.rel instanceof Hintable)) {
+            Commons.context(root.rel).logger().warning("Unable to set hint " + 
HintDefinition.ENFORCE_JOIN_ORDER
+                + " passed as an external parameter to the root relation 
operator ["
+                + 
RelOptUtil.toString(HintUtils.noInputsRelWrap(root.rel)).trim()
+                + "] because it is not a Hintable.");
+
+            return root;
+        }
+
+        List<RelHint> newHints = 
Stream.concat(HintUtils.allRelHints(root.rel).stream(),
+            
Stream.of(RelHint.builder(HintDefinition.ENFORCE_JOIN_ORDER.name()).build())).collect(Collectors.toList());
+
+        root = root.withRel(((Hintable)root.rel).withHints(newHints));
+
+        RelOptUtil.propagateRelHints(root.rel, false);
+
+        return root;
+    }
+
     /**
      * Extracts planner-level hints like 'DISABLE_RULE' if the root node is a 
combining node like 'UNION'.
      */
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
index 8de3f3018b6..73b315ae3d6 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
@@ -207,8 +207,8 @@ public enum PlannerPhase {
                     CoreRules.MINUS_MERGE,
                     CoreRules.INTERSECT_MERGE,
                     CoreRules.UNION_REMOVE,
-                    CoreRules.JOIN_COMMUTE,
                     CoreRules.AGGREGATE_REMOVE,
+                    // Works also as CoreRules#JOIN_COMMUTE and overrides it 
if defined after.
                     CoreRules.JOIN_COMMUTE_OUTER,
 
                     // Useful of this rule is not clear now.
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcQueryTest.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcQueryTest.java
index 97504e94ca1..ee0a1adc613 100644
--- 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcQueryTest.java
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcQueryTest.java
@@ -76,14 +76,27 @@ public class JdbcQueryTest extends GridCommonAbstractTest {
     /** {@inheritDoc} */
     @Override protected void beforeTest() throws Exception {
         startGrids(nodesCnt);
-        conn = DriverManager.getConnection(url);
-        conn.setSchema("PUBLIC");
-        stmt = conn.createStatement();
+
+        connect(url);
 
         assert stmt != null;
         assert !stmt.isClosed();
     }
 
+    /** */
+    private void connect(String url) throws Exception {
+        if (stmt != null)
+            stmt.close();
+
+        if (conn != null)
+            conn.close();
+
+        conn = DriverManager.getConnection(url);
+        conn.setSchema("PUBLIC");
+
+        stmt = conn.createStatement();
+    }
+
     /** {@inheritDoc} */
     @Override protected void afterTest() throws Exception {
         if (stmt != null && !stmt.isClosed()) {
@@ -202,6 +215,52 @@ public class JdbcQueryTest extends GridCommonAbstractTest {
         }
     }
 
+    /** Test enforced join order parameter. */
+    @Test
+    public void testEnforcedJoinOrder() throws Exception {
+        stmt.execute("CREATE TABLE Person1(\"ID\" INT, PRIMARY KEY(\"ID\"), 
\"NAME\" VARCHAR) WITH template=REPLICATED");
+        stmt.execute("CREATE TABLE Person2(\"ID\" INT, PRIMARY KEY(\"ID\"), 
\"NAME\" VARCHAR) WITH template=REPLICATED");
+
+        for (int i = 0; i < 3; ++i)
+            stmt.execute(String.format("INSERT INTO Person1 VALUES (%d, 
'Name')", i));
+
+        for (int i = 0; i < 100; ++i)
+            stmt.addBatch(String.format("INSERT INTO Person2 VALUES (%d, 
'Name')", i));
+
+        stmt.executeBatch();
+
+        String scan1 = "Scan(table=[[PUBLIC, PERSON1]]";
+        String scan2 = "Scan(table=[[PUBLIC, PERSON2]]";
+
+        connect(url + "&enforceJoinOrder=true");
+
+        try (ResultSet rs = stmt.executeQuery("EXPLAIN PLAN FOR SELECT p2.Name 
from Person1 p1 LEFT JOIN Person2 " +
+            "p2 on p2.NAME=p1.NAME")) {
+            assertTrue(rs.next());
+
+            String plan = rs.getString(1);
+
+            // Joins as in the query.
+            assertTrue(plan.indexOf(scan1) < plan.indexOf(scan2));
+
+            // Join type is not changed.
+            assertTrue(plan.contains("joinType=[left]"));
+        }
+
+        try (ResultSet rs = stmt.executeQuery("EXPLAIN PLAN FOR SELECT /*+ 
DISABLE_RULE('NestedLoopJoinConverter') */ " +
+            "p2.Name from Person2 p2 RIGHT JOIN Person1 p1 on 
p2.NAME=p1.NAME")) {
+            assertTrue(rs.next());
+
+            String plan = rs.getString(1);
+
+            // Joins as in the query.
+            assertTrue(plan.indexOf(scan1) > plan.indexOf(scan2));
+
+            // Join type is not changed.
+            assertTrue(plan.contains("joinType=[right]"));
+        }
+    }
+
     /** Test batched execution of prepared statement. */
     @Test
     public void testBatchPrepared() throws Exception {
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/HintsTestSuite.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/HintsTestSuite.java
index 4271935d626..f54352e0fb0 100644
--- 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/HintsTestSuite.java
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/HintsTestSuite.java
@@ -27,7 +27,8 @@ import org.junit.runners.Suite;
 @Suite.SuiteClasses({
     CommonHintsPlannerTest.class,
     NoIndexHintPlannerTest.class,
-    ForceIndexHintPlannerTest.class
+    ForceIndexHintPlannerTest.class,
+    JoinOrderHintsPlannerTest.class,
 })
 public class HintsTestSuite {
 }
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/JoinOrderHintsPlannerTest.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/JoinOrderHintsPlannerTest.java
new file mode 100644
index 00000000000..a0a9a54ea9d
--- /dev/null
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/JoinOrderHintsPlannerTest.java
@@ -0,0 +1,279 @@
+/*
+ * 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.processors.query.calcite.planner.hints;
+
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rel.core.SetOp;
+import org.apache.calcite.rel.rules.JoinPushThroughJoinRule;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition;
+import 
org.apache.ignite.internal.processors.query.calcite.planner.AbstractPlannerTest;
+import org.apache.ignite.internal.processors.query.calcite.planner.TestTable;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema;
+import 
org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.testframework.LogListener;
+import org.apache.ignite.testframework.junits.logger.GridTestLog4jLogger;
+import org.apache.logging.log4j.Level;
+import org.junit.Test;
+
+/**
+ * Planner test for join order hints.
+ */
+public class JoinOrderHintsPlannerTest extends AbstractPlannerTest {
+    /** */
+    private IgniteSchema schema;
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+
+        ((GridTestLog4jLogger)log).setLevel(Level.INFO);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void setup() {
+        super.setup();
+
+        int tblNum = 3;
+        int fldNum = 3;
+
+        TestTable[] tables = new TestTable[tblNum];
+        Object[] fields = new Object[tblNum * 2];
+
+        for (int f = 0; f < fldNum; ++f) {
+            fields[f * 2] = "V" + (f + 1);
+            fields[f * 2 + 1] = Integer.class;
+        }
+
+        // Tables with growing records number.
+        for (int t = 0; t < tables.length; ++t) {
+            tables[t] = createTable("TBL" + (t + 1), Math.min(1_000_000, 
(int)Math.pow(10, t + 1)),
+                IgniteDistributions.broadcast(), fields);
+        }
+
+        schema = createSchema(tables);
+    }
+
+    /**
+     * Tests {@link JoinPushThroughJoinRule#LEFT} is disabled by {@link 
HintDefinition#ENFORCE_JOIN_ORDER}.
+     */
+    @Test
+    public void testDisabledJoinPushThroughJoinLeft() throws Exception {
+        // Disabling some join rules simplifies exposing of commuted and/or 
re-ordered joins.
+        String disabledRules = "DISABLE_RULE('MergeJoinConverter', 
'CorrelatedNestedLoopJoin')";
+
+        // Tests swapping of joins is disabled and the order appears in the 
query, 'TBL3 -> TBL2 -> TBL1':
+        // Join
+        //   Join
+        //     TableScan(TBL3)
+        //     TableScan(TBL2)
+        //   TableScan(TBL1)
+        String sql = String.format("select /*+ %s, %s */ t3.* from TBL3 t3, 
TBL2 t2, TBL1 t1 where t1.v1=t3.v1 and " +
+            "t1.v2=t2.v2", HintDefinition.ENFORCE_JOIN_ORDER.name(), 
disabledRules);
+
+        assertPlan(sql, schema, hasChildThat(isInstanceOf(Join.class)
+            .and(input(0, isInstanceOf(Join.class)
+                .and(input(0, isTableScan("TBL3")))
+                .and(input(1, isTableScan("TBL2")))))
+            .and(input(1, isTableScan("TBL1")))));
+    }
+
+    /**
+     * Tests commuting of LEFT-to-RIGHT JOIN is disabled by {@link 
HintDefinition#ENFORCE_JOIN_ORDER}.
+     */
+    @Test
+    public void testDisabledLeftJoinTypeCommuting() throws Exception {
+        doTestDisabledJoinTypeCommuting("LEFT");
+    }
+
+    /**
+     * Tests commuting of RIGHT-to-LEFT JOIN is disabled by {@link 
HintDefinition#ENFORCE_JOIN_ORDER}.
+     */
+    @Test
+    public void testDisabledRightJoinTypeCommuting() throws Exception {
+        doTestDisabledJoinTypeCommuting("RIGHT");
+    }
+
+    /**
+     * Tests commuting of {@code joinType} is disabled.
+     *
+     * @param joinType LEFT or RIGHT JOIN type to test in upper case.
+     */
+    private void doTestDisabledJoinTypeCommuting(String joinType) throws 
Exception {
+        // Disabling some join rules simplifies exposing of commuted and/or 
re-ordered joins.
+        String disabledRules = "DISABLE_RULE('MergeJoinConverter', 
'CorrelatedNestedLoopJoin')";
+
+        // Tests commuting of the join type is disabled.
+        String sql = String.format("select /*+ %s, %s */ t3.* from TBL2 t2 %s 
JOIN TBL1 t1 on t2.v2=t1.v1 %s JOIN " +
+            "TBL3 t3 on t2.v1=t3.v3", 
HintDefinition.ENFORCE_JOIN_ORDER.name(), disabledRules, joinType, joinType);
+
+        assertPlan(sql, schema, hasChildThat(isInstanceOf(Join.class)
+            .and(j -> j.getJoinType() != 
JoinRelType.valueOf(joinType))).negate());
+    }
+
+    /**
+     * Tests {@link JoinPushThroughJoinRule#RIGHT} is disabled by {@link 
HintDefinition#ENFORCE_JOIN_ORDER}.
+     */
+    @Test
+    public void testDisabledJoinPushThroughJoinRight() throws Exception {
+        // Disabling some join rules simplifies exposing of commuted and/or 
re-ordered joins.
+        String disabledRules = "DISABLE_RULE('MergeJoinConverter', 
'CorrelatedNestedLoopJoin')";
+
+        // Tests the swapping of joins is disabled and the order appears as in 
the query, 'TBL1->TBL2->TBL3':
+        // Join
+        //   Join
+        //     TableScan(TBL1)
+        //     TableScan(TBL2)
+        //   TableScan(TBL3)
+        String sql = String.format("select /*+ %s, %s */ t3.* from TBL1 t1, 
TBL2 t2, TBL3 t3 where t1.v1=t3.v1 and " +
+            "t1.v2=t2.v2", HintDefinition.ENFORCE_JOIN_ORDER.name(), 
disabledRules);
+
+        assertPlan(sql, schema, hasChildThat(isInstanceOf(Join.class)
+            .and(input(0, isInstanceOf(Join.class)
+                .and(input(0, isTableScan("TBL1")))
+                .and(input(1, isTableScan("TBL2")))))
+            .and(input(1, isTableScan("TBL3")))));
+    }
+
+    /**
+     * Tests the commuting of join inputs is disabled by {@link 
HintDefinition#ENFORCE_JOIN_ORDER}.
+     */
+    @Test
+    public void testDisabledCommutingOfJoinInputs() throws Exception {
+        // Disabling some join rules simplifies exposing of commuted and/or 
re-ordered joins.
+        String disabledRules = "DISABLE_RULE('MergeJoinConverter', 
'CorrelatedNestedLoopJoin')";
+
+        String sql = String.format("select /*+ %s, %s */ t3.* from TBL1 t1 
JOIN TBL3 t3 on t1.v1=t3.v3 JOIN TBL2 t2 on " +
+            "t2.v2=t1.v1", HintDefinition.ENFORCE_JOIN_ORDER.name(), 
disabledRules);
+
+        // Tests the plan has no commuted join inputs.
+        assertPlan(sql, schema, nodeOrAnyChild(isInstanceOf(Join.class)
+            .and(input(0, isTableScan("TBL1")))
+            .and(input(1, isTableScan("TBL3")))));
+    }
+
+    /**
+     * Tests that the sub-join has inputs order matching the sub-query.
+     */
+    @Test
+    public void testDisabledCommutingOfJoinInputsInSubquery() throws Exception 
{
+        String sqlTpl = "SELECT %s t2.v1, t3.v2 from TBL2 t2 JOIN TBL3 t3 on 
t2.v1=t3.v1 where t2.v2 in " +
+            "(SELECT %s t2.v2 from TBL2 t2 JOIN TBL3 t3 on t2.v2=t3.v3)";
+
+        // Tests the hint is applied for the whole query.
+        assertPlan(String.format(sqlTpl, "/*+ " + 
HintDefinition.ENFORCE_JOIN_ORDER + " */", ""), schema,
+            nodeOrAnyChild(isInstanceOf(Join.class)
+                .and(input(0, nodeOrAnyChild(isInstanceOf(Join.class)
+                    .and(input(0, nodeOrAnyChild(isTableScan("TBL2"))))
+                    .and(input(1, nodeOrAnyChild(isTableScan("TBL3")))))))
+                .and(input(1, nodeOrAnyChild(isInstanceOf(Join.class)
+                    .and(input(0, nodeOrAnyChild(isTableScan("TBL2"))))
+                    .and(input(1, nodeOrAnyChild(isTableScan("TBL3")))))))));
+
+        // Tests the hint is applied for the sub-query.
+        assertPlan(String.format(sqlTpl, "", "/*+ " + 
HintDefinition.ENFORCE_JOIN_ORDER + " */"), schema,
+            nodeOrAnyChild(isInstanceOf(Join.class)
+                .and(input(0, 
nodeOrAnyChild(isTableScan("TBL2").or(isTableScan("TBL3")))))
+                .and(input(1, nodeOrAnyChild(isInstanceOf(Join.class)
+                    .and(input(0, nodeOrAnyChild(isTableScan("TBL2"))))
+                    .and(input(1, nodeOrAnyChild(isTableScan("TBL3")))))))));
+    }
+
+    /**
+     * Tests join plan building duration. Without enabled forced order, takes 
too long.
+     */
+    @Test
+    public void testJoinPlanBuildingDuration() throws Exception {
+        // Just a 3-tables join.
+        String sql = "SELECT /*+ " + HintDefinition.ENFORCE_JOIN_ORDER + " */ 
T1.V1, T2.V1, T2.V2, T3.V1, T3.V2, T3.V3 " +
+            "FROM TBL1 T1 JOIN TBL2 T2 ON T1.V3=T2.V1 JOIN TBL3 T3 ON 
T2.V3=T3.V1 AND T2.V2=T3.V2";
+
+        long time = 0;
+
+        // Heat a bit and measure only the last run.
+        for (int i = 0; i < 6; ++i) {
+            time = System.nanoTime();
+
+            physicalPlan(sql, schema);
+
+            time = U.nanosToMillis(System.nanoTime() - time);
+
+            log.info("Plan building took " + time + "ms.");
+        }
+
+        assertTrue("Plan building took too long: " + time + "ms.", time < 
3000L);
+    }
+
+    /** */
+    @Test
+    public void testUnions() throws Exception {
+        String sqlTpl = "SELECT %s t2.v1, t3.v2 from TBL2 t2 JOIN TBL3 t3 on 
t2.v1=t3.v1 UNION ALL " +
+            "SELECT %s t1.v1, t2.v2 from TBL1 t1 JOIN TBL2 t2 on t1.v1=t2.v2";
+
+        String hint = HintDefinition.ENFORCE_JOIN_ORDER.toString();
+
+        assertPlan(String.format(sqlTpl, "/*+ " + hint + " */", ""), schema,
+            nodeOrAnyChild(isInstanceOf(SetOp.class)
+                .and(input(0, nodeOrAnyChild(isInstanceOf(Join.class)
+                    .and(input(0, nodeOrAnyChild(isTableScan("TBL2"))))
+                    .and(input(1, nodeOrAnyChild(isTableScan("TBL3")))))))));
+
+        assertPlan(String.format(sqlTpl, "", "/*+ " + hint + " */"), schema,
+            nodeOrAnyChild(isInstanceOf(SetOp.class)
+                .and(input(1, nodeOrAnyChild(isInstanceOf(Join.class)
+                    .and(input(0, nodeOrAnyChild(isTableScan("TBL1"))))
+                    .and(input(1, nodeOrAnyChild(isTableScan("TBL2")))))))));
+
+        assertPlan(String.format(sqlTpl, "/*+ " + hint + " */", "/*+ " + hint 
+ " */"), schema,
+            nodeOrAnyChild(isInstanceOf(SetOp.class)
+                .and(input(1, nodeOrAnyChild(isInstanceOf(Join.class)
+                    .and(input(0, nodeOrAnyChild(isTableScan("TBL1"))))
+                    .and(input(1, nodeOrAnyChild(isTableScan("TBL2")))))))
+                .and(input(0, nodeOrAnyChild(isInstanceOf(Join.class)
+                    .and(input(0, nodeOrAnyChild(isTableScan("TBL2"))))
+                    .and(input(1, nodeOrAnyChild(isTableScan("TBL3")))))))));
+    }
+
+    /** */
+    @Test
+    public void testWrongParams() throws Exception {
+        LogListener lsnr = LogListener.matches("Hint '" + 
HintDefinition.ENFORCE_JOIN_ORDER
+            + "' can't have any key-value option").build();
+
+        lsnrLog.registerListener(lsnr);
+
+        ((GridTestLog4jLogger)log).setLevel(Level.DEBUG);
+
+        physicalPlan("select /*+ " + HintDefinition.ENFORCE_JOIN_ORDER.name() 
+ "(a='b') */ t3.* from TBL1 t1, " +
+            "TBL2 t2, TBL3 t3 where t1.v1=t3.v1 and t1.v2=t2.v2", schema);
+
+        assertTrue(lsnr.check());
+
+        lsnrLog.clearListeners();
+
+        lsnr = LogListener.matches("Hint '" + 
HintDefinition.ENFORCE_JOIN_ORDER + "' can't have any option").build();
+
+        lsnrLog.registerListener(lsnr);
+
+        physicalPlan("select /*+ " + HintDefinition.ENFORCE_JOIN_ORDER.name() 
+ "(OPTION) */ t3.* from TBL1 t1, " +
+            "TBL2 t2, TBL3 t3 where t1.v1=t3.v1 and t1.v2=t2.v2", schema);
+
+        assertTrue(lsnr.check());
+    }
+}


Reply via email to