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.
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 1af0022..0f3f564 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs
@@ -1608,6 +1608,13 @@ 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 0bbd859..41a8f87 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs
@@ -672,6 +672,10 @@ 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 @@ 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 @@ _ => 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 e1e397f..eb435f1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs
@@ -16714,7 +16714,7 @@ && 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 722cc14..9fb208b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs
@@ -20128,3 +20128,18 @@ all_dialects().verified_stmt("SELECT X'ab''cd'"); all_dialects().one_statement_parses_to("SELECT x'''' N", "SELECT X'''' AS N"); } + +#[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 d0027b0..0d08dc0 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs
@@ -10022,3 +10022,12 @@ pg().verified_stmt("SELECT ||/ -2"); pg().verified_stmt("SELECT |/ ||/ 2"); } + +#[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 2b16ffe..3722234 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs
@@ -4919,3 +4919,43 @@ snowflake().one_statement_parses_to("RM @````", "REMOVE @````"); snowflake().verified_stmt(r#"REMOVE @"stage""name""#); } + +#[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()), + ); +}