Skip to content

Commit 2b9fdc4

Browse files
Snowflake: parse FETCH INTO, CALL INTO, ALTER PROCEDURE, WITH AS PROCEDURE
1 parent 2c5d82f commit 2b9fdc4

7 files changed

Lines changed: 512 additions & 33 deletions

File tree

src/ast/ddl.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5723,6 +5723,80 @@ impl Spanned for AlterFunction {
57235723
}
57245724
}
57255725

5726+
/// Snowflake `ALTER PROCEDURE [IF EXISTS] <name> ( [<arg_type> [, ...]] ) <operation>`.
5727+
///
5728+
/// Kept distinct from [`AlterFunction`] because the procedure grammar carries
5729+
/// the `EXECUTE AS { CALLER | OWNER }` rights operation, which has no function
5730+
/// analog.
5731+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
5732+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5733+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
5734+
pub struct AlterProcedure {
5735+
/// `IF EXISTS` flag.
5736+
pub if_exists: bool,
5737+
/// Procedure name.
5738+
pub name: ObjectName,
5739+
/// Argument-type signature (`(NUMBER, VARCHAR)`), used to disambiguate
5740+
/// overloads. Empty when the parentheses hold no arguments.
5741+
pub args: Vec<DataType>,
5742+
/// Operation applied to the procedure.
5743+
pub operation: AlterProcedureOperation,
5744+
}
5745+
5746+
/// Operation for [`AlterProcedure`].
5747+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
5748+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5749+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
5750+
pub enum AlterProcedureOperation {
5751+
/// `RENAME TO <new_name>`
5752+
RenameTo {
5753+
/// New procedure name.
5754+
new_name: ObjectName,
5755+
},
5756+
/// `SET COMMENT = <value>`
5757+
SetComment {
5758+
/// The comment value expression.
5759+
comment: Expr,
5760+
},
5761+
/// `UNSET COMMENT`
5762+
UnsetComment,
5763+
/// `EXECUTE AS { CALLER | OWNER }`
5764+
ExecuteAs(ProcedureExecuteAs),
5765+
}
5766+
5767+
impl fmt::Display for AlterProcedure {
5768+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5769+
write!(f, "ALTER PROCEDURE ")?;
5770+
if self.if_exists {
5771+
write!(f, "IF EXISTS ")?;
5772+
}
5773+
write!(
5774+
f,
5775+
"{}({}) {}",
5776+
self.name,
5777+
display_comma_separated(&self.args),
5778+
self.operation
5779+
)
5780+
}
5781+
}
5782+
5783+
impl fmt::Display for AlterProcedureOperation {
5784+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5785+
match self {
5786+
AlterProcedureOperation::RenameTo { new_name } => write!(f, "RENAME TO {new_name}"),
5787+
AlterProcedureOperation::SetComment { comment } => write!(f, "SET COMMENT = {comment}"),
5788+
AlterProcedureOperation::UnsetComment => write!(f, "UNSET COMMENT"),
5789+
AlterProcedureOperation::ExecuteAs(execute_as) => write!(f, "EXECUTE AS {execute_as}"),
5790+
}
5791+
}
5792+
}
5793+
5794+
impl Spanned for AlterProcedure {
5795+
fn span(&self) -> Span {
5796+
Span::empty()
5797+
}
5798+
}
5799+
57265800
/// CREATE POLICY statement.
57275801
///
57285802
/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.html)

src/ast/mod.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ pub use self::dcl::{
6464
pub use self::ddl::{
6565
Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, AlterConnectorOwner,
6666
AlterFunction, AlterFunctionAction, AlterFunctionKind, AlterFunctionOperation,
67-
AlterIndexOperation, AlterOperator, AlterOperatorClass, AlterOperatorClassOperation,
67+
AlterIndexOperation, AlterProcedure, AlterProcedureOperation,
68+
AlterOperator, AlterOperatorClass, AlterOperatorClassOperation,
6869
AlterOperatorFamily, AlterOperatorFamilyOperation, AlterOperatorOperation, AlterPolicy,
6970
AlterPolicyOperation, AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm,
7071
AlterTableLock, AlterTableOperation, AlterTableType, AlterType, AlterTypeAddValue,
@@ -4363,6 +4364,48 @@ pub enum Statement {
43634364
/// Optional target table to fetch rows into.
43644365
into: Option<ObjectName>,
43654366
},
4367+
/// Snowflake scripting `FETCH <cursor> INTO <var> [, <var> ...]`.
4368+
///
4369+
/// Unlike the ISO/PostgreSQL [`Statement::Fetch`], the scripting form has
4370+
/// no direction and no `FROM`/`IN`; it binds the current cursor row into
4371+
/// one or more local variables.
4372+
FetchInto {
4373+
/// Cursor name.
4374+
cursor: Ident,
4375+
/// One or more variable targets.
4376+
into: Vec<ObjectName>,
4377+
},
4378+
/// Snowflake `CALL <proc>(<args>) INTO <var> [, <var> ...]`.
4379+
///
4380+
/// Like [`Statement::Call`] but captures the procedure result into one or
4381+
/// more local variables.
4382+
CallInto {
4383+
/// The procedure call.
4384+
function: Function,
4385+
/// One or more variable targets.
4386+
into: Vec<ObjectName>,
4387+
},
4388+
/// Snowflake `ALTER PROCEDURE`.
4389+
AlterProcedure(AlterProcedure),
4390+
/// Snowflake anonymous procedure:
4391+
/// `WITH <name> AS PROCEDURE (<args>) RETURNS <type> LANGUAGE <lang>
4392+
/// [EXECUTE AS ...] AS <body> CALL <name>(<args>)`.
4393+
WithProcedure {
4394+
/// Procedure name introduced by the `WITH` clause.
4395+
name: Ident,
4396+
/// Optional procedure parameters.
4397+
params: Option<Vec<ProcedureParam>>,
4398+
/// Optional return type.
4399+
returns: Option<DataType>,
4400+
/// Optional language identifier.
4401+
language: Option<Ident>,
4402+
/// Optional `EXECUTE AS { CALLER | OWNER }` rights clause.
4403+
execute_as: Option<ProcedureExecuteAs>,
4404+
/// Procedure body statements.
4405+
body: ConditionalStatements,
4406+
/// The trailing `CALL <name>(<args>)` statement.
4407+
call: Box<Statement>,
4408+
},
43664409
/// ```sql
43674410
/// FLUSH [NO_WRITE_TO_BINLOG | LOCAL] flush_option [, flush_option] ... | tables_option
43684411
/// ```
@@ -6153,6 +6196,41 @@ impl fmt::Display for Statement {
61536196

61546197
Ok(())
61556198
}
6199+
Statement::FetchInto { cursor, into } => {
6200+
write!(f, "FETCH {cursor} INTO {}", display_comma_separated(into))
6201+
}
6202+
Statement::CallInto { function, into } => {
6203+
write!(
6204+
f,
6205+
"CALL {function} INTO {}",
6206+
display_comma_separated(into)
6207+
)
6208+
}
6209+
Statement::AlterProcedure(alter_procedure) => write!(f, "{alter_procedure}"),
6210+
Statement::WithProcedure {
6211+
name,
6212+
params,
6213+
returns,
6214+
language,
6215+
execute_as,
6216+
body,
6217+
call,
6218+
} => {
6219+
write!(f, "WITH {name} AS PROCEDURE")?;
6220+
if let Some(p) = params {
6221+
write!(f, " ({})", display_comma_separated(p))?;
6222+
}
6223+
if let Some(ret) = returns {
6224+
write!(f, " RETURNS {ret}")?;
6225+
}
6226+
if let Some(language) = language {
6227+
write!(f, " LANGUAGE {language}")?;
6228+
}
6229+
if let Some(execute_as) = execute_as {
6230+
write!(f, " EXECUTE AS {execute_as}")?;
6231+
}
6232+
write!(f, " AS {body} {call}")
6233+
}
61566234
Statement::Directory {
61576235
overwrite,
61586236
local,

src/ast/spans.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,10 @@ impl Spanned for Statement {
584584
Statement::Let { .. } => Span::empty(),
585585
Statement::Null => Span::empty(),
586586
Statement::PutGetFiles { .. } => Span::empty(),
587+
Statement::FetchInto { .. } => Span::empty(),
588+
Statement::CallInto { .. } => Span::empty(),
589+
Statement::AlterProcedure { .. } => Span::empty(),
590+
Statement::WithProcedure { .. } => Span::empty(),
587591
}
588592
}
589593
}

src/dialect/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,12 @@ pub trait Dialect: Debug + Any {
11221122
false
11231123
}
11241124

1125+
/// Returns true if this dialect accepts an `INTO <var> [, ...]` tail after
1126+
/// a procedure call, e.g. `CALL p(1) INTO :ret` in Snowflake scripting.
1127+
fn supports_call_into(&self) -> bool {
1128+
false
1129+
}
1130+
11251131
/// Returns true if this dialect supports `$` as a prefix for money literals
11261132
/// e.g. `SELECT $123.45` (SQL Server)
11271133
fn supports_dollar_as_money_prefix(&self) -> bool {

src/dialect/snowflake.rs

Lines changed: 153 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ use crate::ast::helpers::stmt_data_loading::{
2727
FileStagingCommand, StageLoadSelectItem, StageLoadSelectItemKind, StageParamsObject,
2828
};
2929
use crate::ast::{
30-
AlterExternalVolumeOperation, AlterFileFormatOperation, AlterStageOperation, AlterTable,
30+
AlterExternalVolumeOperation, AlterFileFormatOperation, AlterProcedure,
31+
AlterProcedureOperation, AlterStageOperation, AlterTable,
3132
AlterTableOperation, AlterTableType, CatalogRestAuthentication, CatalogRestConfig,
3233
CatalogSource, CatalogSyncNamespaceMode, CatalogTableFormat, ColumnOption, ColumnPolicy,
3334
ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, CreateTable, CreateTableLikeKind,
34-
DollarQuotedString, Expr, ExternalVolumeEncryption, ExternalVolumeStorageLocation, Ident,
35+
DollarQuotedString, Expr, ExternalVolumeEncryption, ExternalVolumeStorageLocation,
36+
Ident, ProcedureExecuteAs,
3537
IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind,
3638
IdentityPropertyOrder, InitializeKind, Insert, MultiTableInsertIntoClause,
3739
MultiTableInsertType, MultiTableInsertValue, MultiTableInsertValues,
@@ -201,6 +203,12 @@ impl Dialect for SnowflakeDialect {
201203
true
202204
}
203205

206+
/// Snowflake scripting accepts `CALL p(...) INTO :var` to capture a
207+
/// procedure result into local variables.
208+
fn supports_call_into(&self) -> bool {
209+
true
210+
}
211+
204212
/// See <https://docs.snowflake.com/en/developer-guide/snowflake-scripting/cursors>
205213
fn supports_for_loop_over_cursor(&self) -> bool {
206214
true
@@ -290,6 +298,24 @@ impl Dialect for SnowflakeDialect {
290298
return Some(parser.parse_begin_exception_end());
291299
}
292300

301+
// Snowflake scripting `FETCH <cursor> INTO <var> [, ...]` has no
302+
// direction and no FROM/IN, so it can't go through the ISO parser.
303+
// Intercept only that shape; anything else falls through untouched.
304+
if parser.peek_keyword(Keyword::FETCH) {
305+
if let Ok(Some(stmt)) = parser.maybe_parse(parse_fetch_into) {
306+
return Some(Ok(stmt));
307+
}
308+
}
309+
310+
// Snowflake anonymous procedure: `WITH <name> AS PROCEDURE ...`. Every
311+
// other `WITH` (ordinary CTE) fails the `AS PROCEDURE` probe and falls
312+
// through to the standard query parser.
313+
if parser.peek_keyword(Keyword::WITH) {
314+
if let Ok(Some(stmt)) = parser.maybe_parse(parse_with_procedure) {
315+
return Some(Ok(stmt));
316+
}
317+
}
318+
293319
if parser.parse_keywords(&[Keyword::ALTER, Keyword::DYNAMIC, Keyword::TABLE]) {
294320
// ALTER DYNAMIC TABLE
295321
return Some(parse_alter_dynamic_table(parser));
@@ -315,6 +341,11 @@ impl Dialect for SnowflakeDialect {
315341
return Some(parse_alter_storage_integration(parser));
316342
}
317343

344+
if parser.parse_keywords(&[Keyword::ALTER, Keyword::PROCEDURE]) {
345+
// ALTER PROCEDURE
346+
return Some(parse_alter_procedure(parser));
347+
}
348+
318349
if parser.parse_keywords(&[Keyword::ALTER, Keyword::FILE, Keyword::FORMAT]) {
319350
// ALTER FILE FORMAT
320351
return Some(parse_alter_file_format(parser));
@@ -1140,6 +1171,126 @@ fn parse_alter_dynamic_table_property(
11401171
})
11411172
}
11421173

1174+
/// Parse Snowflake scripting `FETCH <cursor> INTO <var> [, <var> ...]`.
1175+
///
1176+
/// The caller has verified the next keyword is `FETCH` and runs this via
1177+
/// `maybe_parse`, so a non-scripting `FETCH` simply errors out and rewinds.
1178+
fn parse_fetch_into(parser: &mut Parser) -> Result<Statement, ParserError> {
1179+
parser.expect_keyword(Keyword::FETCH)?;
1180+
let cursor = parser.parse_identifier()?;
1181+
parser.expect_keyword(Keyword::INTO)?;
1182+
let into = parser.parse_scripting_into_targets()?;
1183+
Ok(Statement::FetchInto { cursor, into })
1184+
}
1185+
1186+
/// Parse `ALTER PROCEDURE [IF EXISTS] <name> ( [<arg_type> [, ...]] )
1187+
/// { RENAME TO ... | SET COMMENT = ... | UNSET COMMENT | EXECUTE AS CALLER|OWNER }`.
1188+
///
1189+
/// The `ALTER PROCEDURE` keywords are already consumed by the caller.
1190+
fn parse_alter_procedure(parser: &mut Parser) -> Result<Statement, ParserError> {
1191+
let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
1192+
let name = parser.parse_object_name(false)?;
1193+
1194+
parser.expect_token(&Token::LParen)?;
1195+
let args = if parser.peek_token_ref().token == Token::RParen {
1196+
vec![]
1197+
} else {
1198+
parser.parse_comma_separated(Parser::parse_data_type)?
1199+
};
1200+
parser.expect_token(&Token::RParen)?;
1201+
1202+
let operation = if parser.parse_keywords(&[Keyword::RENAME, Keyword::TO]) {
1203+
AlterProcedureOperation::RenameTo {
1204+
new_name: parser.parse_object_name(false)?,
1205+
}
1206+
} else if parser.parse_keywords(&[Keyword::EXECUTE, Keyword::AS]) {
1207+
let execute_as = if parser.parse_keyword(Keyword::CALLER) {
1208+
ProcedureExecuteAs::Caller
1209+
} else {
1210+
parser.expect_keyword_is(Keyword::OWNER)?;
1211+
ProcedureExecuteAs::Owner
1212+
};
1213+
AlterProcedureOperation::ExecuteAs(execute_as)
1214+
} else if parser.parse_keyword(Keyword::SET) {
1215+
parser.expect_keyword_is(Keyword::COMMENT)?;
1216+
parser.expect_token(&Token::Eq)?;
1217+
AlterProcedureOperation::SetComment {
1218+
comment: parser.parse_expr()?,
1219+
}
1220+
} else if parser.parse_keyword(Keyword::UNSET) {
1221+
parser.expect_keyword_is(Keyword::COMMENT)?;
1222+
AlterProcedureOperation::UnsetComment
1223+
} else {
1224+
return parser.expected_ref(
1225+
"RENAME TO, SET COMMENT, UNSET COMMENT, or EXECUTE AS after ALTER PROCEDURE",
1226+
parser.peek_token_ref(),
1227+
);
1228+
};
1229+
1230+
Ok(Statement::AlterProcedure(AlterProcedure {
1231+
if_exists,
1232+
name,
1233+
args,
1234+
operation,
1235+
}))
1236+
}
1237+
1238+
/// Parse Snowflake anonymous procedure:
1239+
/// `WITH <name> AS PROCEDURE (<args>) RETURNS <type> LANGUAGE <lang>
1240+
/// [EXECUTE AS ...] AS <body> CALL <name>(<args>)`.
1241+
///
1242+
/// The caller runs this via `maybe_parse`, so an ordinary CTE fails the
1243+
/// `AS PROCEDURE` probe and rewinds.
1244+
fn parse_with_procedure(parser: &mut Parser) -> Result<Statement, ParserError> {
1245+
parser.expect_keyword(Keyword::WITH)?;
1246+
let name = parser.parse_identifier()?;
1247+
parser.expect_keyword_is(Keyword::AS)?;
1248+
parser.expect_keyword_is(Keyword::PROCEDURE)?;
1249+
1250+
let params = parser.parse_optional_procedure_parameters()?;
1251+
1252+
let returns = if parser.parse_keyword(Keyword::RETURNS) {
1253+
Some(parser.parse_data_type()?)
1254+
} else {
1255+
None
1256+
};
1257+
// Snowflake allows a `NOT NULL` return-type annotation; drop it.
1258+
let _ = parser.parse_keywords(&[Keyword::NOT, Keyword::NULL]);
1259+
1260+
let language = if parser.parse_keyword(Keyword::LANGUAGE) {
1261+
Some(parser.parse_identifier()?)
1262+
} else {
1263+
None
1264+
};
1265+
1266+
let execute_as = if parser.parse_keywords(&[Keyword::EXECUTE, Keyword::AS]) {
1267+
if parser.parse_keyword(Keyword::CALLER) {
1268+
Some(ProcedureExecuteAs::Caller)
1269+
} else {
1270+
parser.expect_keyword_is(Keyword::OWNER)?;
1271+
Some(ProcedureExecuteAs::Owner)
1272+
}
1273+
} else {
1274+
None
1275+
};
1276+
1277+
parser.expect_keyword_is(Keyword::AS)?;
1278+
let body = parser.parse_procedure_body()?;
1279+
1280+
parser.expect_keyword(Keyword::CALL)?;
1281+
let call = Box::new(parser.parse_call()?);
1282+
1283+
Ok(Statement::WithProcedure {
1284+
name,
1285+
params,
1286+
returns,
1287+
language,
1288+
execute_as,
1289+
body,
1290+
call,
1291+
})
1292+
}
1293+
11431294
/// Parse snowflake alter materialized view.
11441295
/// <https://docs.snowflake.com/en/sql-reference/sql/alter-materialized-view>
11451296
///

0 commit comments

Comments
 (0)