Skip to main content

pgevolve_core/render/
view.rs

1//! View and materialized-view renderer.
2//!
3//! Produces `CREATE VIEW` and `CREATE MATERIALIZED VIEW` statements for use
4//! in `render_catalog` (shadow-validate path) and `pgevolve dump`.
5
6use crate::ir::view::{MaterializedView, View};
7
8/// Render a `View` as a `CREATE VIEW ... AS <body>;` SQL string.
9///
10/// Intentionally omits `OR REPLACE` — this renderer targets a fresh shadow
11/// DB (apply path), so the simpler `CREATE VIEW` form is correct.
12#[must_use]
13pub fn render_view(v: &View) -> String {
14    use crate::plan::rewrite::views as emit;
15    emit::emit_create_view(v, false)
16}
17
18/// Render a `MaterializedView` as a `CREATE MATERIALIZED VIEW ... AS <body> WITH NO DATA;` string.
19#[must_use]
20pub fn render_materialized_view(mv: &MaterializedView) -> String {
21    use crate::plan::rewrite::views as emit;
22    emit::emit_create_materialized_view(mv)
23}
24
25#[cfg(test)]
26mod tests {
27    use super::*;
28    use crate::identifier::{Identifier, QualifiedName};
29    use crate::ir::column_type::ColumnType;
30    use crate::ir::view::{MaterializedView, View, ViewColumn};
31    use crate::parse::normalize_body::NormalizedBody;
32
33    fn id(s: &str) -> Identifier {
34        Identifier::from_unquoted(s).unwrap()
35    }
36
37    fn qn(schema: &str, name: &str) -> QualifiedName {
38        QualifiedName::new(id(schema), id(name))
39    }
40
41    fn body(sql: &str) -> NormalizedBody {
42        NormalizedBody::from_sql(sql).unwrap()
43    }
44
45    #[test]
46    fn render_view_produces_create_view() {
47        let v = View {
48            qname: qn("app", "active_users"),
49            columns: vec![],
50            body_canonical: body("SELECT id FROM app.users WHERE active = true"),
51            body_dependencies: vec![],
52            security_barrier: None,
53            security_invoker: None,
54            comment: None,
55            raw_body: String::new(),
56            owner: None,
57            grants: vec![],
58        };
59        let sql = render_view(&v);
60        assert!(sql.starts_with("CREATE VIEW"), "got: {sql}");
61        assert!(sql.contains("app.active_users"), "got: {sql}");
62        assert!(sql.contains("SELECT"), "got: {sql}");
63        assert!(sql.ends_with(';'), "got: {sql}");
64    }
65
66    #[test]
67    fn render_view_no_or_replace() {
68        let v = View {
69            qname: qn("app", "v"),
70            columns: vec![],
71            body_canonical: body("SELECT 1"),
72            body_dependencies: vec![],
73            security_barrier: None,
74            security_invoker: None,
75            comment: None,
76            raw_body: String::new(),
77            owner: None,
78            grants: vec![],
79        };
80        let sql = render_view(&v);
81        assert!(
82            !sql.contains("OR REPLACE"),
83            "render_view should not emit OR REPLACE: {sql}"
84        );
85    }
86
87    #[test]
88    fn render_materialized_view_produces_create_materialized_view() {
89        let mv = MaterializedView {
90            qname: qn("app", "user_summary"),
91            columns: vec![ViewColumn {
92                name: id("cnt"),
93                column_type: ColumnType::BigInt,
94                comment: None,
95            }],
96            body_canonical: body("SELECT count(*) FROM app.users"),
97            body_dependencies: vec![],
98            comment: None,
99            raw_body: String::new(),
100            owner: None,
101            grants: vec![],
102            storage: crate::ir::reloptions::MaterializedViewStorageOptions::default(),
103        };
104        let sql = render_materialized_view(&mv);
105        assert!(sql.starts_with("CREATE MATERIALIZED VIEW"), "got: {sql}");
106        assert!(sql.contains("app.user_summary"), "got: {sql}");
107        assert!(sql.contains("SELECT"), "got: {sql}");
108        assert!(sql.ends_with(';'), "got: {sql}");
109    }
110}