diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7576123b..cb6058a0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4409,6 +4409,13 @@ pub enum Statement { /// ``` /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/create-user) CreateUser(CreateUser), + /// Re-sorts rows and reclaims space in either a specified table or all tables in the current database + /// + /// ```sql + /// VACUUM tbl + /// ``` + /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_VACUUM_command.html) + Vacuum(VacuumStatement), } /// ```sql @@ -6343,6 +6350,7 @@ impl fmt::Display for Statement { Statement::ExportData(e) => write!(f, "{e}"), Statement::CreateUser(s) => write!(f, "{s}"), Statement::AlterSchema(s) => write!(f, "{s}"), + Statement::Vacuum(s) => write!(f, "{s}"), } } } @@ -10604,6 +10612,50 @@ impl fmt::Display for InitializeKind { } } +/// Re-sorts rows and reclaims space in either a specified table or all tables in the current database +/// +/// '''sql +/// VACUUM [ FULL | SORT ONLY | DELETE ONLY | REINDEX | RECLUSTER ] [ \[ table_name \] [ TO threshold PERCENT ] \[ BOOST \] ] +/// ''' +/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_VACUUM_command.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct VacuumStatement { + pub full: bool, + pub sort_only: bool, + pub delete_only: bool, + pub reindex: bool, + pub recluster: bool, + pub table_name: Option, + pub threshold: Option, + pub boost: bool, +} + +impl fmt::Display for VacuumStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "VACUUM{}{}{}{}{}", + if self.full { " FULL" } else { "" }, + if self.sort_only { " SORT ONLY" } else { "" }, + if self.delete_only { " DELETE ONLY" } else { "" }, + if self.reindex { " REINDEX" } else { "" }, + if self.recluster { " RECLUSTER" } else { "" }, + )?; + if let Some(table_name) = &self.table_name { + write!(f, " {table_name}")?; + } + if let Some(threshold) = &self.threshold { + write!(f, " TO {threshold} PERCENT")?; + } + if self.boost { + write!(f, " BOOST")?; + } + Ok(()) + } +} + #[cfg(test)] mod tests { use crate::tokenizer::Location; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 8eb22c8e..7f017582 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -552,6 +552,7 @@ impl Spanned for Statement { ), Statement::CreateUser(..) => Span::empty(), Statement::AlterSchema(s) => s.span(), + Statement::Vacuum(..) => Span::empty(), } } } diff --git a/src/keywords.rs b/src/keywords.rs index 245b1437..12687130 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -144,6 +144,7 @@ define_keywords!( BLOOMFILTER, BOOL, BOOLEAN, + BOOST, BOTH, BOX, BRIN, @@ -761,6 +762,7 @@ define_keywords!( REGR_SXX, REGR_SXY, REGR_SYY, + REINDEX, RELATIVE, RELAY, RELEASE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 57a98286..52db37b7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -649,6 +649,10 @@ impl<'a> Parser<'a> { self.prev_token(); self.parse_export_data() } + Keyword::VACUUM => { + self.prev_token(); + self.parse_vacuum() + } _ => self.expected("an SQL statement", next_token), }, Token::LParen => { @@ -16932,6 +16936,40 @@ impl<'a> Parser<'a> { })) } + fn parse_vacuum(&mut self) -> Result { + self.expect_keyword(Keyword::VACUUM)?; + let full = self.parse_keyword(Keyword::FULL); + let sort_only = self.parse_keywords(&[Keyword::SORT, Keyword::ONLY]); + let delete_only = self.parse_keywords(&[Keyword::DELETE, Keyword::ONLY]); + let reindex = self.parse_keyword(Keyword::REINDEX); + let recluster = self.parse_keyword(Keyword::RECLUSTER); + let (table_name, threshold, boost) = + match self.maybe_parse(|p| p.parse_object_name(false))? { + Some(table_name) => { + let threshold = if self.parse_keyword(Keyword::TO) { + let value = self.parse_value()?; + self.expect_keyword(Keyword::PERCENT)?; + Some(value.value) + } else { + None + }; + let boost = self.parse_keyword(Keyword::BOOST); + (Some(table_name), threshold, boost) + } + _ => (None, None, false), + }; + Ok(Statement::Vacuum(VacuumStatement { + full, + sort_only, + delete_only, + reindex, + recluster, + table_name, + threshold, + boost, + })) + } + /// Consume the parser and return its underlying token buffer pub fn into_tokens(self) -> Vec { self.tokens diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index d539adf6..90652ff4 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -407,3 +407,48 @@ fn parse_string_literal_backslash_escape() { fn parse_utf8_multibyte_idents() { redshift().verified_stmt("SELECT 🚀.city AS 🎸 FROM customers AS 🚀"); } + +#[test] +fn parse_vacuum() { + let stmt = redshift().verified_stmt("VACUUM FULL"); + match stmt { + Statement::Vacuum(v) => { + assert!(v.full); + assert_eq!(v.table_name, None); + } + _ => unreachable!(), + } + let stmt = redshift().verified_stmt("VACUUM tbl"); + match stmt { + Statement::Vacuum(v) => { + assert_eq!( + v.table_name, + Some(ObjectName::from(vec![Ident::new("tbl"),])) + ); + } + _ => unreachable!(), + } + let stmt = redshift().verified_stmt( + "VACUUM FULL SORT ONLY DELETE ONLY REINDEX RECLUSTER db1.sc1.tbl1 TO 20 PERCENT BOOST", + ); + match stmt { + Statement::Vacuum(v) => { + assert!(v.full); + assert!(v.sort_only); + assert!(v.delete_only); + assert!(v.reindex); + assert!(v.recluster); + assert_eq!( + v.table_name, + Some(ObjectName::from(vec![ + Ident::new("db1"), + Ident::new("sc1"), + Ident::new("tbl1"), + ])) + ); + assert_eq!(v.threshold, Some(number("20"))); + assert!(v.boost); + } + _ => unreachable!(), + } +}