You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by lu...@apache.org on 2026/09/21 20:30:22 UTC
(datafusion-sqlparser-rs) 01/01: Snowflake: Fix stage delimiter handling and gate stage table factor on dialect
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 <ca...@gmail.com>
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: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org