diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index d1728566e..63d8a05c2 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -49,6 +49,7 @@ pub use self::mysql::MySqlDialect; pub use self::oracle::OracleDialect; pub use self::postgresql::PostgreSqlDialect; pub use self::redshift::RedshiftSqlDialect; +pub use self::snowflake::parse_snowflake_stage_name; pub use self::snowflake::SnowflakeDialect; pub use self::sqlite::SQLiteDialect; use crate::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectNamePart, Statement}; diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index eade01c04..69635547f 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -1149,7 +1149,7 @@ pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result { + Token::LParen | Token::RParen => { parser.prev_token(); break; } @@ -1167,6 +1167,8 @@ pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result Result { match parser.next_token().token { Token::AtSign => { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6fd7b5ca4..6b63f47f9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1273,6 +1273,11 @@ impl<'a> Parser<'a> { // SQLite has single-quoted identifiers id_parts.push(Ident::with_quote('\'', s)) } + Token::Placeholder(s) => { + // Snowflake uses $1, $2, etc. for positional column references + // in staged data queries like: SELECT t.$1 FROM @stage t + id_parts.push(Ident::new(s)) + } Token::Mul => { return Ok(Expr::QualifiedWildcard( ObjectName::from(id_parts), @@ -1898,6 +1903,13 @@ impl<'a> Parser<'a> { chain.push(AccessExpr::Dot(expr)); self.advance_token(); // The consumed string } + Token::Placeholder(s) => { + // Snowflake uses $1, $2, etc. for positional column references + // in staged data queries like: SELECT t.$1 FROM @stage t + let expr = Expr::Identifier(Ident::with_span(next_token.span, s)); + chain.push(AccessExpr::Dot(expr)); + self.advance_token(); // The consumed placeholder + } // Fallback to parsing an arbitrary expression. _ => match self.parse_subexpr(self.dialect.prec_value(Precedence::Period))? { // If we get back a compound field access or identifier, @@ -15103,6 +15115,11 @@ impl<'a> Parser<'a> { && self.peek_keyword_with_tokens(Keyword::SEMANTIC_VIEW, &[Token::LParen]) { self.parse_semantic_view_table_factor() + } else if dialect_of!(self is SnowflakeDialect) + && self.peek_token_ref().token == Token::AtSign + { + // Snowflake stage reference: @mystage or @namespace.stage + self.parse_snowflake_stage_table_factor() } else { let name = self.parse_object_name(true)?; @@ -15199,6 +15216,35 @@ impl<'a> Parser<'a> { } } + /// Parse a Snowflake stage reference as a table factor. + /// Handles syntax like: `@mystage1 (file_format => 'myformat', pattern => '...')` + fn parse_snowflake_stage_table_factor(&mut self) -> Result { + // Parse the stage name starting with @ + let name = crate::dialect::parse_snowflake_stage_name(self)?; + + // Parse optional stage options like (file_format => 'myformat', pattern => '...') + let args = if self.consume_token(&Token::LParen) { + Some(self.parse_table_function_args()?) + } else { + None + }; + + let alias = self.maybe_parse_table_alias()?; + + Ok(TableFactor::Table { + name, + alias, + args, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }) + } + fn maybe_parse_table_sample(&mut self) -> Result>, ParserError> { let modifier = if self.parse_keyword(Keyword::TABLESAMPLE) { TableSampleModifier::TableSample diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 5889b2bd0..3e41b968b 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4533,3 +4533,8 @@ fn test_alter_external_table() { snowflake() .verified_stmt("ALTER EXTERNAL TABLE IF EXISTS some_table REFRESH 'year=2025/month=12/'"); } + +#[test] +fn test_select_dollar_column_from_stage() { + snowflake().verified_stmt("SELECT t.$1, t.$2 FROM @mystage1(file_format => 'myformat', pattern => '.*data.*[.]csv.gz') t"); +}