You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by da...@apache.org on 2019/11/12 02:21:48 UTC
[calcite] branch master updated: [CALCITE-482] Implement sql and
planner hints
This is an automated email from the ASF dual-hosted git repository.
danny0405 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/master by this push:
new 52bc246 [CALCITE-482] Implement sql and planner hints
52bc246 is described below
commit 52bc246d5e07510689635f407f4d9dc494b0af4d
Author: yuzhao.cyz <yu...@alibaba-inc.com>
AuthorDate: Sun Aug 4 15:52:20 2019 +0800
[CALCITE-482] Implement sql and planner hints
* Define a ORACLE stype hint syntax for both query(after SELECT keyword) hint and table(after table reference) hint
* The hints would be translated to RelHint instances and propagated
right after the sql node was converted to relational expression
* Introduce HintStrategyTable and HintStrategy to customize the hints
propagation
* Add a new interface RelOptRuleCall.transformTo(RelNode, Map, BiFunction) to
make the hints copy strategy overridable
* Cache the hint strategies into RelOptCluster so that user can query the
strategies during rule planning
---
core/src/main/codegen/templates/Parser.jj | 141 +++++-
.../adapter/enumerable/EnumerableProject.java | 11 -
.../calcite/adapter/enumerable/EnumerableRel.java | 11 -
.../org/apache/calcite/adapter/jdbc/JdbcTable.java | 2 +-
.../org/apache/calcite/plan/RelOptCluster.java | 31 +-
.../org/apache/calcite/plan/RelOptRuleCall.java | 37 +-
.../java/org/apache/calcite/plan/RelOptUtil.java | 26 ++
.../org/apache/calcite/plan/hep/HepRuleCall.java | 7 +-
.../calcite/plan/volcano/VolcanoRuleCall.java | 5 +-
.../calcite/prepare/CalciteMaterializer.java | 2 +-
.../apache/calcite/prepare/CalcitePrepareImpl.java | 2 +-
.../main/java/org/apache/calcite/rel/RelRoot.java | 21 +-
.../java/org/apache/calcite/rel/core/Join.java | 42 +-
.../java/org/apache/calcite/rel/core/Project.java | 28 +-
.../org/apache/calcite/rel/core/TableScan.java | 24 +-
.../calcite/rel/hint/ExplicitHintMatcher.java | 44 ++
.../calcite/rel/hint/ExplicitHintStrategy.java | 53 +++
.../apache/calcite/rel/hint/HintStrategies.java | 63 +++
.../org/apache/calcite/rel/hint/HintStrategy.java | 48 ++
.../calcite/rel/hint/HintStrategyCascade.java | 61 +++
.../apache/calcite/rel/hint/HintStrategyTable.java | 150 ++++++
.../java/org/apache/calcite/rel/hint/Hintable.java | 90 ++++
.../calcite/rel/hint/NodeTypeHintStrategy.java | 82 ++++
.../java/org/apache/calcite/rel/hint/RelHint.java | 112 +++++
.../org/apache/calcite/rel/hint/package-info.java | 97 ++++
.../apache/calcite/rel/logical/LogicalJoin.java | 25 +-
.../apache/calcite/rel/logical/LogicalProject.java | 29 +-
.../calcite/rel/logical/LogicalTableScan.java | 35 +-
.../calcite/rel/rel2sql/RelToSqlConverter.java | 10 +-
.../apache/calcite/rel/rel2sql/SqlImplementor.java | 2 +-
.../main/java/org/apache/calcite/sql/SqlHint.java | 163 +++++++
.../main/java/org/apache/calcite/sql/SqlKind.java | 10 +
.../calcite/sql/SqlProcedureCallOperator.java | 1 +
.../java/org/apache/calcite/sql/SqlSelect.java | 18 +-
.../org/apache/calcite/sql/SqlSelectOperator.java | 15 +-
.../java/org/apache/calcite/sql/SqlTableRef.java | 72 +++
.../main/java/org/apache/calcite/sql/SqlUtil.java | 49 ++
.../calcite/sql/dialect/MysqlSqlDialect.java | 3 +-
.../calcite/sql/validate/IdentifierNamespace.java | 11 +-
.../calcite/sql/validate/SqlValidatorImpl.java | 44 +-
.../calcite/sql/validate/SqlValidatorUtil.java | 2 +-
.../sql2rel/RelStructuredTypeFlattener.java | 4 +
.../apache/calcite/sql2rel/SqlToRelConverter.java | 211 ++++++++-
.../apache/calcite/sql/parser/SqlParserTest.java | 116 +++++
.../java/org/apache/calcite/test/CalciteSuite.java | 1 +
.../org/apache/calcite/test/MockRelOptPlanner.java | 4 +-
.../apache/calcite/test/SqlHintsConverterTest.java | 516 +++++++++++++++++++++
.../org/apache/calcite/test/SqlToRelTestBase.java | 7 +-
.../calcite/test/catalog/MockCatalogReader.java | 4 +-
.../org/apache/calcite/sql/ddl/SqlDdlNodes.java | 2 +-
site/_docs/reference.md | 15 +-
51 files changed, 2458 insertions(+), 101 deletions(-)
diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj
index 3f80693..9e14098 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -58,6 +58,7 @@ import org.apache.calcite.sql.SqlExplainFormat;
import org.apache.calcite.sql.SqlExplainLevel;
import org.apache.calcite.sql.SqlFunction;
import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlHint;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlInsert;
import org.apache.calcite.sql.SqlInsertKeyword;
@@ -90,6 +91,7 @@ import org.apache.calcite.sql.SqlSelect;
import org.apache.calcite.sql.SqlSelectKeyword;
import org.apache.calcite.sql.SqlSetOption;
import org.apache.calcite.sql.SqlSnapshot;
+import org.apache.calcite.sql.SqlTableRef;
import org.apache.calcite.sql.SqlTimeLiteral;
import org.apache.calcite.sql.SqlTimestampLiteral;
import org.apache.calcite.sql.SqlTypeNameSpec;
@@ -1056,6 +1058,115 @@ SqlNode SqlStmtEof() :
<#include "/@includes/"+file />
</#list>
+SqlNodeList ParenthesizedKeyValueOptionCommaList() :
+{
+ final Span s;
+ final List<SqlNode> list = new ArrayList<SqlNode>();
+}
+{
+ { s = span(); }
+ <LPAREN>
+ [
+ KeyValueOption(list)
+ (
+ <COMMA>
+ KeyValueOption(list)
+ )*
+ ]
+ <RPAREN> {
+ return new SqlNodeList(list, s.end(this));
+ }
+}
+
+/**
+* Parses an option with format key=val whose key is a simple identifier
+* and value is a string literal.
+*/
+void KeyValueOption(List<SqlNode> list) :
+{
+ final SqlIdentifier id;
+ final SqlNode value;
+}
+{
+ id = SimpleIdentifier()
+ <EQ>
+ value = StringLiteral() {
+ list.add(id);
+ list.add(value);
+ }
+}
+
+void CommaSepatatedSqlHints(List<SqlNode> hints) :
+{
+ SqlIdentifier hintName;
+ SqlNodeList hintOptions;
+}
+{
+ hintName = SimpleIdentifier()
+ ( LOOKAHEAD(5)
+ hintOptions = ParenthesizedKeyValueOptionCommaList() {
+ hints.add(new SqlHint(Span.of(hintOptions).end(this), hintName, hintOptions));
+ }
+ |
+ hintOptions = ParenthesizedSimpleIdentifierList() {
+ hints.add(new SqlHint(Span.of(hintOptions).end(this), hintName, hintOptions));
+ }
+ |
+ {
+ hintOptions = SqlNodeList.EMPTY;
+ hints.add(new SqlHint(Span.of(hintOptions).end(this), hintName, hintOptions));
+ }
+ )
+ (
+ <COMMA>
+ hintName = SimpleIdentifier()
+ (
+ LOOKAHEAD(5)
+ hintOptions = ParenthesizedKeyValueOptionCommaList() {
+ hints.add(new SqlHint(Span.of(hintOptions).end(this), hintName, hintOptions));
+ }
+ |
+ hintOptions = ParenthesizedSimpleIdentifierList() {
+ hints.add(new SqlHint(Span.of(hintOptions).end(this), hintName, hintOptions));
+ }
+ |
+ {
+ hintOptions = SqlNodeList.EMPTY;
+ hints.add(new SqlHint(Span.of(hintOptions).end(this), hintName, hintOptions));
+ }
+ )
+ )*
+}
+
+/**
+ * Parses a table reference with optional hints.
+ */
+SqlNode TableRefWithHintsOpt() :
+{
+ SqlNode tableRef;
+ SqlNodeList hintList;
+ final List<SqlNode> hints = new ArrayList<SqlNode>();
+ final Span s;
+}
+{
+ { s = span(); }
+ tableRef = CompoundIdentifier()
+ [
+ LOOKAHEAD(2)
+ <HINT_BEG>
+ CommaSepatatedSqlHints(hints)
+ <COMMENT_END>
+ {
+ hintList = new SqlNodeList(hints, s.addAll(hints).end(this));
+ tableRef = new SqlTableRef(Span.of(tableRef, hintList).pos(),
+ (SqlIdentifier) tableRef, hintList);
+ }
+ ]
+ {
+ return tableRef;
+ }
+}
+
/**
* Parses a leaf SELECT expression without ORDER BY.
*/
@@ -1069,6 +1180,7 @@ SqlSelect SqlSelect() :
final SqlNodeList groupBy;
final SqlNode having;
final SqlNodeList windowDecls;
+ final List<SqlNode> hints = new ArrayList<SqlNode>();
final Span s;
}
{
@@ -1076,6 +1188,11 @@ SqlSelect SqlSelect() :
{
s = span();
}
+ [
+ <HINT_BEG>
+ CommaSepatatedSqlHints(hints)
+ <COMMENT_END>
+ ]
SqlSelectKeywords(keywords)
(
<STREAM> {
@@ -1112,7 +1229,8 @@ SqlSelect SqlSelect() :
{
return new SqlSelect(s.end(this), keywordList,
new SqlNodeList(selectList, Span.of(selectList).pos()),
- fromClause, where, groupBy, having, windowDecls, null, null, null);
+ fromClause, where, groupBy, having, windowDecls, null, null, null,
+ new SqlNodeList(hints, getPos()));
}
}
@@ -1360,7 +1478,7 @@ SqlNode SqlInsert() :
SqlInsertKeywords(keywords) {
keywordList = new SqlNodeList(keywords, s.addAll(keywords).pos());
}
- <INTO> table = CompoundIdentifier()
+ <INTO> table = TableRefWithHintsOpt()
[
LOOKAHEAD(5)
[ <EXTEND> ]
@@ -1409,7 +1527,7 @@ SqlNode SqlDelete() :
<DELETE> {
s = span();
}
- <FROM> table = CompoundIdentifier()
+ <FROM> table = TableRefWithHintsOpt()
[
[ <EXTEND> ]
extendList = ExtendList() {
@@ -1441,7 +1559,7 @@ SqlNode SqlUpdate() :
}
{
<UPDATE> { s = span(); }
- table = CompoundIdentifier() {
+ table = TableRefWithHintsOpt() {
targetColumnList = new SqlNodeList(s.pos());
sourceExpressionList = new SqlNodeList(s.pos());
}
@@ -1493,7 +1611,7 @@ SqlNode SqlMerge() :
final Span s;
}
{
- <MERGE> { s = span(); } <INTO> table = CompoundIdentifier()
+ <MERGE> { s = span(); } <INTO> table = TableRefWithHintsOpt()
[
[ <EXTEND> ]
extendList = ExtendList() {
@@ -1891,7 +2009,7 @@ SqlNode TableRef2(boolean lateral) :
{
(
LOOKAHEAD(2)
- tableRef = CompoundIdentifier()
+ tableRef = TableRefWithHintsOpt()
[
[ <EXTEND> ]
extendList = ExtendList() {
@@ -3631,6 +3749,7 @@ SqlCall SequenceExpression() :
(
<NEXT> { f = SqlStdOperatorTable.NEXT_VALUE; s = span(); }
|
+ LOOKAHEAD(3)
<CURRENT> { f = SqlStdOperatorTable.CURRENT_VALUE; s = span(); }
)
<VALUE> <FOR> sequenceRef = CompoundIdentifier() {
@@ -7441,6 +7560,12 @@ of the 'normal states'.
/* COMMENTS */
+<DEFAULT, DQID, BTID> TOKEN :
+{
+ < HINT_BEG: "/*+" ~["/"] >
+| < COMMENT_END: "*/" >
+}
+
<DEFAULT, DQID, BTID> MORE :
{
<"/**" ~["/"]> { pushState(); } : IN_FORMAL_COMMENT
@@ -7459,13 +7584,13 @@ of the 'normal states'.
<IN_FORMAL_COMMENT>
SPECIAL_TOKEN :
{
- <FORMAL_COMMENT: "*/" > { popState(); }
+ <FORMAL_COMMENT: <COMMENT_END> > { popState(); }
}
<IN_MULTI_LINE_COMMENT>
SPECIAL_TOKEN :
{
- <MULTI_LINE_COMMENT: "*/" > { popState(); }
+ <MULTI_LINE_COMMENT: <COMMENT_END> > { popState(); }
}
<IN_FORMAL_COMMENT,IN_MULTI_LINE_COMMENT>
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProject.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProject.java
index 9418a20..c37dfe6 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProject.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProject.java
@@ -25,8 +25,6 @@ import org.apache.calcite.rel.metadata.RelMdCollation;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.rex.RexUtil;
-import org.apache.calcite.sql.validate.SqlValidatorUtil;
import org.apache.calcite.util.Util;
import java.util.List;
@@ -76,15 +74,6 @@ public class EnumerableProject extends Project implements EnumerableRel {
return new EnumerableProject(cluster, traitSet, input, projects, rowType);
}
- static RelNode create(RelNode child, List<? extends RexNode> projects,
- List<String> fieldNames) {
- final RelOptCluster cluster = child.getCluster();
- final RelDataType rowType =
- RexUtil.createStructType(cluster.getTypeFactory(), projects,
- fieldNames, SqlValidatorUtil.F_SUGGESTER);
- return create(child, projects, rowType);
- }
-
public EnumerableProject copy(RelTraitSet traitSet, RelNode input,
List<RexNode> projects, RelDataType rowType) {
return new EnumerableProject(getCluster(), traitSet, input,
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRel.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRel.java
index fd80af7..346eb9c 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRel.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRel.java
@@ -18,9 +18,6 @@ package org.apache.calcite.adapter.enumerable;
import org.apache.calcite.linq4j.tree.BlockStatement;
import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.core.RelFactories;
-
-import com.google.common.base.Preconditions;
/**
* A relational expression of one of the
@@ -29,14 +26,6 @@ import com.google.common.base.Preconditions;
*/
public interface EnumerableRel
extends RelNode {
- RelFactories.FilterFactory FILTER_FACTORY =
- (input, condition, variablesSet) -> {
- Preconditions.checkArgument(variablesSet.isEmpty(),
- "EnumerableFilter does not allow variables");
- return EnumerableFilter.create(input, condition);
- };
-
- RelFactories.ProjectFactory PROJECT_FACTORY = EnumerableProject::create;
//~ Methods ----------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcTable.java b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcTable.java
index c21d8b0..e14de38 100644
--- a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcTable.java
@@ -147,7 +147,7 @@ public class JdbcTable extends AbstractQueryableTable
SqlParserPos.ZERO);
SqlSelect node =
new SqlSelect(SqlParserPos.ZERO, SqlNodeList.EMPTY, selectList,
- tableName(), null, null, null, null, null, null, null);
+ tableName(), null, null, null, null, null, null, null, null);
final SqlPrettyWriter writer = new SqlPrettyWriter(jdbcSchema.dialect);
node.unparse(writer, 0, 0);
return writer.toSqlString();
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptCluster.java b/core/src/main/java/org/apache/calcite/plan/RelOptCluster.java
index 79dcc5c..d68a36c 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptCluster.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptCluster.java
@@ -18,6 +18,7 @@ package org.apache.calcite.plan;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.CorrelationId;
+import org.apache.calcite.rel.hint.HintStrategyTable;
import org.apache.calcite.rel.metadata.DefaultRelMetadataProvider;
import org.apache.calcite.rel.metadata.JaninoRelMetadataProvider;
import org.apache.calcite.rel.metadata.MetadataFactory;
@@ -50,6 +51,7 @@ public class RelOptCluster {
private final RexBuilder rexBuilder;
private RelMetadataProvider metadataProvider;
private MetadataFactory metadataFactory;
+ private HintStrategyTable hintStrategies;
private final RelTraitSet emptyTraitSet;
private RelMetadataQuery mq;
private Supplier<RelMetadataQuery> mqSupplier;
@@ -179,7 +181,6 @@ public class RelOptCluster {
}
/**
- * Returns the RelMetadataQuery supplier
* @return The supplier of RelMetadataQuery
*/
public Supplier<RelMetadataQuery> getMetadataQuerySupplier() {
@@ -195,6 +196,34 @@ public class RelOptCluster {
}
/**
+ * Setup the hint propagation strategies to be used during rule planning.
+ *
+ * <p>Use <code>RelOptNode.getCluster().getHintStrategies()</code> to fetch
+ * the hint strategies.
+ *
+ * <p>Note that this method is only for internal use, the cluster {@code hintStrategies}
+ * would be always set up with the instance configured by
+ * {@link org.apache.calcite.sql2rel.SqlToRelConverter.ConfigBuilder}.
+ *
+ * @param hintStrategies The specified hint strategies to override the default one(empty)
+ */
+ public RelOptCluster withHintStrategies(HintStrategyTable hintStrategies) {
+ Objects.requireNonNull(hintStrategies);
+ this.hintStrategies = hintStrategies;
+ return this;
+ }
+
+ /**
+ * Returns the hint strategies of this cluster. It is immutable during the whole planning phrase.
+ */
+ public HintStrategyTable getHintStrategies() {
+ if (this.hintStrategies == null) {
+ this.hintStrategies = HintStrategyTable.EMPTY;
+ }
+ return this.hintStrategies;
+ }
+
+ /**
* Constructs a new id for a correlating variable. It is unique within the
* whole query.
*/
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptRuleCall.java b/core/src/main/java/org/apache/calcite/plan/RelOptRuleCall.java
index 3de7571..9a3db76 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptRuleCall.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptRuleCall.java
@@ -18,6 +18,7 @@ package org.apache.calcite.plan;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.Filter;
+import org.apache.calcite.rel.hint.Hintable;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.calcite.tools.RelBuilder;
import org.apache.calcite.util.trace.CalciteTrace;
@@ -30,6 +31,7 @@ import org.slf4j.Logger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.BiFunction;
/**
* A <code>RelOptRuleCall</code> is an invocation of a {@link RelOptRule} with a
@@ -220,16 +222,49 @@ public abstract class RelOptRuleCall {
* rel.getTraits()</code> will be copied from <code>
* this.rels[0].getTraitSet()</code>.
*
+ * <p>The implementation of this method also guarantees that the original
+ * relational expression (that is, <code>this.rels[0]</code>) has its hints propagated to
+ * the new relational expression (<code>rel</code>). The hints propagation strategy can be
+ * customized through specified {@code handler}.
+ *
+ * @param rel Relational expression equivalent to the root relational
+ * expression of the rule call, {@code call.rels(0)}
+ * @param equiv Map of other equivalences
+ * @param handler Handler to customize the relational expression that would
+ * be registered into the planner, the 1th argument is the
+ * original expression and the 2th argument is the new relational
+ * expression
+ */
+ public abstract void transformTo(RelNode rel,
+ Map<RelNode, RelNode> equiv,
+ BiFunction<RelNode, RelNode, RelNode> handler);
+
+ /**
+ * Registers that a rule has produced an equivalent relational expression,
+ * with specified equivalences.
+ *
+ * <p>The hints are copied fully from the original expression
+ * (that is, <code>this.rels[0]</code>) to the new relational expression
+ * (<code>rel</code>) if both of them are all instances of
+ * {@link Hintable}.
+ *
* @param rel Relational expression equivalent to the root relational
* expression of the rule call, {@code call.rels(0)}
* @param equiv Map of other equivalences
*/
- public abstract void transformTo(RelNode rel, Map<RelNode, RelNode> equiv);
+ public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv) {
+ transformTo(rel, equiv, RelOptUtil::copyRelHints);
+ }
/**
* Registers that a rule has produced an equivalent relational expression,
* but no other equivalences.
*
+ * <p>The hints are copied fully from the original expression
+ * (that is, <code>this.rels[0]</code>) to the new relational expression
+ * (<code>rel</code>) if both of them are all instances of
+ * {@link Hintable}.
+ *
* @param rel Relational expression equivalent to the root relational
* expression of the rule call, {@code call.rels(0)}
*/
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
index 44fa568..042c0f6 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
@@ -44,6 +44,9 @@ import org.apache.calcite.rel.core.TableScan;
import org.apache.calcite.rel.externalize.RelJsonWriter;
import org.apache.calcite.rel.externalize.RelWriterImpl;
import org.apache.calcite.rel.externalize.RelXmlWriter;
+import org.apache.calcite.rel.hint.HintStrategyTable;
+import org.apache.calcite.rel.hint.Hintable;
+import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.logical.LogicalAggregate;
import org.apache.calcite.rel.logical.LogicalCalc;
import org.apache.calcite.rel.logical.LogicalFilter;
@@ -384,6 +387,29 @@ public abstract class RelOptUtil {
}
/**
+ * Copy the {@link org.apache.calcite.rel.hint.RelHint}s from {@code originalRel}
+ * to {@code newRel} if both of them are {@link Hintable}.
+ *
+ * <p>The hints are also filtered by the specified hint strategies.
+ *
+ * @param originalRel Original relational expression
+ * @param newRel New relational expression
+ * @return A copy of {@code newRel} with hints of {@code originalRel}, or {@code newRel}
+ * directly if one of them are not {@link Hintable}
+ */
+ public static RelNode copyRelHints(RelNode originalRel, RelNode newRel) {
+ if (originalRel instanceof Hintable
+ && newRel instanceof Hintable
+ && ((Hintable) originalRel).getHints().size() > 0) {
+ HintStrategyTable hintStrategies = originalRel.getCluster().getHintStrategies();
+ final List<RelHint> hints = ((Hintable) originalRel).getHints();
+ return ((Hintable) newRel)
+ .attachHints(hintStrategies.apply(hints, newRel));
+ }
+ return newRel;
+ }
+
+ /**
* Returns a permutation describing where output fields come from. In
* the returned map, value of {@code map.getTargetOpt(i)} is {@code n} if
* field {@code i} projects input field {@code n} or applies a cast on
diff --git a/core/src/main/java/org/apache/calcite/plan/hep/HepRuleCall.java b/core/src/main/java/org/apache/calcite/plan/hep/HepRuleCall.java
index 3a40ec0..38f846a 100644
--- a/core/src/main/java/org/apache/calcite/plan/hep/HepRuleCall.java
+++ b/core/src/main/java/org/apache/calcite/plan/hep/HepRuleCall.java
@@ -25,6 +25,7 @@ import org.apache.calcite.rel.RelNode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.function.BiFunction;
/**
* HepRuleCall implements {@link RelOptRuleCall} for a {@link HepPlanner}. It
@@ -52,11 +53,13 @@ public class HepRuleCall extends RelOptRuleCall {
//~ Methods ----------------------------------------------------------------
// implement RelOptRuleCall
- public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv) {
+ public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv,
+ BiFunction<RelNode, RelNode, RelNode> handler) {
final RelNode rel0 = rels[0];
RelOptUtil.verifyTypeEquivalence(rel0, rel, rel0);
+ rel = handler.apply(rel0, rel);
results.add(rel);
- rel(0).getCluster().invalidateMetadataQuery();
+ rel0.getCluster().invalidateMetadataQuery();
}
List<RelNode> getResults() {
diff --git a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRuleCall.java b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRuleCall.java
index aff1eb4..786a036 100644
--- a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRuleCall.java
+++ b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRuleCall.java
@@ -33,6 +33,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.BiFunction;
/**
* <code>VolcanoRuleCall</code> implements the {@link RelOptRuleCall} interface
@@ -87,7 +88,9 @@ public class VolcanoRuleCall extends RelOptRuleCall {
//~ Methods ----------------------------------------------------------------
// implement RelOptRuleCall
- public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv) {
+ public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv,
+ BiFunction<RelNode, RelNode, RelNode> handler) {
+ rel = handler.apply(rels[0], rel);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Transform to: rel#{} via {}{}", rel.getId(), getRule(),
equiv.isEmpty() ? "" : " with equivalences " + equiv);
diff --git a/core/src/main/java/org/apache/calcite/prepare/CalciteMaterializer.java b/core/src/main/java/org/apache/calcite/prepare/CalciteMaterializer.java
index 86bbca9..ae02dcd 100644
--- a/core/src/main/java/org/apache/calcite/prepare/CalciteMaterializer.java
+++ b/core/src/main/java/org/apache/calcite/prepare/CalciteMaterializer.java
@@ -99,7 +99,7 @@ class CalciteMaterializer extends CalcitePrepareImpl.CalcitePreparingStmt {
RelOptTable table =
this.catalogReader.getTable(materialization.materializedTable.path());
- materialization.tableRel = sqlToRelConverter2.toRel(table);
+ materialization.tableRel = sqlToRelConverter2.toRel(table, null);
}
/** Converts a relational expression to use a
diff --git a/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java b/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java
index 8f076a2..898f955 100644
--- a/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java
+++ b/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java
@@ -985,7 +985,7 @@ public class CalcitePrepareImpl implements CalcitePrepare {
? ((Sort) rel).collation
: RelCollations.EMPTY;
RelRoot root = new RelRoot(rel, resultType, SqlKind.SELECT, fields,
- collation);
+ collation, new ArrayList<>());
if (timingTracer != null) {
timingTracer.traceTime("end sql2rel");
diff --git a/core/src/main/java/org/apache/calcite/rel/RelRoot.java b/core/src/main/java/org/apache/calcite/rel/RelRoot.java
index c744492..887a476 100644
--- a/core/src/main/java/org/apache/calcite/rel/RelRoot.java
+++ b/core/src/main/java/org/apache/calcite/rel/RelRoot.java
@@ -16,6 +16,8 @@
*/
package org.apache.calcite.rel;
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.logical.LogicalProject;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexBuilder;
@@ -78,6 +80,7 @@ public class RelRoot {
public final SqlKind kind;
public final ImmutableList<Pair<Integer, String>> fields;
public final RelCollation collation;
+ public final ImmutableList<RelHint> hints;
/**
* Creates a RelRoot.
@@ -87,12 +90,13 @@ public class RelRoot {
*/
public RelRoot(RelNode rel, RelDataType validatedRowType, SqlKind kind,
- List<Pair<Integer, String>> fields, RelCollation collation) {
+ List<Pair<Integer, String>> fields, RelCollation collation, List<RelHint> hints) {
this.rel = rel;
this.validatedRowType = validatedRowType;
this.kind = kind;
this.fields = ImmutableList.copyOf(fields);
this.collation = Objects.requireNonNull(collation);
+ this.hints = ImmutableList.copyOf(hints);
}
/** Creates a simple RelRoot. */
@@ -106,7 +110,7 @@ public class RelRoot {
ImmutableIntList.identity(rowType.getFieldCount());
final List<String> names = rowType.getFieldNames();
return new RelRoot(rel, rowType, kind, Pair.zip(refs, names),
- RelCollations.EMPTY);
+ RelCollations.EMPTY, new ArrayList<>());
}
@Override public String toString() {
@@ -122,7 +126,7 @@ public class RelRoot {
if (rel == this.rel) {
return this;
}
- return new RelRoot(rel, validatedRowType, kind, fields, collation);
+ return new RelRoot(rel, validatedRowType, kind, fields, collation, hints);
}
/** Creates a copy, assigning a new kind. */
@@ -130,11 +134,16 @@ public class RelRoot {
if (kind == this.kind) {
return this;
}
- return new RelRoot(rel, validatedRowType, kind, fields, collation);
+ return new RelRoot(rel, validatedRowType, kind, fields, collation, hints);
}
public RelRoot withCollation(RelCollation collation) {
- return new RelRoot(rel, validatedRowType, kind, fields, collation);
+ return new RelRoot(rel, validatedRowType, kind, fields, collation, hints);
+ }
+
+ /** Creates a copy, assigning the query hints. */
+ public RelRoot withHints(List<RelHint> hints) {
+ return new RelRoot(rel, validatedRowType, kind, fields, collation, hints);
}
/** Returns the root relational expression, creating a {@link LogicalProject}
@@ -158,7 +167,7 @@ public class RelRoot {
for (Pair<Integer, String> field : fields) {
projects.add(rexBuilder.makeInputRef(rel, field.left));
}
- return LogicalProject.create(rel, projects, Pair.right(fields));
+ return RelOptUtil.copyRelHints(rel, LogicalProject.create(rel, projects, Pair.right(fields)));
}
public boolean isNameTrivial() {
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Join.java b/core/src/main/java/org/apache/calcite/rel/core/Join.java
index 57339d9..6ffc395 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Join.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Join.java
@@ -23,6 +23,8 @@ import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.BiRel;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelWriter;
+import org.apache.calcite.rel.hint.Hintable;
+import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.metadata.RelMdUtil;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.calcite.rel.type.RelDataType;
@@ -39,6 +41,7 @@ import org.apache.calcite.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -52,11 +55,12 @@ import java.util.Set;
* The set of output rows is a subset of the cartesian product of the two
* inputs; precisely which subset depends on the join condition.
*/
-public abstract class Join extends BiRel {
+public abstract class Join extends BiRel implements Hintable {
//~ Instance fields --------------------------------------------------------
protected final RexNode condition;
protected final ImmutableSet<CorrelationId> variablesSet;
+ protected final ImmutableList<RelHint> hints;
/**
* Values must be of enumeration {@link JoinRelType}, except that
@@ -86,6 +90,7 @@ public abstract class Join extends BiRel {
*
* @param cluster Cluster
* @param traitSet Trait set
+ * @param hints Hints
* @param left Left input
* @param right Right input
* @param condition Join condition
@@ -97,6 +102,7 @@ public abstract class Join extends BiRel {
protected Join(
RelOptCluster cluster,
RelTraitSet traitSet,
+ List<RelHint> hints,
RelNode left,
RelNode right,
RexNode condition,
@@ -107,6 +113,36 @@ public abstract class Join extends BiRel {
this.variablesSet = ImmutableSet.copyOf(variablesSet);
this.joinType = Objects.requireNonNull(joinType);
this.joinInfo = JoinInfo.of(left, right, condition);
+ this.hints = ImmutableList.copyOf(hints);
+ }
+
+ /**
+ * Creates a Join.
+ *
+ * <p>Note: We plan to change the {@code variablesStopped} parameter to
+ * {@code Set<CorrelationId> variablesSet}
+ * {@link org.apache.calcite.util.Bug#upgrade(String) before version 2.0},
+ * because {@link #getVariablesSet()}
+ * is preferred over {@link #getVariablesStopped()}.
+ * This constructor is not deprecated, for now, because maintaining overloaded
+ * constructors in multiple sub-classes would be onerous.
+ *
+ * @param cluster Cluster
+ * @param traitSet Trait set
+ * @param left Left input
+ * @param right Right input
+ * @param condition Join condition
+ * @param joinType Join type
+ * @param variablesSet Set variables that are set by the
+ * LHS and used by the RHS and are not available to
+ * nodes above this Join in the tree
+ */
+ protected Join(
+ RelOptCluster cluster, RelTraitSet traitSet, RelNode left,
+ RelNode right, RexNode condition, Set<CorrelationId> variablesSet,
+ JoinRelType joinType) {
+ this(cluster, traitSet, new ArrayList<>(), left, right,
+ condition, variablesSet, joinType);
}
@Deprecated // to be removed before 2.0
@@ -311,6 +347,10 @@ public abstract class Join extends BiRel {
public JoinInfo analyzeCondition() {
return joinInfo;
}
+
+ @Override public ImmutableList<RelHint> getHints() {
+ return hints;
+ }
}
// End Join.java
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Project.java b/core/src/main/java/org/apache/calcite/rel/core/Project.java
index eee00b3..381bba2 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Project.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Project.java
@@ -25,6 +25,8 @@ import org.apache.calcite.rel.RelInput;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelWriter;
import org.apache.calcite.rel.SingleRel;
+import org.apache.calcite.rel.hint.Hintable;
+import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeField;
@@ -44,6 +46,7 @@ import org.apache.calcite.util.mapping.Mappings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
+import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -54,11 +57,13 @@ import java.util.Set;
*
* @see org.apache.calcite.rel.logical.LogicalProject
*/
-public abstract class Project extends SingleRel {
+public abstract class Project extends SingleRel implements Hintable {
//~ Instance fields --------------------------------------------------------
protected final ImmutableList<RexNode> exps;
+ protected final ImmutableList<RelHint> hints;
+
//~ Constructors -----------------------------------------------------------
/**
@@ -66,6 +71,7 @@ public abstract class Project extends SingleRel {
*
* @param cluster Cluster that this relational expression belongs to
* @param traits Traits of this relational expression
+ * @param hints Hints of this relation expression
* @param input Input relational expression
* @param projects List of expressions for the input columns
* @param rowType Output row type
@@ -73,16 +79,32 @@ public abstract class Project extends SingleRel {
protected Project(
RelOptCluster cluster,
RelTraitSet traits,
+ List<RelHint> hints,
RelNode input,
List<? extends RexNode> projects,
RelDataType rowType) {
super(cluster, traits, input);
assert rowType != null;
this.exps = ImmutableList.copyOf(projects);
+ this.hints = ImmutableList.copyOf(hints);
this.rowType = rowType;
assert isValid(Litmus.THROW, null);
}
+ /**
+ * Creates a Project.
+ *
+ * @param cluster Cluster that this relational expression belongs to
+ * @param traits Traits of this relational expression
+ * @param input Input relational expression
+ * @param projects List of expressions for the input columns
+ * @param rowType Output row type
+ */
+ protected Project(RelOptCluster cluster, RelTraitSet traits,
+ RelNode input, List<? extends RexNode> projects, RelDataType rowType) {
+ this(cluster, traits, new ArrayList<>(), input, projects, rowType);
+ }
+
@Deprecated // to be removed before 2.0
protected Project(RelOptCluster cluster, RelTraitSet traitSet, RelNode input,
List<? extends RexNode> projects, RelDataType rowType, int flags) {
@@ -173,6 +195,10 @@ public abstract class Project extends SingleRel {
return Pair.zip(getProjects(), getRowType().getFieldNames());
}
+ @Override public ImmutableList<RelHint> getHints() {
+ return hints;
+ }
+
@Deprecated // to be removed before 2.0
public int getFlags() {
return 1;
diff --git a/core/src/main/java/org/apache/calcite/rel/core/TableScan.java b/core/src/main/java/org/apache/calcite/rel/core/TableScan.java
index 07dce01..b461915 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/TableScan.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/TableScan.java
@@ -27,6 +27,8 @@ import org.apache.calcite.rel.RelInput;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelShuttle;
import org.apache.calcite.rel.RelWriter;
+import org.apache.calcite.rel.hint.Hintable;
+import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeField;
@@ -36,6 +38,8 @@ import org.apache.calcite.tools.RelBuilder;
import org.apache.calcite.util.ImmutableBitSet;
import org.apache.calcite.util.ImmutableIntList;
+import com.google.common.collect.ImmutableList;
+
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@@ -43,7 +47,8 @@ import java.util.Set;
/**
* Relational operator that returns the contents of a table.
*/
-public abstract class TableScan extends AbstractRelNode {
+public abstract class TableScan
+ extends AbstractRelNode implements Hintable {
//~ Instance fields --------------------------------------------------------
/**
@@ -51,15 +56,26 @@ public abstract class TableScan extends AbstractRelNode {
*/
protected final RelOptTable table;
+ /**
+ * The table hints.
+ */
+ protected final ImmutableList<RelHint> hints;
+
//~ Constructors -----------------------------------------------------------
protected TableScan(RelOptCluster cluster, RelTraitSet traitSet,
- RelOptTable table) {
+ List<RelHint> hints, RelOptTable table) {
super(cluster, traitSet);
this.table = table;
if (table.getRelOptSchema() != null) {
cluster.getPlanner().registerSchema(table.getRelOptSchema());
}
+ this.hints = ImmutableList.copyOf(hints);
+ }
+
+ protected TableScan(RelOptCluster cluster, RelTraitSet traitSet,
+ RelOptTable table) {
+ this(cluster, traitSet, new ArrayList<>(), table);
}
/**
@@ -162,6 +178,10 @@ public abstract class TableScan extends AbstractRelNode {
@Override public RelNode accept(RelShuttle shuttle) {
return shuttle.visit(this);
}
+
+ @Override public ImmutableList<RelHint> getHints() {
+ return hints;
+ }
}
// End TableScan.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/ExplicitHintMatcher.java b/core/src/main/java/org/apache/calcite/rel/hint/ExplicitHintMatcher.java
new file mode 100644
index 0000000..b366545
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/ExplicitHintMatcher.java
@@ -0,0 +1,44 @@
+/*
+ * 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.calcite.rel.hint;
+
+import org.apache.calcite.rel.RelNode;
+
+/**
+ * A function to customize whether a relational expression should match a hint.
+ *
+ * <p>Usually you may not need to implement this function,
+ * {@link NodeTypeHintStrategy} is enough for most of the {@link RelHint}s.
+ *
+ * <p>Some of the hints can only be matched to the relational
+ * expression with special match conditions(not only the relational expression type).
+ * i.e. "hash_join(r, st)", this hint can only be applied to JOIN expression that
+ * has "r" and "st" as the input table names. To implement this, you may need to customize an
+ * {@link ExplicitHintStrategy} with the {@link ExplicitHintMatcher}.
+ *
+ * @param <R> Relational expression to test if the hint matches
+ *
+ * @see ExplicitHintStrategy
+ * @see HintStrategies
+ */
+@FunctionalInterface
+public interface ExplicitHintMatcher<R extends RelNode> {
+ boolean apply(RelHint hint, R node);
+}
+
+// End ExplicitHintMatcher.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/ExplicitHintStrategy.java b/core/src/main/java/org/apache/calcite/rel/hint/ExplicitHintStrategy.java
new file mode 100644
index 0000000..a70187d
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/ExplicitHintStrategy.java
@@ -0,0 +1,53 @@
+/*
+ * 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.calcite.rel.hint;
+
+import org.apache.calcite.rel.RelNode;
+
+/**
+ * A hint strategy whose rules are totally customized.
+ *
+ * @see ExplicitHintMatcher
+ */
+public class ExplicitHintStrategy implements HintStrategy {
+ //~ Instance fields --------------------------------------------------------
+
+ private final ExplicitHintMatcher matcher;
+
+ /**
+ * Creates an {@code ExplicitHintStrategy} with specified {@code matcher}.
+ *
+ * <p>Make this constructor package-protected intentionally, use
+ * {@link HintStrategies#explicit(ExplicitHintMatcher)}.
+ *
+ * @param matcher ExplicitHintMatcher instance to test
+ * if a hint can be applied to a rel
+ */
+ ExplicitHintStrategy(ExplicitHintMatcher matcher) {
+ this.matcher = matcher;
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ @Override public boolean supportsRel(RelHint hint, RelNode rel) {
+ //noinspection unchecked
+ return this.matcher.apply(hint, rel);
+ }
+}
+
+// End ExplicitHintStrategy.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/HintStrategies.java b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategies.java
new file mode 100644
index 0000000..4d97786
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategies.java
@@ -0,0 +1,63 @@
+/*
+ * 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.calcite.rel.hint;
+
+/**
+ * A collection of hint strategies.
+ */
+public abstract class HintStrategies {
+ /** A hint strategy that indicates a hint can only be used to
+ * the whole query(no specific nodes). */
+ public static final HintStrategy SET_VAR =
+ new NodeTypeHintStrategy(NodeTypeHintStrategy.NodeType.SET_VAR);
+
+ /** A hint strategy that indicates a hint can only be used to
+ * {@link org.apache.calcite.rel.core.Join} nodes. */
+ public static final HintStrategy JOIN =
+ new NodeTypeHintStrategy(NodeTypeHintStrategy.NodeType.JOIN);
+
+ /** A hint strategy that indicates a hint can only be used to
+ * {@link org.apache.calcite.rel.core.TableScan} nodes. */
+ public static final HintStrategy TABLE_SCAN =
+ new NodeTypeHintStrategy(NodeTypeHintStrategy.NodeType.TABLE_SCAN);
+
+ /** A hint strategy that indicates a hint can only be used to
+ * {@link org.apache.calcite.rel.core.Project} nodes. */
+ public static final HintStrategy PROJECT =
+ new NodeTypeHintStrategy(NodeTypeHintStrategy.NodeType.PROJECT);
+
+ /**
+ * Create a hint strategy from a specific matcher whose rules are totally customized.
+ *
+ * @param matcher The strategy matcher
+ * @return A ExplicitHintStrategy instance.
+ */
+ public static HintStrategy explicit(ExplicitHintMatcher matcher) {
+ return new ExplicitHintStrategy(matcher);
+ }
+
+ /**
+ * Creates a HintStrategyCascade instance whose strategy rules are satisfied only if
+ * all the {@code hintStrategies} are satisfied.
+ */
+ public static HintStrategy cascade(HintStrategy... hintStrategies) {
+ return new HintStrategyCascade(hintStrategies);
+ }
+}
+
+// End HintStrategies.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/HintStrategy.java b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategy.java
new file mode 100644
index 0000000..8e7623b
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategy.java
@@ -0,0 +1,48 @@
+/*
+ * 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.calcite.rel.hint;
+
+import org.apache.calcite.rel.RelNode;
+
+/**
+ * A {@code HintStrategy} indicates whether a {@link org.apache.calcite.rel.RelNode}
+ * can apply the specified hint.
+ *
+ * <p>Typically, every supported hint should register a {@code HintStrategy}
+ * into the {@link HintStrategyTable}. For example, {@link HintStrategies#JOIN} implies
+ * that this hint would be propagated and applied to the {@link org.apache.calcite.rel.core.Join}
+ * relational expressions.
+ *
+ * <p>In {@link HintStrategyTable} the strategy is used for
+ * hints registration.
+ *
+ * @see HintStrategyTable
+ */
+public interface HintStrategy {
+
+ /**
+ * Decides if the given {@code hint} can be applied to
+ * the relational expression {@code rel}.
+ *
+ * @param hint The hint
+ * @param rel The relational expression
+ * @return True if the {@code hint} can be applied to the {@code rel}
+ */
+ boolean supportsRel(RelHint hint, RelNode rel);
+}
+
+// End HintStrategy.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/HintStrategyCascade.java b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategyCascade.java
new file mode 100644
index 0000000..7494f7b
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategyCascade.java
@@ -0,0 +1,61 @@
+/*
+ * 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.calcite.rel.hint;
+
+import org.apache.calcite.rel.RelNode;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Strategy to decide if a hint should apply to a relational expression
+ * by using a series of {@link HintStrategy} rules in a given order.
+ * If a rule fails, returns false early, else return true.
+ */
+public class HintStrategyCascade implements HintStrategy {
+ //~ Instance fields --------------------------------------------------------
+
+ private ImmutableList<HintStrategy> strategies;
+
+ /**
+ * Creates a HintStrategyCascade from an array of hint strategies.
+ *
+ * <p>Make this constructor package-protected intentionally.
+ * Use {@link HintStrategies#cascade}.</p>
+ */
+ HintStrategyCascade(HintStrategy... strategies) {
+ assert strategies != null;
+ assert strategies.length > 1;
+ for (HintStrategy strategy : strategies) {
+ assert strategy != null;
+ }
+ this.strategies = ImmutableList.copyOf(strategies);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ @Override public boolean supportsRel(RelHint hint, RelNode rel) {
+ for (HintStrategy strategy : strategies) {
+ if (!strategy.supportsRel(hint, rel)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
+
+// End HintStrategyCascade.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/HintStrategyTable.java b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategyTable.java
new file mode 100644
index 0000000..c6ce9be
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategyTable.java
@@ -0,0 +1,150 @@
+/*
+ * 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.calcite.rel.hint;
+
+import org.apache.calcite.rel.RelNode;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * {@code HintStrategy} collection indicating which kind of
+ * {@link org.apache.calcite.rel.RelNode} a hint can apply to.
+ *
+ * <p>Typically, every supported hints should register a {@code HintStrategy}
+ * in this collection. For example, {@link HintStrategies#JOIN} implies that this hint
+ * would be propagated and applied to the {@link org.apache.calcite.rel.core.Join}
+ * relational expressions.
+ *
+ * <p>A {@code HintStrategy} can be used independently or cascaded with other strategies
+ * with method {@link HintStrategies#cascade}.
+ *
+ * <p>The matching for hint name is case in-sensitive.
+ *
+ * @see HintStrategy
+ */
+public class HintStrategyTable {
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Empty strategies. */
+ // Need to replace the EMPTY with DEFAULT if we have any hint implementations.
+ public static final HintStrategyTable EMPTY = new HintStrategyTable(new HashMap<>());
+
+ //~ Instance fields --------------------------------------------------------
+
+ /** Mapping from hint name to strategy. */
+ private final Map<Key, HintStrategy> hintStrategyMap;
+
+ private HintStrategyTable(Map<Key, HintStrategy> strategies) {
+ this.hintStrategyMap = ImmutableMap.copyOf(strategies);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ /**
+ * Apply this {@link HintStrategyTable} to the given relational
+ * expression for the {@code hints}.
+ *
+ * @param hints Hints that may attach to the {@code rel}
+ * @param rel Relational expression
+ * @return A hints list that can be attached to the {@code rel}
+ */
+ public List<RelHint> apply(List<RelHint> hints, RelNode rel) {
+ return hints.stream()
+ .filter(relHint -> supportsRel(relHint, rel))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Check if the give hint name is valid.
+ *
+ * @param name The hint name
+ */
+ public void validateHint(String name) {
+ if (!this.hintStrategyMap.containsKey(Key.of(name))) {
+ throw new RuntimeException("Hint: " + name + " should be registered in the HintStrategies.");
+ }
+ }
+
+ private boolean supportsRel(RelHint hint, RelNode rel) {
+ final Key key = Key.of(hint.hintName);
+ assert this.hintStrategyMap.containsKey(key);
+ return this.hintStrategyMap.get(key).supportsRel(hint, rel);
+ }
+
+ /**
+ * @return A strategies builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Key used to keep the strategies which ignores the case sensitivity.
+ */
+ private static class Key {
+ private String name;
+ private Key(String name) {
+ this.name = name;
+ }
+
+ static Key of(String name) {
+ return new Key(name.toLowerCase(Locale.ROOT));
+ }
+
+ @Override public boolean equals(Object obj) {
+ if (!(obj instanceof Key)) {
+ return false;
+ }
+ return Objects.equals(this.name, ((Key) obj).name);
+ }
+
+ @Override public int hashCode() {
+ return this.name.hashCode();
+ }
+ }
+
+ //~ Inner Class ------------------------------------------------------------
+
+ /**
+ * Builder for {@code HintStrategyTable}.
+ */
+ public static class Builder {
+ private Map<Key, HintStrategy> hintStrategyMap;
+
+ public Builder() {
+ this.hintStrategyMap = new HashMap<>();
+ }
+
+ public Builder addHintStrategy(String hintName, HintStrategy strategy) {
+ this.hintStrategyMap.put(Key.of(hintName), strategy);
+ return this;
+ }
+
+ public HintStrategyTable build() {
+ return new HintStrategyTable(this.hintStrategyMap);
+ }
+ }
+}
+
+// End HintStrategyTable.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/Hintable.java b/core/src/main/java/org/apache/calcite/rel/hint/Hintable.java
new file mode 100644
index 0000000..20e1864
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/Hintable.java
@@ -0,0 +1,90 @@
+/*
+ * 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.calcite.rel.hint;
+
+import org.apache.calcite.linq4j.function.Experimental;
+import org.apache.calcite.rel.RelNode;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Base class for relational expressions with {@link RelHint}s.
+ *
+ * <p>Relational expressions that can attach hints should implement
+ * this interface.
+ *
+ * <p>This interface is experimental, currently, we make
+ * {@link org.apache.calcite.rel.core.Project}
+ * {@link org.apache.calcite.rel.core.Join}
+ * {@link org.apache.calcite.rel.core.TableScan}
+ * implement this interface and add an argument named "hints" to construct these
+ * relational expressions if hints are attached.
+ *
+ * <p>This design is not that elegant and mature, because we have to copy the hints whenever these
+ * relational expressions are copied or used to derive new relational expressions.
+ * Even though we have implemented the mechanism to propagate the hints, for large queries,
+ * there would be many cases where the hints are not copied to the right RelNode,
+ * and the effort/memory is wasted if we are copying the hint to a RelNode
+ * but the hint is not used.
+ */
+@Experimental
+public interface Hintable {
+
+ /**
+ * Attach list of hints to this relational expression, should be overridden by
+ * every logical node that supports hint. This method is only for
+ * internal use during sql-to-rel conversion.
+ *
+ * <p>The sub-class should return a new copy of the relational expression. We make
+ * the default implementation return the relational expression directly only
+ * because not every kind of relational expression supports hints.
+ *
+ * @param hintList The hints to attach to this relational expression
+ * @return Relational expression with the hints {@code hintList} attached
+ */
+ default RelNode attachHints(List<RelHint> hintList) {
+ return (RelNode) this;
+ }
+
+ /**
+ * @return The hints list of this relational expressions
+ */
+ ImmutableList<RelHint> getHints();
+
+ /**
+ * Merge this relation expression's hints with the given hint list.
+ *
+ * <p>The default behavior is to put them in one list and eliminate the duplicates.
+ *
+ * @param hintList Hints to be merged
+ * @return A merged hint list
+ */
+ default List<RelHint> mergeHints(List<RelHint> hintList) {
+ Objects.requireNonNull(hintList);
+ final Set<RelHint> hints = new LinkedHashSet<>(getHints());
+ hints.addAll(hintList);
+ return new ArrayList<>(hints);
+ }
+}
+
+// End Hintable.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/NodeTypeHintStrategy.java b/core/src/main/java/org/apache/calcite/rel/hint/NodeTypeHintStrategy.java
new file mode 100644
index 0000000..3b95492
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/NodeTypeHintStrategy.java
@@ -0,0 +1,82 @@
+/*
+ * 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.calcite.rel.hint;
+
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.core.TableScan;
+
+/**
+ * A hint strategy that specifies which kind of relational
+ * expression the hint can be applied to.
+ */
+public class NodeTypeHintStrategy implements HintStrategy {
+
+ /**
+ * Enumeration of the relational expression types that the hints
+ * may be propagated to.
+ */
+ enum NodeType {
+ /**
+ * The hint is used for the whole query, kind of like a query config.
+ * This kind of hints would never be propagated.
+ */
+ SET_VAR(RelNode.class),
+
+ /**
+ * The hint would be propagated to the Join nodes.
+ */
+ JOIN(Join.class),
+
+ /**
+ * The hint would be propagated to the TableScan nodes.
+ */
+ TABLE_SCAN(TableScan.class),
+
+ /**
+ * The hint would be propagated to the Project nodes.
+ */
+ PROJECT(Project.class);
+
+ /** Relational expression clazz that the hint can apply to. */
+ private Class<?> relClazz;
+
+ NodeType(Class relClazz) {
+ this.relClazz = relClazz;
+ }
+ }
+
+ private NodeType nodeType;
+
+ public NodeTypeHintStrategy(NodeType nodeType) {
+ this.nodeType = nodeType;
+ }
+
+ @Override public boolean supportsRel(RelHint hint, RelNode rel) {
+ switch (this.nodeType) {
+ // The SET_VAR type never propagate any hints.
+ case SET_VAR:
+ return false;
+ default:
+ return this.nodeType.relClazz.isInstance(rel);
+ }
+ }
+}
+
+// End NodeTypeHintStrategy.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/RelHint.java b/core/src/main/java/org/apache/calcite/rel/hint/RelHint.java
new file mode 100644
index 0000000..62ea6a2
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/RelHint.java
@@ -0,0 +1,112 @@
+/*
+ * 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.calcite.rel.hint;
+
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Represents a hint within a relation expression.
+ */
+public class RelHint {
+ //~ Instance fields --------------------------------------------------------
+
+ public final ImmutableBitSet inheritPath;
+ public final String hintName;
+ public final List<String> listOptions;
+ public final Map<String, String> kvOptions;
+
+ //~ Constructors -----------------------------------------------------------
+
+ /**
+ * Creates a {@code RelHint}.
+ *
+ * @param inheritPath Hint inherit path
+ * @param hintName Hint name
+ * @param listOption Hint options as string list
+ * @param kvOptions Hint options as string key value pair
+ */
+ public RelHint(
+ Iterable<Integer> inheritPath,
+ String hintName,
+ @Nullable List<String> listOption,
+ @Nullable Map<String, String> kvOptions) {
+ Objects.requireNonNull(inheritPath);
+ Objects.requireNonNull(hintName);
+ this.inheritPath = ImmutableBitSet.of(inheritPath);
+ this.hintName = hintName;
+ this.listOptions = listOption == null ? ImmutableList.of() : ImmutableList.copyOf(listOption);
+ this.kvOptions = kvOptions == null ? ImmutableMap.of() : ImmutableMap.copyOf(kvOptions);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ /**
+ * Represents a copy of this hint that has a specified inherit path.
+ *
+ * @param inheritPath Hint path
+ * @return the new {@code RelHint}
+ */
+ public RelHint copy(List<Integer> inheritPath) {
+ Objects.requireNonNull(inheritPath);
+ return new RelHint(inheritPath, hintName, listOptions, kvOptions);
+ }
+
+ @Override public boolean equals(Object obj) {
+ if (!(obj instanceof RelHint)) {
+ return false;
+ }
+ final RelHint that = (RelHint) obj;
+ return this.hintName.equals(that.hintName)
+ && this.inheritPath.equals(that.inheritPath)
+ && Objects.equals(this.listOptions, that.listOptions)
+ && Objects.equals(this.kvOptions, that.kvOptions);
+ }
+
+ @Override public int hashCode() {
+ return Objects.hash(this.hintName, this.inheritPath,
+ this.listOptions, this.kvOptions);
+ }
+
+ @Override public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("[")
+ .append(this.hintName)
+ .append(" inheritPath:")
+ .append(this.inheritPath);
+ if (this.listOptions.size() > 0 || this.kvOptions.size() > 0) {
+ builder.append(" options:");
+ if (this.listOptions.size() > 0) {
+ builder.append(this.listOptions.toString());
+ } else {
+ builder.append(this.kvOptions.toString());
+ }
+ }
+ builder.append("]");
+ return builder.toString();
+ }
+}
+
+// End RelHint.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/package-info.java b/core/src/main/java/org/apache/calcite/rel/hint/package-info.java
new file mode 100644
index 0000000..e00c6d6
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/package-info.java
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+
+/**
+ * Defines hints interfaces and utilities for relational expressions.
+ *
+ * <h2>The Syntax</h2>
+ * We support the Oracle style hint grammar for both query hint(right after the "SELECT" keyword)
+ * and the table hint(right after the table name reference). i.e.
+ *
+ * <pre>
+ * select /*+ NO_HASH_JOIN, RESOURCE(mem='128mb', parallelism='24') */
+ * from
+ * emp /*+ INDEX(idx1, idx2) */
+ * join
+ * dept /*+ PROPERTIES(k1='v1', k2='v2') */
+ * on emp.deptno=dept.deptno
+ * </pre>
+ *
+ * <h2>Customize Hint Matching Rules</h2>
+ * Calcite implements a framework to define and propagate the hints. In order to make the hints
+ * propagate efficiently, every hint referenced in the sql statement needs to
+ * register the propagation rules.
+ *
+ * <p>We support two kinds of matching rules for rule registration:
+ *
+ * <ol>
+ * <li>One is {@link org.apache.calcite.rel.hint.NodeTypeHintStrategy}, this rule matches a
+ * relational expression by the relational node type.</li>
+ * <li>Another is {@link org.apache.calcite.rel.hint.ExplicitHintStrategy},
+ * this rule's matching logic can be totally customized.</li>
+ * </ol>
+ *
+ * <p>Here is the code snippet to illustrate how to config the strategies:
+ *
+ * <pre>
+ * // Initialize a HintStrategyTable.
+ * HintStrategyTable strategies = HintStrategyTable.builder()
+ * .addHintStrategy("time_zone", HintStrategies.SET_VAR)
+ * .addHintStrategy("index", HintStrategies.TABLE_SCAN)
+ * .addHintStrategy("resource", HintStrategies.PROJECT)
+ * .addHintStrategy("use_hash_join",
+ * HintStrategies.cascade(HintStrategies.JOIN,
+ * HintStrategies.explicit((hint, rel) -> {
+ * ...
+ * })))
+ * .build();
+ * // Config the strategies in the config.
+ * SqlToRelConverter.Config config = SqlToRelConverter.configBuilder()
+ * .withHintStrategyTable(strategies)
+ * .build();
+ * // Use the config to initialize the SqlToRelConverter.
+ * ...
+ * </pre>
+ *
+ * <h2>Hints Propagation</h2>
+ * There are two cases we need to consider the hints propagation:
+ *
+ * <ul>
+ * <li>Right after a {@code SqlNode} was converted to {@code RelNode}, we would
+ * propagate the hints from the attaching to its input(children) nodes. The hints was
+ * propagated recursively with a {@code RelShuttle}, see
+ * SqlToRelConverter#RelHintPropagateShuttle for how it works.</li>
+ * <li>During rule planning, in the transforming phrase of a {@code RelOptRule},
+ * you should copy the hints by hand if it is needed;
+ * We make some effort to make the thing easier: right before the new relational expression
+ * was registered into the planner, the hints of the old relational expression was
+ * copied into the new expression if both of them all implement
+ * {@link org.apache.calcite.rel.hint.Hintable}.</li>
+ * </ul>
+ *
+ * <h2>Caution</h2>
+ * We do not support hints propagation with decorrelation(the hints would all be dropped).
+ *
+ * <h2>Design Doc</h2>
+ * <a href="https://docs.google.com/document/d/1mykz-w2t1Yw7CH6NjUWpWqCAf_6YNKxSc59gXafrNCs/edit?usp=sharing">Calcite SQL and Planner Hints Design</a>.
+ */
+@PackageMarker
+package org.apache.calcite.rel.hint;
+
+import org.apache.calcite.avatica.util.PackageMarker;
+
+// End package-info.java
diff --git a/core/src/main/java/org/apache/calcite/rel/logical/LogicalJoin.java b/core/src/main/java/org/apache/calcite/rel/logical/LogicalJoin.java
index 696d0b6..6f6f7c7 100644
--- a/core/src/main/java/org/apache/calcite/rel/logical/LogicalJoin.java
+++ b/core/src/main/java/org/apache/calcite/rel/logical/LogicalJoin.java
@@ -26,12 +26,14 @@ import org.apache.calcite.rel.RelWriter;
import org.apache.calcite.rel.core.CorrelationId;
import org.apache.calcite.rel.core.Join;
import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rex.RexNode;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -71,6 +73,7 @@ public final class LogicalJoin extends Join {
*
* @param cluster Cluster
* @param traitSet Trait set
+ * @param hints Hints
* @param left Left input
* @param right Right input
* @param condition Join condition
@@ -88,6 +91,7 @@ public final class LogicalJoin extends Join {
public LogicalJoin(
RelOptCluster cluster,
RelTraitSet traitSet,
+ List<RelHint> hints,
RelNode left,
RelNode right,
RexNode condition,
@@ -95,12 +99,21 @@ public final class LogicalJoin extends Join {
JoinRelType joinType,
boolean semiJoinDone,
ImmutableList<RelDataTypeField> systemFieldList) {
- super(cluster, traitSet, left, right, condition, variablesSet, joinType);
+ super(cluster, traitSet, hints, left, right, condition, variablesSet, joinType);
this.semiJoinDone = semiJoinDone;
this.systemFieldList = Objects.requireNonNull(systemFieldList);
}
@Deprecated // to be removed before 2.0
+ public LogicalJoin(RelOptCluster cluster, RelTraitSet traitSet,
+ RelNode left, RelNode right, RexNode condition, Set<CorrelationId> variablesSet,
+ JoinRelType joinType, boolean semiJoinDone,
+ ImmutableList<RelDataTypeField> systemFieldList) {
+ this(cluster, traitSet, new ArrayList<>(), left, right, condition,
+ variablesSet, joinType, semiJoinDone, systemFieldList);
+ }
+
+ @Deprecated // to be removed before 2.0
public LogicalJoin(RelOptCluster cluster, RelTraitSet traitSet, RelNode left,
RelNode right, RexNode condition, JoinRelType joinType,
Set<String> variablesStopped, boolean semiJoinDone,
@@ -132,6 +145,7 @@ public final class LogicalJoin extends Join {
*/
public LogicalJoin(RelInput input) {
this(input.getCluster(), input.getCluster().traitSetOf(Convention.NONE),
+ new ArrayList<>(),
input.getInputs().get(0), input.getInputs().get(1),
input.getExpression("condition"), ImmutableSet.of(),
input.getEnum("joinType", JoinRelType.class), false,
@@ -145,7 +159,7 @@ public final class LogicalJoin extends Join {
boolean semiJoinDone, ImmutableList<RelDataTypeField> systemFieldList) {
final RelOptCluster cluster = left.getCluster();
final RelTraitSet traitSet = cluster.traitSetOf(Convention.NONE);
- return new LogicalJoin(cluster, traitSet, left, right, condition,
+ return new LogicalJoin(cluster, traitSet, ImmutableList.of(), left, right, condition,
variablesSet, joinType, semiJoinDone, systemFieldList);
}
@@ -177,7 +191,7 @@ public final class LogicalJoin extends Join {
RelNode left, RelNode right, JoinRelType joinType, boolean semiJoinDone) {
assert traitSet.containsIfApplicable(Convention.NONE);
return new LogicalJoin(getCluster(),
- getCluster().traitSetOf(Convention.NONE), left, right, conditionExpr,
+ getCluster().traitSetOf(Convention.NONE), hints, left, right, conditionExpr,
variablesSet, joinType, semiJoinDone, systemFieldList);
}
@@ -199,6 +213,11 @@ public final class LogicalJoin extends Join {
public List<RelDataTypeField> getSystemFieldList() {
return systemFieldList;
}
+
+ @Override public RelNode attachHints(List<RelHint> hintList) {
+ return new LogicalJoin(getCluster(), traitSet, mergeHints(hintList),
+ left, right, condition, variablesSet, joinType, semiJoinDone, systemFieldList);
+ }
}
// End LogicalJoin.java
diff --git a/core/src/main/java/org/apache/calcite/rel/logical/LogicalProject.java b/core/src/main/java/org/apache/calcite/rel/logical/LogicalProject.java
index c5bbd4e..d7d129e 100644
--- a/core/src/main/java/org/apache/calcite/rel/logical/LogicalProject.java
+++ b/core/src/main/java/org/apache/calcite/rel/logical/LogicalProject.java
@@ -25,6 +25,7 @@ import org.apache.calcite.rel.RelInput;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelShuttle;
import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.metadata.RelMdCollation;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.calcite.rel.type.RelDataType;
@@ -33,6 +34,7 @@ import org.apache.calcite.rex.RexUtil;
import org.apache.calcite.sql.validate.SqlValidatorUtil;
import org.apache.calcite.util.Util;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -49,6 +51,7 @@ public final class LogicalProject extends Project {
*
* @param cluster Cluster this relational expression belongs to
* @param traitSet Traits of this relational expression
+ * @param hints Hints of this relational expression
* @param input Input relational expression
* @param projects List of expressions for the input columns
* @param rowType Output row type
@@ -56,13 +59,30 @@ public final class LogicalProject extends Project {
public LogicalProject(
RelOptCluster cluster,
RelTraitSet traitSet,
+ List<RelHint> hints,
RelNode input,
List<? extends RexNode> projects,
RelDataType rowType) {
- super(cluster, traitSet, input, projects, rowType);
+ super(cluster, traitSet, hints, input, projects, rowType);
assert traitSet.containsIfApplicable(Convention.NONE);
}
+ /**
+ * Creates a LogicalProject.
+ *
+ * <p>Use {@link #create} unless you know what you're doing.
+ *
+ * @param cluster Cluster this relational expression belongs to
+ * @param traitSet Traits of this relational expression
+ * @param input Input relational expression
+ * @param projects List of expressions for the input columns
+ * @param rowType Output row type
+ */
+ public LogicalProject(RelOptCluster cluster, RelTraitSet traitSet,
+ RelNode input, List<? extends RexNode> projects, RelDataType rowType) {
+ this(cluster, traitSet, new ArrayList<>(), input, projects, rowType);
+ }
+
@Deprecated // to be removed before 2.0
public LogicalProject(RelOptCluster cluster, RelTraitSet traitSet,
RelNode input, List<? extends RexNode> projects, RelDataType rowType,
@@ -114,12 +134,17 @@ public final class LogicalProject extends Project {
@Override public LogicalProject copy(RelTraitSet traitSet, RelNode input,
List<RexNode> projects, RelDataType rowType) {
- return new LogicalProject(getCluster(), traitSet, input, projects, rowType);
+ return new LogicalProject(getCluster(), traitSet, hints, input, projects, rowType);
}
@Override public RelNode accept(RelShuttle shuttle) {
return shuttle.visit(this);
}
+
+ @Override public RelNode attachHints(List<RelHint> hintList) {
+ return new LogicalProject(getCluster(), traitSet, mergeHints(hintList),
+ input, getProjects(), rowType);
+ }
}
// End LogicalProject.java
diff --git a/core/src/main/java/org/apache/calcite/rel/logical/LogicalTableScan.java b/core/src/main/java/org/apache/calcite/rel/logical/LogicalTableScan.java
index 6b6b74a..958f171 100644
--- a/core/src/main/java/org/apache/calcite/rel/logical/LogicalTableScan.java
+++ b/core/src/main/java/org/apache/calcite/rel/logical/LogicalTableScan.java
@@ -24,10 +24,12 @@ import org.apache.calcite.rel.RelCollationTraitDef;
import org.apache.calcite.rel.RelInput;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.schema.Table;
import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -65,8 +67,18 @@ public final class LogicalTableScan extends TableScan {
* <p>Use {@link #create} unless you know what you're doing.
*/
public LogicalTableScan(RelOptCluster cluster, RelTraitSet traitSet,
+ List<RelHint> hints, RelOptTable table) {
+ super(cluster, traitSet, hints, table);
+ }
+
+ /**
+ * Creates a LogicalTableScan.
+ *
+ * <p>Use {@link #create} unless you know what you're doing.
+ */
+ public LogicalTableScan(RelOptCluster cluster, RelTraitSet traitSet,
RelOptTable table) {
- super(cluster, traitSet, table);
+ this(cluster, traitSet, new ArrayList<>(), table);
}
@Deprecated // to be removed before 2.0
@@ -89,11 +101,12 @@ public final class LogicalTableScan extends TableScan {
/** Creates a LogicalTableScan.
*
- * @param cluster Cluster
+ * @param cluster Cluster
* @param relOptTable Table
+ * @param hints The hints
*/
public static LogicalTableScan create(RelOptCluster cluster,
- final RelOptTable relOptTable) {
+ final RelOptTable relOptTable, List<RelHint> hints) {
final Table table = relOptTable.unwrap(Table.class);
final RelTraitSet traitSet =
cluster.traitSetOf(Convention.NONE)
@@ -103,7 +116,21 @@ public final class LogicalTableScan extends TableScan {
}
return ImmutableList.of();
});
- return new LogicalTableScan(cluster, traitSet, relOptTable);
+ return new LogicalTableScan(cluster, traitSet, hints, relOptTable);
+ }
+
+ /** Creates a LogicalTableScan.
+ *
+ * @param cluster Cluster
+ * @param relOptTable Table
+ */
+ public static LogicalTableScan create(RelOptCluster cluster,
+ final RelOptTable relOptTable) {
+ return create(cluster, relOptTable, new ArrayList<>());
+ }
+
+ @Override public RelNode attachHints(List<RelHint> hintList) {
+ return new LogicalTableScan(getCluster(), traitSet, mergeHints(hintList), table);
}
}
diff --git a/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java b/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
index 074471d..cf5cbc2 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
@@ -475,7 +475,7 @@ public class RelToSqlConverter extends SqlImplementor
new SqlSelect(POS, null,
new SqlNodeList(values2, POS),
getDual(), null, null,
- null, null, null, null, null));
+ null, null, null, null, null, null));
}
if (list.isEmpty()) {
// In this case we need to construct the following query:
@@ -490,19 +490,19 @@ public class RelToSqlConverter extends SqlImplementor
if (dual == null) {
query = new SqlSelect(POS, null,
new SqlNodeList(nullColumnNames, POS), null, null, null, null,
- null, null, null, null);
+ null, null, null, null, null);
// Wrap "SELECT 1 AS x"
// as "SELECT * FROM (SELECT 1 AS x) AS t WHERE false"
query = new SqlSelect(POS, null,
new SqlNodeList(ImmutableList.of(SqlIdentifier.star(POS)), POS),
as(query, "t"), createAlwaysFalseCondition(), null, null,
- null, null, null, null);
+ null, null, null, null, null);
} else {
query = new SqlSelect(POS, null,
new SqlNodeList(nullColumnNames, POS),
dual, createAlwaysFalseCondition(), null,
- null, null, null, null, null);
+ null, null, null, null, null, null);
}
} else if (list.size() == 1) {
query = list.get(0);
@@ -542,7 +542,7 @@ public class RelToSqlConverter extends SqlImplementor
null, query,
createAlwaysFalseCondition(),
null, null, null,
- null, null, null);
+ null, null, null, null);
}
}
return result(query, clauses, e, null);
diff --git a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
index 7198e49..a2f273d 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
@@ -433,7 +433,7 @@ public abstract class SqlImplementor {
node = as(node, "t");
}
return new SqlSelect(POS, SqlNodeList.EMPTY, null, node, null, null, null,
- SqlNodeList.EMPTY, null, null, null);
+ SqlNodeList.EMPTY, null, null, null, null);
}
/** Returns whether we need to add an alias if this node is to be the FROM
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlHint.java b/core/src/main/java/org/apache/calcite/sql/SqlHint.java
new file mode 100644
index 0000000..8716ed4
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/SqlHint.java
@@ -0,0 +1,163 @@
+/*
+ * 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.calcite.sql;
+
+import org.apache.calcite.sql.parser.SqlParserPos;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A <code>SqlHint</code> is a node of a parse tree which represents
+ * a sql hint expression.
+ *
+ * <p>Basic hint grammar is: hint_name[(option1, option2 ...)].
+ * The hint_name should be a simple identifier, the options part is optional.
+ * For every option, it can be a simple identifier or a key value pair whose key
+ * is a simple identifier and value is a string literal. The identifier option and key
+ * value pair can not be mixed in, they should be either all simple identifiers
+ * or all key value pairs.
+ *
+ * <p>We support 2 kinds of hints in the parser:
+ * <ul>
+ * <li>Top mode hint, right after the select keyword, i.e.:
+ * <pre>
+ * select /*+ hint1, hint2, ... */ ...
+ * </pre>
+ * </li>
+ * <li>Table hint: right after the referenced table name, i.e.:
+ * <pre>
+ * select f0, f1, f2 from t1 /*+ hint1, hint2, ... */ ...
+ * </pre>
+ * </li>
+ * </ul>
+ */
+public class SqlHint extends SqlCall {
+ //~ Instance fields --------------------------------------------------------
+
+ private final SqlIdentifier name;
+ private final SqlNodeList options;
+ private final List<String> optionList;
+ private final Map<String, String> optionKVPairs;
+
+ private static final SqlOperator OPERATOR =
+ new SqlSpecialOperator("HINT", SqlKind.HINT);
+
+ //~ Constructors -----------------------------------------------------------
+
+ public SqlHint(SqlParserPos pos, SqlIdentifier name, SqlNodeList options) {
+ super(pos);
+ this.name = name;
+ this.options = options;
+ this.optionList = getOptionAsList();
+ this.optionKVPairs = getOptionAsKVPairs();
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ public SqlOperator getOperator() {
+ return OPERATOR;
+ }
+
+ public List<SqlNode> getOperandList() {
+ return ImmutableList.of(name, options);
+ }
+
+ /**
+ * @return The sql hint name
+ */
+ public String getName() {
+ return name.getSimple();
+ }
+
+ public List<String> getOptionList() {
+ return optionList;
+ }
+
+ public Map<String, String> getOptionKVPairs() {
+ return optionKVPairs;
+ }
+
+ /**
+ * @return A string list if the hint option is a list of
+ * simple SQL identifier, else an empty list
+ */
+ private List<String> getOptionAsList() {
+ if (this.options.size() == 0 || isOptionsAsKVPairs()) {
+ return ImmutableList.of();
+ } else {
+ final List<String> attrs = this.options.getList().stream()
+ .map(node -> ((SqlIdentifier) node).getSimple())
+ .collect(Collectors.toList());
+ return ImmutableList.copyOf(attrs);
+ }
+ }
+
+ /**
+ * @return A key value string map if the hint option is a list of
+ * pair that contains a simple SQL identifier and a string literal,
+ * else an empty map
+ */
+ private Map<String, String> getOptionAsKVPairs() {
+ if (this.options.size() == 0 || !isOptionsAsKVPairs()) {
+ return ImmutableMap.of();
+ } else {
+ final Map<String, String> attrs = new HashMap<>();
+ for (int i = 0; i < this.options.size() - 1; i += 2) {
+ final SqlNode k = this.options.get(i);
+ final SqlNode v = this.options.get(i + 1);
+ assert k instanceof SqlIdentifier;
+ assert v instanceof SqlLiteral;
+ attrs.put(((SqlIdentifier) k).getSimple(),
+ ((SqlLiteral) v).getValueAs(String.class));
+ }
+ return ImmutableMap.copyOf(attrs);
+ }
+ }
+
+ private boolean isOptionsAsKVPairs() {
+ return this.options.getList()
+ .stream()
+ .anyMatch(node -> node instanceof SqlLiteral);
+ }
+
+ @Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
+ name.unparse(writer, leftPrec, rightPrec);
+ if (this.options.size() > 0) {
+ SqlWriter.Frame frame = writer.startList(SqlWriter.FrameTypeEnum.FUN_CALL, "(", ")");
+ for (int i = 0; i < options.size(); i++) {
+ SqlNode option = options.get(i);
+ SqlNode nextOption = i < options.size() - 1 ? options.get(i + 1) : null;
+ writer.sep(",", false);
+ option.unparse(writer, leftPrec, rightPrec);
+ if (nextOption instanceof SqlLiteral) {
+ writer.print("=");
+ nextOption.unparse(writer, leftPrec, rightPrec);
+ i += 1;
+ }
+ }
+ writer.endList(frame);
+ }
+ }
+}
+
+// End SqlHint.java
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlKind.java b/core/src/main/java/org/apache/calcite/sql/SqlKind.java
index 1a27ada..5ae3cab 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlKind.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlKind.java
@@ -92,6 +92,16 @@ public enum SqlKind {
SELECT,
/**
+ * Sql Hint statement.
+ */
+ HINT,
+
+ /**
+ * Table reference.
+ */
+ TABLE_REF,
+
+ /**
* JOIN operator or compound FROM clause.
*
* <p>A FROM clause with more than one table is represented as if it were a
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlProcedureCallOperator.java b/core/src/main/java/org/apache/calcite/sql/SqlProcedureCallOperator.java
index ef02914..e3537b4 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlProcedureCallOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlProcedureCallOperator.java
@@ -56,6 +56,7 @@ public class SqlProcedureCallOperator extends SqlPrefixOperator {
null,
null,
null,
+ null,
null);
}
}
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlSelect.java b/core/src/main/java/org/apache/calcite/sql/SqlSelect.java
index f98ec2e..3f9dbc6 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlSelect.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlSelect.java
@@ -48,6 +48,7 @@ public class SqlSelect extends SqlCall {
SqlNodeList orderBy;
SqlNode offset;
SqlNode fetch;
+ SqlNodeList hints;
//~ Constructors -----------------------------------------------------------
@@ -61,7 +62,8 @@ public class SqlSelect extends SqlCall {
SqlNodeList windowDecls,
SqlNodeList orderBy,
SqlNode offset,
- SqlNode fetch) {
+ SqlNode fetch,
+ SqlNodeList hints) {
super(pos);
this.keywordList = Objects.requireNonNull(keywordList != null
? keywordList : new SqlNodeList(pos));
@@ -75,6 +77,7 @@ public class SqlSelect extends SqlCall {
this.orderBy = orderBy;
this.offset = offset;
this.fetch = fetch;
+ this.hints = hints;
}
//~ Methods ----------------------------------------------------------------
@@ -212,6 +215,19 @@ public class SqlSelect extends SqlCall {
this.fetch = fetch;
}
+ public void setHints(SqlNodeList hints) {
+ this.hints = hints;
+ }
+
+ public SqlNodeList getHints() {
+ return this.hints;
+ }
+
+ public boolean hasHints() {
+ // The hints may be passed as null explicitly.
+ return this.hints != null && this.hints.size() > 0;
+ }
+
public void validate(SqlValidator validator, SqlValidatorScope scope) {
validator.validateQuery(this, scope, validator.getUnknownType());
}
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlSelectOperator.java b/core/src/main/java/org/apache/calcite/sql/SqlSelectOperator.java
index 80961d4..856eeec 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlSelectOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlSelectOperator.java
@@ -71,7 +71,8 @@ public class SqlSelectOperator extends SqlOperator {
(SqlNodeList) operands[6],
(SqlNodeList) operands[7],
operands[8],
- operands[9]);
+ operands[9],
+ (SqlNodeList) operands[10]);
}
/**
@@ -104,6 +105,7 @@ public class SqlSelectOperator extends SqlOperator {
SqlNodeList orderBy,
SqlNode offset,
SqlNode fetch,
+ SqlNodeList hints,
SqlParserPos pos) {
return new SqlSelect(
pos,
@@ -116,7 +118,8 @@ public class SqlSelectOperator extends SqlOperator {
windowDecls,
orderBy,
offset,
- fetch);
+ fetch,
+ hints);
}
public <R> void acceptCall(
@@ -139,6 +142,14 @@ public class SqlSelectOperator extends SqlOperator {
final SqlWriter.Frame selectFrame =
writer.startList(SqlWriter.FrameTypeEnum.SELECT);
writer.sep("SELECT");
+
+ if (select.hasHints()) {
+ writer.sep("/*+");
+ select.hints.unparse(writer, leftPrec, rightPrec);
+ writer.print("*/");
+ writer.newlineAndIndent();
+ }
+
for (int i = 0; i < select.keywordList.size(); i++) {
final SqlNode keyword = select.keywordList.get(i);
keyword.unparse(writer, 0, 0);
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlTableRef.java b/core/src/main/java/org/apache/calcite/sql/SqlTableRef.java
new file mode 100644
index 0000000..87a6f6c
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/SqlTableRef.java
@@ -0,0 +1,72 @@
+/*
+ * 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.calcite.sql;
+
+import org.apache.calcite.sql.parser.SqlParserPos;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * A <code>SqlTableRef</code> is a node of a parse tree which represents
+ * a table reference.
+ *
+ * <p>It can be attached with a sql hint statement, see {@link SqlHint} for details.
+ */
+public class SqlTableRef extends SqlCall {
+
+ //~ Instance fields --------------------------------------------------------
+
+ private final SqlIdentifier tableName;
+ private final SqlNodeList hints;
+
+ //~ Static fields/initializers ---------------------------------------------
+
+ private static final SqlOperator OPERATOR =
+ new SqlSpecialOperator("TABLE_REF", SqlKind.TABLE_REF);
+
+ //~ Constructors -----------------------------------------------------------
+
+ public SqlTableRef(SqlParserPos pos, SqlIdentifier tableName, SqlNodeList hints) {
+ super(pos);
+ this.tableName = tableName;
+ this.hints = hints;
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ public SqlOperator getOperator() {
+ return OPERATOR;
+ }
+
+ public List<SqlNode> getOperandList() {
+ return ImmutableList.of(tableName, hints);
+ }
+
+ @Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
+ tableName.unparse(writer, leftPrec, rightPrec);
+ if (this.hints != null && this.hints.size() > 0) {
+ writer.newlineAndIndent();
+ writer.keyword("/*+");
+ this.hints.unparse(writer, 0, 0);
+ writer.keyword("*/");
+ }
+ }
+}
+
+// End SqlTableRef.java
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlUtil.java b/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
index bbd1a6a..5740fdf 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
@@ -19,6 +19,10 @@ package org.apache.calcite.sql;
import org.apache.calcite.avatica.util.ByteString;
import org.apache.calcite.linq4j.Ord;
import org.apache.calcite.linq4j.function.Functions;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.hint.HintStrategyTable;
+import org.apache.calcite.rel.hint.Hintable;
+import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rel.type.RelDataTypePrecedenceList;
@@ -973,6 +977,51 @@ public abstract class SqlUtil {
}
}
+ /**
+ * Returns an immutable list of {@link RelHint} from sql hints, with a given
+ * inherit path from the root node.
+ *
+ * <p>The inherit path would be empty [].
+ *
+ * @param hintStrategies The hint strategies to validate if the sql hints
+ * @param sqlHints The sql hints nodes
+ * @return the {@code RelHint} list
+ */
+ public static List<RelHint> getRelHint(HintStrategyTable hintStrategies, SqlNodeList sqlHints) {
+ final List<RelHint> relHints = new ArrayList<>();
+ if (sqlHints == null || sqlHints.size() == 0) {
+ return relHints;
+ }
+ for (SqlNode node : sqlHints) {
+ assert node instanceof SqlHint;
+ final SqlHint sqlHint = (SqlHint) node;
+ hintStrategies.validateHint(sqlHint.getName());
+ final RelHint relHint = new RelHint(new ArrayList<>(),
+ sqlHint.getName(), sqlHint.getOptionList(), sqlHint.getOptionKVPairs());
+ relHints.add(relHint);
+ }
+ return ImmutableList.copyOf(relHints);
+ }
+
+ /**
+ * Attach the {@code hints} to {@code rel} with specified hint strategies.
+ *
+ * @param hintStrategies The strategies to filter the hints
+ * @param hints The original hints to be attached
+ * @return A copy of {@code rel} if there are any hints can be attached given
+ * the hint strategies, or the original node if such hints don't exist
+ */
+ public static RelNode attachRelHint(
+ HintStrategyTable hintStrategies,
+ List<RelHint> hints,
+ Hintable rel) {
+ final List<RelHint> relHints = hintStrategies.apply(hints, (RelNode) rel);
+ if (relHints.size() > 0) {
+ return rel.attachHints(relHints);
+ }
+ return (RelNode) rel;
+ }
+
//~ Inner Classes ----------------------------------------------------------
/**
diff --git a/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java b/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java
index 52df66c..7c2654c 100644
--- a/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java
@@ -143,7 +143,8 @@ public class MysqlSqlDialect extends SqlDialect {
final SqlNode operand = ((SqlBasicCall) aggCall).operand(0);
final SqlLiteral nullLiteral = SqlLiteral.createNull(SqlParserPos.ZERO);
final SqlNode unionOperand = new SqlSelect(SqlParserPos.ZERO, SqlNodeList.EMPTY,
- SqlNodeList.of(nullLiteral), null, null, null, null, SqlNodeList.EMPTY, null, null, null);
+ SqlNodeList.of(nullLiteral), null, null, null, null,
+ SqlNodeList.EMPTY, null, null, null, SqlNodeList.EMPTY);
// For MySQL, generate
// CASE COUNT(*)
// WHEN 0 THEN NULL
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/IdentifierNamespace.java b/core/src/main/java/org/apache/calcite/sql/validate/IdentifierNamespace.java
index 8546742..060482c 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/IdentifierNamespace.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/IdentifierNamespace.java
@@ -20,6 +20,7 @@ import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlNodeList;
import org.apache.calcite.sql.parser.SqlParserPos;
@@ -89,8 +90,14 @@ public class IdentifierNamespace extends AbstractNamespace {
switch (node.getKind()) {
case EXTEND:
final SqlCall call = (SqlCall) node;
- return Pair.of((SqlIdentifier) call.getOperandList().get(0),
- (SqlNodeList) call.getOperandList().get(1));
+ final SqlNode operand0 = call.operand(0);
+ final SqlIdentifier identifier = operand0.getKind() == SqlKind.TABLE_REF
+ ? ((SqlCall) operand0).operand(0)
+ : (SqlIdentifier) operand0;
+ return Pair.of(identifier, call.operand(1));
+ case TABLE_REF:
+ final SqlCall tableRef = (SqlCall) node;
+ return Pair.of(tableRef.operand(0), null);
default:
return Pair.of((SqlIdentifier) node, null);
}
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
index 9478384..8855ea4 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
@@ -1077,13 +1077,19 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
// Handle extended identifiers.
final SqlCall call = (SqlCall) node;
switch (call.getOperator().getKind()) {
+ case TABLE_REF:
+ return getNamespace(call.operand(0), scope);
case EXTEND:
- final SqlIdentifier id = (SqlIdentifier) call.getOperandList().get(0);
+ final SqlNode operand0 = call.getOperandList().get(0);
+ final SqlIdentifier identifier = operand0.getKind() == SqlKind.TABLE_REF
+ ? ((SqlCall) operand0).operand(0)
+ : (SqlIdentifier) operand0;
final DelegatingScope idScope = (DelegatingScope) scope;
- return getNamespace(id, idScope);
+ return getNamespace(identifier, idScope);
case AS:
final SqlNode nested = call.getOperandList().get(0);
switch (nested.getKind()) {
+ case TABLE_REF:
case EXTEND:
return getNamespace(nested, scope);
}
@@ -1116,6 +1122,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
return ns;
}
// fall through
+ case TABLE_REF:
case SNAPSHOT:
case OVER:
case COLLECTION_TABLE:
@@ -1231,7 +1238,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
new SqlNodeList(SqlParserPos.ZERO);
selectList.add(SqlIdentifier.star(SqlParserPos.ZERO));
return new SqlSelect(node.getParserPosition(), null, selectList, node,
- null, null, null, null, null, null, null);
+ null, null, null, null, null, null, null, null);
}
case ORDER_BY: {
@@ -1288,7 +1295,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
}
return new SqlSelect(SqlParserPos.ZERO, null, selectList, orderBy.query,
null, null, null, null, orderList, orderBy.offset,
- orderBy.fetch);
+ orderBy.fetch, null);
}
case EXPLICIT_TABLE: {
@@ -1297,7 +1304,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
final SqlNodeList selectList = new SqlNodeList(SqlParserPos.ZERO);
selectList.add(SqlIdentifier.star(SqlParserPos.ZERO));
return new SqlSelect(SqlParserPos.ZERO, null, selectList, call.operand(0),
- null, null, null, null, null, null, null);
+ null, null, null, null, null, null, null, null);
}
case DELETE: {
@@ -1391,7 +1398,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
call.getCondition());
SqlSelect select =
new SqlSelect(SqlParserPos.ZERO, null, selectList, outerJoin, null,
- null, null, null, null, null, null);
+ null, null, null, null, null, null, null);
call.setSourceSelect(select);
// Source for the insert call is a select of the source table
@@ -1409,7 +1416,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
final SqlNode insertSource = SqlNode.clone(sourceTableRef);
select =
new SqlSelect(SqlParserPos.ZERO, null, selectList, insertSource, null,
- null, null, null, null, null, null);
+ null, null, null, null, null, null, null);
insertCall.setSource(select);
}
}
@@ -1474,7 +1481,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
}
source =
new SqlSelect(SqlParserPos.ZERO, null, selectList, source, null, null,
- null, null, null, null, null);
+ null, null, null, null, null, null);
source = SqlValidatorUtil.addAlias(source, UPDATE_SRC_ALIAS);
SqlMerge mergeCall =
new SqlMerge(updateCall.getParserPosition(), target, condition, source,
@@ -1529,7 +1536,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
call.getAlias().getSimple());
}
return new SqlSelect(SqlParserPos.ZERO, null, selectList, sourceTable,
- call.getCondition(), null, null, null, null, null, null);
+ call.getCondition(), null, null, null, null, null, null, null);
}
/**
@@ -1550,7 +1557,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
call.getAlias().getSimple());
}
return new SqlSelect(SqlParserPos.ZERO, null, selectList, sourceTable,
- call.getCondition(), null, null, null, null, null, null);
+ call.getCondition(), null, null, null, null, null, null, null);
}
/**
@@ -2319,6 +2326,22 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
return newNode;
+ case TABLE_REF:
+ call = (SqlCall) node;
+ registerFrom(parentScope,
+ usingScope,
+ register,
+ call.operand(0),
+ enclosingNode,
+ alias,
+ extendList,
+ forceNullable,
+ lateral);
+ if (extendList != null && extendList.size() != 0) {
+ return enclosingNode;
+ }
+ return newNode;
+
case EXTEND:
final SqlCall extend = (SqlCall) node;
return registerFrom(parentScope,
@@ -3108,6 +3131,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
Objects.requireNonNull(targetRowType);
switch (node.getKind()) {
case AS:
+ case TABLE_REF:
validateFrom(
((SqlCall) node).operand(0),
targetRowType,
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
index fa96e3e..57e01a6 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
@@ -1150,7 +1150,7 @@ public class SqlValidatorUtil {
final SqlSelect select0 = new SqlSelect(SqlParserPos.ZERO, null,
new SqlNodeList(Collections.singletonList(expr), SqlParserPos.ZERO),
new SqlIdentifier(tableName, SqlParserPos.ZERO),
- null, null, null, null, null, null, null);
+ null, null, null, null, null, null, null, null);
Prepare.CatalogReader catalogReader = createSingleTableCatalogReader(
caseSensitive,
tableName,
diff --git a/core/src/main/java/org/apache/calcite/sql2rel/RelStructuredTypeFlattener.java b/core/src/main/java/org/apache/calcite/sql2rel/RelStructuredTypeFlattener.java
index 521edfa..8b9f499 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/RelStructuredTypeFlattener.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/RelStructuredTypeFlattener.java
@@ -19,6 +19,7 @@ package org.apache.calcite.sql2rel;
import org.apache.calcite.linq4j.Ord;
import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.RelCollation;
import org.apache.calcite.rel.RelFieldCollation;
@@ -437,6 +438,7 @@ public class RelStructuredTypeFlattener implements ReflectiveVisitor {
getNewForOldRel(rel.getRight()),
rel.getCondition().accept(new RewriteRexShuttle()),
rel.getVariablesSet(), rel.getJoinType());
+ newRel = (LogicalJoin) RelOptUtil.copyRelHints(rel, newRel);
setNewForOldRel(rel, newRel);
}
@@ -508,6 +510,7 @@ public class RelStructuredTypeFlattener implements ReflectiveVisitor {
RelNode newRel = relBuilder.push(newInput)
.projectNamed(newProjects, newNames, true)
.build();
+ newRel = RelOptUtil.copyRelHints(rel, newRel);
setNewForOldRel(rel, newRel);
}
@@ -729,6 +732,7 @@ public class RelStructuredTypeFlattener implements ReflectiveVisitor {
if (!SqlTypeUtil.isFlat(rel.getRowType())) {
newRel = coverNewRelByFlatteningProjection(rel, newRel);
}
+ newRel = RelOptUtil.copyRelHints(rel, newRel);
setNewForOldRel(rel, newRel);
}
diff --git a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
index 186b3f7..97a94ce 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -34,6 +34,7 @@ import org.apache.calcite.rel.RelCollations;
import org.apache.calcite.rel.RelFieldCollation;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelRoot;
+import org.apache.calcite.rel.RelShuttleImpl;
import org.apache.calcite.rel.SingleRel;
import org.apache.calcite.rel.core.AggregateCall;
import org.apache.calcite.rel.core.Collect;
@@ -49,6 +50,9 @@ import org.apache.calcite.rel.core.Sort;
import org.apache.calcite.rel.core.TableScan;
import org.apache.calcite.rel.core.Uncollect;
import org.apache.calcite.rel.core.Values;
+import org.apache.calcite.rel.hint.HintStrategyTable;
+import org.apache.calcite.rel.hint.Hintable;
+import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.logical.LogicalAggregate;
import org.apache.calcite.rel.logical.LogicalCorrelate;
import org.apache.calcite.rel.logical.LogicalFilter;
@@ -187,9 +191,11 @@ import java.math.BigDecimal;
import java.util.AbstractList;
import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
@@ -241,6 +247,7 @@ public class SqlToRelConverter {
private final SqlOperatorTable opTab;
protected final RelDataTypeFactory typeFactory;
private final SqlNodeToRexConverter exprConverter;
+ private final HintStrategyTable hintStrategies;
private int explainParamCount;
public final SqlToRelConverter.Config config;
private final RelBuilder relBuilder;
@@ -320,11 +327,13 @@ public class SqlToRelConverter {
this.subQueryConverter = new NoOpSubQueryConverter();
this.rexBuilder = cluster.getRexBuilder();
this.typeFactory = rexBuilder.getTypeFactory();
- this.cluster = Objects.requireNonNull(cluster);
this.exprConverter = new SqlNodeToRexConverterImpl(convertletTable);
this.explainParamCount = 0;
this.config = new ConfigBuilder().withConfig(config).build();
this.relBuilder = config.getRelBuilderFactory().create(cluster, null);
+ this.hintStrategies = config.getHintStrategyTable();
+ this.cluster = Objects.requireNonNull(cluster)
+ .withHintStrategies(this.hintStrategies);
}
//~ Methods ----------------------------------------------------------------
@@ -581,8 +590,18 @@ public class SqlToRelConverter {
}
final RelDataType validatedRowType = validator.getValidatedNodeType(query);
+ List<RelHint> hints = new ArrayList<>();
+ if (query.getKind() == SqlKind.SELECT) {
+ final SqlSelect select = (SqlSelect) query;
+ if (select.hasHints()) {
+ hints = SqlUtil.getRelHint(hintStrategies, select.getHints());
+ }
+ }
+ // propagate the hints.
+ result = result.accept(new RelHintPropagateShuttle(this.hintStrategies));
return RelRoot.of(result, validatedRowType, query.getKind())
- .withCollation(collation);
+ .withCollation(collation)
+ .withHints(hints);
}
private static boolean isStream(SqlNode query) {
@@ -675,10 +694,30 @@ public class SqlToRelConverter {
if (select.isDistinct()) {
distinctify(bb, true);
}
+
convertOrder(
select, bb, collation, orderExprList, select.getOffset(),
select.getFetch());
- bb.setRoot(bb.root, true);
+
+ if (select.hasHints()) {
+ final List<RelHint> hints = SqlUtil.getRelHint(hintStrategies, select.getHints());
+ // Attach the hints to the first project we found from the root node.
+ bb.setRoot(bb.root
+ .accept(
+ new RelShuttleImpl() {
+ boolean attached = false;
+ @Override public RelNode visit(LogicalProject project) {
+ if (!attached) {
+ attached = true;
+ return project.attachHints(hints);
+ } else {
+ return project;
+ }
+ }
+ }), true);
+ } else {
+ bb.setRoot(bb.root, true);
+ }
}
/**
@@ -2046,15 +2085,23 @@ public class SqlToRelConverter {
}
return;
+ case TABLE_REF:
+ call = (SqlCall) from;
+ convertIdentifier(bb, call.operand(0), null, call.operand(1));
+ return;
+
case IDENTIFIER:
- convertIdentifier(bb, (SqlIdentifier) from, null);
+ convertIdentifier(bb, (SqlIdentifier) from, null, null);
return;
case EXTEND:
call = (SqlCall) from;
- SqlIdentifier id = (SqlIdentifier) call.getOperandList().get(0);
+ final SqlNode operand0 = call.getOperandList().get(0);
+ final SqlIdentifier id = operand0.getKind() == SqlKind.TABLE_REF
+ ? ((SqlCall) operand0).operand(0)
+ : (SqlIdentifier) operand0;
SqlNodeList extendedColumns = (SqlNodeList) call.getOperandList().get(1);
- convertIdentifier(bb, id, extendedColumns);
+ convertIdentifier(bb, id, extendedColumns, null);
return;
case SNAPSHOT:
@@ -2329,7 +2376,7 @@ public class SqlToRelConverter {
}
private void convertIdentifier(Blackboard bb, SqlIdentifier id,
- SqlNodeList extendedColumns) {
+ SqlNodeList extendedColumns, SqlNodeList tableHints) {
final SqlValidatorNamespace fromNamespace =
validator.getNamespace(id).resolve();
if (fromNamespace.getNode() != null) {
@@ -2352,10 +2399,12 @@ public class SqlToRelConverter {
table = table.extend(extendedFields);
}
final RelNode tableRel;
+ final List<RelHint> hints = SqlUtil.getRelHint(hintStrategies, tableHints);
if (config.isConvertTableAccess()) {
- tableRel = toRel(table);
+ tableRel = toRel(table, hints);
} else {
- tableRel = LogicalTableScan.create(cluster, table);
+ tableRel = SqlUtil.attachRelHint(hintStrategies,
+ hints, LogicalTableScan.create(cluster, table));
}
bb.setRoot(tableRel, true);
if (usedDataset[0]) {
@@ -2392,7 +2441,7 @@ public class SqlToRelConverter {
final RelDataType rowType = table.getRowType(typeFactory);
RelOptTable relOptTable = RelOptTableImpl.create(null, rowType, table,
udf.getNameAsId().names);
- RelNode converted = toRel(relOptTable);
+ RelNode converted = toRel(relOptTable, null);
bb.setRoot(converted, true);
return;
}
@@ -3323,8 +3372,11 @@ public class SqlToRelConverter {
return ViewExpanders.toRelContext(viewExpander, cluster);
}
- public RelNode toRel(final RelOptTable table) {
- final RelNode scan = table.toRel(createToRelContext());
+ public RelNode toRel(final RelOptTable table, final List<RelHint> hints) {
+ final RelNode rel = table.toRel(createToRelContext());
+ final RelNode scan = rel instanceof Hintable && hints != null && hints.size() > 0
+ ? SqlUtil.attachRelHint(hintStrategies, hints, (Hintable) rel)
+ : rel;
final InitializerExpressionFactory ief =
Util.first(table.unwrap(InitializerExpressionFactory.class),
@@ -5588,6 +5640,117 @@ public class SqlToRelConverter {
}
}
+ /**
+ * A {@code RelShuttle} which propagates all the hints of relational expression to
+ * their children nodes.
+ *
+ * <p>Given a plan:
+ *
+ * <blockquote><pre>
+ * Filter Hint1
+ * +- Join
+ * +- Scan
+ * +- Project Hint2
+ * +- Scan
+ * </pre></blockquote>
+ *
+ * <p>Every hint has a {@code inheritPath} (integers list) which records its propagate path,
+ * number `0` represents the hint is propagated from the first(left) child, number `1`
+ * represents the hint is propagated from the second(right) child, so the plan would have
+ * hints path as follows:
+ *
+ * <ol>
+ * <li>Filter would have hints {Hint1[]}</li>
+ * <li>Join would have hints {Hint1[0]}</li>
+ * <li>Scan would have hints {Hint1[0, 0]}</li>
+ * <li>Project would have hints {Hint1[0,1], Hint2}</li>
+ * <li>Scan2 would have hints {[Hint1[0, 1, 0], Hint2[0]}</li>
+ * </ol>
+ */
+ private static class RelHintPropagateShuttle extends RelShuttleImpl {
+ /**
+ * Stack recording the hints and its current inheritPath.
+ */
+ private final Deque<Pair<List<RelHint>, Deque<Integer>>> inheritPaths =
+ new ArrayDeque<>();
+
+ /**
+ * The hint strategies to decide if a hint should be attached to
+ * a relational expression.
+ */
+ private final HintStrategyTable hintStrategies;
+
+ RelHintPropagateShuttle(HintStrategyTable hintStrategies) {
+ this.hintStrategies = hintStrategies;
+ }
+
+ /**
+ * Visits a particular child of a parent.
+ */
+ protected RelNode visitChild(RelNode parent, int i, RelNode child) {
+ inheritPaths.forEach(inheritPath -> inheritPath.right.push(i));
+ try {
+ RelNode child2 = child.accept(this);
+ if (child2 != child) {
+ final List<RelNode> newInputs = new ArrayList<>(parent.getInputs());
+ newInputs.set(i, child2);
+ return parent.copy(parent.getTraitSet(), newInputs);
+ }
+ return parent;
+ } finally {
+ inheritPaths.forEach(inheritPath -> inheritPath.right.pop());
+ }
+ }
+
+ public RelNode visit(LogicalJoin join) {
+ final LogicalJoin join1 = (LogicalJoin) super.visit(join);
+ return attachHints(join1);
+ }
+
+ public RelNode visit(LogicalProject project) {
+ final List<RelHint> topHints = project.getHints();
+ final boolean hasHints = topHints != null && topHints.size() > 0;
+ if (hasHints) {
+ inheritPaths.push(Pair.of(topHints, new ArrayDeque<>()));
+ }
+ final RelNode project1 = super.visit(project);
+ if (hasHints) {
+ inheritPaths.pop();
+ }
+ return attachHints(project1);
+ }
+
+ public RelNode visit(TableScan scan) {
+ TableScan scan1 = (TableScan) super.visit(scan);
+ return attachHints(scan1);
+ }
+
+ private RelNode attachHints(RelNode original) {
+ assert original instanceof Hintable;
+ if (inheritPaths.size() > 0) {
+ final List<RelHint> hints = inheritPaths.stream()
+ .sorted(Comparator.comparingInt(o -> o.right.size()))
+ .map(path -> copyWithInheritPath(path.left, path.right))
+ .reduce(new ArrayList<>(), (acc, hints1) -> {
+ acc.addAll(hints1);
+ return acc;
+ });
+ final List<RelHint> filteredHints = hintStrategies.apply(hints, original);
+ if (filteredHints.size() > 0) {
+ return ((Hintable) original).attachHints(filteredHints);
+ }
+ }
+ return original;
+ }
+
+ private static List<RelHint> copyWithInheritPath(List<RelHint> hints,
+ Deque<Integer> inheritPath) {
+ return hints.stream()
+ .map(hint -> hint.copy(Arrays.asList(inheritPath.toArray(new Integer[]{}))))
+ .collect(Collectors.toList());
+ }
+ }
+
/** Creates a builder for a {@link Config}. */
public static ConfigBuilder configBuilder() {
return new ConfigBuilder();
@@ -5648,6 +5811,11 @@ public class SqlToRelConverter {
/** Returns the factory to create {@link RelBuilder}, never null. Default is
* {@link RelFactories#LOGICAL_BUILDER}. */
RelBuilderFactory getRelBuilderFactory();
+
+ /** Returns the hint strategies used to decide how the hints are propagated to
+ * the relational expressions. Default is
+ * {@link HintStrategyTable#EMPTY}. */
+ HintStrategyTable getHintStrategyTable();
}
/** Builder for a {@link Config}. */
@@ -5660,6 +5828,7 @@ public class SqlToRelConverter {
private boolean expand = true;
private int inSubQueryThreshold = DEFAULT_IN_SUB_QUERY_THRESHOLD;
private RelBuilderFactory relBuilderFactory = RelFactories.LOGICAL_BUILDER;
+ private HintStrategyTable hintStrategyTable = HintStrategyTable.EMPTY;
private ConfigBuilder() {}
@@ -5673,6 +5842,7 @@ public class SqlToRelConverter {
this.expand = config.isExpand();
this.inSubQueryThreshold = config.getInSubQueryThreshold();
this.relBuilderFactory = config.getRelBuilderFactory();
+ this.hintStrategyTable = config.getHintStrategyTable();
return this;
}
@@ -5722,11 +5892,17 @@ public class SqlToRelConverter {
return this;
}
+ public ConfigBuilder withHintStrategyTable(
+ HintStrategyTable hintStrategyTable) {
+ this.hintStrategyTable = hintStrategyTable;
+ return this;
+ }
+
/** Builds a {@link Config}. */
public Config build() {
return new ConfigImpl(convertTableAccess, decorrelationEnabled,
trimUnusedFields, createValuesRel, explain, expand,
- inSubQueryThreshold, relBuilderFactory);
+ inSubQueryThreshold, relBuilderFactory, hintStrategyTable);
}
}
@@ -5741,11 +5917,13 @@ public class SqlToRelConverter {
private final boolean expand;
private final int inSubQueryThreshold;
private final RelBuilderFactory relBuilderFactory;
+ private final HintStrategyTable hintStrategyTable;
private ConfigImpl(boolean convertTableAccess, boolean decorrelationEnabled,
boolean trimUnusedFields, boolean createValuesRel, boolean explain,
boolean expand, int inSubQueryThreshold,
- RelBuilderFactory relBuilderFactory) {
+ RelBuilderFactory relBuilderFactory,
+ HintStrategyTable hintStrategyTable) {
this.convertTableAccess = convertTableAccess;
this.decorrelationEnabled = decorrelationEnabled;
this.trimUnusedFields = trimUnusedFields;
@@ -5754,6 +5932,7 @@ public class SqlToRelConverter {
this.expand = expand;
this.inSubQueryThreshold = inSubQueryThreshold;
this.relBuilderFactory = relBuilderFactory;
+ this.hintStrategyTable = hintStrategyTable;
}
public boolean isConvertTableAccess() {
@@ -5787,6 +5966,10 @@ public class SqlToRelConverter {
public RelBuilderFactory getRelBuilderFactory() {
return relBuilderFactory;
}
+
+ public HintStrategyTable getHintStrategyTable() {
+ return hintStrategyTable;
+ }
}
}
diff --git a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 13f986d..2739a60 100644
--- a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -8577,6 +8577,122 @@ public class SqlParserTest {
sql(sql2).ok(expected);
}
+ @Test public void testQueryHint() {
+ final String sql = "select "
+ + "/*+ properties(k1='v1', k2='v2'), no_hash_join, Index(idx1, idx2) */ "
+ + "empno, ename, deptno from emps";
+ final String expected = "SELECT\n"
+ + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `NO_HASH_JOIN`, `INDEX`(`IDX1`, `IDX2`) */\n"
+ + "`EMPNO`, `ENAME`, `DEPTNO`\n"
+ + "FROM `EMPS`";
+ sql(sql).ok(expected);
+ }
+
+ @Test public void testTableHintsInQuery() {
+ final String hint = "/*+ PROPERTIES(K1 ='v1', K2 ='v2'), INDEX(IDX0, IDX1) */";
+ final String sql1 = String.format(Locale.ROOT, "select * from t %s", hint);
+ final String expected1 = "SELECT *\n"
+ + "FROM `T`\n"
+ + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `INDEX`(`IDX0`, `IDX1`) */";
+ sql(sql1).ok(expected1);
+ final String sql2 = String.format(Locale.ROOT, "select * from\n"
+ + "(select * from t %s union all select * from t %s )", hint, hint);
+ final String expected2 = "SELECT *\n"
+ + "FROM (SELECT *\n"
+ + "FROM `T`\n"
+ + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `INDEX`(`IDX0`, `IDX1`) */\n"
+ + "UNION ALL\n"
+ + "SELECT *\n"
+ + "FROM `T`\n"
+ + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `INDEX`(`IDX0`, `IDX1`) */)";
+ sql(sql2).ok(expected2);
+ final String sql3 = String.format(Locale.ROOT, "select * from t %s join t %s", hint, hint);
+ final String expected3 = "SELECT *\n"
+ + "FROM `T`\n"
+ + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `INDEX`(`IDX0`, `IDX1`) */\n"
+ + "INNER JOIN `T`\n"
+ + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `INDEX`(`IDX0`, `IDX1`) */";
+ sql(sql3).ok(expected3);
+ }
+
+ @Test public void testTableHintsInInsert() {
+ final String sql = "insert into emps\n"
+ + "/*+ PROPERTIES(k1='v1', k2='v2'), INDEX(idx0, idx1) */\n"
+ + "select * from emps";
+ final String expected = "INSERT INTO `EMPS`\n"
+ + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `INDEX`(`IDX0`, `IDX1`) */\n"
+ + "(SELECT *\n"
+ + "FROM `EMPS`)";
+ sql(sql).ok(expected);
+ }
+
+ @Test public void testTableHintsInDelete() {
+ final String sql = "delete from emps\n"
+ + "/*+ properties(k1='v1', k2='v2'), index(idx1, idx2), no_hash_join */\n"
+ + "where empno=12";
+ final String expected = "DELETE FROM `EMPS`\n"
+ + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `INDEX`(`IDX1`, `IDX2`), `NO_HASH_JOIN` */\n"
+ + "WHERE (`EMPNO` = 12)";
+ sql(sql).ok(expected);
+ }
+
+ @Test public void testTableHintsInUpdate() {
+ final String sql = "update emps\n"
+ + "/*+ properties(k1='v1', k2='v2'), index(idx1, idx2), no_hash_join */\n"
+ + "set empno = empno + 1, sal = sal - 1 \n"
+ + "where empno=12";
+ final String expected = "UPDATE `EMPS`\n"
+ + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), "
+ + "`INDEX`(`IDX1`, `IDX2`), `NO_HASH_JOIN` */ "
+ + "SET `EMPNO` = (`EMPNO` + 1)\n"
+ + ", `SAL` = (`SAL` - 1)\n"
+ + "WHERE (`EMPNO` = 12)";
+ sql(sql).ok(expected);
+ }
+
+ @Test public void testTableHintsInMerge() {
+ final String sql = "merge into emps\n"
+ + "/*+ properties(k1='v1', k2='v2'), index(idx1, idx2), no_hash_join */ e\n"
+ + "using tempemps as t\n"
+ + "on e.empno = t.empno\n"
+ + "when matched then update\n"
+ + "set name = t.name, deptno = t.deptno, salary = t.salary * .1\n"
+ + "when not matched then insert (name, dept, salary)\n"
+ + "values(t.name, 10, t.salary * .15)";
+ final String expected = "MERGE INTO `EMPS`\n"
+ + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), "
+ + "`INDEX`(`IDX1`, `IDX2`), `NO_HASH_JOIN` */ "
+ + "AS `E`\n"
+ + "USING `TEMPEMPS` AS `T`\n"
+ + "ON (`E`.`EMPNO` = `T`.`EMPNO`)\n"
+ + "WHEN MATCHED THEN UPDATE SET `NAME` = `T`.`NAME`\n"
+ + ", `DEPTNO` = `T`.`DEPTNO`\n"
+ + ", `SALARY` = (`T`.`SALARY` * 0.1)\n"
+ + "WHEN NOT MATCHED THEN INSERT (`NAME`, `DEPT`, `SALARY`) "
+ + "(VALUES (ROW(`T`.`NAME`, 10, (`T`.`SALARY` * 0.15))))";
+ sql(sql).ok(expected);
+ }
+
+ @Test public void testInvalidHintFormat() {
+ final String sql1 = "select "
+ + "/*+ properties(k1^=^123, k2='v2'), no_hash_join() */ "
+ + "empno, ename, deptno from emps";
+ sql(sql1).fails("(?s).*Encountered \"= 123\" at line 1, column 25.\n.*");
+ final String sql2 = "select "
+ + "/*+ properties(k1, k2^=^'v2'), no_hash_join */ "
+ + "empno, ename, deptno from emps";
+ sql(sql2).fails("(?s).*Encountered \"=\" at line 1, column 29.\n.*");
+ final String sql3 = "select "
+ + "/*+ no_hash_join() */ "
+ + "empno, ename, deptno from emps";
+ // allow empty options.
+ final String expected3 = "SELECT\n"
+ + "/*+ `NO_HASH_JOIN` */\n"
+ + "`EMPNO`, `ENAME`, `DEPTNO`\n"
+ + "FROM `EMPS`";
+ sql(sql3).ok(expected3);
+ }
+
//~ Inner Interfaces -------------------------------------------------------
/**
diff --git a/core/src/test/java/org/apache/calcite/test/CalciteSuite.java b/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
index 88a949d..4019c2e 100644
--- a/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
+++ b/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
@@ -140,6 +140,7 @@ import org.junit.runners.Suite;
SqlValidatorUtilTest.class,
TypeFinderTest.class,
RexShuttleTest.class,
+ SqlHintsConverterTest.class,
// medium tests (above 0.1s)
SqlParserTest.class,
diff --git a/core/src/test/java/org/apache/calcite/test/MockRelOptPlanner.java b/core/src/test/java/org/apache/calcite/test/MockRelOptPlanner.java
index 77c8427..6f51176 100644
--- a/core/src/test/java/org/apache/calcite/test/MockRelOptPlanner.java
+++ b/core/src/test/java/org/apache/calcite/test/MockRelOptPlanner.java
@@ -35,6 +35,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.function.BiFunction;
/**
* MockRelOptPlanner is a mock implementation of the {@link RelOptPlanner}
@@ -233,7 +234,8 @@ public class MockRelOptPlanner extends AbstractRelOptPlanner {
}
// implement RelOptRuleCall
- public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv) {
+ public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv,
+ BiFunction<RelNode, RelNode, RelNode> handler) {
transformationResult = rel;
}
}
diff --git a/core/src/test/java/org/apache/calcite/test/SqlHintsConverterTest.java b/core/src/test/java/org/apache/calcite/test/SqlHintsConverterTest.java
new file mode 100644
index 0000000..9659fbc
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/test/SqlHintsConverterTest.java
@@ -0,0 +1,516 @@
+/*
+ * 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.calcite.test;
+
+import org.apache.calcite.adapter.enumerable.EnumerableConvention;
+import org.apache.calcite.adapter.enumerable.EnumerableHashJoin;
+import org.apache.calcite.adapter.enumerable.EnumerableRules;
+import org.apache.calcite.plan.Convention;
+import org.apache.calcite.plan.ConventionTraitDef;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.plan.hep.HepPlanner;
+import org.apache.calcite.plan.hep.HepProgram;
+import org.apache.calcite.plan.hep.HepProgramBuilder;
+import org.apache.calcite.plan.volcano.VolcanoPlanner;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelShuttleImpl;
+import org.apache.calcite.rel.RelVisitor;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.JoinInfo;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.hint.HintStrategies;
+import org.apache.calcite.rel.hint.HintStrategyTable;
+import org.apache.calcite.rel.hint.RelHint;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.calcite.rel.logical.LogicalProject;
+import org.apache.calcite.rel.rules.FilterMergeRule;
+import org.apache.calcite.rel.rules.FilterProjectTransposeRule;
+import org.apache.calcite.rel.rules.ProjectMergeRule;
+import org.apache.calcite.sql.SqlDelete;
+import org.apache.calcite.sql.SqlInsert;
+import org.apache.calcite.sql.SqlNodeList;
+import org.apache.calcite.sql.SqlTableRef;
+import org.apache.calcite.sql.SqlUpdate;
+import org.apache.calcite.sql.SqlUtil;
+import org.apache.calcite.sql2rel.SqlToRelConverter;
+import org.apache.calcite.tools.Program;
+import org.apache.calcite.tools.Programs;
+import org.apache.calcite.tools.RuleSet;
+import org.apache.calcite.tools.RuleSets;
+import org.apache.calcite.util.Util;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+/**
+ * Unit test for {@link org.apache.calcite.rel.hint.RelHint}.
+ */
+public class SqlHintsConverterTest extends SqlToRelTestBase {
+ //~ Static fields/initializers ---------------------------------------------
+
+ private static final String HINT = "properties(k1='v1', k2='v2'), index(ename), no_hash_join";
+
+ private static final RelHint PROPS_HINT = new RelHint(new ArrayList<>(),
+ "PROPERTIES", null,
+ ImmutableMap.of("K1", "v1", "K2", "v2"));
+
+ private static final RelHint IDX_HINT = new RelHint(new ArrayList<>(), "INDEX",
+ ImmutableList.of("ENAME"), null);
+
+ private static final RelHint JOIN_HINT = new RelHint(new ArrayList<>(), "NO_HASH_JOIN",
+ null, null);
+
+ private static final HintStrategyTable HINT_STRATEGY_TABLE = createHintStrategies();
+
+ @Rule public ExpectedException expectedEx = ExpectedException.none();
+
+ //~ Tests ------------------------------------------------------------------
+
+ @Test public void testQueryHint() {
+ final String sql = withHint("select /*+ %s */ *\n"
+ + "from emp e1\n"
+ + "inner join dept d1 on e1.deptno = d1.deptno\n"
+ + "inner join emp e2 on e1.ename = e2.job");
+ final List<String> expectedHints = Arrays.asList(
+ "Project:[[PROPERTIES inheritPath:{} options:{K1=v1, K2=v2}], "
+ + "[INDEX inheritPath:{} options:[ENAME]], "
+ + "[NO_HASH_JOIN inheritPath:{}]]",
+ "LogicalJoin:[[NO_HASH_JOIN inheritPath:{0}]]",
+ "LogicalJoin:[[NO_HASH_JOIN inheritPath:{0}]]",
+ "TableScan:[[PROPERTIES inheritPath:{0} options:{K1=v1, K2=v2}], "
+ + "[INDEX inheritPath:{0} options:[ENAME]]]",
+ "TableScan:[[PROPERTIES inheritPath:{0, 1} options:{K1=v1, K2=v2}], "
+ + "[INDEX inheritPath:{0, 1} options:[ENAME]]]",
+ "TableScan:[[PROPERTIES inheritPath:{0, 1} options:{K1=v1, K2=v2}], "
+ + "[INDEX inheritPath:{0, 1} options:[ENAME]]]");
+ sql(sql).ok(expectedHints);
+ }
+
+ @Test public void testNestedQueryHint() {
+ final String sql = "select /*+ resource(parallelism='3') */ empno\n"
+ + "from (select /*+ resource(mem='20Mb')*/ empno, ename from emp)";
+ sql(sql).ok(
+ Collections.singletonList("Project:[[RESOURCE inheritPath:{} options:{PARALLELISM=3}]]"));
+ }
+
+ @Test public void testTwoLevelNestedQueryHint() {
+ final String sql = "select /*+ resource(parallelism='3'), no_hash_join */ empno\n"
+ + "from (select /*+ resource(mem='20Mb')*/ empno, ename\n"
+ + "from emp left join dept on emp.deptno = dept.deptno)";
+ final List<String> expectedHints = Arrays.asList(
+ "Project:[[RESOURCE inheritPath:{} options:{PARALLELISM=3}]"
+ + ", [NO_HASH_JOIN inheritPath:{}]]",
+ "LogicalJoin:[[NO_HASH_JOIN inheritPath:{0}]]");
+ sql(sql).ok(expectedHints);
+ }
+
+ @Test public void testInvalidQueryHint() {
+ final String sql = "select /*+ weird_hint */ empno\n"
+ + "from (select /*+ resource(mem='20Mb')*/ empno, ename\n"
+ + "from emp left join dept on emp.deptno = dept.deptno)";
+ sql(sql).fails("Hint: WEIRD_HINT should be registered in the HintStrategies.");
+
+ final String sql1 = "select /*+ resource(mem='20Mb')*/ empno\n"
+ + "from (select /*+ weird_kv_hint(k1='v1') */ empno, ename\n"
+ + "from emp left join dept on emp.deptno = dept.deptno)";
+ sql(sql1).fails("Hint: WEIRD_KV_HINT should be registered in the HintStrategies.");
+ }
+
+ @Test public void testTableHintsInJoin() {
+ final String sql = "select\n"
+ + "ename, job, sal, dept.name\n"
+ + "from emp /*+ index(idx1, idx2) */\n"
+ + "join dept /*+ properties(k1='v1', k2='v2') */\n"
+ + "on emp.deptno = dept.deptno";
+ final List<String> expectedHints = Arrays.asList(
+ "TableScan:[[INDEX inheritPath:{} options:[IDX1, IDX2]]]",
+ "TableScan:[[PROPERTIES inheritPath:{} options:{K1=v1, K2=v2}]]");
+ sql(sql).ok(expectedHints);
+ }
+
+ @Test public void testTableHintsInSelect() {
+ final String sql = withHint("select * from emp /*+ %s */");
+ final List<String> expectedHints = Collections
+ .singletonList("TableScan:["
+ + "[PROPERTIES inheritPath:{} options:{K1=v1, K2=v2}], "
+ + "[INDEX inheritPath:{} options:[ENAME]]]");
+ sql(sql).ok(expectedHints);
+ }
+
+ @Test public void testTableHintsInInsert() throws Exception {
+ final String sql = withHint("insert into dept /*+ %s */ (deptno, name) "
+ + "select deptno, name from dept");
+ final SqlInsert insert = (SqlInsert) tester.parseQuery(sql);
+ assert insert.getTargetTable() instanceof SqlTableRef;
+ final SqlTableRef tableRef = (SqlTableRef) insert.getTargetTable();
+ List<RelHint> hints = SqlUtil.getRelHint(HINT_STRATEGY_TABLE,
+ (SqlNodeList) tableRef.getOperandList().get(1));
+ assertHintsEquals(Arrays.asList(PROPS_HINT, IDX_HINT, JOIN_HINT), hints);
+ }
+
+ @Test public void testTableHintsInUpdate() throws Exception {
+ final String sql = withHint("update emp /*+ %s */ set name = 'test' where deptno = 1");
+ final SqlUpdate sqlUpdate = (SqlUpdate) tester.parseQuery(sql);
+ assert sqlUpdate.getTargetTable() instanceof SqlTableRef;
+ final SqlTableRef tableRef = (SqlTableRef) sqlUpdate.getTargetTable();
+ List<RelHint> hints = SqlUtil.getRelHint(HINT_STRATEGY_TABLE,
+ (SqlNodeList) tableRef.getOperandList().get(1));
+ assertHintsEquals(Arrays.asList(PROPS_HINT, IDX_HINT, JOIN_HINT), hints);
+ }
+
+ @Test public void testTableHintsInDelete() throws Exception {
+ final String sql = withHint("delete from emp /*+ %s */ where deptno = 1");
+ final SqlDelete sqlDelete = (SqlDelete) tester.parseQuery(sql);
+ assert sqlDelete.getTargetTable() instanceof SqlTableRef;
+ final SqlTableRef tableRef = (SqlTableRef) sqlDelete.getTargetTable();
+ List<RelHint> hints = SqlUtil.getRelHint(HINT_STRATEGY_TABLE,
+ (SqlNodeList) tableRef.getOperandList().get(1));
+ assertHintsEquals(Arrays.asList(PROPS_HINT, IDX_HINT, JOIN_HINT), hints);
+ }
+
+ @Test public void testInvalidTableHints() {
+ final String sql = "select\n"
+ + "ename, job, sal, dept.name\n"
+ + "from emp /*+ weird_hint(idx1, idx2) */\n"
+ + "join dept /*+ properties(k1='v1', k2='v2') */\n"
+ + "on emp.deptno = dept.deptno";
+ sql(sql).fails("Hint: WEIRD_HINT should be registered in the HintStrategies.");
+
+ final String sql1 = "select\n"
+ + "ename, job, sal, dept.name\n"
+ + "from emp /*+ index(idx1, idx2) */\n"
+ + "join dept /*+ weird_kv_hint(k1='v1', k2='v2') */\n"
+ + "on emp.deptno = dept.deptno";
+ sql(sql1).fails("Hint: WEIRD_HINT should be registered in the HintStrategies.");
+ }
+
+ @Test public void testJoinHintRequiresSpecificInputs() {
+ final String sql = "select /*+ use_hash_join(r, s), use_hash_join(emp, dept) */\n"
+ + "ename, job, sal, dept.name\n"
+ + "from emp join dept on emp.deptno = dept.deptno";
+ final List<String> expectedHints = Arrays.asList(
+ "Project:[[USE_HASH_JOIN inheritPath:{} options:[R, S]], "
+ + "[USE_HASH_JOIN inheritPath:{} options:[EMP, DEPT]]]",
+ "LogicalJoin:[[USE_HASH_JOIN inheritPath:{0} options:[EMP, DEPT]]]");
+ // Hint use_hash_join(r, s) expect to be ignored by the join node.
+ sql(sql).ok(expectedHints);
+ }
+
+ @Test public void testHintsPropagationInHepPlannerRules() {
+ final String sql = "select /*+ use_hash_join(r, s), use_hash_join(emp, dept) */\n"
+ + "ename, job, sal, dept.name\n"
+ + "from emp join dept on emp.deptno = dept.deptno";
+ final RelNode rel = tester.convertSqlToRel(sql).rel;
+ final RelHint hint = new RelHint(
+ Collections.singletonList(0),
+ "USE_HASH_JOIN",
+ Arrays.asList("EMP", "DEPT"),
+ null);
+ // Validate Hep planner.
+ HepProgram program = new HepProgramBuilder()
+ .addRuleInstance(MockJoinRule.INSTANCE)
+ .build();
+ HepPlanner planner = new HepPlanner(program);
+ planner.setRoot(rel);
+ RelNode newRel = planner.findBestExp();
+ new ValidateHintVisitor(hint, Join.class).go(newRel);
+ }
+
+ @Test public void testHintsPropagationInVolcanoPlannerRules() {
+ final String sql = "select /*+ use_hash_join(r, s), use_hash_join(emp, dept) */\n"
+ + "ename, job, sal, dept.name\n"
+ + "from emp join dept on emp.deptno = dept.deptno";
+ RelOptPlanner planner = new VolcanoPlanner();
+ planner.addRelTraitDef(ConventionTraitDef.INSTANCE);
+ Tester tester1 = tester.withDecorrelation(true)
+ .withClusterFactory(
+ relOptCluster -> RelOptCluster.create(planner, relOptCluster.getRexBuilder()));
+ final RelNode rel = tester1.convertSqlToRel(sql).rel;
+ final RelHint hint = new RelHint(
+ Collections.singletonList(0),
+ "USE_HASH_JOIN",
+ Arrays.asList("EMP", "DEPT"),
+ null);
+ // Validate Volcano planner.
+ RuleSet ruleSet = RuleSets.ofList(
+ new MockEnumerableJoinRule(hint), // Rule to validate the hint.
+ FilterProjectTransposeRule.INSTANCE,
+ FilterMergeRule.INSTANCE,
+ ProjectMergeRule.INSTANCE,
+ EnumerableRules.ENUMERABLE_JOIN_RULE,
+ EnumerableRules.ENUMERABLE_PROJECT_RULE,
+ EnumerableRules.ENUMERABLE_FILTER_RULE,
+ EnumerableRules.ENUMERABLE_SORT_RULE,
+ EnumerableRules.ENUMERABLE_LIMIT_RULE,
+ EnumerableRules.ENUMERABLE_TABLE_SCAN_RULE);
+ Program program = Programs.of(ruleSet);
+ RelTraitSet toTraits = rel
+ .getCluster()
+ .traitSet()
+ .replace(EnumerableConvention.INSTANCE);
+
+ program.run(planner, rel, toTraits,
+ Collections.emptyList(), Collections.emptyList());
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ @Override protected Tester createTester() {
+ return super.createTester()
+ .withConfig(SqlToRelConverter
+ .configBuilder()
+ .withHintStrategyTable(HINT_STRATEGY_TABLE)
+ .build());
+ }
+
+ /** Format the query with hint {@link #HINT}. */
+ private static String withHint(String sql) {
+ return String.format(Locale.ROOT, sql, HINT);
+ }
+
+ /** Sets the SQL statement for a test. */
+ public final Sql sql(String sql) {
+ return new Sql(sql, tester, expectedEx);
+ }
+
+ /**
+ * Creates mock hint strategies.
+ *
+ * @return HintStrategyTable instance
+ */
+ private static HintStrategyTable createHintStrategies() {
+ return HintStrategyTable.builder()
+ .addHintStrategy("no_hash_join", HintStrategies.JOIN)
+ .addHintStrategy("time_zone", HintStrategies.SET_VAR)
+ .addHintStrategy("index", HintStrategies.TABLE_SCAN)
+ .addHintStrategy("properties", HintStrategies.TABLE_SCAN)
+ .addHintStrategy("resource", HintStrategies.PROJECT)
+ .addHintStrategy("use_hash_join",
+ HintStrategies.cascade(HintStrategies.JOIN,
+ HintStrategies.explicit((hint, rel) -> {
+ if (!(rel instanceof LogicalJoin)) {
+ return false;
+ }
+ LogicalJoin join = (LogicalJoin) rel;
+ final List<String> tableNames = hint.listOptions;
+ final List<String> inputTables = join.getInputs().stream()
+ .filter(input -> input instanceof TableScan)
+ .map(scan -> Util.last(scan.getTable().getQualifiedName()))
+ .collect(Collectors.toList());
+ return equalsStringList(tableNames, inputTables);
+ })))
+ .build();
+ }
+
+ private static boolean equalsStringList(List<String> l, List<String> r) {
+ if (l.size() != r.size()) {
+ return false;
+ }
+ for (String s : l) {
+ if (!r.contains(s)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static void assertHintsEquals(List<RelHint> expected, List<RelHint> actual) {
+ assertArrayEquals(expected.toArray(new RelHint[0]), actual.toArray(new RelHint[0]));
+ }
+
+ //~ Inner Class ------------------------------------------------------------
+
+ /** A Mock rule to validate the hint. */
+ private static class MockJoinRule extends RelOptRule {
+ public static final MockJoinRule INSTANCE = new MockJoinRule();
+
+ MockJoinRule() {
+ super(operand(LogicalJoin.class, any()), "MockJoinRule");
+ }
+
+ public void onMatch(RelOptRuleCall call) {
+ LogicalJoin join = call.rel(0);
+ assertThat(1, is(join.getHints().size()));
+ call.transformTo(
+ LogicalJoin.create(join.getLeft(),
+ join.getRight(),
+ join.getCondition(),
+ join.getVariablesSet(),
+ join.getJoinType()));
+ }
+ }
+
+ /** A Mock rule to validate the hint.
+ * This rule also converts the rel to EnumerableConvention. */
+ private static class MockEnumerableJoinRule extends ConverterRule {
+ private final RelHint expectedHint;
+
+ MockEnumerableJoinRule(RelHint hint) {
+ super(
+ LogicalJoin.class,
+ Convention.NONE,
+ EnumerableConvention.INSTANCE,
+ "MockEnumerableJoinRule");
+ this.expectedHint = hint;
+ }
+
+ @Override public RelNode convert(RelNode rel) {
+ LogicalJoin join = (LogicalJoin) rel;
+ assertThat(join.getHints().size(), is(1));
+ assertEquals(expectedHint, join.getHints().get(0));
+ List<RelNode> newInputs = new ArrayList<>();
+ for (RelNode input : join.getInputs()) {
+ if (!(input.getConvention() instanceof EnumerableConvention)) {
+ input =
+ convert(
+ input,
+ input.getTraitSet()
+ .replace(EnumerableConvention.INSTANCE));
+ }
+ newInputs.add(input);
+ }
+ final RelOptCluster cluster = join.getCluster();
+ final RelNode left = newInputs.get(0);
+ final RelNode right = newInputs.get(1);
+ final JoinInfo info = join.analyzeCondition();
+ return EnumerableHashJoin.create(
+ left,
+ right,
+ info.getEquiCondition(left, right, cluster.getRexBuilder()),
+ join.getVariablesSet(),
+ join.getJoinType());
+ }
+ }
+
+ /** A visitor to validate the join node has specific hint. **/
+ private static class ValidateHintVisitor extends RelVisitor {
+ private RelHint expectedHint;
+ private Class clazz;
+
+ /**
+ * Creates the validate visitor.
+ *
+ * @param hint the hint to validate
+ * @param clazz the node type to validate the hint with
+ */
+ ValidateHintVisitor(RelHint hint, Class clazz) {
+ this.expectedHint = hint;
+ this.clazz = clazz;
+ }
+
+ @Override public void visit(
+ RelNode node,
+ int ordinal,
+ RelNode parent) {
+ if (clazz.isInstance(node)) {
+ Join join = (Join) node;
+ assertThat(join.getHints().size(), is(1));
+ assertEquals(expectedHint, join.getHints().get(0));
+ }
+ super.visit(node, ordinal, parent);
+ }
+ }
+
+ /** Sql test tool. */
+ private static class Sql {
+ private String sql;
+ private Tester tester;
+ private List<String> hintsCollect;
+ private ExpectedException expectedEx;
+
+ Sql(String sql, Tester tester, ExpectedException expectedEx) {
+ this.sql = sql;
+ this.tester = tester;
+ this.hintsCollect = new ArrayList<>();
+ this.expectedEx = expectedEx;
+ }
+
+ Sql tester(Tester tester) {
+ this.tester = tester;
+ return this;
+ }
+
+ Sql ok(List<String> expectedHints) {
+ final HintCollector collector = new HintCollector(hintsCollect);
+ final RelNode rel = tester.convertSqlToRel(sql).rel;
+ rel.accept(collector);
+ assertArrayEquals(expectedHints.toArray(new String[0]), hintsCollect.toArray(new String[0]));
+ return this;
+ }
+
+ Sql fails(String failedMsg) {
+ expectedEx.expect(RuntimeException.class);
+ expectedEx.expectMessage(failedMsg);
+ tester.convertSqlToRel(sql);
+ return this;
+ }
+
+ /** A shuttle to collect all the hints within the relational expression into a collection. */
+ private static class HintCollector extends RelShuttleImpl {
+ private final List<String> hintsCollect;
+
+ HintCollector(List<String> hintsCollect) {
+ this.hintsCollect = hintsCollect;
+ }
+
+ @Override public RelNode visit(TableScan scan) {
+ if (scan.getHints().size() > 0) {
+ this.hintsCollect.add("TableScan:" + scan.getHints().toString());
+ }
+ return super.visit(scan);
+ }
+
+ @Override public RelNode visit(LogicalJoin join) {
+ if (join.getHints().size() > 0) {
+ this.hintsCollect.add("LogicalJoin:" + join.getHints().toString());
+ }
+ return super.visit(join);
+ }
+
+ @Override public RelNode visit(LogicalProject project) {
+ if (project.getHints().size() > 0) {
+ this.hintsCollect.add("Project:" + project.getHints().toString());
+ }
+ return super.visit(project);
+ }
+ }
+ }
+}
+
+// End SqlHintsConverterTest.java
diff --git a/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java b/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java
index 5958371..db104c1 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java
@@ -886,8 +886,11 @@ public abstract class SqlToRelTestBase {
private final RelOptCluster cluster;
private final SqlToRelConverter.Config config;
- MockViewExpander(SqlValidator validator, Prepare.CatalogReader catalogReader,
- RelOptCluster cluster, SqlToRelConverter.Config config) {
+ MockViewExpander(
+ SqlValidator validator,
+ Prepare.CatalogReader catalogReader,
+ RelOptCluster cluster,
+ SqlToRelConverter.Config config) {
this.validator = validator;
this.catalogReader = catalogReader;
this.cluster = cluster;
diff --git a/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReader.java b/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReader.java
index bba9b2f..97179b0 100644
--- a/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReader.java
+++ b/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReader.java
@@ -25,6 +25,7 @@ import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
import org.apache.calcite.linq4j.QueryProvider;
import org.apache.calcite.linq4j.Queryable;
import org.apache.calcite.linq4j.tree.Expression;
+import org.apache.calcite.linq4j.tree.Expressions;
import org.apache.calcite.plan.RelOptSchema;
import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.prepare.CalciteCatalogReader;
@@ -551,7 +552,8 @@ public abstract class MockCatalogReader extends CalciteCatalogReader {
}
public Expression getExpression(Class clazz) {
- throw new UnsupportedOperationException();
+ // Return a true constant just to pass the tests in EnumerableTableScanRule.
+ return Expressions.constant(true);
}
public void addColumn(String name, RelDataType type) {
diff --git a/server/src/main/java/org/apache/calcite/sql/ddl/SqlDdlNodes.java b/server/src/main/java/org/apache/calcite/sql/ddl/SqlDdlNodes.java
index 0b8c11a..34b8ea1 100644
--- a/server/src/main/java/org/apache/calcite/sql/ddl/SqlDdlNodes.java
+++ b/server/src/main/java/org/apache/calcite/sql/ddl/SqlDdlNodes.java
@@ -224,7 +224,7 @@ public class SqlDdlNodes {
.addAll(columnList)
.build());
return new SqlSelect(p, null, selectList, from, null, null, null, null,
- null, null, null);
+ null, null, null, null);
}
/** Populates the table called {@code name} by executing {@code query}. */
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index b26674c..36566dd 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -199,7 +199,7 @@ orderItem:
expression [ ASC | DESC ] [ NULLS FIRST | NULLS LAST ]
select:
- SELECT [ STREAM ] [ ALL | DISTINCT ]
+ SELECT [ '/*+' hint [, hint]* '*/' ] [ STREAM ] [ ALL | DISTINCT ]
{ * | projectItem [, projectItem ]* }
FROM tableExpression
[ WHERE booleanExpression ]
@@ -234,7 +234,7 @@ tableReference:
tablePrimary:
[ [ catalogName . ] schemaName . ] tableName
'(' TABLE [ [ catalogName . ] schemaName . ] tableName ')'
- | tablePrimary [ EXTEND ] '(' columnDecl [, columnDecl ]* ')'
+ | tablePrimary [ '/*+' hint [, hint]* '*/' ] [ EXTEND ] '(' columnDecl [, columnDecl ]* ')'
| [ LATERAL ] '(' query ')'
| UNNEST '(' expression ')' [ WITH ORDINALITY ]
| [ LATERAL ] TABLE '(' [ SPECIFIC ] functionName '(' expression [, expression ]* ')' ')'
@@ -242,6 +242,17 @@ tablePrimary:
columnDecl:
column type [ NOT NULL ]
+hint:
+ hintName
+ | hintName '(' hintOptions ')'
+
+hintOptions:
+ hintKVOption [, hintKVOption]*
+ | optionName, [, optionName]*
+
+hintKVOption:
+ optionName '=' stringLiteral
+
values:
VALUES expression [, expression ]*