Skip to content

Commit 2ea773a

Browse files
authored
Fixed select dollar column from stage for snowflake (#2165)
1 parent 798fbe4 commit 2ea773a

File tree

4 files changed

+62
-1
lines changed

4 files changed

+62
-1
lines changed

src/dialect/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub use self::mysql::MySqlDialect;
4949
pub use self::oracle::OracleDialect;
5050
pub use self::postgresql::PostgreSqlDialect;
5151
pub use self::redshift::RedshiftSqlDialect;
52+
pub use self::snowflake::parse_snowflake_stage_name;
5253
pub use self::snowflake::SnowflakeDialect;
5354
pub use self::sqlite::SQLiteDialect;
5455

src/dialect/snowflake.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1230,7 +1230,7 @@ pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result<Ident, ParserE
12301230
parser.prev_token();
12311231
break;
12321232
}
1233-
Token::RParen => {
1233+
Token::LParen | Token::RParen => {
12341234
parser.prev_token();
12351235
break;
12361236
}
@@ -1248,6 +1248,8 @@ pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result<Ident, ParserE
12481248
Ok(Ident::new(ident))
12491249
}
12501250

1251+
/// Parses a Snowflake stage name, which may start with `@` for internal stages.
1252+
/// Examples: `@mystage`, `@namespace.stage`, `schema.table`
12511253
pub fn parse_snowflake_stage_name(parser: &mut Parser) -> Result<ObjectName, ParserError> {
12521254
match parser.next_token().token {
12531255
Token::AtSign => {

src/parser/mod.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,6 +1284,11 @@ impl<'a> Parser<'a> {
12841284
// SQLite has single-quoted identifiers
12851285
id_parts.push(Ident::with_quote('\'', s))
12861286
}
1287+
Token::Placeholder(s) => {
1288+
// Snowflake uses $1, $2, etc. for positional column references
1289+
// in staged data queries like: SELECT t.$1 FROM @stage t
1290+
id_parts.push(Ident::new(s))
1291+
}
12871292
Token::Mul => {
12881293
return Ok(Expr::QualifiedWildcard(
12891294
ObjectName::from(id_parts),
@@ -1946,6 +1951,13 @@ impl<'a> Parser<'a> {
19461951
chain.push(AccessExpr::Dot(expr));
19471952
self.advance_token(); // The consumed string
19481953
}
1954+
Token::Placeholder(s) => {
1955+
// Snowflake uses $1, $2, etc. for positional column references
1956+
// in staged data queries like: SELECT t.$1 FROM @stage t
1957+
let expr = Expr::Identifier(Ident::with_span(next_token.span, s));
1958+
chain.push(AccessExpr::Dot(expr));
1959+
self.advance_token(); // The consumed placeholder
1960+
}
19491961
// Fallback to parsing an arbitrary expression, but restrict to expression
19501962
// types that are valid after the dot operator. This ensures that e.g.
19511963
// `T.interval` is parsed as a compound identifier, not as an interval
@@ -15435,6 +15447,9 @@ impl<'a> Parser<'a> {
1543515447
&& self.peek_keyword_with_tokens(Keyword::SEMANTIC_VIEW, &[Token::LParen])
1543615448
{
1543715449
self.parse_semantic_view_table_factor()
15450+
} else if self.peek_token_ref().token == Token::AtSign {
15451+
// Stage reference: @mystage or @namespace.stage (e.g. Snowflake)
15452+
self.parse_snowflake_stage_table_factor()
1543815453
} else {
1543915454
let name = self.parse_object_name(true)?;
1544015455

@@ -15531,6 +15546,37 @@ impl<'a> Parser<'a> {
1553115546
}
1553215547
}
1553315548

15549+
/// Parse a Snowflake stage reference as a table factor.
15550+
/// Handles syntax like: `@mystage1 (file_format => 'myformat', pattern => '...')`
15551+
///
15552+
/// See: <https://docs.snowflake.com/en/user-guide/querying-stage>
15553+
fn parse_snowflake_stage_table_factor(&mut self) -> Result<TableFactor, ParserError> {
15554+
// Parse the stage name starting with @
15555+
let name = crate::dialect::parse_snowflake_stage_name(self)?;
15556+
15557+
// Parse optional stage options like (file_format => 'myformat', pattern => '...')
15558+
let args = if self.consume_token(&Token::LParen) {
15559+
Some(self.parse_table_function_args()?)
15560+
} else {
15561+
None
15562+
};
15563+
15564+
let alias = self.maybe_parse_table_alias()?;
15565+
15566+
Ok(TableFactor::Table {
15567+
name,
15568+
alias,
15569+
args,
15570+
with_hints: vec![],
15571+
version: None,
15572+
partitions: vec![],
15573+
with_ordinality: false,
15574+
json_path: None,
15575+
sample: None,
15576+
index_hints: vec![],
15577+
})
15578+
}
15579+
1553415580
fn maybe_parse_table_sample(&mut self) -> Result<Option<Box<TableSample>>, ParserError> {
1553515581
let modifier = if self.parse_keyword(Keyword::TABLESAMPLE) {
1553615582
TableSampleModifier::TableSample

tests/sqlparser_snowflake.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4878,3 +4878,15 @@ fn test_truncate_table_if_exists() {
48784878
snowflake().verified_stmt("TRUNCATE TABLE my_table");
48794879
snowflake().verified_stmt("TRUNCATE IF EXISTS my_table");
48804880
}
4881+
4882+
#[test]
4883+
fn test_select_dollar_column_from_stage() {
4884+
// With table function args and alias
4885+
snowflake().verified_stmt("SELECT t.$1, t.$2 FROM @mystage1(file_format => 'myformat', pattern => '.*data.*[.]csv.gz') t");
4886+
// Without table function args, with alias
4887+
snowflake().verified_stmt("SELECT t.$1, t.$2 FROM @mystage1 t");
4888+
// Without table function args, without alias
4889+
snowflake().verified_stmt("SELECT $1, $2 FROM @mystage1");
4890+
// With table function args, without alias
4891+
snowflake().verified_stmt("SELECT $1, $2 FROM @mystage1(file_format => 'myformat')");
4892+
}

0 commit comments

Comments
 (0)