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            check_option: None,
55            comment: None,
56            raw_body: String::new(),
57            owner: None,
58            grants: vec![],
59        };
60        let sql = render_view(&v);
61        assert!(sql.starts_with("CREATE VIEW"), "got: {sql}");
62        assert!(sql.contains("app.active_users"), "got: {sql}");
63        assert!(sql.contains("SELECT"), "got: {sql}");
64        assert!(sql.ends_with(';'), "got: {sql}");
65    }
66
67    #[test]
68    fn render_view_no_or_replace() {
69        let v = View {
70            qname: qn("app", "v"),
71            columns: vec![],
72            body_canonical: body("SELECT 1"),
73            body_dependencies: vec![],
74            security_barrier: None,
75            security_invoker: None,
76            check_option: None,
77            comment: None,
78            raw_body: String::new(),
79            owner: None,
80            grants: vec![],
81        };
82        let sql = render_view(&v);
83        assert!(
84            !sql.contains("OR REPLACE"),
85            "render_view should not emit OR REPLACE: {sql}"
86        );
87    }
88
89    #[test]
90    fn render_materialized_view_produces_create_materialized_view() {
91        let mv = MaterializedView {
92            qname: qn("app", "user_summary"),
93            columns: vec![ViewColumn {
94                name: id("cnt"),
95                column_type: ColumnType::BigInt,
96                comment: None,
97            }],
98            body_canonical: body("SELECT count(*) FROM app.users"),
99            body_dependencies: vec![],
100            comment: None,
101            raw_body: String::new(),
102            owner: None,
103            grants: vec![],
104            storage: crate::ir::reloptions::MaterializedViewStorageOptions::default(),
105        };
106        let sql = render_materialized_view(&mv);
107        assert!(sql.starts_with("CREATE MATERIALIZED VIEW"), "got: {sql}");
108        assert!(sql.contains("app.user_summary"), "got: {sql}");
109        assert!(sql.contains("SELECT"), "got: {sql}");
110        assert!(sql.ends_with(';'), "got: {sql}");
111    }
112}