Skip to main content

drizzle_postgres/builder/
refresh.rs

1//! `REFRESH MATERIALIZED VIEW` statements for `PostgreSQL`.
2//!
3//! Build the statement with [`refresh_materialized_view`] or
4//! [`RefreshMaterializedView::new`] and run it with a driver's
5//! `execute(...)`, which accepts any `ToSQL` value.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use drizzle_postgres::builder::refresh::RefreshMaterializedView;
11//! # use drizzle_core::traits::{SQLTableInfo, SQLViewInfo};
12//! # struct UserStats;
13//! # impl SQLTableInfo for UserStats {
14//! #     fn name(&self) -> &'static str { "user_stats" }
15//! #     fn schema(&self) -> Option<&'static str> { None }
16//! # }
17//! # impl SQLViewInfo for UserStats {
18//! #     fn definition_sql(&self) -> std::borrow::Cow<'static, str> { "SELECT 1".into() }
19//! #     fn is_materialized(&self) -> bool { true }
20//! # }
21//! # let user_stats = UserStats;
22//! use drizzle_core::ToSQL;
23//!
24//! // `user_stats` stands for a materialized view defined with `#[PostgresView]`.
25//! let refresh = RefreshMaterializedView::new(&user_stats);
26//! assert_eq!(refresh.to_sql().sql(), r#"REFRESH MATERIALIZED VIEW "user_stats""#);
27//!
28//! // Keep the view readable while it refreshes (needs a unique index).
29//! let refresh = RefreshMaterializedView::new(&user_stats).concurrently();
30//! assert_eq!(
31//!     refresh.to_sql().sql(),
32//!     r#"REFRESH MATERIALIZED VIEW CONCURRENTLY "user_stats""#
33//! );
34//!
35//! // Empty the view; it cannot be queried until refreshed again.
36//! let refresh = RefreshMaterializedView::new(&user_stats).with_no_data();
37//! assert_eq!(
38//!     refresh.to_sql().sql(),
39//!     r#"REFRESH MATERIALIZED VIEW "user_stats" WITH NO DATA"#
40//! );
41//! ```
42
43use crate::values::PostgresValue;
44use core::marker::PhantomData;
45use drizzle_core::traits::{SQLTableInfo, SQLViewInfo};
46use drizzle_core::{SQL, ToSQL, Token};
47
48//------------------------------------------------------------------------------
49// Type State Markers
50//------------------------------------------------------------------------------
51
52/// [`RefreshMaterializedView`] state before any option is chosen.
53#[derive(Debug, Clone, Copy, Default)]
54pub struct RefreshInitial;
55
56/// [`RefreshMaterializedView`] state after `.concurrently()`.
57#[derive(Debug, Clone, Copy, Default)]
58pub struct RefreshConcurrently;
59
60/// [`RefreshMaterializedView`] state after `.with_no_data()`.
61#[derive(Debug, Clone, Copy, Default)]
62pub struct RefreshWithNoData;
63
64//------------------------------------------------------------------------------
65// RefreshMaterializedView Builder
66//------------------------------------------------------------------------------
67
68/// Builds a `REFRESH MATERIALIZED VIEW` statement.
69///
70/// ```sql
71/// REFRESH MATERIALIZED VIEW [ CONCURRENTLY ] view_name [ WITH [ NO ] DATA ]
72/// ```
73///
74/// `CONCURRENTLY` and `WITH NO DATA` cannot be combined; the state parameter
75/// enforces this. A view outside the `public` schema is schema-qualified.
76/// See the [module docs](self) for examples.
77#[derive(Debug, Clone)]
78pub struct RefreshMaterializedView<'a, State = RefreshInitial> {
79    sql: SQL<'a, PostgresValue<'a>>,
80    _state: PhantomData<State>,
81}
82
83impl<'a> RefreshMaterializedView<'a, RefreshInitial> {
84    /// Starts `REFRESH MATERIALIZED VIEW view`.
85    #[must_use]
86    pub fn new<V: SQLViewInfo>(view: &'a V) -> Self {
87        Self {
88            sql: SQL::from_iter([Token::REFRESH, Token::MATERIALIZED, Token::VIEW])
89                .append(qualified_view_name(view)),
90            _state: PhantomData,
91        }
92    }
93
94    /// Adds `CONCURRENTLY`: the view stays readable during the refresh.
95    ///
96    /// `PostgreSQL` requires a unique index on the materialized view for this.
97    /// Cannot be combined with `WITH NO DATA`.
98    #[must_use]
99    pub fn concurrently(self) -> RefreshMaterializedView<'a, RefreshConcurrently> {
100        // Rebuild as REFRESH MATERIALIZED VIEW CONCURRENTLY <name>: the name
101        // chunks are everything after the three leading keywords.
102        let mut sql = SQL::from_iter([
103            Token::REFRESH,
104            Token::MATERIALIZED,
105            Token::VIEW,
106            Token::CONCURRENTLY,
107        ]);
108        for chunk in self.sql.chunks.into_iter().skip(3) {
109            sql = sql.push(chunk);
110        }
111
112        RefreshMaterializedView {
113            sql,
114            _state: PhantomData,
115        }
116    }
117
118    /// Adds `WITH NO DATA`: the view is emptied instead of refreshed.
119    ///
120    /// The view cannot be queried until a later refresh fills it. Cannot be
121    /// combined with `CONCURRENTLY`.
122    #[must_use]
123    pub fn with_no_data(self) -> RefreshMaterializedView<'a, RefreshWithNoData> {
124        RefreshMaterializedView {
125            sql: self.sql.push(Token::WITH).push(Token::NO).push(Token::DATA),
126            _state: PhantomData,
127        }
128    }
129
130    /// Adds `WITH DATA`, the default behaviour, explicitly.
131    #[must_use]
132    pub fn with_data(self) -> Self {
133        Self {
134            sql: self.sql.push(Token::WITH).push(Token::DATA),
135            _state: PhantomData,
136        }
137    }
138}
139
140/// `"schema"."name"` for views in a non-default schema, otherwise the bare name
141/// so the statement resolves through `search_path` exactly like the view's own
142/// DDL (which also leaves `public` unqualified).
143fn qualified_view_name<'a, V: SQLViewInfo>(view: &V) -> SQL<'a, PostgresValue<'a>> {
144    let name = SQL::ident(view.name());
145    match SQLTableInfo::schema(view) {
146        Some(schema) if schema != "public" => SQL::ident(schema).push(Token::DOT).append(name),
147        _ => name,
148    }
149}
150
151//------------------------------------------------------------------------------
152// ToSQL implementations
153//------------------------------------------------------------------------------
154
155impl<'a, State> ToSQL<'a, PostgresValue<'a>> for RefreshMaterializedView<'a, State> {
156    fn to_sql(&self) -> SQL<'a, PostgresValue<'a>> {
157        self.sql.clone()
158    }
159}
160
161//------------------------------------------------------------------------------
162// Helper function for the query builder
163//------------------------------------------------------------------------------
164
165/// Starts `REFRESH MATERIALIZED VIEW view`. Same as [`RefreshMaterializedView::new`].
166///
167/// # Examples
168///
169/// ```rust
170/// use drizzle_postgres::builder::refresh_materialized_view;
171/// # use drizzle_core::traits::{SQLTableInfo, SQLViewInfo};
172/// # struct UserStats;
173/// # impl SQLTableInfo for UserStats {
174/// #     fn name(&self) -> &'static str { "user_stats" }
175/// #     fn schema(&self) -> Option<&'static str> { None }
176/// # }
177/// # impl SQLViewInfo for UserStats {
178/// #     fn definition_sql(&self) -> std::borrow::Cow<'static, str> { "SELECT 1".into() }
179/// #     fn is_materialized(&self) -> bool { true }
180/// # }
181/// # let user_stats = UserStats;
182/// use drizzle_core::ToSQL;
183///
184/// let refresh = refresh_materialized_view(&user_stats).concurrently();
185/// assert_eq!(
186///     refresh.to_sql().sql(),
187///     r#"REFRESH MATERIALIZED VIEW CONCURRENTLY "user_stats""#
188/// );
189/// ```
190pub fn refresh_materialized_view<V: SQLViewInfo>(
191    view: &V,
192) -> RefreshMaterializedView<'_, RefreshInitial> {
193    RefreshMaterializedView::new(view)
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    // Mock view for testing
201    struct TestView;
202
203    impl drizzle_core::traits::SQLTableInfo for TestView {
204        fn name(&self) -> &'static str {
205            "user_stats"
206        }
207
208        fn schema(&self) -> Option<&'static str> {
209            Some("public")
210        }
211    }
212
213    impl SQLViewInfo for TestView {
214        fn definition_sql(&self) -> std::borrow::Cow<'static, str> {
215            "SELECT * FROM users".into()
216        }
217
218        fn is_materialized(&self) -> bool {
219            true
220        }
221    }
222
223    #[test]
224    fn test_basic_refresh() {
225        let view = TestView;
226        let refresh = RefreshMaterializedView::new(&view);
227        let sql = refresh.to_sql();
228
229        assert_eq!(sql.sql(), r#"REFRESH MATERIALIZED VIEW "user_stats""#);
230    }
231
232    #[test]
233    fn test_concurrent_refresh() {
234        let view = TestView;
235        let refresh = RefreshMaterializedView::new(&view).concurrently();
236        let sql = refresh.to_sql();
237
238        assert_eq!(
239            sql.sql(),
240            r#"REFRESH MATERIALIZED VIEW CONCURRENTLY "user_stats""#
241        );
242    }
243
244    #[test]
245    fn test_refresh_with_no_data() {
246        let view = TestView;
247        let refresh = RefreshMaterializedView::new(&view).with_no_data();
248        let sql = refresh.to_sql();
249
250        assert_eq!(
251            sql.sql(),
252            r#"REFRESH MATERIALIZED VIEW "user_stats" WITH NO DATA"#
253        );
254    }
255
256    #[test]
257    fn test_refresh_with_data() {
258        let view = TestView;
259        let refresh = RefreshMaterializedView::new(&view).with_data();
260        let sql = refresh.to_sql();
261
262        assert_eq!(
263            sql.sql(),
264            r#"REFRESH MATERIALIZED VIEW "user_stats" WITH DATA"#
265        );
266    }
267
268    struct ExplicitSchemaView;
269
270    impl drizzle_core::traits::SQLTableInfo for ExplicitSchemaView {
271        fn name(&self) -> &'static str {
272            "user_stats"
273        }
274
275        fn schema(&self) -> Option<&'static str> {
276            Some("analytics")
277        }
278    }
279
280    impl SQLViewInfo for ExplicitSchemaView {
281        fn definition_sql(&self) -> std::borrow::Cow<'static, str> {
282            "SELECT * FROM users".into()
283        }
284
285        fn is_materialized(&self) -> bool {
286            true
287        }
288    }
289
290    #[test]
291    fn test_non_public_schema_is_qualified() {
292        let view = ExplicitSchemaView;
293        assert_eq!(
294            RefreshMaterializedView::new(&view).to_sql().sql(),
295            r#"REFRESH MATERIALIZED VIEW "analytics"."user_stats""#
296        );
297        assert_eq!(
298            RefreshMaterializedView::new(&view)
299                .concurrently()
300                .to_sql()
301                .sql(),
302            r#"REFRESH MATERIALIZED VIEW CONCURRENTLY "analytics"."user_stats""#
303        );
304    }
305
306    #[test]
307    fn test_helper_function() {
308        let view = TestView;
309        let refresh = refresh_materialized_view(&view);
310        let sql = refresh.to_sql();
311
312        assert_eq!(sql.sql(), r#"REFRESH MATERIALIZED VIEW "user_stats""#);
313    }
314}