content: /*
   * 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.fun;
  
  import org.apache.calcite.avatica.util.TimeUnitRange;
  import org.apache.calcite.rel.type.RelDataType;
  import org.apache.calcite.sql.SqlCall;
  import org.apache.calcite.sql.SqlCharStringLiteral;
  import org.apache.calcite.sql.SqlFunction;
  import org.apache.calcite.sql.SqlFunctionCategory;
  import org.apache.calcite.sql.SqlIntervalQualifier;
  import org.apache.calcite.sql.SqlKind;
  import org.apache.calcite.sql.SqlOperatorBinding;
  import org.apache.calcite.sql.SqlWriter;
  import org.apache.calcite.sql.type.OperandTypes;
  import org.apache.calcite.sql.type.ReturnTypes;
  import org.apache.calcite.sql.type.SqlTypeFamily;
  import org.apache.calcite.sql.type.SqlTypeName;
  import org.apache.calcite.sql.validate.SqlMonotonicity;
  import org.apache.calcite.sql.validate.SqlValidator;
  import org.apache.calcite.sql.validate.SqlValidatorScope;
  import org.apache.calcite.util.Util;
  
  import com.google.common.collect.ImmutableSet;
  
  import static org.apache.calcite.sql.validate.SqlNonNullableAccessors.getOperandLiteralValueOrThrow;
  import static org.apache.calcite.util.Static.RESOURCE;
  
  import static java.util.Objects.requireNonNull;
  
  /**
   * The SQL <code>EXTRACT</code> operator. Extracts a specified field value from
   * a DATETIME or an INTERVAL. E.g.<br>
   * <code>EXTRACT(HOUR FROM INTERVAL '364 23:59:59')</code> returns <code>
   * 23</code>
   */
  public class SqlExtractFunction extends SqlFunction {
    //~ Constructors -----------------------------------------------------------
  
    // SQL2003, Part 2, Section 4.4.3 - extract returns a exact numeric
    // TODO: Return type should be decimal for seconds
    public SqlExtractFunction(String name, boolean allowString) {
      super(name, SqlKind.EXTRACT, ReturnTypes.BIGINT_NULLABLE, null,
          allowString
              ? OperandTypes.INTERVALINTERVAL_INTERVALDATETIME
                  .or(OperandTypes.family(SqlTypeFamily.STRING, SqlTypeFamily.DATETIME))
              : OperandTypes.INTERVALINTERVAL_INTERVALDATETIME,
          SqlFunctionCategory.SYSTEM);
    }
  
    //~ Methods ----------------------------------------------------------------
  
    @Override public String getSignatureTemplate(int operandsCount) {
      Util.discard(operandsCount);
      return "{0}({1} FROM {2})";
    }
  
    @Override public void unparse(
        SqlWriter writer,
        SqlCall call,
        int leftPrec,
        int rightPrec) {
      final SqlWriter.Frame frame = writer.startFunCall(getName());
      SqlIntervalQualifier.asIdentifier(call.operand(0))
          .unparse(writer, 0, 0);
      writer.sep("FROM");
      call.operand(1).unparse(writer, 0, 0);
      writer.endFunCall(frame);
    }
  
    // List of types that support EXTRACT(X, ...) where X is MONTH or larger
    private static final ImmutableSet<SqlTypeName> MONTH_AND_ABOVE_TYPES =
        new ImmutableSet.Builder<SqlTypeName>()
        .add(SqlTypeName.DATE)
        .add(SqlTypeName.TIMESTAMP)
        .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
        .addAll(SqlTypeName.YEAR_INTERVAL_TYPES)
        .build();
  
    // List of types that support EXTRACT(X, ...) where X is between DAY and WEEK
    private static final ImmutableSet<SqlTypeName> DAY_TO_WEEK_TYPES =
        new ImmutableSet.Builder<SqlTypeName>()
            .add(SqlTypeName.DATE)
            .add(SqlTypeName.TIMESTAMP)
            .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
            .build();
  
    // List of types that support EXTRACT(EPOCH, ...)
    private static final ImmutableSet<SqlTypeName> EPOCH_TYPES =
        new ImmutableSet.Builder<SqlTypeName>()
            .add(SqlTypeName.DATE)
            .add(SqlTypeName.TIMESTAMP)
            .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
            .addAll(SqlTypeName.YEAR_INTERVAL_TYPES)
            .addAll(SqlTypeName.DAY_INTERVAL_TYPES)
            .build();
  
    // List of types that support EXTRACT(DAY, ...)
    private static final ImmutableSet<SqlTypeName> DAY_TYPES =
        new ImmutableSet.Builder<SqlTypeName>()
            .add(SqlTypeName.DATE)
            .add(SqlTypeName.TIMESTAMP)
            .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
            .add(SqlTypeName.INTERVAL_DAY)
            .add(SqlTypeName.INTERVAL_DAY_HOUR)
            .add(SqlTypeName.INTERVAL_DAY_MINUTE)
            .add(SqlTypeName.INTERVAL_DAY_SECOND)
            .addAll(SqlTypeName.YEAR_INTERVAL_TYPES)
            .build();
  
    // List of types that support EXTRACT(X, ...) where X is
    // between HOUR and NANOSECOND
    private static final ImmutableSet<SqlTypeName> HOUR_TO_NANOSECOND_TYPES =
        new ImmutableSet.Builder<SqlTypeName>()
            .add(SqlTypeName.DATE)
            .add(SqlTypeName.TIMESTAMP)
            .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
            .add(SqlTypeName.TIME)
            .add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE)
            .addAll(SqlTypeName.YEAR_INTERVAL_TYPES)
            .addAll(SqlTypeName.DAY_INTERVAL_TYPES)
            .build();
  
    @Override public void validateCall(SqlCall call, SqlValidator validator,
        SqlValidatorScope scope, SqlValidatorScope operandScope) {
      super.validateCall(call, validator, scope, operandScope);
  
      // This is either a time unit or a time frame:
      //
      //  * In "EXTRACT(YEAR FROM x)" operand 0 is a SqlIntervalQualifier with
      //    startUnit = YEAR and timeFrameName = null.
      //
      //  * In "EXTRACT(MINUTE15 FROM x)" operand 0 is a SqlIntervalQualifier with
      //    startUnit = EPOCH and timeFrameName = 'MINUTE15'.
      //
      // If the latter, check that timeFrameName is valid.
      SqlIntervalQualifier qualifier;
      if (call.operand(0) instanceof SqlCharStringLiteral) {
        final SqlCharStringLiteral stringLiteral = call.operand(0);
        qualifier =
            new SqlIntervalQualifier(requireNonNull(stringLiteral.toValue()),
                call.operand(0).getParserPosition());
      } else {
        qualifier = call.operand(0);
      }
      validator.validateTimeFrame(qualifier);
      TimeUnitRange range = qualifier.timeUnitRange;
  
      RelDataType type = validator.getValidatedNodeTypeIfKnown(call.operand(1));
      if (type == null) {
        return;
      }
  
      SqlTypeName typeName = type.getSqlTypeName();
      boolean legal;
      switch (range) {
      case YEAR:
      case MONTH:
      case ISOYEAR:
      case QUARTER:
      case DECADE:
      case CENTURY:
      case MILLENNIUM:
        legal = MONTH_AND_ABOVE_TYPES.contains(typeName);
        break;
      case WEEK:
      case DOW:
      case ISODOW:
      case DOY:
        legal = DAY_TO_WEEK_TYPES.contains(typeName);
        break;
      case EPOCH:
        legal = EPOCH_TYPES.contains(typeName);
        break;
      case DAY:
        legal = DAY_TYPES.contains(typeName);
        break;
      case HOUR:
      case MINUTE:
      case SECOND:
      case MILLISECOND:
      case MICROSECOND:
      case NANOSECOND:
        legal = HOUR_TO_NANOSECOND_TYPES.contains(typeName);
        break;
      case YEAR_TO_MONTH:
      case DAY_TO_HOUR:
      case DAY_TO_MINUTE:
      case DAY_TO_SECOND:
      case HOUR_TO_MINUTE:
      case HOUR_TO_SECOND:
      case MINUTE_TO_SECOND:
      default:
        legal = false;
        break;
      }
  
      if (!legal) {
        throw validator.newValidationError(call,
            RESOURCE.canNotApplyOp2Type(call.getOperator().getName(),
                call.getCallSignature(validator, scope),
                call.getOperator().getAllowedSignatures()));
      }
    }
  
    @Override public SqlMonotonicity getMonotonicity(SqlOperatorBinding call) {
      final TimeUnitRange value;
      if (SqlTypeName.CHAR_TYPES.contains(call.getOperandType(0).getSqlTypeName())) {
        value =
            TimeUnitRange.of(
                SqlIntervalQualifier.stringToDatePartTimeUnit(
                    requireNonNull(call.getOperandLiteralValue(0, String.class))),
            null);
      } else {
        value = getOperandLiteralValueOrThrow(call, 0, TimeUnitRange.class);
      }
  
      switch (value) {
      case YEAR:
        return call.getOperandMonotonicity(1).unstrict();
      default:
        return SqlMonotonicity.NOT_MONOTONIC;
      }
    }
  }
  