blob: b3a29c715db54a52ac1888e317e0822f31a0e672 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.calcite.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.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 typeName().getDefaultScale();
} 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;
}
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.
assert unit != null;
switch (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[] fillIntervalValueArray(
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[] fillIntervalValueArray(
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 fillIntervalValueArray(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 fillIntervalValueArray(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 fillIntervalValueArray(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
return fillIntervalValueArray(sign, ZERO, quarter);
} 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
return fillIntervalValueArray(sign, ZERO, week);
} 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 fillIntervalValueArray(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 fillIntervalValueArray(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 fillIntervalValueArray(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 fillIntervalValueArray(
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 fillIntervalValueArray(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 fillIntervalValueArray(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 fillIntervalValueArray(
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 fillIntervalValueArray(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 fillIntervalValueArray(
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 fillIntervalValueArray(
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 " + toString()));
}
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;
}
}