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;
  
  import org.apache.calcite.avatica.util.TimeUnit;
  import org.apache.calcite.avatica.util.TimeUnitRange;
  import org.apache.calcite.rel.type.RelDataType;
  import org.apache.calcite.rel.type.RelDataTypeSystem;
  import org.apache.calcite.rel.type.TimeFrames;
  import org.apache.calcite.runtime.CalciteContextException;
  import org.apache.calcite.sql.parser.SqlParserPos;
  import org.apache.calcite.sql.type.SqlTypeName;
  import org.apache.calcite.sql.util.SqlVisitor;
  import org.apache.calcite.sql.validate.SqlValidator;
  import org.apache.calcite.sql.validate.SqlValidatorScope;
  import org.apache.calcite.util.Litmus;
  import org.apache.calcite.util.Util;
  
  import com.google.common.collect.ImmutableSet;
  
  import org.checkerframework.checker.nullness.qual.Nullable;
  
  import java.math.BigDecimal;
  import java.util.Locale;
  import java.util.Set;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import static org.apache.calcite.linq4j.Nullness.castNonNull;
  import static org.apache.calcite.util.Static.RESOURCE;
  
  import static java.util.Objects.requireNonNull;
  
  /**
   * Represents an INTERVAL qualifier.
   *
   * <p>INTERVAL qualifier is defined as follows:
   *
   * <blockquote><code>
   * &lt;interval qualifier&gt; ::=<br>
   * &nbsp;&nbsp; &lt;start field&gt; TO &lt;end field&gt;<br>
   * &nbsp;&nbsp;| &lt;single datetime field&gt;<br>
   * &lt;start field&gt; ::=<br>
   * &nbsp;&nbsp; &lt;non-second primary datetime field&gt;<br>
   * &nbsp;&nbsp; [ &lt;left paren&gt; &lt;interval leading field precision&gt;
   * &lt;right paren&gt; ]<br>
   * &lt;end field&gt; ::=<br>
   * &nbsp;&nbsp; &lt;non-second primary datetime field&gt;<br>
   * &nbsp;&nbsp;| SECOND [ &lt;left paren&gt;
   * &lt;interval fractional seconds precision&gt; &lt;right paren&gt; ]<br>
   * &lt;single datetime field&gt; ::=<br>
   * &nbsp;&nbsp;&lt;non-second primary datetime field&gt;<br>
   * &nbsp;&nbsp;[ &lt;left paren&gt; &lt;interval leading field precision&gt;
   * &lt;right paren&gt; ]<br>
   * &nbsp;&nbsp;| SECOND [ &lt;left paren&gt;
   * &lt;interval leading field precision&gt;<br>
   * &nbsp;&nbsp;[ &lt;comma&gt; &lt;interval fractional seconds precision&gt; ]
   * &lt;right paren&gt; ]<br>
   * &lt;primary datetime field&gt; ::=<br>
   * &nbsp;&nbsp;&lt;non-second primary datetime field&gt;<br>
   * &nbsp;&nbsp;| SECOND<br>
   * &lt;non-second primary datetime field&gt; ::= YEAR | MONTH | DAY | HOUR
   * | MINUTE<br>
   * &lt;interval fractional seconds precision&gt; ::=
   * &lt;unsigned integer&gt;<br>
   * &lt;interval leading field precision&gt; ::= &lt;unsigned integer&gt;
   * </code></blockquote>
   *
   * <p>Examples include:
   *
   * <ul>
   * <li><code>INTERVAL '1:23:45.678' HOUR TO SECOND</code></li>
   * <li><code>INTERVAL '1 2:3:4' DAY TO SECOND</code></li>
   * <li><code>INTERVAL '1 2:3:4' DAY(4) TO SECOND(4)</code></li>
   * </ul>
   *
   * <p>An instance of this class is immutable.
   */
  public class SqlIntervalQualifier extends SqlNode {
    //~ Static fields/initializers ---------------------------------------------
  
    private static final BigDecimal ZERO = BigDecimal.ZERO;
    private static final BigDecimal THOUSAND = BigDecimal.valueOf(1000);
    private static final BigDecimal INT_MAX_VALUE_PLUS_ONE =
        BigDecimal.valueOf(Integer.MAX_VALUE).add(BigDecimal.ONE);
  
    private static final Set<TimeUnitRange> TIME_UNITS =
        ImmutableSet.of(TimeUnitRange.HOUR,
            TimeUnitRange.MINUTE,
            TimeUnitRange.SECOND);
  
    private static final Set<TimeUnitRange> MONTH_UNITS =
        ImmutableSet.of(TimeUnitRange.MILLENNIUM,
            TimeUnitRange.CENTURY,
            TimeUnitRange.DECADE,
            TimeUnitRange.YEAR,
            TimeUnitRange.ISOYEAR,
            TimeUnitRange.QUARTER,
            TimeUnitRange.MONTH);
  
    private static final Set<TimeUnitRange> DAY_UNITS =
        ImmutableSet.of(TimeUnitRange.WEEK,
            TimeUnitRange.DAY);
  
    private static final Set<TimeUnitRange> DATE_UNITS =
        ImmutableSet.<TimeUnitRange>builder()
            .addAll(MONTH_UNITS).addAll(DAY_UNITS).build();
  
    private static final Set<String> WEEK_FRAMES =
        ImmutableSet.<String>builder()
            .addAll(TimeFrames.WEEK_FRAME_NAMES)
            .add("ISOWEEK")
            .add("WEEK")
            .add("SQL_TSI_WEEK")
            .build();
  
    private static final Set<String> TSI_TIME_FRAMES =
        ImmutableSet.of(
            "SQL_TSI_FRAC_SECOND",
            "SQL_TSI_MICROSECOND",
            "SQL_TSI_SECOND",
            "SQL_TSI_MINUTE",
            "SQL_TSI_HOUR");
  
    private static final Set<String> TSI_DATE_FRAMES =
        ImmutableSet.of(
            "SQL_TSI_DAY",
            "SQL_TSI_WEEK",
            "SQL_TSI_MONTH",
            "SQL_TSI_QUARTER",
            "SQL_TSI_YEAR");
  
    //~ Instance fields --------------------------------------------------------
  
    private final int startPrecision;
    public final @Nullable String timeFrameName;
    public final TimeUnitRange timeUnitRange;
    private final int fractionalSecondPrecision;
  
    //~ Constructors -----------------------------------------------------------
  
    private SqlIntervalQualifier(SqlParserPos pos, @Nullable String timeFrameName,
        TimeUnitRange timeUnitRange, int startPrecision,
        int fractionalSecondPrecision) {
      super(pos);
      this.timeFrameName = timeFrameName;
      this.timeUnitRange = requireNonNull(timeUnitRange, "timeUnitRange");
      this.startPrecision = startPrecision;
      this.fractionalSecondPrecision = fractionalSecondPrecision;
    }
  
    public SqlIntervalQualifier(
        TimeUnit startUnit,
        int startPrecision,
        @Nullable TimeUnit endUnit,
        int fractionalSecondPrecision,
        SqlParserPos pos) {
      this(pos, null,
          TimeUnitRange.of(requireNonNull(startUnit, "startUnit"),
              endUnit == startUnit ? null : endUnit),
          startPrecision, fractionalSecondPrecision);
    }
  
    public SqlIntervalQualifier(
        TimeUnit startUnit,
        @Nullable TimeUnit endUnit,
        SqlParserPos pos) {
      this(
          startUnit,
          RelDataType.PRECISION_NOT_SPECIFIED,
          endUnit,
          RelDataType.PRECISION_NOT_SPECIFIED,
          pos);
    }
  
    /** Creates a qualifier based on a time frame name. */
    public SqlIntervalQualifier(String timeFrameName,
        SqlParserPos pos) {
      this(pos, requireNonNull(timeFrameName, "timeFrameName"),
          // EPOCH is a placeholder because code expects a non-null TimeUnitRange.
          TimeUnitRange.EPOCH, RelDataType.PRECISION_NOT_SPECIFIED,
          RelDataType.PRECISION_NOT_SPECIFIED);
    }
  
    //~ Methods ----------------------------------------------------------------
  
    @Override public SqlKind getKind() {
      return SqlKind.INTERVAL_QUALIFIER;
    }
  
    public SqlTypeName typeName() {
      switch (timeUnitRange) {
      case YEAR:
      case ISOYEAR:
      case CENTURY:
      case DECADE:
      case MILLENNIUM:
        return SqlTypeName.INTERVAL_YEAR;
      case YEAR_TO_MONTH:
        return SqlTypeName.INTERVAL_YEAR_MONTH;
      case MONTH:
      case QUARTER:
        return SqlTypeName.INTERVAL_MONTH;
      case DOW:
      case ISODOW:
      case DOY:
      case DAY:
      case WEEK:
        return SqlTypeName.INTERVAL_DAY;
      case DAY_TO_HOUR:
        return SqlTypeName.INTERVAL_DAY_HOUR;
      case DAY_TO_MINUTE:
        return SqlTypeName.INTERVAL_DAY_MINUTE;
      case DAY_TO_SECOND:
        return SqlTypeName.INTERVAL_DAY_SECOND;
      case HOUR:
        return SqlTypeName.INTERVAL_HOUR;
      case HOUR_TO_MINUTE:
        return SqlTypeName.INTERVAL_HOUR_MINUTE;
      case HOUR_TO_SECOND:
        return SqlTypeName.INTERVAL_HOUR_SECOND;
      case MINUTE:
        return SqlTypeName.INTERVAL_MINUTE;
      case MINUTE_TO_SECOND:
        return SqlTypeName.INTERVAL_MINUTE_SECOND;
      case SECOND:
      case MILLISECOND:
      case EPOCH:
      case MICROSECOND:
      case NANOSECOND:
        return SqlTypeName.INTERVAL_SECOND;
      default:
        throw new AssertionError(timeUnitRange);
      }
    }
  
    /** Whether this is a DATE interval (including all week intervals). */
    public boolean isDate() {
      return DATE_UNITS.contains(timeUnitRange)
          || timeFrameName != null && TSI_DATE_FRAMES.contains(timeFrameName)
          || isWeek();
    }
  
    /** Whether this is a TIME interval. */
    public boolean isTime() {
      return TIME_UNITS.contains(timeUnitRange)
          || timeFrameName != null && TSI_TIME_FRAMES.contains(timeFrameName);
    }
  
    /** Whether this is a TIMESTAMP interval (including all week intervals). */
    public boolean isTimestamp() {
      return isDate() || isTime();
    }
  
    /** Whether this qualifier represents {@code WEEK}, {@code ISOWEEK},
     * or {@code WEEK(}<i>weekday</i>{@code )}
     * (for <i>weekday</i> in {@code SUNDAY} .. {@code SATURDAY}). */
    public boolean isWeek() {
      return timeUnitRange == TimeUnitRange.WEEK
          || timeFrameName != null && WEEK_FRAMES.contains(timeFrameName);
    }
  
    @Override public void validate(
        SqlValidator validator,
        SqlValidatorScope scope) {
      validator.validateIntervalQualifier(this);
    }
  
    @Override public <R> R accept(SqlVisitor<R> visitor) {
      return visitor.visit(this);
    }
  
    @Override public boolean equalsDeep(@Nullable SqlNode node, Litmus litmus) {
      if (node == null) {
        return litmus.fail("other==null");
      }
      final String thisString = this.toString();
      final String thatString = node.toString();
      if (!thisString.equals(thatString)) {
        return litmus.fail("{} != {}", this, node);
      }
      return litmus.succeed();
    }
  
    public int getStartPrecision(RelDataTypeSystem typeSystem) {
      if (startPrecision == RelDataType.PRECISION_NOT_SPECIFIED) {
        return typeSystem.getMaxPrecision(typeName());
      } else {
        return startPrecision;
      }
    }
  
    public int getStartPrecisionPreservingDefault() {
      return startPrecision;
    }
  
    /** Returns {@code true} if start precision is not specified. */
    public boolean useDefaultStartPrecision() {
      return startPrecision == RelDataType.PRECISION_NOT_SPECIFIED;
    }
  
    public static int combineStartPrecisionPreservingDefault(
        RelDataTypeSystem typeSystem,
        SqlIntervalQualifier qual1,
        SqlIntervalQualifier qual2) {
      final int start1 = qual1.getStartPrecision(typeSystem);
      final int start2 = qual2.getStartPrecision(typeSystem);
      if (start1 > start2) {
        // qual1 is more precise, but if it has the default indicator
        // set, we need to return that indicator so result will also
        // use default
        return qual1.getStartPrecisionPreservingDefault();
      } else if (start1 < start2) {
        // qual2 is more precise, but if it has the default indicator
        // set, we need to return that indicator so result will also
        // use default
        return qual2.getStartPrecisionPreservingDefault();
      } else {
        // they are equal.  return default if both are default,
        // otherwise return exact precision
        if (qual1.useDefaultStartPrecision()
            && qual2.useDefaultStartPrecision()) {
          return qual1.getStartPrecisionPreservingDefault();
        } else {
          return start1;
        }
      }
    }
  
    public int getFractionalSecondPrecision(RelDataTypeSystem typeSystem) {
      if (fractionalSecondPrecision == RelDataType.PRECISION_NOT_SPECIFIED) {
        return typeSystem.getDefaultScale(typeName());
      } else {
        return fractionalSecondPrecision;
      }
    }
  
    public int getFractionalSecondPrecisionPreservingDefault() {
      if (useDefaultFractionalSecondPrecision()) {
        return RelDataType.PRECISION_NOT_SPECIFIED;
      } else {
        return fractionalSecondPrecision;
      }
    }
  
    /** Returns {@code true} if fractional second precision is not specified. */
    public boolean useDefaultFractionalSecondPrecision() {
      return fractionalSecondPrecision == RelDataType.PRECISION_NOT_SPECIFIED;
    }
  
    public static int combineFractionalSecondPrecisionPreservingDefault(
        RelDataTypeSystem typeSystem,
        SqlIntervalQualifier qual1,
        SqlIntervalQualifier qual2) {
      final int p1 = qual1.getFractionalSecondPrecision(typeSystem);
      final int p2 = qual2.getFractionalSecondPrecision(typeSystem);
      if (p1 > p2) {
        // qual1 is more precise, but if it has the default indicator
        // set, we need to return that indicator so result will also
        // use default
        return qual1.getFractionalSecondPrecisionPreservingDefault();
      } else if (p1 < p2) {
        // qual2 is more precise, but if it has the default indicator
        // set, we need to return that indicator so result will also
        // use default
        return qual2.getFractionalSecondPrecisionPreservingDefault();
      } else {
        // they are equal.  return default if both are default,
        // otherwise return exact precision
        if (qual1.useDefaultFractionalSecondPrecision()
            && qual2.useDefaultFractionalSecondPrecision()) {
          return qual1.getFractionalSecondPrecisionPreservingDefault();
        } else {
          return p1;
        }
      }
    }
  
    public TimeUnit getStartUnit() {
      return timeUnitRange.startUnit;
    }
  
    public TimeUnit getEndUnit() {
      return timeUnitRange.endUnit;
    }
  
    /** Returns {@code SECOND} for both {@code HOUR TO SECOND} and
     * {@code SECOND}. */
    public TimeUnit getUnit() {
      return Util.first(timeUnitRange.endUnit, timeUnitRange.startUnit);
    }
  
    @Override public SqlNode clone(SqlParserPos pos) {
      return new SqlIntervalQualifier(timeUnitRange.startUnit, startPrecision,
          timeUnitRange.endUnit, fractionalSecondPrecision, pos);
    }
  
    @Override public void unparse(
        SqlWriter writer,
        int leftPrec,
        int rightPrec) {
      writer.getDialect()
          .unparseSqlIntervalQualifier(writer, this, RelDataTypeSystem.DEFAULT);
    }
  
    /**
     * Returns whether this interval has a single datetime field.
     *
     * <p>Returns {@code true} if it is of the form {@code unit},
     * {@code false} if it is of the form {@code unit TO unit}.
     */
    public boolean isSingleDatetimeField() {
      return timeUnitRange.endUnit == null;
    }
  
    public final boolean isYearMonth() {
      return timeUnitRange.startUnit.yearMonth;
    }
  
    /**
     * Returns 1 or -1.
     */
    public int getIntervalSign(String value) {
      int sign = 1; // positive until proven otherwise
  
      if (!Util.isNullOrEmpty(value)) {
        if ('-' == value.charAt(0)) {
          sign = -1; // Negative
        }
      }
  
      return sign;
    }
  
    public static TimeUnit stringToDatePartTimeUnit(String stringValue) {
      final String timeUnitString = stringValue.toUpperCase(Locale.ROOT);
  
      switch (timeUnitString) {
      case "MICROSECOND":
        return TimeUnit.MICROSECOND;
      case "MILLISECOND":
        return TimeUnit.MILLISECOND;
      case "SECOND":
        return TimeUnit.SECOND;
      case "MINUTE":
        return TimeUnit.MINUTE;
      case "HOUR":
        return TimeUnit.HOUR;
      case "DAY":
        return TimeUnit.DAY;
      case "DAYOFWEEK":
      case "DOW":
        return TimeUnit.DOW;
      case "DAYOFYEAR":
      case "DOY":
        return TimeUnit.DOY;
      case "ISODOW":
        return TimeUnit.ISODOW;
      case "ISODOY":
        return TimeUnit.ISOYEAR;
      case "WEEK":
        return TimeUnit.WEEK;
      case "MONTH":
        return TimeUnit.MONTH;
      case "QUARTER":
        return TimeUnit.QUARTER;
      case "YEAR":
        return TimeUnit.YEAR;
      case "EPOCH":
        return TimeUnit.EPOCH;
      case "DECADE":
        return TimeUnit.DECADE;
      case "CENTURY":
        return TimeUnit.CENTURY;
      case "MILLENNIUM":
        return TimeUnit.MILLENNIUM;
      default:
        throw new IllegalArgumentException("Date/Time units \"" + stringValue + "\" not recognized");
      }
    }
  
    private static String stripLeadingSign(String value) {
      String unsignedValue = value;
  
      if (!Util.isNullOrEmpty(value)) {
        if (('-' == value.charAt(0)) || ('+' == value.charAt(0))) {
          unsignedValue = value.substring(1);
        }
      }
  
      return unsignedValue;
    }
  
    private boolean isLeadFieldInRange(RelDataTypeSystem typeSystem,
        BigDecimal value, @SuppressWarnings("unused") TimeUnit unit) {
      // we should never get handed a negative field value
      assert value.compareTo(ZERO) >= 0;
  
      // Leading fields are only restricted by startPrecision.
      final int startPrecision = getStartPrecision(typeSystem);
      return startPrecision < POWERS10.length
          ? value.compareTo(POWERS10[startPrecision]) < 0
          : value.compareTo(INT_MAX_VALUE_PLUS_ONE) < 0;
    }
  
    private void checkLeadFieldInRange(RelDataTypeSystem typeSystem, int sign,
        BigDecimal value, TimeUnit unit, SqlParserPos pos) {
      if (!isLeadFieldInRange(typeSystem, value, unit)) {
        throw fieldExceedsPrecisionException(
            pos, sign, value, unit, getStartPrecision(typeSystem));
      }
    }
  
    private static final BigDecimal[] POWERS10 = {
        ZERO,
      BigDecimal.valueOf(10),
      BigDecimal.valueOf(100),
      BigDecimal.valueOf(1000),
      BigDecimal.valueOf(10000),
      BigDecimal.valueOf(100000),
      BigDecimal.valueOf(1000000),
      BigDecimal.valueOf(10000000),
      BigDecimal.valueOf(100000000),
      BigDecimal.valueOf(1000000000),
    };
  
    private static boolean isFractionalSecondFieldInRange(BigDecimal field) {
      // we should never get handed a negative field value
      assert field.compareTo(ZERO) >= 0;
  
      // Fractional second fields are only restricted by precision, which
      // has already been checked for using pattern matching.
      // Therefore, always return true
      return true;
    }
  
    private static boolean isSecondaryFieldInRange(BigDecimal field, TimeUnit unit) {
      // we should never get handed a negative field value
      assert field.compareTo(ZERO) >= 0;
  
      // YEAR and DAY can never be secondary units,
      // nor can unit be null.
      switch (requireNonNull(unit, "unit")) {
      case YEAR:
      case DAY:
      default:
        throw Util.unexpected(unit);
  
        // Secondary field limits, as per section 4.6.3 of SQL2003 spec
      case MONTH:
      case HOUR:
      case MINUTE:
      case SECOND:
        return unit.isValidValue(field);
      }
    }
  
    private static BigDecimal normalizeSecondFraction(String secondFracStr) {
      // Decimal value can be more than 3 digits. So just get
      // the millisecond part.
      return new BigDecimal("0." + secondFracStr).multiply(THOUSAND);
    }
  
    private static int[] fillYearMonthIntervalValueArray(
        int sign,
        BigDecimal year,
        BigDecimal month) {
      int[] ret = new int[3];
  
      ret[0] = sign;
      ret[1] = year.intValue();
      ret[2] = month.intValue();
  
      return ret;
    }
  
    private static int[] fillDayTimeIntervalValueArray(
        int sign,
        BigDecimal day,
        BigDecimal hour,
        BigDecimal minute,
        BigDecimal second,
        BigDecimal secondFrac) {
      int[] ret = new int[6];
  
      ret[0] = sign;
      ret[1] = day.intValue();
      ret[2] = hour.intValue();
      ret[3] = minute.intValue();
      ret[4] = second.intValue();
      ret[5] = secondFrac.intValue();
  
      return ret;
    }
  
    /**
     * Validates an INTERVAL literal against a YEAR interval qualifier.
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    private int[] evaluateIntervalLiteralAsYear(
        RelDataTypeSystem typeSystem, int sign,
        String value,
        String originalValue,
        SqlParserPos pos) {
      BigDecimal year;
  
      // validate as YEAR(startPrecision), e.g. 'YY'
      String intervalPattern = "(\\d+)";
  
      Matcher m = Pattern.compile(intervalPattern).matcher(value);
      if (m.matches()) {
        // Break out  field values
        try {
          year = parseField(m, 1);
        } catch (NumberFormatException e) {
          throw invalidValueException(pos, originalValue);
        }
  
        // Validate individual fields
        checkLeadFieldInRange(typeSystem, sign, year, TimeUnit.YEAR, pos);
  
        // package values up for return
        return fillYearMonthIntervalValueArray(sign, year, ZERO);
      } else {
        throw invalidValueException(pos, originalValue);
      }
    }
  
    /**
     * Validates an INTERVAL literal against a YEAR TO MONTH interval qualifier.
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    private int[] evaluateIntervalLiteralAsYearToMonth(
        RelDataTypeSystem typeSystem, int sign,
        String value,
        String originalValue,
        SqlParserPos pos) {
      BigDecimal year;
      BigDecimal month;
  
      // validate as YEAR(startPrecision) TO MONTH, e.g. 'YY-DD'
      String intervalPattern = "(\\d+)-(\\d{1,2})";
  
      Matcher m = Pattern.compile(intervalPattern).matcher(value);
      if (m.matches()) {
        // Break out  field values
        try {
          year = parseField(m, 1);
          month = parseField(m, 2);
        } catch (NumberFormatException e) {
          throw invalidValueException(pos, originalValue);
        }
  
        // Validate individual fields
        checkLeadFieldInRange(typeSystem, sign, year, TimeUnit.YEAR, pos);
        if (!isSecondaryFieldInRange(month, TimeUnit.MONTH)) {
          throw invalidValueException(pos, originalValue);
        }
  
        // package values up for return
        return fillYearMonthIntervalValueArray(sign, year, month);
      } else {
        throw invalidValueException(pos, originalValue);
      }
    }
  
    /**
     * Validates an INTERVAL literal against a MONTH interval qualifier.
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    private int[] evaluateIntervalLiteralAsMonth(
        RelDataTypeSystem typeSystem, int sign,
        String value,
        String originalValue,
        SqlParserPos pos) {
      BigDecimal month;
  
      // validate as MONTH(startPrecision), e.g. 'MM'
      String intervalPattern = "(\\d+)";
  
      Matcher m = Pattern.compile(intervalPattern).matcher(value);
      if (m.matches()) {
        // Break out  field values
        try {
          month = parseField(m, 1);
        } catch (NumberFormatException e) {
          throw invalidValueException(pos, originalValue);
        }
  
        // Validate individual fields
        checkLeadFieldInRange(typeSystem, sign, month, TimeUnit.MONTH, pos);
  
        // package values up for return
        return fillYearMonthIntervalValueArray(sign, ZERO, month);
      } else {
        throw invalidValueException(pos, originalValue);
      }
    }
  
    /**
     * Validates an INTERVAL literal against a QUARTER interval qualifier.
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    private int[] evaluateIntervalLiteralAsQuarter(
        RelDataTypeSystem typeSystem, int sign,
        String value,
        String originalValue,
        SqlParserPos pos) {
      BigDecimal quarter;
  
      // validate as QUARTER(startPrecision), e.g. 'MM'
      String intervalPattern = "(\\d+)";
  
      Matcher m = Pattern.compile(intervalPattern).matcher(value);
      if (m.matches()) {
        // Break out  field values
        try {
          quarter = parseField(m, 1);
        } catch (NumberFormatException e) {
          throw invalidValueException(pos, originalValue);
        }
  
        // Validate individual fields
        checkLeadFieldInRange(typeSystem, sign, quarter, TimeUnit.QUARTER, pos);
  
        // package values up for return
        final BigDecimal months = quarter.multiply(BigDecimal.valueOf(3));
        return fillYearMonthIntervalValueArray(sign, ZERO, months);
      } else {
        throw invalidValueException(pos, originalValue);
      }
    }
  
    /**
     * Validates an INTERVAL literal against a WEEK interval qualifier.
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    private int[] evaluateIntervalLiteralAsWeek(
        RelDataTypeSystem typeSystem, int sign,
        String value,
        String originalValue,
        SqlParserPos pos) {
      BigDecimal week;
  
      // validate as WEEK(startPrecision), e.g. 'MM'
      String intervalPattern = "(\\d+)";
  
      Matcher m = Pattern.compile(intervalPattern).matcher(value);
      if (m.matches()) {
        // Break out  field values
        try {
          week = parseField(m, 1);
        } catch (NumberFormatException e) {
          throw invalidValueException(pos, originalValue);
        }
  
        // Validate individual fields
        checkLeadFieldInRange(typeSystem, sign, week, TimeUnit.WEEK, pos);
  
        // package values up for return
        final BigDecimal days = week.multiply(BigDecimal.valueOf(7));
        return fillDayTimeIntervalValueArray(sign, days, ZERO, ZERO, ZERO, ZERO);
      } else {
        throw invalidValueException(pos, originalValue);
      }
    }
  
    /**
     * Validates an INTERVAL literal against a DAY interval qualifier.
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    private int[] evaluateIntervalLiteralAsDay(
        RelDataTypeSystem typeSystem, int sign,
        String value,
        String originalValue,
        SqlParserPos pos) {
      BigDecimal day;
  
      // validate as DAY(startPrecision), e.g. 'DD'
      String intervalPattern = "(\\d+)";
  
      Matcher m = Pattern.compile(intervalPattern).matcher(value);
      if (m.matches()) {
        // Break out  field values
        try {
          day = parseField(m, 1);
        } catch (NumberFormatException e) {
          throw invalidValueException(pos, originalValue);
        }
  
        // Validate individual fields
        checkLeadFieldInRange(typeSystem, sign, day, TimeUnit.DAY, pos);
  
        // package values up for return
        return fillDayTimeIntervalValueArray(sign, day, ZERO, ZERO, ZERO, ZERO);
      } else {
        throw invalidValueException(pos, originalValue);
      }
    }
  
    /**
     * Validates an INTERVAL literal against a DAY TO HOUR interval qualifier.
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    private int[] evaluateIntervalLiteralAsDayToHour(
        RelDataTypeSystem typeSystem, int sign,
        String value,
        String originalValue,
        SqlParserPos pos) {
      BigDecimal day;
      BigDecimal hour;
  
      // validate as DAY(startPrecision) TO HOUR, e.g. 'DD HH'
      String intervalPattern = "(\\d+) (\\d{1,2})";
  
      Matcher m = Pattern.compile(intervalPattern).matcher(value);
      if (m.matches()) {
        // Break out  field values
        try {
          day = parseField(m, 1);
          hour = parseField(m, 2);
        } catch (NumberFormatException e) {
          throw invalidValueException(pos, originalValue);
        }
  
        // Validate individual fields
        checkLeadFieldInRange(typeSystem, sign, day, TimeUnit.DAY, pos);
        if (!isSecondaryFieldInRange(hour, TimeUnit.HOUR)) {
          throw invalidValueException(pos, originalValue);
        }
  
        // package values up for return
        return fillDayTimeIntervalValueArray(sign, day, hour, ZERO, ZERO, ZERO);
      } else {
        throw invalidValueException(pos, originalValue);
      }
    }
  
    /**
     * Validates an INTERVAL literal against a DAY TO MINUTE interval qualifier.
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    private int[] evaluateIntervalLiteralAsDayToMinute(
        RelDataTypeSystem typeSystem, int sign,
        String value,
        String originalValue,
        SqlParserPos pos) {
      BigDecimal day;
      BigDecimal hour;
      BigDecimal minute;
  
      // validate as DAY(startPrecision) TO MINUTE, e.g. 'DD HH:MM'
      String intervalPattern = "(\\d+) (\\d{1,2}):(\\d{1,2})";
  
      Matcher m = Pattern.compile(intervalPattern).matcher(value);
      if (m.matches()) {
        // Break out  field values
        try {
          day = parseField(m, 1);
          hour = parseField(m, 2);
          minute = parseField(m, 3);
        } catch (NumberFormatException e) {
          throw invalidValueException(pos, originalValue);
        }
  
        // Validate individual fields
        checkLeadFieldInRange(typeSystem, sign, day, TimeUnit.DAY, pos);
        if (!isSecondaryFieldInRange(hour, TimeUnit.HOUR)
            || !isSecondaryFieldInRange(minute, TimeUnit.MINUTE)) {
          throw invalidValueException(pos, originalValue);
        }
  
        // package values up for return
        return fillDayTimeIntervalValueArray(sign, day, hour, minute, ZERO, ZERO);
      } else {
        throw invalidValueException(pos, originalValue);
      }
    }
  
    /**
     * Validates an INTERVAL literal against a DAY TO SECOND interval qualifier.
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    private int[] evaluateIntervalLiteralAsDayToSecond(
        RelDataTypeSystem typeSystem, int sign,
        String value,
        String originalValue,
        SqlParserPos pos) {
      BigDecimal day;
      BigDecimal hour;
      BigDecimal minute;
      BigDecimal second;
      BigDecimal secondFrac;
      boolean hasFractionalSecond;
  
      // validate as DAY(startPrecision) TO MINUTE,
      // e.g. 'DD HH:MM:SS' or 'DD HH:MM:SS.SSS'
      // Note: must check two patterns, since fractional second is optional
      final int fractionalSecondPrecision =
          getFractionalSecondPrecision(typeSystem);
      String intervalPatternWithFracSec =
          "(\\d+) (\\d{1,2}):(\\d{1,2}):(\\d{1,2})\\.(\\d{0,"
          + fractionalSecondPrecision + "})";
      String intervalPatternWithoutFracSec =
          "(\\d+) (\\d{1,2}):(\\d{1,2}):(\\d{1,2})";
  
      Matcher m = Pattern.compile(intervalPatternWithFracSec).matcher(value);
      if (m.matches()) {
        hasFractionalSecond = true;
      } else {
        m = Pattern.compile(intervalPatternWithoutFracSec).matcher(value);
        hasFractionalSecond = false;
      }
  
      if (m.matches()) {
        // Break out  field values
        try {
          day = parseField(m, 1);
          hour = parseField(m, 2);
          minute = parseField(m, 3);
          second = parseField(m, 4);
        } catch (NumberFormatException e) {
          throw invalidValueException(pos, originalValue);
        }
  
        if (hasFractionalSecond) {
          secondFrac = normalizeSecondFraction(castNonNull(m.group(5)));
        } else {
          secondFrac = ZERO;
        }
  
        // Validate individual fields
        checkLeadFieldInRange(typeSystem, sign, day, TimeUnit.DAY, pos);
        if (!isSecondaryFieldInRange(hour, TimeUnit.HOUR)
            || !isSecondaryFieldInRange(minute, TimeUnit.MINUTE)
            || !isSecondaryFieldInRange(second, TimeUnit.SECOND)
            || !isFractionalSecondFieldInRange(secondFrac)) {
          throw invalidValueException(pos, originalValue);
        }
  
        // package values up for return
        return fillDayTimeIntervalValueArray(
            sign,
            day,
            hour,
            minute,
            second,
            secondFrac);
      } else {
        throw invalidValueException(pos, originalValue);
      }
    }
  
    /**
     * Validates an INTERVAL literal against an HOUR interval qualifier.
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    private int[] evaluateIntervalLiteralAsHour(
        RelDataTypeSystem typeSystem, int sign,
        String value,
        String originalValue,
        SqlParserPos pos) {
      BigDecimal hour;
  
      // validate as HOUR(startPrecision), e.g. 'HH'
      String intervalPattern = "(\\d+)";
  
      Matcher m = Pattern.compile(intervalPattern).matcher(value);
      if (m.matches()) {
        // Break out  field values
        try {
          hour = parseField(m, 1);
        } catch (NumberFormatException e) {
          throw invalidValueException(pos, originalValue);
        }
  
        // Validate individual fields
        checkLeadFieldInRange(typeSystem, sign, hour, TimeUnit.HOUR, pos);
  
        // package values up for return
        return fillDayTimeIntervalValueArray(sign, ZERO, hour, ZERO, ZERO, ZERO);
      } else {
        throw invalidValueException(pos, originalValue);
      }
    }
  
    /**
     * Validates an INTERVAL literal against an HOUR TO MINUTE interval
     * qualifier.
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    private int[] evaluateIntervalLiteralAsHourToMinute(
        RelDataTypeSystem typeSystem, int sign,
        String value,
        String originalValue,
        SqlParserPos pos) {
      BigDecimal hour;
      BigDecimal minute;
  
      // validate as HOUR(startPrecision) TO MINUTE, e.g. 'HH:MM'
      String intervalPattern = "(\\d+):(\\d{1,2})";
  
      Matcher m = Pattern.compile(intervalPattern).matcher(value);
      if (m.matches()) {
        // Break out  field values
        try {
          hour = parseField(m, 1);
          minute = parseField(m, 2);
        } catch (NumberFormatException e) {
          throw invalidValueException(pos, originalValue);
        }
  
        // Validate individual fields
        checkLeadFieldInRange(typeSystem, sign, hour, TimeUnit.HOUR, pos);
        if (!isSecondaryFieldInRange(minute, TimeUnit.MINUTE)) {
          throw invalidValueException(pos, originalValue);
        }
  
        // package values up for return
        return fillDayTimeIntervalValueArray(sign, ZERO, hour, minute, ZERO, ZERO);
      } else {
        throw invalidValueException(pos, originalValue);
      }
    }
  
    /**
     * Validates an INTERVAL literal against an HOUR TO SECOND interval
     * qualifier.
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    private int[] evaluateIntervalLiteralAsHourToSecond(
        RelDataTypeSystem typeSystem, int sign,
        String value,
        String originalValue,
        SqlParserPos pos) {
      BigDecimal hour;
      BigDecimal minute;
      BigDecimal second;
      BigDecimal secondFrac;
      boolean hasFractionalSecond;
  
      // validate as HOUR(startPrecision) TO SECOND,
      // e.g. 'HH:MM:SS' or 'HH:MM:SS.SSS'
      // Note: must check two patterns, since fractional second is optional
      final int fractionalSecondPrecision =
          getFractionalSecondPrecision(typeSystem);
      String intervalPatternWithFracSec =
          "(\\d+):(\\d{1,2}):(\\d{1,2})\\.(\\d{0,"
          + fractionalSecondPrecision + "})";
      String intervalPatternWithoutFracSec =
          "(\\d+):(\\d{1,2}):(\\d{1,2})";
  
      Matcher m = Pattern.compile(intervalPatternWithFracSec).matcher(value);
      if (m.matches()) {
        hasFractionalSecond = true;
      } else {
        m = Pattern.compile(intervalPatternWithoutFracSec).matcher(value);
        hasFractionalSecond = false;
      }
  
      if (m.matches()) {
        // Break out  field values
        try {
          hour = parseField(m, 1);
          minute = parseField(m, 2);
          second = parseField(m, 3);
        } catch (NumberFormatException e) {
          throw invalidValueException(pos, originalValue);
        }
  
        if (hasFractionalSecond) {
          secondFrac = normalizeSecondFraction(castNonNull(m.group(4)));
        } else {
          secondFrac = ZERO;
        }
  
        // Validate individual fields
        checkLeadFieldInRange(typeSystem, sign, hour, TimeUnit.HOUR, pos);
        if (!isSecondaryFieldInRange(minute, TimeUnit.MINUTE)
            || !isSecondaryFieldInRange(second, TimeUnit.SECOND)
            || !isFractionalSecondFieldInRange(secondFrac)) {
          throw invalidValueException(pos, originalValue);
        }
  
        // package values up for return
        return fillDayTimeIntervalValueArray(
            sign,
            ZERO,
            hour,
            minute,
            second,
            secondFrac);
      } else {
        throw invalidValueException(pos, originalValue);
      }
    }
  
    /**
     * Validates an INTERVAL literal against an MINUTE interval qualifier.
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    private int[] evaluateIntervalLiteralAsMinute(
        RelDataTypeSystem typeSystem, int sign,
        String value,
        String originalValue,
        SqlParserPos pos) {
      BigDecimal minute;
  
      // validate as MINUTE(startPrecision), e.g. 'MM'
      String intervalPattern = "(\\d+)";
  
      Matcher m = Pattern.compile(intervalPattern).matcher(value);
      if (m.matches()) {
        // Break out  field values
        try {
          minute = parseField(m, 1);
        } catch (NumberFormatException e) {
          throw invalidValueException(pos, originalValue);
        }
  
        // Validate individual fields
        checkLeadFieldInRange(typeSystem, sign, minute, TimeUnit.MINUTE, pos);
  
        // package values up for return
        return fillDayTimeIntervalValueArray(sign, ZERO, ZERO, minute, ZERO, ZERO);
      } else {
        throw invalidValueException(pos, originalValue);
      }
    }
  
    /**
     * Validates an INTERVAL literal against an MINUTE TO SECOND interval
     * qualifier.
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    private int[] evaluateIntervalLiteralAsMinuteToSecond(
        RelDataTypeSystem typeSystem, int sign,
        String value,
        String originalValue,
        SqlParserPos pos) {
      BigDecimal minute;
      BigDecimal second;
      BigDecimal secondFrac;
      boolean hasFractionalSecond;
  
      // validate as MINUTE(startPrecision) TO SECOND,
      // e.g. 'MM:SS' or 'MM:SS.SSS'
      // Note: must check two patterns, since fractional second is optional
      final int fractionalSecondPrecision =
          getFractionalSecondPrecision(typeSystem);
      String intervalPatternWithFracSec =
          "(\\d+):(\\d{1,2})\\.(\\d{0," + fractionalSecondPrecision + "})";
      String intervalPatternWithoutFracSec =
          "(\\d+):(\\d{1,2})";
  
      Matcher m = Pattern.compile(intervalPatternWithFracSec).matcher(value);
      if (m.matches()) {
        hasFractionalSecond = true;
      } else {
        m = Pattern.compile(intervalPatternWithoutFracSec).matcher(value);
        hasFractionalSecond = false;
      }
  
      if (m.matches()) {
        // Break out  field values
        try {
          minute = parseField(m, 1);
          second = parseField(m, 2);
        } catch (NumberFormatException e) {
          throw invalidValueException(pos, originalValue);
        }
  
        if (hasFractionalSecond) {
          secondFrac = normalizeSecondFraction(castNonNull(m.group(3)));
        } else {
          secondFrac = ZERO;
        }
  
        // Validate individual fields
        checkLeadFieldInRange(typeSystem, sign, minute, TimeUnit.MINUTE, pos);
        if (!isSecondaryFieldInRange(second, TimeUnit.SECOND)
            || !isFractionalSecondFieldInRange(secondFrac)) {
          throw invalidValueException(pos, originalValue);
        }
  
        // package values up for return
        return fillDayTimeIntervalValueArray(
            sign,
            ZERO,
            ZERO,
            minute,
            second,
            secondFrac);
      } else {
        throw invalidValueException(pos, originalValue);
      }
    }
  
    /**
     * Validates an INTERVAL literal against an SECOND interval qualifier.
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    private int[] evaluateIntervalLiteralAsSecond(
        RelDataTypeSystem typeSystem,
        int sign,
        String value,
        String originalValue,
        SqlParserPos pos) {
      BigDecimal second;
      BigDecimal secondFrac;
      boolean hasFractionalSecond;
  
      // validate as SECOND(startPrecision, fractionalSecondPrecision)
      // e.g. 'SS' or 'SS.SSS'
      // Note: must check two patterns, since fractional second is optional
      final int fractionalSecondPrecision =
          getFractionalSecondPrecision(typeSystem);
      String intervalPatternWithFracSec =
          "(\\d+)\\.(\\d{0," + fractionalSecondPrecision + "})";
      String intervalPatternWithoutFracSec =
          "(\\d+)";
  
      Matcher m = Pattern.compile(intervalPatternWithFracSec).matcher(value);
      if (m.matches()) {
        hasFractionalSecond = true;
      } else {
        m = Pattern.compile(intervalPatternWithoutFracSec).matcher(value);
        hasFractionalSecond = false;
      }
  
      if (m.matches()) {
        // Break out  field values
        try {
          second = parseField(m, 1);
        } catch (NumberFormatException e) {
          throw invalidValueException(pos, originalValue);
        }
  
        if (hasFractionalSecond) {
          secondFrac = normalizeSecondFraction(castNonNull(m.group(2)));
        } else {
          secondFrac = ZERO;
        }
  
        // Validate individual fields
        checkLeadFieldInRange(typeSystem, sign, second, TimeUnit.SECOND, pos);
        if (!isFractionalSecondFieldInRange(secondFrac)) {
          throw invalidValueException(pos, originalValue);
        }
  
        // package values up for return
        return fillDayTimeIntervalValueArray(
            sign, ZERO, ZERO, ZERO, second, secondFrac);
      } else {
        throw invalidValueException(pos, originalValue);
      }
    }
  
    /**
     * Validates an INTERVAL literal according to the rules specified by the
     * interval qualifier. The assumption is made that the interval qualifier has
     * been validated prior to calling this method. Evaluating against an
     * invalid qualifier could lead to strange results.
     *
     * @return field values, never null
     *
     * @throws org.apache.calcite.runtime.CalciteContextException if the interval
     * value is illegal
     */
    public int[] evaluateIntervalLiteral(String value, SqlParserPos pos,
        RelDataTypeSystem typeSystem) {
      // save original value for if we have to throw
      final String value0 = value;
  
      // First strip off any leading whitespace
      value = value.trim();
  
      // check if the sign was explicitly specified.  Record
      // the explicit or implicit sign, and strip it off to
      // simplify pattern matching later.
      final int sign = getIntervalSign(value);
      value = stripLeadingSign(value);
  
      // If we have an empty or null literal at this point,
      // it's illegal.  Complain and bail out.
      if (Util.isNullOrEmpty(value)) {
        throw invalidValueException(pos, value0);
      }
  
      // Validate remaining string according to the pattern
      // that corresponds to the start and end units as
      // well as explicit or implicit precision and range.
      switch (timeUnitRange) {
      case YEAR:
        return evaluateIntervalLiteralAsYear(typeSystem, sign, value, value0,
            pos);
      case YEAR_TO_MONTH:
        return evaluateIntervalLiteralAsYearToMonth(typeSystem, sign, value,
            value0, pos);
      case MONTH:
        return evaluateIntervalLiteralAsMonth(typeSystem, sign, value, value0,
            pos);
      case QUARTER:
        return evaluateIntervalLiteralAsQuarter(typeSystem, sign, value, value0,
            pos);
      case WEEK:
        return evaluateIntervalLiteralAsWeek(typeSystem, sign, value, value0,
            pos);
      case DAY:
        return evaluateIntervalLiteralAsDay(typeSystem, sign, value, value0, pos);
      case DAY_TO_HOUR:
        return evaluateIntervalLiteralAsDayToHour(typeSystem, sign, value, value0,
            pos);
      case DAY_TO_MINUTE:
        return evaluateIntervalLiteralAsDayToMinute(typeSystem, sign, value,
            value0, pos);
      case DAY_TO_SECOND:
        return evaluateIntervalLiteralAsDayToSecond(typeSystem, sign, value,
            value0, pos);
      case HOUR:
        return evaluateIntervalLiteralAsHour(typeSystem, sign, value, value0,
            pos);
      case HOUR_TO_MINUTE:
        return evaluateIntervalLiteralAsHourToMinute(typeSystem, sign, value,
            value0, pos);
      case HOUR_TO_SECOND:
        return evaluateIntervalLiteralAsHourToSecond(typeSystem, sign, value,
            value0, pos);
      case MINUTE:
        return evaluateIntervalLiteralAsMinute(typeSystem, sign, value, value0,
            pos);
      case MINUTE_TO_SECOND:
        return evaluateIntervalLiteralAsMinuteToSecond(typeSystem, sign, value,
            value0, pos);
      case SECOND:
        return evaluateIntervalLiteralAsSecond(typeSystem, sign, value, value0,
            pos);
      default:
        throw invalidValueException(pos, value0);
      }
    }
  
    private static BigDecimal parseField(Matcher m, int i) {
      return new BigDecimal(castNonNull(m.group(i)));
    }
  
    private CalciteContextException invalidValueException(SqlParserPos pos,
        String value) {
      return SqlUtil.newContextException(pos,
          RESOURCE.unsupportedIntervalLiteral("'" + value + "'",
              "INTERVAL " + this));
    }
  
    private static CalciteContextException fieldExceedsPrecisionException(
        SqlParserPos pos, int sign, BigDecimal value, TimeUnit type,
        int precision) {
      if (sign == -1) {
        value = value.negate();
      }
      return SqlUtil.newContextException(pos,
          RESOURCE.intervalFieldExceedsPrecision(
              value, type.name() + "(" + precision + ")"));
    }
  
    /** Converts a {@link SqlIntervalQualifier} to a
     * {@link org.apache.calcite.sql.SqlIdentifier} if it is a time frame
     * reference.
     *
     * <p>Helps with unparsing of EXTRACT, FLOOR, CEIL functions. */
    public static SqlNode asIdentifier(SqlNode node) {
      if (node instanceof SqlIntervalQualifier) {
        SqlIntervalQualifier intervalQualifier = (SqlIntervalQualifier) node;
        if (intervalQualifier.timeFrameName != null) {
          return new SqlIdentifier(intervalQualifier.timeFrameName,
              node.getParserPosition());
        }
      }
      return node;
    }
  }
  