SqlUnpivot.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.calcite.sql;

import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.util.SqlBasicVisitor;
import org.apache.calcite.sql.util.SqlVisitor;
import org.apache.calcite.util.ImmutableNullableList;
import org.apache.calcite.util.Util;

import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

import static java.util.Objects.requireNonNull;

/**
 * Parse tree node that represents UNPIVOT applied to a table reference
 * (or sub-query).
 *
 * <p>Syntax:
 * <blockquote><pre>{@code
 * SELECT *
 * FROM query
 * UNPIVOT [ { INCLUDE | EXCLUDE } NULLS ] (
 *   columns FOR columns IN ( columns [ AS values ], ...))
 *
 * where:
 *
 * columns: column
 *        | '(' column, ... ')'
 * values:  value
 *        | '(' value, ... ')'
 * }</pre></blockquote>
 */
public class SqlUnpivot extends SqlCall {

  public SqlNode query;
  public final boolean includeNulls;
  public final SqlNodeList measureList;
  public final SqlNodeList axisList;
  public final SqlNodeList inList;

  static final Operator OPERATOR = new Operator(SqlKind.UNPIVOT);

  //~ Constructors -----------------------------------------------------------

  public SqlUnpivot(SqlParserPos pos, SqlNode query, boolean includeNulls,
      SqlNodeList measureList, SqlNodeList axisList, SqlNodeList inList) {
    super(pos);
    this.query = requireNonNull(query, "query");
    this.includeNulls = includeNulls;
    this.measureList = requireNonNull(measureList, "measureList");
    this.axisList = requireNonNull(axisList, "axisList");
    this.inList = requireNonNull(inList, "inList");
  }

  //~ Methods ----------------------------------------------------------------

  @Override public SqlOperator getOperator() {
    return OPERATOR;
  }

  @Override public List<SqlNode> getOperandList() {
    return ImmutableNullableList.of(query, measureList, axisList, inList);
  }

  @SuppressWarnings("nullness")
  @Override public void setOperand(int i, @Nullable SqlNode operand) {
    // Only 'query' is mutable. (It is required for validation.)
    switch (i) {
    case 0:
      query = operand;
      break;
    default:
      super.setOperand(i, operand);
    }
  }

  @Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
    query.unparse(writer, leftPrec, 0);
    writer.keyword("UNPIVOT");
    writer.keyword(includeNulls ? "INCLUDE NULLS" : "EXCLUDE NULLS");
    final SqlWriter.Frame frame = writer.startList("(", ")");
    // force parentheses if there is more than one foo
    final int leftPrec1 = measureList.size() > 1 ? 1 : 0;
    measureList.unparse(writer, leftPrec1, 0);
    writer.sep("FOR");
    // force parentheses if there is more than one axis
    final int leftPrec2 = axisList.size() > 1 ? 1 : 0;
    axisList.unparse(writer, leftPrec2, 0);
    writer.sep("IN");
    writer.list(SqlWriter.FrameTypeEnum.PARENTHESES, SqlWriter.COMMA,
        SqlPivot.stripList(inList));
    writer.endList(frame);
  }

  /** Returns the measure list as SqlIdentifiers. */
  @SuppressWarnings({"unchecked", "rawtypes"})
  public void forEachMeasure(Consumer<SqlIdentifier> consumer) {
    ((List<SqlIdentifier>) (List) measureList).forEach(consumer);
  }

  /** Returns contents of the IN clause {@code (nodeList, valueList)} pairs.
   * {@code valueList} is null if the entry has no {@code AS} clause. */
  public void forEachNameValues(
      BiConsumer<SqlNodeList, @Nullable SqlNodeList> consumer) {
    for (SqlNode node : inList) {
      switch (node.getKind()) {
      case AS:
        final SqlCall call = (SqlCall) node;
        assert call.getOperandList().size() == 2;
        final SqlNodeList nodeList = call.operand(0);
        final SqlNodeList valueList = call.operand(1);
        consumer.accept(nodeList, valueList);
        break;
      default:
        final SqlNodeList nodeList2 = (SqlNodeList) node;
        consumer.accept(nodeList2, null);
      }
    }
  }

  /** Returns the set of columns that are referenced in the {@code FOR}
   * clause. All columns that are not used will be part of the returned row. */
  public Set<String> usedColumnNames() {
    final Set<String> columnNames = new HashSet<>();
    final SqlVisitor<Void> nameCollector = new SqlBasicVisitor<Void>() {
      @Override public Void visit(SqlIdentifier id) {
        columnNames.add(Util.last(id.names));
        return super.visit(id);
      }
    };
    forEachNameValues((aliasList, valueList) ->
        aliasList.accept(nameCollector));
    return columnNames;
  }

  /** Computes an alias. In the query fragment
   * <blockquote>
   *   {@code UNPIVOT ... FOR ... IN ((c1, c2) AS 'c1_c2', (c3, c4))}
   * </blockquote>
   * note that {@code (c3, c4)} has no {@code AS}. The computed alias is
   * 'C3_C4'. */
  public static String aliasValue(SqlNodeList aliasList) {
    final StringBuilder b = new StringBuilder();
    aliasList.forEach(alias -> {
      if (b.length() > 0) {
        b.append('_');
      }
      b.append(Util.last(((SqlIdentifier) alias).names));
    });
    return b.toString();
  }

  /** Unpivot operator. */
  static class Operator extends SqlSpecialOperator {
    Operator(SqlKind kind) {
      super(kind.name(), kind);
    }
  }
}