Skip to content

Commit a326bd2

Browse files
julianhydeolivrlee
andcommitted
[CALCITE-5424] Customize handling of literals based on type system
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]>
1 parent 7fb8578 commit a326bd2

File tree

19 files changed

+268
-39
lines changed

19 files changed

+268
-39
lines changed

babel/src/test/java/org/apache/calcite/test/BabelParserTest.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -213,15 +213,16 @@ class BabelParserTest extends SqlParserTest {
213213
/** PostgreSQL and Redshift allow TIMESTAMP literals that contain only a
214214
* date part. */
215215
@Test void testShortTimestampLiteral() {
216+
// Parser doesn't actually check the contents of the string. The validator
217+
// will convert it to '1969-07-20 00:00:00', when it has decided that
218+
// TIMESTAMP maps to the TIMESTAMP type.
216219
sql("select timestamp '1969-07-20'")
217-
.ok("SELECT TIMESTAMP '1969-07-20 00:00:00'");
220+
.ok("SELECT TIMESTAMP '1969-07-20'");
218221
// PostgreSQL allows the following. We should too.
219222
sql("select ^timestamp '1969-07-20 1:2'^")
220-
.fails("Illegal TIMESTAMP literal '1969-07-20 1:2': not in format "
221-
+ "'yyyy-MM-dd HH:mm:ss'"); // PostgreSQL gives 1969-07-20 01:02:00
223+
.ok("SELECT TIMESTAMP '1969-07-20 1:2'");
222224
sql("select ^timestamp '1969-07-20:23:'^")
223-
.fails("Illegal TIMESTAMP literal '1969-07-20:23:': not in format "
224-
+ "'yyyy-MM-dd HH:mm:ss'"); // PostgreSQL gives 1969-07-20 23:00:00
225+
.ok("SELECT TIMESTAMP '1969-07-20:23:'");
225226
}
226227

227228
/** Tests parsing PostgreSQL-style "::" cast operator. */

core/src/main/codegen/templates/Parser.jj

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4609,15 +4609,24 @@ SqlLiteral DateTimeLiteral() :
46094609
}
46104610
|
46114611
<DATE> { s = span(); } p = SimpleStringLiteral() {
4612-
return SqlParserUtil.parseDateLiteral(p, s.end(this));
4612+
return SqlLiteral.createUnknown("DATE", p, s.end(this));
4613+
}
4614+
|
4615+
<DATETIME> { s = span(); } p = SimpleStringLiteral() {
4616+
return SqlLiteral.createUnknown("DATETIME", p, s.end(this));
46134617
}
46144618
|
46154619
<TIME> { s = span(); } p = SimpleStringLiteral() {
4616-
return SqlParserUtil.parseTimeLiteral(p, s.end(this));
4620+
return SqlLiteral.createUnknown("TIME", p, s.end(this));
46174621
}
46184622
|
4623+
LOOKAHEAD(2)
46194624
<TIMESTAMP> { s = span(); } p = SimpleStringLiteral() {
4620-
return SqlParserUtil.parseTimestampLiteral(p, s.end(this));
4625+
return SqlLiteral.createUnknown("TIMESTAMP", p, s.end(this));
4626+
}
4627+
|
4628+
<TIMESTAMP> { s = span(); } <WITH> <LOCAL> <TIME> <ZONE> p = SimpleStringLiteral() {
4629+
return SqlLiteral.createUnknown("TIMESTAMP WITH LOCAL TIME ZONE", p, s.end(this));
46214630
}
46224631
}
46234632

@@ -7644,6 +7653,7 @@ SqlPostfixOperator PostfixRowOperator() :
76447653
| < DATABASE: "DATABASE" >
76457654
| < DATE: "DATE" >
76467655
| < DATE_TRUNC: "DATE_TRUNC" >
7656+
| < DATETIME: "DATETIME" >
76477657
| < DATETIME_INTERVAL_CODE: "DATETIME_INTERVAL_CODE" >
76487658
| < DATETIME_INTERVAL_PRECISION: "DATETIME_INTERVAL_PRECISION" >
76497659
| < DAY: "DAY" >

core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1409,7 +1409,7 @@ public static SqlNode toSql(RexLiteral literal) {
14091409
return SqlLiteral.createTime(castNonNull(literal.getValueAs(TimeString.class)),
14101410
literal.getType().getPrecision(), POS);
14111411
case TIMESTAMP:
1412-
return SqlLiteral.createTimestamp(
1412+
return SqlLiteral.createTimestamp(typeName,
14131413
castNonNull(literal.getValueAs(TimestampString.class)),
14141414
literal.getType().getPrecision(), POS);
14151415
case ANY:

core/src/main/java/org/apache/calcite/rex/RexToSqlNodeConverterImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public RexToSqlNodeConverterImpl(RexSqlConvertletTable convertletTable) {
8585
// Timestamp
8686
if (SqlTypeFamily.TIMESTAMP.getTypeNames().contains(
8787
literal.getTypeName())) {
88-
return SqlLiteral.createTimestamp(
88+
return SqlLiteral.createTimestamp(literal.getTypeName(),
8989
requireNonNull(literal.getValueAs(TimestampString.class),
9090
"literal.getValueAs(TimestampString.class)"),
9191
0,

core/src/main/java/org/apache/calcite/sql/SqlLiteral.java

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ public static boolean valueMatchesType(
205205
case TIME:
206206
return value instanceof TimeString;
207207
case TIMESTAMP:
208+
case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
208209
return value instanceof TimestampString;
209210
case INTERVAL_YEAR:
210211
case INTERVAL_YEAR_MONTH:
@@ -229,6 +230,8 @@ public static boolean valueMatchesType(
229230
|| (value instanceof SqlSampleSpec);
230231
case MULTISET:
231232
return true;
233+
case UNKNOWN:
234+
return value instanceof String;
232235
case INTEGER: // not allowed -- use Decimal
233236
case VARCHAR: // not allowed -- use Char
234237
case VARBINARY: // not allowed -- use Binary
@@ -826,6 +829,28 @@ public RelDataType createSqlType(RelDataTypeFactory typeFactory) {
826829
}
827830
}
828831

832+
/** Creates a literal whose type is unknown until validation time.
833+
* The literal has a tag that looks like a type name, but the tag cannot be
834+
* resolved until validation time, when we have the mapping from type aliases
835+
* to types.
836+
*
837+
* <p>For example,
838+
* <blockquote>{@code
839+
* TIMESTAMP '1969-07-20 22:56:00'
840+
* }</blockquote>
841+
* calls {@code createUnknown("TIMESTAMP", "1969-07-20 22:56:00")}; at
842+
* validate time, we may discover that "TIMESTAMP" maps to the type
843+
* "TIMESTAMP WITH LOCAL TIME ZONE".
844+
*
845+
* @param tag Type name, e.g. "TIMESTAMP", "TIMESTAMP WITH LOCAL TIME ZONE"
846+
* @param value String encoding of the value
847+
* @param pos Parser position
848+
*/
849+
public static SqlLiteral createUnknown(String tag, String value,
850+
SqlParserPos pos) {
851+
return new SqlUnknownLiteral(tag, value, pos);
852+
}
853+
829854
@Deprecated // to be removed before 2.0
830855
public static SqlDateLiteral createDate(
831856
Calendar calendar,
@@ -844,15 +869,25 @@ public static SqlTimestampLiteral createTimestamp(
844869
Calendar calendar,
845870
int precision,
846871
SqlParserPos pos) {
847-
return createTimestamp(TimestampString.fromCalendarFields(calendar),
848-
precision, pos);
872+
return createTimestamp(SqlTypeName.TIMESTAMP,
873+
TimestampString.fromCalendarFields(calendar), precision, pos);
849874
}
850875

876+
@Deprecated // to be removed before 2.0
851877
public static SqlTimestampLiteral createTimestamp(
852878
TimestampString ts,
853879
int precision,
854880
SqlParserPos pos) {
855-
return new SqlTimestampLiteral(ts, precision, false, pos);
881+
return createTimestamp(SqlTypeName.TIMESTAMP, ts, precision, pos);
882+
}
883+
884+
/** Creates a TIMESTAMP or TIMESTAMP WITH TIME ZONE literal. */
885+
public static SqlTimestampLiteral createTimestamp(
886+
SqlTypeName typeName,
887+
TimestampString ts,
888+
int precision,
889+
SqlParserPos pos) {
890+
return new SqlTimestampLiteral(ts, precision, typeName, pos);
856891
}
857892

858893
@Deprecated // to be removed before 2.0

core/src/main/java/org/apache/calcite/sql/SqlTimestampLiteral.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ public class SqlTimestampLiteral extends SqlAbstractDateTimeLiteral {
3434
//~ Constructors -----------------------------------------------------------
3535

3636
SqlTimestampLiteral(TimestampString ts, int precision,
37-
boolean hasTimeZone, SqlParserPos pos) {
38-
super(ts, hasTimeZone, SqlTypeName.TIMESTAMP, precision, pos);
37+
SqlTypeName typeName, SqlParserPos pos) {
38+
super(ts, false, typeName, precision, pos);
3939
Preconditions.checkArgument(this.precision >= 0);
40+
Preconditions.checkArgument(typeName == SqlTypeName.TIMESTAMP
41+
|| typeName == SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE);
4042
}
4143

4244
//~ Methods ----------------------------------------------------------------
@@ -45,11 +47,11 @@ public class SqlTimestampLiteral extends SqlAbstractDateTimeLiteral {
4547
return new SqlTimestampLiteral(
4648
(TimestampString) Objects.requireNonNull(value, "value"),
4749
precision,
48-
hasTimeZone, pos);
50+
getTypeName(), pos);
4951
}
5052

5153
@Override public String toString() {
52-
return "TIMESTAMP '" + toFormattedString() + "'";
54+
return getTypeName() + " '" + toFormattedString() + "'";
5355
}
5456

5557
/**
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.calcite.sql;
18+
19+
import org.apache.calcite.sql.parser.SqlParserPos;
20+
import org.apache.calcite.sql.parser.SqlParserUtil;
21+
import org.apache.calcite.sql.type.SqlTypeName;
22+
import org.apache.calcite.util.NlsString;
23+
import org.apache.calcite.util.Util;
24+
25+
import static java.util.Objects.requireNonNull;
26+
27+
/**
28+
* Literal whose type is not yet known.
29+
*/
30+
public class SqlUnknownLiteral extends SqlLiteral {
31+
public final String tag;
32+
33+
SqlUnknownLiteral(String tag, String value, SqlParserPos pos) {
34+
super(requireNonNull(value, "value"), SqlTypeName.UNKNOWN, pos);
35+
this.tag = requireNonNull(tag, "tag");
36+
}
37+
38+
@Override public String getValue() {
39+
return (String) requireNonNull(super.getValue(), "value");
40+
}
41+
42+
@Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
43+
final NlsString nlsString = new NlsString(getValue(), null, null);
44+
writer.keyword(tag);
45+
writer.literal(nlsString.asSql(true, true, writer.getDialect()));
46+
}
47+
48+
49+
/** Converts this unknown literal to a literal of known type. */
50+
public SqlLiteral resolve(SqlTypeName typeName) {
51+
switch (typeName) {
52+
case DATE:
53+
return SqlParserUtil.parseDateLiteral(getValue(), pos);
54+
case TIME:
55+
return SqlParserUtil.parseTimeLiteral(getValue(), pos);
56+
case TIMESTAMP:
57+
return SqlParserUtil.parseTimestampLiteral(getValue(), pos);
58+
case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
59+
return SqlParserUtil.parseTimestampWithLocalTimeZoneLiteral(getValue(), pos);
60+
default:
61+
throw Util.unexpected(typeName);
62+
}
63+
}
64+
}

core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.apache.calcite.sql.SqlUtil;
4141
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
4242
import org.apache.calcite.sql.parser.impl.SqlParserImpl;
43+
import org.apache.calcite.sql.type.SqlTypeName;
4344
import org.apache.calcite.util.DateString;
4445
import org.apache.calcite.util.PrecedenceClimbingParser;
4546
import org.apache.calcite.util.TimeString;
@@ -338,6 +339,17 @@ public static SqlTimeLiteral parseTimeLiteral(String s, SqlParserPos pos) {
338339

339340
public static SqlTimestampLiteral parseTimestampLiteral(String s,
340341
SqlParserPos pos) {
342+
return parseTimestampLiteral(SqlTypeName.TIMESTAMP, s, pos);
343+
}
344+
345+
public static SqlTimestampLiteral parseTimestampWithLocalTimeZoneLiteral(
346+
String s, SqlParserPos pos) {
347+
return parseTimestampLiteral(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, s,
348+
pos);
349+
}
350+
351+
private static SqlTimestampLiteral parseTimestampLiteral(SqlTypeName typeName,
352+
String s, SqlParserPos pos) {
341353
final Format format = Format.get();
342354
DateTimeUtils.PrecisionTime pt = null;
343355
// Allow timestamp literals with and without time fields (as does
@@ -352,13 +364,13 @@ public static SqlTimestampLiteral parseTimestampLiteral(String s,
352364
}
353365
if (pt == null) {
354366
throw SqlUtil.newContextException(pos,
355-
RESOURCE.illegalLiteral("TIMESTAMP", s,
367+
RESOURCE.illegalLiteral(typeName.getName().replace('_', ' '), s,
356368
RESOURCE.badFormat(DateTimeUtils.TIMESTAMP_FORMAT_STRING).str()));
357369
}
358370
final TimestampString ts =
359371
TimestampString.fromCalendarFields(pt.getCalendar())
360372
.withFraction(pt.getFraction());
361-
return SqlLiteral.createTimestamp(ts, pt.getPrecision(), pos);
373+
return SqlLiteral.createTimestamp(typeName, ts, pt.getPrecision(), pos);
362374
}
363375

364376
public static SqlIntervalLiteral parseIntervalLiteral(SqlParserPos pos,

core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,14 @@ public enum SqlTypeName {
298298
return VALUES_MAP.get(name);
299299
}
300300

301+
/** Returns the SqlTypeName value whose name or {@link #getSpaceName()}
302+
* matches the given name, or throws {@link IllegalArgumentException}; never
303+
* returns null. */
304+
public static SqlTypeName lookup(String tag) {
305+
String tag2 = tag.replace(' ', '_');
306+
return valueOf(tag2);
307+
}
308+
301309
public boolean allowsNoPrecNoScale() {
302310
return (signatures & PrecScale.NO_NO) != 0;
303311
}
@@ -945,7 +953,7 @@ public SqlLiteral createLiteral(Object o, SqlParserPos pos) {
945953
? TimeString.fromCalendarFields((Calendar) o)
946954
: (TimeString) o, 0 /* todo */, pos);
947955
case TIMESTAMP:
948-
return SqlLiteral.createTimestamp(o instanceof Calendar
956+
return SqlLiteral.createTimestamp(this, o instanceof Calendar
949957
? TimestampString.fromCalendarFields((Calendar) o)
950958
: (TimestampString) o, 0 /* todo */, pos);
951959
default:
@@ -955,7 +963,13 @@ public SqlLiteral createLiteral(Object o, SqlParserPos pos) {
955963

956964
/** Returns the name of this type. */
957965
public String getName() {
958-
return toString();
966+
return name();
967+
}
968+
969+
/** Returns the name of this type, with underscores converted to spaces,
970+
* for example "TIMESTAMP WITH LOCAL TIME ZONE", "DATE". */
971+
public String getSpaceName() {
972+
return name().replace('_', ' ');
959973
}
960974

961975
/**

core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,13 @@ CalciteException handleUnresolvedFunction(SqlCall call,
705705
*/
706706
SqlNode expand(SqlNode expr, SqlValidatorScope scope);
707707

708+
/** Resolves a literal.
709+
*
710+
* <p>Usually returns the literal unchanged, but if the literal is of type
711+
* {@link org.apache.calcite.sql.type.SqlTypeName#UNKNOWN} looks up its type
712+
* and converts to the appropriate literal subclass. */
713+
SqlLiteral resolveLiteral(SqlLiteral literal);
714+
708715
/**
709716
* Returns whether a field is a system field. Such fields may have
710717
* particular properties such as sortedness and nullability.

0 commit comments

Comments
 (0)