/* * 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> * * <interval qualifier> ::=<br> *    <start field> TO <end field><br> *   | <single datetime field><br> * <start field> ::=<br> *    <non-second primary datetime field><br> *    [ <left paren> <interval leading field precision> * <right paren> ]<br> * <end field> ::=<br> *    <non-second primary datetime field><br> *   | SECOND [ <left paren> * <interval fractional seconds precision> <right paren> ]<br> * <single datetime field> ::=<br> *   <non-second primary datetime field><br> *   [ <left paren> <interval leading field precision> * <right paren> ]<br> *   | SECOND [ <left paren> * <interval leading field precision><br> *   [ <comma> <interval fractional seconds precision> ] * <right paren> ]<br> * <primary datetime field> ::=<br> *   <non-second primary datetime field><br> *   | SECOND<br> * <non-second primary datetime field> ::= YEAR | MONTH | DAY | HOUR * | MINUTE<br> * <interval fractional seconds precision> ::= * <unsigned integer><br> * <interval leading field precision> ::= <unsigned integer> * * </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