diff --git a/avatica/src/main/java/org/apache/calcite/avatica/util/Cursor.java b/avatica/src/main/java/org/apache/calcite/avatica/util/Cursor.java index f700763c4cb3..8eab72f3ba0b 100644 --- a/avatica/src/main/java/org/apache/calcite/avatica/util/Cursor.java +++ b/avatica/src/main/java/org/apache/calcite/avatica/util/Cursor.java @@ -18,7 +18,6 @@ import org.apache.calcite.avatica.ColumnMetaData; -import java.io.Closeable; import java.io.InputStream; import java.io.Reader; import java.math.BigDecimal; @@ -41,7 +40,7 @@ * Interface to an iteration that is similar to, and can easily support, * a JDBC {@link java.sql.ResultSet}, but is simpler to implement. */ -public interface Cursor extends Closeable { +public interface Cursor extends AutoCloseable { /** * Creates a list of accessors, one per column. * diff --git a/avatica/src/main/java/org/apache/calcite/avatica/util/IteratorCursor.java b/avatica/src/main/java/org/apache/calcite/avatica/util/IteratorCursor.java index 781f6c636533..c09373bc2ae4 100644 --- a/avatica/src/main/java/org/apache/calcite/avatica/util/IteratorCursor.java +++ b/avatica/src/main/java/org/apache/calcite/avatica/util/IteratorCursor.java @@ -16,8 +16,6 @@ */ package org.apache.calcite.avatica.util; -import java.io.Closeable; -import java.io.IOException; import java.util.Iterator; import java.util.NoSuchElementException; @@ -57,10 +55,12 @@ public boolean next() { public void close() { current = null; position = Position.CLOSED; - if (iterator instanceof Closeable) { + if (iterator instanceof AutoCloseable) { try { - ((Closeable) iterator).close(); - } catch (IOException e) { + ((AutoCloseable) iterator).close(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { throw new RuntimeException(e); } } diff --git a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcImplementor.java b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcImplementor.java index 02f9e070ac5e..e7da0ef8d17f 100644 --- a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcImplementor.java +++ b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcImplementor.java @@ -17,565 +17,30 @@ package org.apache.calcite.adapter.jdbc; import org.apache.calcite.adapter.java.JavaTypeFactory; -import org.apache.calcite.linq4j.tree.Expressions; -import org.apache.calcite.rel.RelFieldCollation; import org.apache.calcite.rel.RelNode; -import org.apache.calcite.rel.core.AggregateCall; -import org.apache.calcite.rel.type.RelDataType; -import org.apache.calcite.rel.type.RelDataTypeField; -import org.apache.calcite.rex.RexCall; -import org.apache.calcite.rex.RexInputRef; -import org.apache.calcite.rex.RexLiteral; -import org.apache.calcite.rex.RexLocalRef; -import org.apache.calcite.rex.RexNode; -import org.apache.calcite.rex.RexProgram; -import org.apache.calcite.sql.SqlBinaryOperator; -import org.apache.calcite.sql.SqlCall; -import org.apache.calcite.sql.SqlDataTypeSpec; +import org.apache.calcite.rel.rel2sql.RelToSqlConverter; import org.apache.calcite.sql.SqlDialect; -import org.apache.calcite.sql.SqlFunction; -import org.apache.calcite.sql.SqlFunctionCategory; -import org.apache.calcite.sql.SqlIdentifier; -import org.apache.calcite.sql.SqlJoin; -import org.apache.calcite.sql.SqlKind; -import org.apache.calcite.sql.SqlLiteral; -import org.apache.calcite.sql.SqlNode; -import org.apache.calcite.sql.SqlNodeList; -import org.apache.calcite.sql.SqlOperator; -import org.apache.calcite.sql.SqlSelect; -import org.apache.calcite.sql.SqlSelectKeyword; -import org.apache.calcite.sql.SqlSetOperator; -import org.apache.calcite.sql.fun.SqlCase; -import org.apache.calcite.sql.fun.SqlStdOperatorTable; -import org.apache.calcite.sql.fun.SqlSumEmptyIsZeroAggFunction; -import org.apache.calcite.sql.parser.SqlParserPos; -import org.apache.calcite.sql.type.BasicSqlType; -import org.apache.calcite.sql.type.ReturnTypes; -import org.apache.calcite.sql.type.SqlTypeName; -import org.apache.calcite.sql.validate.SqlValidatorUtil; -import org.apache.calcite.util.Pair; import org.apache.calcite.util.Util; -import com.google.common.collect.ImmutableList; - -import java.util.AbstractList; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; /** * State for generating a SQL statement. */ -public class JdbcImplementor { - public static final SqlParserPos POS = SqlParserPos.ZERO; - - /** Oracle's {@code SUBSTR} function. - * Oracle does not support {@link SqlStdOperatorTable#SUBSTRING}. */ - public static final SqlFunction ORACLE_SUBSTR = - new SqlFunction("SUBSTR", SqlKind.OTHER_FUNCTION, - ReturnTypes.ARG0_NULLABLE_VARYING, null, null, - SqlFunctionCategory.STRING); - - final SqlDialect dialect; - private final Set aliasSet = new LinkedHashSet<>(); - +public class JdbcImplementor extends RelToSqlConverter { public JdbcImplementor(SqlDialect dialect, JavaTypeFactory typeFactory) { - this.dialect = dialect; + super(dialect); Util.discard(typeFactory); } - /** Creates a result based on a single relational expression. */ - public Result result(SqlNode node, Collection clauses, RelNode rel) { - final String alias2 = SqlValidatorUtil.getAlias(node, -1); - final String alias3 = alias2 != null ? alias2 : "t"; - final String alias4 = - SqlValidatorUtil.uniquify( - alias3, aliasSet, SqlValidatorUtil.EXPR_SUGGESTER); - final String alias5 = alias2 == null || !alias2.equals(alias4) ? alias4 - : null; - return new Result(node, clauses, alias5, - Collections.singletonList(Pair.of(alias4, rel.getRowType()))); - } - - /** Creates a result based on a join. (Each join could contain one or more - * relational expressions.) */ - public Result result(SqlNode join, Result leftResult, Result rightResult) { - final List> list = new ArrayList<>(); - list.addAll(leftResult.aliases); - list.addAll(rightResult.aliases); - return new Result(join, Expressions.list(Clause.FROM), null, list); - } - - /** Wraps a node in a SELECT statement that has no clauses: - * "SELECT ... FROM (node)". */ - SqlSelect wrapSelect(SqlNode node) { - assert node instanceof SqlJoin - || node instanceof SqlIdentifier - || node instanceof SqlCall - && (((SqlCall) node).getOperator() instanceof SqlSetOperator - || ((SqlCall) node).getOperator() == SqlStdOperatorTable.AS) - : node; - return new SqlSelect(POS, SqlNodeList.EMPTY, null, node, null, null, null, - SqlNodeList.EMPTY, null, null, null); - } - - public Result visitChild(int i, RelNode e) { - return ((JdbcRel) e).implement(this); - } - - /** Context for translating a {@link RexNode} expression (within a - * {@link RelNode}) into a {@link SqlNode} expression (within a SQL parse - * tree). */ - public abstract class Context { - final int fieldCount; - - protected Context(int fieldCount) { - this.fieldCount = fieldCount; - } - - public abstract SqlNode field(int ordinal); - - /** Converts an expression from {@link RexNode} to {@link SqlNode} - * format. */ - SqlNode toSql(RexProgram program, RexNode rex) { - switch (rex.getKind()) { - case LOCAL_REF: - final int index = ((RexLocalRef) rex).getIndex(); - return toSql(program, program.getExprList().get(index)); - - case INPUT_REF: - return field(((RexInputRef) rex).getIndex()); - - case LITERAL: - final RexLiteral literal = (RexLiteral) rex; - if (literal.getTypeName() == SqlTypeName.SYMBOL) { - final SqlLiteral.SqlSymbol symbol = - (SqlLiteral.SqlSymbol) literal.getValue(); - return SqlLiteral.createSymbol(symbol, POS); - } - switch (literal.getTypeName().getFamily()) { - case CHARACTER: - return SqlLiteral.createCharString((String) literal.getValue2(), POS); - case NUMERIC: - case EXACT_NUMERIC: - return SqlLiteral.createExactNumeric(literal.getValue().toString(), - POS); - case APPROXIMATE_NUMERIC: - return SqlLiteral.createApproxNumeric( - literal.getValue().toString(), POS); - case BOOLEAN: - return SqlLiteral.createBoolean((Boolean) literal.getValue(), POS); - case DATE: - return SqlLiteral.createDate((Calendar) literal.getValue(), POS); - case TIME: - return SqlLiteral.createTime((Calendar) literal.getValue(), - literal.getType().getPrecision(), POS); - case TIMESTAMP: - return SqlLiteral.createTimestamp((Calendar) literal.getValue(), - literal.getType().getPrecision(), POS); - case ANY: - case NULL: - switch (literal.getTypeName()) { - case NULL: - return SqlLiteral.createNull(POS); - // fall through - } - default: - throw new AssertionError(literal + ": " + literal.getTypeName()); - } - case CASE: - final RexCall caseCall = (RexCall) rex; - final List caseNodeList = - toSql(program, caseCall.getOperands()); - final SqlNode valueNode; - final List whenList = Expressions.list(); - final List thenList = Expressions.list(); - final SqlNode elseNode; - if (caseNodeList.size() % 2 == 0) { - // switched: - // "case x when v1 then t1 when v2 then t2 ... else e end" - valueNode = caseNodeList.get(0); - for (int i = 1; i < caseNodeList.size() - 1; i += 2) { - whenList.add(caseNodeList.get(i)); - thenList.add(caseNodeList.get(i + 1)); - } - } else { - // other: "case when w1 then t1 when w2 then t2 ... else e end" - valueNode = null; - for (int i = 0; i < caseNodeList.size() - 1; i += 2) { - whenList.add(caseNodeList.get(i)); - thenList.add(caseNodeList.get(i + 1)); - } - } - elseNode = caseNodeList.get(caseNodeList.size() - 1); - return new SqlCase(POS, valueNode, new SqlNodeList(whenList, POS), - new SqlNodeList(thenList, POS), elseNode); - - default: - final RexCall call = (RexCall) rex; - final SqlOperator op = call.getOperator(); - final List nodeList = toSql(program, call.getOperands()); - switch (rex.getKind()) { - case CAST: - nodeList.add(toSql(call.getType())); - } - if (op instanceof SqlBinaryOperator && nodeList.size() > 2) { - // In RexNode trees, OR and AND have any number of children; - // SqlCall requires exactly 2. So, convert to a left-deep binary tree. - return createLeftCall(op, nodeList); - } - if (op == SqlStdOperatorTable.SUBSTRING) { - switch (dialect.getDatabaseProduct()) { - case ORACLE: - return ORACLE_SUBSTR.createCall(new SqlNodeList(nodeList, POS)); - } - } - return op.createCall(new SqlNodeList(nodeList, POS)); - } - } - - private SqlNode createLeftCall(SqlOperator op, List nodeList) { - if (nodeList.size() == 2) { - return op.createCall(new SqlNodeList(nodeList, POS)); - } - final List butLast = Util.skipLast(nodeList); - final SqlNode last = nodeList.get(nodeList.size() - 1); - final SqlNode call = createLeftCall(op, butLast); - return op.createCall(new SqlNodeList(ImmutableList.of(call, last), POS)); - } - - private SqlNode toSql(RelDataType type) { - switch (dialect.getDatabaseProduct()) { - case MYSQL: - switch (type.getSqlTypeName()) { - case VARCHAR: - // MySQL doesn't have a VARCHAR type, only CHAR. - return new SqlDataTypeSpec(new SqlIdentifier("CHAR", POS), - type.getPrecision(), -1, null, null, POS); - case INTEGER: - return new SqlDataTypeSpec(new SqlIdentifier("_UNSIGNED", POS), - type.getPrecision(), -1, null, null, POS); - } - break; - } - if (type instanceof BasicSqlType) { - return new SqlDataTypeSpec( - new SqlIdentifier(type.getSqlTypeName().name(), POS), - type.getPrecision(), - type.getScale(), - type.getCharset() != null - && dialect.supportsCharSet() - ? type.getCharset().name() - : null, - null, - POS); - } - throw new AssertionError(type); // TODO: implement - } - - private List toSql(RexProgram program, List operandList) { - final List list = new ArrayList<>(); - for (RexNode rex : operandList) { - list.add(toSql(program, rex)); - } - return list; - } - - public List fieldList() { - return new AbstractList() { - public SqlNode get(int index) { - return field(index); - } - - public int size() { - return fieldCount; - } - }; - } - - /** Converts a call to an aggregate function to an expression. */ - public SqlNode toSql(AggregateCall aggCall) { - SqlOperator op = aggCall.getAggregation(); - if (op instanceof SqlSumEmptyIsZeroAggFunction) { - op = SqlStdOperatorTable.SUM; - } - final List operands = Expressions.list(); - for (int arg : aggCall.getArgList()) { - operands.add(field(arg)); - } - return op.createCall( - aggCall.isDistinct() ? SqlSelectKeyword.DISTINCT.symbol(POS) : null, - POS, operands.toArray(new SqlNode[operands.size()])); - } - - /** Converts a collation to an ORDER BY item. */ - public SqlNode toSql(RelFieldCollation collation) { - SqlNode node = field(collation.getFieldIndex()); - switch (collation.getDirection()) { - case DESCENDING: - case STRICTLY_DESCENDING: - node = SqlStdOperatorTable.DESC.createCall(POS, node); - } - switch (collation.nullDirection) { - case FIRST: - node = SqlStdOperatorTable.NULLS_FIRST.createCall(POS, node); - break; - case LAST: - node = SqlStdOperatorTable.NULLS_LAST.createCall(POS, node); - break; - } - return node; - } - - public JdbcImplementor implementor() { - return JdbcImplementor.this; - } - } - - private static int computeFieldCount( - List> aliases) { - int x = 0; - for (Pair alias : aliases) { - x += alias.right.getFieldCount(); - } - return x; - } - - Context aliasContext(List> aliases, - boolean qualified) { - return new AliasContext(aliases, qualified); - } - - Context joinContext(Context leftContext, Context rightContext) { - return new JoinContext(leftContext, rightContext); - } - - /** Implementation of Context that precedes field references with their - * "table alias" based on the current sub-query's FROM clause. */ - public class AliasContext extends Context { - private final boolean qualified; - private final List> aliases; - - /** Creates an AliasContext; use {@link #aliasContext(List, boolean)}. */ - private AliasContext(List> aliases, - boolean qualified) { - super(computeFieldCount(aliases)); - this.aliases = aliases; - this.qualified = qualified; - } - - public SqlNode field(int ordinal) { - for (Pair alias : aliases) { - final List fields = alias.right.getFieldList(); - if (ordinal < fields.size()) { - RelDataTypeField field = fields.get(ordinal); - return new SqlIdentifier(!qualified - ? ImmutableList.of(field.getName()) - : ImmutableList.of(alias.left, field.getName()), - POS); - } - ordinal -= fields.size(); - } - throw new AssertionError( - "field ordinal " + ordinal + " out of range " + aliases); - } - } - - /** Context for translating ON clause of a JOIN from {@link RexNode} to - * {@link SqlNode}. */ - class JoinContext extends Context { - private final JdbcImplementor.Context leftContext; - private final JdbcImplementor.Context rightContext; - - /** Creates a JoinContext; use {@link #joinContext(Context, Context)}. */ - private JoinContext(Context leftContext, Context rightContext) { - super(leftContext.fieldCount + rightContext.fieldCount); - this.leftContext = leftContext; - this.rightContext = rightContext; - } - - public SqlNode field(int ordinal) { - if (ordinal < leftContext.fieldCount) { - return leftContext.field(ordinal); - } else { - return rightContext.field(ordinal - leftContext.fieldCount); - } - } - } - - /** Result of implementing a node. */ - public class Result { - final SqlNode node; - private final String neededAlias; - private final List> aliases; - final Expressions.FluentList clauses; - - private Result(SqlNode node, Collection clauses, String neededAlias, - List> aliases) { - this.node = node; - this.neededAlias = neededAlias; - this.aliases = aliases; - this.clauses = Expressions.list(clauses); - } - - /** Once you have a Result of implementing a child relational expression, - * call this method to create a Builder to implement the current relational - * expression by adding additional clauses to the SQL query. - * - *

You need to declare which clauses you intend to add. If the clauses - * are "later", you can add to the same query. For example, "GROUP BY" comes - * after "WHERE". But if they are the same or earlier, this method will - * start a new SELECT that wraps the previous result.

- * - *

When you have called - * {@link Builder#setSelect(org.apache.calcite.sql.SqlNodeList)}, - * {@link Builder#setWhere(org.apache.calcite.sql.SqlNode)} etc. call - * {@link Builder#result(org.apache.calcite.sql.SqlNode, java.util.Collection, org.apache.calcite.rel.RelNode)} - * to fix the new query.

- * - * @param rel Relational expression being implemented - * @param clauses Clauses that will be generated to implement current - * relational expression - * @return A builder - */ - public Builder builder(JdbcRel rel, Clause... clauses) { - final Clause maxClause = maxClause(); - boolean needNew = false; - for (Clause clause : clauses) { - if (maxClause.ordinal() >= clause.ordinal()) { - needNew = true; - } - } - SqlSelect select; - Expressions.FluentList clauseList = Expressions.list(); - if (needNew) { - select = subSelect(); - } else { - select = asSelect(); - clauseList.addAll(this.clauses); - } - clauseList.appendAll(clauses); - Context newContext; - final SqlNodeList selectList = select.getSelectList(); - if (selectList != null) { - newContext = new Context(selectList.size()) { - @Override public SqlNode field(int ordinal) { - final SqlNode selectItem = selectList.get(ordinal); - switch (selectItem.getKind()) { - case AS: - return ((SqlCall) selectItem).operand(0); - } - return selectItem; - } - }; - } else { - newContext = aliasContext(aliases, aliases.size() > 1); - } - return new Builder(rel, clauseList, select, newContext); - } - - // make private? - public Clause maxClause() { - Clause maxClause = null; - for (Clause clause : clauses) { - if (maxClause == null || clause.ordinal() > maxClause.ordinal()) { - maxClause = clause; - } - } - assert maxClause != null; - return maxClause; - } - - /** Returns a node that can be included in the FROM clause or a JOIN. It has - * an alias that is unique within the query. The alias is implicit if it - * can be derived using the usual rules (For example, "SELECT * FROM emp" is - * equivalent to "SELECT * FROM emp AS emp".) */ - public SqlNode asFrom() { - if (neededAlias != null) { - return SqlStdOperatorTable.AS.createCall(POS, node, - new SqlIdentifier(neededAlias, POS)); - } - return node; - } - - public SqlSelect subSelect() { - return wrapSelect(asFrom()); - } - - /** Converts a non-query node into a SELECT node. Set operators (UNION, - * INTERSECT, EXCEPT) remain as is. */ - SqlSelect asSelect() { - if (node instanceof SqlSelect) { - return (SqlSelect) node; - } - return wrapSelect(node); - } - - /** Converts a non-query node into a SELECT node. Set operators (UNION, - * INTERSECT, EXCEPT) remain as is. */ - public SqlNode asQuery() { - if (node instanceof SqlCall - && ((SqlCall) node).getOperator() instanceof SqlSetOperator) { - return node; - } - return asSelect(); - } - - /** Returns a context that always qualifies identifiers. Useful if the - * Context deals with just one arm of a join, yet we wish to generate - * a join condition that qualifies column names to disambiguate them. */ - public Context qualifiedContext() { - return aliasContext(aliases, true); - } - } - - /** Builder. */ - public class Builder { - private final JdbcRel rel; - private final List clauses; - private final SqlSelect select; - public final Context context; - - public Builder(JdbcRel rel, List clauses, SqlSelect select, - Context context) { - this.rel = rel; - this.clauses = clauses; - this.select = select; - this.context = context; - } - - public void setSelect(SqlNodeList nodeList) { - select.setSelectList(nodeList); - } - - public void setWhere(SqlNode node) { - assert clauses.contains(Clause.WHERE); - select.setWhere(node); - } - - public void setGroupBy(SqlNodeList nodeList) { - assert clauses.contains(Clause.GROUP_BY); - select.setGroupBy(nodeList); - } - - public void setOrderBy(SqlNodeList nodeList) { - assert clauses.contains(Clause.ORDER_BY); - select.setOrderBy(nodeList); - } - - public Result result() { - return JdbcImplementor.this.result(select, clauses, rel); - } + /** @see #dispatch */ + public Result visit(JdbcTableScan scan) { + return result(scan.jdbcTable.tableName(), + Collections.singletonList(Clause.FROM), scan); } - /** Clauses in a SQL query. Ordered by evaluation order. - * SELECT is set only when there is a NON-TRIVIAL SELECT clause. */ - enum Clause { - FROM, WHERE, GROUP_BY, HAVING, SELECT, SET_OP, ORDER_BY + public Result implement(RelNode node) { + return dispatch(node); } } diff --git a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcRules.java b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcRules.java index cd915154c5db..5fbb08fc7ac6 100644 --- a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcRules.java +++ b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcRules.java @@ -16,10 +16,8 @@ */ package org.apache.calcite.adapter.jdbc; -import org.apache.calcite.linq4j.Ord; 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.Convention; import org.apache.calcite.plan.RelOptCluster; import org.apache.calcite.plan.RelOptCost; @@ -31,7 +29,6 @@ import org.apache.calcite.prepare.Prepare; import org.apache.calcite.rel.InvalidRelException; import org.apache.calcite.rel.RelCollation; -import org.apache.calcite.rel.RelFieldCollation; import org.apache.calcite.rel.RelNode; import org.apache.calcite.rel.RelWriter; import org.apache.calcite.rel.SingleRel; @@ -59,49 +56,25 @@ import org.apache.calcite.rel.logical.LogicalUnion; import org.apache.calcite.rel.logical.LogicalValues; import org.apache.calcite.rel.metadata.RelMetadataQuery; +import org.apache.calcite.rel.rel2sql.SqlImplementor; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rex.RexCall; import org.apache.calcite.rex.RexInputRef; import org.apache.calcite.rex.RexLiteral; -import org.apache.calcite.rex.RexLocalRef; import org.apache.calcite.rex.RexMultisetUtil; import org.apache.calcite.rex.RexNode; import org.apache.calcite.rex.RexProgram; import org.apache.calcite.schema.ModifiableTable; -import org.apache.calcite.sql.JoinConditionType; -import org.apache.calcite.sql.JoinType; import org.apache.calcite.sql.SqlAggFunction; -import org.apache.calcite.sql.SqlBasicCall; -import org.apache.calcite.sql.SqlCall; import org.apache.calcite.sql.SqlDialect; -import org.apache.calcite.sql.SqlFunction; -import org.apache.calcite.sql.SqlFunctionCategory; -import org.apache.calcite.sql.SqlIdentifier; -import org.apache.calcite.sql.SqlJoin; -import org.apache.calcite.sql.SqlKind; -import org.apache.calcite.sql.SqlLiteral; -import org.apache.calcite.sql.SqlNode; -import org.apache.calcite.sql.SqlNodeList; -import org.apache.calcite.sql.SqlOperator; -import org.apache.calcite.sql.SqlSelect; -import org.apache.calcite.sql.SqlSetOperator; -import org.apache.calcite.sql.fun.SqlCase; -import org.apache.calcite.sql.fun.SqlSingleValueAggFunction; import org.apache.calcite.sql.fun.SqlStdOperatorTable; -import org.apache.calcite.sql.parser.SqlParserPos; -import org.apache.calcite.sql.type.InferTypes; -import org.apache.calcite.sql.type.OperandTypes; -import org.apache.calcite.sql.type.ReturnTypes; -import org.apache.calcite.sql.validate.SqlValidatorUtil; import org.apache.calcite.util.ImmutableBitSet; -import org.apache.calcite.util.Pair; import org.apache.calcite.util.Util; import org.apache.calcite.util.trace.CalciteTrace; import com.google.common.collect.ImmutableList; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Set; import java.util.logging.Logger; @@ -117,8 +90,6 @@ private JdbcRules() { protected static final Logger LOGGER = CalciteTrace.getPlannerTracer(); - private static final SqlParserPos POS = SqlParserPos.ZERO; - public static List rules(JdbcConvention out) { return ImmutableList.of( new JdbcToEnumerableConverterRule(out), @@ -150,53 +121,6 @@ public static List rules(JdbcConvention out) { MYSQL_AGG_FUNCS = builder.build(); } - private static void addSelect( - List selectList, SqlNode node, RelDataType rowType) { - String name = rowType.getFieldNames().get(selectList.size()); - String alias = SqlValidatorUtil.getAlias(node, -1); - if (alias == null || !alias.equals(name)) { - node = SqlStdOperatorTable.AS.createCall( - POS, node, new SqlIdentifier(name, POS)); - } - selectList.add(node); - } - - private static JdbcImplementor.Result setOpToSql(JdbcImplementor implementor, - SqlSetOperator operator, JdbcRel rel) { - List list = Expressions.list(); - for (Ord input : Ord.zip(rel.getInputs())) { - final JdbcImplementor.Result result = - implementor.visitChild(input.i, input.e); - list.add(result.asSelect()); - } - final SqlCall node = operator.createCall(new SqlNodeList(list, POS)); - final List clauses = - Expressions.list(JdbcImplementor.Clause.SET_OP); - return implementor.result(node, clauses, rel); - } - - private static boolean isStar(List exps, RelDataType inputRowType) { - int i = 0; - for (RexNode ref : exps) { - if (!(ref instanceof RexInputRef)) { - return false; - } else if (((RexInputRef) ref).getIndex() != i++) { - return false; - } - } - return i == inputRowType.getFieldCount(); - } - - private static boolean isStar(RexProgram program) { - int i = 0; - for (RexLocalRef ref : program.getProjectList()) { - if (ref.getIndex() != i++) { - return false; - } - } - return i == program.getInputRowType().getFieldCount(); - } - /** Abstract base class for rule that converts to JDBC. */ abstract static class JdbcConverterRule extends ConverterRule { protected final JdbcConvention out; @@ -247,7 +171,7 @@ private JdbcJoinRule(JdbcConvention out) { * Returns whether a condition is supported by {@link JdbcJoin}. * *

Corresponds to the capabilities of - * {@link JdbcJoin#convertConditionToSqlNode}. + * {@link SqlImplementor#convertConditionToSqlNode}. * * @param node Condition * @return Whether condition is supported @@ -327,131 +251,7 @@ protected JdbcJoin( } public JdbcImplementor.Result implement(JdbcImplementor implementor) { - final JdbcImplementor.Result leftResult = - implementor.visitChild(0, left); - final JdbcImplementor.Result rightResult = - implementor.visitChild(1, right); - final JdbcImplementor.Context leftContext = leftResult.qualifiedContext(); - final JdbcImplementor.Context rightContext = - rightResult.qualifiedContext(); - SqlNode sqlCondition = convertConditionToSqlNode(condition, - leftContext, - rightContext, - left.getRowType().getFieldCount()); - SqlNode join = - new SqlJoin(POS, - leftResult.asFrom(), - SqlLiteral.createBoolean(false, POS), - joinType(joinType).symbol(POS), - rightResult.asFrom(), - JoinConditionType.ON.symbol(POS), - sqlCondition); - return implementor.result(join, leftResult, rightResult); - } - - /** - * Convert {@link RexNode} condition into {@link SqlNode} - * - * @param node condition Node - * @param leftContext LeftContext - * @param rightContext RightContext - * @param leftFieldCount Number of field on left result - * @return SqlJoin which represent the condition - */ - private SqlNode convertConditionToSqlNode(RexNode node, - JdbcImplementor.Context leftContext, - JdbcImplementor.Context rightContext, int leftFieldCount) { - if (!(node instanceof RexCall)) { - throw new AssertionError(node); - } - final List operands; - final SqlOperator op; - switch (node.getKind()) { - case AND: - case OR: - operands = ((RexCall) node).getOperands(); - op = ((RexCall) node).getOperator(); - SqlNode sqlCondition = null; - for (RexNode operand : operands) { - SqlNode x = convertConditionToSqlNode(operand, leftContext, - rightContext, leftFieldCount); - if (sqlCondition == null) { - sqlCondition = x; - } else { - sqlCondition = op.createCall(POS, sqlCondition, x); - } - } - return sqlCondition; - - case EQUALS: - case IS_NOT_DISTINCT_FROM: - case NOT_EQUALS: - case GREATER_THAN: - case GREATER_THAN_OR_EQUAL: - case LESS_THAN: - case LESS_THAN_OR_EQUAL: - operands = ((RexCall) node).getOperands(); - op = ((RexCall) node).getOperator(); - if (operands.size() == 2 - && operands.get(0) instanceof RexInputRef - && operands.get(1) instanceof RexInputRef) { - final RexInputRef op0 = (RexInputRef) operands.get(0); - final RexInputRef op1 = (RexInputRef) operands.get(1); - - if (op0.getIndex() < leftFieldCount - && op1.getIndex() >= leftFieldCount) { - // Arguments were of form 'op0 = op1' - return op.createCall(POS, - leftContext.field(op0.getIndex()), - rightContext.field(op1.getIndex() - leftFieldCount)); - } - if (op1.getIndex() < leftFieldCount - && op0.getIndex() >= leftFieldCount) { - // Arguments were of form 'op1 = op0' - return reverseOperatorDirection(op).createCall(POS, - leftContext.field(op1.getIndex()), - rightContext.field(op0.getIndex() - leftFieldCount)); - } - } - final JdbcImplementor.Context joinContext = - leftContext.implementor().joinContext(leftContext, rightContext); - return joinContext.toSql(null, node); - } - throw new AssertionError(node); - } - - private static SqlOperator reverseOperatorDirection(SqlOperator op) { - switch (op.kind) { - case GREATER_THAN: - return SqlStdOperatorTable.LESS_THAN; - case GREATER_THAN_OR_EQUAL: - return SqlStdOperatorTable.LESS_THAN_OR_EQUAL; - case LESS_THAN: - return SqlStdOperatorTable.GREATER_THAN; - case LESS_THAN_OR_EQUAL: - return SqlStdOperatorTable.GREATER_THAN_OR_EQUAL; - case EQUALS: - case IS_NOT_DISTINCT_FROM: - case NOT_EQUALS: - return op; - default: - throw new AssertionError(op); - } - } - - private static JoinType joinType(JoinRelType joinType) { - switch (joinType) { - case LEFT: - return JoinType.LEFT; - case RIGHT: - return JoinType.RIGHT; - case INNER: - return JoinType.INNER; - case FULL: - return JoinType.FULL; - default: - throw new AssertionError(joinType); - } + return implementor.implement(this); } } @@ -523,25 +323,7 @@ public RelNode copy(RelTraitSet traitSet, List inputs) { } public JdbcImplementor.Result implement(JdbcImplementor implementor) { - JdbcImplementor.Result x = implementor.visitChild(0, getInput()); - final JdbcImplementor.Builder builder = - program.getCondition() != null - ? x.builder(this, JdbcImplementor.Clause.FROM, - JdbcImplementor.Clause.WHERE) - : x.builder(this, JdbcImplementor.Clause.FROM); - if (!isStar(program)) { - final List selectList = new ArrayList<>(); - for (RexLocalRef ref : program.getProjectList()) { - SqlNode sqlExpr = builder.context.toSql(program, ref); - addSelect(selectList, sqlExpr, getRowType()); - } - builder.setSelect(new SqlNodeList(selectList, POS)); - } - if (program.getCondition() != null) { - builder.setWhere( - builder.context.toSql(program, program.getCondition())); - } - return builder.result(); + return implementor.implement(this); } } @@ -601,19 +383,7 @@ public JdbcProject(RelOptCluster cluster, RelTraitSet traitSet, } public JdbcImplementor.Result implement(JdbcImplementor implementor) { - JdbcImplementor.Result x = implementor.visitChild(0, getInput()); - if (isStar(exps, getInput().getRowType())) { - return x; - } - final JdbcImplementor.Builder builder = - x.builder(this, JdbcImplementor.Clause.SELECT); - final List selectList = new ArrayList<>(); - for (RexNode ref : exps) { - SqlNode sqlExpr = builder.context.toSql(null, ref); - addSelect(selectList, sqlExpr, getRowType()); - } - builder.setSelect(new SqlNodeList(selectList, POS)); - return builder.result(); + return implementor.implement(this); } } @@ -656,11 +426,7 @@ public JdbcFilter copy(RelTraitSet traitSet, RelNode input, } public JdbcImplementor.Result implement(JdbcImplementor implementor) { - JdbcImplementor.Result x = implementor.visitChild(0, getInput()); - final JdbcImplementor.Builder builder = - x.builder(this, JdbcImplementor.Clause.WHERE); - builder.setWhere(builder.context.toSql(null, condition)); - return builder.result(); + return implementor.implement(this); } } @@ -693,6 +459,18 @@ public RelNode convert(RelNode rel) { } } + /** Returns whether this JDBC data source can implement a given aggregate + * function. */ + private static boolean canImplement(SqlAggFunction aggregation, + SqlDialect sqlDialect) { + switch (sqlDialect.getDatabaseProduct()) { + case MYSQL: + return MYSQL_AGG_FUNCS.contains(aggregation); + default: + return AGG_FUNCS.contains(aggregation); + } + } + /** Aggregate operator implemented in JDBC convention. */ public static class JdbcAggregate extends Aggregate implements JdbcRel { public JdbcAggregate( @@ -717,18 +495,6 @@ public JdbcAggregate( } } - /** Returns whether this JDBC data source can implement a given aggregate - * function. */ - private boolean canImplement(SqlAggFunction aggregation, - SqlDialect sqlDialect) { - switch (sqlDialect.getDatabaseProduct()) { - case MYSQL: - return MYSQL_AGG_FUNCS.contains(aggregation); - default: - return AGG_FUNCS.contains(aggregation); - } - } - @Override public JdbcAggregate copy(RelTraitSet traitSet, RelNode input, boolean indicator, ImmutableBitSet groupSet, List groupSets, List aggCalls) { @@ -743,111 +509,7 @@ private boolean canImplement(SqlAggFunction aggregation, } public JdbcImplementor.Result implement(JdbcImplementor implementor) { - // "select a, b, sum(x) from ( ... ) group by a, b" - final JdbcImplementor.Result x = implementor.visitChild(0, getInput()); - final JdbcImplementor.Builder builder = - x.builder(this, JdbcImplementor.Clause.GROUP_BY); - List groupByList = Expressions.list(); - final List selectList = new ArrayList<>(); - for (int group : groupSet) { - final SqlNode field = builder.context.field(group); - addSelect(selectList, field, getRowType()); - groupByList.add(field); - } - for (AggregateCall aggCall : aggCalls) { - SqlNode aggCallSqlNode = builder.context.toSql(aggCall); - if (aggCall.getAggregation() instanceof SqlSingleValueAggFunction) { - aggCallSqlNode = - rewriteSingleValueExpr(aggCallSqlNode, implementor.dialect); - } - addSelect(selectList, aggCallSqlNode, getRowType()); - } - builder.setSelect(new SqlNodeList(selectList, POS)); - if (!groupByList.isEmpty() || aggCalls.isEmpty()) { - // Some databases don't support "GROUP BY ()". We can omit it as long - // as there is at least one aggregate function. - builder.setGroupBy(new SqlNodeList(groupByList, POS)); - } - return builder.result(); - } - - /** Rewrite SINGLE_VALUE into expression based on database variants - * E.g. HSQLDB, MYSQL, ORACLE, etc - */ - private SqlNode rewriteSingleValueExpr(SqlNode aggCall, - SqlDialect sqlDialect) { - final SqlNode operand = ((SqlBasicCall) aggCall).operand(0); - final SqlNode caseOperand; - final SqlNode elseExpr; - final SqlNode countCall = - SqlStdOperatorTable.COUNT.createCall(POS, operand); - - final SqlLiteral nullLiteral = SqlLiteral.createNull(POS); - final SqlNode wrappedOperand; - switch (sqlDialect.getDatabaseProduct()) { - case MYSQL: - case HSQLDB: - // For MySQL, generate - // CASE COUNT(*) - // WHEN 0 THEN NULL - // WHEN 1 THEN - // ELSE (SELECT NULL UNION ALL SELECT NULL) - // END - // - // For hsqldb, generate - // CASE COUNT(*) - // WHEN 0 THEN NULL - // WHEN 1 THEN MIN() - // ELSE (VALUES 1 UNION ALL VALUES 1) - // END - caseOperand = countCall; - - final SqlNodeList selectList = new SqlNodeList(POS); - selectList.add(nullLiteral); - final SqlNode unionOperand; - switch (sqlDialect.getDatabaseProduct()) { - case MYSQL: - wrappedOperand = operand; - unionOperand = new SqlSelect(POS, SqlNodeList.EMPTY, selectList, - null, null, null, null, SqlNodeList.EMPTY, null, null, null); - break; - default: - wrappedOperand = SqlStdOperatorTable.MIN.createCall(POS, operand); - unionOperand = SqlStdOperatorTable.VALUES.createCall(POS, - SqlLiteral.createApproxNumeric("0", POS)); - } - - SqlCall unionAll = SqlStdOperatorTable.UNION_ALL - .createCall(POS, unionOperand, unionOperand); - - final SqlNodeList subQuery = new SqlNodeList(POS); - subQuery.add(unionAll); - - final SqlNodeList selectList2 = new SqlNodeList(POS); - selectList2.add(nullLiteral); - elseExpr = SqlStdOperatorTable.SCALAR_QUERY.createCall(POS, subQuery); - break; - - default: - LOGGER.fine("SINGLE_VALUE rewrite not supported for " - + sqlDialect.getDatabaseProduct()); - return aggCall; - } - - final SqlNodeList whenList = new SqlNodeList(POS); - whenList.add(SqlLiteral.createExactNumeric("0", POS)); - whenList.add(SqlLiteral.createExactNumeric("1", POS)); - - final SqlNodeList thenList = new SqlNodeList(POS); - thenList.add(nullLiteral); - thenList.add(wrappedOperand); - - SqlNode caseExpr = - new SqlCase(POS, caseOperand, whenList, thenList, elseExpr); - - LOGGER.fine("SINGLE_VALUE rewritten into [" + caseExpr + "]"); - - return caseExpr; + return implementor.implement(this); } } @@ -895,34 +557,10 @@ public JdbcSort( } public JdbcImplementor.Result implement(JdbcImplementor implementor) { - final JdbcImplementor.Result x = implementor.visitChild(0, getInput()); - final JdbcImplementor.Builder builder = - x.builder(this, JdbcImplementor.Clause.ORDER_BY); - List orderByList = Expressions.list(); - for (RelFieldCollation fieldCollation : collation.getFieldCollations()) { - if (fieldCollation.nullDirection - != RelFieldCollation.NullDirection.UNSPECIFIED - && implementor.dialect.getDatabaseProduct() - == SqlDialect.DatabaseProduct.MYSQL) { - orderByList.add( - ISNULL_FUNCTION.createCall(POS, - builder.context.field(fieldCollation.getFieldIndex()))); - fieldCollation = new RelFieldCollation(fieldCollation.getFieldIndex(), - fieldCollation.getDirection()); - } - orderByList.add(builder.context.toSql(fieldCollation)); - } - builder.setOrderBy(new SqlNodeList(orderByList, POS)); - return builder.result(); + return implementor.implement(this); } } - /** MySQL specific function. */ - private static final SqlFunction ISNULL_FUNCTION = - new SqlFunction("ISNULL", SqlKind.OTHER_FUNCTION, - ReturnTypes.BOOLEAN, InferTypes.FIRST_KNOWN, - OperandTypes.ANY, SqlFunctionCategory.SYSTEM); - /** * Rule to convert an {@link org.apache.calcite.rel.logical.LogicalUnion} to a * {@link org.apache.calcite.adapter.jdbc.JdbcRules.JdbcUnion}. @@ -961,10 +599,7 @@ public JdbcUnion copy( } public JdbcImplementor.Result implement(JdbcImplementor implementor) { - final SqlSetOperator operator = all - ? SqlStdOperatorTable.UNION_ALL - : SqlStdOperatorTable.UNION; - return setOpToSql(implementor, operator, this); + return implementor.implement(this); } } @@ -1008,11 +643,7 @@ public JdbcIntersect copy( } public JdbcImplementor.Result implement(JdbcImplementor implementor) { - return setOpToSql(implementor, - all - ? SqlStdOperatorTable.INTERSECT_ALL - : SqlStdOperatorTable.INTERSECT, - this); + return implementor.implement(this); } } @@ -1051,11 +682,7 @@ public JdbcMinus copy(RelTraitSet traitSet, List inputs, } public JdbcImplementor.Result implement(JdbcImplementor implementor) { - return setOpToSql(implementor, - all - ? SqlStdOperatorTable.EXCEPT_ALL - : SqlStdOperatorTable.EXCEPT, - this); + return implementor.implement(this); } } @@ -1126,7 +753,7 @@ public JdbcTableModify(RelOptCluster cluster, } public JdbcImplementor.Result implement(JdbcImplementor implementor) { - throw new AssertionError(); // TODO: + return implementor.implement(this); } } @@ -1156,37 +783,7 @@ public static class JdbcValues extends Values implements JdbcRel { } public JdbcImplementor.Result implement(JdbcImplementor implementor) { - final List fields = getRowType().getFieldNames(); - final List clauses = Collections.singletonList( - JdbcImplementor.Clause.SELECT); - final List> pairs = ImmutableList.of(); - final JdbcImplementor.Context context = - implementor.aliasContext(pairs, false); - final List selects = new ArrayList<>(); - for (List tuple : tuples) { - final List selectList = new ArrayList<>(); - for (Pair literal : Pair.zip(tuple, fields)) { - selectList.add( - SqlStdOperatorTable.AS.createCall( - POS, - context.toSql(null, literal.left), - new SqlIdentifier(literal.right, POS))); - } - selects.add( - new SqlSelect(POS, SqlNodeList.EMPTY, - new SqlNodeList(selectList, POS), null, null, null, - null, null, null, null, null)); - } - SqlNode query = null; - for (SqlSelect select : selects) { - if (query == null) { - query = select; - } else { - query = SqlStdOperatorTable.UNION_ALL.createCall(POS, query, - select); - } - } - return implementor.result(query, clauses, this); + return implementor.implement(this); } } } diff --git a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcUtils.java b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcUtils.java index da5eef89dac9..0bbd023770ad 100644 --- a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcUtils.java +++ b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcUtils.java @@ -77,13 +77,7 @@ SqlDialect get(DataSource dataSource) { List key = ImmutableList.of(productName, productVersion); SqlDialect dialect = map.get(key); if (dialect == null) { - final SqlDialect.DatabaseProduct product = - SqlDialect.getProduct(productName, productVersion); - dialect = - new SqlDialect( - product, - productName, - metaData.getIdentifierQuoteString()); + dialect = SqlDialect.create(metaData); map.put(key, dialect); map0.put(dataSource, dialect); } 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 new file mode 100644 index 000000000000..4bfd4023020e --- /dev/null +++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java @@ -0,0 +1,306 @@ +/* + * 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.rel2sql; + +import org.apache.calcite.linq4j.tree.Expressions; +import org.apache.calcite.rel.RelFieldCollation; +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.core.Aggregate; +import org.apache.calcite.rel.core.AggregateCall; +import org.apache.calcite.rel.core.Calc; +import org.apache.calcite.rel.core.Filter; +import org.apache.calcite.rel.core.Intersect; +import org.apache.calcite.rel.core.Join; +import org.apache.calcite.rel.core.Minus; +import org.apache.calcite.rel.core.Project; +import org.apache.calcite.rel.core.Sort; +import org.apache.calcite.rel.core.TableModify; +import org.apache.calcite.rel.core.TableScan; +import org.apache.calcite.rel.core.Union; +import org.apache.calcite.rel.core.Values; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rex.RexLiteral; +import org.apache.calcite.rex.RexLocalRef; +import org.apache.calcite.rex.RexNode; +import org.apache.calcite.rex.RexProgram; +import org.apache.calcite.sql.JoinConditionType; +import org.apache.calcite.sql.SqlDialect; +import org.apache.calcite.sql.SqlIdentifier; +import org.apache.calcite.sql.SqlJoin; +import org.apache.calcite.sql.SqlLiteral; +import org.apache.calcite.sql.SqlNode; +import org.apache.calcite.sql.SqlNodeList; +import org.apache.calcite.sql.SqlSelect; +import org.apache.calcite.sql.fun.SqlSingleValueAggFunction; +import org.apache.calcite.sql.fun.SqlStdOperatorTable; +import org.apache.calcite.sql.parser.SqlParserPos; +import org.apache.calcite.sql.validate.SqlValidatorUtil; +import org.apache.calcite.util.Pair; +import org.apache.calcite.util.ReflectUtil; +import org.apache.calcite.util.ReflectiveVisitor; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Utility to convert relational expressions to SQL abstract syntax tree. + */ +public class RelToSqlConverter extends SqlImplementor + implements ReflectiveVisitor { + + private final ReflectUtil.MethodDispatcher dispatcher; + + /** Creates a RelToSqlConverter. */ + public RelToSqlConverter(SqlDialect dialect) { + super(dialect); + dispatcher = ReflectUtil.createMethodDispatcher(Result.class, this, "visit", + RelNode.class); + } + + /** Dispatches a call to the {@code visit(Xxx e)} method where {@code Xxx} + * most closely matches the runtime type of the argument. */ + protected Result dispatch(RelNode e) { + return dispatcher.invoke(e); + } + + public Result visitChild(int i, RelNode e) { + return dispatch(e); + } + + /** @see #dispatch */ + public Result visit(RelNode e) { + throw new AssertionError("Need to implement " + e.getClass().getName()); + } + + /** @see #dispatch */ + public Result visit(Join e) { + final Result leftResult = visitChild(0, e.getLeft()); + final Result rightResult = visitChild(1, e.getRight()); + final Context leftContext = leftResult.qualifiedContext(); + final Context rightContext = + rightResult.qualifiedContext(); + SqlNode sqlCondition = convertConditionToSqlNode(e.getCondition(), + leftContext, + rightContext, + e.getLeft().getRowType().getFieldCount()); + SqlNode join = + new SqlJoin(POS, + leftResult.asFrom(), + SqlLiteral.createBoolean(false, POS), + joinType(e.getJoinType()).symbol(POS), + rightResult.asFrom(), + JoinConditionType.ON.symbol(POS), + sqlCondition); + return result(join, leftResult, rightResult); + } + + /** @see #dispatch */ + public Result visit(Filter e) { + Result x = visitChild(0, e.getInput()); + final Builder builder = + x.builder(e, Clause.WHERE); + builder.setWhere(builder.context.toSql(null, e.getCondition())); + return builder.result(); + } + + /** @see #dispatch */ + public Result visit(Project e) { + Result x = visitChild(0, e.getInput()); + if (isStar(e.getChildExps(), e.getInput().getRowType())) { + return x; + } + final Builder builder = + x.builder(e, Clause.SELECT); + final List selectList = new ArrayList<>(); + for (RexNode ref : e.getChildExps()) { + SqlNode sqlExpr = builder.context.toSql(null, ref); + addSelect(selectList, sqlExpr, e.getRowType()); + } + + builder.setSelect(new SqlNodeList(selectList, POS)); + return builder.result(); + } + + /** @see #dispatch */ + public Result visit(Aggregate e) { + // "select a, b, sum(x) from ( ... ) group by a, b" + final Result x = visitChild(0, e.getInput()); + final Builder builder; + if (e.getInput() instanceof Project) { + builder = x.builder(e); + builder.clauses.add(Clause.GROUP_BY); + } else { + builder = x.builder(e, Clause.GROUP_BY); + } + List groupByList = Expressions.list(); + final List selectList = new ArrayList<>(); + for (int group : e.getGroupSet()) { + final SqlNode field = builder.context.field(group); + addSelect(selectList, field, e.getRowType()); + groupByList.add(field); + } + for (AggregateCall aggCall : e.getAggCallList()) { + SqlNode aggCallSqlNode = builder.context.toSql(aggCall); + if (aggCall.getAggregation() instanceof SqlSingleValueAggFunction) { + aggCallSqlNode = + rewriteSingleValueExpr(aggCallSqlNode, dialect); + } + addSelect(selectList, aggCallSqlNode, e.getRowType()); + } + builder.setSelect(new SqlNodeList(selectList, POS)); + if (!groupByList.isEmpty() || e.getAggCallList().isEmpty()) { + // Some databases don't support "GROUP BY ()". We can omit it as long + // as there is at least one aggregate function. + builder.setGroupBy(new SqlNodeList(groupByList, POS)); + } + return builder.result(); + } + + /** @see #dispatch */ + public Result visit(TableScan e) { + final SqlIdentifier identifier = + new SqlIdentifier(e.getTable().getQualifiedName(), SqlParserPos.ZERO); + return result(identifier, Collections.singletonList(Clause.FROM), e); + } + + /** @see #dispatch */ + public Result visit(Union e) { + return setOpToSql(e.all + ? SqlStdOperatorTable.UNION_ALL + : SqlStdOperatorTable.UNION, e); + } + + /** @see #dispatch */ + public Result visit(Intersect e) { + return setOpToSql(e.all + ? SqlStdOperatorTable.INTERSECT_ALL + : SqlStdOperatorTable.INTERSECT, e); + } + + /** @see #dispatch */ + public Result visit(Minus e) { + return setOpToSql(e.all + ? SqlStdOperatorTable.EXCEPT_ALL + : SqlStdOperatorTable.EXCEPT, e); + } + + /** @see #dispatch */ + public Result visit(Calc e) { + Result x = visitChild(0, e.getInput()); + final RexProgram program = e.getProgram(); + Builder builder = + program.getCondition() != null + ? x.builder(e, Clause.WHERE) + : x.builder(e); + if (!isStar(program)) { + final List selectList = new ArrayList<>(); + for (RexLocalRef ref : program.getProjectList()) { + SqlNode sqlExpr = builder.context.toSql(program, ref); + addSelect(selectList, sqlExpr, e.getRowType()); + } + builder.setSelect(new SqlNodeList(selectList, POS)); + } + + if (program.getCondition() != null) { + builder.setWhere( + builder.context.toSql(program, program.getCondition())); + } + return builder.result(); + } + + /** @see #dispatch */ + public Result visit(Values e) { + final List fields = e.getRowType().getFieldNames(); + final List clauses = Collections.singletonList(Clause.SELECT); + final List> pairs = ImmutableList.of(); + final Context context = aliasContext(pairs, false); + final List selects = new ArrayList<>(); + for (List tuple : e.getTuples()) { + final List selectList = new ArrayList<>(); + for (Pair literal : Pair.zip(tuple, fields)) { + selectList.add( + SqlStdOperatorTable.AS.createCall( + POS, + context.toSql(null, literal.left), + new SqlIdentifier(literal.right, POS))); + } + selects.add( + new SqlSelect(POS, SqlNodeList.EMPTY, + new SqlNodeList(selectList, POS), null, null, null, + null, null, null, null, null)); + } + SqlNode query = null; + for (SqlSelect select : selects) { + if (query == null) { + query = select; + } else { + query = SqlStdOperatorTable.UNION_ALL.createCall(POS, query, + select); + } + } + return result(query, clauses, e); + } + + /** @see #dispatch */ + public Result visit(Sort e) { + Result x = visitChild(0, e.getInput()); + Builder builder = x.builder(e, Clause.ORDER_BY); + List orderByList = Expressions.list(); + for (RelFieldCollation field : e.getCollation().getFieldCollations()) { + builder.addOrderItem(orderByList, field); + } + if (!orderByList.isEmpty()) { + builder.setOrderBy(new SqlNodeList(orderByList, POS)); + x = builder.result(); + } + if (e.fetch != null) { + builder = x.builder(e, Clause.FETCH); + builder.setFetch(builder.context.toSql(null, e.fetch)); + x = builder.result(); + } + if (e.offset != null) { + builder = x.builder(e, Clause.OFFSET); + builder.setOffset(builder.context.toSql(null, e.offset)); + x = builder.result(); + } + return x; + } + + /** @see #dispatch */ + public Result visit(TableModify e) { + throw new AssertionError("not implemented: " + e); + } + + @Override public void addSelect(List selectList, SqlNode node, + RelDataType rowType) { + String name = rowType.getFieldNames().get(selectList.size()); + String alias = SqlValidatorUtil.getAlias(node, -1); + if (name.toLowerCase().startsWith("expr$")) { + //Put it in ordinalMap + ordinalMap.put(name.toLowerCase(), node); + } else if (alias == null || !alias.equals(name)) { + node = SqlStdOperatorTable.AS.createCall( + POS, node, new SqlIdentifier(name, POS)); + } + selectList.add(node); + } +} + +// End RelToSqlConverter.java 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 new file mode 100644 index 000000000000..3c236146eec8 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java @@ -0,0 +1,915 @@ +/* + * 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.rel2sql; + +import org.apache.calcite.linq4j.Ord; +import org.apache.calcite.linq4j.tree.Expressions; +import org.apache.calcite.rel.RelFieldCollation; +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.core.AggregateCall; +import org.apache.calcite.rel.core.JoinRelType; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeField; +import org.apache.calcite.rex.RexCall; +import org.apache.calcite.rex.RexInputRef; +import org.apache.calcite.rex.RexLiteral; +import org.apache.calcite.rex.RexLocalRef; +import org.apache.calcite.rex.RexNode; +import org.apache.calcite.rex.RexProgram; +import org.apache.calcite.sql.JoinType; +import org.apache.calcite.sql.SqlBasicCall; +import org.apache.calcite.sql.SqlBinaryOperator; +import org.apache.calcite.sql.SqlCall; +import org.apache.calcite.sql.SqlDataTypeSpec; +import org.apache.calcite.sql.SqlDialect; +import org.apache.calcite.sql.SqlFunction; +import org.apache.calcite.sql.SqlFunctionCategory; +import org.apache.calcite.sql.SqlIdentifier; +import org.apache.calcite.sql.SqlJoin; +import org.apache.calcite.sql.SqlKind; +import org.apache.calcite.sql.SqlLiteral; +import org.apache.calcite.sql.SqlNode; +import org.apache.calcite.sql.SqlNodeList; +import org.apache.calcite.sql.SqlOperator; +import org.apache.calcite.sql.SqlSelect; +import org.apache.calcite.sql.SqlSelectKeyword; +import org.apache.calcite.sql.SqlSetOperator; +import org.apache.calcite.sql.fun.SqlCase; +import org.apache.calcite.sql.fun.SqlStdOperatorTable; +import org.apache.calcite.sql.fun.SqlSumEmptyIsZeroAggFunction; +import org.apache.calcite.sql.parser.SqlParserPos; +import org.apache.calcite.sql.type.BasicSqlType; +import org.apache.calcite.sql.type.InferTypes; +import org.apache.calcite.sql.type.OperandTypes; +import org.apache.calcite.sql.type.ReturnTypes; +import org.apache.calcite.sql.type.SqlTypeName; +import org.apache.calcite.sql.type.SqlTypeUtil; +import org.apache.calcite.sql.validate.SqlValidatorUtil; +import org.apache.calcite.util.Pair; +import org.apache.calcite.util.Util; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +/** + * State for generating a SQL statement. + */ +public abstract class SqlImplementor { + private static final Logger LOGGER = + Logger.getLogger(SqlImplementor.class.getName()); + + public static final SqlParserPos POS = SqlParserPos.ZERO; + + /** Oracle's {@code SUBSTR} function. + * Oracle does not support {@link SqlStdOperatorTable#SUBSTRING}. */ + public static final SqlFunction ORACLE_SUBSTR = + new SqlFunction("SUBSTR", SqlKind.OTHER_FUNCTION, + ReturnTypes.ARG0_NULLABLE_VARYING, null, null, + SqlFunctionCategory.STRING); + + /** MySQL specific function. */ + public static final SqlFunction ISNULL_FUNCTION = + new SqlFunction("ISNULL", SqlKind.OTHER_FUNCTION, + ReturnTypes.BOOLEAN, InferTypes.FIRST_KNOWN, + OperandTypes.ANY, SqlFunctionCategory.SYSTEM); + + public final SqlDialect dialect; + protected final Set aliasSet = new LinkedHashSet<>(); + protected final Map ordinalMap = new HashMap<>(); + + protected SqlImplementor(SqlDialect dialect) { + this.dialect = Preconditions.checkNotNull(dialect); + } + + public abstract Result visitChild(int i, RelNode e); + + /** Rewrite SINGLE_VALUE into expression based on database variants + * E.g. HSQLDB, MYSQL, ORACLE, etc + */ + public static SqlNode rewriteSingleValueExpr(SqlNode aggCall, + SqlDialect sqlDialect) { + final SqlNode operand = ((SqlBasicCall) aggCall).operand(0); + final SqlNode caseOperand; + final SqlNode elseExpr; + final SqlNode countCall = + SqlStdOperatorTable.COUNT.createCall(POS, operand); + + final SqlLiteral nullLiteral = SqlLiteral.createNull(POS); + final SqlNode wrappedOperand; + switch (sqlDialect.getDatabaseProduct()) { + case MYSQL: + case HSQLDB: + // For MySQL, generate + // CASE COUNT(*) + // WHEN 0 THEN NULL + // WHEN 1 THEN + // ELSE (SELECT NULL UNION ALL SELECT NULL) + // END + // + // For hsqldb, generate + // CASE COUNT(*) + // WHEN 0 THEN NULL + // WHEN 1 THEN MIN() + // ELSE (VALUES 1 UNION ALL VALUES 1) + // END + caseOperand = countCall; + + final SqlNodeList selectList = new SqlNodeList(POS); + selectList.add(nullLiteral); + final SqlNode unionOperand; + switch (sqlDialect.getDatabaseProduct()) { + case MYSQL: + wrappedOperand = operand; + unionOperand = new SqlSelect(POS, SqlNodeList.EMPTY, selectList, + null, null, null, null, SqlNodeList.EMPTY, null, null, null); + break; + default: + wrappedOperand = SqlStdOperatorTable.MIN.createCall(POS, operand); + unionOperand = SqlStdOperatorTable.VALUES.createCall(POS, + SqlLiteral.createApproxNumeric("0", POS)); + } + + SqlCall unionAll = SqlStdOperatorTable.UNION_ALL + .createCall(POS, unionOperand, unionOperand); + + final SqlNodeList subQuery = new SqlNodeList(POS); + subQuery.add(unionAll); + + final SqlNodeList selectList2 = new SqlNodeList(POS); + selectList2.add(nullLiteral); + elseExpr = SqlStdOperatorTable.SCALAR_QUERY.createCall(POS, subQuery); + break; + + default: + LOGGER.fine("SINGLE_VALUE rewrite not supported for " + + sqlDialect.getDatabaseProduct()); + return aggCall; + } + + final SqlNodeList whenList = new SqlNodeList(POS); + whenList.add(SqlLiteral.createExactNumeric("0", POS)); + whenList.add(SqlLiteral.createExactNumeric("1", POS)); + + final SqlNodeList thenList = new SqlNodeList(POS); + thenList.add(nullLiteral); + thenList.add(wrappedOperand); + + SqlNode caseExpr = + new SqlCase(POS, caseOperand, whenList, thenList, elseExpr); + + LOGGER.fine("SINGLE_VALUE rewritten into [" + caseExpr + "]"); + + return caseExpr; + } + + public void addSelect(List selectList, SqlNode node, + RelDataType rowType) { + String name = rowType.getFieldNames().get(selectList.size()); + String alias = SqlValidatorUtil.getAlias(node, -1); + if (alias == null || !alias.equals(name)) { + node = SqlStdOperatorTable.AS.createCall( + POS, node, new SqlIdentifier(name, POS)); + } + selectList.add(node); + } + + public static boolean isStar(List exps, RelDataType inputRowType) { + int i = 0; + for (RexNode ref : exps) { + if (!(ref instanceof RexInputRef)) { + return false; + } else if (((RexInputRef) ref).getIndex() != i++) { + return false; + } + } + return i == inputRowType.getFieldCount(); + } + + public static boolean isStar(RexProgram program) { + int i = 0; + for (RexLocalRef ref : program.getProjectList()) { + if (ref.getIndex() != i++) { + return false; + } + } + return i == program.getInputRowType().getFieldCount(); + } + + public Result setOpToSql(SqlSetOperator operator, RelNode rel) { + List list = Expressions.list(); + for (Ord input : Ord.zip(rel.getInputs())) { + final Result result = visitChild(input.i, input.e); + list.add(result.asSelect()); + } + final SqlCall node = operator.createCall(new SqlNodeList(list, POS)); + final List clauses = + Expressions.list(Clause.SET_OP); + return result(node, clauses, rel); + } + + /** + * Converts a {@link RexNode} condition into a {@link SqlNode}. + * + * @param node condition Node + * @param leftContext LeftContext + * @param rightContext RightContext + * @param leftFieldCount Number of field on left result + * @return SqlJoin which represent the condition + */ + public static SqlNode convertConditionToSqlNode(RexNode node, + Context leftContext, + Context rightContext, int leftFieldCount) { + if (!(node instanceof RexCall)) { + throw new AssertionError(node); + } + final List operands; + final SqlOperator op; + switch (node.getKind()) { + case AND: + case OR: + operands = ((RexCall) node).getOperands(); + op = ((RexCall) node).getOperator(); + SqlNode sqlCondition = null; + for (RexNode operand : operands) { + SqlNode x = convertConditionToSqlNode(operand, leftContext, + rightContext, leftFieldCount); + if (sqlCondition == null) { + sqlCondition = x; + } else { + sqlCondition = op.createCall(POS, sqlCondition, x); + } + } + return sqlCondition; + + case EQUALS: + case IS_NOT_DISTINCT_FROM: + case NOT_EQUALS: + case GREATER_THAN: + case GREATER_THAN_OR_EQUAL: + case LESS_THAN: + case LESS_THAN_OR_EQUAL: + node = stripCastFromString(node); + operands = ((RexCall) node).getOperands(); + op = ((RexCall) node).getOperator(); + if (operands.size() == 2 + && operands.get(0) instanceof RexInputRef + && operands.get(1) instanceof RexInputRef) { + final RexInputRef op0 = (RexInputRef) operands.get(0); + final RexInputRef op1 = (RexInputRef) operands.get(1); + + if (op0.getIndex() < leftFieldCount + && op1.getIndex() >= leftFieldCount) { + // Arguments were of form 'op0 = op1' + return op.createCall(POS, + leftContext.field(op0.getIndex()), + rightContext.field(op1.getIndex() - leftFieldCount)); + } + if (op1.getIndex() < leftFieldCount + && op0.getIndex() >= leftFieldCount) { + // Arguments were of form 'op1 = op0' + return reverseOperatorDirection(op).createCall(POS, + leftContext.field(op1.getIndex()), + rightContext.field(op0.getIndex() - leftFieldCount)); + } + } + final Context joinContext = + leftContext.implementor().joinContext(leftContext, rightContext); + return joinContext.toSql(null, node); + } + throw new AssertionError(node); + } + + /** Removes cast from string. + * + *

For example, {@code x > CAST('2015-01-07' AS DATE)} + * becomes {@code x > '2015-01-07'}. + */ + private static RexNode stripCastFromString(RexNode node) { + switch (node.getKind()) { + case EQUALS: + case IS_NOT_DISTINCT_FROM: + case NOT_EQUALS: + case GREATER_THAN: + case GREATER_THAN_OR_EQUAL: + case LESS_THAN: + case LESS_THAN_OR_EQUAL: + final RexCall call = (RexCall) node; + final RexNode o0 = call.operands.get(0); + final RexNode o1 = call.operands.get(1); + if (o0.getKind() == SqlKind.CAST + && o1.getKind() != SqlKind.CAST) { + final RexNode o0b = ((RexCall) o0).getOperands().get(0); + switch (o0b.getType().getSqlTypeName()) { + case CHAR: + case VARCHAR: + return call.clone(call.getType(), ImmutableList.of(o0b, o1)); + } + } + if (o1.getKind() == SqlKind.CAST + && o0.getKind() != SqlKind.CAST) { + final RexNode o1b = ((RexCall) o1).getOperands().get(0); + switch (o1b.getType().getSqlTypeName()) { + case CHAR: + case VARCHAR: + return call.clone(call.getType(), ImmutableList.of(o0, o1b)); + } + } + } + return node; + } + + private static SqlOperator reverseOperatorDirection(SqlOperator op) { + switch (op.kind) { + case GREATER_THAN: + return SqlStdOperatorTable.LESS_THAN; + case GREATER_THAN_OR_EQUAL: + return SqlStdOperatorTable.LESS_THAN_OR_EQUAL; + case LESS_THAN: + return SqlStdOperatorTable.GREATER_THAN; + case LESS_THAN_OR_EQUAL: + return SqlStdOperatorTable.GREATER_THAN_OR_EQUAL; + case EQUALS: + case IS_NOT_DISTINCT_FROM: + case NOT_EQUALS: + return op; + default: + throw new AssertionError(op); + } + } + + public static JoinType joinType(JoinRelType joinType) { + switch (joinType) { + case LEFT: + return JoinType.LEFT; + case RIGHT: + return JoinType.RIGHT; + case INNER: + return JoinType.INNER; + case FULL: + return JoinType.FULL; + default: + throw new AssertionError(joinType); + } + } + + /** Creates a result based on a single relational expression. */ + public Result result(SqlNode node, Collection clauses, RelNode rel) { + final String alias2 = SqlValidatorUtil.getAlias(node, -1); + final String alias3 = alias2 != null ? alias2 : "t"; + final String alias4 = + SqlValidatorUtil.uniquify( + alias3, aliasSet, SqlValidatorUtil.EXPR_SUGGESTER); + final String alias5 = alias2 == null || !alias2.equals(alias4) ? alias4 + : null; + return new Result(node, clauses, alias5, + Collections.singletonList(Pair.of(alias4, rel.getRowType()))); + } + + /** Creates a result based on a join. (Each join could contain one or more + * relational expressions.) */ + public Result result(SqlNode join, Result leftResult, Result rightResult) { + final List> list = new ArrayList<>(); + list.addAll(leftResult.aliases); + list.addAll(rightResult.aliases); + return new Result(join, Expressions.list(Clause.FROM), null, list); + } + + /** Wraps a node in a SELECT statement that has no clauses: + * "SELECT ... FROM (node)". */ + SqlSelect wrapSelect(SqlNode node) { + assert node instanceof SqlJoin + || node instanceof SqlIdentifier + || node instanceof SqlCall + && (((SqlCall) node).getOperator() instanceof SqlSetOperator + || ((SqlCall) node).getOperator() == SqlStdOperatorTable.AS) + : node; + return new SqlSelect(POS, SqlNodeList.EMPTY, null, node, null, null, null, + SqlNodeList.EMPTY, null, null, null); + } + + /** Context for translating a {@link RexNode} expression (within a + * {@link RelNode}) into a {@link SqlNode} expression (within a SQL parse + * tree). */ + public abstract class Context { + final int fieldCount; + private final boolean ignoreCast; + + protected Context(int fieldCount) { + this(fieldCount, false); + } + + protected Context(int fieldCount, boolean ignoreCast) { + this.fieldCount = fieldCount; + this.ignoreCast = ignoreCast; + } + + public abstract SqlNode field(int ordinal); + + /** Converts an expression from {@link RexNode} to {@link SqlNode} + * format. */ + public SqlNode toSql(RexProgram program, RexNode rex) { + switch (rex.getKind()) { + case LOCAL_REF: + final int index = ((RexLocalRef) rex).getIndex(); + return toSql(program, program.getExprList().get(index)); + + case INPUT_REF: + return field(((RexInputRef) rex).getIndex()); + + case LITERAL: + final RexLiteral literal = (RexLiteral) rex; + if (literal.getTypeName() == SqlTypeName.SYMBOL) { + final SqlLiteral.SqlSymbol symbol = + (SqlLiteral.SqlSymbol) literal.getValue(); + return SqlLiteral.createSymbol(symbol, POS); + } + switch (literal.getTypeName().getFamily()) { + case CHARACTER: + return SqlLiteral.createCharString((String) literal.getValue2(), POS); + case NUMERIC: + case EXACT_NUMERIC: + return SqlLiteral.createExactNumeric(literal.getValue().toString(), + POS); + case APPROXIMATE_NUMERIC: + return SqlLiteral.createApproxNumeric( + literal.getValue().toString(), POS); + case BOOLEAN: + return SqlLiteral.createBoolean((Boolean) literal.getValue(), POS); + case DATE: + return SqlLiteral.createDate((Calendar) literal.getValue(), POS); + case TIME: + return SqlLiteral.createTime((Calendar) literal.getValue(), + literal.getType().getPrecision(), POS); + case TIMESTAMP: + return SqlLiteral.createTimestamp((Calendar) literal.getValue(), + literal.getType().getPrecision(), POS); + case ANY: + case NULL: + switch (literal.getTypeName()) { + case NULL: + return SqlLiteral.createNull(POS); + // fall through + } + default: + throw new AssertionError(literal + ": " + literal.getTypeName()); + } + case CASE: + final RexCall caseCall = (RexCall) rex; + final List caseNodeList = + toSql(program, caseCall.getOperands()); + final SqlNode valueNode; + final List whenList = Expressions.list(); + final List thenList = Expressions.list(); + final SqlNode elseNode; + if (caseNodeList.size() % 2 == 0) { + // switched: + // "case x when v1 then t1 when v2 then t2 ... else e end" + valueNode = caseNodeList.get(0); + for (int i = 1; i < caseNodeList.size() - 1; i += 2) { + whenList.add(caseNodeList.get(i)); + thenList.add(caseNodeList.get(i + 1)); + } + } else { + // other: "case when w1 then t1 when w2 then t2 ... else e end" + valueNode = null; + for (int i = 0; i < caseNodeList.size() - 1; i += 2) { + whenList.add(caseNodeList.get(i)); + thenList.add(caseNodeList.get(i + 1)); + } + } + elseNode = caseNodeList.get(caseNodeList.size() - 1); + return new SqlCase(POS, valueNode, new SqlNodeList(whenList, POS), + new SqlNodeList(thenList, POS), elseNode); + + default: + final RexCall call = (RexCall) stripCastFromString(rex); + final SqlOperator op = call.getOperator(); + final List nodeList = toSql(program, call.getOperands()); + switch (call.getKind()) { + case CAST: + if (ignoreCast) { + assert nodeList.size() == 1; + return nodeList.get(0); + } else { + nodeList.add(toSql(call.getType())); + } + } + if (op instanceof SqlBinaryOperator && nodeList.size() > 2) { + // In RexNode trees, OR and AND have any number of children; + // SqlCall requires exactly 2. So, convert to a left-deep binary tree. + return createLeftCall(op, nodeList); + } + if (op == SqlStdOperatorTable.SUBSTRING) { + switch (dialect.getDatabaseProduct()) { + case ORACLE: + return ORACLE_SUBSTR.createCall(new SqlNodeList(nodeList, POS)); + } + } + return op.createCall(new SqlNodeList(nodeList, POS)); + } + } + + private SqlNode createLeftCall(SqlOperator op, List nodeList) { + if (nodeList.size() == 2) { + return op.createCall(new SqlNodeList(nodeList, POS)); + } + final List butLast = Util.skipLast(nodeList); + final SqlNode last = nodeList.get(nodeList.size() - 1); + final SqlNode call = createLeftCall(op, butLast); + return op.createCall(new SqlNodeList(ImmutableList.of(call, last), POS)); + } + + private SqlNode toSql(RelDataType type) { + switch (dialect.getDatabaseProduct()) { + case MYSQL: + switch (type.getSqlTypeName()) { + case VARCHAR: + // MySQL doesn't have a VARCHAR type, only CHAR. + return new SqlDataTypeSpec(new SqlIdentifier("CHAR", POS), + type.getPrecision(), -1, null, null, POS); + case INTEGER: + return new SqlDataTypeSpec(new SqlIdentifier("_UNSIGNED", POS), + type.getPrecision(), -1, null, null, POS); + } + break; + } + if (type instanceof BasicSqlType) { + return new SqlDataTypeSpec( + new SqlIdentifier(type.getSqlTypeName().name(), POS), + type.getPrecision(), + type.getScale(), + type.getCharset() != null + && dialect.supportsCharSet() + ? type.getCharset().name() + : null, + null, + POS); + } + return SqlTypeUtil.convertTypeToSpec(type); + } + + private List toSql(RexProgram program, List operandList) { + final List list = new ArrayList<>(); + for (RexNode rex : operandList) { + list.add(toSql(program, rex)); + } + return list; + } + + public List fieldList() { + return new AbstractList() { + public SqlNode get(int index) { + return field(index); + } + + public int size() { + return fieldCount; + } + }; + } + + /** Converts a call to an aggregate function to an expression. */ + public SqlNode toSql(AggregateCall aggCall) { + SqlOperator op = aggCall.getAggregation(); + if (op instanceof SqlSumEmptyIsZeroAggFunction) { + op = SqlStdOperatorTable.SUM; + } + final List operands = Expressions.list(); + for (int arg : aggCall.getArgList()) { + operands.add(field(arg)); + } + return op.createCall( + aggCall.isDistinct() ? SqlSelectKeyword.DISTINCT.symbol(POS) : null, + POS, operands.toArray(new SqlNode[operands.size()])); + } + + /** Converts a collation to an ORDER BY item. */ + public SqlNode toSql(RelFieldCollation collation) { + SqlNode node = field(collation.getFieldIndex()); + switch (collation.getDirection()) { + case DESCENDING: + case STRICTLY_DESCENDING: + node = SqlStdOperatorTable.DESC.createCall(POS, node); + } + if (collation.nullDirection != dialect.defaultNullDirection(collation.direction)) { + switch (collation.nullDirection) { + case FIRST: + node = SqlStdOperatorTable.NULLS_FIRST.createCall(POS, node); + break; + case LAST: + node = SqlStdOperatorTable.NULLS_LAST.createCall(POS, node); + break; + } + } + return node; + } + + public SqlImplementor implementor() { + return SqlImplementor.this; + } + } + + private static int computeFieldCount( + List> aliases) { + int x = 0; + for (Pair alias : aliases) { + x += alias.right.getFieldCount(); + } + return x; + } + + public Context aliasContext(List> aliases, + boolean qualified) { + return new AliasContext(aliases, qualified); + } + + public Context joinContext(Context leftContext, Context rightContext) { + return new JoinContext(leftContext, rightContext); + } + + /** Implementation of Context that precedes field references with their + * "table alias" based on the current sub-query's FROM clause. */ + public class AliasContext extends Context { + private final boolean qualified; + private final List> aliases; + + /** Creates an AliasContext; use {@link #aliasContext(List, boolean)}. */ + protected AliasContext(List> aliases, + boolean qualified) { + super(computeFieldCount(aliases)); + this.aliases = aliases; + this.qualified = qualified; + } + + public SqlNode field(int ordinal) { + for (Pair alias : aliases) { + final List fields = alias.right.getFieldList(); + if (ordinal < fields.size()) { + RelDataTypeField field = fields.get(ordinal); + final SqlNode mappedSqlNode = + ordinalMap.get(field.getName().toLowerCase()); + if (mappedSqlNode != null) { + return mappedSqlNode; + } + return new SqlIdentifier(!qualified + ? ImmutableList.of(field.getName()) + : ImmutableList.of(alias.left, field.getName()), + POS); + } + ordinal -= fields.size(); + } + throw new AssertionError( + "field ordinal " + ordinal + " out of range " + aliases); + } + } + + /** Context for translating ON clause of a JOIN from {@link RexNode} to + * {@link SqlNode}. */ + class JoinContext extends Context { + private final SqlImplementor.Context leftContext; + private final SqlImplementor.Context rightContext; + + /** Creates a JoinContext; use {@link #joinContext(Context, Context)}. */ + private JoinContext(Context leftContext, Context rightContext) { + super(leftContext.fieldCount + rightContext.fieldCount); + this.leftContext = leftContext; + this.rightContext = rightContext; + } + + public SqlNode field(int ordinal) { + if (ordinal < leftContext.fieldCount) { + return leftContext.field(ordinal); + } else { + return rightContext.field(ordinal - leftContext.fieldCount); + } + } + } + + /** Result of implementing a node. */ + public class Result { + final SqlNode node; + private final String neededAlias; + private final List> aliases; + final Expressions.FluentList clauses; + + public Result(SqlNode node, Collection clauses, String neededAlias, + List> aliases) { + this.node = node; + this.neededAlias = neededAlias; + this.aliases = aliases; + this.clauses = Expressions.list(clauses); + } + + /** Once you have a Result of implementing a child relational expression, + * call this method to create a Builder to implement the current relational + * expression by adding additional clauses to the SQL query. + * + *

You need to declare which clauses you intend to add. If the clauses + * are "later", you can add to the same query. For example, "GROUP BY" comes + * after "WHERE". But if they are the same or earlier, this method will + * start a new SELECT that wraps the previous result. + * + *

When you have called + * {@link Builder#setSelect(SqlNodeList)}, + * {@link Builder#setWhere(SqlNode)} etc. call + * {@link Builder#result(SqlNode, Collection, RelNode)} + * to fix the new query. + * + * @param rel Relational expression being implemented + * @param clauses Clauses that will be generated to implement current + * relational expression + * @return A builder + */ + public Builder builder(RelNode rel, Clause... clauses) { + final Clause maxClause = maxClause(); + boolean needNew = false; + // If old and new clause are equal and belong to below set, + // then new SELECT wrap is not required + Set nonWrapSet = ImmutableSet.of(Clause.SELECT); + for (Clause clause : clauses) { + if (maxClause.ordinal() > clause.ordinal() + || (maxClause.equals(clause) && !nonWrapSet.contains(clause))) { + needNew = true; + } + } + SqlSelect select; + Expressions.FluentList clauseList = Expressions.list(); + if (needNew) { + select = subSelect(); + } else { + select = asSelect(); + clauseList.addAll(this.clauses); + } + clauseList.appendAll(clauses); + Context newContext; + final SqlNodeList selectList = select.getSelectList(); + if (selectList != null) { + newContext = new Context(selectList.size()) { + public SqlNode field(int ordinal) { + final SqlNode selectItem = selectList.get(ordinal); + switch (selectItem.getKind()) { + case AS: + return ((SqlCall) selectItem).operand(0); + } + return selectItem; + } + }; + } else { + newContext = aliasContext(aliases, aliases.size() > 1); + } + return new Builder(rel, clauseList, select, newContext); + } + + // make private? + public Clause maxClause() { + Clause maxClause = null; + for (Clause clause : clauses) { + if (maxClause == null || clause.ordinal() > maxClause.ordinal()) { + maxClause = clause; + } + } + assert maxClause != null; + return maxClause; + } + + /** Returns a node that can be included in the FROM clause or a JOIN. It has + * an alias that is unique within the query. The alias is implicit if it + * can be derived using the usual rules (For example, "SELECT * FROM emp" is + * equivalent to "SELECT * FROM emp AS emp".) */ + public SqlNode asFrom() { + if (neededAlias != null) { + return SqlStdOperatorTable.AS.createCall(POS, node, + new SqlIdentifier(neededAlias, POS)); + } + return node; + } + + public SqlSelect subSelect() { + return wrapSelect(asFrom()); + } + + /** Converts a non-query node into a SELECT node. Set operators (UNION, + * INTERSECT, EXCEPT) remain as is. */ + public SqlSelect asSelect() { + if (node instanceof SqlSelect) { + return (SqlSelect) node; + } + return wrapSelect(node); + } + + /** Converts a non-query node into a SELECT node. Set operators (UNION, + * INTERSECT, EXCEPT) remain as is. */ + public SqlNode asQuery() { + if (node instanceof SqlCall + && ((SqlCall) node).getOperator() instanceof SqlSetOperator) { + return node; + } + return asSelect(); + } + + /** Returns a context that always qualifies identifiers. Useful if the + * Context deals with just one arm of a join, yet we wish to generate + * a join condition that qualifies column names to disambiguate them. */ + public Context qualifiedContext() { + return aliasContext(aliases, true); + } + } + + /** Builder. */ + public class Builder { + private final RelNode rel; + final List clauses; + private final SqlSelect select; + public final Context context; + + public Builder(RelNode rel, List clauses, SqlSelect select, + Context context) { + this.rel = rel; + this.clauses = clauses; + this.select = select; + this.context = context; + } + + public void setSelect(SqlNodeList nodeList) { + select.setSelectList(nodeList); + } + + public void setWhere(SqlNode node) { + assert clauses.contains(Clause.WHERE); + select.setWhere(node); + } + + public void setGroupBy(SqlNodeList nodeList) { + assert clauses.contains(Clause.GROUP_BY); + select.setGroupBy(nodeList); + } + + public void setOrderBy(SqlNodeList nodeList) { + assert clauses.contains(Clause.ORDER_BY); + select.setOrderBy(nodeList); + } + + public void setFetch(SqlNode fetch) { + assert clauses.contains(Clause.FETCH); + select.setFetch(fetch); + } + + public void setOffset(SqlNode offset) { + assert clauses.contains(Clause.OFFSET); + select.setOffset(offset); + } + + public void addOrderItem(List orderByList, + RelFieldCollation field) { + if (field.nullDirection != RelFieldCollation.NullDirection.UNSPECIFIED + && dialect.getDatabaseProduct() == SqlDialect.DatabaseProduct.MYSQL) { + orderByList.add( + ISNULL_FUNCTION.createCall(POS, + context.field(field.getFieldIndex()))); + field = new RelFieldCollation(field.getFieldIndex(), + field.getDirection(), + RelFieldCollation.NullDirection.UNSPECIFIED); + } + orderByList.add(context.toSql(field)); + } + + public Result result() { + return SqlImplementor.this.result(select, clauses, rel); + } + } + + /** Clauses in a SQL query. Ordered by evaluation order. + * SELECT is set only when there is a NON-TRIVIAL SELECT clause. */ + public enum Clause { + FROM, WHERE, GROUP_BY, HAVING, SELECT, SET_OP, ORDER_BY, FETCH, OFFSET + } +} + +// End SqlImplementor.java diff --git a/core/src/main/java/org/apache/calcite/rel/rel2sql/package-info.java b/core/src/main/java/org/apache/calcite/rel/rel2sql/package-info.java new file mode 100644 index 000000000000..8339ac5a0727 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/package-info.java @@ -0,0 +1,26 @@ +/* + * 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. + */ + +/** + * Translates a relational expression to SQL parse tree. + */ +@PackageMarker +package org.apache.calcite.rel.rel2sql; + +import org.apache.calcite.avatica.util.PackageMarker; + +// End package-info.java diff --git a/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java b/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java index 2f4cada77101..3fccdfadae83 100644 --- a/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java +++ b/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java @@ -72,7 +72,8 @@ public abstract class PruneEmptyRules { public void onMatch(RelOptRuleCall call) { LogicalUnion union = call.rel(0); final List childRels = call.getChildRels(union); - final List newChildRels = new ArrayList(); + assert childRels != null; + final List newChildRels = new ArrayList<>(); for (RelNode childRel : childRels) { if (!isEmpty(childRel)) { newChildRels.add(childRel); diff --git a/core/src/main/java/org/apache/calcite/rel/stream/StreamRules.java b/core/src/main/java/org/apache/calcite/rel/stream/StreamRules.java index 28b99725133f..48e7bbf3dbdb 100644 --- a/core/src/main/java/org/apache/calcite/rel/stream/StreamRules.java +++ b/core/src/main/java/org/apache/calcite/rel/stream/StreamRules.java @@ -24,18 +24,22 @@ import org.apache.calcite.rel.RelNode; import org.apache.calcite.rel.core.Aggregate; import org.apache.calcite.rel.core.Filter; +import org.apache.calcite.rel.core.Join; import org.apache.calcite.rel.core.Project; import org.apache.calcite.rel.core.Sort; import org.apache.calcite.rel.core.TableScan; import org.apache.calcite.rel.core.Union; +import org.apache.calcite.rel.core.Values; import org.apache.calcite.rel.logical.LogicalAggregate; import org.apache.calcite.rel.logical.LogicalFilter; +import org.apache.calcite.rel.logical.LogicalJoin; import org.apache.calcite.rel.logical.LogicalProject; import org.apache.calcite.rel.logical.LogicalSort; import org.apache.calcite.rel.logical.LogicalTableScan; import org.apache.calcite.rel.logical.LogicalUnion; import org.apache.calcite.schema.StreamableTable; import org.apache.calcite.schema.Table; +import org.apache.calcite.tools.RelBuilder; import org.apache.calcite.util.Util; import com.google.common.collect.ImmutableList; @@ -56,7 +60,9 @@ private StreamRules() {} new DeltaAggregateTransposeRule(), new DeltaSortTransposeRule(), new DeltaUnionTransposeRule(), - new DeltaTableScanRule()); + new DeltaJoinTransposeRule(), + new DeltaTableScanRule(), + new DeltaTableScanToEmptyRule()); /** Planner rule that pushes a {@link Delta} through a {@link Project}. */ public static class DeltaProjectTransposeRule extends RelOptRule { @@ -193,6 +199,75 @@ private DeltaTableScanRule() { } } } + + /** + * Planner rule that converts {@link Delta} over a {@link TableScan} of + * a table other than {@link org.apache.calcite.schema.StreamableTable} to + * an empty {@link Values}. + */ + public static class DeltaTableScanToEmptyRule extends RelOptRule { + private DeltaTableScanToEmptyRule() { + super( + operand(Delta.class, + operand(TableScan.class, none()))); + } + + @Override public void onMatch(RelOptRuleCall call) { + final Delta delta = call.rel(0); + final TableScan scan = call.rel(1); + final RelOptTable relOptTable = scan.getTable(); + final StreamableTable streamableTable = + relOptTable.unwrap(StreamableTable.class); + final RelBuilder builder = call.builder(); + if (streamableTable == null) { + call.transformTo(builder.values(delta.getRowType()).build()); + } + } + } + + /** + * Planner rule that pushes a {@link Delta} through a {@link Join}. + * + *

We apply something analogous to the + * product rule of + * differential calculus to implement the transpose: + * + *

stream(x join y) → + * x join stream(y) union all stream(x) join y
+ */ + public static class DeltaJoinTransposeRule extends RelOptRule { + + public DeltaJoinTransposeRule() { + super( + operand(Delta.class, + operand(Join.class, any()))); + } + + public void onMatch(RelOptRuleCall call) { + final Delta delta = call.rel(0); + Util.discard(delta); + final Join join = call.rel(1); + final RelNode left = join.getLeft(); + final RelNode right = join.getRight(); + + final LogicalDelta rightWithDelta = LogicalDelta.create(right); + final LogicalJoin joinL = LogicalJoin.create(left, rightWithDelta, join.getCondition(), + join.getJoinType(), join.getVariablesStopped(), join.isSemiJoinDone(), + ImmutableList.copyOf(join.getSystemFieldList())); + + final LogicalDelta leftWithDelta = LogicalDelta.create(left); + final LogicalJoin joinR = LogicalJoin.create(leftWithDelta, right, join.getCondition(), + join.getJoinType(), join.getVariablesStopped(), join.isSemiJoinDone(), + ImmutableList.copyOf(join.getSystemFieldList())); + + List inputsToUnion = Lists.newArrayList(); + inputsToUnion.add(joinL); + inputsToUnion.add(joinR); + + final LogicalUnion newNode = LogicalUnion.create(inputsToUnion, true); + call.transformTo(newNode); + } + } } // End StreamRules.java diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java index aa701d93270f..6eabafdb83fb 100644 --- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java +++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java @@ -603,6 +603,9 @@ ExInst illegalArgumentForTableFunctionCall(String a0, @BaseMessage("Table ''{0}'' not found") ExInst tableNotFound(String tableName); + + @BaseMessage("Cannot stream results of a query with no streaming inputs: ''{0}''. At least one input should be convertible to a stream") + ExInst cannotStreamResultsForNonStreamingInputs(String inputs); } // End CalciteResource.java diff --git a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java index f33fd1e91e5e..bacf239e08b4 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java @@ -16,6 +16,11 @@ */ package org.apache.calcite.sql; +import org.apache.calcite.config.NullCollation; +import org.apache.calcite.rel.RelFieldCollation; + +import com.google.common.base.Preconditions; + import java.sql.DatabaseMetaData; import java.sql.SQLException; import java.sql.Timestamp; @@ -56,6 +61,7 @@ public class SqlDialect { private final String identifierEndQuoteString; private final String identifierEscapedQuote; private final DatabaseProduct databaseProduct; + private final NullCollation nullCollation; //~ Constructors ----------------------------------------------------------- @@ -82,10 +88,32 @@ public static SqlDialect create(DatabaseMetaData databaseMetaData) { } catch (SQLException e) { throw FakeUtil.newInternal(e, "while detecting database product"); } + String databaseProductVersion; + try { + databaseProductVersion = databaseMetaData.getDatabaseProductVersion(); + } catch (SQLException e) { + throw FakeUtil.newInternal(e, "while detecting database version"); + } final DatabaseProduct databaseProduct = - getProduct(databaseProductName, null); - return new SqlDialect( - databaseProduct, databaseProductName, identifierQuoteString); + getProduct(databaseProductName, databaseProductVersion); + NullCollation nullCollation; + try { + if (databaseMetaData.nullsAreSortedAtEnd()) { + nullCollation = NullCollation.LAST; + } else if (databaseMetaData.nullsAreSortedAtStart()) { + nullCollation = NullCollation.FIRST; + } else if (databaseMetaData.nullsAreSortedLow()) { + nullCollation = NullCollation.LOW; + } else if (databaseMetaData.nullsAreSortedHigh()) { + nullCollation = NullCollation.HIGH; + } else { + throw new IllegalArgumentException("cannot deduce null collation"); + } + } catch (SQLException e) { + throw new IllegalArgumentException("cannot deduce null collation", e); + } + return new SqlDialect(databaseProduct, databaseProductName, + identifierQuoteString, nullCollation); } /** @@ -97,13 +125,32 @@ public static SqlDialect create(DatabaseMetaData databaseMetaData) { * is not supported. If "[", close quote is * deemed to be "]". */ + @Deprecated // to be removed before 2.0 public SqlDialect( DatabaseProduct databaseProduct, String databaseProductName, String identifierQuoteString) { - assert databaseProduct != null; - assert databaseProductName != null; - this.databaseProduct = databaseProduct; + this(databaseProduct, databaseProductName, identifierQuoteString, + NullCollation.HIGH); + } + + /** + * Creates a SqlDialect. + * + * @param databaseProduct Database product; may be UNKNOWN, never null + * @param databaseProductName Database product name from JDBC driver + * @param identifierQuoteString String to quote identifiers. Null if quoting + * is not supported. If "[", close quote is + * deemed to be "]". + * @param nullCollation Whether NULL values appear first or last + */ + public SqlDialect( + DatabaseProduct databaseProduct, + String databaseProductName, + String identifierQuoteString, NullCollation nullCollation) { + Preconditions.checkNotNull(this.nullCollation = nullCollation); + Preconditions.checkNotNull(databaseProductName); + this.databaseProduct = Preconditions.checkNotNull(databaseProduct); if (identifierQuoteString != null) { identifierQuoteString = identifierQuoteString.trim(); if (identifierQuoteString.equals("")) { @@ -419,6 +466,32 @@ public boolean supportsCharSet() { } } + /** Returns how NULL values are sorted if an ORDER BY item does not contain + * NULLS ASCENDING or NULLS DESCENDING. */ + public NullCollation getNullCollation() { + return nullCollation; + } + + /** Returns whether NULL values are sorted first or last, in this dialect, + * in an ORDER BY item of a given direction. */ + public RelFieldCollation.NullDirection defaultNullDirection( + RelFieldCollation.Direction direction) { + switch (direction) { + case ASCENDING: + case STRICTLY_ASCENDING: + return getNullCollation().last(false) + ? RelFieldCollation.NullDirection.LAST + : RelFieldCollation.NullDirection.FIRST; + case DESCENDING: + case STRICTLY_DESCENDING: + return getNullCollation().last(true) + ? RelFieldCollation.NullDirection.LAST + : RelFieldCollation.NullDirection.FIRST; + default: + return RelFieldCollation.NullDirection.UNSPECIFIED; + } + } + /** * A few utility functions copied from org.apache.calcite.util.Util. We have * copied them because we wish to keep SqlDialect's dependencies to a @@ -478,31 +551,31 @@ public static String replace( * whether the database allows expressions to appear in the GROUP BY clause. */ public enum DatabaseProduct { - ACCESS("Access", "\""), - CALCITE("Apache Calcite", "\""), - MSSQL("Microsoft SQL Server", "["), - MYSQL("MySQL", "`"), - ORACLE("Oracle", "\""), - DERBY("Apache Derby", null), - DB2("IBM DB2", null), - FIREBIRD("Firebird", null), - H2("H2", "\""), - HIVE("Apache Hive", null), - INFORMIX("Informix", null), - INGRES("Ingres", null), - LUCIDDB("LucidDB", "\""), - INTERBASE("Interbase", null), - PHOENIX("Phoenix", "\""), - POSTGRESQL("PostgreSQL", "\""), - NETEZZA("Netezza", "\""), - INFOBRIGHT("Infobright", "`"), - NEOVIEW("Neoview", null), - SYBASE("Sybase", null), - TERADATA("Teradata", "\""), - HSQLDB("Hsqldb", null), - VERTICA("Vertica", "\""), - SQLSTREAM("SQLstream", "\""), - PARACCEL("Paraccel", "\""), + ACCESS("Access", "\"", NullCollation.HIGH), + CALCITE("Apache Calcite", "\"", NullCollation.HIGH), + MSSQL("Microsoft SQL Server", "[", NullCollation.HIGH), + MYSQL("MySQL", "`", NullCollation.HIGH), + ORACLE("Oracle", "\"", NullCollation.HIGH), + DERBY("Apache Derby", null, NullCollation.HIGH), + DB2("IBM DB2", null, NullCollation.HIGH), + FIREBIRD("Firebird", null, NullCollation.HIGH), + H2("H2", "\"", NullCollation.HIGH), + HIVE("Apache Hive", null, NullCollation.HIGH), + INFORMIX("Informix", null, NullCollation.HIGH), + INGRES("Ingres", null, NullCollation.HIGH), + LUCIDDB("LucidDB", "\"", NullCollation.HIGH), + INTERBASE("Interbase", null, NullCollation.HIGH), + PHOENIX("Phoenix", "\"", NullCollation.HIGH), + POSTGRESQL("PostgreSQL", "\"", NullCollation.HIGH), + NETEZZA("Netezza", "\"", NullCollation.HIGH), + INFOBRIGHT("Infobright", "`", NullCollation.HIGH), + NEOVIEW("Neoview", null, NullCollation.HIGH), + SYBASE("Sybase", null, NullCollation.HIGH), + TERADATA("Teradata", "\"", NullCollation.HIGH), + HSQLDB("Hsqldb", null, NullCollation.HIGH), + VERTICA("Vertica", "\"", NullCollation.HIGH), + SQLSTREAM("SQLstream", "\"", NullCollation.HIGH), + PARACCEL("Paraccel", "\"", NullCollation.HIGH), /** * Placeholder for the unknown database. * @@ -510,15 +583,18 @@ public enum DatabaseProduct { * do something database-specific like quoting identifiers, don't rely * on this dialect to do what you want. */ - UNKNOWN("Unknown", "`"); + UNKNOWN("Unknown", "`", NullCollation.HIGH); private SqlDialect dialect = null; private String databaseProductName; private String quoteString; + private final NullCollation nullCollation; - DatabaseProduct(String databaseProductName, String quoteString) { + DatabaseProduct(String databaseProductName, String quoteString, + NullCollation nullCollation) { this.databaseProductName = databaseProductName; this.quoteString = quoteString; + this.nullCollation = nullCollation; } /** @@ -535,7 +611,8 @@ public enum DatabaseProduct { public SqlDialect getDialect() { if (dialect == null) { dialect = - new SqlDialect(this, databaseProductName, quoteString); + new SqlDialect(this, databaseProductName, quoteString, + nullCollation); } return dialect; } 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 888a070ccb80..13dd0dd09336 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlSelectOperator.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlSelectOperator.java @@ -236,6 +236,26 @@ public void unparse( unparseListClause(writer, select.orderBy); writer.endList(orderFrame); } + if (select.offset != null) { + final SqlWriter.Frame offsetFrame = + writer.startList(SqlWriter.FrameTypeEnum.OFFSET); + writer.newlineAndIndent(); + writer.keyword("OFFSET"); + select.offset.unparse(writer, -1, -1); + writer.keyword("ROWS"); + writer.endList(offsetFrame); + } + if (select.fetch != null) { + final SqlWriter.Frame fetchFrame = + writer.startList(SqlWriter.FrameTypeEnum.FETCH); + writer.newlineAndIndent(); + writer.keyword("FETCH"); + writer.keyword("NEXT"); + select.fetch.unparse(writer, -1, -1); + writer.keyword("ROWS"); + writer.keyword("ONLY"); + writer.endList(fetchFrame); + } writer.endList(selectFrame); } 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 0390c0781c81..1218d5ae969b 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 @@ -82,6 +82,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; +import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; @@ -3016,17 +3017,47 @@ private SqlModality deduceModality(SqlNode query) { public boolean validateModality(SqlSelect select, SqlModality modality, boolean fail) { final SelectScope scope = getRawSelectScope(select); - for (Pair namespace : scope.children) { - if (!namespace.right.supportsModality(modality)) { - switch (modality) { - case STREAM: + + switch (modality) { + case STREAM: + if (scope.children.size() == 1) { + for (Pair namespace : scope.children) { + if (!namespace.right.supportsModality(modality)) { + if (fail) { + throw newValidationError(namespace.right.getNode(), + Static.RESOURCE.cannotConvertToStream(namespace.left)); + } else { + return false; + } + } + } + } else { + int supportsModalityCount = 0; + for (Pair namespace : scope.children) { + if (namespace.right.supportsModality(modality)) { + ++supportsModalityCount; + } + } + + if (supportsModalityCount == 0) { if (fail) { - throw newValidationError(namespace.right.getNode(), - Static.RESOURCE.cannotConvertToStream(namespace.left)); + List inputList = new ArrayList(); + for (Pair namespace : scope.children) { + inputList.add(namespace.left); + } + String inputs = Joiner.on(", ").join(inputList); + + throw newValidationError(select, + Static.RESOURCE.cannotStreamResultsForNonStreamingInputs(inputs)); } else { return false; } - default: + } + } + break; + default: + for (Pair namespace : scope.children) { + if (!namespace.right.supportsModality(modality)) { if (fail) { throw newValidationError(namespace.right.getNode(), Static.RESOURCE.cannotConvertToRelation(namespace.left)); diff --git a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties index 9787ba97c88b..7e97a519154b 100644 --- a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties +++ b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties @@ -197,4 +197,5 @@ ModifiableViewMustBeBasedOnSingleTable=Modifiable view must be based on a single MoreThanOneMappedColumn=View is not modifiable. More than one expression maps to column ''{0}'' of base table ''{1}'' NoValueSuppliedForViewColumn=View is not modifiable. No value is supplied for NOT NULL column ''{0}'' of base table ''{1}'' TableNotFound=Table ''{0}'' not found +CannotStreamResultsForNonStreamingInputs=Cannot stream results of a query with no streaming inputs: ''{0}''. At least one input should be convertible to a stream # End CalciteResource.properties diff --git a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java new file mode 100644 index 000000000000..c3350bbe05c8 --- /dev/null +++ b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java @@ -0,0 +1,386 @@ +/* + * 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.rel2sql; + +import org.apache.calcite.plan.RelTraitDef; +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.schema.SchemaPlus; +import org.apache.calcite.sql.SqlDialect; +import org.apache.calcite.sql.SqlNode; +import org.apache.calcite.sql.parser.SqlParser; +import org.apache.calcite.test.CalciteAssert; +import org.apache.calcite.tools.FrameworkConfig; +import org.apache.calcite.tools.Frameworks; +import org.apache.calcite.tools.Planner; +import org.apache.calcite.tools.Program; + +import org.junit.Ignore; +import org.junit.Test; + +import java.util.List; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * Tests for utility {@link RelToSqlConverter} + */ +public class RelToSqlConverterTest { + private Planner logicalPlanner = getPlanner(null); + + private void checkRel2Sql(Planner planner, String query, String expectedQeury) { + try { + SqlNode parse = planner.parse(query); + SqlNode validate = planner.validate(parse); + RelNode rel = planner.rel(validate).rel; + final RelToSqlConverter converter = + new RelToSqlConverter(SqlDialect.CALCITE); + final SqlNode sqlNode = converter.visitChild(0, rel).asQuery(); + assertThat(sqlNode.toSqlString(SqlDialect.CALCITE).getSql(), + equalTo(expectedQeury)); + } catch (Exception e) { + assertTrue("Parsing failed throwing error: " + e.getMessage(), false); + } + } + + private Planner getPlanner(List traitDefs, Program... programs) { + return getPlanner(traitDefs, SqlParser.Config.DEFAULT, programs); + } + + private Planner getPlanner(List traitDefs, + SqlParser.Config parserConfig, + Program... programs) { + final SchemaPlus rootSchema = Frameworks.createRootSchema(true); + final FrameworkConfig config = Frameworks.newConfigBuilder() + .parserConfig(parserConfig) + .defaultSchema( + CalciteAssert.addSchema(rootSchema, CalciteAssert.SchemaSpec.JDBC_FOODMART)) + .traitDefs(traitDefs) + .programs(programs) + .build(); + return Frameworks.getPlanner(config); + } + + @Test + public void testSimpleSelectStarFromProductTable() { + String query = "select * from \"product\""; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT *\nFROM \"foodmart\".\"product\""); + } + + + @Test + public void testSimpleSelectQueryFromProductTable() { + String query = "select \"product_id\", \"product_class_id\" from \"product\""; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT \"product_id\", \"product_class_id\"\n" + + "FROM \"foodmart\".\"product\""); + } + + //TODO: add test for query -> select * from product + + @Test + public void testSelectQueryWithWhereClauseOfLessThan() { + String query = + "select \"product_id\", \"shelf_width\" from \"product\" where \"product_id\" < 10"; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT \"product_id\", \"shelf_width\"\n" + + "FROM \"foodmart\".\"product\"\n" + + "WHERE \"product_id\" < 10"); + } + + @Test + public void testSelectQueryWithWhereClauseOfBasicOperators() { + String query = "select * from \"product\" " + + "where (\"product_id\" = 10 OR \"product_id\" <= 5) " + + "AND (80 >= \"shelf_width\" OR \"shelf_width\" > 30)"; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT *\n" + + "FROM \"foodmart\".\"product\"\n" + + "WHERE (\"product_id\" = 10 OR \"product_id\" <= 5) " + + "AND (80 >= \"shelf_width\" OR \"shelf_width\" > 30)"); + } + + + @Test + public void testSelectQueryWithGroupBy() { + String query = "select count(*) from \"product\" group by \"product_class_id\", \"product_id\""; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT COUNT(*)\n" + + "FROM \"foodmart\".\"product\"\n" + + "GROUP BY \"product_class_id\", \"product_id\""); + } + + @Test + public void testSelectQueryWithMinAggregateFunction() { + String query = "select min(\"net_weight\") from \"product\" group by \"product_class_id\" "; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT MIN(\"net_weight\")\n" + + "FROM \"foodmart\".\"product\"\n" + + "GROUP BY \"product_class_id\""); + } + + @Test + public void testSelectQueryWithMinAggregateFunction1() { + String query = "select \"product_class_id\", min(\"net_weight\") from" + + " \"product\" group by \"product_class_id\""; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT \"product_class_id\", MIN(\"net_weight\")\n" + + "FROM \"foodmart\".\"product\"\n" + + "GROUP BY \"product_class_id\""); + } + + @Test + public void testSelectQueryWithSumAggregateFunction() { + String query = + "select sum(\"net_weight\") from \"product\" group by \"product_class_id\" "; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT SUM(\"net_weight\")\n" + + "FROM \"foodmart\".\"product\"\n" + + "GROUP BY \"product_class_id\""); + } + + @Test + public void testSelectQueryWithMultipleAggregateFunction() { + String query = + "select sum(\"net_weight\"), min(\"low_fat\"), count(*)" + + " from \"product\" group by \"product_class_id\" "; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT SUM(\"net_weight\"), MIN(\"low_fat\"), COUNT(*)\n" + + "FROM \"foodmart\".\"product\"\n" + + "GROUP BY \"product_class_id\""); + } + + @Test + public void testSelectQueryWithMultipleAggregateFunction1() { + String query = + "select \"product_class_id\", sum(\"net_weight\"), min(\"low_fat\"), count(*)" + + " from \"product\" group by \"product_class_id\" "; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT \"product_class_id\", SUM(\"net_weight\"), MIN(\"low_fat\"), COUNT(*)\n" + + "FROM \"foodmart\".\"product\"\n" + + "GROUP BY \"product_class_id\"" + ); + } + + @Test + public void testSelectQueryWithGroupByAndProjectList() { + String query = + "select \"product_class_id\", \"product_id\", count(*) from \"product\" group " + + "by \"product_class_id\", \"product_id\" "; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT \"product_class_id\", \"product_id\", COUNT(*)\n" + + "FROM \"foodmart\".\"product\"\n" + + "GROUP BY \"product_class_id\", \"product_id\""); + } + + @Test + public void testSelectQueryWithGroupByAndProjectList1() { + String query = + "select count(*) from \"product\" group by \"product_class_id\", \"product_id\""; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT COUNT(*)\n" + + "FROM \"foodmart\".\"product\"\n" + + "GROUP BY \"product_class_id\", \"product_id\""); + } + + @Test + public void testSelectQueryWithGroupByHaving() { + String query = "select count(*) from \"product\" group by \"product_class_id\"," + + " \"product_id\" having \"product_id\" > 10"; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT COUNT(*)\n" + + "FROM (SELECT \"product_class_id\", \"product_id\", COUNT(*)\n" + + "FROM \"foodmart\".\"product\"\n" + + "GROUP BY \"product_class_id\", \"product_id\") AS \"t0\"\n" + + "WHERE \"product_id\" > 10"); + } + + @Test + public void testSelectQueryWithOrderByClause() { + String query = "select \"product_id\" from \"product\" order by \"net_weight\""; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT \"product_id\", \"net_weight\"\n" + + "FROM \"foodmart\".\"product\"\n" + + "ORDER BY \"net_weight\""); + } + + @Test + public void testSelectQueryWithOrderByClause1() { + String query = + "select \"product_id\", \"net_weight\" from \"product\" order by \"net_weight\""; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT \"product_id\", \"net_weight\"\n" + + "FROM \"foodmart\".\"product\"\n" + + "ORDER BY \"net_weight\""); + } + + @Test + public void testSelectQueryWithTwoOrderByClause() { + String query = + "select \"product_id\" from \"product\" order by \"net_weight\", \"gross_weight\""; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT \"product_id\", \"net_weight\", \"gross_weight\"\n" + + "FROM \"foodmart\".\"product\"\n" + + "ORDER BY \"net_weight\", \"gross_weight\""); + } + + @Test + public void testSelectQueryWithAscDescOrderByClause() { + String query = + "select \"product_id\" from \"product\" order by \"net_weight\" asc, " + + "\"gross_weight\" desc, \"low_fat\""; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT \"product_id\", \"net_weight\", \"gross_weight\", \"low_fat\"\n" + + "FROM \"foodmart\".\"product\"\n" + + "ORDER BY \"net_weight\", \"gross_weight\" DESC, \"low_fat\""); + } + + @Ignore("Need to fix this by enhancing dialects") + @Test + public void testSelectQueryWithLimitClause() { + String query = "select \"product_id\" from \"product\" limit 100 offset 10"; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT \"product_id\"\n" + + "FROM \"foodmart\".\"product\"\n" + + "LIMIT 100 OFFSET 10"); + } + + @Test + public void testSelectQueryWithLimitClauseWithoutOrder() { + String query = "select \"product_id\" from \"product\" limit 100 offset 10"; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT \"product_id\"\n" + + "FROM \"foodmart\".\"product\"\n" + + "OFFSET 10 ROWS\n" + + "FETCH NEXT 100 ROWS ONLY"); + } + + @Test + public void testSelectQueryWithLimitOffsetClause() { + String query = "select \"product_id\" from \"product\" order by \"net_weight\" asc" + + " limit 100 offset 10"; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT \"product_id\", \"net_weight\"\n" + + "FROM \"foodmart\".\"product\"\n" + + "ORDER BY \"net_weight\"\n" + + "OFFSET 10 ROWS\n" + + "FETCH NEXT 100 ROWS ONLY"); + } + + @Test + public void testSelectQueryWithFetchOffsetClause() { + String query = "select \"product_id\" from \"product\" order by \"product_id\"" + + " offset 10 rows fetch next 100 rows only"; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT \"product_id\"\n" + + "FROM \"foodmart\".\"product\"\n" + + "ORDER BY \"product_id\"\n" + + "OFFSET 10 ROWS\n" + + "FETCH NEXT 100 ROWS ONLY"); + } + + @Test + public void testSelectQueryComplex() { + String query = + "select count(*), \"units_per_case\" from \"product\" where \"cases_per_pallet\" > 100 " + + "group by \"product_id\", \"units_per_case\" order by \"units_per_case\" desc"; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT COUNT(*), \"units_per_case\"\n" + + "FROM \"foodmart\".\"product\"\n" + + "WHERE \"cases_per_pallet\" > 100\n" + + "GROUP BY \"product_id\", \"units_per_case\"\n" + + "ORDER BY \"units_per_case\" DESC"); + } + + @Test + public void testSelectQueryWithGroup() { + String query = + "select count(*), sum(\"employee_id\") from \"reserve_employee\" " + + "where \"hire_date\" > '2015-01-01' " + + "and (\"position_title\" = 'SDE' or \"position_title\" = 'SDM') " + + "group by \"store_id\", \"position_title\""; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT COUNT(*), SUM(\"employee_id\")\n" + + "FROM \"foodmart\".\"reserve_employee\"\n" + + "WHERE \"hire_date\" > '2015-01-01' " + + "AND (\"position_title\" = 'SDE' OR \"position_title\" = 'SDM')\n" + + "GROUP BY \"store_id\", \"position_title\""); + } + + @Test + public void testSimpleJoin() { + String query = "select *\n" + + "from \"sales_fact_1997\" as s\n" + + " join \"customer\" as c using (\"customer_id\")\n" + + " join \"product\" as p using (\"product_id\")\n" + + " join \"product_class\" as pc using (\"product_class_id\")\n" + + "where c.\"city\" = 'San Francisco'\n" + + "and pc.\"product_department\" = 'Snacks'\n"; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT *\nFROM \"foodmart\".\"sales_fact_1997\"\n" + + "INNER JOIN \"foodmart\".\"customer\" " + + "ON \"sales_fact_1997\".\"customer_id\" = \"customer\".\"customer_id\"\n" + + "INNER JOIN \"foodmart\".\"product\" " + + "ON \"sales_fact_1997\".\"product_id\" = \"product\".\"product_id\"\n" + + "INNER JOIN \"foodmart\".\"product_class\" " + + "ON \"product\".\"product_class_id\" = \"product_class\".\"product_class_id\"\n" + + "WHERE \"customer\".\"city\" = 'San Francisco' AND " + + "\"product_class\".\"product_department\" = 'Snacks'" + ); + } + + @Test public void testSimpleIn() { + String query = "select * from \"department\" where \"department_id\" in (\n" + + " select \"department_id\" from \"employee\"\n" + + " where \"store_id\" < 150)"; + checkRel2Sql(this.logicalPlanner, + query, + "SELECT \"department\".\"department_id\", \"department\".\"department_description\"\n" + + "FROM \"foodmart\".\"department\"\nINNER JOIN " + + "(SELECT \"department_id\"\nFROM \"foodmart\".\"employee\"\n" + + "WHERE \"store_id\" < 150\nGROUP BY \"department_id\") AS \"t1\" " + + "ON \"department\".\"department_id\" = \"t1\".\"department_id\""); + } +} + +// End RelToSqlConverterTest.java diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java b/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java index 1a71ec467b14..50019e17d4e0 100644 --- a/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java +++ b/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java @@ -72,7 +72,9 @@ public class SqlAdvisorTest extends SqlValidatorTestCase { "TABLE(CATALOG.SALES.BONUS)", "TABLE(CATALOG.SALES.ORDERS)", "TABLE(CATALOG.SALES.SALGRADE)", - "TABLE(CATALOG.SALES.SHIPMENTS)"); + "TABLE(CATALOG.SALES.SHIPMENTS)", + "TABLE(CATALOG.SALES.PRODUCTS)", + "TABLE(CATALOG.SALES.SUPPLIERS)"); private static final List SCHEMAS = Arrays.asList( diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlTester.java b/core/src/test/java/org/apache/calcite/sql/test/SqlTester.java index 5c14704c511d..4144eb1a13c0 100644 --- a/core/src/test/java/org/apache/calcite/sql/test/SqlTester.java +++ b/core/src/test/java/org/apache/calcite/sql/test/SqlTester.java @@ -25,7 +25,6 @@ import org.apache.calcite.sql.validate.SqlMonotonicity; import org.apache.calcite.test.SqlValidatorTestCase; -import java.io.Closeable; import java.sql.ResultSet; /** @@ -41,7 +40,7 @@ * queries in different ways, for example, using a C++ versus Java calculator. * An implementation might even ignore certain calls altogether. */ -public interface SqlTester extends Closeable, SqlValidatorTestCase.Tester { +public interface SqlTester extends AutoCloseable, SqlValidatorTestCase.Tester { //~ Enums ------------------------------------------------------------------ /** 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 95bc225b6807..0ca057a26b45 100644 --- a/core/src/test/java/org/apache/calcite/test/CalciteSuite.java +++ b/core/src/test/java/org/apache/calcite/test/CalciteSuite.java @@ -25,6 +25,7 @@ import org.apache.calcite.plan.volcano.VolcanoPlannerTest; import org.apache.calcite.plan.volcano.VolcanoPlannerTraitTest; import org.apache.calcite.rel.RelCollationTest; +import org.apache.calcite.rel.rel2sql.RelToSqlConverterTest; import org.apache.calcite.rex.RexExecutorTest; import org.apache.calcite.runtime.BinarySearchTest; import org.apache.calcite.runtime.EnumerablesTest; @@ -106,6 +107,7 @@ JdbcFrontLinqBackTest.class, JdbcFrontJdbcBackTest.class, SqlToRelConverterTest.class, + RelToSqlConverterTest.class, SqlOperatorTest.class, ChunkListTest.class, FrameworksTest.class, diff --git a/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java b/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java index 71b3115142c5..529df0480c02 100644 --- a/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java +++ b/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java @@ -250,6 +250,22 @@ public MockCatalogReader init() { shipmentsStream.addColumn("ORDERID", intType); registerTable(shipmentsStream); + // Register "PRODUCTS" table. + MockTable productsTable = MockTable.create(this, salesSchema, "PRODUCTS", + false, 200D); + productsTable.addColumn("PRODUCTID", intType); + productsTable.addColumn("NAME", varchar20Type); + productsTable.addColumn("SUPPLIERID", intType); + registerTable(productsTable); + + // Register "SUPPLIERS" table. + MockTable suppliersTable = MockTable.create(this, salesSchema, "SUPPLIERS", + false, 10D); + suppliersTable.addColumn("SUPPLIERID", intType); + suppliersTable.addColumn("NAME", varchar20Type); + suppliersTable.addColumn("CITY", intType); + registerTable(suppliersTable); + // Register "EMP_20" view. // Same columns as "EMP", // but "DEPTNO" not visible and set to 20 by default diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java index 026f41ef2fdb..64d1c560bb6a 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -132,6 +132,12 @@ private static String cannotConvertToRelation(String table) { return "Cannot convert stream '" + table + "' to relation"; } + private static String cannotStreamResultsForNonStreamingInputs(String inputs) { + return "Cannot stream results of a query with no streaming inputs: '" + + inputs + + "'. At least one input should be convertible to a stream"; + } + @Test public void testMultipleSameAsPass() { check("select 1 as again,2 as \"again\", 3 as AGAiN from (values (true))"); } @@ -7415,6 +7421,15 @@ public void _testValuesWithAggFuncs() { + "order by floor(rowtime to hour), rowtime desc").ok(); } + @Test public void testStreamJoin() { + sql("select stream \n" + + "orders.rowtime as rowtime, orders.orderId as orderId, products.supplierId as supplierId \n" + + "from orders join products on orders.productId = products.productId").ok(); + sql("^select stream *\n" + + "from products join suppliers on products.supplierId = suppliers.supplierId^") + .fails(cannotStreamResultsForNonStreamingInputs("PRODUCTS, SUPPLIERS")); + } + @Test public void testNew() { // (To debug individual statements, paste them into this method.) // 1 2 3 4 5 6 diff --git a/core/src/test/java/org/apache/calcite/test/StreamTest.java b/core/src/test/java/org/apache/calcite/test/StreamTest.java index 269a2e5ead2f..649db3d4db2c 100644 --- a/core/src/test/java/org/apache/calcite/test/StreamTest.java +++ b/core/src/test/java/org/apache/calcite/test/StreamTest.java @@ -59,6 +59,7 @@ public class StreamTest { public static final String STREAM_SCHEMA_NAME = "STREAMS"; public static final String INFINITE_STREAM_SCHEMA_NAME = "INFINITE_STREAMS"; + public static final String STREAM_JOINS_SCHEMA_NAME = "STREAM_JOINS"; private static String schemaFor(String name, Class clazz) { return " {\n" @@ -74,6 +75,27 @@ private static String schemaFor(String name, Class clazz + " }"; } + private static final String STREAM_JOINS_MODEL = "{\n" + + " version: '1.0',\n" + + " defaultSchema: 'STREAM_JOINS',\n" + + " schemas: [\n" + + " {\n" + + " name: 'STREAM_JOINS',\n" + + " tables: [ {\n" + + " type: 'custom',\n" + + " name: 'ORDERS',\n" + + " stream: {\n" + + " stream: true\n" + + " },\n" + + " factory: '" + OrdersStreamTableFactory.class.getName() + "'\n" + + " }, \n" + + " {\n" + + " type: 'custom',\n" + + " name: 'PRODUCTS',\n" + + " factory: '" + ProductsTableFactory.class.getName() + "'\n" + + " }]\n" + + " }]}"; + public static final String STREAM_MODEL = "{\n" + " version: '1.0',\n" + " defaultSchema: 'foodmart',\n" @@ -212,6 +234,33 @@ private static String schemaFor(String name, Class clazz .returnsCount(100); } + @Test public void testStreamToRelationJoin() { + CalciteAssert.model(STREAM_JOINS_MODEL) + .withDefaultSchema(STREAM_JOINS_SCHEMA_NAME) + .query("select stream " + + "orders.rowtime as rowtime, orders.id as orderId, products.supplier as supplierId " + + "from orders join products on orders.product = products.id") + .convertContains("LogicalDelta\n" + + " LogicalProject(ROWTIME=[$0], ORDERID=[$1], SUPPLIERID=[$5])\n" + + " LogicalProject(ROWTIME=[$0], ID=[$1], PRODUCT=[$2], UNITS=[$3], ID0=[$5], SUPPLIER=[$6])\n" + + " LogicalJoin(condition=[=($4, $5)], joinType=[inner])\n" + + " LogicalProject(ROWTIME=[$0], ID=[$1], PRODUCT=[$2], UNITS=[$3], PRODUCT4=[CAST($2):VARCHAR(32) CHARACTER SET \"ISO-8859-1\" COLLATE \"ISO-8859-1$en_US$primary\" NOT NULL])\n" + + " LogicalTableScan(table=[[STREAM_JOINS, ORDERS]])\n" + + " LogicalTableScan(table=[[STREAM_JOINS, PRODUCTS]])\n") + .explainContains("" + + "EnumerableCalc(expr#0..6=[{inputs}], proj#0..1=[{exprs}], SUPPLIERID=[$t6])\n" + + " EnumerableJoin(condition=[=($4, $5)], joinType=[inner])\n" + + " EnumerableCalc(expr#0..3=[{inputs}], expr#4=[CAST($t2):VARCHAR(32) CHARACTER SET \"ISO-8859-1\" COLLATE \"ISO-8859-1$en_US$primary\" NOT NULL], proj#0..4=[{exprs}])\n" + + " EnumerableInterpreter\n" + + " BindableTableScan(table=[[]])\n" + + " EnumerableInterpreter\n" + + " BindableTableScan(table=[[STREAM_JOINS, PRODUCTS]])") + .returns( + startsWith("ROWTIME=2015-02-15 10:15:00; ORDERID=1; SUPPLIERID=1", + "ROWTIME=2015-02-15 10:24:15; ORDERID=2; SUPPLIERID=0", + "ROWTIME=2015-02-15 10:24:45; ORDERID=3; SUPPLIERID=1")); + } + private Function startsWith(String... rows) { final ImmutableList rowList = ImmutableList.copyOf(rows); return new Function() { @@ -276,14 +325,14 @@ public OrdersStreamTableFactory() { public Table create(SchemaPlus schema, String name, Map operand, RelDataType rowType) { - final ImmutableList rows = ImmutableList.of( - new Object[] {ts(10, 15, 0), 1, "paint", 10}, - new Object[] {ts(10, 24, 15), 2, "paper", 5}, - new Object[] {ts(10, 24, 45), 3, "brush", 12}, - new Object[] {ts(10, 58, 0), 4, "paint", 3}, - new Object[] {ts(11, 10, 0), 5, "paint", 3}); - - return new OrdersTable(rows); + final Object[][] rows = { + {ts(10, 15, 0), 1, "paint", 10}, + {ts(10, 24, 15), 2, "paper", 5}, + {ts(10, 24, 45), 3, "brush", 12}, + {ts(10, 58, 0), 4, "paint", 3}, + {ts(11, 10, 0), 5, "paint", 3} + }; + return new OrdersTable(ImmutableList.copyOf(rows)); } private Object ts(int h, int m, int s) { @@ -358,10 +407,61 @@ public void remove() { }); } - @Override public Table stream() { + public Table stream() { return this; } } + + /** + * Mocks a simple relation to use for stream joining test. + */ + public static class ProductsTableFactory implements TableFactory { + public Table create(SchemaPlus schema, String name, + Map operand, RelDataType rowType) { + final Object[][] rows = { + {"paint", 1}, + {"paper", 0}, + {"brush", 1} + }; + return new ProductsTable(ImmutableList.copyOf(rows)); + } + } + + /** + * Table representing the PRODUCTS relation. + */ + public static class ProductsTable implements ScannableTable { + private final ImmutableList rows; + + public ProductsTable(ImmutableList rows) { + this.rows = rows; + } + + private final RelProtoDataType protoRowType = new RelProtoDataType() { + public RelDataType apply(RelDataTypeFactory a0) { + return a0.builder() + .add("ID", SqlTypeName.VARCHAR, 32) + .add("SUPPLIER", SqlTypeName.INTEGER) + .build(); + } + }; + + public Enumerable scan(DataContext root) { + return Linq4j.asEnumerable(rows); + } + + public RelDataType getRowType(RelDataTypeFactory typeFactory) { + return protoRowType.apply(typeFactory); + } + + public Statistic getStatistic() { + return Statistics.of(200d, ImmutableList.of()); + } + + public Schema.TableType getJdbcTableType() { + return Schema.TableType.TABLE; + } + } } // End StreamTest.java diff --git a/core/src/test/java/org/apache/calcite/tools/PlannerTest.java b/core/src/test/java/org/apache/calcite/tools/PlannerTest.java index 01816df59cc7..b76965304b98 100644 --- a/core/src/test/java/org/apache/calcite/tools/PlannerTest.java +++ b/core/src/test/java/org/apache/calcite/tools/PlannerTest.java @@ -578,7 +578,7 @@ public void onMatch(RelOptRuleCall call) { "select * from (select * from \"emps\") as t\n" + "where \"name\" like '%e%'"); final SqlDialect hiveDialect = - new SqlDialect(SqlDialect.DatabaseProduct.HIVE, "Hive", null); + SqlDialect.DatabaseProduct.HIVE.getDialect(); assertThat(Util.toLinux(parse.toSqlString(hiveDialect).getSql()), equalTo("SELECT *\n" + "FROM (SELECT *\n" diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/Enumerator.java b/linq4j/src/main/java/org/apache/calcite/linq4j/Enumerator.java index 143e644b03b5..0bc2e01528f1 100644 --- a/linq4j/src/main/java/org/apache/calcite/linq4j/Enumerator.java +++ b/linq4j/src/main/java/org/apache/calcite/linq4j/Enumerator.java @@ -16,8 +16,6 @@ */ package org.apache.calcite.linq4j; -import java.io.Closeable; - /** * Supports a simple iteration over a collection. * @@ -28,7 +26,7 @@ * * @param Element type */ -public interface Enumerator extends Closeable { +public interface Enumerator extends AutoCloseable { /** * Gets the current element in the collection. * diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/Linq4j.java b/linq4j/src/main/java/org/apache/calcite/linq4j/Linq4j.java index da46850b3c51..c0d8d75c33ac 100644 --- a/linq4j/src/main/java/org/apache/calcite/linq4j/Linq4j.java +++ b/linq4j/src/main/java/org/apache/calcite/linq4j/Linq4j.java @@ -20,11 +20,7 @@ import com.google.common.collect.Lists; -import java.io.Closeable; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.sql.ResultSet; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -41,9 +37,6 @@ private Linq4j() {} private static final Object DUMMY = new Object(); - private static final Method AUTO_CLOSEABLE_CLOSE_METHOD = - getMethod("java.lang.AutoCloseable", "close"); - public static Method getMethod(String className, String methodName, Class... parameterTypes) { try { @@ -436,39 +429,13 @@ public static T requireNonNull(T o) { /** Closes an iterator, if it can be closed. */ private static void closeIterator(Iterator iterator) { - if (AUTO_CLOSEABLE_CLOSE_METHOD != null) { - // JDK 1.7 or later - if (AUTO_CLOSEABLE_CLOSE_METHOD.getDeclaringClass() - .isInstance(iterator)) { - try { - AUTO_CLOSEABLE_CLOSE_METHOD.invoke(iterator); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } catch (InvocationTargetException e) { - throw new RuntimeException(e.getCause()); - } - } - } else { - // JDK 1.5 or 1.6. No AutoCloseable. Cover the two most common cases - // with a close(). - if (iterator instanceof Closeable) { - try { - ((Closeable) iterator).close(); - return; - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - if (iterator instanceof ResultSet) { - try { - ((ResultSet) iterator).close(); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(e); - } + if (iterator instanceof AutoCloseable) { + try { + ((AutoCloseable) iterator).close(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); } } } @@ -687,7 +654,8 @@ public void close() { } /** Iterator that reads from an underlying {@link Enumerator}. */ - private static class EnumeratorIterator implements Iterator, Closeable { + private static class EnumeratorIterator + implements Iterator, AutoCloseable { private final Enumerator enumerator; boolean hasNext; @@ -710,7 +678,7 @@ public void remove() { throw new UnsupportedOperationException(); } - public void close() throws IOException { + public void close() { enumerator.close(); } }