org.apache.calcite.sql.SqlIntervalQualifier Maven / Gradle / Ivy
Show all versions of calcite-core Show documentation
/*
* 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.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 java.math.BigDecimal;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.apache.calcite.util.Static.RESOURCE;
/**
* Represents an INTERVAL qualifier.
*
* INTERVAL qualifier is defined as follows:
*
*
* <interval qualifier> ::=
* <start field> TO <end field>
* | <single datetime field>
* <start field> ::=
* <non-second primary datetime field>
* [ <left paren> <interval leading field precision>
* <right paren> ]
* <end field> ::=
* <non-second primary datetime field>
* | SECOND [ <left paren>
* <interval fractional seconds precision> <right paren> ]
* <single datetime field> ::=
* <non-second primary datetime field>
* [ <left paren> <interval leading field precision>
* <right paren> ]
* | SECOND [ <left paren>
* <interval leading field precision>
* [ <comma> <interval fractional seconds precision> ]
* <right paren> ]
* <primary datetime field> ::=
* <non-second primary datetime field>
* | SECOND
* <non-second primary datetime field> ::= YEAR | MONTH | DAY | HOUR
* | MINUTE
* <interval fractional seconds precision> ::=
* <unsigned integer>
* <interval leading field precision> ::= <unsigned integer>
*
*
* Examples include:
*
*
* INTERVAL '1:23:45.678' HOUR TO SECOND
* INTERVAL '1 2:3:4' DAY TO SECOND
* INTERVAL '1 2:3:4' DAY(4) TO SECOND(4)
*
*
* 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);
//~ Instance fields --------------------------------------------------------
private final int startPrecision;
public final TimeUnitRange timeUnitRange;
private final int fractionalSecondPrecision;
//~ Constructors -----------------------------------------------------------
public SqlIntervalQualifier(
TimeUnit startUnit,
int startPrecision,
TimeUnit endUnit,
int fractionalSecondPrecision,
SqlParserPos pos) {
super(pos);
if (endUnit == startUnit) {
endUnit = null;
}
this.timeUnitRange =
TimeUnitRange.of(Objects.requireNonNull(startUnit), endUnit);
this.startPrecision = startPrecision;
this.fractionalSecondPrecision = fractionalSecondPrecision;
}
public SqlIntervalQualifier(
TimeUnit startUnit,
TimeUnit endUnit,
SqlParserPos pos) {
this(
startUnit,
RelDataType.PRECISION_NOT_SPECIFIED,
endUnit,
RelDataType.PRECISION_NOT_SPECIFIED,
pos);
}
//~ Methods ----------------------------------------------------------------
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);
}
}
public void validate(
SqlValidator validator,
SqlValidatorScope scope) {
validator.validateIntervalQualifier(this);
}
public R accept(SqlVisitor visitor) {
return visitor.visit(this);
}
public boolean equalsDeep(SqlNode node, Litmus litmus) {
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.getDefaultPrecision(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);
}
public SqlNode clone(SqlParserPos pos) {
return new SqlIntervalQualifier(timeUnitRange.startUnit, startPrecision,
timeUnitRange.endUnit, fractionalSecondPrecision, pos);
}
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.
*
* 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;
}
/**
* @return 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 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, 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 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 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 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 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 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 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{1,"
+ 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(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{1,"
+ 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(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{1," + 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(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{1," + 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(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 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 BigDecimal parseField(Matcher m, int i) {
return new BigDecimal(m.group(i));
}
private CalciteContextException invalidValueException(SqlParserPos pos,
String value) {
return SqlUtil.newContextException(pos,
RESOURCE.unsupportedIntervalLiteral(
"'" + value + "'", "INTERVAL " + toString()));
}
private 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 + ")"));
}
}
// End SqlIntervalQualifier.java