You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by if...@apache.org on 2025/01/22 17:27:36 UTC

(datafusion-sqlparser-rs) branch bigquery-reserved-columns created (now 6ef8b472)

This is an automated email from the ASF dual-hosted git repository.

iffyio pushed a change to branch bigquery-reserved-columns
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git


      at 6ef8b472 BigQuery: Fix column identifier reserved keywords list

This branch includes the following new commits:

     new 6ef8b472 BigQuery: Fix column identifier reserved keywords list

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org


(datafusion-sqlparser-rs) 01/01: BigQuery: Fix column identifier reserved keywords list

Posted by if...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

iffyio pushed a commit to branch bigquery-reserved-columns
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git

commit 6ef8b4720e6bccb5b503f1c8d60e99b39ba141a5
Author: ifeanyi <if...@validio.io>
AuthorDate: Wed Jan 22 18:16:39 2025 +0100

    BigQuery: Fix column identifier reserved keywords list
    
    Parser currently uses the same `RESERVED_FOR_COLUMN_ALIAS` keywords
    list to avoid lookahead when parsing column identifiers. This
    assumed that all listed keywords were reservered across all dialects
    which isn't the case.
    So that the following valid BigQuery statement previously failed
    due to `OFFSET` being flagged as a keyword.
    ```sql
    SELECT 1, OFFSET FROM T
    ```
    This updates the parser to support dialect specific
    `RESERVED_FOR_COLUMN_ALIAS` list
---
 src/dialect/bigquery.rs     | 25 +++++++++++++++++++++++
 src/dialect/mod.rs          | 17 ++++++++++++++--
 src/parser/mod.rs           |  7 +++++--
 tests/sqlparser_bigquery.rs |  9 +++++++++
 tests/sqlparser_common.rs   | 49 +++++++++++++++++++++++++++++++++------------
 5 files changed, 90 insertions(+), 17 deletions(-)

diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs
index e92169a3..21c3f038 100644
--- a/src/dialect/bigquery.rs
+++ b/src/dialect/bigquery.rs
@@ -16,6 +16,27 @@
 // under the License.
 
 use crate::dialect::Dialect;
+use crate::keywords::Keyword;
+
+/// These keywords are disallowed as column names. Such that
+/// `SELECT 5 AS <col> FROM T` is rejected by BigQuery.
+const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[
+    Keyword::WITH,
+    Keyword::SELECT,
+    Keyword::WHERE,
+    Keyword::GROUP,
+    Keyword::HAVING,
+    Keyword::ORDER,
+    Keyword::LATERAL,
+    Keyword::LIMIT,
+    Keyword::FETCH,
+    Keyword::UNION,
+    Keyword::EXCEPT,
+    Keyword::INTERSECT,
+    Keyword::FROM,
+    Keyword::INTO,
+    Keyword::END,
+];
 
 /// A [`Dialect`] for [Google Bigquery](https://cloud.google.com/bigquery/)
 #[derive(Debug, Default)]
@@ -82,4 +103,8 @@ impl Dialect for BigQueryDialect {
     fn supports_timestamp_versioning(&self) -> bool {
         true
     }
+
+    fn get_reserved_keywords_for_column_identifier(&self) -> &'static [Keyword] {
+        RESERVED_FOR_COLUMN_ALIAS
+    }
 }
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index 119bb3cf..9675e7e0 100644
--- a/src/dialect/mod.rs
+++ b/src/dialect/mod.rs
@@ -781,7 +781,7 @@ pub trait Dialect: Debug + Any {
         keywords::RESERVED_FOR_IDENTIFIER.contains(&kw)
     }
 
-    // Returns reserved keywords when looking to parse a [TableFactor].
+    /// Returns reserved keywords when looking to parse a [TableFactor].
     /// See [Self::supports_from_trailing_commas]
     fn get_reserved_keywords_for_table_factor(&self) -> &[Keyword] {
         keywords::RESERVED_FOR_TABLE_FACTOR
@@ -821,11 +821,24 @@ pub trait Dialect: Debug + Any {
         false
     }
 
+    /// Returns reserved keywords when looking to parse column
+    /// identifiers.
+    /// i.e. (unquoted) words that may not be used as column identifiers
+    /// for the dialect.
+    /// For example given: `SELECT <col> AS <alias> FROM T`. The returned
+    /// keywords are not allowed in `<col>` and `<alias>`
+    fn get_reserved_keywords_for_column_identifier(&self) -> &'static [Keyword] {
+        keywords::RESERVED_FOR_COLUMN_ALIAS
+    }
+
     /// Returns true if the specified keyword should be parsed as a select item alias.
     /// When explicit is true, the keyword is preceded by an `AS` word. Parser is provided
     /// to enable looking ahead if needed.
     fn is_select_item_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool {
-        explicit || !keywords::RESERVED_FOR_COLUMN_ALIAS.contains(kw)
+        explicit
+            || !self
+                .get_reserved_keywords_for_column_identifier()
+                .contains(kw)
     }
 
     /// Returns true if the specified keyword should be parsed as a table factor alias.
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 355e520a..f713edd2 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -3989,7 +3989,8 @@ impl<'a> Parser<'a> {
         trailing_commas: bool,
         reserved_keywords: Option<&[Keyword]>,
     ) -> bool {
-        let reserved_keywords = reserved_keywords.unwrap_or(keywords::RESERVED_FOR_COLUMN_ALIAS);
+        let reserved_keywords = reserved_keywords
+            .unwrap_or_else(|| self.dialect.get_reserved_keywords_for_column_identifier());
         if !self.consume_token(&Token::Comma) {
             true
         } else if trailing_commas {
@@ -11153,7 +11154,9 @@ impl<'a> Parser<'a> {
             };
 
             let with_offset_alias = if with_offset {
-                match self.parse_optional_alias(keywords::RESERVED_FOR_COLUMN_ALIAS) {
+                match self.parse_optional_alias(
+                    self.dialect.get_reserved_keywords_for_column_identifier(),
+                ) {
                     Ok(Some(alias)) => Some(alias),
                     Ok(None) => None,
                     Err(e) => return Err(e),
diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs
index a173a6cc..65f39d65 100644
--- a/tests/sqlparser_bigquery.rs
+++ b/tests/sqlparser_bigquery.rs
@@ -213,6 +213,15 @@ fn parse_raw_literal() {
     );
 }
 
+#[test]
+fn parse_big_query_non_reserved_column_alias() {
+    let sql = r#"SELECT OFFSET, EXPLAIN, ANALYZE, SORT, TOP, VIEW FROM T"#;
+    bigquery().verified_stmt(sql);
+
+    let sql = r#"SELECT 1 AS OFFSET, 2 AS EXPLAIN, 3 AS ANALYZE FROM T"#;
+    bigquery().verified_stmt(sql);
+}
+
 #[test]
 fn parse_delete_statement() {
     let sql = "DELETE \"table\" WHERE 1";
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index ebd6bef0..591246fb 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -253,8 +253,15 @@ fn parse_insert_default_values() {
 
 #[test]
 fn parse_insert_select_returning() {
-    verified_stmt("INSERT INTO t SELECT 1 RETURNING 2");
-    let stmt = verified_stmt("INSERT INTO t SELECT x RETURNING x AS y");
+    // Dialects that support `RETURNING` as a column identifier do
+    // not support this syntax.
+    let dialects = all_dialects_where(|d| {
+        d.get_reserved_keywords_for_column_identifier()
+            .contains(&Keyword::RETURNING)
+    });
+
+    dialects.verified_stmt("INSERT INTO t SELECT 1 RETURNING 2");
+    let stmt = dialects.verified_stmt("INSERT INTO t SELECT x RETURNING x AS y");
     match stmt {
         Statement::Insert(Insert {
             returning: Some(ret),
@@ -6897,9 +6904,6 @@ fn parse_union_except_intersect_minus() {
     verified_stmt("SELECT 1 EXCEPT SELECT 2");
     verified_stmt("SELECT 1 EXCEPT ALL SELECT 2");
     verified_stmt("SELECT 1 EXCEPT DISTINCT SELECT 1");
-    verified_stmt("SELECT 1 MINUS SELECT 2");
-    verified_stmt("SELECT 1 MINUS ALL SELECT 2");
-    verified_stmt("SELECT 1 MINUS DISTINCT SELECT 1");
     verified_stmt("SELECT 1 INTERSECT SELECT 2");
     verified_stmt("SELECT 1 INTERSECT ALL SELECT 2");
     verified_stmt("SELECT 1 INTERSECT DISTINCT SELECT 1");
@@ -6918,6 +6922,16 @@ fn parse_union_except_intersect_minus() {
     verified_stmt("SELECT 1 AS x, 2 AS y INTERSECT BY NAME SELECT 9 AS y, 8 AS x");
     verified_stmt("SELECT 1 AS x, 2 AS y INTERSECT ALL BY NAME SELECT 9 AS y, 8 AS x");
     verified_stmt("SELECT 1 AS x, 2 AS y INTERSECT DISTINCT BY NAME SELECT 9 AS y, 8 AS x");
+
+    // Dialects that support `MINUS` as column identifier
+    // do not support `MINUS` as a set operator.
+    let dialects = all_dialects_where(|d| {
+        d.get_reserved_keywords_for_column_identifier()
+            .contains(&Keyword::MINUS)
+    });
+    dialects.verified_stmt("SELECT 1 MINUS SELECT 2");
+    dialects.verified_stmt("SELECT 1 MINUS ALL SELECT 2");
+    dialects.verified_stmt("SELECT 1 MINUS DISTINCT SELECT 1");
 }
 
 #[test]
@@ -7594,19 +7608,28 @@ fn parse_invalid_subquery_without_parens() {
 
 #[test]
 fn parse_offset() {
+    // Dialects that support `OFFSET` as column identifiers
+    // don't support this syntax.
+    let dialects = all_dialects_where(|d| {
+        d.get_reserved_keywords_for_column_identifier()
+            .contains(&Keyword::OFFSET)
+    });
+
     let expect = Some(Offset {
         value: Expr::Value(number("2")),
         rows: OffsetRows::Rows,
     });
-    let ast = verified_query("SELECT foo FROM bar OFFSET 2 ROWS");
+    let ast = dialects.verified_query("SELECT foo FROM bar OFFSET 2 ROWS");
     assert_eq!(ast.offset, expect);
-    let ast = verified_query("SELECT foo FROM bar WHERE foo = 4 OFFSET 2 ROWS");
+    let ast = dialects.verified_query("SELECT foo FROM bar WHERE foo = 4 OFFSET 2 ROWS");
     assert_eq!(ast.offset, expect);
-    let ast = verified_query("SELECT foo FROM bar ORDER BY baz OFFSET 2 ROWS");
+    let ast = dialects.verified_query("SELECT foo FROM bar ORDER BY baz OFFSET 2 ROWS");
     assert_eq!(ast.offset, expect);
-    let ast = verified_query("SELECT foo FROM bar WHERE foo = 4 ORDER BY baz OFFSET 2 ROWS");
+    let ast =
+        dialects.verified_query("SELECT foo FROM bar WHERE foo = 4 ORDER BY baz OFFSET 2 ROWS");
     assert_eq!(ast.offset, expect);
-    let ast = verified_query("SELECT foo FROM (SELECT * FROM bar OFFSET 2 ROWS) OFFSET 2 ROWS");
+    let ast =
+        dialects.verified_query("SELECT foo FROM (SELECT * FROM bar OFFSET 2 ROWS) OFFSET 2 ROWS");
     assert_eq!(ast.offset, expect);
     match *ast.body {
         SetExpr::Select(s) => match only(s.from).relation {
@@ -7617,7 +7640,7 @@ fn parse_offset() {
         },
         _ => panic!("Test broke"),
     }
-    let ast = verified_query("SELECT 'foo' OFFSET 0 ROWS");
+    let ast = dialects.verified_query("SELECT 'foo' OFFSET 0 ROWS");
     assert_eq!(
         ast.offset,
         Some(Offset {
@@ -7625,7 +7648,7 @@ fn parse_offset() {
             rows: OffsetRows::Rows,
         })
     );
-    let ast = verified_query("SELECT 'foo' OFFSET 1 ROW");
+    let ast = dialects.verified_query("SELECT 'foo' OFFSET 1 ROW");
     assert_eq!(
         ast.offset,
         Some(Offset {
@@ -7633,7 +7656,7 @@ fn parse_offset() {
             rows: OffsetRows::Row,
         })
     );
-    let ast = verified_query("SELECT 'foo' OFFSET 1");
+    let ast = dialects.verified_query("SELECT 'foo' OFFSET 1");
     assert_eq!(
         ast.offset,
         Some(Offset {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org