You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@ignite.apache.org by "alex-plekhanov (via GitHub)" <gi...@apache.org> on 2023/12/21 18:13:48 UTC

Re: [PR] IGNITE-20323 : SQL hint for join type. [ignite]

alex-plekhanov commented on code in PR #10918:
URL: https://github.com/apache/ignite/pull/10918#discussion_r1434134966


##########
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintDefinition.java:
##########
@@ -96,6 +96,84 @@ public enum HintDefinition {
         @Override public HintOptionsChecker optionsChecker() {
             return NO_INDEX.optionsChecker();
         }
+    },
+
+    /** Forces merge join. */
+    MERGE_JOIN {
+        /** {@inheritDoc} */
+        @Override public HintPredicate predicate() {
+            return HintPredicates.JOIN;
+        }
+
+        /** {@inheritDoc} */
+        @Override public HintOptionsChecker optionsChecker() {
+            return HintsConfig.OPTS_CHECK_NO_KV;
+        }
+    },
+
+    /** Disables merge join. */
+    NO_MERGE_JOIN {
+        /** {@inheritDoc} */
+        @Override public HintPredicate predicate() {
+            return MERGE_JOIN.predicate();
+        }
+
+        /** {@inheritDoc} */
+        @Override public HintOptionsChecker optionsChecker() {
+            return MERGE_JOIN.optionsChecker();
+        }
+    },
+
+    /** Forces merge join. */
+    NL_JOIN {
+        /** {@inheritDoc} */
+        @Override public HintPredicate predicate() {
+            return MERGE_JOIN.predicate();

Review Comment:
   Looks inconsistent. NL_JOIN should not rely on MERGE_JOIN. Lets use the same `predicate` and `optionsChecker` as MERGE_JOIN, but not call MERGE_JOIN methods.



##########
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/AbstractIgniteJoinConverterRule.java:
##########
@@ -0,0 +1,192 @@
+/*
+ * 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.rule;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.volcano.RelSubset;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.hint.RelHint;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintUtils;
+
+import static org.apache.calcite.util.Util.last;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_NL_JOIN;
+
+/** */
+abstract class AbstractIgniteJoinConverterRule extends AbstractIgniteConverterRule<LogicalJoin> {
+    /** Known join type hints and the opposite hints. */
+    private static final EnumMap<HintDefinition, HintDefinition> HINTS = new EnumMap<>(HintDefinition.class);
+
+    /** Hint disabing this join type. */
+    private final HintDefinition knownDisableHint;
+
+    /** Hint forcing usage of this join type. */
+    private final HintDefinition knownForceHint;
+
+    static {
+        HINTS.put(NL_JOIN, NO_NL_JOIN);
+        HINTS.put(CNL_JOIN, NO_CNL_JOIN);
+        HINTS.put(MERGE_JOIN, NO_MERGE_JOIN);
+    }
+
+    /** */
+    protected AbstractIgniteJoinConverterRule(String descriptionPrefix, HintDefinition forceHint,

Review Comment:
   Each parameter on own line (codestyle)



##########
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/AbstractIgniteJoinConverterRule.java:
##########
@@ -0,0 +1,192 @@
+/*
+ * 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.rule;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.volcano.RelSubset;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.hint.RelHint;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintUtils;
+
+import static org.apache.calcite.util.Util.last;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_NL_JOIN;
+
+/** */
+abstract class AbstractIgniteJoinConverterRule extends AbstractIgniteConverterRule<LogicalJoin> {
+    /** Known join type hints and the opposite hints. */
+    private static final EnumMap<HintDefinition, HintDefinition> HINTS = new EnumMap<>(HintDefinition.class);
+
+    /** Hint disabing this join type. */
+    private final HintDefinition knownDisableHint;
+
+    /** Hint forcing usage of this join type. */
+    private final HintDefinition knownForceHint;
+
+    static {
+        HINTS.put(NL_JOIN, NO_NL_JOIN);
+        HINTS.put(CNL_JOIN, NO_CNL_JOIN);
+        HINTS.put(MERGE_JOIN, NO_MERGE_JOIN);
+    }
+
+    /** */
+    protected AbstractIgniteJoinConverterRule(String descriptionPrefix, HintDefinition forceHint,
+        HintDefinition disableHint) {
+        super(LogicalJoin.class, descriptionPrefix);
+
+        assert forceHint != disableHint;
+        assert HINTS.containsKey(forceHint);
+        assert HINTS.containsValue(disableHint);
+        assert !HINTS.containsKey(disableHint);
+        assert !HINTS.containsValue(forceHint);

Review Comment:
   We have 1:1 mapping, so we don't need to pass second parameter and these asserts too. We always can determine `disableHint` for `forceHint`



##########
modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/JoinOrderHintsPlannerTest.java:
##########
@@ -76,7 +76,7 @@ public class JoinOrderHintsPlannerTest extends AbstractPlannerTest {
     @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')";
+        String disabledRules = "NO_MERGE_JOIN, NO_CNL_JOIN";

Review Comment:
   NO_MERGE_JOIN, NO_CNL_JOIN -> NL_JOIN (in all tests)



##########
modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexScanlIntegrationTest.java:
##########
@@ -69,7 +69,7 @@ public void testNullsInCNLJSearchRow() {
 
         RowCountingIndex idx = injectRowCountingIndex(grid(0), "T", "T_IDX");
 
-        String sql = "SELECT /*+ DISABLE_RULE('NestedLoopJoinConverter', 'MergeJoinConverter') */ t1.i1, t2.i1 " +
+        String sql = "SELECT /*+ NO_MERGE_JOIN, NO_NL_JOIN */ t1.i1, t2.i1 " +

Review Comment:
   NO_MERGE_JOIN, NO_NL_JOIN -> CNL_JOIN (in all tests)



##########
modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/CalciteErrorHandlilngIntegrationTest.java:
##########
@@ -101,7 +101,7 @@ public void assertionOnDeserialization() throws Exception {
 
         sql(client, "create table test (id int primary key, val varchar)");
 
-        String sql = "select /*+ DISABLE_RULE('NestedLoopJoinConverter', 'MergeJoinConverter') */ t1.id " +
+        String sql = "select /*+ NO_MERGE_JOIN, NO_NL_JOIN */ t1.id " +

Review Comment:
   Here we disable two type of join to force CNL Join, lets just use `CNL_JOIN` hint.



##########
modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexSpoolIntegrationTest.java:
##########
@@ -136,7 +136,7 @@ public void test() throws Exception {
         List<FieldsQueryCursor<List<?>>> cursors = engine.query(
             null,
             "PUBLIC",
-            "SELECT /*+ DISABLE_RULE('NestedLoopJoinConverter', 'MergeJoinConverter') */" +
+            "SELECT /*+ NO_MERGE_JOIN, NO_NL_JOIN */" +

Review Comment:
   NO_MERGE_JOIN, NO_NL_JOIN -> CNL_JOIN



##########
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/AbstractIgniteConverterRule.java:
##########
@@ -26,7 +26,7 @@
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
 
 /** */
-public abstract class AbstractIgniteConverterRule<T extends RelNode> extends ConverterRule {
+abstract class AbstractIgniteConverterRule<T extends RelNode> extends ConverterRule {

Review Comment:
   Redundant change



##########
modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/HashSpoolIntegrationTest.java:
##########
@@ -55,8 +55,7 @@ public void testNullsInSearchRowMultipleColumns() {
         executeSql("INSERT INTO t0 VALUES (null, 0), (1, null), (null, 2), (3, null), (1, 1)");
         executeSql("INSERT INTO t1 VALUES (null, 0), (null, 1), (2, null), (3, null), (1, 1)");
 
-        String sql = "SELECT /*+ DISABLE_RULE ('MergeJoinConverter', 'NestedLoopJoinConverter', " +
-            "'FilterSpoolMergeToSortedIndexSpoolRule')*/ * " +
+        String sql = "SELECT /*+ NO_MERGE_JOIN, NO_NL_JOIN, DISABLE_RULE('FilterSpoolMergeToSortedIndexSpoolRule') */ * " +

Review Comment:
   NO_MERGE_JOIN, NO_NL_JOIN -> CNL_JOIN (in all tests)



##########
modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexRebuildIntegrationTest.java:
##########
@@ -321,7 +321,7 @@ public void testRebuildOnRemoteNodeCorrelated() throws Exception {
         IgniteEx initNode = grid(0);
 
         // Correlated join with correlation in filter, without project.
-        String sql = "SELECT /*+ DISABLE_RULE('MergeJoinConverter', 'NestedLoopJoinConverter') */ tbl2.id, tbl.val " +
+        String sql = "SELECT /*+ NO_MERGE_JOIN, NO_NL_JOIN */ tbl2.id, tbl.val " +

Review Comment:
   NO_MERGE_JOIN, NO_NL_JOIN -> CNL_JOIN (in all tests)



##########
modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/rules/JoinCommuteRulesTest.java:
##########
@@ -60,7 +60,7 @@ public class JoinCommuteRulesTest extends GridCommonAbstractTest {
     /** */
     @Test
     public void testCommuteOuter() {
-        String sql = "SELECT /*+ DISABLE_RULE('CorrelatedNestedLoopJoin', 'MergeJoinConverter') */ " +
+        String sql = "SELECT /*+ NO_MERGE_JOIN, NO_CNL_JOIN */ " +

Review Comment:
   NO_MERGE_JOIN, NO_CNL_JOIN -> NL_JOIN (in all tests)



##########
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/AbstractIgniteJoinConverterRule.java:
##########
@@ -0,0 +1,192 @@
+/*
+ * 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.rule;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.volcano.RelSubset;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.hint.RelHint;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintUtils;
+
+import static org.apache.calcite.util.Util.last;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_NL_JOIN;
+
+/** */
+abstract class AbstractIgniteJoinConverterRule extends AbstractIgniteConverterRule<LogicalJoin> {
+    /** Known join type hints and the opposite hints. */
+    private static final EnumMap<HintDefinition, HintDefinition> HINTS = new EnumMap<>(HintDefinition.class);
+
+    /** Hint disabing this join type. */
+    private final HintDefinition knownDisableHint;
+
+    /** Hint forcing usage of this join type. */
+    private final HintDefinition knownForceHint;
+
+    static {
+        HINTS.put(NL_JOIN, NO_NL_JOIN);
+        HINTS.put(CNL_JOIN, NO_CNL_JOIN);
+        HINTS.put(MERGE_JOIN, NO_MERGE_JOIN);
+    }
+
+    /** */
+    protected AbstractIgniteJoinConverterRule(String descriptionPrefix, HintDefinition forceHint,
+        HintDefinition disableHint) {
+        super(LogicalJoin.class, descriptionPrefix);
+
+        assert forceHint != disableHint;
+        assert HINTS.containsKey(forceHint);
+        assert HINTS.containsValue(disableHint);
+        assert !HINTS.containsKey(disableHint);
+        assert !HINTS.containsValue(forceHint);
+
+        knownDisableHint = disableHint;
+        knownForceHint = forceHint;
+    }
+
+    /** {@inheritDoc} */
+    @Override public final boolean matches(RelOptRuleCall call) {
+        return super.matches(call) && matchesCall(call) && !disabledByHints(call.rel(0));
+    }
+
+    /** */
+    private boolean disabledByHints(LogicalJoin join) {
+        if (HintUtils.allRelHints(join).isEmpty())
+            return false;
+
+        boolean ruleDisabled = false;
+
+        Map<String, Collection<HintDefinition>> hintedTables = new HashMap<>();
+
+        Set<String> joinTbls = joinTblNames(join);
+
+        assert joinTbls.size() < 3;
+
+        for (RelHint hint : HintUtils.hints(join, HINTS.keySet(), HINTS.values())) {
+            Set<String> matchedTbls = hint.listOptions.isEmpty() ? joinTbls : new HashSet<>(hint.listOptions);
+
+            if (!hint.listOptions.isEmpty())
+                matchedTbls.retainAll(joinTbls);
+
+            if (matchedTbls.isEmpty())
+                continue;
+
+            HintDefinition curHintDef = HintDefinition.valueOf(hint.hintName);
+            boolean curHintIsDisable = !HINTS.containsKey(curHintDef);
+            boolean unableToProcess = false;
+
+            for (String tbl : joinTbls) {
+                Collection<HintDefinition> prevTblHints = hintedTables.get(tbl);
+
+                if (prevTblHints == null)
+                    continue;
+
+                int disableCnt = 0;
+
+                for (HintDefinition prevTblHint : prevTblHints) {
+                    boolean prevHintIsDisable = !HINTS.containsKey(prevTblHint);
+
+                    if (prevHintIsDisable)
+                        ++disableCnt;
+
+                    boolean opposite = prevTblHint != curHintDef && (!prevHintIsDisable || !curHintIsDisable)
+                        && (prevTblHint == HINTS.get(curHintDef) || curHintDef == HINTS.get(prevTblHint)
+                        || (!curHintIsDisable && HINTS.get(prevTblHint) != null));
+
+                    // Prohibited: disabling all join types, combinations of forcing and disabling same join type,
+                    // forcing of different join types.
+                    if (curHintIsDisable && disableCnt == HINTS.size() - 1 || opposite)
+                        unableToProcess = true;
+                }
+            }
+
+            if (unableToProcess) {
+                HintUtils.skippedHint(join, hint, "This join type is already disabled or forced to use before " +
+                    "by previous hints");
+
+                continue;
+            }
+
+            for (String tbl : matchedTbls) {
+                hintedTables.compute(tbl, (t, tblHints) -> {
+                    if (tblHints == null)
+                        tblHints = new ArrayList<>();
+
+                    tblHints.add(curHintDef);
+
+                    return tblHints;
+                });
+            }
+
+            // This join type is directyly disabled or other join type is forced.
+            if (curHintIsDisable && curHintDef == knownDisableHint || !curHintIsDisable && knownForceHint != curHintDef)
+                ruleDisabled = true;
+        }
+
+        return ruleDisabled;
+    }
+
+    /** */
+    private static String relDescription(Collection<String> tbls) {

Review Comment:
   Not used



##########
modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/MemoryQuotasIntegrationTest.java:
##########
@@ -240,7 +240,7 @@ public void testNestedLoopJoinNode() {
         for (int i = 0; i < 800; i++)
             sql("INSERT INTO tbl2 VALUES (?, ?)", i, new byte[1000]);
 
-        assertQuery("SELECT /*+ DISABLE_RULE('CorrelatedNestedLoopJoin', 'MergeJoinConverter') */ " +
+        assertQuery("SELECT /*+ NO_MERGE_JOIN, NO_CNL_JOIN */ " +

Review Comment:
   NO_MERGE_JOIN, NO_NL_JOIN -> CNL_JOIN (in all tests)



##########
modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/JoinTypeHintPlannerTest.java:
##########
@@ -0,0 +1,500 @@
+/*
+ * 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 java.util.Arrays;
+import java.util.function.Predicate;
+import org.apache.calcite.rel.RelNode;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+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.rel.AbstractIgniteJoin;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCorrelatedNestedLoopJoin;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteMergeJoin;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteNestedLoopJoin;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema;
+import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
+import org.apache.ignite.testframework.LogListener;
+import org.apache.ignite.testframework.junits.logger.GridTestLog4jLogger;
+import org.apache.logging.log4j.Level;
+import org.junit.Test;
+
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_NL_JOIN;
+
+/**
+ * Planner test for index hints.
+ */
+public class JoinTypeHintPlannerTest extends AbstractPlannerTest {
+    /** */
+    private static final String[] CORE_JOIN_REORDER_RULES = {"JoinCommuteRule", "JoinPushThroughJoinRule:left",
+        "JoinPushThroughJoinRule:right"};
+
+    /** */
+    private IgniteSchema schema;
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+
+        ((GridTestLog4jLogger)log).setLevel(Level.INFO);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void setup() {
+        super.setup();
+
+        TestTable[] tables = new TestTable[5];
+
+        for (int i = 0; i < tables.length; ++i) {
+            tables[i] = createTable("TBL" + (i + 1), (int)Math.min(1_000_000, Math.pow(10, i)),
+                IgniteDistributions.broadcast(), "ID", Integer.class, "V1", Integer.class, "V2", Integer.class,
+                "V3", Integer.class).addIndex(QueryUtils.PRIMARY_KEY_INDEX, 0);
+        }
+
+        schema = createSchema(tables);
+    }
+
+    /**
+     * Tests nested loop join is disabled by hinta.

Review Comment:
   Type: hinta



##########
modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/JoinTypeHintPlannerTest.java:
##########
@@ -0,0 +1,500 @@
+/*
+ * 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 java.util.Arrays;
+import java.util.function.Predicate;
+import org.apache.calcite.rel.RelNode;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+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.rel.AbstractIgniteJoin;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCorrelatedNestedLoopJoin;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteMergeJoin;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteNestedLoopJoin;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema;
+import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
+import org.apache.ignite.testframework.LogListener;
+import org.apache.ignite.testframework.junits.logger.GridTestLog4jLogger;
+import org.apache.logging.log4j.Level;
+import org.junit.Test;
+
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_NL_JOIN;
+
+/**
+ * Planner test for index hints.
+ */
+public class JoinTypeHintPlannerTest extends AbstractPlannerTest {
+    /** */
+    private static final String[] CORE_JOIN_REORDER_RULES = {"JoinCommuteRule", "JoinPushThroughJoinRule:left",
+        "JoinPushThroughJoinRule:right"};
+
+    /** */
+    private IgniteSchema schema;
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+
+        ((GridTestLog4jLogger)log).setLevel(Level.INFO);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void setup() {
+        super.setup();
+
+        TestTable[] tables = new TestTable[5];
+
+        for (int i = 0; i < tables.length; ++i) {
+            tables[i] = createTable("TBL" + (i + 1), (int)Math.min(1_000_000, Math.pow(10, i)),
+                IgniteDistributions.broadcast(), "ID", Integer.class, "V1", Integer.class, "V2", Integer.class,
+                "V3", Integer.class).addIndex(QueryUtils.PRIMARY_KEY_INDEX, 0);
+        }
+
+        schema = createSchema(tables);
+    }
+
+    /**
+     * Tests nested loop join is disabled by hinta.
+     */
+    @Test
+    public void testDisableNCLJoin() throws Exception {

Review Comment:
   testDisableNCLJoin -> testDisableCNLJoin



##########
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/AbstractIgniteJoinConverterRule.java:
##########
@@ -0,0 +1,192 @@
+/*
+ * 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.rule;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.volcano.RelSubset;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.hint.RelHint;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintUtils;
+
+import static org.apache.calcite.util.Util.last;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_NL_JOIN;
+
+/** */
+abstract class AbstractIgniteJoinConverterRule extends AbstractIgniteConverterRule<LogicalJoin> {
+    /** Known join type hints and the opposite hints. */
+    private static final EnumMap<HintDefinition, HintDefinition> HINTS = new EnumMap<>(HintDefinition.class);
+
+    /** Hint disabing this join type. */
+    private final HintDefinition knownDisableHint;
+
+    /** Hint forcing usage of this join type. */
+    private final HintDefinition knownForceHint;
+
+    static {
+        HINTS.put(NL_JOIN, NO_NL_JOIN);
+        HINTS.put(CNL_JOIN, NO_CNL_JOIN);
+        HINTS.put(MERGE_JOIN, NO_MERGE_JOIN);
+    }
+
+    /** */
+    protected AbstractIgniteJoinConverterRule(String descriptionPrefix, HintDefinition forceHint,
+        HintDefinition disableHint) {
+        super(LogicalJoin.class, descriptionPrefix);
+
+        assert forceHint != disableHint;
+        assert HINTS.containsKey(forceHint);
+        assert HINTS.containsValue(disableHint);
+        assert !HINTS.containsKey(disableHint);
+        assert !HINTS.containsValue(forceHint);
+
+        knownDisableHint = disableHint;
+        knownForceHint = forceHint;
+    }
+
+    /** {@inheritDoc} */
+    @Override public final boolean matches(RelOptRuleCall call) {
+        return super.matches(call) && matchesCall(call) && !disabledByHints(call.rel(0));
+    }
+
+    /** */
+    private boolean disabledByHints(LogicalJoin join) {
+        if (HintUtils.allRelHints(join).isEmpty())
+            return false;
+
+        boolean ruleDisabled = false;
+
+        Map<String, Collection<HintDefinition>> hintedTables = new HashMap<>();
+
+        Set<String> joinTbls = joinTblNames(join);
+
+        assert joinTbls.size() < 3;
+
+        for (RelHint hint : HintUtils.hints(join, HINTS.keySet(), HINTS.values())) {
+            Set<String> matchedTbls = hint.listOptions.isEmpty() ? joinTbls : new HashSet<>(hint.listOptions);
+
+            if (!hint.listOptions.isEmpty())
+                matchedTbls.retainAll(joinTbls);
+
+            if (matchedTbls.isEmpty())
+                continue;
+
+            HintDefinition curHintDef = HintDefinition.valueOf(hint.hintName);
+            boolean curHintIsDisable = !HINTS.containsKey(curHintDef);
+            boolean unableToProcess = false;
+
+            for (String tbl : joinTbls) {
+                Collection<HintDefinition> prevTblHints = hintedTables.get(tbl);
+
+                if (prevTblHints == null)
+                    continue;
+
+                int disableCnt = 0;
+
+                for (HintDefinition prevTblHint : prevTblHints) {
+                    boolean prevHintIsDisable = !HINTS.containsKey(prevTblHint);
+
+                    if (prevHintIsDisable)
+                        ++disableCnt;
+
+                    boolean opposite = prevTblHint != curHintDef && (!prevHintIsDisable || !curHintIsDisable)
+                        && (prevTblHint == HINTS.get(curHintDef) || curHintDef == HINTS.get(prevTblHint)
+                        || (!curHintIsDisable && HINTS.get(prevTblHint) != null));

Review Comment:
   Looks overcomplicated. At least `HINTS.get(prevTblHint) != null` can be simplified to `!prevHintIsDisable', perhaps something else.



##########
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/AbstractIgniteJoinConverterRule.java:
##########
@@ -0,0 +1,192 @@
+/*
+ * 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.rule;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.volcano.RelSubset;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.hint.RelHint;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintUtils;
+
+import static org.apache.calcite.util.Util.last;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_NL_JOIN;
+
+/** */
+abstract class AbstractIgniteJoinConverterRule extends AbstractIgniteConverterRule<LogicalJoin> {
+    /** Known join type hints and the opposite hints. */
+    private static final EnumMap<HintDefinition, HintDefinition> HINTS = new EnumMap<>(HintDefinition.class);
+
+    /** Hint disabing this join type. */
+    private final HintDefinition knownDisableHint;
+
+    /** Hint forcing usage of this join type. */
+    private final HintDefinition knownForceHint;
+
+    static {
+        HINTS.put(NL_JOIN, NO_NL_JOIN);
+        HINTS.put(CNL_JOIN, NO_CNL_JOIN);
+        HINTS.put(MERGE_JOIN, NO_MERGE_JOIN);
+    }
+
+    /** */
+    protected AbstractIgniteJoinConverterRule(String descriptionPrefix, HintDefinition forceHint,
+        HintDefinition disableHint) {
+        super(LogicalJoin.class, descriptionPrefix);
+
+        assert forceHint != disableHint;
+        assert HINTS.containsKey(forceHint);
+        assert HINTS.containsValue(disableHint);
+        assert !HINTS.containsKey(disableHint);
+        assert !HINTS.containsValue(forceHint);
+
+        knownDisableHint = disableHint;
+        knownForceHint = forceHint;
+    }
+
+    /** {@inheritDoc} */
+    @Override public final boolean matches(RelOptRuleCall call) {
+        return super.matches(call) && matchesCall(call) && !disabledByHints(call.rel(0));
+    }
+
+    /** */
+    private boolean disabledByHints(LogicalJoin join) {
+        if (HintUtils.allRelHints(join).isEmpty())
+            return false;
+
+        boolean ruleDisabled = false;
+
+        Map<String, Collection<HintDefinition>> hintedTables = new HashMap<>();
+
+        Set<String> joinTbls = joinTblNames(join);
+
+        assert joinTbls.size() < 3;
+
+        for (RelHint hint : HintUtils.hints(join, HINTS.keySet(), HINTS.values())) {
+            Set<String> matchedTbls = hint.listOptions.isEmpty() ? joinTbls : new HashSet<>(hint.listOptions);
+
+            if (!hint.listOptions.isEmpty())
+                matchedTbls.retainAll(joinTbls);
+
+            if (matchedTbls.isEmpty())
+                continue;
+
+            HintDefinition curHintDef = HintDefinition.valueOf(hint.hintName);
+            boolean curHintIsDisable = !HINTS.containsKey(curHintDef);
+            boolean unableToProcess = false;
+
+            for (String tbl : joinTbls) {
+                Collection<HintDefinition> prevTblHints = hintedTables.get(tbl);
+
+                if (prevTblHints == null)
+                    continue;
+
+                int disableCnt = 0;
+
+                for (HintDefinition prevTblHint : prevTblHints) {
+                    boolean prevHintIsDisable = !HINTS.containsKey(prevTblHint);
+
+                    if (prevHintIsDisable)
+                        ++disableCnt;
+
+                    boolean opposite = prevTblHint != curHintDef && (!prevHintIsDisable || !curHintIsDisable)
+                        && (prevTblHint == HINTS.get(curHintDef) || curHintDef == HINTS.get(prevTblHint)
+                        || (!curHintIsDisable && HINTS.get(prevTblHint) != null));
+
+                    // Prohibited: disabling all join types, combinations of forcing and disabling same join type,
+                    // forcing of different join types.
+                    if (curHintIsDisable && disableCnt == HINTS.size() - 1 || opposite)
+                        unableToProcess = true;
+                }
+            }
+
+            if (unableToProcess) {
+                HintUtils.skippedHint(join, hint, "This join type is already disabled or forced to use before " +
+                    "by previous hints");
+
+                continue;
+            }
+
+            for (String tbl : matchedTbls) {
+                hintedTables.compute(tbl, (t, tblHints) -> {
+                    if (tblHints == null)
+                        tblHints = new ArrayList<>();
+
+                    tblHints.add(curHintDef);
+
+                    return tblHints;
+                });

Review Comment:
   Can be simplified to `hintedTables.computeIfAbsent(tbl, t -> new ArrayList<>()).add(curHintDef);`



##########
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/AbstractIgniteJoinConverterRule.java:
##########
@@ -0,0 +1,192 @@
+/*
+ * 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.rule;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.volcano.RelSubset;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.hint.RelHint;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintUtils;
+
+import static org.apache.calcite.util.Util.last;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_NL_JOIN;
+
+/** */
+abstract class AbstractIgniteJoinConverterRule extends AbstractIgniteConverterRule<LogicalJoin> {
+    /** Known join type hints and the opposite hints. */
+    private static final EnumMap<HintDefinition, HintDefinition> HINTS = new EnumMap<>(HintDefinition.class);
+
+    /** Hint disabing this join type. */
+    private final HintDefinition knownDisableHint;
+
+    /** Hint forcing usage of this join type. */
+    private final HintDefinition knownForceHint;
+
+    static {
+        HINTS.put(NL_JOIN, NO_NL_JOIN);
+        HINTS.put(CNL_JOIN, NO_CNL_JOIN);
+        HINTS.put(MERGE_JOIN, NO_MERGE_JOIN);
+    }
+
+    /** */
+    protected AbstractIgniteJoinConverterRule(String descriptionPrefix, HintDefinition forceHint,
+        HintDefinition disableHint) {
+        super(LogicalJoin.class, descriptionPrefix);
+
+        assert forceHint != disableHint;
+        assert HINTS.containsKey(forceHint);
+        assert HINTS.containsValue(disableHint);
+        assert !HINTS.containsKey(disableHint);
+        assert !HINTS.containsValue(forceHint);
+
+        knownDisableHint = disableHint;
+        knownForceHint = forceHint;
+    }
+
+    /** {@inheritDoc} */
+    @Override public final boolean matches(RelOptRuleCall call) {
+        return super.matches(call) && matchesCall(call) && !disabledByHints(call.rel(0));
+    }
+
+    /** */
+    private boolean disabledByHints(LogicalJoin join) {
+        if (HintUtils.allRelHints(join).isEmpty())
+            return false;
+
+        boolean ruleDisabled = false;
+
+        Map<String, Collection<HintDefinition>> hintedTables = new HashMap<>();
+
+        Set<String> joinTbls = joinTblNames(join);
+
+        assert joinTbls.size() < 3;
+
+        for (RelHint hint : HintUtils.hints(join, HINTS.keySet(), HINTS.values())) {
+            Set<String> matchedTbls = hint.listOptions.isEmpty() ? joinTbls : new HashSet<>(hint.listOptions);
+
+            if (!hint.listOptions.isEmpty())
+                matchedTbls.retainAll(joinTbls);
+
+            if (matchedTbls.isEmpty())
+                continue;
+
+            HintDefinition curHintDef = HintDefinition.valueOf(hint.hintName);
+            boolean curHintIsDisable = !HINTS.containsKey(curHintDef);
+            boolean unableToProcess = false;
+
+            for (String tbl : joinTbls) {
+                Collection<HintDefinition> prevTblHints = hintedTables.get(tbl);
+
+                if (prevTblHints == null)
+                    continue;
+
+                int disableCnt = 0;
+
+                for (HintDefinition prevTblHint : prevTblHints) {
+                    boolean prevHintIsDisable = !HINTS.containsKey(prevTblHint);
+
+                    if (prevHintIsDisable)
+                        ++disableCnt;
+
+                    boolean opposite = prevTblHint != curHintDef && (!prevHintIsDisable || !curHintIsDisable)
+                        && (prevTblHint == HINTS.get(curHintDef) || curHintDef == HINTS.get(prevTblHint)
+                        || (!curHintIsDisable && HINTS.get(prevTblHint) != null));
+
+                    // Prohibited: disabling all join types, combinations of forcing and disabling same join type,
+                    // forcing of different join types.
+                    if (curHintIsDisable && disableCnt == HINTS.size() - 1 || opposite)
+                        unableToProcess = true;
+                }
+            }
+
+            if (unableToProcess) {
+                HintUtils.skippedHint(join, hint, "This join type is already disabled or forced to use before " +
+                    "by previous hints");
+
+                continue;
+            }
+
+            for (String tbl : matchedTbls) {
+                hintedTables.compute(tbl, (t, tblHints) -> {
+                    if (tblHints == null)
+                        tblHints = new ArrayList<>();
+
+                    tblHints.add(curHintDef);
+
+                    return tblHints;
+                });
+            }
+
+            // This join type is directyly disabled or other join type is forced.
+            if (curHintIsDisable && curHintDef == knownDisableHint || !curHintIsDisable && knownForceHint != curHintDef)
+                ruleDisabled = true;
+        }
+
+        return ruleDisabled;
+    }
+
+    /** */
+    private static String relDescription(Collection<String> tbls) {
+        return "Join tables " + tbls.stream().sorted().collect(Collectors.joining(","));
+    }
+
+    /** */
+    protected static Set<String> joinTblNames(Join join) {
+        Set<String> res = new LinkedHashSet<>();
+
+        for (RelNode in : join.getInputs()) {
+            if (in instanceof RelSubset) {
+                for (RelNode sub : ((RelSubset)in).getRels()) {
+                    if (sub instanceof TableScan)
+                        res.add(last(((TableScan)sub).getTable().getQualifiedName()));

Review Comment:
   It's better to use alias instead of table name. Do we have information about aliases on this phase?
   How we can force join type only for subqueries?
   For example `SELECT * FROM (SELECT ... FROM t1 JOIN t2 ON (...)) AS alias1 JOIN t2 AS alias2  ON (..)`. How we can set hint for join between alias1 to alias2?



##########
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/AbstractIgniteJoinConverterRule.java:
##########
@@ -0,0 +1,192 @@
+/*
+ * 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.rule;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.volcano.RelSubset;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.hint.RelHint;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintUtils;
+
+import static org.apache.calcite.util.Util.last;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_NL_JOIN;
+
+/** */
+abstract class AbstractIgniteJoinConverterRule extends AbstractIgniteConverterRule<LogicalJoin> {
+    /** Known join type hints and the opposite hints. */
+    private static final EnumMap<HintDefinition, HintDefinition> HINTS = new EnumMap<>(HintDefinition.class);
+
+    /** Hint disabing this join type. */
+    private final HintDefinition knownDisableHint;
+
+    /** Hint forcing usage of this join type. */
+    private final HintDefinition knownForceHint;
+
+    static {
+        HINTS.put(NL_JOIN, NO_NL_JOIN);
+        HINTS.put(CNL_JOIN, NO_CNL_JOIN);
+        HINTS.put(MERGE_JOIN, NO_MERGE_JOIN);
+    }
+
+    /** */
+    protected AbstractIgniteJoinConverterRule(String descriptionPrefix, HintDefinition forceHint,
+        HintDefinition disableHint) {
+        super(LogicalJoin.class, descriptionPrefix);
+
+        assert forceHint != disableHint;
+        assert HINTS.containsKey(forceHint);
+        assert HINTS.containsValue(disableHint);
+        assert !HINTS.containsKey(disableHint);
+        assert !HINTS.containsValue(forceHint);
+
+        knownDisableHint = disableHint;
+        knownForceHint = forceHint;
+    }
+
+    /** {@inheritDoc} */
+    @Override public final boolean matches(RelOptRuleCall call) {
+        return super.matches(call) && matchesCall(call) && !disabledByHints(call.rel(0));
+    }
+
+    /** */
+    private boolean disabledByHints(LogicalJoin join) {
+        if (HintUtils.allRelHints(join).isEmpty())
+            return false;
+
+        boolean ruleDisabled = false;
+
+        Map<String, Collection<HintDefinition>> hintedTables = new HashMap<>();
+
+        Set<String> joinTbls = joinTblNames(join);
+
+        assert joinTbls.size() < 3;
+
+        for (RelHint hint : HintUtils.hints(join, HINTS.keySet(), HINTS.values())) {
+            Set<String> matchedTbls = hint.listOptions.isEmpty() ? joinTbls : new HashSet<>(hint.listOptions);
+
+            if (!hint.listOptions.isEmpty())
+                matchedTbls.retainAll(joinTbls);
+
+            if (matchedTbls.isEmpty())
+                continue;
+
+            HintDefinition curHintDef = HintDefinition.valueOf(hint.hintName);
+            boolean curHintIsDisable = !HINTS.containsKey(curHintDef);
+            boolean unableToProcess = false;
+
+            for (String tbl : joinTbls) {
+                Collection<HintDefinition> prevTblHints = hintedTables.get(tbl);
+
+                if (prevTblHints == null)
+                    continue;
+
+                int disableCnt = 0;
+
+                for (HintDefinition prevTblHint : prevTblHints) {
+                    boolean prevHintIsDisable = !HINTS.containsKey(prevTblHint);
+
+                    if (prevHintIsDisable)
+                        ++disableCnt;
+
+                    boolean opposite = prevTblHint != curHintDef && (!prevHintIsDisable || !curHintIsDisable)
+                        && (prevTblHint == HINTS.get(curHintDef) || curHintDef == HINTS.get(prevTblHint)
+                        || (!curHintIsDisable && HINTS.get(prevTblHint) != null));
+
+                    // Prohibited: disabling all join types, combinations of forcing and disabling same join type,
+                    // forcing of different join types.
+                    if (curHintIsDisable && disableCnt == HINTS.size() - 1 || opposite)
+                        unableToProcess = true;
+                }
+            }
+
+            if (unableToProcess) {
+                HintUtils.skippedHint(join, hint, "This join type is already disabled or forced to use before " +
+                    "by previous hints");
+
+                continue;
+            }
+
+            for (String tbl : matchedTbls) {
+                hintedTables.compute(tbl, (t, tblHints) -> {
+                    if (tblHints == null)
+                        tblHints = new ArrayList<>();
+
+                    tblHints.add(curHintDef);
+
+                    return tblHints;
+                });
+            }
+
+            // This join type is directyly disabled or other join type is forced.
+            if (curHintIsDisable && curHintDef == knownDisableHint || !curHintIsDisable && knownForceHint != curHintDef)
+                ruleDisabled = true;
+        }
+
+        return ruleDisabled;
+    }
+
+    /** */
+    private static String relDescription(Collection<String> tbls) {
+        return "Join tables " + tbls.stream().sorted().collect(Collectors.joining(","));
+    }
+
+    /** */
+    protected static Set<String> joinTblNames(Join join) {
+        Set<String> res = new LinkedHashSet<>();
+
+        for (RelNode in : join.getInputs()) {
+            if (in instanceof RelSubset) {
+                for (RelNode sub : ((RelSubset)in).getRels()) {

Review Comment:
   ((RelSubset)in).getOriginal()



##########
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/AbstractIgniteJoinConverterRule.java:
##########
@@ -0,0 +1,192 @@
+/*
+ * 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.rule;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.volcano.RelSubset;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.hint.RelHint;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintUtils;
+
+import static org.apache.calcite.util.Util.last;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_CNL_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_MERGE_JOIN;
+import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_NL_JOIN;
+
+/** */
+abstract class AbstractIgniteJoinConverterRule extends AbstractIgniteConverterRule<LogicalJoin> {
+    /** Known join type hints and the opposite hints. */
+    private static final EnumMap<HintDefinition, HintDefinition> HINTS = new EnumMap<>(HintDefinition.class);
+
+    /** Hint disabing this join type. */
+    private final HintDefinition knownDisableHint;
+
+    /** Hint forcing usage of this join type. */
+    private final HintDefinition knownForceHint;
+
+    static {
+        HINTS.put(NL_JOIN, NO_NL_JOIN);
+        HINTS.put(CNL_JOIN, NO_CNL_JOIN);
+        HINTS.put(MERGE_JOIN, NO_MERGE_JOIN);
+    }
+
+    /** */
+    protected AbstractIgniteJoinConverterRule(String descriptionPrefix, HintDefinition forceHint,
+        HintDefinition disableHint) {
+        super(LogicalJoin.class, descriptionPrefix);
+
+        assert forceHint != disableHint;
+        assert HINTS.containsKey(forceHint);
+        assert HINTS.containsValue(disableHint);
+        assert !HINTS.containsKey(disableHint);
+        assert !HINTS.containsValue(forceHint);
+
+        knownDisableHint = disableHint;
+        knownForceHint = forceHint;
+    }
+
+    /** {@inheritDoc} */
+    @Override public final boolean matches(RelOptRuleCall call) {
+        return super.matches(call) && matchesCall(call) && !disabledByHints(call.rel(0));
+    }
+
+    /** */
+    private boolean disabledByHints(LogicalJoin join) {
+        if (HintUtils.allRelHints(join).isEmpty())
+            return false;
+
+        boolean ruleDisabled = false;
+
+        Map<String, Collection<HintDefinition>> hintedTables = new HashMap<>();
+
+        Set<String> joinTbls = joinTblNames(join);
+
+        assert joinTbls.size() < 3;
+
+        for (RelHint hint : HintUtils.hints(join, HINTS.keySet(), HINTS.values())) {
+            Set<String> matchedTbls = hint.listOptions.isEmpty() ? joinTbls : new HashSet<>(hint.listOptions);
+
+            if (!hint.listOptions.isEmpty())
+                matchedTbls.retainAll(joinTbls);
+
+            if (matchedTbls.isEmpty())
+                continue;
+
+            HintDefinition curHintDef = HintDefinition.valueOf(hint.hintName);
+            boolean curHintIsDisable = !HINTS.containsKey(curHintDef);
+            boolean unableToProcess = false;
+
+            for (String tbl : joinTbls) {
+                Collection<HintDefinition> prevTblHints = hintedTables.get(tbl);
+
+                if (prevTblHints == null)
+                    continue;
+
+                int disableCnt = 0;
+
+                for (HintDefinition prevTblHint : prevTblHints) {
+                    boolean prevHintIsDisable = !HINTS.containsKey(prevTblHint);
+
+                    if (prevHintIsDisable)
+                        ++disableCnt;
+
+                    boolean opposite = prevTblHint != curHintDef && (!prevHintIsDisable || !curHintIsDisable)
+                        && (prevTblHint == HINTS.get(curHintDef) || curHintDef == HINTS.get(prevTblHint)
+                        || (!curHintIsDisable && HINTS.get(prevTblHint) != null));
+
+                    // Prohibited: disabling all join types, combinations of forcing and disabling same join type,
+                    // forcing of different join types.
+                    if (curHintIsDisable && disableCnt == HINTS.size() - 1 || opposite)
+                        unableToProcess = true;
+                }
+            }
+
+            if (unableToProcess) {
+                HintUtils.skippedHint(join, hint, "This join type is already disabled or forced to use before " +
+                    "by previous hints");
+
+                continue;
+            }
+
+            for (String tbl : matchedTbls) {
+                hintedTables.compute(tbl, (t, tblHints) -> {
+                    if (tblHints == null)
+                        tblHints = new ArrayList<>();
+
+                    tblHints.add(curHintDef);
+
+                    return tblHints;
+                });
+            }
+
+            // This join type is directyly disabled or other join type is forced.
+            if (curHintIsDisable && curHintDef == knownDisableHint || !curHintIsDisable && knownForceHint != curHintDef)
+                ruleDisabled = true;
+        }
+
+        return ruleDisabled;
+    }
+
+    /** */
+    private static String relDescription(Collection<String> tbls) {
+        return "Join tables " + tbls.stream().sorted().collect(Collectors.joining(","));
+    }
+
+    /** */
+    protected static Set<String> joinTblNames(Join join) {
+        Set<String> res = new LinkedHashSet<>();
+
+        for (RelNode in : join.getInputs()) {
+            if (in instanceof RelSubset) {
+                for (RelNode sub : ((RelSubset)in).getRels()) {
+                    if (sub instanceof TableScan)
+                        res.add(last(((TableScan)sub).getTable().getQualifiedName()));
+                }
+            }
+            else if (in instanceof TableScan) {

Review Comment:
   Redundant braces



##########
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintUtils.java:
##########
@@ -70,6 +70,14 @@ public static List<RelHint> hints(RelNode rel, HintDefinition... hintDefs) {
             .apply(filterHints(rel, allRelHints(rel), Arrays.asList(hintDefs)), rel);
     }
 
+    /**
+     * @return Hints filtered with {@code hintDefs} and suitable for {@code rel}.
+     * @see HintStrategyTable#apply(List, RelNode)
+     */
+    public static List<RelHint> hints(RelNode rel, Collection<HintDefinition>... hintDefs) {

Review Comment:
   Strange contract for utility method. It's used only once, and it's better to just this usage.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org