blob: 406ed57e9a9bf8911129531755e0c6359c098d52 [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.eigenbase.sql;
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.*;
import org.eigenbase.reltype.RelDataType;
import org.eigenbase.reltype.RelDataTypeSystem;
import org.eigenbase.sql.parser.*;
import org.eigenbase.sql.type.*;
import org.eigenbase.sql.util.*;
import org.eigenbase.sql.validate.*;
import org.eigenbase.util.*;
import org.eigenbase.util14.DateTimeUtil;
import net.hydromatic.optiq.runtime.SqlFunctions;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import static org.eigenbase.util.Static.RESOURCE;
/**
* 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>
*
* 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);
//~ Enums ------------------------------------------------------------------
/**
* Enumeration of time units used to construct an interval.
*/
public enum TimeUnit implements SqlLiteral.SqlSymbol {
YEAR(true, ' ', 12 /* months */, null),
MONTH(true, '-', 1 /* months */, BigDecimal.valueOf(12)),
DAY(false, '-', DateTimeUtil.MILLIS_PER_DAY, null),
HOUR(false, ' ', DateTimeUtil.MILLIS_PER_HOUR, BigDecimal.valueOf(24)),
MINUTE(false, ':', DateTimeUtil.MILLIS_PER_MINUTE, BigDecimal.valueOf(60)),
SECOND(false, ':', DateTimeUtil.MILLIS_PER_SECOND, BigDecimal.valueOf(60));
public final boolean yearMonth;
public final char separator;
public final long multiplier;
private final BigDecimal limit;
private static final TimeUnit[] CACHED_VALUES = values();
private TimeUnit(
boolean yearMonth,
char separator,
long multiplier,
BigDecimal limit) {
this.yearMonth = yearMonth;
this.separator = separator;
this.multiplier = multiplier;
this.limit = limit;
}
/**
* Returns the TimeUnit associated with an ordinal. The value returned
* is null if the ordinal is not a member of the TimeUnit enumeration.
*/
public static TimeUnit getValue(int ordinal) {
return ordinal < 0 || ordinal >= CACHED_VALUES.length
? null
: CACHED_VALUES[ordinal];
}
public static final String GET_VALUE_METHOD_NAME = "getValue";
/**
* Returns whether a given value is valid for a field of this time unit.
*
* @param field Field value
* @return Whether value
*/
public boolean isValidValue(BigDecimal field) {
return field.compareTo(ZERO) >= 0
&& (limit == null
|| field.compareTo(limit) < 0);
}
}
private enum TimeUnitRange {
YEAR(TimeUnit.YEAR, null),
YEAR_TO_MONTH(TimeUnit.YEAR, TimeUnit.MONTH),
MONTH(TimeUnit.MONTH, null),
DAY(TimeUnit.DAY, null),
DAY_TO_HOUR(TimeUnit.DAY, TimeUnit.HOUR),
DAY_TO_MINUTE(TimeUnit.DAY, TimeUnit.MINUTE),
DAY_TO_SECOND(TimeUnit.DAY, TimeUnit.SECOND),
HOUR(TimeUnit.HOUR, null),
HOUR_TO_MINUTE(TimeUnit.HOUR, TimeUnit.MINUTE),
HOUR_TO_SECOND(TimeUnit.HOUR, TimeUnit.SECOND),
MINUTE(TimeUnit.MINUTE, null),
MINUTE_TO_SECOND(TimeUnit.MINUTE, TimeUnit.SECOND),
SECOND(TimeUnit.SECOND, null);
private final TimeUnit startUnit;
private final TimeUnit endUnit;
private static final Map<Pair<TimeUnit, TimeUnit>, TimeUnitRange> MAP;
static {
ImmutableMap.Builder<Pair<TimeUnit, TimeUnit>, TimeUnitRange> builder =
ImmutableMap.builder();
for (TimeUnitRange value : values()) {
builder.put(Pair.of(value.startUnit, value.endUnit), value);
}
MAP = builder.build();
}
/**
* Creates a TimeUnitRange.
*
* @param startUnit Start time unit
* @param endUnit End time unit
*/
TimeUnitRange(TimeUnit startUnit, TimeUnit endUnit) {
assert startUnit != null;
this.startUnit = startUnit;
this.endUnit = endUnit;
}
/**
* Returns a TimeUnitRange with a given start and end unit.
*
* @param startUnit Start unit
* @param endUnit End unit
* @return Time unit range, or null if not valid
*/
public static TimeUnitRange of(
TimeUnit startUnit, TimeUnit endUnit) {
return MAP.get(new Pair<TimeUnit, TimeUnit>(startUnit, endUnit));
}
}
//~ Instance fields --------------------------------------------------------
private final int startPrecision;
private final TimeUnitRange timeUnitRange;
private final int fractionalSecondPrecision;
//~ Constructors -----------------------------------------------------------
public SqlIntervalQualifier(
TimeUnit startUnit,
int startPrecision,
TimeUnit endUnit,
int fractionalSecondPrecision,
SqlParserPos pos) {
super(pos);
this.timeUnitRange =
TimeUnitRange.of(Preconditions.checkNotNull(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() {
return isYearMonth()
? SqlTypeName.INTERVAL_YEAR_MONTH
: SqlTypeName.INTERVAL_DAY_TIME;
}
public SqlFunctions.TimeUnitRange foo() {
return SqlFunctions.TimeUnitRange.valueOf(timeUnitRange.name());
}
public void validate(
SqlValidator validator,
SqlValidatorScope scope) {
validator.validateIntervalQualifier(this);
}
public <R> R accept(SqlVisitor<R> visitor) {
return visitor.visit(this);
}
public boolean equalsDeep(SqlNode node, boolean fail) {
final String thisString = this.toString();
final String thatString = node.toString();
if (!thisString.equals(thatString)) {
assert !fail : this + "!=" + node;
return false;
}
return true;
}
public int getStartPrecision(RelDataTypeSystem typeSystem) {
if (startPrecision == RelDataType.PRECISION_NOT_SPECIFIED) {
return typeSystem.getDefaultPrecision(typeName());
} else {
return startPrecision;
}
}
public int getStartPrecisionPreservingDefault() {
return startPrecision;
}
private 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 startPrecision;
}
}
private 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;
}
public SqlNode clone(SqlParserPos pos) {
return new SqlIntervalQualifier(timeUnitRange.startUnit, startPrecision,
timeUnitRange.endUnit, fractionalSecondPrecision, pos);
}
public void unparse(
SqlWriter writer,
int leftPrec,
int rightPrec) {
unparse(RelDataTypeSystem.DEFAULT, writer);
}
public void unparse(RelDataTypeSystem typeSystem, SqlWriter writer) {
final String start = timeUnitRange.startUnit.name();
final int fractionalSecondPrecision =
getFractionalSecondPrecision(typeSystem);
final int startPrecision = getStartPrecision(typeSystem);
if (timeUnitRange.startUnit == TimeUnit.SECOND) {
if (!useDefaultFractionalSecondPrecision()) {
final SqlWriter.Frame frame = writer.startFunCall(start);
writer.print(startPrecision);
writer.sep(",", true);
writer.print(getFractionalSecondPrecision(typeSystem));
writer.endList(frame);
} else if (!useDefaultStartPrecision()) {
final SqlWriter.Frame frame = writer.startFunCall(start);
writer.print(startPrecision);
writer.endList(frame);
} else {
writer.keyword(start);
}
} else {
if (!useDefaultStartPrecision()) {
final SqlWriter.Frame frame = writer.startFunCall(start);
writer.print(startPrecision);
writer.endList(frame);
} else {
writer.keyword(start);
}
if (null != timeUnitRange.endUnit) {
writer.keyword("TO");
final String end = timeUnitRange.endUnit.name();
if ((TimeUnit.SECOND == timeUnitRange.endUnit)
&& (!useDefaultFractionalSecondPrecision())) {
final SqlWriter.Frame frame = writer.startFunCall(end);
writer.print(fractionalSecondPrecision);
writer.endList(frame);
} else {
writer.keyword(end);
}
}
}
}
/**
* Does this interval have a single datetime field
*
* Return true not of form unit TO unit.
*/
public boolean isSingleDatetimeField() {
return timeUnitRange.endUnit == null;
}
public final boolean isYearMonth() {
return timeUnitRange.startUnit.yearMonth;
}
/**
* @return 1 or -1
*/
private 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 EigenbaseContextException 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 EigenbaseContextException 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 EigenbaseContextException 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 EigenbaseContextException 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 EigenbaseContextException 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 EigenbaseContextException 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 EigenbaseContextException 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 EigenbaseContextException 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 EigenbaseContextException 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 EigenbaseContextException 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 EigenbaseContextException 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 EigenbaseContextException 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 EigenbaseContextException 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 EigenbaseContextException 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 EigenbaseContextException invalidValueException(SqlParserPos pos,
String value) {
return SqlUtil.newContextException(pos,
RESOURCE.unsupportedIntervalLiteral(
"'" + value + "'", "INTERVAL " + toString()));
}
private EigenbaseContextException 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