elefant_tools/models/
view.rs

1use crate::models::hypertable_retention::HypertableRetention;
2use crate::object_id::{HaveDependencies, ObjectId};
3use crate::pg_interval::Interval;
4use crate::quoting::AttemptedKeywordUsage::ColumnName;
5use crate::quoting::{quote_value_string, IdentifierQuoter, Quotable};
6use crate::whitespace_ignorant_string::WhitespaceIgnorantString;
7use crate::{HypertableCompression, PostgresSchema};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Eq, PartialEq, Default, Clone, Serialize, Deserialize)]
11pub struct PostgresView {
12    pub name: String,
13    pub definition: WhitespaceIgnorantString,
14    pub columns: Vec<PostgresViewColumn>,
15    pub comment: Option<String>,
16    pub is_materialized: bool,
17    pub view_options: ViewOptions,
18    pub object_id: ObjectId,
19    pub depends_on: Vec<ObjectId>,
20}
21
22impl HaveDependencies for &PostgresView {
23    fn depends_on(&self) -> &Vec<ObjectId> {
24        &self.depends_on
25    }
26
27    fn object_id(&self) -> ObjectId {
28        self.object_id
29    }
30}
31
32impl PostgresView {
33    pub fn get_create_view_sql(
34        &self,
35        schema: &PostgresSchema,
36        identifier_quoter: &IdentifierQuoter,
37    ) -> String {
38        let escaped_relation_name = format!(
39            "{}.{}",
40            schema.name.quote(identifier_quoter, ColumnName),
41            self.name.quote(identifier_quoter, ColumnName)
42        );
43
44        let mut sql = "create".to_string();
45
46        if self.is_materialized {
47            sql.push_str(" materialized");
48        }
49
50        sql.push_str(" view ");
51        sql.push_str(&escaped_relation_name);
52
53        sql.push_str(" (");
54
55        for (i, column) in self.columns.iter().enumerate() {
56            if i != 0 {
57                sql.push_str(", ");
58            }
59
60            sql.push_str(&column.name.quote(identifier_quoter, ColumnName));
61        }
62
63        sql.push_str(") ");
64
65        if let ViewOptions::TimescaleContinuousAggregate { .. } = &self.view_options {
66            sql.push_str("with (timescaledb.continuous) ");
67        }
68
69        sql.push_str("as ");
70
71        sql.push_str(&self.definition);
72
73        if self.is_materialized {
74            while sql.ends_with(';') {
75                sql.pop();
76            }
77            sql.push_str(" with no data;");
78        }
79
80        if let Some(comment) = &self.comment {
81            sql.push_str("\ncomment on ");
82            if self.is_materialized {
83                sql.push_str("materialized ");
84            }
85            sql.push_str("view ");
86            sql.push_str(&escaped_relation_name);
87            sql.push_str(" is ");
88            sql.push_str(&quote_value_string(comment));
89            sql.push(';');
90        }
91
92        if let ViewOptions::TimescaleContinuousAggregate {
93            refresh,
94            compression,
95            retention,
96        } = &self.view_options
97        {
98            if let Some(refresh) = refresh {
99                sql.push_str("\nselect add_continuous_aggregate_policy('");
100                sql.push_str(&escaped_relation_name);
101                sql.push_str("', start_offset => INTERVAL '");
102                sql.push_str(&refresh.start_offset.to_postgres());
103                sql.push_str("', end_offset => INTERVAL '");
104                sql.push_str(&refresh.end_offset.to_postgres());
105                sql.push_str("', schedule_interval => INTERVAL '");
106                sql.push_str(&refresh.interval.to_postgres());
107                sql.push_str("');");
108            }
109
110            if let Some(compression) = compression {
111                sql.push_str("alter materialized view ");
112                compression.add_compression_settings(
113                    &mut sql,
114                    &escaped_relation_name,
115                    identifier_quoter,
116                );
117            }
118
119            if let Some(retention) = retention {
120                sql.push('\n');
121                retention.add_retention(&mut sql, &escaped_relation_name);
122            }
123        }
124
125        sql
126    }
127
128    pub fn get_refresh_sql(
129        &self,
130        schema: &PostgresSchema,
131        identifier_quoter: &IdentifierQuoter,
132    ) -> Option<String> {
133        if let ViewOptions::TimescaleContinuousAggregate { .. } = &self.view_options {
134            let sql = format!(
135                "call refresh_continuous_aggregate('{}.{}', null, null);",
136                schema.name.quote(identifier_quoter, ColumnName),
137                self.name.quote(identifier_quoter, ColumnName)
138            );
139            Some(sql)
140        } else if self.is_materialized {
141            let sql = format!(
142                "refresh materialized view {}.{};",
143                schema.name.quote(identifier_quoter, ColumnName),
144                self.name.quote(identifier_quoter, ColumnName)
145            );
146            Some(sql)
147        } else {
148            None
149        }
150    }
151}
152
153#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
154pub struct PostgresViewColumn {
155    pub name: String,
156    pub ordinal_position: i32,
157}
158
159#[allow(clippy::large_enum_variant)]
160#[derive(Debug, Eq, PartialEq, Default, Clone, Serialize, Deserialize)]
161#[serde(tag = "type")]
162pub enum ViewOptions {
163    #[default]
164    None,
165    TimescaleContinuousAggregate {
166        refresh: Option<TimescaleContinuousAggregateRefreshOptions>,
167        compression: Option<HypertableCompression>,
168        retention: Option<HypertableRetention>,
169    },
170}
171
172#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
173pub struct TimescaleContinuousAggregateRefreshOptions {
174    pub interval: Interval,
175    pub start_offset: Interval,
176    pub end_offset: Interval,
177}