All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hazelcast.org.apache.calcite.rel.rel2sql.RelToSqlConverter Maven / Gradle / Ivy

There is a newer version: 5.5.0
Show newest version
/*
 * 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 com.hazelcast.org.apache.calcite.rel.rel2sql;

import com.hazelcast.org.apache.calcite.adapter.jdbc.JdbcTable;
import com.hazelcast.org.apache.calcite.linq4j.Ord;
import com.hazelcast.org.apache.calcite.linq4j.tree.Expressions;
import com.hazelcast.org.apache.calcite.plan.RelOptTable;
import com.hazelcast.org.apache.calcite.rel.RelCollation;
import com.hazelcast.org.apache.calcite.rel.RelCollations;
import com.hazelcast.org.apache.calcite.rel.RelFieldCollation;
import com.hazelcast.org.apache.calcite.rel.RelNode;
import com.hazelcast.org.apache.calcite.rel.core.Aggregate;
import com.hazelcast.org.apache.calcite.rel.core.AggregateCall;
import com.hazelcast.org.apache.calcite.rel.core.Calc;
import com.hazelcast.org.apache.calcite.rel.core.Correlate;
import com.hazelcast.org.apache.calcite.rel.core.CorrelationId;
import com.hazelcast.org.apache.calcite.rel.core.Filter;
import com.hazelcast.org.apache.calcite.rel.core.Intersect;
import com.hazelcast.org.apache.calcite.rel.core.Join;
import com.hazelcast.org.apache.calcite.rel.core.JoinRelType;
import com.hazelcast.org.apache.calcite.rel.core.Match;
import com.hazelcast.org.apache.calcite.rel.core.Minus;
import com.hazelcast.org.apache.calcite.rel.core.Project;
import com.hazelcast.org.apache.calcite.rel.core.Sort;
import com.hazelcast.org.apache.calcite.rel.core.TableFunctionScan;
import com.hazelcast.org.apache.calcite.rel.core.TableModify;
import com.hazelcast.org.apache.calcite.rel.core.TableScan;
import com.hazelcast.org.apache.calcite.rel.core.Uncollect;
import com.hazelcast.org.apache.calcite.rel.core.Union;
import com.hazelcast.org.apache.calcite.rel.core.Values;
import com.hazelcast.org.apache.calcite.rel.core.Window;
import com.hazelcast.org.apache.calcite.rel.hint.RelHint;
import com.hazelcast.org.apache.calcite.rel.logical.LogicalProject;
import com.hazelcast.org.apache.calcite.rel.logical.LogicalSort;
import com.hazelcast.org.apache.calcite.rel.type.RelDataType;
import com.hazelcast.org.apache.calcite.rel.type.RelDataTypeField;
import com.hazelcast.org.apache.calcite.rex.RexBuilder;
import com.hazelcast.org.apache.calcite.rex.RexCall;
import com.hazelcast.org.apache.calcite.rex.RexInputRef;
import com.hazelcast.org.apache.calcite.rex.RexLiteral;
import com.hazelcast.org.apache.calcite.rex.RexLocalRef;
import com.hazelcast.org.apache.calcite.rex.RexNode;
import com.hazelcast.org.apache.calcite.rex.RexProgram;
import com.hazelcast.org.apache.calcite.sql.JoinConditionType;
import com.hazelcast.org.apache.calcite.sql.JoinType;
import com.hazelcast.org.apache.calcite.sql.SqlBasicCall;
import com.hazelcast.org.apache.calcite.sql.SqlCall;
import com.hazelcast.org.apache.calcite.sql.SqlDelete;
import com.hazelcast.org.apache.calcite.sql.SqlDialect;
import com.hazelcast.org.apache.calcite.sql.SqlHint;
import com.hazelcast.org.apache.calcite.sql.SqlIdentifier;
import com.hazelcast.org.apache.calcite.sql.SqlInsert;
import com.hazelcast.org.apache.calcite.sql.SqlIntervalLiteral;
import com.hazelcast.org.apache.calcite.sql.SqlJoin;
import com.hazelcast.org.apache.calcite.sql.SqlKind;
import com.hazelcast.org.apache.calcite.sql.SqlLiteral;
import com.hazelcast.org.apache.calcite.sql.SqlMatchRecognize;
import com.hazelcast.org.apache.calcite.sql.SqlNode;
import com.hazelcast.org.apache.calcite.sql.SqlNodeList;
import com.hazelcast.org.apache.calcite.sql.SqlSelect;
import com.hazelcast.org.apache.calcite.sql.SqlTableRef;
import com.hazelcast.org.apache.calcite.sql.SqlUpdate;
import com.hazelcast.org.apache.calcite.sql.SqlUtil;
import com.hazelcast.org.apache.calcite.sql.fun.SqlInternalOperators;
import com.hazelcast.org.apache.calcite.sql.fun.SqlSingleValueAggFunction;
import com.hazelcast.org.apache.calcite.sql.fun.SqlStdOperatorTable;
import com.hazelcast.org.apache.calcite.sql.parser.SqlParserPos;
import com.hazelcast.org.apache.calcite.sql.util.SqlShuttle;
import com.hazelcast.org.apache.calcite.sql.validate.SqlValidatorUtil;
import com.hazelcast.org.apache.calcite.util.ImmutableBitSet;
import com.hazelcast.org.apache.calcite.util.Pair;
import com.hazelcast.org.apache.calcite.util.Permutation;
import com.hazelcast.org.apache.calcite.util.ReflectUtil;
import com.hazelcast.org.apache.calcite.util.ReflectiveVisitor;
import com.hazelcast.org.apache.calcite.util.Util;

import com.hazelcast.com.google.common.collect.ImmutableList;
import com.hazelcast.com.google.common.collect.ImmutableMap;
import com.hazelcast.com.google.common.collect.ImmutableSet;
import com.hazelcast.com.google.common.collect.Iterables;
import com.hazelcast.com.google.common.collect.Ordering;

import com.hazelcast.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 com.hazelcast.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 dispatcher;

  private final Deque 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 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 clauses,
      @Nullable String neededAlias, @Nullable RelDataType neededType,
      Map 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}.
   *
   * 

Comma-join is one possible syntax for {@code CROSS JOIN ... ON TRUE}, * supported on most but not all databases * (see {@link SqlDialect#emulateJoinTypeForCrossJoin()}). * *

For example, the following queries are equivalent: * *

{@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
   * }
* *

Examples: *

    *
  • {@code FROM (x CROSS JOIN y ON TRUE) CROSS JOIN z ON TRUE} * is a comma join, because all joins are comma joins; *
  • {@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; *
  • {@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; *
  • {@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. *
*/ 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 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 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 rexOvers = new ArrayList<>(); for (Window.Group group: e.groups) { rexOvers.addAll(builder.context.toSql(group, e.constants, inputFieldCount)); } final List 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 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 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 selectList = new ArrayList<>(); final List 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 groupByList, List 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 selectList, List 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)}. * *

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 generateGroupList(Builder builder, List selectList, Aggregate aggregate, List groupList) { final List 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 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 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 groupKeys, ImmutableBitSet groupSet, ImmutableBitSet wholeGroupSet) { final List 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 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 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 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 clauses = ImmutableList.of(Clause.SELECT); final Map 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 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 list = new ArrayList<>(); for (List tuple : e.getTuples()) { final List values2 = new ArrayList<>(); final SqlNodeList exprList = exprList(context, tuple); for (Pair 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 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 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 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 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()); 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 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 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 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 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 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 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 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> entry : e.getSubsets().entrySet()) { SqlNode left = new SqlIdentifier(entry.getKey(), POS); List 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 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 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 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 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 createAsFullOperands(RelDataType rowType, SqlNode leftOperand, String alias) { final List 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 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 expectedClauses; Frame(RelNode parent, int ordinalInParent, RelNode r, boolean anon, boolean ignoreClauses, Iterable 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); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy