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 <[email protected]> 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
