diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ee8383e29..d1c7d76ff 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2547,6 +2547,16 @@ pub enum Statement { /// ``` /// Note: this is a MySQL-specific statement. See UnlockTables, + /// ```sql + /// UNLOAD(statement) TO [ WITH options ] + /// ``` + /// See Redshift and + // Athena + Unload { + query: Box, + to: Ident, + with: Vec, + }, } impl fmt::Display for Statement { @@ -4060,6 +4070,15 @@ impl fmt::Display for Statement { Statement::UnlockTables => { write!(f, "UNLOCK TABLES") } + Statement::Unload { query, to, with } => { + write!(f, "UNLOAD({query}) TO {to}")?; + + if !with.is_empty() { + write!(f, " WITH ({})", display_comma_separated(with))?; + } + + Ok(()) + } } } } diff --git a/src/keywords.rs b/src/keywords.rs index 8161e8af3..3b98b2367 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -689,6 +689,7 @@ define_keywords!( UNION, UNIQUE, UNKNOWN, + UNLOAD, UNLOCK, UNLOGGED, UNNEST, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e112d16f2..fa26e2ebe 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -516,6 +516,7 @@ impl<'a> Parser<'a> { Keyword::MERGE => Ok(self.parse_merge()?), // `PRAGMA` is sqlite specific https://www.sqlite.org/pragma.html Keyword::PRAGMA => Ok(self.parse_pragma()?), + Keyword::UNLOAD => Ok(self.parse_unload()?), // `INSTALL` is duckdb specific https://duckdb.org/docs/extensions/overview Keyword::INSTALL if dialect_of!(self is DuckDbDialect | GenericDialect) => { Ok(self.parse_install()?) @@ -524,7 +525,6 @@ impl<'a> Parser<'a> { Keyword::LOAD if dialect_of!(self is DuckDbDialect | GenericDialect) => { Ok(self.parse_load()?) } - _ => self.expected("an SQL statement", next_token), }, Token::LParen => { @@ -8947,6 +8947,23 @@ impl<'a> Parser<'a> { }) } + pub fn parse_unload(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + let query = self.parse_query()?; + self.expect_token(&Token::RParen)?; + + self.expect_keyword(Keyword::TO)?; + let to = self.parse_identifier(false)?; + + let with_options = self.parse_options(Keyword::WITH)?; + + Ok(Statement::Unload { + query: Box::new(query), + to, + with: with_options, + }) + } + pub fn parse_merge_clauses(&mut self) -> Result, ParserError> { let mut clauses: Vec = vec![]; loop { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a32468fb8..405395b7d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8434,6 +8434,64 @@ fn parse_binary_operators_without_whitespace() { ); } +#[test] +fn parse_unload() { + let unload = verified_stmt("UNLOAD(SELECT cola FROM tab) TO 's3://...' WITH (format = 'AVRO')"); + assert_eq!( + unload, + Statement::Unload { + query: Box::new(Query { + body: Box::new(SetExpr::Select(Box::new(Select { + distinct: None, + top: None, + projection: vec![UnnamedExpr(Expr::Identifier(Ident::new("cola"))),], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident::new("tab")]), + alias: None, + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + }, + joins: vec![], + }], + lateral_views: vec![], + selection: None, + group_by: GroupByExpr::Expressions(vec![]), + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None, + value_table_mode: None, + }))), + with: None, + limit: None, + limit_by: vec![], + offset: None, + fetch: None, + locks: vec![], + for_clause: None, + order_by: vec![], + }), + to: Ident { + value: "s3://...".to_string(), + quote_style: Some('\'') + }, + with: vec![SqlOption { + name: Ident { + value: "format".to_string(), + quote_style: None + }, + value: Expr::Value(Value::SingleQuotedString("AVRO".to_string())) + }] + } + ); +} + #[test] fn test_savepoint() { match verified_stmt("SAVEPOINT test1") {