diff --git a/fuzz/fuzz_seeds/postgres_2769_refresh_materialized_view.sql b/fuzz/fuzz_seeds/postgres_2769_refresh_materialized_view.sql new file mode 100644 index 0000000000..333f26ba1b --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_2769_refresh_materialized_view.sql @@ -0,0 +1,3 @@ +REFRESH MATERIALIZED VIEW m; +REFRESH MATERIALIZED VIEW CONCURRENTLY app.m WITH NO DATA; +REFRESH MATERIALIZED VIEW m WITH DATA diff --git a/src/ast/mod.rs b/src/ast/mod.rs index cefead4aa5..f62f09907d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5113,6 +5113,13 @@ pub enum Statement { /// ``` /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-reset.html) Reset(ResetStatement), + /// Replace the contents of a materialized view. + /// + /// ```sql + /// REFRESH MATERIALIZED VIEW [ CONCURRENTLY ] name [ WITH [ NO ] DATA ] + /// ``` + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-refreshmaterializedview.html) + RefreshMaterializedView(RefreshMaterializedView), } impl From for Statement { @@ -6589,6 +6596,7 @@ impl fmt::Display for Statement { Statement::Vacuum(s) => write!(f, "{s}"), Statement::AlterUser(s) => write!(f, "{s}"), Statement::Reset(s) => write!(f, "{s}"), + Statement::RefreshMaterializedView(s) => write!(f, "{s}"), } } } @@ -12364,6 +12372,37 @@ pub struct ResetStatement { pub reset: Reset, } +/// `REFRESH MATERIALIZED VIEW [ CONCURRENTLY ] name [ WITH [ NO ] DATA ]` +/// +/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-refreshmaterializedview.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 RefreshMaterializedView { + /// Whether `CONCURRENTLY` was specified. + pub concurrently: bool, + /// Name of the materialized view. + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub name: ObjectName, + /// `Some(true)` for `WITH DATA`, `Some(false)` for `WITH NO DATA`. + pub with_data: Option, +} + +impl fmt::Display for RefreshMaterializedView { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("REFRESH MATERIALIZED VIEW ")?; + if self.concurrently { + f.write_str("CONCURRENTLY ")?; + } + write!(f, "{}", self.name)?; + match self.with_data { + Some(true) => f.write_str(" WITH DATA"), + Some(false) => f.write_str(" WITH NO DATA"), + None => Ok(()), + } + } +} + /// Query optimizer hints are optionally supported comments after the /// `SELECT`, `INSERT`, `UPDATE`, `REPLACE`, `MERGE`, and `DELETE` keywords in /// the corresponding statements. @@ -12796,6 +12835,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(r: RefreshMaterializedView) -> Self { + Self::RefreshMaterializedView(r) + } +} + #[cfg(test)] mod tests { use crate::tokenizer::Location; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index e89d575700..8b008204c1 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -45,12 +45,12 @@ use super::{ NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, OperateFunctionArg, OrderBy, OrderByExpr, OrderByKind, OutputClause, Parens, Partition, PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, - RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, - ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, Setting, SqlOption, Statement, - Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, - TableObject, TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, - UserMappingUser, Values, ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, - WithFill, + RaiseStatement, RaiseStatementValue, ReferentialAction, RefreshMaterializedView, + RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, + SetExpr, Setting, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, + TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, + TableWithJoins, Update, UpdateTableFromKind, Use, UserMappingUser, Values, ViewColumnDef, + WhileStatement, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -525,6 +525,7 @@ impl Spanned for Statement { Statement::Vacuum(..) => Span::empty(), Statement::AlterUser(..) => Span::empty(), Statement::Reset(..) => Span::empty(), + Statement::RefreshMaterializedView(refresh) => refresh.span(), Statement::CreateAggregate(create_aggregate) => create_aggregate.span(), } } @@ -939,6 +940,12 @@ impl Spanned for Analyze { } } +impl Spanned for RefreshMaterializedView { + fn span(&self) -> Span { + self.name.span() + } +} + /// # partial span /// /// Missing spans: diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d523b48cb5..4e006e26ff 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -789,6 +789,7 @@ impl<'a> Parser<'a> { self.parse_vacuum() } Keyword::RESET => self.parse_reset().map(Into::into), + Keyword::REFRESH => self.parse_refresh_materialized_view().map(Into::into), _ => self.expected("an SQL statement", next_token), }, Token::LParen => { @@ -22426,6 +22427,25 @@ impl<'a> Parser<'a> { reset: Reset::ConfigurationParameter(obj), }) } + + /// Parse a `REFRESH MATERIALIZED VIEW` statement, after the `REFRESH` keyword. + fn parse_refresh_materialized_view(&mut self) -> Result { + self.expect_keywords(&[Keyword::MATERIALIZED, Keyword::VIEW])?; + let concurrently = self.parse_keyword(Keyword::CONCURRENTLY); + let name = self.parse_object_name(false)?; + let with_data = if self.parse_keywords(&[Keyword::WITH, Keyword::DATA]) { + Some(true) + } else if self.parse_keywords(&[Keyword::WITH, Keyword::NO, Keyword::DATA]) { + Some(false) + } else { + None + }; + Ok(RefreshMaterializedView { + concurrently, + name, + with_data, + }) + } } fn maybe_prefixed_expr(expr: Expr, prefix: Option) -> Expr { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index bd1d7606af..7a09f95979 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -23106,3 +23106,97 @@ fn parse_alter_table_reset_options() { ); } } + +#[test] +fn parse_refresh_materialized_view() { + let sql = "REFRESH MATERIALIZED VIEW CONCURRENTLY app.m WITH NO DATA"; + assert_eq!( + Parser::parse_sql(&GenericDialect {}, sql).unwrap()[0].span(), + Span::new(Location::new(1, 40), Location::new(1, 45)) + ); + match verified_stmt(sql) { + Statement::RefreshMaterializedView(RefreshMaterializedView { + concurrently, + name, + with_data, + }) => { + assert!(concurrently); + assert_eq!( + name, + ObjectName::from(vec![Ident::new("app"), Ident::new("m")]) + ); + assert_eq!(with_data, Some(false)); + } + stmt => panic!("unexpected statement: {stmt:?}"), + } + match verified_stmt("REFRESH MATERIALIZED VIEW m") { + Statement::RefreshMaterializedView(RefreshMaterializedView { + concurrently, + with_data, + .. + }) => { + assert!(!concurrently); + assert_eq!(with_data, None); + } + stmt => panic!("unexpected statement: {stmt:?}"), + } + match verified_stmt("REFRESH MATERIALIZED VIEW m WITH DATA") { + Statement::RefreshMaterializedView(RefreshMaterializedView { with_data, .. }) => { + assert_eq!(with_data, Some(true)); + } + stmt => panic!("unexpected statement: {stmt:?}"), + } + + for (sql, error) in [ + ( + "REFRESH MATERIALIZED VIEW", + "Expected: identifier, found: EOF", + ), + ("REFRESH VIEW m", "Expected: MATERIALIZED, found: VIEW"), + ( + "REFRESH MATERIALIZED VIEW m WITH", + "Expected: end of statement, found: WITH", + ), + ] { + assert_eq!( + all_dialects().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(error.to_string()), + "{sql}" + ); + } +} + +#[test] +#[cfg(feature = "visitor")] +fn test_visit_relations_refresh_materialized_view() { + let stmts = all_dialects().parse_sql_statements("REFRESH MATERIALIZED VIEW app.m"); + let mut relations = vec![]; + let _ = visit_relations(&stmts.unwrap(), |relation| { + relations.push(relation.to_string()); + core::ops::ControlFlow::<()>::Continue(()) + }); + assert_eq!(relations, vec!["app.m"]); +} + +#[test] +fn display_refresh_materialized_view_propagates_write_errors() { + use core::fmt::{self, Write}; + + struct RejectingWriter(&'static str); + + impl Write for RejectingWriter { + fn write_str(&mut self, value: &str) -> fmt::Result { + if value.contains(self.0) { + Err(fmt::Error) + } else { + Ok(()) + } + } + } + + let statement = verified_stmt("REFRESH MATERIALIZED VIEW CONCURRENTLY app.m WITH NO DATA"); + for rejected in ["REFRESH", "CONCURRENTLY", "app", "NO DATA"] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{statement}"), Err(fmt::Error), "{rejected}"); + } +}