Skip to main content

drizzle_postgres/builder/
refresh.rs

1//! REFRESH MATERIALIZED VIEW query builder for `PostgreSQL`
2//!
3//! This module provides a builder for constructing `REFRESH MATERIALIZED VIEW` statements.
4//!
5//! # Examples
6//!
7//! ```rust
8//! # let _ = r####"
9//! use drizzle_postgres::builder::refresh::RefreshMaterializedView;
10//!
11//! // Basic refresh
12//! let refresh = RefreshMaterializedView::new(&my_view);
13//!
14//! // Concurrent refresh (allows reads during refresh)
15//! let refresh = RefreshMaterializedView::new(&my_view).concurrently();
16//!
17//! // Refresh without data (empties the view)
18//! let refresh = RefreshMaterializedView::new(&my_view).with_no_data();
19//! # "####;
20//! ```
21
22use crate::values::PostgresValue;
23use core::marker::PhantomData;
24use drizzle_core::traits::{SQLTableInfo, SQLViewInfo};
25use drizzle_core::{SQL, ToSQL, Token};
26
27//------------------------------------------------------------------------------
28// Type State Markers
29//------------------------------------------------------------------------------
30
31/// Marker for the initial state of `RefreshMaterializedView`
32#[derive(Debug, Clone, Copy, Default)]
33pub struct RefreshInitial;
34
35/// Marker for the state after CONCURRENTLY is set
36#[derive(Debug, Clone, Copy, Default)]
37pub struct RefreshConcurrently;
38
39/// Marker for the state after WITH NO DATA is set
40#[derive(Debug, Clone, Copy, Default)]
41pub struct RefreshWithNoData;
42
43//------------------------------------------------------------------------------
44// RefreshMaterializedView Builder
45//------------------------------------------------------------------------------
46
47/// Builder for REFRESH MATERIALIZED VIEW statements
48///
49/// `PostgreSQL` syntax:
50/// ```sql
51/// REFRESH MATERIALIZED VIEW [ CONCURRENTLY ] view_name [ WITH [ NO ] DATA ]
52/// ```
53///
54/// Note: CONCURRENTLY and WITH NO DATA are mutually exclusive in `PostgreSQL`.
55/// CONCURRENTLY requires the materialized view to have a unique index.
56#[derive(Debug, Clone)]
57pub struct RefreshMaterializedView<'a, State = RefreshInitial> {
58    sql: SQL<'a, PostgresValue<'a>>,
59    _state: PhantomData<State>,
60}
61
62impl<'a> RefreshMaterializedView<'a, RefreshInitial> {
63    /// Creates a new REFRESH MATERIALIZED VIEW builder for the given view
64    #[must_use]
65    pub fn new<V: SQLViewInfo>(view: &'a V) -> Self {
66        Self {
67            sql: SQL::from_iter([Token::REFRESH, Token::MATERIALIZED, Token::VIEW])
68                .append(qualified_view_name(view)),
69            _state: PhantomData,
70        }
71    }
72
73    /// Adds the CONCURRENTLY option
74    ///
75    /// This allows the view to be refreshed without locking out concurrent reads.
76    /// Requires the materialized view to have at least one unique index.
77    ///
78    /// Note: Cannot be combined with WITH NO DATA.
79    #[must_use]
80    pub fn concurrently(self) -> RefreshMaterializedView<'a, RefreshConcurrently> {
81        // Rebuild as REFRESH MATERIALIZED VIEW CONCURRENTLY <name>: the name
82        // chunks are everything after the three leading keywords.
83        let mut sql = SQL::from_iter([
84            Token::REFRESH,
85            Token::MATERIALIZED,
86            Token::VIEW,
87            Token::CONCURRENTLY,
88        ]);
89        for chunk in self.sql.chunks.into_iter().skip(3) {
90            sql = sql.push(chunk);
91        }
92
93        RefreshMaterializedView {
94            sql,
95            _state: PhantomData,
96        }
97    }
98
99    /// Adds the WITH NO DATA option
100    ///
101    /// This causes the materialized view to be emptied rather than refreshed with data.
102    /// The view cannot be queried until data is added with a subsequent REFRESH.
103    ///
104    /// Note: Cannot be combined with CONCURRENTLY.
105    #[must_use]
106    pub fn with_no_data(self) -> RefreshMaterializedView<'a, RefreshWithNoData> {
107        RefreshMaterializedView {
108            sql: self.sql.push(Token::WITH).push(Token::NO).push(Token::DATA),
109            _state: PhantomData,
110        }
111    }
112
113    /// Adds the WITH DATA option (explicit, but this is the default behavior)
114    #[must_use]
115    pub fn with_data(self) -> Self {
116        Self {
117            sql: self.sql.push(Token::WITH).push(Token::DATA),
118            _state: PhantomData,
119        }
120    }
121}
122
123/// `"schema"."name"` for views in a non-default schema, otherwise the bare name
124/// so the statement resolves through `search_path` exactly like the view's own
125/// DDL (which also leaves `public` unqualified).
126fn qualified_view_name<'a, V: SQLViewInfo>(view: &V) -> SQL<'a, PostgresValue<'a>> {
127    let name = SQL::ident(view.name());
128    match SQLTableInfo::schema(view) {
129        Some(schema) if schema != "public" => SQL::ident(schema).push(Token::DOT).append(name),
130        _ => name,
131    }
132}
133
134//------------------------------------------------------------------------------
135// ToSQL implementations
136//------------------------------------------------------------------------------
137
138impl<'a, State> ToSQL<'a, PostgresValue<'a>> for RefreshMaterializedView<'a, State> {
139    fn to_sql(&self) -> SQL<'a, PostgresValue<'a>> {
140        self.sql.clone()
141    }
142}
143
144//------------------------------------------------------------------------------
145// Helper function for the query builder
146//------------------------------------------------------------------------------
147
148/// Creates a REFRESH MATERIALIZED VIEW statement for the given view
149pub fn refresh_materialized_view<V: SQLViewInfo>(
150    view: &V,
151) -> RefreshMaterializedView<'_, RefreshInitial> {
152    RefreshMaterializedView::new(view)
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    // Mock view for testing
160    struct TestView;
161
162    impl drizzle_core::traits::SQLTableInfo for TestView {
163        fn name(&self) -> &'static str {
164            "user_stats"
165        }
166
167        fn schema(&self) -> Option<&'static str> {
168            Some("public")
169        }
170    }
171
172    impl SQLViewInfo for TestView {
173        fn definition_sql(&self) -> std::borrow::Cow<'static, str> {
174            "SELECT * FROM users".into()
175        }
176
177        fn is_materialized(&self) -> bool {
178            true
179        }
180    }
181
182    #[test]
183    fn test_basic_refresh() {
184        let view = TestView;
185        let refresh = RefreshMaterializedView::new(&view);
186        let sql = refresh.to_sql();
187
188        assert_eq!(sql.sql(), r#"REFRESH MATERIALIZED VIEW "user_stats""#);
189    }
190
191    #[test]
192    fn test_concurrent_refresh() {
193        let view = TestView;
194        let refresh = RefreshMaterializedView::new(&view).concurrently();
195        let sql = refresh.to_sql();
196
197        assert_eq!(
198            sql.sql(),
199            r#"REFRESH MATERIALIZED VIEW CONCURRENTLY "user_stats""#
200        );
201    }
202
203    #[test]
204    fn test_refresh_with_no_data() {
205        let view = TestView;
206        let refresh = RefreshMaterializedView::new(&view).with_no_data();
207        let sql = refresh.to_sql();
208
209        assert_eq!(
210            sql.sql(),
211            r#"REFRESH MATERIALIZED VIEW "user_stats" WITH NO DATA"#
212        );
213    }
214
215    #[test]
216    fn test_refresh_with_data() {
217        let view = TestView;
218        let refresh = RefreshMaterializedView::new(&view).with_data();
219        let sql = refresh.to_sql();
220
221        assert_eq!(
222            sql.sql(),
223            r#"REFRESH MATERIALIZED VIEW "user_stats" WITH DATA"#
224        );
225    }
226
227    struct ExplicitSchemaView;
228
229    impl drizzle_core::traits::SQLTableInfo for ExplicitSchemaView {
230        fn name(&self) -> &'static str {
231            "user_stats"
232        }
233
234        fn schema(&self) -> Option<&'static str> {
235            Some("analytics")
236        }
237    }
238
239    impl SQLViewInfo for ExplicitSchemaView {
240        fn definition_sql(&self) -> std::borrow::Cow<'static, str> {
241            "SELECT * FROM users".into()
242        }
243
244        fn is_materialized(&self) -> bool {
245            true
246        }
247    }
248
249    #[test]
250    fn test_non_public_schema_is_qualified() {
251        let view = ExplicitSchemaView;
252        assert_eq!(
253            RefreshMaterializedView::new(&view).to_sql().sql(),
254            r#"REFRESH MATERIALIZED VIEW "analytics"."user_stats""#
255        );
256        assert_eq!(
257            RefreshMaterializedView::new(&view)
258                .concurrently()
259                .to_sql()
260                .sql(),
261            r#"REFRESH MATERIALIZED VIEW CONCURRENTLY "analytics"."user_stats""#
262        );
263    }
264
265    #[test]
266    fn test_helper_function() {
267        let view = TestView;
268        let refresh = refresh_materialized_view(&view);
269        let sql = refresh.to_sql();
270
271        assert_eq!(sql.sql(), r#"REFRESH MATERIALIZED VIEW "user_stats""#);
272    }
273}