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

LucaCappelletti94 pushed a commit to branch snowflake-stage-table-factor-fixes
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git

commit 937d808ac74ebd4c308c2042fee3b2d902337a57
Author: LucaCappelletti94 <[email protected]>
AuthorDate: Mon Sep 21 22:27:33 2026 +0200

    Snowflake: Fix stage delimiter handling and gate stage table factor on 
dialect
    
    - Gate parse_snowflake_stage_table_factor behind Dialect::supports_stages(),
      preventing non-Snowflake dialects (such as PostgreSQL) from parsing 
@stage as
      a table factor.
    - In parse_stage_name_identifier, handle ',' and ';' delimiters without 
consuming
      them, and reject empty or bare '@' stage names.
---
 src/dialect/mod.rs           |  7 +++++++
 src/dialect/snowflake.rs     | 15 +++++++++------
 src/parser/mod.rs            |  2 +-
 tests/sqlparser_common.rs    | 15 +++++++++++++++
 tests/sqlparser_postgres.rs  |  9 +++++++++
 tests/sqlparser_snowflake.rs | 40 ++++++++++++++++++++++++++++++++++++++++
 6 files changed, 81 insertions(+), 7 deletions(-)

diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index 7c4744c5..ddb6e219 100644
--- a/src/dialect/mod.rs
+++ b/src/dialect/mod.rs
@@ -1584,6 +1584,13 @@ pub trait Dialect: Debug + Any {
         false
     }
 
+    /// Returns true if this dialect supports Snowflake-style stages in table 
factors (e.g. `@stage`).
+    ///
+    /// [Snowflake](https://docs.snowflake.com/en/user-guide/querying-stage)
+    fn supports_stages(&self) -> bool {
+        false
+    }
+
     /// Support quote delimited string literals, e.g. `Q'{...}'`
     ///
     /// 
[Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Literals.html#GUID-1824CBAA-6E16-4921-B2A6-112FB02248DA)
diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs
index 0bbd859e..41a8f870 100644
--- a/src/dialect/snowflake.rs
+++ b/src/dialect/snowflake.rs
@@ -672,6 +672,10 @@ impl Dialect for SnowflakeDialect {
         true
     }
 
+    fn supports_stages(&self) -> bool {
+        true
+    }
+
     /// See <https://docs.snowflake.com/en/sql-reference/sql/select#parameters>
     fn supports_select_wildcard_replace(&self) -> bool {
         true
@@ -1314,12 +1318,8 @@ pub fn parse_stage_name_identifier(parser: &mut Parser) 
-> Result<Ident, ParserE
     let mut ident = String::new();
     while let Some(next_token) = parser.next_token_no_skip() {
         match &next_token.token {
-            Token::Whitespace(_) | Token::SemiColon => break,
-            Token::Period => {
-                parser.prev_token();
-                break;
-            }
-            Token::LParen | Token::RParen => {
+            Token::Whitespace(_) => break,
+            Token::Period | Token::Comma | Token::SemiColon | Token::LParen | 
Token::RParen => {
                 parser.prev_token();
                 break;
             }
@@ -1336,6 +1336,9 @@ pub fn parse_stage_name_identifier(parser: &mut Parser) 
-> Result<Ident, ParserE
             _ => return parser.expected_ref("stage name identifier", 
parser.peek_token_ref()),
         }
     }
+    if ident.is_empty() || ident == "@" {
+        return parser.expected_ref("stage name identifier", 
parser.peek_token_ref());
+    }
     Ok(Ident::new(ident))
 }
 
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 15f135ff..2191435d 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -16742,7 +16742,7 @@ impl<'a> Parser<'a> {
             && self.peek_keyword_with_tokens(Keyword::SEMANTIC_VIEW, 
&[Token::LParen])
         {
             self.parse_semantic_view_table_factor()
-        } else if self.peek_token_ref().token == Token::AtSign {
+        } else if self.dialect.supports_stages() && 
self.peek_token_ref().token == Token::AtSign {
             // Stage reference: @mystage or @namespace.stage (e.g. Snowflake)
             self.parse_snowflake_stage_table_factor()
         } else {
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index c4aa607d..9e0042ef 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -20080,3 +20080,18 @@ fn parse_unary_minus_never_renders_line_comment() {
     all_dialects().verified_stmt("SELECT -1");
     all_dialects().verified_stmt("SELECT -x");
 }
+
+#[test]
+fn parse_stage_table_factor() {
+    let supported = all_dialects_where(|d| d.supports_stages());
+    supported.verified_stmt("SELECT * FROM @stage");
+    supported.verified_stmt("SELECT * FROM @stage, my_table");
+
+    let unsupported = all_dialects_where(|d| !d.supports_stages() && 
!d.is_identifier_start('@'));
+    assert_eq!(
+        unsupported
+            .parse_sql_statements("SELECT * FROM @stage")
+            .unwrap_err(),
+        ParserError::ParserError("Expected: identifier, found: @".to_string()),
+    );
+}
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index ab2d4b8e..e4946fc1 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -9984,3 +9984,12 @@ fn parse_unary_minus_before_pg_prefix_operators() {
     pg().verified_stmt("SELECT - @ 2");
     pg().one_statement_parses_to("SELECT - #x", "SELECT - # x");
 }
+
+#[test]
+fn parse_stage_table_factor_rejected() {
+    let sql = "SELECT * FROM @stage";
+    assert_eq!(
+        pg().parse_sql_statements(sql).unwrap_err(),
+        ParserError::ParserError("Expected: identifier, found: @".to_string()),
+    );
+}
diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs
index 059560dc..174bd197 100644
--- a/tests/sqlparser_snowflake.rs
+++ b/tests/sqlparser_snowflake.rs
@@ -4912,3 +4912,43 @@ fn test_select_dollar_column_from_stage() {
     // With table function args, without alias
     snowflake().verified_stmt("SELECT $1, $2 FROM @mystage1(file_format => 
'myformat')");
 }
+
+#[test]
+fn test_stage_name_delimiters() {
+    snowflake().verified_stmt("SELECT * FROM @stage1, @stage2");
+    snowflake().verified_stmt("SELECT * FROM @stage, my_table");
+    snowflake().verified_stmt("SELECT * FROM my_table, @stage");
+    snowflake().verified_stmt("SELECT * FROM @namespace.stage_name, item");
+    snowflake().verified_stmt("SELECT * FROM @stage(file_format => 
'myformat'), my_table");
+    snowflake().verified_stmt("SELECT * FROM @stage AS s, my_table");
+    snowflake().verified_stmt("SELECT * FROM @stage s, my_table");
+    let stmts = snowflake()
+        .parse_sql_statements("SELECT * FROM @stage; SELECT 1")
+        .unwrap();
+    assert_eq!(stmts.len(), 2);
+
+    assert_eq!(
+        snowflake()
+            .parse_sql_statements("SELECT * FROM @")
+            .unwrap_err(),
+        ParserError::ParserError("Expected: stage name identifier, found: 
EOF".to_string()),
+    );
+    assert_eq!(
+        snowflake()
+            .parse_sql_statements("SELECT * FROM @;")
+            .unwrap_err(),
+        ParserError::ParserError("Expected: stage name identifier, found: 
;".to_string()),
+    );
+    assert_eq!(
+        snowflake()
+            .parse_sql_statements("SELECT * FROM @, item")
+            .unwrap_err(),
+        ParserError::ParserError("Expected: stage name identifier, found: 
,".to_string()),
+    );
+    assert_eq!(
+        snowflake()
+            .parse_sql_statements("SELECT * FROM @.stage")
+            .unwrap_err(),
+        ParserError::ParserError("Expected: stage name identifier, found: 
.".to_string()),
+    );
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to