Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3592,6 +3592,13 @@ pub enum Statement {
/// SELECT
/// ```
Query(Box<Query>),
/// Snowflake pipe operator chain: `stmt1 ->> stmt2 ->> ...`
///
/// See <https://docs.snowflake.com/en/sql-reference/operators-flow>
Pipe {
/// The chained SQL statements separated by `->>`.
statements: Vec<Statement>,
},
/// ```sql
/// INSERT
/// ```
Expand Down Expand Up @@ -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 ")?;

Expand Down
11 changes: 11 additions & 0 deletions src/ast/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1722,6 +1722,14 @@ pub enum TableFactor {
/// The alias for the table.
alias: Option<TableAlias>,
},
/// Snowflake pipe result reference: `$1`, `$2`, etc.
///
/// Used in FROM clauses of pipe-chained statements to reference previous results.
/// See <https://docs.snowflake.com/en/sql-reference/operators-flow>
PipeResultScan {
/// 1-based index of the previous statement whose result is referenced.
index: u64,
},
/// Snowflake's SEMANTIC_VIEW function for semantic models.
///
/// <https://docs.snowflake.com/en/sql-reference/constructs/semantic_view>
Expand Down Expand Up @@ -2507,6 +2515,9 @@ impl fmt::Display for TableFactor {
}
Ok(())
}
TableFactor::PipeResultScan { index } => {
write!(f, "${index}")
}
TableFactor::SemanticView {
name,
dimensions,
Expand Down
2 changes: 2 additions & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
8 changes: 8 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://docs.snowflake.com/en/sql-reference/operators-flow>
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
Expand Down
16 changes: 16 additions & 0 deletions src/dialect/snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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),
}
}
Expand Down
42 changes: 42 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -625,11 +625,34 @@ impl<'a> Parser<'a> {
pub fn parse_statement(&mut self) -> Result<Statement, ParserError> {
let _guard = self.recursion_counter.try_decrease()?;

let stmt = self.parse_single_statement_no_pipe()?;

// Handle Snowflake pipe operator: chain multiple statements with ->>
// See <https://docs.snowflake.com/en/sql-reference/operators-flow>
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<Statement, ParserError> {
// 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<Statement, ParserError> {
let next_token = self.next_token();
match &next_token.token {
Token::Word(w) => match w.keyword {
Expand Down Expand Up @@ -13730,6 +13753,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();
Expand Down Expand Up @@ -16593,6 +16617,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
Expand Down Expand Up @@ -16706,6 +16736,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 <https://docs.snowflake.com/en/sql-reference/operators-flow>
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::<u64>() {
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 {
Expand Down
6 changes: 5 additions & 1 deletion tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_long_arrow_pipe_operator()),
),
("#>", HashArrow, pg_and_generic()),
("#>>", HashLongArrow, pg_and_generic()),
("@>", AtArrow, all_dialects()),
Expand Down
81 changes: 81 additions & 0 deletions tests/sqlparser_snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Comment thread
BenSatori marked this conversation as resolved.
// 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"),
}
}