You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2019/10/11 00:53:22 UTC
[calcite] 02/02: [CALCITE-3383] Plural time units
This is an automated email from the ASF dual-hosted git repository.
jhyde pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite.git
commit 4be2d08e2116234a7df982014f8ae5bc79a7e0bf
Author: Julian Hyde <jh...@apache.org>
AuthorDate: Wed Oct 9 19:16:45 2019 -0700
[CALCITE-3383] Plural time units
Add SqlConformance.allowPluralTimeUnits(); if it is true, we allow
INTERVAL '2' days
otherwise we only allow
INTERVAL '2' DAY
---
babel/src/main/codegen/config.fmpp | 21 ++--
core/src/main/codegen/config.fmpp | 6 +
core/src/main/codegen/templates/Parser.jj | 111 +++++++++++++++----
.../apache/calcite/runtime/CalciteResource.java | 3 +
.../calcite/sql/parser/SqlAbstractParserImpl.java | 4 +
.../org/apache/calcite/sql/parser/SqlParser.java | 9 ++
.../sql/validate/SqlAbstractConformance.java | 4 +
.../calcite/sql/validate/SqlConformance.java | 15 +++
.../calcite/sql/validate/SqlConformanceEnum.java | 10 ++
.../calcite/runtime/CalciteResource.properties | 1 +
core/src/test/codegen/config.fmpp | 6 +
.../apache/calcite/sql/parser/SqlParserTest.java | 123 ++++++++++++++++-----
server/src/main/codegen/config.fmpp | 6 +
site/_docs/reference.md | 10 ++
14 files changed, 269 insertions(+), 60 deletions(-)
diff --git a/babel/src/main/codegen/config.fmpp b/babel/src/main/codegen/config.fmpp
index c120557..503bd1c 100644
--- a/babel/src/main/codegen/config.fmpp
+++ b/babel/src/main/codegen/config.fmpp
@@ -84,6 +84,7 @@ data: {
"DATABASE"
"DATETIME_INTERVAL_CODE"
"DATETIME_INTERVAL_PRECISION"
+ "DAYS"
"DECADE"
"DEFAULTS"
"DEFERRABLE"
@@ -124,6 +125,7 @@ data: {
"GOTO"
"GRANTED"
"HIERARCHY"
+ "HOURS"
"IGNORE"
"IMMEDIATE"
"IMMEDIATELY"
@@ -160,7 +162,9 @@ data: {
"MICROSECOND"
"MILLENNIUM"
"MILLISECOND"
+ "MINUTES"
"MINVALUE"
+ "MONTHS"
"MORE_"
"MUMPS"
"NAME"
@@ -227,6 +231,7 @@ data: {
"SCOPE_CATALOGS"
"SCOPE_NAME"
"SCOPE_SCHEMA"
+ "SECONDS"
"SECTION"
"SECURITY"
"SELF"
@@ -332,14 +337,10 @@ data: {
"WRAPPER"
"WRITE"
"XML"
+ "YEARS"
"ZONE"
]
- # List of non-reserved keywords to remove;
- # items in this list become reserved
- nonReservedKeywordsToRemove: [
- ]
-
# List of non-reserved keywords to add;
# items in this list become non-reserved
nonReservedKeywordsToAdd: [
@@ -456,7 +457,6 @@ data: {
"DATA"
# "DATE"
"DAY"
-# "DAYS" # not a keyword in Calcite
"DEALLOCATE"
"DEC"
"DECIMAL"
@@ -535,7 +535,6 @@ data: {
# "HAVING"
"HOLD"
"HOUR"
-# "HOURS" # not a keyword in Calcite
"IDENTITY"
# "IF" # not a keyword in Calcite
"IMMEDIATE"
@@ -604,7 +603,6 @@ data: {
"MIN"
# "MINUS"
"MINUTE"
-# "MINUTES" # not a keyword in Calcite
"MOD"
"MODIFIES"
"MODULE"
@@ -717,7 +715,6 @@ data: {
"SCROLL"
"SEARCH"
"SECOND"
-# "SECONDS" # not a keyword in Calcite
"SECTION"
"SEEK"
# "SELECT"
@@ -818,10 +815,14 @@ data: {
"WORK"
"WRITE"
"YEAR"
-# "YEARS" # not a keyword in Calcite
"ZONE"
]
+ # List of non-reserved keywords to remove;
+ # items in this list become reserved
+ nonReservedKeywordsToRemove: [
+ ]
+
# List of additional join types. Each is a method with no arguments.
# Example: LeftSemiJoin()
joinTypes: [
diff --git a/core/src/main/codegen/config.fmpp b/core/src/main/codegen/config.fmpp
index e2bde0f..88ed04cd 100644
--- a/core/src/main/codegen/config.fmpp
+++ b/core/src/main/codegen/config.fmpp
@@ -104,6 +104,7 @@ data: {
"DATABASE"
"DATETIME_INTERVAL_CODE"
"DATETIME_INTERVAL_PRECISION"
+ "DAYS"
"DECADE"
"DEFAULTS"
"DEFERRABLE"
@@ -144,6 +145,7 @@ data: {
"GOTO"
"GRANTED"
"HIERARCHY"
+ "HOURS"
"IGNORE"
"IMMEDIATE"
"IMMEDIATELY"
@@ -180,7 +182,9 @@ data: {
"MICROSECOND"
"MILLENNIUM"
"MILLISECOND"
+ "MINUTES"
"MINVALUE"
+ "MONTHS"
"MORE_"
"MUMPS"
"NAME"
@@ -247,6 +251,7 @@ data: {
"SCOPE_CATALOGS"
"SCOPE_NAME"
"SCOPE_SCHEMA"
+ "SECONDS"
"SECTION"
"SECURITY"
"SELF"
@@ -352,6 +357,7 @@ data: {
"WRAPPER"
"WRITE"
"XML"
+ "YEARS"
"ZONE"
]
diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj
index 47a88bc..afebba8 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -234,6 +234,20 @@ public class ${parser.class} extends SqlAbstractParserImpl
return SqlStdOperatorTable.EXTEND.createCall(
Span.of(table, extendList).pos(), table, extendList);
}
+
+ /** Adds a warning that a token such as "HOURS" was used,
+ * whereas the SQL standard only allows "HOUR".
+ *
+ * <p>Currently, we silently add an exception to a list of warnings. In
+ * future, we may have better compliance checking, for example a strict
+ * compliance mode that throws if any non-standard features are used. */
+ private TimeUnit warn(TimeUnit timeUnit) throws ParseException {
+ final String token = getToken(0).image.toUpperCase(Locale.ROOT);
+ warnings.add(
+ SqlUtil.newContextException(getPos(),
+ RESOURCE.nonStandardFeatureUsed(token)));
+ return timeUnit;
+ }
}
PARSER_END(${parser.class})
@@ -4146,66 +4160,111 @@ SqlLiteral IntervalLiteral() :
}
}
+TimeUnit Year() :
+{
+}
+{
+ <YEAR> { return TimeUnit.YEAR; }
+|
+ <YEARS> { return warn(TimeUnit.YEAR); }
+}
+
+TimeUnit Month() :
+{
+}
+{
+ <MONTH> { return TimeUnit.MONTH; }
+|
+ <MONTHS> { return warn(TimeUnit.MONTH); }
+}
+
+TimeUnit Day() :
+{
+}
+{
+ <DAY> { return TimeUnit.DAY; }
+|
+ <DAYS> { return warn(TimeUnit.DAY); }
+}
+
+TimeUnit Hour() :
+{
+}
+{
+ <HOUR> { return TimeUnit.HOUR; }
+|
+ <HOURS> { return warn(TimeUnit.HOUR); }
+}
+
+TimeUnit Minute() :
+{
+}
+{
+ <MINUTE> { return TimeUnit.MINUTE; }
+|
+ <MINUTES> { return warn(TimeUnit.MINUTE); }
+}
+
+TimeUnit Second() :
+{
+}
+{
+ <SECOND> { return TimeUnit.SECOND; }
+|
+ <SECONDS> { return warn(TimeUnit.SECOND); }
+}
+
SqlIntervalQualifier IntervalQualifier() :
{
- TimeUnit start;
+ final TimeUnit start;
TimeUnit end = null;
int startPrec = RelDataType.PRECISION_NOT_SPECIFIED;
int secondFracPrec = RelDataType.PRECISION_NOT_SPECIFIED;
}
{
(
- <YEAR> [ <LPAREN> startPrec = UnsignedIntLiteral() <RPAREN> ]
+ start = Year() [ <LPAREN> startPrec = UnsignedIntLiteral() <RPAREN> ]
[
- LOOKAHEAD(2) <TO> <MONTH>
- {
- end = TimeUnit.MONTH;
- }
+ LOOKAHEAD(2) <TO> end = Month()
]
- { start = TimeUnit.YEAR; }
|
- <MONTH> [ <LPAREN> startPrec = UnsignedIntLiteral() <RPAREN> ]
- { start = TimeUnit.MONTH; }
+ start = Month() [ <LPAREN> startPrec = UnsignedIntLiteral() <RPAREN> ]
|
- <DAY> [ <LPAREN> startPrec = UnsignedIntLiteral() <RPAREN> ]
+ start = Day() [ <LPAREN> startPrec = UnsignedIntLiteral() <RPAREN> ]
[ LOOKAHEAD(2) <TO>
(
- <HOUR> { end = TimeUnit.HOUR; }
+ end = Hour()
|
- <MINUTE> { end = TimeUnit.MINUTE; }
+ end = Minute()
|
- <SECOND> { end = TimeUnit.SECOND; }
+ end = Second()
[ <LPAREN> secondFracPrec = UnsignedIntLiteral() <RPAREN> ]
)
]
- { start = TimeUnit.DAY; }
|
- <HOUR> [ <LPAREN> startPrec = UnsignedIntLiteral() <RPAREN> ]
+ start = Hour() [ <LPAREN> startPrec = UnsignedIntLiteral() <RPAREN> ]
[ LOOKAHEAD(2) <TO>
(
- <MINUTE> { end = TimeUnit.MINUTE; }
+ end = Minute()
|
- <SECOND> { end = TimeUnit.SECOND; }
+ end = Second()
[ <LPAREN> secondFracPrec = UnsignedIntLiteral() <RPAREN> ]
)
]
- { start = TimeUnit.HOUR; }
|
- <MINUTE> [ <LPAREN> startPrec = UnsignedIntLiteral() <RPAREN> ]
+ start = Minute() [ <LPAREN> startPrec = UnsignedIntLiteral() <RPAREN> ]
[ LOOKAHEAD(2) <TO>
(
- <SECOND> { end = TimeUnit.SECOND; }
+ end = Second()
[ <LPAREN> secondFracPrec = UnsignedIntLiteral() <RPAREN> ]
)
]
- { start = TimeUnit.MINUTE; }
|
- <SECOND>
+ start = Second()
[ <LPAREN> startPrec = UnsignedIntLiteral()
[ <COMMA> secondFracPrec = UnsignedIntLiteral() ]
<RPAREN>
]
- { start = TimeUnit.SECOND; }
)
{
return new SqlIntervalQualifier(start,
@@ -6575,6 +6634,7 @@ SqlPostfixOperator PostfixRowOperator() :
| < DATETIME_INTERVAL_CODE: "DATETIME_INTERVAL_CODE" >
| < DATETIME_INTERVAL_PRECISION: "DATETIME_INTERVAL_PRECISION" >
| < DAY: "DAY" >
+| < DAYS: "DAYS" >
| < DEALLOCATE: "DEALLOCATE" >
| < DEC: "DEC" >
| < DECADE: "DECADE" >
@@ -6675,6 +6735,7 @@ SqlPostfixOperator PostfixRowOperator() :
| < HIERARCHY: "HIERARCHY" >
| < HOLD: "HOLD" >
| < HOUR: "HOUR" >
+| < HOURS: "HOURS" >
| < IDENTITY: "IDENTITY" >
| < IGNORE: "IGNORE" >
| < IMMEDIATE: "IMMEDIATE" >
@@ -6762,11 +6823,13 @@ SqlPostfixOperator PostfixRowOperator() :
| < MILLENNIUM: "MILLENNIUM" >
| < MIN: "MIN" >
| < MINUTE: "MINUTE" >
+| < MINUTES: "MINUTES" >
| < MINVALUE: "MINVALUE" >
| < MOD: "MOD" >
| < MODIFIES: "MODIFIES" >
| < MODULE: "MODULE" >
| < MONTH: "MONTH" >
+| < MONTHS: "MONTHS" >
| < MORE_: "MORE" >
| < MULTISET: "MULTISET" >
| < MUMPS: "MUMPS" >
@@ -6921,6 +6984,7 @@ SqlPostfixOperator PostfixRowOperator() :
| < SCROLL: "SCROLL" >
| < SEARCH: "SEARCH" >
| < SECOND: "SECOND" >
+| < SECONDS: "SECONDS" >
| < SECTION: "SECTION" >
| < SECURITY: "SECURITY" >
| < SEEK: "SEEK" >
@@ -7106,6 +7170,7 @@ SqlPostfixOperator PostfixRowOperator() :
| < WRITE: "WRITE" >
| < XML: "XML" >
| < YEAR: "YEAR" >
+| < YEARS: "YEARS" >
| < ZONE: "ZONE" >
<#-- additional parser keywords are included here -->
<#list parser.keywords as keyword>
diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
index da277e1..9805b4e 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
@@ -560,6 +560,9 @@ public interface CalciteResource {
@BaseMessage("Statement preparation aborted")
ExInst<CalciteException> preparationAborted();
+ @BaseMessage("Warning: use of non-standard feature ''{0}''")
+ ExInst<CalciteException> nonStandardFeatureUsed(String feature);
+
@BaseMessage("SELECT DISTINCT not supported")
@Property(name = "FeatureDefinition", value = "SQL:2003 Part 2 Annex F")
Feature sQLFeature_E051_01();
diff --git a/core/src/main/java/org/apache/calcite/sql/parser/SqlAbstractParserImpl.java b/core/src/main/java/org/apache/calcite/sql/parser/SqlAbstractParserImpl.java
index 552031a..8615821 100644
--- a/core/src/main/java/org/apache/calcite/sql/parser/SqlAbstractParserImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/parser/SqlAbstractParserImpl.java
@@ -17,6 +17,7 @@
package org.apache.calcite.sql.parser;
import org.apache.calcite.avatica.util.Casing;
+import org.apache.calcite.runtime.CalciteContextException;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlFunctionCategory;
import org.apache.calcite.sql.SqlIdentifier;
@@ -36,6 +37,7 @@ import java.io.Reader;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -330,6 +332,8 @@ public abstract class SqlAbstractParserImpl {
protected String originalSql;
+ protected final List<CalciteContextException> warnings = new ArrayList<>();
+
//~ Methods ----------------------------------------------------------------
/**
diff --git a/core/src/main/java/org/apache/calcite/sql/parser/SqlParser.java b/core/src/main/java/org/apache/calcite/sql/parser/SqlParser.java
index 61e0a01..32c5b71 100644
--- a/core/src/main/java/org/apache/calcite/sql/parser/SqlParser.java
+++ b/core/src/main/java/org/apache/calcite/sql/parser/SqlParser.java
@@ -30,6 +30,7 @@ import org.apache.calcite.util.SourceStringReader;
import java.io.Reader;
import java.io.StringReader;
+import java.util.List;
import java.util.Objects;
/**
@@ -214,6 +215,14 @@ public class SqlParser {
}
/**
+ * Returns the warnings that were generated by the previous invocation
+ * of the parser.
+ */
+ public List<CalciteContextException> getWarnings() {
+ return parser.warnings;
+ }
+
+ /**
* Builder for a {@link Config}.
*/
public static ConfigBuilder configBuilder() {
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java
index f676419..643ff5c 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java
@@ -102,6 +102,10 @@ public abstract class SqlAbstractConformance implements SqlConformance {
public boolean allowExtendedTrim() {
return SqlConformanceEnum.DEFAULT.allowExtendedTrim();
}
+
+ public boolean allowPluralTimeUnits() {
+ return SqlConformanceEnum.DEFAULT.allowPluralTimeUnits();
+ }
}
// End SqlAbstractConformance.java
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java
index 644886f..b4e68c7 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java
@@ -393,6 +393,21 @@ public interface SqlConformance {
* false otherwise.
*/
boolean allowExtendedTrim();
+
+ /**
+ * Whether interval literals should allow plural time units
+ * such as "YEARS" and "DAYS" in interval literals.
+ *
+ * <p>Under strict behavior, {@code INTERVAL '2' DAY} is valid
+ * and {@code INTERVAL '2' DAYS} is invalid;
+ * PostgreSQL allows both; Oracle only allows singular time units.
+ *
+ * <p>Among the built-in conformance levels, true in
+ * {@link SqlConformanceEnum#BABEL},
+ * {@link SqlConformanceEnum#LENIENT};
+ * false otherwise.
+ */
+ boolean allowPluralTimeUnits();
}
// End SqlConformance.java
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java
index 5d9c44e..b2abc84 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java
@@ -304,6 +304,16 @@ public enum SqlConformanceEnum implements SqlConformance {
}
}
+ @Override public boolean allowPluralTimeUnits() {
+ switch (this) {
+ case BABEL:
+ case LENIENT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
}
// End SqlConformanceEnum.java
diff --git a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
index f4852a2..7263d92 100644
--- a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++ b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -185,6 +185,7 @@ InvalidDatetimeFormat=''{0}'' is not a valid datetime format
InsertIntoAlwaysGenerated=Cannot INSERT into generated column ''{0}''
ArgumentMustHaveScaleZero=Argument to function ''{0}'' must have a scale of 0
PreparationAborted=Statement preparation aborted
+NonStandardFeatureUsed=Warning: use of non-standard feature ''{0}''
SQLFeature_E051_01=SELECT DISTINCT not supported
SQLFeature_E071_03=EXCEPT not supported
SQLFeature_E101_03=UPDATE not supported
diff --git a/core/src/test/codegen/config.fmpp b/core/src/test/codegen/config.fmpp
index 5be63a4..0667110 100644
--- a/core/src/test/codegen/config.fmpp
+++ b/core/src/test/codegen/config.fmpp
@@ -88,6 +88,7 @@ data: {
"DATABASE"
"DATETIME_INTERVAL_CODE"
"DATETIME_INTERVAL_PRECISION"
+ "DAYS"
"DECADE"
"DEFAULTS"
"DEFERRABLE"
@@ -128,6 +129,7 @@ data: {
"GOTO"
"GRANTED"
"HIERARCHY"
+ "HOURS"
"IGNORE"
"IMMEDIATE"
"IMMEDIATELY"
@@ -164,7 +166,9 @@ data: {
"MICROSECOND"
"MILLENNIUM"
"MILLISECOND"
+ "MINUTES"
"MINVALUE"
+ "MONTHS"
"MORE_"
"MUMPS"
"NAME"
@@ -231,6 +235,7 @@ data: {
"SCOPE_CATALOGS"
"SCOPE_NAME"
"SCOPE_SCHEMA"
+ "SECONDS"
"SECTION"
"SECURITY"
"SELF"
@@ -336,6 +341,7 @@ data: {
"WRAPPER"
"WRITE"
"XML"
+ "YEARS"
"ZONE"
]
diff --git a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
index d2acca0..13f986d 100644
--- a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -35,6 +35,7 @@ import org.apache.calcite.sql.validate.SqlConformanceEnum;
import org.apache.calcite.test.DiffTestCase;
import org.apache.calcite.util.Bug;
import org.apache.calcite.util.ConversionUtil;
+import org.apache.calcite.util.Pair;
import org.apache.calcite.util.SourceStringReader;
import org.apache.calcite.util.TestUtil;
import org.apache.calcite.util.Util;
@@ -53,16 +54,20 @@ import org.junit.Test;
import java.io.Reader;
import java.io.StringReader;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import java.util.SortedSet;
import java.util.TreeSet;
+import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
+import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
@@ -590,11 +595,11 @@ public class SqlParserTest {
}
protected Sql sql(String sql) {
- return new Sql(sql, false, null);
+ return new Sql(sql, false, null, parser -> { });
}
protected Sql expr(String sql) {
- return new Sql(sql, true, null);
+ return new Sql(sql, true, null, parser -> { });
}
/** Creates an instance of helper class {@link SqlList} to test parsing a
@@ -6420,11 +6425,17 @@ public class SqlParserTest {
.fails("Encountered \"<EOF>\" at line 1, column 12\\.\n"
+ "Was expecting one of:\n"
+ " \"DAY\" \\.\\.\\.\n"
+ + " \"DAYS\" \\.\\.\\.\n"
+ " \"HOUR\" \\.\\.\\.\n"
+ + " \"HOURS\" \\.\\.\\.\n"
+ " \"MINUTE\" \\.\\.\\.\n"
+ + " \"MINUTES\" \\.\\.\\.\n"
+ " \"MONTH\" \\.\\.\\.\n"
+ + " \"MONTHS\" \\.\\.\\.\n"
+ " \"SECOND\" \\.\\.\\.\n"
+ + " \"SECONDS\" \\.\\.\\.\n"
+ " \"YEAR\" \\.\\.\\.\n"
+ + " \"YEARS\" \\.\\.\\.\n"
+ " ");
// illegal qualifiers, no precision in either field
@@ -6791,6 +6802,45 @@ public class SqlParserTest {
.fails(ANY);
}
+ /** Tests that plural time units are allowed when not in strict mode. */
+ @Test public void testIntervalPluralUnits() {
+ expr("interval '2' years")
+ .hasWarning(checkWarnings("YEARS"))
+ .ok("INTERVAL '2' YEAR");
+ expr("interval '2:1' years to months")
+ .hasWarning(checkWarnings("YEARS", "MONTHS"))
+ .ok("INTERVAL '2:1' YEAR TO MONTH");
+ expr("interval '2' days")
+ .hasWarning(checkWarnings("DAYS"))
+ .ok("INTERVAL '2' DAY");
+ expr("interval '2:1' days to hours")
+ .hasWarning(checkWarnings("DAYS", "HOURS"))
+ .ok("INTERVAL '2:1' DAY TO HOUR");
+ expr("interval '2:1' day to hours")
+ .hasWarning(checkWarnings("HOURS"))
+ .ok("INTERVAL '2:1' DAY TO HOUR");
+ expr("interval '2:1' days to hour")
+ .hasWarning(checkWarnings("DAYS"))
+ .ok("INTERVAL '2:1' DAY TO HOUR");
+ expr("interval '1:1' minutes to seconds")
+ .hasWarning(checkWarnings("MINUTES", "SECONDS"))
+ .ok("INTERVAL '1:1' MINUTE TO SECOND");
+ }
+
+ @Nonnull private Consumer<List<? extends Throwable>> checkWarnings(
+ String... tokens) {
+ final List<String> messages = new ArrayList<>();
+ for (String token : tokens) {
+ messages.add("Warning: use of non-standard feature '" + token + "'");
+ }
+ return throwables -> {
+ assertThat(throwables.size(), is(messages.size()));
+ for (Pair<? extends Throwable, String> pair : Pair.zip(throwables, messages)) {
+ assertThat(pair.left.getMessage(), containsString(pair.right));
+ }
+ };
+ }
+
@Test public void testMiscIntervalQualifier() {
expr("interval '-' day")
.ok("INTERVAL '-' DAY");
@@ -8535,9 +8585,11 @@ public class SqlParserTest {
protected interface Tester {
void checkList(String sql, List<String> expected);
- void check(String sql, SqlDialect dialect, String expected);
+ void check(String sql, SqlDialect dialect, String expected,
+ Consumer<SqlParser> parserChecker);
- void checkExp(String sql, String expected);
+ void checkExp(String sql, String expected,
+ Consumer<SqlParser> parserChecker);
void checkFails(String sql, boolean list, String expectedMsgPattern);
@@ -8573,19 +8625,23 @@ public class SqlParserTest {
}
}
- public void check(String sql, SqlDialect dialect, String expected) {
+ public void check(String sql, SqlDialect dialect, String expected,
+ Consumer<SqlParser> parserChecker) {
final SqlNode sqlNode = parseStmtAndHandleEx(sql,
- dialect == null ? UnaryOperator.identity() : dialect::configureParser);
+ dialect == null ? UnaryOperator.identity() : dialect::configureParser,
+ parserChecker);
check(sqlNode, dialect, expected);
}
protected SqlNode parseStmtAndHandleEx(String sql,
- UnaryOperator<SqlParser.ConfigBuilder> transform) {
+ UnaryOperator<SqlParser.ConfigBuilder> transform,
+ Consumer<SqlParser> parserChecker) {
final SqlParser parser =
getSqlParser(new SourceStringReader(sql), transform);
final SqlNode sqlNode;
try {
sqlNode = parser.parseStmt();
+ parserChecker.accept(parser);
} catch (SqlParseException e) {
throw new RuntimeException("Error while parsing SQL: " + sql, e);
}
@@ -8603,18 +8659,20 @@ public class SqlParserTest {
return sqlNodeList;
}
- public void checkExp(
- String sql,
- String expected) {
- final SqlNode sqlNode = parseExpressionAndHandleEx(sql);
+ public void checkExp(String sql, String expected,
+ Consumer<SqlParser> parserChecker) {
+ final SqlNode sqlNode = parseExpressionAndHandleEx(sql, parserChecker);
final String actual = sqlNode.toSqlString(null, true).getSql();
TestUtil.assertEqualsVerbose(expected, linux(actual));
}
- protected SqlNode parseExpressionAndHandleEx(String sql) {
+ protected SqlNode parseExpressionAndHandleEx(String sql,
+ Consumer<SqlParser> parserChecker) {
final SqlNode sqlNode;
try {
- sqlNode = getSqlParser(sql).parseExpression();
+ final SqlParser parser = getSqlParser(sql);
+ sqlNode = parser.parseExpression();
+ parserChecker.accept(parser);
} catch (SqlParseException e) {
throw new RuntimeException("Error while parsing expression: " + sql, e);
}
@@ -8735,10 +8793,11 @@ public class SqlParserTest {
checkList(sqlNodeList2, expected);
}
- @Override public void check(String sql, SqlDialect dialect,
- String expected) {
+ @Override public void check(String sql, SqlDialect dialect, String expected,
+ Consumer<SqlParser> parserChecker) {
SqlNode sqlNode = parseStmtAndHandleEx(sql,
- dialect == null ? UnaryOperator.identity() : dialect::configureParser);
+ dialect == null ? UnaryOperator.identity() : dialect::configureParser,
+ parserChecker);
// Unparse with the given dialect, always parenthesize.
final String actual = sqlNode.toSqlString(dialect, true).getSql();
@@ -8754,7 +8813,7 @@ public class SqlParserTest {
final Quoting q = quoting;
try {
quoting = Quoting.DOUBLE_QUOTE;
- sqlNode2 = parseStmtAndHandleEx(sql1, b -> b);
+ sqlNode2 = parseStmtAndHandleEx(sql1, b -> b, parser -> { });
} finally {
quoting = q;
}
@@ -8771,8 +8830,9 @@ public class SqlParserTest {
assertEquals(expected, linux(actual2));
}
- @Override public void checkExp(String sql, String expected) {
- SqlNode sqlNode = parseExpressionAndHandleEx(sql);
+ @Override public void checkExp(String sql, String expected,
+ Consumer<SqlParser> parserChecker) {
+ SqlNode sqlNode = parseExpressionAndHandleEx(sql, parserChecker);
// Unparse with no dialect, always parenthesize.
final String actual = sqlNode.toSqlString(null, true).getSql();
@@ -8784,11 +8844,12 @@ public class SqlParserTest {
sqlNode.toSqlString(CalciteSqlDialect.DEFAULT, false).getSql();
// Parse and unparse again.
+ // (Turn off parser checking, and use double-quotes.)
SqlNode sqlNode2;
final Quoting q = quoting;
try {
quoting = Quoting.DOUBLE_QUOTE;
- sqlNode2 = parseExpressionAndHandleEx(sql1);
+ sqlNode2 = parseExpressionAndHandleEx(sql1, parser -> { });
} finally {
quoting = q;
}
@@ -8830,11 +8891,14 @@ public class SqlParserTest {
private final String sql;
private final boolean expression;
private final SqlDialect dialect;
+ private final Consumer<SqlParser> parserChecker;
- Sql(String sql, boolean expression, SqlDialect dialect) {
- this.sql = sql;
+ Sql(String sql, boolean expression, SqlDialect dialect,
+ Consumer<SqlParser> parserChecker) {
+ this.sql = Objects.requireNonNull(sql);
this.expression = expression;
this.dialect = dialect;
+ this.parserChecker = Objects.requireNonNull(parserChecker);
}
public Sql same() {
@@ -8843,9 +8907,9 @@ public class SqlParserTest {
public Sql ok(String expected) {
if (expression) {
- getTester().checkExp(sql, expected);
+ getTester().checkExp(sql, expected, parserChecker);
} else {
- getTester().check(sql, dialect, expected);
+ getTester().check(sql, dialect, expected, parserChecker);
}
return this;
}
@@ -8859,6 +8923,11 @@ public class SqlParserTest {
return this;
}
+ public Sql hasWarning(Consumer<List<? extends Throwable>> messageMatcher) {
+ return new Sql(sql, expression, dialect, parser ->
+ messageMatcher.accept(parser.getWarnings()));
+ }
+
public Sql node(Matcher<SqlNode> matcher) {
getTester().checkNode(sql, matcher);
return this;
@@ -8866,18 +8935,18 @@ public class SqlParserTest {
/** Flags that this is an expression, not a whole query. */
public Sql expression() {
- return expression ? this : new Sql(sql, true, dialect);
+ return expression ? this : new Sql(sql, true, dialect, parserChecker);
}
/** Removes the carets from the SQL string. Useful if you want to run
* a test once at a conformance level where it fails, then run it again
* at a conformance level where it succeeds. */
public Sql sansCarets() {
- return new Sql(sql.replace("^", ""), expression, dialect);
+ return new Sql(sql.replace("^", ""), expression, dialect, parserChecker);
}
public Sql withDialect(SqlDialect dialect) {
- return new Sql(sql, expression, dialect);
+ return new Sql(sql, expression, dialect, parserChecker);
}
}
diff --git a/server/src/main/codegen/config.fmpp b/server/src/main/codegen/config.fmpp
index c2e9e2a..cbdfa74 100644
--- a/server/src/main/codegen/config.fmpp
+++ b/server/src/main/codegen/config.fmpp
@@ -96,6 +96,7 @@ data: {
"DATABASE"
"DATETIME_INTERVAL_CODE"
"DATETIME_INTERVAL_PRECISION"
+ "DAYS"
"DECADE"
"DEFAULTS"
"DEFERRABLE"
@@ -136,6 +137,7 @@ data: {
"GOTO"
"GRANTED"
"HIERARCHY"
+ "HOURS"
"IGNORE"
"IMMEDIATE"
"IMMEDIATELY"
@@ -172,7 +174,9 @@ data: {
"MICROSECOND"
"MILLENNIUM"
"MILLISECOND"
+ "MINUTES"
"MINVALUE"
+ "MONTHS"
"MORE_"
"MUMPS"
"NAME"
@@ -239,6 +243,7 @@ data: {
"SCOPE_CATALOGS"
"SCOPE_NAME"
"SCOPE_SCHEMA"
+ "SECONDS"
"SECTION"
"SECURITY"
"SELF"
@@ -344,6 +349,7 @@ data: {
"WRAPPER"
"WRITE"
"XML"
+ "YEARS"
"ZONE"
]
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 595b443..3390ba9 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -463,6 +463,7 @@ DATABASE,
DATETIME_INTERVAL_CODE,
DATETIME_INTERVAL_PRECISION,
**DAY**,
+DAYS,
**DEALLOCATE**,
**DEC**,
DECADE,
@@ -563,6 +564,7 @@ GRANTED,
HIERARCHY,
**HOLD**,
**HOUR**,
+HOURS,
**IDENTITY**,
IGNORE,
IMMEDIATE,
@@ -651,11 +653,13 @@ MILLISECOND,
**MIN**,
**MINUS**,
**MINUTE**,
+MINUTES,
MINVALUE,
**MOD**,
**MODIFIES**,
**MODULE**,
**MONTH**,
+MONTHS,
MORE,
**MULTISET**,
MUMPS,
@@ -810,6 +814,7 @@ SCOPE_SCHEMA,
**SCROLL**,
**SEARCH**,
**SECOND**,
+SECONDS,
SECTION,
SECURITY,
**SEEK**,
@@ -994,6 +999,7 @@ WRAPPER,
WRITE,
XML,
**YEAR**,
+YEARS,
ZONE.
{% comment %} end {% endcomment %}
@@ -1061,6 +1067,10 @@ Note:
it will rely on the supplied time zone to provide correct semantics.
* GEOMETRY is allowed only in certain
[conformance levels]({{ site.apiRoot }}/org/apache/calcite/sql/validate/SqlConformance.html#allowGeometry--).
+* Interval literals may only use time units
+ YEAR, MONTH, DAY, HOUR, MINUTE and SECOND. In certain
+ [conformance levels]({{ site.apiRoot }}/org/apache/calcite/sql/validate/SqlConformance.html#allowPluralTimeUnits--),
+ we also allow their plurals, YEARS, MONTHS, DAYS, HOURS, MINUTES and SECONDS.
### Non-scalar types