From 92b5ab794ec634e34adf8cc2e2506cdd754580e7 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 27 Oct 2026 22:20:18 +0100 Subject: [PATCH 1/3] PostgreSQL: Support ALTER MATERIALIZED VIEW --- src/ast/ddl.rs | 13 +++++++-- src/ast/mod.rs | 2 +- src/parser/mod.rs | 18 +++++++++--- tests/sqlparser_common.rs | 9 ++++-- tests/sqlparser_postgres.rs | 56 +++++++++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 0343b65a95..fdad96f1e8 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -4857,11 +4857,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, @@ -4985,7 +4990,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 0b0893d7e4..fb2b01c9b2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3911,7 +3911,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 3b13ed5a36..5a8e2898a8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11876,7 +11876,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(false), @@ -11924,7 +11928,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 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 SCHEMA or USER or OPERATOR", self.peek_token_ref(), ) } @@ -12188,8 +12192,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)?; @@ -12231,6 +12235,11 @@ impl<'a> Parser<'a> { old_column_name, new_column_name: self.parse_identifier()?, } + } else if materialized { + return self.expected_ref( + "OWNER TO, RENAME, SET or RESET after ALTER MATERIALIZED VIEW", + self.peek_token_ref(), + ); } else if self.parse_keyword(Keyword::ALTER) { let _ = self.parse_keyword(Keyword::COLUMN); let column_name = self.parse_identifier()?; @@ -12259,6 +12268,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 7627f39185..d93d24739c 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 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 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 389521e1ce..be1e4f7f0f 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -12349,3 +12349,59 @@ fn parse_column_generic_options() { ); } } + +#[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); + } + + for (sql, expected) in [ + ( + "ALTER MATERIALIZED VIEW m AS SELECT 1", + "Expected: OWNER TO, RENAME, SET or RESET after ALTER MATERIALIZED VIEW, found: AS", + ), + ( + "ALTER MATERIALIZED VIEW m ALTER COLUMN c SET DEFAULT 1", + "Expected: OWNER TO, RENAME, SET or RESET after ALTER MATERIALIZED VIEW, found: ALTER", + ), + ( + "ALTER MATERIALIZED m OWNER TO alice", + "Expected: VIEW, found: m", + ), + ] { + assert_eq!( + pg_and_generic().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(expected.to_string()), + "{sql}" + ); + } +} From 667c1470ea27576c68a2af2a9262498a3f53dc23 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 30 Oct 2026 11:50:21 +0100 Subject: [PATCH 2/3] Test ALTER MATERIALIZED VIEW formatter errors --- tests/sqlparser_postgres.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index be1e4f7f0f..7aef268b70 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -12405,3 +12405,30 @@ fn parse_alter_materialized_view() { ); } } + +#[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:?}" + ); + } +} From 11c3bf3458fb653db4e8fa73c717c6442a4b6006 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 31 Oct 2026 14:23:23 +0100 Subject: [PATCH 3/3] Parse ALTER COLUMN in ALTER MATERIALIZED VIEW as in ALTER VIEW --- src/parser/mod.rs | 5 ----- tests/sqlparser_postgres.rs | 41 +++++++++++++++++++------------------ 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5a8e2898a8..bc44579ac8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12235,11 +12235,6 @@ impl<'a> Parser<'a> { old_column_name, new_column_name: self.parse_identifier()?, } - } else if materialized { - return self.expected_ref( - "OWNER TO, RENAME, SET or RESET after ALTER MATERIALIZED VIEW", - self.peek_token_ref(), - ); } else if self.parse_keyword(Keyword::ALTER) { let _ = self.parse_keyword(Keyword::COLUMN); let column_name = self.parse_identifier()?; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7aef268b70..612f34590d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -12383,27 +12383,28 @@ fn parse_alter_materialized_view() { ] { 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"); - for (sql, expected) in [ - ( - "ALTER MATERIALIZED VIEW m AS SELECT 1", - "Expected: OWNER TO, RENAME, SET or RESET after ALTER MATERIALIZED VIEW, found: AS", - ), - ( - "ALTER MATERIALIZED VIEW m ALTER COLUMN c SET DEFAULT 1", - "Expected: OWNER TO, RENAME, SET or RESET after ALTER MATERIALIZED VIEW, found: ALTER", - ), - ( - "ALTER MATERIALIZED m OWNER TO alice", - "Expected: VIEW, found: m", - ), - ] { - assert_eq!( - pg_and_generic().parse_sql_statements(sql).unwrap_err(), - ParserError::ParserError(expected.to_string()), - "{sql}" - ); - } + 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]