Skip to content

Commit

Permalink
[CALCITE-5424] Customize handling of literals based on type system
Browse files Browse the repository at this point in the history
Literals introduced by the keyword DATE, TIME, DATETIME,
TIMESTAMP, TIMESTAMP WITH LOCAL TIME ZONE are represented by
the parser by new class SqlUnknownLiteral. Determining the
actual type is deferred until validation time; the validator
determines the actual type based on the type system's type
alias map, and then validates the character string.

Close #3036

Co-authored-by: Julian Hyde <[email protected]>
Co-authored-by: Oliver Lee <[email protected]>
  • Loading branch information
julianhyde and olivrlee committed Jan 25, 2023
1 parent d9eeba4 commit f8f8a51
Show file tree
Hide file tree
Showing 19 changed files with 268 additions and 39 deletions.
11 changes: 6 additions & 5 deletions babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -213,15 +213,16 @@ class BabelParserTest extends SqlParserTest {
/** PostgreSQL and Redshift allow TIMESTAMP literals that contain only a
* date part. */
@Test void testShortTimestampLiteral() {
// Parser doesn't actually check the contents of the string. The validator
// will convert it to '1969-07-20 00:00:00', when it has decided that
// TIMESTAMP maps to the TIMESTAMP type.
sql("select timestamp '1969-07-20'")
.ok("SELECT TIMESTAMP '1969-07-20 00:00:00'");
.ok("SELECT TIMESTAMP '1969-07-20'");
// PostgreSQL allows the following. We should too.
sql("select ^timestamp '1969-07-20 1:2'^")
.fails("Illegal TIMESTAMP literal '1969-07-20 1:2': not in format "
+ "'yyyy-MM-dd HH:mm:ss'"); // PostgreSQL gives 1969-07-20 01:02:00
.ok("SELECT TIMESTAMP '1969-07-20 1:2'");
sql("select ^timestamp '1969-07-20:23:'^")
.fails("Illegal TIMESTAMP literal '1969-07-20:23:': not in format "
+ "'yyyy-MM-dd HH:mm:ss'"); // PostgreSQL gives 1969-07-20 23:00:00
.ok("SELECT TIMESTAMP '1969-07-20:23:'");
}

/** Tests parsing PostgreSQL-style "::" cast operator. */
Expand Down
16 changes: 13 additions & 3 deletions core/src/main/codegen/templates/Parser.jj
Original file line number Diff line number Diff line change
Expand Up @@ -4609,15 +4609,24 @@ SqlLiteral DateTimeLiteral() :
}
|
<DATE> { s = span(); } p = SimpleStringLiteral() {
return SqlParserUtil.parseDateLiteral(p, s.end(this));
return SqlLiteral.createUnknown("DATE", p, s.end(this));
}
|
<DATETIME> { s = span(); } p = SimpleStringLiteral() {
return SqlLiteral.createUnknown("DATETIME", p, s.end(this));
}
|
<TIME> { s = span(); } p = SimpleStringLiteral() {
return SqlParserUtil.parseTimeLiteral(p, s.end(this));
return SqlLiteral.createUnknown("TIME", p, s.end(this));
}
|
LOOKAHEAD(2)
<TIMESTAMP> { s = span(); } p = SimpleStringLiteral() {
return SqlParserUtil.parseTimestampLiteral(p, s.end(this));
return SqlLiteral.createUnknown("TIMESTAMP", p, s.end(this));
}
|
<TIMESTAMP> { s = span(); } <WITH> <LOCAL> <TIME> <ZONE> p = SimpleStringLiteral() {
return SqlLiteral.createUnknown("TIMESTAMP WITH LOCAL TIME ZONE", p, s.end(this));
}
}

Expand Down Expand Up @@ -7644,6 +7653,7 @@ SqlPostfixOperator PostfixRowOperator() :
| < DATABASE: "DATABASE" >
| < DATE: "DATE" >
| < DATE_TRUNC: "DATE_TRUNC" >
| < DATETIME: "DATETIME" >
| < DATETIME_INTERVAL_CODE: "DATETIME_INTERVAL_CODE" >
| < DATETIME_INTERVAL_PRECISION: "DATETIME_INTERVAL_PRECISION" >
| < DAY: "DAY" >
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1409,7 +1409,7 @@ public static SqlNode toSql(RexLiteral literal) {
return SqlLiteral.createTime(castNonNull(literal.getValueAs(TimeString.class)),
literal.getType().getPrecision(), POS);
case TIMESTAMP:
return SqlLiteral.createTimestamp(
return SqlLiteral.createTimestamp(typeName,
castNonNull(literal.getValueAs(TimestampString.class)),
literal.getType().getPrecision(), POS);
case ANY:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public RexToSqlNodeConverterImpl(RexSqlConvertletTable convertletTable) {
// Timestamp
if (SqlTypeFamily.TIMESTAMP.getTypeNames().contains(
literal.getTypeName())) {
return SqlLiteral.createTimestamp(
return SqlLiteral.createTimestamp(literal.getTypeName(),
requireNonNull(literal.getValueAs(TimestampString.class),
"literal.getValueAs(TimestampString.class)"),
0,
Expand Down
41 changes: 38 additions & 3 deletions core/src/main/java/org/apache/calcite/sql/SqlLiteral.java
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ public static boolean valueMatchesType(
case TIME:
return value instanceof TimeString;
case TIMESTAMP:
case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
return value instanceof TimestampString;
case INTERVAL_YEAR:
case INTERVAL_YEAR_MONTH:
Expand All @@ -229,6 +230,8 @@ public static boolean valueMatchesType(
|| (value instanceof SqlSampleSpec);
case MULTISET:
return true;
case UNKNOWN:
return value instanceof String;
case INTEGER: // not allowed -- use Decimal
case VARCHAR: // not allowed -- use Char
case VARBINARY: // not allowed -- use Binary
Expand Down Expand Up @@ -826,6 +829,28 @@ public RelDataType createSqlType(RelDataTypeFactory typeFactory) {
}
}

/** Creates a literal whose type is unknown until validation time.
* The literal has a tag that looks like a type name, but the tag cannot be
* resolved until validation time, when we have the mapping from type aliases
* to types.
*
* <p>For example,
* <blockquote>{@code
* TIMESTAMP '1969-07-20 22:56:00'
* }</blockquote>
* calls {@code createUnknown("TIMESTAMP", "1969-07-20 22:56:00")}; at
* validate time, we may discover that "TIMESTAMP" maps to the type
* "TIMESTAMP WITH LOCAL TIME ZONE".
*
* @param tag Type name, e.g. "TIMESTAMP", "TIMESTAMP WITH LOCAL TIME ZONE"
* @param value String encoding of the value
* @param pos Parser position
*/
public static SqlLiteral createUnknown(String tag, String value,
SqlParserPos pos) {
return new SqlUnknownLiteral(tag, value, pos);
}

@Deprecated // to be removed before 2.0
public static SqlDateLiteral createDate(
Calendar calendar,
Expand All @@ -844,15 +869,25 @@ public static SqlTimestampLiteral createTimestamp(
Calendar calendar,
int precision,
SqlParserPos pos) {
return createTimestamp(TimestampString.fromCalendarFields(calendar),
precision, pos);
return createTimestamp(SqlTypeName.TIMESTAMP,
TimestampString.fromCalendarFields(calendar), precision, pos);
}

@Deprecated // to be removed before 2.0
public static SqlTimestampLiteral createTimestamp(
TimestampString ts,
int precision,
SqlParserPos pos) {
return new SqlTimestampLiteral(ts, precision, false, pos);
return createTimestamp(SqlTypeName.TIMESTAMP, ts, precision, pos);
}

/** Creates a TIMESTAMP or TIMESTAMP WITH TIME ZONE literal. */
public static SqlTimestampLiteral createTimestamp(
SqlTypeName typeName,
TimestampString ts,
int precision,
SqlParserPos pos) {
return new SqlTimestampLiteral(ts, precision, typeName, pos);
}

@Deprecated // to be removed before 2.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ public class SqlTimestampLiteral extends SqlAbstractDateTimeLiteral {
//~ Constructors -----------------------------------------------------------

SqlTimestampLiteral(TimestampString ts, int precision,
boolean hasTimeZone, SqlParserPos pos) {
super(ts, hasTimeZone, SqlTypeName.TIMESTAMP, precision, pos);
SqlTypeName typeName, SqlParserPos pos) {
super(ts, false, typeName, precision, pos);
Preconditions.checkArgument(this.precision >= 0);
Preconditions.checkArgument(typeName == SqlTypeName.TIMESTAMP
|| typeName == SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE);
}

//~ Methods ----------------------------------------------------------------
Expand All @@ -45,11 +47,11 @@ public class SqlTimestampLiteral extends SqlAbstractDateTimeLiteral {
return new SqlTimestampLiteral(
(TimestampString) Objects.requireNonNull(value, "value"),
precision,
hasTimeZone, pos);
getTypeName(), pos);
}

@Override public String toString() {
return "TIMESTAMP '" + toFormattedString() + "'";
return getTypeName() + " '" + toFormattedString() + "'";
}

/**
Expand Down
64 changes: 64 additions & 0 deletions core/src/main/java/org/apache/calcite/sql/SqlUnknownLiteral.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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.sql.parser.SqlParserPos;
import org.apache.calcite.sql.parser.SqlParserUtil;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.util.NlsString;
import org.apache.calcite.util.Util;

import static java.util.Objects.requireNonNull;

/**
* Literal whose type is not yet known.
*/
public class SqlUnknownLiteral extends SqlLiteral {
public final String tag;

SqlUnknownLiteral(String tag, String value, SqlParserPos pos) {
super(requireNonNull(value, "value"), SqlTypeName.UNKNOWN, pos);
this.tag = requireNonNull(tag, "tag");
}

@Override public String getValue() {
return (String) requireNonNull(super.getValue(), "value");
}

@Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
final NlsString nlsString = new NlsString(getValue(), null, null);
writer.keyword(tag);
writer.literal(nlsString.asSql(true, true, writer.getDialect()));
}


/** Converts this unknown literal to a literal of known type. */
public SqlLiteral resolve(SqlTypeName typeName) {
switch (typeName) {
case DATE:
return SqlParserUtil.parseDateLiteral(getValue(), pos);
case TIME:
return SqlParserUtil.parseTimeLiteral(getValue(), pos);
case TIMESTAMP:
return SqlParserUtil.parseTimestampLiteral(getValue(), pos);
case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
return SqlParserUtil.parseTimestampWithLocalTimeZoneLiteral(getValue(), pos);
default:
throw Util.unexpected(typeName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.apache.calcite.sql.SqlUtil;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.impl.SqlParserImpl;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.util.DateString;
import org.apache.calcite.util.PrecedenceClimbingParser;
import org.apache.calcite.util.TimeString;
Expand Down Expand Up @@ -338,6 +339,17 @@ public static SqlTimeLiteral parseTimeLiteral(String s, SqlParserPos pos) {

public static SqlTimestampLiteral parseTimestampLiteral(String s,
SqlParserPos pos) {
return parseTimestampLiteral(SqlTypeName.TIMESTAMP, s, pos);
}

public static SqlTimestampLiteral parseTimestampWithLocalTimeZoneLiteral(
String s, SqlParserPos pos) {
return parseTimestampLiteral(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, s,
pos);
}

private static SqlTimestampLiteral parseTimestampLiteral(SqlTypeName typeName,
String s, SqlParserPos pos) {
final Format format = Format.get();
DateTimeUtils.PrecisionTime pt = null;
// Allow timestamp literals with and without time fields (as does
Expand All @@ -352,13 +364,13 @@ public static SqlTimestampLiteral parseTimestampLiteral(String s,
}
if (pt == null) {
throw SqlUtil.newContextException(pos,
RESOURCE.illegalLiteral("TIMESTAMP", s,
RESOURCE.illegalLiteral(typeName.getName().replace('_', ' '), s,
RESOURCE.badFormat(DateTimeUtils.TIMESTAMP_FORMAT_STRING).str()));
}
final TimestampString ts =
TimestampString.fromCalendarFields(pt.getCalendar())
.withFraction(pt.getFraction());
return SqlLiteral.createTimestamp(ts, pt.getPrecision(), pos);
return SqlLiteral.createTimestamp(typeName, ts, pt.getPrecision(), pos);
}

public static SqlIntervalLiteral parseIntervalLiteral(SqlParserPos pos,
Expand Down
18 changes: 16 additions & 2 deletions core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,14 @@ public enum SqlTypeName {
return VALUES_MAP.get(name);
}

/** Returns the SqlTypeName value whose name or {@link #getSpaceName()}
* matches the given name, or throws {@link IllegalArgumentException}; never
* returns null. */
public static SqlTypeName lookup(String tag) {
String tag2 = tag.replace(' ', '_');
return valueOf(tag2);
}

public boolean allowsNoPrecNoScale() {
return (signatures & PrecScale.NO_NO) != 0;
}
Expand Down Expand Up @@ -945,7 +953,7 @@ public SqlLiteral createLiteral(Object o, SqlParserPos pos) {
? TimeString.fromCalendarFields((Calendar) o)
: (TimeString) o, 0 /* todo */, pos);
case TIMESTAMP:
return SqlLiteral.createTimestamp(o instanceof Calendar
return SqlLiteral.createTimestamp(this, o instanceof Calendar
? TimestampString.fromCalendarFields((Calendar) o)
: (TimestampString) o, 0 /* todo */, pos);
default:
Expand All @@ -955,7 +963,13 @@ public SqlLiteral createLiteral(Object o, SqlParserPos pos) {

/** Returns the name of this type. */
public String getName() {
return toString();
return name();
}

/** Returns the name of this type, with underscores converted to spaces,
* for example "TIMESTAMP WITH LOCAL TIME ZONE", "DATE". */
public String getSpaceName() {
return name().replace('_', ' ');
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,13 @@ CalciteException handleUnresolvedFunction(SqlCall call,
*/
SqlNode expand(SqlNode expr, SqlValidatorScope scope);

/** Resolves a literal.
*
* <p>Usually returns the literal unchanged, but if the literal is of type
* {@link org.apache.calcite.sql.type.SqlTypeName#UNKNOWN} looks up its type
* and converts to the appropriate literal subclass. */
SqlLiteral resolveLiteral(SqlLiteral literal);

/**
* Returns whether a field is a system field. Such fields may have
* particular properties such as sortedness and nullability.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
import org.apache.calcite.sql.SqlSnapshot;
import org.apache.calcite.sql.SqlSyntax;
import org.apache.calcite.sql.SqlTableFunction;
import org.apache.calcite.sql.SqlUnknownLiteral;
import org.apache.calcite.sql.SqlUnpivot;
import org.apache.calcite.sql.SqlUnresolvedFunction;
import org.apache.calcite.sql.SqlUpdate;
Expand Down Expand Up @@ -6145,6 +6146,26 @@ protected void validateFeature(
assert feature.getProperties().get("FeatureDefinition") != null;
}

@Override public SqlLiteral resolveLiteral(SqlLiteral literal) {
switch (literal.getTypeName()) {
case UNKNOWN:
final SqlUnknownLiteral unknownLiteral = (SqlUnknownLiteral) literal;
final SqlIdentifier identifier =
new SqlIdentifier(unknownLiteral.tag, SqlParserPos.ZERO);
final @Nullable RelDataType type = catalogReader.getNamedType(identifier);
final SqlTypeName typeName;
if (type != null) {
typeName = type.getSqlTypeName();
} else {
typeName = SqlTypeName.lookup(unknownLiteral.tag);
}
return unknownLiteral.resolve(typeName);

default:
return literal;
}
}

public SqlNode expandSelectExpr(SqlNode expr,
SelectScope scope, SqlSelect select) {
final Expander expander = new SelectExpander(this, scope, select);
Expand Down Expand Up @@ -6443,7 +6464,7 @@ private class DeriveTypeVisitor implements SqlVisitor<RelDataType> {
}

@Override public RelDataType visit(SqlLiteral literal) {
return literal.createSqlType(typeFactory);
return resolveLiteral(literal).createSqlType(typeFactory);
}

@Override public RelDataType visit(SqlCall call) {
Expand Down Expand Up @@ -6592,6 +6613,10 @@ public SqlNode go(SqlNode root) {
return expandedExpr;
}

@Override public @Nullable SqlNode visit(SqlLiteral literal) {
return validator.resolveLiteral(literal);
}

@Override protected SqlNode visitScoped(SqlCall call) {
switch (call.getKind()) {
case SCALAR_QUERY:
Expand Down
Loading

0 comments on commit f8f8a51

Please sign in to comment.