diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 51ac7e8bae..50f1951dfe 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -4913,11 +4913,16 @@ impl Spanned for AlterCollation { } } -/// `ALTER VIEW` statement. +/// `ALTER [ MATERIALIZED ] VIEW` statement. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altermaterializedview.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 AlterView { + /// Whether `MATERIALIZED` was specified. A materialized view's operation is never + /// [`AlterViewOperation::As`] or [`AlterViewOperation::AlterColumn`]. + pub materialized: bool, /// View name being altered. #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] pub name: ObjectName, @@ -5041,7 +5046,11 @@ impl fmt::Display for AlterViewOperation { impl fmt::Display for AlterView { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "ALTER VIEW ")?; + write!(f, "ALTER ")?; + if self.materialized { + write!(f, "MATERIALIZED ")?; + } + write!(f, "VIEW ")?; if self.if_exists { write!(f, "IF EXISTS ")?; } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6640bf7bfb..ecfee10939 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3922,7 +3922,7 @@ pub enum Statement { operation: AlterIndexOperation, }, /// ```sql - /// ALTER VIEW + /// ALTER [ MATERIALIZED ] VIEW /// ``` AlterView(AlterView), /// ```sql diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 536e753087..35fcb4fc98 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11896,7 +11896,11 @@ impl<'a> Parser<'a> { self.prev_token(); self.parse_alter_schema() } - Keyword::VIEW => self.parse_alter_view(), + Keyword::VIEW => self.parse_alter_view(false), + Keyword::MATERIALIZED => { + self.expect_keyword_is(Keyword::VIEW)?; + self.parse_alter_view(true) + } Keyword::TYPE => self.parse_alter_type(), Keyword::COLLATION => self.parse_alter_collation().map(Into::into), Keyword::TABLE => self.parse_alter_table(None), @@ -11948,7 +11952,7 @@ impl<'a> Parser<'a> { _ => { self.index = index; self.expected_ref( - "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or FOREIGN TABLE or SCHEMA or USER or OPERATOR", + "one of VIEW or MATERIALIZED VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or FOREIGN TABLE or SCHEMA or USER or OPERATOR", self.peek_token_ref(), ) } @@ -12226,8 +12230,8 @@ impl<'a> Parser<'a> { .into()) } - /// Parse an `ALTER VIEW` statement. - pub fn parse_alter_view(&mut self) -> Result { + /// Parse an `ALTER [ MATERIALIZED ] VIEW` statement. + pub fn parse_alter_view(&mut self, materialized: bool) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let name = self.parse_object_name(false)?; @@ -12297,6 +12301,7 @@ impl<'a> Parser<'a> { }; Ok(AlterView { + materialized, name, if_exists, operation, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 16d1dcc002..0882b95c8a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5669,6 +5669,7 @@ fn parse_alter_view() { let sql = "ALTER VIEW myschema.myview AS SELECT foo FROM bar"; match verified_stmt(sql) { Statement::AlterView(AlterView { + materialized: false, name, if_exists, operation: @@ -5723,6 +5724,7 @@ fn parse_alter_view_with_columns() { let sql = "ALTER VIEW v (has, cols) AS SELECT 1, 2"; match verified_stmt(sql) { Statement::AlterView(AlterView { + materialized: false, name, if_exists, operation: @@ -5752,6 +5754,7 @@ fn parse_alter_view_operations() { assert_eq!( verified_stmt("ALTER VIEW IF EXISTS s.v OWNER TO o"), Statement::AlterView(AlterView { + materialized: false, name: ObjectName::from(vec![Ident::new("s"), Ident::new("v")]), if_exists: true, operation: AlterViewOperation::OwnerTo(Owner::Ident(Ident::new("o"))), @@ -5902,7 +5905,7 @@ fn display_alter_view_propagates_write_errors() { } let with_options = verified_stmt("ALTER VIEW IF EXISTS v WITH (x = 1) AS SELECT 1"); - for rejected in ["ALTER VIEW ", "IF EXISTS ", "WITH ("] { + for rejected in ["ALTER ", "VIEW ", "IF EXISTS ", "WITH ("] { let mut writer = RejectingWriter(rejected); assert_eq!( write!(writer, "{with_options}"), @@ -5912,7 +5915,7 @@ fn display_alter_view_propagates_write_errors() { } let columns = verified_stmt("ALTER VIEW IF EXISTS v (zeta) AS SELECT 1"); - for rejected in ["ALTER VIEW ", "IF EXISTS ", "("] { + for rejected in ["ALTER ", "VIEW ", "IF EXISTS ", "("] { let mut writer = RejectingWriter(rejected); assert_eq!(write!(writer, "{columns}"), Err(fmt::Error), "{rejected:?}"); } @@ -16193,7 +16196,7 @@ fn parse_alter_trigger_spans() { #[test] fn parse_alter_rejects_missing_or_unknown_object_type() { - let objects = "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or FOREIGN TABLE or SCHEMA or USER or OPERATOR"; + let objects = "one of VIEW or MATERIALIZED VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or FOREIGN TABLE or SCHEMA or USER or OPERATOR"; for (sql, found) in [ ("ALTER", "EOF"), ("ALTER 1", "1"), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 26edced406..319f27cbff 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -12773,3 +12773,87 @@ fn parse_alter_foreign_table_rejects_clauses() { ); } } + +#[test] +fn parse_alter_materialized_view() { + assert_eq!( + pg_and_generic().verified_stmt("ALTER MATERIALIZED VIEW IF EXISTS s.m OWNER TO alice"), + Statement::AlterView(AlterView { + materialized: true, + name: ObjectName::from(vec![Ident::new("s"), Ident::new("m")]), + if_exists: true, + operation: AlterViewOperation::OwnerTo(Owner::Ident(Ident::new("alice"))), + }) + ); + assert_eq!( + pg_and_generic().verified_stmt("ALTER MATERIALIZED VIEW m SET (fillfactor = 70)"), + Statement::AlterView(AlterView { + materialized: true, + name: ObjectName::from(vec![Ident::new("m")]), + if_exists: false, + operation: AlterViewOperation::SetOptions { + options: vec![SqlOption::KeyValue { + key: ObjectName::from(vec![Ident::new("fillfactor")]), + value: Expr::value(number("70")), + }], + }, + }) + ); + for sql in [ + "ALTER MATERIALIZED VIEW m RESET (fillfactor, autovacuum_enabled)", + "ALTER MATERIALIZED VIEW m RENAME TO m2", + "ALTER MATERIALIZED VIEW m RENAME COLUMN a TO b", + "ALTER MATERIALIZED VIEW m SET SCHEMA s", + ] { + pg_and_generic().verified_stmt(sql); + } + assert_eq!( + pg_and_generic().verified_stmt("ALTER MATERIALIZED VIEW m ALTER COLUMN c SET DEFAULT 1"), + Statement::AlterView(AlterView { + materialized: true, + name: ObjectName::from(vec![Ident::new("m")]), + if_exists: false, + operation: AlterViewOperation::AlterColumn { + column_name: Ident::new("c"), + op: AlterColumnOperation::SetDefault { + value: Expr::value(number("1")), + }, + }, + }) + ); + pg_and_generic().verified_stmt("ALTER MATERIALIZED VIEW m ALTER COLUMN c DROP DEFAULT"); + + assert_eq!( + pg_and_generic() + .parse_sql_statements("ALTER MATERIALIZED m OWNER TO alice") + .unwrap_err(), + ParserError::ParserError("Expected: VIEW, found: m".to_string()) + ); +} + +#[test] +fn display_alter_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 == self.0 { + Err(fmt::Error) + } else { + Ok(()) + } + } + } + + let materialized = pg_and_generic().verified_stmt("ALTER MATERIALIZED VIEW m RENAME TO m2"); + for rejected in ["ALTER ", "MATERIALIZED ", "VIEW "] { + let mut writer = RejectingWriter(rejected); + assert_eq!( + write!(writer, "{materialized}"), + Err(fmt::Error), + "{rejected:?}" + ); + } +}