Skip to content

Commit a0caf12

Browse files
wojpadloWojciech Padłoclaude
authored
Task LAV-2255: accept parenthesized (CALL …) as a RESULTSET payload (#2663)
Add a SetExpr::Call variant and a supports_call_in_resultset dialect flag to the vendored parser so `(CALL p(args))` is accepted in the RESULTSET initializer (DEFAULT / :=, DECLARE and LET) and bare-assignment positions, and rejected — at the CALL keyword, matching Snowflake's `unexpected 'CALL'` — for the unparenthesized and CURSOR FOR forms. Lowering unwraps the CALL payload through call_to_select in query_to_statement, so every static-inline consumer (RETURN TABLE, FOR loop, cursor) streams the callee's rows under the callee's own columns; the bare-assignment runtime path re-parses the stored `CALL …` text through __snowflake$prepare_query. Also resolve `:param`/`:local` refs in a declared-column RETURN TABLE(rs) query (the untyped RETURNS TABLE() path already did), so a table callee whose resultset references its parameters works. Co-authored-by: Wojciech Padło <wojciech.padlo@localstack.cloud> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 510f3e4 commit a0caf12

6 files changed

Lines changed: 217 additions & 15 deletions

File tree

‎src/ast/query.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,11 @@ pub enum SetExpr {
185185
/// payload SQL is only known at runtime, so the contained statement carries
186186
/// the dynamic-SQL expression rather than a static query body.
187187
Execute(Statement),
188+
/// `CALL` statement used as the query payload of a Snowflake `RESULTSET`
189+
/// declaration or bare assignment (`RESULTSET DEFAULT (CALL p(...))`,
190+
/// `r := (CALL p(...))`). The callee's result set becomes the resultset's
191+
/// rows.
192+
Call(Statement),
188193
}
189194

190195
impl SetExpr {
@@ -215,6 +220,7 @@ impl fmt::Display for SetExpr {
215220
SetExpr::Table(t) => t.fmt(f),
216221
SetExpr::Show(v) => v.fmt(f),
217222
SetExpr::Execute(v) => v.fmt(f),
223+
SetExpr::Call(v) => v.fmt(f),
218224
SetExpr::SetOperation {
219225
left,
220226
right,

‎src/ast/spans.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ impl Spanned for SetExpr {
238238
SetExpr::Merge(statement) => statement.span(),
239239
SetExpr::Show(statement) => statement.span(),
240240
SetExpr::Execute(statement) => statement.span(),
241+
SetExpr::Call(statement) => statement.span(),
241242
}
242243
}
243244
}

‎src/dialect/mod.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,14 @@ pub trait Dialect: Debug + Any {
451451
false
452452
}
453453

454+
/// Returns true if the dialect accepts a parenthesized `CALL` statement as
455+
/// the query payload of a `RESULTSET` declaration or bare assignment
456+
/// (`res RESULTSET DEFAULT (CALL p(...))`, `res := (CALL p(...))`). Unlike
457+
/// `SHOW`, this is not accepted in a `CURSOR FOR` payload.
458+
fn supports_call_in_resultset(&self) -> bool {
459+
false
460+
}
461+
454462
/// Returns true if the dialect supports the MATCH_RECOGNIZE operation.
455463
fn supports_match_recognize(&self) -> bool {
456464
false

‎src/dialect/snowflake.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@ impl Dialect for SnowflakeDialect {
230230
true
231231
}
232232

233+
/// See <https://docs.snowflake.com/en/developer-guide/snowflake-scripting/resultsets>
234+
fn supports_call_in_resultset(&self) -> bool {
235+
true
236+
}
237+
233238
fn supports_match_recognize(&self) -> bool {
234239
true
235240
}

‎src/parser/mod.rs‎

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,12 +1104,13 @@ impl<'a> Parser<'a> {
11041104
&& self.peek_nth_token_ref(1).token == Token::Assignment
11051105
{
11061106
// bare assignment: var := expr
1107-
// Accepts `(EXECUTE IMMEDIATE …)` / `(SHOW …)` payloads the same
1108-
// way a RESULTSET declaration initializer does, so a dynamic
1109-
// query can be assigned to an existing RESULTSET variable.
1107+
// Accepts `(EXECUTE IMMEDIATE …)` / `(SHOW …)` / `(CALL …)`
1108+
// payloads the same way a RESULTSET declaration initializer
1109+
// does, so a dynamic query or procedure call can be assigned to
1110+
// an existing RESULTSET variable.
11101111
let target = self.parse_identifier()?;
11111112
self.expect_token(&Token::Assignment)?;
1112-
let value = self.parse_snowflake_declaration_payload_expr()?;
1113+
let value = self.parse_snowflake_declaration_payload_expr(true)?;
11131114
Statement::Assignment { target, value }
11141115
} else if let Some(kind) = loop_control_keyword(&self.peek_nth_token_ref(0).token) {
11151116
// Loop-control statements only make sense inside a scripting
@@ -8717,14 +8718,14 @@ impl<'a> Parser<'a> {
87178718
Some(DeclareType::Cursor),
87188719
None,
87198720
Some(DeclareAssignment::For(Box::new(
8720-
self.parse_snowflake_declaration_payload_expr()?,
8721+
self.parse_snowflake_declaration_payload_expr(false)?,
87218722
))),
87228723
None,
87238724
),
87248725
}
87258726
} else if self.parse_keyword(Keyword::RESULTSET) {
87268727
let assigned_expr = if self.peek_token_ref().token != Token::SemiColon {
8727-
self.parse_snowflake_variable_declaration_expression()?
8728+
self.parse_snowflake_variable_declaration_expression(true)?
87288729
} else {
87298730
// Nothing more to do. The statement has no further parameters.
87308731
None
@@ -8743,13 +8744,13 @@ impl<'a> Parser<'a> {
87438744
} else {
87448745
// Without an explicit keyword, the only valid option is variable declaration.
87458746
let (assigned_expr, data_type) = if let Some(assigned_expr) =
8746-
self.parse_snowflake_variable_declaration_expression()?
8747+
self.parse_snowflake_variable_declaration_expression(false)?
87478748
{
87488749
(Some(assigned_expr), None)
87498750
} else if let Token::Word(_) = &self.peek_token_ref().token {
87508751
let data_type = self.parse_data_type()?;
87518752
(
8752-
self.parse_snowflake_variable_declaration_expression()?,
8753+
self.parse_snowflake_variable_declaration_expression(false)?,
87538754
Some(data_type),
87548755
)
87558756
} else {
@@ -8860,18 +8861,19 @@ impl<'a> Parser<'a> {
88608861
/// <https://docs.snowflake.com/en/sql-reference/snowflake-scripting/declare#variable-declaration-syntax>
88618862
pub fn parse_snowflake_variable_declaration_expression(
88628863
&mut self,
8864+
allow_call: bool,
88638865
) -> Result<Option<DeclareAssignment>, ParserError> {
88648866
Ok(match &self.peek_token_ref().token {
88658867
Token::Word(w) if w.keyword == Keyword::DEFAULT => {
88668868
self.next_token(); // Skip `DEFAULT`
88678869
Some(DeclareAssignment::Default(Box::new(
8868-
self.parse_snowflake_declaration_payload_expr()?,
8870+
self.parse_snowflake_declaration_payload_expr(allow_call)?,
88698871
)))
88708872
}
88718873
Token::Assignment => {
88728874
self.next_token(); // Skip `:=`
88738875
Some(DeclareAssignment::DuckAssignment(Box::new(
8874-
self.parse_snowflake_declaration_payload_expr()?,
8876+
self.parse_snowflake_declaration_payload_expr(allow_call)?,
88758877
)))
88768878
}
88778879
_ => None,
@@ -8880,11 +8882,64 @@ impl<'a> Parser<'a> {
88808882

88818883
/// Parses the expression payload of a Snowflake `RESULTSET` / `CURSOR`
88828884
/// declaration. Identical to [`Parser::parse_expr`] except that, under a
8883-
/// dialect that allows it, a parenthesized `SHOW` statement is accepted as
8884-
/// the query payload and wrapped as an [`Expr::Subquery`] whose body is a
8885-
/// [`SetExpr::Show`]. The pre-existing `(SELECT ...)` subquery path is left
8886-
/// untouched.
8887-
fn parse_snowflake_declaration_payload_expr(&mut self) -> Result<Expr, ParserError> {
8885+
/// dialect that allows it, a parenthesized `SHOW` / `EXECUTE IMMEDIATE`
8886+
/// statement is accepted as the query payload and wrapped as an
8887+
/// [`Expr::Subquery`] whose body is a [`SetExpr::Show`] / [`SetExpr::Execute`].
8888+
/// When `allow_call` is set, a parenthesized `CALL` is likewise accepted and
8889+
/// wrapped as a [`SetExpr::Call`] — the `RESULTSET` initializer and
8890+
/// bare-assignment positions pass `true`, the `CURSOR FOR` position passes
8891+
/// `false` (Snowflake rejects `CURSOR FOR (CALL ...)`). The pre-existing
8892+
/// `(SELECT ...)` subquery path is left untouched.
8893+
fn parse_snowflake_declaration_payload_expr(
8894+
&mut self,
8895+
allow_call: bool,
8896+
) -> Result<Expr, ParserError> {
8897+
let is_paren_call = allow_call
8898+
&& self.dialect.supports_call_in_resultset()
8899+
&& self.peek_nth_token_ref(0).token == Token::LParen
8900+
&& matches!(
8901+
&self.peek_nth_token_ref(1).token,
8902+
Token::Word(w) if w.keyword == Keyword::CALL
8903+
);
8904+
if is_paren_call {
8905+
self.expect_token(&Token::LParen)?;
8906+
self.expect_keyword_is(Keyword::CALL)?;
8907+
let call = self.parse_call()?;
8908+
self.expect_token(&Token::RParen)?;
8909+
return Ok(Expr::Subquery(Box::new(Query {
8910+
with: None,
8911+
body: Box::new(SetExpr::Call(call)),
8912+
order_by: None,
8913+
limit_clause: None,
8914+
fetch: None,
8915+
locks: vec![],
8916+
for_clause: None,
8917+
settings: None,
8918+
format_clause: None,
8919+
pipe_operators: vec![],
8920+
})));
8921+
}
8922+
// Reject a `CALL` payload the accepted arm above did not consume. A bare
8923+
// (unparenthesized) `CALL` is never a valid payload expression, and a
8924+
// `(CALL ...)` in a position that forbids the procedure-call payload
8925+
// (`CURSOR FOR`, or any dialect without `supports_call_in_resultset`) is
8926+
// likewise invalid. Snowflake rejects both at the `CALL` keyword itself;
8927+
// the fork would otherwise parse `CALL` as a bare identifier and
8928+
// misreport the following token. Point the error at the `CALL` keyword.
8929+
let bare_call =
8930+
matches!(&self.peek_nth_token_ref(0).token, Token::Word(w) if w.keyword == Keyword::CALL);
8931+
let paren_call = self.peek_nth_token_ref(0).token == Token::LParen
8932+
&& matches!(
8933+
&self.peek_nth_token_ref(1).token,
8934+
Token::Word(w) if w.keyword == Keyword::CALL
8935+
);
8936+
if bare_call {
8937+
return self.expected_ref("an expression", self.peek_nth_token_ref(0));
8938+
}
8939+
if paren_call {
8940+
self.expect_token(&Token::LParen)?;
8941+
return self.expected_ref("an expression", self.peek_nth_token_ref(0));
8942+
}
88888943
let is_paren_execute = self.dialect.supports_execute_immediate()
88898944
&& self.peek_nth_token_ref(0).token == Token::LParen
88908945
&& matches!(

‎tests/sqlparser_snowflake.rs‎

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2589,6 +2589,133 @@ fn parse_snowflake_declare_show_payload() {
25892589
}
25902590
}
25912591

2592+
#[test]
2593+
fn parse_snowflake_declare_call_payload() {
2594+
// The `CALL` payload embedded in a declaration's parenthesized subquery.
2595+
fn payload_call(stmt: Statement) -> Statement {
2596+
match stmt {
2597+
Statement::Declare { mut stmts } => {
2598+
assert_eq!(1, stmts.len());
2599+
let Declare { assignment, .. } = stmts.swap_remove(0);
2600+
let query = match assignment {
2601+
Some(DeclareAssignment::Default(expr))
2602+
| Some(DeclareAssignment::DuckAssignment(expr)) => match *expr {
2603+
Expr::Subquery(query) => query,
2604+
other => panic!("expected subquery payload, got {other:?}"),
2605+
},
2606+
other => panic!("unexpected declaration payload: {other:?}"),
2607+
};
2608+
match *query.body {
2609+
SetExpr::Call(call) => call,
2610+
other => panic!("expected CALL body, got {other:?}"),
2611+
}
2612+
}
2613+
other => panic!("expected DECLARE, got {other:?}"),
2614+
}
2615+
}
2616+
2617+
for sql in [
2618+
"DECLARE res RESULTSET DEFAULT (CALL p())",
2619+
"DECLARE res RESULTSET := (CALL p(1, 2))",
2620+
] {
2621+
// Round-trips through Display, and the embedded statement is a CALL.
2622+
let stmt = snowflake().verified_stmt(sql);
2623+
assert!(matches!(payload_call(stmt), Statement::Call(_)));
2624+
}
2625+
2626+
// Mid-body `LET res RESULTSET := (CALL p())` routes through the same
2627+
// declaration shape.
2628+
let sql = r#"CREATE PROCEDURE p() RETURNS VARCHAR LANGUAGE SQL AS $$
2629+
BEGIN
2630+
LET res RESULTSET := (CALL q(1));
2631+
RETURN 'OK';
2632+
END $$"#;
2633+
let stmts = snowflake()
2634+
.parse_sql_statements(sql)
2635+
.expect("LET RESULTSET := (CALL ...) should parse");
2636+
let body = match &stmts[0] {
2637+
Statement::CreateProcedure { body, .. } => body,
2638+
other => panic!("expected CreateProcedure, got {other:?}"),
2639+
};
2640+
let begin_stmts = match body {
2641+
ConditionalStatements::BeginEnd(bes) => &bes.statements,
2642+
other => panic!("expected BeginEnd body, got {other:?}"),
2643+
};
2644+
let decl = match &begin_stmts[0] {
2645+
Statement::Declare { stmts } => &stmts[0],
2646+
other => panic!("expected Declare, got {other:?}"),
2647+
};
2648+
assert_eq!(decl.declare_type, Some(DeclareType::ResultSet));
2649+
match &decl.assignment {
2650+
Some(DeclareAssignment::DuckAssignment(expr)) => match expr.as_ref() {
2651+
Expr::Subquery(query) => assert!(matches!(*query.body, SetExpr::Call(_))),
2652+
other => panic!("expected subquery payload, got {other:?}"),
2653+
},
2654+
other => panic!("expected RESULTSET := assignment, got {other:?}"),
2655+
}
2656+
2657+
// Bare assignment `res := (CALL p())` to an already-declared RESULTSET.
2658+
let sql = r#"CREATE PROCEDURE p() RETURNS VARCHAR LANGUAGE SQL AS $$
2659+
BEGIN
2660+
res := (CALL q(3));
2661+
RETURN 'OK';
2662+
END $$"#;
2663+
let stmts = snowflake()
2664+
.parse_sql_statements(sql)
2665+
.expect("bare res := (CALL ...) should parse");
2666+
let body = match &stmts[0] {
2667+
Statement::CreateProcedure { body, .. } => body,
2668+
other => panic!("expected CreateProcedure, got {other:?}"),
2669+
};
2670+
let begin_stmts = match body {
2671+
ConditionalStatements::BeginEnd(bes) => &bes.statements,
2672+
other => panic!("expected BeginEnd body, got {other:?}"),
2673+
};
2674+
match &begin_stmts[0] {
2675+
Statement::Assignment { target, value } => {
2676+
assert_eq!(target.value, "res");
2677+
match value {
2678+
Expr::Subquery(query) => assert!(matches!(*query.body, SetExpr::Call(_))),
2679+
other => panic!("expected subquery payload, got {other:?}"),
2680+
}
2681+
}
2682+
other => panic!("expected Assignment, got {other:?}"),
2683+
}
2684+
2685+
// The pre-existing SELECT / SHOW / EXECUTE payloads are unaffected.
2686+
snowflake().verified_stmt("DECLARE res RESULTSET DEFAULT (SELECT price FROM invoices)");
2687+
2688+
// Parentheses are required: a bare `CALL` payload is rejected at the `CALL`
2689+
// keyword (Snowflake's `unexpected 'CALL'`), never parsed as an identifier.
2690+
assert_eq!(
2691+
ParserError::ParserError("Expected: an expression, found: CALL".to_owned()),
2692+
snowflake()
2693+
.parse_sql_statements("DECLARE res RESULTSET DEFAULT CALL p()")
2694+
.unwrap_err()
2695+
);
2696+
assert_eq!(
2697+
ParserError::ParserError("Expected: an expression, found: CALL".to_owned()),
2698+
snowflake()
2699+
.parse_sql_statements("DECLARE res RESULTSET := CALL p()")
2700+
.unwrap_err()
2701+
);
2702+
2703+
// `CURSOR FOR (CALL ...)` is rejected at the `CALL` keyword; the CALL
2704+
// payload is only accepted in the RESULTSET / bare-assignment positions.
2705+
assert_eq!(
2706+
ParserError::ParserError("Expected: an expression, found: CALL".to_owned()),
2707+
snowflake()
2708+
.parse_sql_statements("DECLARE cur CURSOR FOR (CALL p())")
2709+
.unwrap_err()
2710+
);
2711+
2712+
// Under a non-Snowflake dialect the parenthesized CALL payload still errors.
2713+
let generic = TestedDialects::new(vec![Box::new(GenericDialect {})]);
2714+
assert!(generic
2715+
.parse_sql_statements("DECLARE res RESULTSET DEFAULT (CALL p())")
2716+
.is_err());
2717+
}
2718+
25922719
#[test]
25932720
fn parse_snowflake_declare_exception() {
25942721
for (sql, expected_name, expected_assigned_expr) in [

0 commit comments

Comments
 (0)