From 74c6e5788cb47c0693d305e285c96699c0944cea Mon Sep 17 00:00:00 2001 From: Ben Herzberg Date: Thu, 13 Aug 2026 17:35:13 +0300 Subject: [PATCH 1/6] Snowflake: Add support for ->> (pipe) operator for chaining SQL statements --- src/ast/mod.rs | 16 ++++++++++ src/ast/query.rs | 11 +++++++ src/ast/spans.rs | 2 ++ src/dialect/mod.rs | 8 +++++ src/dialect/snowflake.rs | 11 +++++++ src/parser/mod.rs | 41 +++++++++++++++++++++++++ tests/sqlparser_common.rs | 6 +++- tests/sqlparser_snowflake.rs | 58 ++++++++++++++++++++++++++++++++++++ 8 files changed, 152 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8a9a67a74..66042177a 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/mod.rs b/src/dialect/mod.rs index f99cbe2ea..275f47555 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 Snowflake `-->` flow/pipe operator for chaining + /// SQL statements? e.g. `SELECT * FROM t ->> SELECT * FROM $1` + /// + /// See + fn supports_snowflake_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..6c09390ed 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_snowflake_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); } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b2b3f42bb..2d49bf8ba 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_snowflake_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 { @@ -16593,6 +16616,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 @@ -16706,6 +16735,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_snowflake_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) = 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 0800bc41f..08e1907f5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1720,7 +1720,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_snowflake_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..d634fbe59 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4912,3 +4912,61 @@ 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"); + + // Pipe operator is not parsed in non-Snowflake dialects + use sqlparser::dialect::GenericDialect; + use sqlparser::parser::Parser; + // In a generic dialect, ->> is a binary operator, not a pipe + let stmts = Parser::parse_sql(&GenericDialect {}, "SELECT 1").unwrap(); + assert_eq!(stmts.len(), 1); +} + +#[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"), + } +} From 5500debc674e6a0bbe980b3138c3e6657c2d7ea8 Mon Sep 17 00:00:00 2001 From: Ben Herzberg Date: Tue, 18 Aug 2026 10:49:49 +0300 Subject: [PATCH 2/6] Fix typo in doc comment: --> should be ->> --- src/dialect/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 275f47555..5a9a82e5f 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -726,7 +726,7 @@ pub trait Dialect: Debug + Any { false } - /// Does the dialect support the Snowflake `-->` flow/pipe operator for chaining + /// Does the dialect support the Snowflake `->>` flow/pipe operator for chaining /// SQL statements? e.g. `SELECT * FROM t ->> SELECT * FROM $1` /// /// See From 480b848e53bc67a25ac8ffed42805930ca1ae913 Mon Sep 17 00:00:00 2001 From: Ben Herzberg Date: Tue, 18 Aug 2026 11:45:19 +0300 Subject: [PATCH 3/6] Snowflake: Fix pipe operator for CREATE DATABASE, SHOW, and add error handling for $0 --- src/dialect/snowflake.rs | 5 +++++ src/parser/mod.rs | 7 +++++++ tests/sqlparser_snowflake.rs | 30 +++++++++++++++++++++++++++--- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 6c09390ed..68f460a18 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -1210,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 2d49bf8ba..900e030ae 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13753,6 +13753,7 @@ impl<'a> Parser<'a> { Token::EOF | Token::Eq | Token::SemiColon | Token::VerticalBarRightAngleBracket => { break } + Token::LongArrow if self.dialect.supports_snowflake_pipe_operator() => break, _ => {} } self.advance_token(); @@ -16742,6 +16743,12 @@ impl<'a> Parser<'a> { if let Some(index_str) = s.strip_prefix('$') { if let Ok(index) = index_str.parse::() { self.next_token(); // consume the $n token + if index == 0 { + return Err(ParserError::ParserError( + "$0 is not a valid pipe result reference; indices start at $1" + .to_string(), + )); + } return Ok(TableFactor::PipeResultScan { index }); } } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index d634fbe59..ea2f8c7b7 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4930,12 +4930,36 @@ fn test_snowflake_pipe_operator() { snowflake() .verified_stmt("CREATE TABLE t (id INT) ->> INSERT INTO t VALUES (1) ->> SELECT * FROM $1"); - // Pipe operator is not parsed in non-Snowflake dialects + // 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( + "$0 is not a valid pipe result reference; indices start at $1".to_string() + )) + ); + + // In GenericDialect, ->> remains a binary (JSON extract) operator, not a pipe use sqlparser::dialect::GenericDialect; use sqlparser::parser::Parser; - // In a generic dialect, ->> is a binary operator, not a pipe - let stmts = Parser::parse_sql(&GenericDialect {}, "SELECT 1").unwrap(); + let stmts = Parser::parse_sql(&GenericDialect {}, "SELECT payload ->> 'name'").unwrap(); assert_eq!(stmts.len(), 1); + // In GenericDialect, SELECT 1 ->> SELECT 2 is a parse error (SELECT 2 is not an expression) + assert!(Parser::parse_sql(&GenericDialect {}, "SELECT 1 ->> SELECT 2").is_err()); } #[test] From aca1bb917d983dc117d9f5be36e81d6df937edde Mon Sep 17 00:00:00 2001 From: Ben Herzberg Date: Tue, 18 Aug 2026 11:48:28 +0300 Subject: [PATCH 4/6] Snowflake: Fix pipe operator for CREATE DATABASE, SHOW, and improve $0/$n validation --- src/parser/mod.rs | 8 +------- tests/sqlparser_snowflake.rs | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 900e030ae..9e3ec45a6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -16741,14 +16741,8 @@ impl<'a> Parser<'a> { if self.dialect.supports_snowflake_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) = index_str.parse::() { + if let Ok(index @ 1..) = index_str.parse::() { self.next_token(); // consume the $n token - if index == 0 { - return Err(ParserError::ParserError( - "$0 is not a valid pipe result reference; indices start at $1" - .to_string(), - )); - } return Ok(TableFactor::PipeResultScan { index }); } } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index ea2f8c7b7..c79df4592 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4949,7 +4949,7 @@ fn test_snowflake_pipe_operator() { assert_eq!( snowflake().parse_sql_statements("SELECT * FROM $0"), Err(ParserError::ParserError( - "$0 is not a valid pipe result reference; indices start at $1".to_string() + "Expected: identifier, found: $0".to_string() )) ); From bf765cd3da1d333ac5f96eae6e4f054e1a6c0a3c Mon Sep 17 00:00:00 2001 From: Ben Herzberg Date: Tue, 18 Aug 2026 12:02:18 +0300 Subject: [PATCH 5/6] Rename supports_snowflake_pipe_operator to supports_long_arrow_pipe_operator --- src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 6 +++--- src/dialect/snowflake.rs | 2 +- src/parser/mod.rs | 6 +++--- tests/sqlparser_common.rs | 2 +- tests/sqlparser_snowflake.rs | 9 ++++----- 6 files changed, 16 insertions(+), 13 deletions(-) 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 5a9a82e5f..a0fe78353 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -726,11 +726,11 @@ pub trait Dialect: Debug + Any { false } - /// Does the dialect support the Snowflake `->>` flow/pipe operator for chaining - /// SQL statements? e.g. `SELECT * FROM t ->> SELECT * FROM $1` + /// Does the dialect support the `->>` flow/pipe operator for chaining SQL statements? + /// e.g. `SELECT * FROM t ->> SELECT * FROM $1` /// /// See - fn supports_snowflake_pipe_operator(&self) -> bool { + fn supports_long_arrow_pipe_operator(&self) -> bool { false } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 68f460a18..588ed01dd 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -694,7 +694,7 @@ impl Dialect for SnowflakeDialect { true } - fn supports_snowflake_pipe_operator(&self) -> bool { + fn supports_long_arrow_pipe_operator(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9e3ec45a6..d783bb785 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -629,7 +629,7 @@ impl<'a> Parser<'a> { // Handle Snowflake pipe operator: chain multiple statements with ->> // See - if self.dialect.supports_snowflake_pipe_operator() + if self.dialect.supports_long_arrow_pipe_operator() && self.peek_token_ref().token == Token::LongArrow { let mut statements = vec![stmt]; @@ -13753,7 +13753,7 @@ impl<'a> Parser<'a> { Token::EOF | Token::Eq | Token::SemiColon | Token::VerticalBarRightAngleBracket => { break } - Token::LongArrow if self.dialect.supports_snowflake_pipe_operator() => break, + Token::LongArrow if self.dialect.supports_long_arrow_pipe_operator() => break, _ => {} } self.advance_token(); @@ -16738,7 +16738,7 @@ impl<'a> Parser<'a> { } else { // Handle Snowflake pipe result references ($1, $2, ...) in FROM clause. // See - if self.dialect.supports_snowflake_pipe_operator() { + 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::() { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 08e1907f5..ca011d52a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1723,7 +1723,7 @@ fn parse_json_ops_without_colon() { ( "->>", LongArrow, - all_dialects_except(|d| d.supports_snowflake_pipe_operator()), + all_dialects_except(|d| d.supports_long_arrow_pipe_operator()), ), ("#>", HashArrow, pg_and_generic()), ("#>>", HashLongArrow, pg_and_generic()), diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index c79df4592..74c1dadcc 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4953,13 +4953,12 @@ fn test_snowflake_pipe_operator() { )) ); - // In GenericDialect, ->> remains a binary (JSON extract) operator, not a pipe + // GenericDialect also supports pipe syntax (it is permissive by design) use sqlparser::dialect::GenericDialect; use sqlparser::parser::Parser; - let stmts = Parser::parse_sql(&GenericDialect {}, "SELECT payload ->> 'name'").unwrap(); - assert_eq!(stmts.len(), 1); - // In GenericDialect, SELECT 1 ->> SELECT 2 is a parse error (SELECT 2 is not an expression) - assert!(Parser::parse_sql(&GenericDialect {}, "SELECT 1 ->> SELECT 2").is_err()); + 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] From 64ef1bbfe929827d27e4a4f4cd66a25b1873dbe4 Mon Sep 17 00:00:00 2001 From: Ben Herzberg Date: Tue, 25 Aug 2026 19:23:08 +0300 Subject: [PATCH 6/6] Fold nested if-let chains in pipe result reference parsing --- src/parser/mod.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3c7f9aff5..871aeae9e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -16763,15 +16763,13 @@ 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. + // Handle 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 }); - } + if let Some(Ok(index @ 1..)) = s.strip_prefix('$').map(str::parse::) { + self.next_token(); // consume the $n token + return Ok(TableFactor::PipeResultScan { index }); } } }