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 04314987373f0d7dd84b7b4e8df3243eef7f8f68
Author: Julian Hyde <[email protected]>
AuthorDate: Mon Jul 20 23:49:16 2020 -0700

    [CALCITE-4080] Allow character literals as column aliases, if 
SqlConformance.allowCharLiteralAlias()
    
    This is for compatibility with MySQL, Microsoft SQL Server,
    and BigQuery dialects of SQL.
    
    Close apache/calcite#2087
---
 core/src/main/codegen/templates/Parser.jj          |  39 +++++-
 .../apache/calcite/runtime/CalciteResource.java    |   3 +
 .../sql/validate/SqlAbstractConformance.java       |   4 +
 .../calcite/sql/validate/SqlConformance.java       |  19 +++
 .../calcite/sql/validate/SqlConformanceEnum.java   |  14 ++
 .../calcite/runtime/CalciteResource.properties     |   1 +
 .../apache/calcite/sql/parser/SqlParserTest.java   | 142 ++++++++++++++++++++-
 7 files changed, 215 insertions(+), 7 deletions(-)

diff --git a/core/src/main/codegen/templates/Parser.jj 
b/core/src/main/codegen/templates/Parser.jj
index b317cb3..1d8465e 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -1828,7 +1828,15 @@ SqlNode SelectItem() :
     e = SelectExpression()
     [
         [ <AS> ]
-        id = SimpleIdentifier() {
+        (
+            id = SimpleIdentifier()
+        |
+            // Mute the warning about ambiguity between alias and continued
+            // string literal.
+            LOOKAHEAD(1)
+            id = SimpleIdentifierFromStringLiteral()
+        )
+        {
             e = SqlStdOperatorTable.AS.createCall(span().end(e), e, id);
         }
     ]
@@ -4117,6 +4125,12 @@ SqlNode StringLiteral() :
         }
     }
     (
+        // The grammar is ambiguous when a continued literals and a character
+        // string alias are both possible. For example, in
+        //   SELECT x'01'\n'ab'
+        // we prefer that 'ab' continues the literal, and is not an alias.
+        // The following LOOKAHEAD mutes the warning about ambiguity.
+        LOOKAHEAD(1)
         <QUOTED_STRING>
         {
             try {
@@ -4166,6 +4180,12 @@ SqlNode StringLiteral() :
         nfrags++;
     }
     (
+        // The grammar is ambiguous when a continued literals and a character
+        // string alias are both possible. For example, in
+        //   SELECT 'taxi'\n'cab'
+        // we prefer that 'cab' continues the literal, and is not an alias.
+        // The following LOOKAHEAD mutes the warning about ambiguity.
+        LOOKAHEAD(1)
         <QUOTED_STRING>
         {
             p = SqlParserUtil.parseString(token.image);
@@ -4679,6 +4699,23 @@ SqlIdentifier SimpleIdentifier() :
 }
 
 /**
+ * Parses a character literal as an SqlIdentifier.
+ * Only valid for column aliases in certain dialects.
+ */
+SqlIdentifier SimpleIdentifierFromStringLiteral() :
+{
+}
+{
+    <QUOTED_STRING> {
+        if (!this.conformance.allowCharLiteralAlias()) {
+            throw SqlUtil.newContextException(getPos(), 
RESOURCE.charLiteralAliasNotValid());
+        }
+        final String s = SqlParserUtil.parseString(token.image);
+        return new SqlIdentifier(s, getPos());
+    }
+}
+
+/**
  * Parses a comma-separated list of simple identifiers.
  */
 void SimpleIdentifierCommaList(List<SqlNode> list) :
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 d932321..53fe755 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
@@ -283,6 +283,9 @@ public interface CalciteResource {
   @BaseMessage("Table or column alias must be a simple identifier")
   ExInst<SqlValidatorException> aliasMustBeSimpleIdentifier();
 
+  @BaseMessage("Expecting alias, found character literal")
+  ExInst<SqlValidatorException> charLiteralAliasNotValid();
+
   @BaseMessage("List of column aliases must have same degree as table; table 
has {0,number,#} columns {1}, whereas alias list has {2,number,#} columns")
   ExInst<SqlValidatorException> aliasListDegree(int a0, String a1, int a2);
 
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 842ee92..531543c 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
@@ -27,6 +27,10 @@ public abstract class SqlAbstractConformance implements 
SqlConformance {
     return SqlConformanceEnum.DEFAULT.isLiberal();
   }
 
+  public boolean allowCharLiteralAlias() {
+    return SqlConformanceEnum.DEFAULT.allowCharLiteralAlias();
+  }
+
   public boolean isGroupByAlias() {
     return SqlConformanceEnum.DEFAULT.isGroupByAlias();
   }
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 dc07d1b..78b7db3 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
@@ -68,6 +68,25 @@ public interface SqlConformance {
   boolean isLiberal();
 
   /**
+   * Whether this dialect allows character literals as column aliases.
+   *
+   * <p>For example,
+   *
+   * <blockquote><pre>
+   *   SELECT empno, sal + comm AS 'remuneration'
+   *   FROM Emp</pre></blockquote>
+   *
+   * <p>Among the built-in conformance levels, true in
+   * {@link SqlConformanceEnum#BABEL},
+   * {@link SqlConformanceEnum#BIG_QUERY},
+   * {@link SqlConformanceEnum#LENIENT},
+   * {@link SqlConformanceEnum#MYSQL_5},
+   * {@link SqlConformanceEnum#SQL_SERVER_2008};
+   * false otherwise.
+   */
+  boolean allowCharLiteralAlias();
+
+  /**
    * Whether to allow aliases from the {@code SELECT} clause to be used as
    * column names in the {@code GROUP BY} clause.
    *
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 3d689a1..ffccfd7 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
@@ -88,6 +88,20 @@ public enum SqlConformanceEnum implements SqlConformance {
     }
   }
 
+  public boolean allowCharLiteralAlias() {
+    switch (this) {
+    case BABEL:
+    case BIG_QUERY:
+    case LENIENT:
+    case MYSQL_5:
+    case SQL_SERVER_2008:
+      return true;
+    default:
+      return false;
+    }
+  }
+
+
   public boolean isGroupByAlias() {
     switch (this) {
     case BABEL:
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 079ef8c..9f4b453 100644
--- 
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++ 
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -97,6 +97,7 @@ NumberLiteralOutOfRange=Numeric literal ''{0}'' out of range
 DateLiteralOutOfRange=Date literal ''{0}'' out of range
 StringFragsOnSameLine=String literal continued on same line
 AliasMustBeSimpleIdentifier=Table or column alias must be a simple identifier
+CharLiteralAliasNotValid=Expecting alias, found character literal
 AliasListDegree=List of column aliases must have same degree as table; table 
has {0,number,#} columns {1}, whereas alias list has {2,number,#} columns
 AliasListDuplicate=Duplicate name ''{0}'' in column alias list
 JoinRequiresCondition=INNER, LEFT, RIGHT or FULL join requires a condition 
(NATURAL keyword or ON or USING clause)
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 603beec..4fa3451 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
@@ -2123,6 +2123,79 @@ public class SqlParserTest {
             + "WHERE (`x`.`DEPTNO` IN (10, 20))");
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-4080";>[CALCITE-4080]
+   * Allow character literals as column aliases, if
+   * SqlConformance.allowCharLiteralAlias()</a>. */
+  @Test void testSingleQuotedAlias() {
+    final String expectingAlias = "Expecting alias, found character literal";
+
+    final String sql1 = "select 1 as ^'a b'^ from t";
+    conformance = SqlConformanceEnum.DEFAULT;
+    sql(sql1).fails(expectingAlias);
+    conformance = SqlConformanceEnum.MYSQL_5;
+    final String sql1b = "SELECT 1 AS `a b`\n"
+        + "FROM `T`";
+    sql(sql1).sansCarets().ok(sql1b);
+    conformance = SqlConformanceEnum.BIG_QUERY;
+    sql(sql1).sansCarets().ok(sql1b);
+    conformance = SqlConformanceEnum.SQL_SERVER_2008;
+    sql(sql1).sansCarets().ok(sql1b);
+
+    // valid on MSSQL (alias contains a single quote)
+    final String sql2 = "with t as (select 1 as ^'x''y'^)\n"
+        + "select [x'y] from t as [u]";
+    conformance = SqlConformanceEnum.DEFAULT;
+    quoting = Quoting.BRACKET;
+    sql(sql2).fails(expectingAlias);
+    conformance = SqlConformanceEnum.MYSQL_5;
+    final String sql2b = "WITH `T` AS (SELECT 1 AS `x'y`) (SELECT `x'y`\n"
+        + "FROM `T` AS `u`)";
+    sql(sql2).sansCarets().ok(sql2b);
+    conformance = SqlConformanceEnum.BIG_QUERY;
+    sql(sql2).sansCarets().ok(sql2b);
+    conformance = SqlConformanceEnum.SQL_SERVER_2008;
+    sql(sql2).sansCarets().ok(sql2b);
+
+    // also valid on MSSQL
+    final String sql3 = "with [t] as (select 1 as [x]) select [x] from [t]";
+    final String sql3b = "WITH `t` AS (SELECT 1 AS `x`) (SELECT `x`\n"
+        + "FROM `t`)";
+    conformance = SqlConformanceEnum.DEFAULT;
+    quoting = Quoting.BRACKET;
+    sql(sql3).sansCarets().ok(sql3b);
+    conformance = SqlConformanceEnum.MYSQL_5;
+    sql(sql3).sansCarets().ok(sql3b);
+    conformance = SqlConformanceEnum.BIG_QUERY;
+    sql(sql3).sansCarets().ok(sql3b);
+    conformance = SqlConformanceEnum.SQL_SERVER_2008;
+    sql(sql3).sansCarets().ok(sql3b);
+
+    // char literal as table alias is invalid on MSSQL (and others)
+    final String sql4 = "with t as (select 1 as x) select x from t as ^'u'^";
+    final String sql4b = "(?s)Encountered \"\\\\'u\\\\'\" at .*";
+    conformance = SqlConformanceEnum.DEFAULT;
+    sql(sql4).fails(sql4b);
+    conformance = SqlConformanceEnum.MYSQL_5;
+    sql(sql4).fails(sql4b);
+    conformance = SqlConformanceEnum.BIG_QUERY;
+    sql(sql4).fails(sql4b);
+    conformance = SqlConformanceEnum.SQL_SERVER_2008;
+    sql(sql4).fails(sql4b);
+
+    // char literal as table alias (without AS) is invalid on MSSQL (and 
others)
+    final String sql5 = "with t as (select 1 as x) select x from t ^'u'^";
+    final String sql5b = "(?s)Encountered \"\\\\'u\\\\'\" at .*";
+    conformance = SqlConformanceEnum.DEFAULT;
+    sql(sql5).fails(sql5b);
+    conformance = SqlConformanceEnum.MYSQL_5;
+    sql(sql5).fails(sql5b);
+    conformance = SqlConformanceEnum.BIG_QUERY;
+    sql(sql5).fails(sql5b);
+    conformance = SqlConformanceEnum.SQL_SERVER_2008;
+    sql(sql5).fails(sql5b);
+  }
+
   @Test void testInList() {
     sql("select * from emp where deptno in (10, 20) and gender = 'F'")
         .ok("SELECT *\n"
@@ -2687,6 +2760,62 @@ public class SqlParserTest {
         .fails("Binary literal string must contain only characters '0' - '9', 
'A' - 'F'");
   }
 
+  /** Tests that ambiguity between extended string literals and character 
string
+   * aliases is always resolved in favor of extended string literals. */
+  @Test void testContinuedLiteralAlias() {
+    final String expectingAlias = "Expecting alias, found character literal";
+
+    // Not ambiguous, because of 'as'.
+    final String sql0 = "select 1 an_alias,\n"
+        + "  x'01'\n"
+        + "  'ab' as x\n"
+        + "from t";
+    final String sql0b = "SELECT 1 AS `AN_ALIAS`, X'01'\n"
+        + "'AB' AS `X`\n"
+        + "FROM `T`";
+    conformance = SqlConformanceEnum.DEFAULT;
+    sql(sql0).ok(sql0b);
+    conformance = SqlConformanceEnum.MYSQL_5;
+    sql(sql0).ok(sql0b);
+    conformance = SqlConformanceEnum.BIG_QUERY;
+    sql(sql0).ok(sql0b);
+
+    // Is 'ab' an alias or is it part of the x'01' 'ab' continued binary string
+    // literal? It's ambiguous, but we prefer the latter.
+    final String sql1 = "select 1 ^'an alias'^,\n"
+        + "  x'01'\n"
+        + "  'ab'\n"
+        + "from t";
+    final String sql1b = "SELECT 1 AS `an alias`, X'01'\n"
+        + "'AB'\n"
+        + "FROM `T`";
+    conformance = SqlConformanceEnum.DEFAULT;
+    sql(sql1).fails(expectingAlias);
+    conformance = SqlConformanceEnum.MYSQL_5;
+    sql(sql1).sansCarets().ok(sql1b);
+    conformance = SqlConformanceEnum.BIG_QUERY;
+    sql(sql1).sansCarets().ok(sql1b);
+
+    // Parser prefers continued character and binary string literals over
+    // character string aliases, regardless of whether the dialect allows
+    // character string aliases.
+    final String sql2 = "select 'continued'\n"
+        + "  'char literal, not alias',\n"
+        + "  x'01'\n"
+        + "  'ab'\n"
+        + "from t";
+    final String sql2b = "SELECT 'continued'\n"
+        + "'char literal, not alias', X'01'\n"
+        + "'AB'\n"
+        + "FROM `T`";
+    conformance = SqlConformanceEnum.DEFAULT;
+    sql(sql2).ok(sql2b);
+    conformance = SqlConformanceEnum.MYSQL_5;
+    sql(sql2).ok(sql2b);
+    conformance = SqlConformanceEnum.BIG_QUERY;
+    sql(sql2).ok(sql2b);
+  }
+
   @Test void testMixedFrom() {
     // REVIEW: Is this syntax even valid?
     sql("select * from a join b using (x), c join d using (y)")
@@ -3960,8 +4089,8 @@ public class SqlParserTest {
 
   @Test void testBitStringNotImplemented() {
     // Bit-string is longer part of the SQL standard. We do not support it.
-    sql("select B^'1011'^ || 'foobar' from (values (true))")
-        .fails("(?s).*Encountered \"\\\\'1011\\\\'\" at line 1, column 9.*");
+    sql("select (B^'1011'^ || 'foobar') from (values (true))")
+        .fails("(?s).*Encountered \"\\\\'1011\\\\'\" at .*");
   }
 
   @Test void testHexAndBinaryString() {
@@ -4040,16 +4169,17 @@ public class SqlParserTest {
   }
 
   @Test void testStringLiteralFails() {
-    sql("select N ^'space'^")
+    sql("select (N ^'space'^)")
         .fails("(?s).*Encountered .*space.* at line 1, column ...*");
-    sql("select _latin1\n^'newline'^")
+    sql("select (_latin1\n^'newline'^)")
         .fails("(?s).*Encountered.*newline.* at line 2, column ...*");
     sql("select ^_unknown-charset''^ from (values(true))")
         .fails("Unknown character set 'unknown-charset'");
 
     // valid syntax, but should give a validator error
-    sql("select N'1' '2' from t")
-        .ok("SELECT _ISO-8859-1'1'\n'2'\n"
+    sql("select (N'1' '2') from t")
+        .ok("SELECT _ISO-8859-1'1'\n"
+            + "'2'\n"
             + "FROM `T`");
   }
 

Reply via email to