blob: 3d39e9ebdbf17c046ade141184e424e3014eadc5 [file] [log] [blame]
/*
* 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.adapter.jdbc.JdbcTable;
import org.apache.calcite.linq4j.Ord;
import org.apache.calcite.linq4j.tree.Expressions;
import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.rel.RelCollation;
import org.apache.calcite.rel.RelCollations;
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.Correlate;
import org.apache.calcite.rel.core.CorrelationId;
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.JoinRelType;
import org.apache.calcite.rel.core.Match;
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.TableFunctionScan;
import org.apache.calcite.rel.core.TableModify;
import org.apache.calcite.rel.core.TableScan;
import org.apache.calcite.rel.core.Uncollect;
import org.apache.calcite.rel.core.Union;
import org.apache.calcite.rel.core.Values;
import org.apache.calcite.rel.core.Window;
import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.logical.LogicalProject;
import org.apache.calcite.rel.logical.LogicalSort;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rex.RexBuilder;
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.JoinConditionType;
import org.apache.calcite.sql.JoinType;
import org.apache.calcite.sql.SqlBasicCall;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlDelete;
import org.apache.calcite.sql.SqlDialect;
import org.apache.calcite.sql.SqlHint;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlInsert;
import org.apache.calcite.sql.SqlIntervalLiteral;
import org.apache.calcite.sql.SqlJoin;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlMatchRecognize;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlNodeList;
import org.apache.calcite.sql.SqlSelect;
import org.apache.calcite.sql.SqlTableRef;
import org.apache.calcite.sql.SqlUpdate;
import org.apache.calcite.sql.SqlUtil;
import org.apache.calcite.sql.fun.SqlInternalOperators;
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.util.SqlShuttle;
import org.apache.calcite.sql.validate.SqlValidatorUtil;
import org.apache.calcite.util.ImmutableBitSet;
import org.apache.calcite.util.Pair;
import org.apache.calcite.util.Permutation;
import org.apache.calcite.util.ReflectUtil;
import org.apache.calcite.util.ReflectiveVisitor;
import org.apache.calcite.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.apache.calcite.rex.RexLiteral.stringValue;
import static java.util.Objects.requireNonNull;
/**
* Utility to convert relational expressions to SQL abstract syntax tree.
*/
public class RelToSqlConverter extends SqlImplementor
implements ReflectiveVisitor {
private final ReflectUtil.MethodDispatcher<Result> dispatcher;
private final Deque<Frame> stack = new ArrayDeque<>();
/** Creates a RelToSqlConverter. */
@SuppressWarnings("argument.type.incompatible")
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);
}
@Override public Result visitInput(RelNode parent, int i, boolean anon,
boolean ignoreClauses, Set<Clause> expectedClauses) {
try {
final RelNode e = parent.getInput(i);
stack.push(new Frame(parent, i, e, anon, ignoreClauses, expectedClauses));
return dispatch(e);
} finally {
stack.pop();
}
}
@Override protected boolean isAnon() {
Frame peek = stack.peek();
return peek == null || peek.anon;
}
@Override protected Result result(SqlNode node, Collection<Clause> clauses,
@Nullable String neededAlias, @Nullable RelDataType neededType,
Map<String, RelDataType> aliases) {
final Frame frame = requireNonNull(stack.peek());
return super.result(node, clauses, neededAlias, neededType, aliases)
.withAnon(isAnon())
.withExpectedClauses(frame.ignoreClauses, frame.expectedClauses,
frame.parent);
}
/** Visits a RelNode; called by {@link #dispatch} via reflection. */
public Result visit(RelNode e) {
throw new AssertionError("Need to implement " + e.getClass().getName());
}
/**
* A SqlShuttle to replace references to a column of a table alias with the expression
* from the select item that is the source of that column.
* ANTI- and SEMI-joins generate an alias for right hand side relation which
* is used in the ON condition. But that alias is never created, so we have to inline references.
*/
private static class AliasReplacementShuttle extends SqlShuttle {
private final String tableAlias;
private final RelDataType tableType;
private final SqlNodeList replaceSource;
AliasReplacementShuttle(String tableAlias, RelDataType tableType,
SqlNodeList replaceSource) {
this.tableAlias = tableAlias;
this.tableType = tableType;
this.replaceSource = replaceSource;
}
@Override public SqlNode visit(SqlIdentifier id) {
if (tableAlias.equals(id.names.get(0))) {
int index = requireNonNull(
tableType.getField(id.names.get(1), false, false),
() -> "field " + id.names.get(1) + " is not found in " + tableType)
.getIndex();
SqlNode selectItem = requireNonNull(replaceSource, "replaceSource").get(index);
if (selectItem.getKind() == SqlKind.AS) {
selectItem = ((SqlCall) selectItem).operand(0);
}
return selectItem.clone(id.getParserPosition());
}
return id;
}
}
/** Visits a Join; called by {@link #dispatch} via reflection. */
public Result visit(Join e) {
switch (e.getJoinType()) {
case ANTI:
case SEMI:
return visitAntiOrSemiJoin(e);
default:
break;
}
final Result leftResult = visitInput(e, 0).resetAlias();
final Result rightResult = visitInput(e, 1).resetAlias();
final Context leftContext = leftResult.qualifiedContext();
final Context rightContext = rightResult.qualifiedContext();
final SqlNode sqlCondition;
final JoinConditionType condType;
JoinType joinType = joinType(e.getJoinType());
if (joinType == JoinType.INNER
&& e.getCondition().isAlwaysTrue()) {
if (isCommaJoin(e)) {
joinType = JoinType.COMMA;
} else {
joinType = JoinType.CROSS;
}
sqlCondition = null;
condType = JoinConditionType.NONE;
} else {
sqlCondition =
convertConditionToSqlNode(e.getCondition(), leftContext,
rightContext);
condType = JoinConditionType.ON;
}
SqlNode join =
new SqlJoin(POS,
leftResult.asFrom(),
SqlLiteral.createBoolean(false, POS),
joinType.symbol(POS),
rightResult.asFrom(),
condType.symbol(POS),
sqlCondition);
return result(join, leftResult, rightResult);
}
protected Result visitAntiOrSemiJoin(Join e) {
final Result leftResult = visitInput(e, 0).resetAlias();
final Result rightResult = visitInput(e, 1).resetAlias();
final Context leftContext = leftResult.qualifiedContext();
final Context rightContext = rightResult.qualifiedContext();
final SqlSelect sqlSelect = leftResult.asSelect();
SqlNode sqlCondition =
convertConditionToSqlNode(e.getCondition(), leftContext, rightContext);
if (leftResult.neededAlias != null) {
SqlShuttle visitor = new AliasReplacementShuttle(leftResult.neededAlias,
e.getLeft().getRowType(), sqlSelect.getSelectList());
sqlCondition = sqlCondition.accept(visitor);
}
SqlNode fromPart = rightResult.asFrom();
SqlSelect existsSqlSelect;
if (fromPart.getKind() == SqlKind.SELECT) {
existsSqlSelect = (SqlSelect) fromPart;
existsSqlSelect.setSelectList(
new SqlNodeList(ImmutableList.of(ONE), POS));
if (existsSqlSelect.getWhere() != null) {
sqlCondition = SqlStdOperatorTable.AND.createCall(POS,
existsSqlSelect.getWhere(),
sqlCondition);
}
existsSqlSelect.setWhere(sqlCondition);
} else {
existsSqlSelect =
new SqlSelect(POS, null,
new SqlNodeList(
ImmutableList.of(ONE), POS),
fromPart, sqlCondition, null,
null, null, null, null, null, null);
}
sqlCondition = SqlStdOperatorTable.EXISTS.createCall(POS, existsSqlSelect);
if (e.getJoinType() == JoinRelType.ANTI) {
sqlCondition = SqlStdOperatorTable.NOT.createCall(POS, sqlCondition);
}
if (sqlSelect.getWhere() != null) {
sqlCondition = SqlStdOperatorTable.AND.createCall(POS,
sqlSelect.getWhere(),
sqlCondition);
}
sqlSelect.setWhere(sqlCondition);
final SqlNode resultNode =
leftResult.neededAlias == null ? sqlSelect
: as(sqlSelect, leftResult.neededAlias);
return result(resultNode, leftResult, rightResult);
}
/** Returns whether this join should be unparsed as a {@link JoinType#COMMA}.
*
* <p>Comma-join is one possible syntax for {@code CROSS JOIN ... ON TRUE},
* supported on most but not all databases
* (see {@link SqlDialect#emulateJoinTypeForCrossJoin()}).
*
* <p>For example, the following queries are equivalent:
*
* <pre>{@code
* // Comma join
* SELECT *
* FROM Emp, Dept
*
* // Cross join
* SELECT *
* FROM Emp CROSS JOIN Dept
*
* // Inner join
* SELECT *
* FROM Emp INNER JOIN Dept ON TRUE
* }</pre>
*
* <p>Examples:
* <ul>
* <li>{@code FROM (x CROSS JOIN y ON TRUE) CROSS JOIN z ON TRUE}
* is a comma join, because all joins are comma joins;
* <li>{@code FROM (x CROSS JOIN y ON TRUE) CROSS JOIN z ON TRUE}
* would not be a comma join when run on Apache Spark, because Spark does
* not support comma join;
* <li>{@code FROM (x CROSS JOIN y ON TRUE) LEFT JOIN z ON TRUE}
* is not comma join because one of the joins is not INNER;
* <li>{@code FROM (x CROSS JOIN y ON c) CROSS JOIN z ON TRUE}
* is not a comma join because one of the joins is ON TRUE.
* </ul>
*/
private boolean isCommaJoin(Join join) {
if (!join.getCondition().isAlwaysTrue()) {
return false;
}
if (dialect.emulateJoinTypeForCrossJoin() != JoinType.COMMA) {
return false;
}
// Find the topmost enclosing Join. For example, if the tree is
// Join((Join(a, b), Join(c, Join(d, e)))
// we get the same topJoin for a, b, c, d and e. Join. Stack is never empty,
// and frame.r on the first iteration is always "join".
assert !stack.isEmpty();
assert stack.element().r == join;
Join j = null;
for (Frame frame : stack) {
j = (Join) frame.r;
if (!(frame.parent instanceof Join)) {
break;
}
}
final Join topJoin = requireNonNull(j, "top join");
// Flatten the join tree, using a breadth-first algorithm.
// After flattening, the list contains all of the joins that will make up
// the FROM clause.
final List<Join> flatJoins = new ArrayList<>();
flatJoins.add(topJoin);
for (int i = 0; i < flatJoins.size();) {
final Join j2 = flatJoins.get(i++);
if (j2.getLeft() instanceof Join) {
flatJoins.add((Join) j2.getLeft());
}
if (j2.getRight() instanceof Join) {
flatJoins.add((Join) j2.getRight());
}
}
// If all joins are cross-joins (INNER JOIN ON TRUE), we can use
// we can use comma syntax "FROM a, b, c, d, e".
for (Join j2 : flatJoins) {
if (j2.getJoinType() != JoinRelType.INNER
|| !j2.getCondition().isAlwaysTrue()) {
return false;
}
}
return true;
}
/** Visits a Correlate; called by {@link #dispatch} via reflection. */
public Result visit(Correlate e) {
final Result leftResult =
visitInput(e, 0)
.resetAlias(e.getCorrelVariable(), e.getRowType());
parseCorrelTable(e, leftResult);
final Result rightResult = visitInput(e, 1);
final SqlNode rightLateral =
SqlStdOperatorTable.LATERAL.createCall(POS, rightResult.node);
final SqlNode rightLateralAs =
SqlStdOperatorTable.AS.createCall(POS, rightLateral,
new SqlIdentifier(
requireNonNull(rightResult.neededAlias,
() -> "rightResult.neededAlias is null, node is " + rightResult.node), POS));
final SqlNode join =
new SqlJoin(POS,
leftResult.asFrom(),
SqlLiteral.createBoolean(false, POS),
JoinType.COMMA.symbol(POS),
rightLateralAs,
JoinConditionType.NONE.symbol(POS),
null);
return result(join, leftResult, rightResult);
}
/** Visits a Filter; called by {@link #dispatch} via reflection. */
public Result visit(Filter e) {
final RelNode input = e.getInput();
if (input instanceof Aggregate) {
final Aggregate aggregate = (Aggregate) input;
final boolean ignoreClauses = aggregate.getInput() instanceof Project;
final Result x = visitInput(e, 0, isAnon(), ignoreClauses,
ImmutableSet.of(Clause.HAVING));
parseCorrelTable(e, x);
final Builder builder = x.builder(e);
x.asSelect().setHaving(
SqlUtil.andExpressions(x.asSelect().getHaving(),
builder.context.toSql(null, e.getCondition())));
return builder.result();
} else {
final Result x = visitInput(e, 0, Clause.WHERE);
parseCorrelTable(e, x);
final Builder builder = x.builder(e);
builder.setWhere(builder.context.toSql(null, e.getCondition()));
return builder.result();
}
}
/** Visits a Project; called by {@link #dispatch} via reflection. */
public Result visit(Project e) {
// If the input is a Sort, wrap SELECT is not required.
final Result x;
if (e.getInput() instanceof Sort) {
x = visitInput(e, 0);
} else {
x = visitInput(e, 0, Clause.SELECT);
}
parseCorrelTable(e, x);
final Builder builder = x.builder(e);
if (!isStar(e.getProjects(), e.getInput().getRowType(), e.getRowType())) {
final List<SqlNode> selectList = new ArrayList<>();
for (RexNode ref : e.getProjects()) {
SqlNode sqlExpr = builder.context.toSql(null, ref);
if (SqlUtil.isNullLiteral(sqlExpr, false)) {
final RelDataTypeField field =
e.getRowType().getFieldList().get(selectList.size());
sqlExpr = castNullType(sqlExpr, field.getType());
}
addSelect(selectList, sqlExpr, e.getRowType());
}
builder.setSelect(new SqlNodeList(selectList, POS));
}
return builder.result();
}
/** Wraps a NULL literal in a CAST operator to a target type.
*
* @param nullLiteral NULL literal
* @param type Target type
*
* @return null literal wrapped in CAST call
*/
private SqlNode castNullType(SqlNode nullLiteral, RelDataType type) {
final SqlNode typeNode = dialect.getCastSpec(type);
if (typeNode == null) {
return nullLiteral;
}
return SqlStdOperatorTable.CAST.createCall(POS, nullLiteral, typeNode);
}
/** Visits a Window; called by {@link #dispatch} via reflection. */
public Result visit(Window e) {
final Result x = visitInput(e, 0);
final Builder builder = x.builder(e);
final RelNode input = e.getInput();
final int inputFieldCount = input.getRowType().getFieldCount();
final List<SqlNode> rexOvers = new ArrayList<>();
for (Window.Group group: e.groups) {
rexOvers.addAll(builder.context.toSql(group, e.constants, inputFieldCount));
}
final List<SqlNode> selectList = new ArrayList<>();
for (RelDataTypeField field: input.getRowType().getFieldList()) {
addSelect(selectList, builder.context.field(field.getIndex()), e.getRowType());
}
for (SqlNode rexOver: rexOvers) {
addSelect(selectList, rexOver, e.getRowType());
}
builder.setSelect(new SqlNodeList(selectList, POS));
return builder.result();
}
/** Visits an Aggregate; called by {@link #dispatch} via reflection. */
public Result visit(Aggregate e) {
final Builder builder =
visitAggregate(e, e.getGroupSet().toList(), Clause.GROUP_BY);
return builder.result();
}
private Builder visitAggregate(Aggregate e, List<Integer> groupKeyList,
Clause... clauses) {
// groupSet contains at least one column that is not in any groupSet.
// Set of clauses that we expect the builder need to add extra Clause.HAVING
// then can add Having filter condition in buildAggregate.
final Set<Clause> clauseSet = new TreeSet<>(Arrays.asList(clauses));
if (!e.getGroupSet().equals(ImmutableBitSet.union(e.getGroupSets()))) {
clauseSet.add(Clause.HAVING);
}
// "select a, b, sum(x) from ( ... ) group by a, b"
final boolean ignoreClauses = e.getInput() instanceof Project;
final Result x = visitInput(e, 0, isAnon(), ignoreClauses, clauseSet);
final Builder builder = x.builder(e);
final List<SqlNode> selectList = new ArrayList<>();
final List<SqlNode> groupByList =
generateGroupList(builder, selectList, e, groupKeyList);
return buildAggregate(e, builder, selectList, groupByList);
}
/**
* Builds the group list for an Aggregate node.
*
* @param e The Aggregate node
* @param builder The SQL builder
* @param groupByList output group list
* @param selectList output select list
*/
protected void buildAggGroupList(Aggregate e, Builder builder,
List<SqlNode> groupByList, List<SqlNode> selectList) {
for (int group : e.getGroupSet()) {
final SqlNode field = builder.context.field(group);
addSelect(selectList, field, e.getRowType());
groupByList.add(field);
}
}
/**
* Builds an aggregate query.
*
* @param e The Aggregate node
* @param builder The SQL builder
* @param selectList The precomputed group list
* @param groupByList The precomputed select list
* @return The aggregate query result
*/
protected Builder buildAggregate(Aggregate e, Builder builder,
List<SqlNode> selectList, List<SqlNode> groupByList) {
for (AggregateCall aggCall : e.getAggCallList()) {
SqlNode aggCallSqlNode = builder.context.toSql(aggCall);
if (aggCall.getAggregation() instanceof SqlSingleValueAggFunction) {
aggCallSqlNode = dialect.rewriteSingleValueExpr(aggCallSqlNode);
}
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));
}
if (builder.clauses.contains(Clause.HAVING) && !e.getGroupSet()
.equals(ImmutableBitSet.union(e.getGroupSets()))) {
// groupSet contains at least one column that is not in any groupSets.
// To make such columns must appear in the output (their value will
// always be NULL), we generate an extra grouping set, then filter
// it out using a "HAVING GROUPING(groupSets) <> 0".
// We want to generate the
final SqlNodeList groupingList = new SqlNodeList(POS);
e.getGroupSet().forEach(g ->
groupingList.add(builder.context.field(g)));
builder.setHaving(
SqlStdOperatorTable.NOT_EQUALS.createCall(POS,
SqlStdOperatorTable.GROUPING.createCall(groupingList), ZERO));
}
return builder;
}
/** Generates the GROUP BY items, for example {@code GROUP BY x, y},
* {@code GROUP BY CUBE (x, y)} or {@code GROUP BY ROLLUP (x, y)}.
*
* <p>Also populates the SELECT clause. If the GROUP BY list is simple, the
* SELECT will be identical; if the GROUP BY list contains GROUPING SETS,
* CUBE or ROLLUP, the SELECT clause will contain the distinct leaf
* expressions. */
private List<SqlNode> generateGroupList(Builder builder,
List<SqlNode> selectList, Aggregate aggregate, List<Integer> groupList) {
final List<Integer> sortedGroupList =
Ordering.natural().sortedCopy(groupList);
assert aggregate.getGroupSet().asList().equals(sortedGroupList)
: "groupList " + groupList + " must be equal to groupSet "
+ aggregate.getGroupSet() + ", just possibly a different order";
final List<SqlNode> groupKeys = new ArrayList<>();
for (int key : groupList) {
final SqlNode field = builder.context.field(key);
groupKeys.add(field);
}
for (int key : sortedGroupList) {
final SqlNode field = builder.context.field(key);
addSelect(selectList, field, aggregate.getRowType());
}
switch (aggregate.getGroupType()) {
case SIMPLE:
return ImmutableList.copyOf(groupKeys);
case CUBE:
if (aggregate.getGroupSet().cardinality() > 1) {
return ImmutableList.of(
SqlStdOperatorTable.CUBE.createCall(SqlParserPos.ZERO, groupKeys));
}
// a singleton CUBE and ROLLUP are the same but we prefer ROLLUP;
// fall through
case ROLLUP:
return ImmutableList.of(
SqlStdOperatorTable.ROLLUP.createCall(SqlParserPos.ZERO, groupKeys));
default:
case OTHER:
// Make sure that the group sets contains all bits.
final List<ImmutableBitSet> groupSets;
if (aggregate.getGroupSet()
.equals(ImmutableBitSet.union(aggregate.groupSets))) {
groupSets = aggregate.getGroupSets();
} else {
groupSets = new ArrayList<>(aggregate.getGroupSets().size() + 1);
groupSets.add(aggregate.getGroupSet());
groupSets.addAll(aggregate.getGroupSets());
}
return ImmutableList.of(
SqlStdOperatorTable.GROUPING_SETS.createCall(SqlParserPos.ZERO,
groupSets.stream()
.map(groupSet ->
groupItem(groupKeys, groupSet, aggregate.getGroupSet()))
.collect(Collectors.toList())));
}
}
private static SqlNode groupItem(List<SqlNode> groupKeys,
ImmutableBitSet groupSet, ImmutableBitSet wholeGroupSet) {
final List<SqlNode> nodes = groupSet.asList().stream()
.map(key -> groupKeys.get(wholeGroupSet.indexOf(key)))
.collect(Collectors.toList());
switch (nodes.size()) {
case 1:
return nodes.get(0);
default:
return SqlStdOperatorTable.ROW.createCall(SqlParserPos.ZERO, nodes);
}
}
/** Visits a TableScan; called by {@link #dispatch} via reflection. */
public Result visit(TableScan e) {
final SqlIdentifier identifier = getSqlTargetTable(e);
final SqlNode node;
final ImmutableList<RelHint> hints = e.getHints();
if (!hints.isEmpty()) {
SqlParserPos pos = identifier.getParserPosition();
node = new SqlTableRef(pos, identifier,
SqlNodeList.of(pos, hints.stream().map(h -> RelToSqlConverter.toSqlHint(h, pos))
.collect(Collectors.toList())));
} else {
node = identifier;
}
return result(node, ImmutableList.of(Clause.FROM), e, null);
}
private static SqlHint toSqlHint(RelHint hint, SqlParserPos pos) {
if (hint.kvOptions != null) {
return new SqlHint(pos, new SqlIdentifier(hint.hintName, pos),
SqlNodeList.of(pos, hint.kvOptions.entrySet().stream()
.flatMap(
e -> Stream.of(new SqlIdentifier(e.getKey(), pos),
SqlLiteral.createCharString(e.getValue(), pos)))
.collect(Collectors.toList())),
SqlHint.HintOptionFormat.KV_LIST);
} else if (hint.listOptions != null) {
return new SqlHint(pos, new SqlIdentifier(hint.hintName, pos),
SqlNodeList.of(pos, hint.listOptions.stream()
.map(e -> SqlLiteral.createCharString(e, pos))
.collect(Collectors.toList())),
SqlHint.HintOptionFormat.LITERAL_LIST);
}
return new SqlHint(pos, new SqlIdentifier(hint.hintName, pos),
SqlNodeList.EMPTY, SqlHint.HintOptionFormat.EMPTY);
}
/** Visits a Union; called by {@link #dispatch} via reflection. */
public Result visit(Union e) {
return setOpToSql(e.all
? SqlStdOperatorTable.UNION_ALL
: SqlStdOperatorTable.UNION, e);
}
/** Visits an Intersect; called by {@link #dispatch} via reflection. */
public Result visit(Intersect e) {
return setOpToSql(e.all
? SqlStdOperatorTable.INTERSECT_ALL
: SqlStdOperatorTable.INTERSECT, e);
}
/** Visits a Minus; called by {@link #dispatch} via reflection. */
public Result visit(Minus e) {
return setOpToSql(e.all
? SqlStdOperatorTable.EXCEPT_ALL
: SqlStdOperatorTable.EXCEPT, e);
}
/** Visits a Calc; called by {@link #dispatch} via reflection. */
public Result visit(Calc e) {
final RexProgram program = e.getProgram();
final ImmutableSet<Clause> expectedClauses =
program.getCondition() != null
? ImmutableSet.of(Clause.WHERE)
: ImmutableSet.of();
final Result x = visitInput(e, 0, expectedClauses);
parseCorrelTable(e, x);
final Builder builder = x.builder(e);
if (!isStar(program)) {
final List<SqlNode> selectList = new ArrayList<>(program.getProjectList().size());
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();
}
/** Visits a Values; called by {@link #dispatch} via reflection. */
public Result visit(Values e) {
final List<Clause> clauses = ImmutableList.of(Clause.SELECT);
final Map<String, RelDataType> pairs = ImmutableMap.of();
final Context context = aliasContext(pairs, false);
SqlNode query;
final boolean rename = stack.size() <= 1
|| !(Iterables.get(stack, 1).r instanceof TableModify);
final List<String> fieldNames = e.getRowType().getFieldNames();
if (!dialect.supportsAliasedValues() && rename) {
// Some dialects (such as Oracle and BigQuery) don't support
// "AS t (c1, c2)". So instead of
// (VALUES (v0, v1), (v2, v3)) AS t (c0, c1)
// we generate
// SELECT v0 AS c0, v1 AS c1 FROM DUAL
// UNION ALL
// SELECT v2 AS c0, v3 AS c1 FROM DUAL
// for Oracle and
// SELECT v0 AS c0, v1 AS c1
// UNION ALL
// SELECT v2 AS c0, v3 AS c1
// for dialects that support SELECT-without-FROM.
List<SqlSelect> list = new ArrayList<>();
for (List<RexLiteral> tuple : e.getTuples()) {
final List<SqlNode> values2 = new ArrayList<>();
final SqlNodeList exprList = exprList(context, tuple);
for (Pair<SqlNode, String> value : Pair.zip(exprList, fieldNames)) {
values2.add(as(value.left, value.right));
}
list.add(
new SqlSelect(POS, null,
new SqlNodeList(values2, POS),
getDual(), null, null,
null, null, null, null, null, null));
}
if (list.isEmpty()) {
// In this case we need to construct the following query:
// SELECT NULL as C0, NULL as C1, NULL as C2 ... FROM DUAL WHERE FALSE
// This would return an empty result set with the same number of columns as the field names.
final List<SqlNode> nullColumnNames = new ArrayList<>(fieldNames.size());
for (String fieldName : fieldNames) {
SqlCall nullColumnName = as(SqlLiteral.createNull(POS), fieldName);
nullColumnNames.add(nullColumnName);
}
final SqlIdentifier dual = getDual();
if (dual == null) {
query = new SqlSelect(POS, null,
new SqlNodeList(nullColumnNames, POS), null, null, null, null,
null, null, null, null, null);
// Wrap "SELECT 1 AS x"
// as "SELECT * FROM (SELECT 1 AS x) AS t WHERE false"
query = new SqlSelect(POS, null, SqlNodeList.SINGLETON_STAR,
as(query, "t"), createAlwaysFalseCondition(), null, null,
null, null, null, null, null);
} else {
query = new SqlSelect(POS, null,
new SqlNodeList(nullColumnNames, POS),
dual, createAlwaysFalseCondition(), null,
null, null, null, null, null, null);
}
} else if (list.size() == 1) {
query = list.get(0);
} else {
query = list.stream()
.map(select -> (SqlNode) select)
.reduce((l, r) -> SqlStdOperatorTable.UNION_ALL.createCall(POS, l, r))
.get();
}
} else {
// Generate ANSI syntax
// (VALUES (v0, v1), (v2, v3))
// or, if rename is required
// (VALUES (v0, v1), (v2, v3)) AS t (c0, c1)
final SqlNodeList selects = new SqlNodeList(POS);
final boolean isEmpty = Values.isEmpty(e);
if (isEmpty) {
// In case of empty values, we need to build:
// SELECT *
// FROM (VALUES (NULL, NULL ...)) AS T (C1, C2 ...)
// WHERE 1 = 0
selects.add(
SqlInternalOperators.ANONYMOUS_ROW.createCall(POS,
Collections.nCopies(fieldNames.size(),
SqlLiteral.createNull(POS))));
} else {
for (List<RexLiteral> tuple : e.getTuples()) {
selects.add(
SqlInternalOperators.ANONYMOUS_ROW.createCall(
exprList(context, tuple)));
}
}
query = SqlStdOperatorTable.VALUES.createCall(selects);
if (rename) {
query = as(query, "t", fieldNames.toArray(new String[0]));
}
if (isEmpty) {
if (!rename) {
query = as(query, "t");
}
query =
new SqlSelect(POS, null, SqlNodeList.SINGLETON_STAR, query,
createAlwaysFalseCondition(), null, null, null,
null, null, null, null);
}
}
return result(query, clauses, e, null);
}
private @Nullable SqlIdentifier getDual() {
final List<String> names = dialect.getSingleRowTableName();
if (names == null) {
return null;
}
return new SqlIdentifier(names, POS);
}
private static SqlNode createAlwaysFalseCondition() {
// Building the select query in the form:
// select * from VALUES(NULL,NULL ...) where 1=0
// Use condition 1=0 since "where false" does not seem to be supported
// on some DB vendors.
return SqlStdOperatorTable.EQUALS.createCall(POS,
ImmutableList.of(ONE, ZERO));
}
/** Visits a Sort; called by {@link #dispatch} via reflection. */
public Result visit(Sort e) {
if (e.getInput() instanceof Aggregate) {
final Aggregate aggregate = (Aggregate) e.getInput();
if (hasTrickyRollup(e, aggregate)) {
// MySQL 5 does not support standard "GROUP BY ROLLUP(x, y)", only
// the non-standard "GROUP BY x, y WITH ROLLUP".
// It does not allow "WITH ROLLUP" in combination with "ORDER BY",
// but "GROUP BY x, y WITH ROLLUP" implicitly sorts by x, y,
// so skip the ORDER BY.
final Set<Integer> groupList = new LinkedHashSet<>();
for (RelFieldCollation fc : e.collation.getFieldCollations()) {
groupList.add(aggregate.getGroupSet().nth(fc.getFieldIndex()));
}
groupList.addAll(Aggregate.Group.getRollup(aggregate.getGroupSets()));
final Builder builder =
visitAggregate(aggregate, ImmutableList.copyOf(groupList),
Clause.GROUP_BY, Clause.OFFSET, Clause.FETCH);
offsetFetch(e, builder);
return builder.result();
}
}
if (e.getInput() instanceof Project) {
// Deal with the case Sort(Project(Aggregate ...))
// by converting it to Project(Sort(Aggregate ...)).
final Project project = (Project) e.getInput();
final Permutation permutation = project.getPermutation();
if (permutation != null
&& project.getInput() instanceof Aggregate) {
final Aggregate aggregate = (Aggregate) project.getInput();
if (hasTrickyRollup(e, aggregate)) {
final RelCollation collation =
RelCollations.permute(e.collation, permutation);
final Sort sort2 =
LogicalSort.create(aggregate, collation, e.offset, e.fetch);
final Project project2 =
LogicalProject.create(
sort2,
ImmutableList.of(),
project.getProjects(),
project.getRowType(),
project.getVariablesSet());
return visit(project2);
}
}
}
final Result x = visitInput(e, 0, Clause.ORDER_BY, Clause.OFFSET,
Clause.FETCH);
final Builder builder = x.builder(e);
if (stack.size() != 1
&& builder.select.getSelectList().equals(SqlNodeList.SINGLETON_STAR)) {
// Generates explicit column names instead of start(*) for
// non-root order by to avoid ambiguity.
final List<SqlNode> selectList = Expressions.list();
for (RelDataTypeField field : e.getRowType().getFieldList()) {
addSelect(selectList, builder.context.field(field.getIndex()), e.getRowType());
}
builder.select.setSelectList(new SqlNodeList(selectList, POS));
}
List<SqlNode> orderByList = Expressions.list();
for (RelFieldCollation field : e.getCollation().getFieldCollations()) {
builder.addOrderItem(orderByList, field);
}
if (!orderByList.isEmpty()) {
builder.setOrderBy(new SqlNodeList(orderByList, POS));
}
offsetFetch(e, builder);
return builder.result();
}
/** Adds OFFSET and FETCH to a builder, if applicable.
* The builder must have been created with OFFSET and FETCH clauses. */
void offsetFetch(Sort e, Builder builder) {
if (e.fetch != null) {
builder.setFetch(builder.context.toSql(null, e.fetch));
}
if (e.offset != null) {
builder.setOffset(builder.context.toSql(null, e.offset));
}
}
public boolean hasTrickyRollup(Sort e, Aggregate aggregate) {
return !dialect.supportsAggregateFunction(SqlKind.ROLLUP)
&& dialect.supportsGroupByWithRollup()
&& (aggregate.getGroupType() == Aggregate.Group.ROLLUP
|| aggregate.getGroupType() == Aggregate.Group.CUBE
&& aggregate.getGroupSet().cardinality() == 1)
&& e.collation.getFieldCollations().stream().allMatch(fc ->
fc.getFieldIndex() < aggregate.getGroupSet().cardinality());
}
private static SqlIdentifier getSqlTargetTable(RelNode e) {
// Use the foreign catalog, schema and table names, if they exist,
// rather than the qualified name of the shadow table in Calcite.
final RelOptTable table = requireNonNull(e.getTable());
return table.maybeUnwrap(JdbcTable.class)
.map(JdbcTable::tableName)
.orElseGet(() ->
new SqlIdentifier(table.getQualifiedName(), SqlParserPos.ZERO));
}
/** Visits a TableModify; called by {@link #dispatch} via reflection. */
public Result visit(TableModify modify) {
final Map<String, RelDataType> pairs = ImmutableMap.of();
final Context context = aliasContext(pairs, false);
// Target Table Name
final SqlIdentifier sqlTargetTable = getSqlTargetTable(modify);
switch (modify.getOperation()) {
case INSERT: {
// Convert the input to a SELECT query or keep as VALUES. Not all
// dialects support naked VALUES, but all support VALUES inside INSERT.
final SqlNode sqlSource =
visitInput(modify, 0).asQueryOrValues();
final SqlInsert sqlInsert =
new SqlInsert(POS, SqlNodeList.EMPTY, sqlTargetTable, sqlSource,
identifierList(modify.getTable().getRowType().getFieldNames()));
return result(sqlInsert, ImmutableList.of(), modify, null);
}
case UPDATE: {
final Result input = visitInput(modify, 0);
final SqlUpdate sqlUpdate =
new SqlUpdate(POS, sqlTargetTable,
identifierList(
requireNonNull(modify.getUpdateColumnList(),
() -> "modify.getUpdateColumnList() is null for " + modify)),
exprList(context,
requireNonNull(modify.getSourceExpressionList(),
() -> "modify.getSourceExpressionList() is null for " + modify)),
((SqlSelect) input.node).getWhere(), input.asSelect(),
null);
return result(sqlUpdate, input.clauses, modify, null);
}
case DELETE: {
final Result input = visitInput(modify, 0);
final SqlDelete sqlDelete =
new SqlDelete(POS, sqlTargetTable,
input.asSelect().getWhere(), input.asSelect(), null);
return result(sqlDelete, input.clauses, modify, null);
}
case MERGE:
default:
throw new AssertionError("not implemented: " + modify);
}
}
/** Converts a list of {@link RexNode} expressions to {@link SqlNode}
* expressions. */
private static SqlNodeList exprList(final Context context,
List<? extends RexNode> exprs) {
return new SqlNodeList(
Util.transform(exprs, e -> context.toSql(null, e)), POS);
}
/** Converts a list of names expressions to a list of single-part
* {@link SqlIdentifier}s. */
private static SqlNodeList identifierList(List<String> names) {
return new SqlNodeList(
Util.transform(names, name -> new SqlIdentifier(name, POS)), POS);
}
/** Visits a Match; called by {@link #dispatch} via reflection. */
public Result visit(Match e) {
final RelNode input = e.getInput();
final Result x = visitInput(e, 0);
final Context context = matchRecognizeContext(x.qualifiedContext());
SqlNode tableRef = x.asQueryOrValues();
final RexBuilder rexBuilder = input.getCluster().getRexBuilder();
final List<SqlNode> partitionSqlList = new ArrayList<>();
for (int key : e.getPartitionKeys()) {
final RexInputRef ref = rexBuilder.makeInputRef(input, key);
SqlNode sqlNode = context.toSql(null, ref);
partitionSqlList.add(sqlNode);
}
final SqlNodeList partitionList = new SqlNodeList(partitionSqlList, POS);
final List<SqlNode> orderBySqlList = new ArrayList<>();
if (e.getOrderKeys() != null) {
for (RelFieldCollation fc : e.getOrderKeys().getFieldCollations()) {
if (fc.nullDirection != RelFieldCollation.NullDirection.UNSPECIFIED) {
boolean first = fc.nullDirection == RelFieldCollation.NullDirection.FIRST;
SqlNode nullDirectionNode =
dialect.emulateNullDirection(context.field(fc.getFieldIndex()),
first, fc.direction.isDescending());
if (nullDirectionNode != null) {
orderBySqlList.add(nullDirectionNode);
fc = new RelFieldCollation(fc.getFieldIndex(), fc.getDirection(),
RelFieldCollation.NullDirection.UNSPECIFIED);
}
}
orderBySqlList.add(context.toSql(fc));
}
}
final SqlNodeList orderByList = new SqlNodeList(orderBySqlList, SqlParserPos.ZERO);
final SqlLiteral rowsPerMatch = e.isAllRows()
? SqlMatchRecognize.RowsPerMatchOption.ALL_ROWS.symbol(POS)
: SqlMatchRecognize.RowsPerMatchOption.ONE_ROW.symbol(POS);
final SqlNode after;
if (e.getAfter() instanceof RexLiteral) {
SqlMatchRecognize.AfterOption value = (SqlMatchRecognize.AfterOption)
((RexLiteral) e.getAfter()).getValue2();
after = SqlLiteral.createSymbol(value, POS);
} else {
RexCall call = (RexCall) e.getAfter();
String operand = requireNonNull(stringValue(call.getOperands().get(0)),
() -> "non-null string value expected for 0th operand of AFTER call " + call);
after = call.getOperator().createCall(POS, new SqlIdentifier(operand, POS));
}
RexNode rexPattern = e.getPattern();
final SqlNode pattern = context.toSql(null, rexPattern);
final SqlLiteral strictStart = SqlLiteral.createBoolean(e.isStrictStart(), POS);
final SqlLiteral strictEnd = SqlLiteral.createBoolean(e.isStrictEnd(), POS);
RexLiteral rexInterval = (RexLiteral) e.getInterval();
SqlIntervalLiteral interval = null;
if (rexInterval != null) {
interval = (SqlIntervalLiteral) context.toSql(null, rexInterval);
}
final SqlNodeList subsetList = new SqlNodeList(POS);
for (Map.Entry<String, SortedSet<String>> entry : e.getSubsets().entrySet()) {
SqlNode left = new SqlIdentifier(entry.getKey(), POS);
List<SqlNode> rhl = new ArrayList<>();
for (String right : entry.getValue()) {
rhl.add(new SqlIdentifier(right, POS));
}
subsetList.add(
SqlStdOperatorTable.EQUALS.createCall(POS, left,
new SqlNodeList(rhl, POS)));
}
final SqlNodeList measureList = new SqlNodeList(POS);
for (Map.Entry<String, RexNode> entry : e.getMeasures().entrySet()) {
final String alias = entry.getKey();
final SqlNode sqlNode = context.toSql(null, entry.getValue());
measureList.add(as(sqlNode, alias));
}
final SqlNodeList patternDefList = new SqlNodeList(POS);
for (Map.Entry<String, RexNode> entry : e.getPatternDefinitions().entrySet()) {
final String alias = entry.getKey();
final SqlNode sqlNode = context.toSql(null, entry.getValue());
patternDefList.add(as(sqlNode, alias));
}
final SqlNode matchRecognize = new SqlMatchRecognize(POS, tableRef,
pattern, strictStart, strictEnd, patternDefList, measureList, after,
subsetList, rowsPerMatch, partitionList, orderByList, interval);
return result(matchRecognize, Expressions.list(Clause.FROM), e, null);
}
private static SqlCall as(SqlNode e, String alias) {
return SqlStdOperatorTable.AS.createCall(POS, e,
new SqlIdentifier(alias, POS));
}
public Result visit(Uncollect e) {
final Result x = visitInput(e, 0);
final SqlNode unnestNode = SqlStdOperatorTable.UNNEST.createCall(POS, x.asStatement());
final List<SqlNode> operands = createAsFullOperands(e.getRowType(), unnestNode,
requireNonNull(x.neededAlias, () -> "x.neededAlias is null, node is " + x.node));
final SqlNode asNode = SqlStdOperatorTable.AS.createCall(POS, operands);
return result(asNode, ImmutableList.of(Clause.FROM), e, null);
}
public Result visit(TableFunctionScan e) {
final List<SqlNode> inputSqlNodes = new ArrayList<>();
final int inputSize = e.getInputs().size();
for (int i = 0; i < inputSize; i++) {
final Result x = visitInput(e, i);
inputSqlNodes.add(x.asStatement());
}
final Context context = tableFunctionScanContext(inputSqlNodes);
SqlNode callNode = context.toSql(null, e.getCall());
// Convert to table function call, "TABLE($function_name(xxx))"
SqlNode tableCall =
new SqlBasicCall(SqlStdOperatorTable.COLLECTION_TABLE,
ImmutableList.of(callNode), SqlParserPos.ZERO);
SqlNode select =
new SqlSelect(SqlParserPos.ZERO, null, SqlNodeList.SINGLETON_STAR,
tableCall, null, null, null, null, null, null, null,
SqlNodeList.EMPTY);
return result(select, ImmutableList.of(Clause.SELECT), e, null);
}
/**
* Creates operands for a full AS operator. Format SqlNode AS alias(col_1, col_2,... ,col_n).
*
* @param rowType Row type of the SqlNode
* @param leftOperand SqlNode
* @param alias alias
*/
public List<SqlNode> createAsFullOperands(RelDataType rowType, SqlNode leftOperand,
String alias) {
final List<SqlNode> result = new ArrayList<>();
result.add(leftOperand);
result.add(new SqlIdentifier(alias, POS));
Ord.forEach(rowType.getFieldNames(), (fieldName, i) -> {
if (SqlUtil.isGeneratedAlias(fieldName)) {
fieldName = "col_" + i;
}
result.add(new SqlIdentifier(fieldName, POS));
});
return result;
}
@Override public void addSelect(List<SqlNode> 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 = as(node, name);
}
selectList.add(node);
}
private void parseCorrelTable(RelNode relNode, Result x) {
for (CorrelationId id : relNode.getVariablesSet()) {
correlTableMap.put(id, x.qualifiedContext());
}
}
/** Stack frame. */
private static class Frame {
private final RelNode parent;
@SuppressWarnings("unused")
private final int ordinalInParent;
private final RelNode r;
private final boolean anon;
private final boolean ignoreClauses;
private final ImmutableSet<? extends Clause> expectedClauses;
Frame(RelNode parent, int ordinalInParent, RelNode r, boolean anon,
boolean ignoreClauses, Iterable<? extends Clause> expectedClauses) {
this.parent = requireNonNull(parent, "parent");
this.ordinalInParent = ordinalInParent;
this.r = requireNonNull(r, "r");
this.anon = anon;
this.ignoreClauses = ignoreClauses;
this.expectedClauses = ImmutableSet.copyOf(expectedClauses);
}
}
}