diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0d2bfff9f..2e08ee68d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -39,13 +39,13 @@ pub use self::ddl::{ }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ - Cte, Distinct, ExceptSelectItem, ExcludeSelectItem, Fetch, ForClause, ForJson, ForXml, - GroupByExpr, IdentWithAlias, Join, JoinConstraint, JoinOperator, JsonTableColumn, - JsonTableColumnErrorHandling, LateralView, LockClause, LockType, NamedWindowDefinition, - NonBlock, Offset, OffsetRows, OrderByExpr, Query, RenameSelectItem, ReplaceSelectElement, - ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Table, - TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode, - Values, WildcardAdditionalOptions, With, + Cte, CteAsMaterialized, Distinct, ExceptSelectItem, ExcludeSelectItem, Fetch, ForClause, + ForJson, ForXml, GroupByExpr, IdentWithAlias, Join, JoinConstraint, JoinOperator, + JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType, + NamedWindowDefinition, NonBlock, Offset, OffsetRows, OrderByExpr, Query, RenameSelectItem, + ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SetOperator, + SetQuantifier, Table, TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, + ValueTableMode, Values, WildcardAdditionalOptions, With, }; pub use self::value::{ escape_quoted_string, DateTimeField, DollarQuotedString, TrimWhereField, Value, diff --git a/src/ast/query.rs b/src/ast/query.rs index f3c2d532c..bf33cdee6 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -383,7 +383,31 @@ impl fmt::Display for With { } } -/// A single CTE (used after `WITH`): `alias [(col1, col2, ...)] AS ( query )` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CteAsMaterialized { + /// The `WITH` statement specifies `AS MATERIALIZED` behavior + Materialized, + /// The `WITH` statement specifies `AS NOT MATERIALIZED` behavior + NotMaterialized, +} + +impl fmt::Display for CteAsMaterialized { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + CteAsMaterialized::Materialized => { + write!(f, "MATERIALIZED")?; + } + CteAsMaterialized::NotMaterialized => { + write!(f, "NOT MATERIALIZED")?; + } + }; + Ok(()) + } +} + +/// A single CTE (used after `WITH`): ` [(col1, col2, ...)] AS ( )` /// The names in the column list before `AS`, when specified, replace the names /// of the columns returned by the query. The parser does not validate that the /// number of columns in the query matches the number of columns in the query. @@ -394,11 +418,15 @@ pub struct Cte { pub alias: TableAlias, pub query: Box, pub from: Option, + pub materialized: Option, } impl fmt::Display for Cte { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} AS ({})", self.alias, self.query)?; + match self.materialized.as_ref() { + None => write!(f, "{} AS ({})", self.alias, self.query)?, + Some(materialized) => write!(f, "{} AS {materialized} ({})", self.alias, self.query)?, + }; if let Some(ref fr) = self.from { write!(f, " FROM {fr}")?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 33a310cf8..94bac6ec1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6719,6 +6719,14 @@ impl<'a> Parser<'a> { let name = self.parse_identifier(false)?; let mut cte = if self.parse_keyword(Keyword::AS) { + let mut is_materialized = None; + if dialect_of!(self is PostgreSqlDialect) { + if self.parse_keyword(Keyword::MATERIALIZED) { + is_materialized = Some(CteAsMaterialized::Materialized); + } else if self.parse_keywords(&[Keyword::NOT, Keyword::MATERIALIZED]) { + is_materialized = Some(CteAsMaterialized::NotMaterialized); + } + } self.expect_token(&Token::LParen)?; let query = Box::new(self.parse_query()?); self.expect_token(&Token::RParen)?; @@ -6730,10 +6738,19 @@ impl<'a> Parser<'a> { alias, query, from: None, + materialized: is_materialized, } } else { let columns = self.parse_parenthesized_column_list(Optional, false)?; self.expect_keyword(Keyword::AS)?; + let mut is_materialized = None; + if dialect_of!(self is PostgreSqlDialect) { + if self.parse_keyword(Keyword::MATERIALIZED) { + is_materialized = Some(CteAsMaterialized::Materialized); + } else if self.parse_keywords(&[Keyword::NOT, Keyword::MATERIALIZED]) { + is_materialized = Some(CteAsMaterialized::NotMaterialized); + } + } self.expect_token(&Token::LParen)?; let query = Box::new(self.parse_query()?); self.expect_token(&Token::RParen)?; @@ -6742,6 +6759,7 @@ impl<'a> Parser<'a> { alias, query, from: None, + materialized: is_materialized, } }; if self.parse_keyword(Keyword::FROM) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 13bc333c4..a32468fb8 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5589,6 +5589,7 @@ fn parse_recursive_cte() { }, query: Box::new(cte_query), from: None, + materialized: None, }; assert_eq!(with.cte_tables.first().unwrap(), &expected); } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1131a79b0..dd93e8991 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3837,3 +3837,12 @@ fn parse_array_agg() { let sql4 = "SELECT ARRAY_AGG(my_schema.sections_tbl.*) AS sections FROM sections_tbl"; pg().verified_stmt(sql4); } + +#[test] +fn parse_mat_cte() { + let sql = r#"WITH cte AS MATERIALIZED (SELECT id FROM accounts) SELECT id FROM cte"#; + pg().verified_stmt(sql); + + let sql2 = r#"WITH cte AS NOT MATERIALIZED (SELECT id FROM accounts) SELECT id FROM cte"#; + pg().verified_stmt(sql2); +}