diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4d86dd6a6..3f4d93b06 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3592,6 +3592,13 @@ pub enum Statement { /// SELECT /// ``` Query(Box), + /// Snowflake pipe operator chain: `stmt1 ->> stmt2 ->> ...` + /// + /// See + Pipe { + /// The chained SQL statements separated by `->>`. + statements: Vec, + }, /// ```sql /// INSERT /// ``` @@ -5151,6 +5158,15 @@ impl fmt::Display for Statement { read = if *read_lock { " WITH READ LOCK" } else { "" } ) } + Statement::Pipe { statements } => { + for (i, stmt) in statements.iter().enumerate() { + if i > 0 { + f.write_str(" ->> ")?; + } + stmt.fmt(f)?; + } + Ok(()) + } Statement::Kill { modifier, id } => { write!(f, "KILL ")?; diff --git a/src/ast/query.rs b/src/ast/query.rs index 2ada46a9f..127044932 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1722,6 +1722,14 @@ pub enum TableFactor { /// The alias for the table. alias: Option, }, + /// Snowflake pipe result reference: `$1`, `$2`, etc. + /// + /// Used in FROM clauses of pipe-chained statements to reference previous results. + /// See + PipeResultScan { + /// 1-based index of the previous statement whose result is referenced. + index: u64, + }, /// Snowflake's SEMANTIC_VIEW function for semantic models. /// /// @@ -2507,6 +2515,9 @@ impl fmt::Display for TableFactor { } Ok(()) } + TableFactor::PipeResultScan { index } => { + write!(f, "${index}") + } TableFactor::SemanticView { name, dimensions, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index a34fe66d9..e96f705c0 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -488,6 +488,7 @@ impl Spanned for Statement { Statement::CreatePolicy { .. } => Span::empty(), Statement::AlterPolicy { .. } => Span::empty(), Statement::AlterConnector { .. } => Span::empty(), + Statement::Pipe { statements } => union_spans(statements.iter().map(|s| s.span())), Statement::DropPolicy { .. } => Span::empty(), Statement::DropConnector { .. } => Span::empty(), Statement::ShowCatalogs { .. } => Span::empty(), @@ -2108,6 +2109,7 @@ impl Spanned for TableFactor { .chain(where_clause.as_ref().map(|e| e.span())) .chain(alias.as_ref().map(|a| a.span())), ), + TableFactor::PipeResultScan { .. } => Span::empty(), TableFactor::OpenJsonTable { .. } => Span::empty(), } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index d408cb181..ee3516b8e 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -320,4 +320,8 @@ impl Dialect for GenericDialect { fn supports_aliased_function_args(&self) -> bool { true } + + fn supports_long_arrow_pipe_operator(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index f99cbe2ea..a0fe78353 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -726,6 +726,14 @@ pub trait Dialect: Debug + Any { false } + /// Does the dialect support the `->>` flow/pipe operator for chaining SQL statements? + /// e.g. `SELECT * FROM t ->> SELECT * FROM $1` + /// + /// See + fn supports_long_arrow_pipe_operator(&self) -> bool { + false + } + /// Does the dialect support MySQL-style `'user'@'host'` grantee syntax? fn supports_user_host_grantee(&self) -> bool { false diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 0bedb12a5..588ed01dd 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -445,6 +445,8 @@ impl Dialect for SnowflakeDialect { // Snowflake supports the `:` cast operator unlike other dialects match &token.token { Token::Colon => Some(Ok(self.prec_value(Precedence::DoubleColon))), + // ->> is the Snowflake pipe operator (statement-level), not a binary expression operator + Token::LongArrow => Some(Ok(self.prec_unknown())), _ => None, } } @@ -692,6 +694,10 @@ impl Dialect for SnowflakeDialect { true } + fn supports_long_arrow_pipe_operator(&self) -> bool { + true + } + fn supports_comma_separated_trim(&self) -> bool { true } @@ -1074,6 +1080,11 @@ pub fn parse_create_table( parser.prev_token(); break; } + Token::LongArrow => { + // Snowflake pipe operator terminates the statement + parser.prev_token(); + break; + } _ => { return parser.expected("end of statement", next_token); } @@ -1199,6 +1210,11 @@ pub fn parse_create_database( _ => return parser.expected("end of statement", next_token), }, Token::SemiColon | Token::EOF => break, + Token::LongArrow => { + // Snowflake pipe operator terminates the statement + parser.prev_token(); + break; + } _ => return parser.expected("end of statement", next_token), } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 953453a22..3c7f9aff5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -625,11 +625,34 @@ impl<'a> Parser<'a> { pub fn parse_statement(&mut self) -> Result { let _guard = self.recursion_counter.try_decrease()?; + let stmt = self.parse_single_statement_no_pipe()?; + + // Handle Snowflake pipe operator: chain multiple statements with ->> + // See + if self.dialect.supports_long_arrow_pipe_operator() + && self.peek_token_ref().token == Token::LongArrow + { + let mut statements = vec![stmt]; + while self.consume_token(&Token::LongArrow) { + statements.push(self.parse_single_statement_no_pipe()?); + } + return Ok(Statement::Pipe { statements }); + } + + Ok(stmt) + } + + /// Parse a single statement without pipe-chain handling. + /// Invokes the dialect override first, then falls back to the standard body. + fn parse_single_statement_no_pipe(&mut self) -> Result { // allow the dialect to override statement parsing if let Some(statement) = self.dialect.parse_statement(self) { return statement; } + self.parse_statement_body() + } + fn parse_statement_body(&mut self) -> Result { let next_token = self.next_token(); match &next_token.token { Token::Word(w) => match w.keyword { @@ -13757,6 +13780,7 @@ impl<'a> Parser<'a> { Token::EOF | Token::Eq | Token::SemiColon | Token::VerticalBarRightAngleBracket => { break } + Token::LongArrow if self.dialect.supports_long_arrow_pipe_operator() => break, _ => {} } self.advance_token(); @@ -16620,6 +16644,12 @@ impl<'a> Parser<'a> { .to_string(), )) } + TableFactor::PipeResultScan { .. } => { + return Err(ParserError::ParserError( + "alias after parenthesized pipe result scan is not supported" + .to_string(), + )) + } }; } // Do not store the extra set of parens in the AST @@ -16733,6 +16763,18 @@ impl<'a> Parser<'a> { // Stage reference: @mystage or @namespace.stage (e.g. Snowflake) self.parse_snowflake_stage_table_factor() } else { + // Handle Snowflake pipe result references ($1, $2, ...) in FROM clause. + // See + if self.dialect.supports_long_arrow_pipe_operator() { + if let Token::Placeholder(ref s) = self.peek_token_ref().token.clone() { + if let Some(index_str) = s.strip_prefix('$') { + if let Ok(index @ 1..) = index_str.parse::() { + self.next_token(); // consume the $n token + return Ok(TableFactor::PipeResultScan { index }); + } + } + } + } let name = self.parse_object_name(true)?; let json_path = match &self.peek_token_ref().token { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c8a2453aa..0c934fb70 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1735,7 +1735,11 @@ fn parse_json_ops_without_colon() { all_dialects_except(|d| d.supports_lambda_functions()), ), ("->", Arrow, pg_and_generic()), - ("->>", LongArrow, all_dialects()), + ( + "->>", + LongArrow, + all_dialects_except(|d| d.supports_long_arrow_pipe_operator()), + ), ("#>", HashArrow, pg_and_generic()), ("#>>", HashLongArrow, pg_and_generic()), ("@>", AtArrow, all_dialects()), diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 059560dcc..74c1dadcc 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4912,3 +4912,84 @@ 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_snowflake_pipe_operator() { + // Basic pipe: two SELECT statements chained + snowflake().verified_stmt("SELECT * FROM tablename ->> SELECT * FROM $1"); + + // Three statements chained + snowflake().verified_stmt( + "SELECT * FROM dept WHERE dname = 'SALES' ->> SELECT * FROM emp WHERE deptno IN (SELECT deptno FROM $1) ->> SELECT ename, sal FROM $1 ORDER BY 2 DESC", + ); + + // Reference to a non-adjacent prior result using $2 + snowflake().verified_stmt("SELECT a FROM t ->> SELECT b FROM t2 ->> SELECT $1 FROM $2"); + + // Non-SELECT statements in the chain (CREATE/INSERT) + snowflake() + .verified_stmt("CREATE TABLE t (id INT) ->> INSERT INTO t VALUES (1) ->> SELECT * FROM $1"); + + // Exact documented SHOW shape from Snowflake docs + snowflake() + .verified_stmt(r#"SHOW WAREHOUSES ->> SELECT "name", "state", "type", "size" FROM $1"#); + + // CREATE DATABASE piped into SELECT + snowflake().verified_stmt("CREATE DATABASE d ->> SELECT 1"); + + // Error: trailing ->> with no following statement + assert_eq!( + snowflake().parse_sql_statements("SELECT 1 ->>"), + Err(ParserError::ParserError( + "Expected: an SQL statement, found: EOF".to_string() + )) + ); + + // Error: $0 is not a valid pipe result reference + assert_eq!( + snowflake().parse_sql_statements("SELECT * FROM $0"), + Err(ParserError::ParserError( + "Expected: identifier, found: $0".to_string() + )) + ); + + // GenericDialect also supports pipe syntax (it is permissive by design) + use sqlparser::dialect::GenericDialect; + use sqlparser::parser::Parser; + Parser::parse_sql(&GenericDialect {}, "SELECT * FROM t ->> SELECT * FROM $1").unwrap(); + // JSON ->> binary operator still works inside expressions + Parser::parse_sql(&GenericDialect {}, "SELECT payload ->> 'name'").unwrap(); +} + +#[test] +fn test_snowflake_pipe_result_scan() { + // $1 in FROM clause is parsed as PipeResultScan { index: 1 } + let stmt = snowflake().verified_stmt("SELECT * FROM $1"); + match stmt { + Statement::Query(q) => { + if let SetExpr::Select(sel) = q.body.as_ref() { + if let TableFactor::PipeResultScan { index } = &sel.from[0].relation { + assert_eq!(*index, 1); + } else { + panic!("expected PipeResultScan"); + } + } + } + _ => panic!("expected Query"), + } + + // $3 is also valid + let stmt2 = snowflake().verified_stmt("SELECT * FROM $3"); + match stmt2 { + Statement::Query(q) => { + if let SetExpr::Select(sel) = q.body.as_ref() { + if let TableFactor::PipeResultScan { index } = &sel.from[0].relation { + assert_eq!(*index, 3); + } else { + panic!("expected PipeResultScan"); + } + } + } + _ => panic!("expected Query"), + } +}