use pg_query::NodeEnum;
use pg_query::protobuf::CreateTableAsStmt;
use crate::identifier::Identifier;
use crate::ir::view::{MaterializedView, ViewColumn};
use crate::parse::builder::shared;
use crate::parse::error::{ParseError, SourceLocation};
use crate::parse::normalize_body::NormalizedBody;
pub(crate) fn build_materialized_view(
stmt: &CreateTableAsStmt,
default_schema: Option<&Identifier>,
location: &SourceLocation,
) -> Result<MaterializedView, ParseError> {
let into = stmt.into.as_ref().ok_or_else(|| ParseError::Structural {
location: location.clone(),
message: "CREATE MATERIALIZED VIEW missing INTO clause".into(),
})?;
let rel = into.rel.as_ref().ok_or_else(|| ParseError::Structural {
location: location.clone(),
message: "CREATE MATERIALIZED VIEW INTO clause missing relation".into(),
})?;
let qname = shared::resolve_qname(rel, default_schema, location)?;
let columns = mv_columns_from_col_names(&into.col_names, location)?;
let raw_body = extract_query_body(stmt.query.as_deref(), location)?;
let storage = crate::parse::builder::reloptions::decode_table_options(&into.options, location)?;
Ok(MaterializedView {
qname,
columns,
body_canonical: NormalizedBody::empty(),
body_dependencies: Vec::new(),
comment: None,
raw_body,
owner: None,
grants: vec![],
storage,
})
}
fn extract_query_body(
query_node: Option<&pg_query::protobuf::Node>,
location: &SourceLocation,
) -> Result<String, ParseError> {
let Some(node) = query_node else {
return Err(ParseError::Structural {
location: location.clone(),
message: "CREATE MATERIALIZED VIEW missing query body".into(),
});
};
let Some(node_inner) = &node.node else {
return Err(ParseError::Structural {
location: location.clone(),
message: "CREATE MATERIALIZED VIEW query body node is empty".into(),
});
};
let deparsed = node_inner
.to_ref()
.deparse()
.map_err(|e| ParseError::Structural {
location: location.clone(),
message: format!("failed to deparse materialized view query body: {e}"),
})?;
if deparsed.trim().is_empty() {
return Err(ParseError::Structural {
location: location.clone(),
message: format!(
"deparsed materialized view query body is empty (node type: {:?})",
std::mem::discriminant(node_inner)
),
});
}
Ok(deparsed)
}
fn mv_columns_from_col_names(
col_names: &[pg_query::protobuf::Node],
location: &SourceLocation,
) -> Result<Vec<ViewColumn>, ParseError> {
if col_names.is_empty() {
return Ok(Vec::new());
}
col_names
.iter()
.map(|node| {
let Some(NodeEnum::String(s)) = node.node.as_ref() else {
return Err(ParseError::Structural {
location: location.clone(),
message: "expected string node in materialized view column name list".into(),
});
};
let name = shared::ident(&s.sval, location)?;
Ok(ViewColumn {
name,
column_type: None,
comment: None,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use pg_query::protobuf::ObjectType;
use std::path::PathBuf;
fn loc() -> SourceLocation {
SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
}
fn parse_matview(sql: &str) -> CreateTableAsStmt {
let parsed = pg_query::parse(sql).expect("parses");
let node = parsed
.protobuf
.stmts
.into_iter()
.next()
.and_then(|r| r.stmt)
.and_then(|n| n.node)
.expect("stmt");
let NodeEnum::CreateTableAsStmt(s) = node else {
panic!("expected CreateTableAsStmt, got: {node:?}");
};
assert_eq!(
ObjectType::try_from(s.objtype),
Ok(ObjectType::ObjectMatview),
"expected OBJECT_MATVIEW"
);
*s
}
fn build(sql: &str) -> MaterializedView {
let stmt = parse_matview(sql);
build_materialized_view(&stmt, None, &loc()).expect("build_materialized_view")
}
#[test]
fn simple_matview() {
let mv = build(
"CREATE MATERIALIZED VIEW app.summary AS SELECT count(*) FROM app.events WITH NO DATA;",
);
assert_eq!(mv.qname.to_string(), "app.summary");
assert!(mv.columns.is_empty());
assert!(mv.body_canonical.canonical_text().is_empty());
}
#[test]
fn matview_with_column_aliases() {
let mv = build("CREATE MATERIALIZED VIEW app.mv(a, b) AS SELECT 1, 2 WITH NO DATA;");
assert_eq!(mv.columns.len(), 2);
assert_eq!(mv.columns[0].name.as_str(), "a");
assert_eq!(mv.columns[1].name.as_str(), "b");
}
#[test]
fn unqualified_mv_uses_default_schema() {
let stmt = parse_matview("CREATE MATERIALIZED VIEW myview AS SELECT 1 WITH NO DATA;");
let app = Identifier::from_unquoted("app").unwrap();
let mv = build_materialized_view(&stmt, Some(&app), &loc()).unwrap();
assert_eq!(mv.qname.to_string(), "app.myview");
}
#[test]
fn unqualified_mv_without_schema_errors() {
let stmt = parse_matview("CREATE MATERIALIZED VIEW myview AS SELECT 1 WITH NO DATA;");
let err = build_materialized_view(&stmt, None, &loc()).unwrap_err();
assert!(matches!(err, ParseError::UnqualifiedName { .. }));
}
#[test]
fn body_canonical_is_empty_sentinel() {
let mv = build("CREATE MATERIALIZED VIEW app.mv AS SELECT 1 WITH NO DATA;");
assert!(mv.body_canonical.canonical_text().is_empty());
assert_eq!(mv.body_canonical.canonical_hash(), &[0u8; 32]);
}
}