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        let schema = SQLTableInfo::schema(view).unwrap_or("public");
67        let name = view.name();
68
69        // Build: REFRESH MATERIALIZED VIEW "schema"."name"
70        let sql = SQL::from_iter([Token::REFRESH, Token::MATERIALIZED, Token::VIEW])
71            .append(SQL::ident(schema))
72            .push(Token::DOT)
73            .append(SQL::ident(name));
74
75        Self {
76            sql,
77            _state: PhantomData,
78        }
79    }
80
81    /// Adds the CONCURRENTLY option
82    ///
83    /// This allows the view to be refreshed without locking out concurrent reads.
84    /// Requires the materialized view to have at least one unique index.
85    ///
86    /// Note: Cannot be combined with WITH NO DATA.
87    #[must_use]
88    pub fn concurrently(self) -> RefreshMaterializedView<'a, RefreshConcurrently> {
89        // We need to insert CONCURRENTLY after VIEW
90        // Current: REFRESH MATERIALIZED VIEW "schema"."name"
91        // Desired: REFRESH MATERIALIZED VIEW CONCURRENTLY "schema"."name"
92
93        // Get schema.name portion (last 3 chunks: ident, dot, ident)
94        let chunks = self.sql.chunks;
95        let schema_name_start = 3; // After REFRESH, MATERIALIZED, VIEW
96
97        let mut new_sql = SQL::from_iter([
98            Token::REFRESH,
99            Token::MATERIALIZED,
100            Token::VIEW,
101            Token::CONCURRENTLY,
102        ]);
103
104        // Append the remaining chunks (schema.name)
105        for chunk in chunks.into_iter().skip(schema_name_start) {
106            new_sql = new_sql.push(chunk);
107        }
108
109        RefreshMaterializedView {
110            sql: new_sql,
111            _state: PhantomData,
112        }
113    }
114
115    /// Adds the WITH NO DATA option
116    ///
117    /// This causes the materialized view to be emptied rather than refreshed with data.
118    /// The view cannot be queried until data is added with a subsequent REFRESH.
119    ///
120    /// Note: Cannot be combined with CONCURRENTLY.
121    #[must_use]
122    pub fn with_no_data(self) -> RefreshMaterializedView<'a, RefreshWithNoData> {
123        RefreshMaterializedView {
124            sql: self.sql.push(Token::WITH).push(Token::NO).push(Token::DATA),
125            _state: PhantomData,
126        }
127    }
128
129    /// Adds the WITH DATA option (explicit, but this is the default behavior)
130    #[must_use]
131    pub fn with_data(self) -> Self {
132        Self {
133            sql: self.sql.push(Token::WITH).push(Token::DATA),
134            _state: PhantomData,
135        }
136    }
137}
138
139//------------------------------------------------------------------------------
140// ToSQL implementations
141//------------------------------------------------------------------------------
142
143impl<'a, State> ToSQL<'a, PostgresValue<'a>> for RefreshMaterializedView<'a, State> {
144    fn to_sql(&self) -> SQL<'a, PostgresValue<'a>> {
145        self.sql.clone()
146    }
147}
148
149//------------------------------------------------------------------------------
150// Helper function for the query builder
151//------------------------------------------------------------------------------
152
153/// Creates a REFRESH MATERIALIZED VIEW statement for the given view
154pub fn refresh_materialized_view<V: SQLViewInfo>(
155    view: &V,
156) -> RefreshMaterializedView<'_, RefreshInitial> {
157    RefreshMaterializedView::new(view)
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    // Mock view for testing
165    struct TestView;
166
167    impl drizzle_core::traits::SQLTableInfo for TestView {
168        fn name(&self) -> &'static str {
169            "user_stats"
170        }
171
172        fn schema(&self) -> Option<&'static str> {
173            Some("public")
174        }
175    }
176
177    impl SQLViewInfo for TestView {
178        fn definition_sql(&self) -> std::borrow::Cow<'static, str> {
179            "SELECT * FROM users".into()
180        }
181
182        fn is_materialized(&self) -> bool {
183            true
184        }
185    }
186
187    #[test]
188    fn test_basic_refresh() {
189        let view = TestView;
190        let refresh = RefreshMaterializedView::new(&view);
191        let sql = refresh.to_sql();
192
193        assert_eq!(
194            sql.sql(),
195            r#"REFRESH MATERIALIZED VIEW "public"."user_stats""#
196        );
197    }
198
199    #[test]
200    fn test_concurrent_refresh() {
201        let view = TestView;
202        let refresh = RefreshMaterializedView::new(&view).concurrently();
203        let sql = refresh.to_sql();
204
205        assert_eq!(
206            sql.sql(),
207            r#"REFRESH MATERIALIZED VIEW CONCURRENTLY "public"."user_stats""#
208        );
209    }
210
211    #[test]
212    fn test_refresh_with_no_data() {
213        let view = TestView;
214        let refresh = RefreshMaterializedView::new(&view).with_no_data();
215        let sql = refresh.to_sql();
216
217        assert_eq!(
218            sql.sql(),
219            r#"REFRESH MATERIALIZED VIEW "public"."user_stats" WITH NO DATA"#
220        );
221    }
222
223    #[test]
224    fn test_refresh_with_data() {
225        let view = TestView;
226        let refresh = RefreshMaterializedView::new(&view).with_data();
227        let sql = refresh.to_sql();
228
229        assert_eq!(
230            sql.sql(),
231            r#"REFRESH MATERIALIZED VIEW "public"."user_stats" WITH DATA"#
232        );
233    }
234
235    #[test]
236    fn test_helper_function() {
237        let view = TestView;
238        let refresh = refresh_materialized_view(&view);
239        let sql = refresh.to_sql();
240
241        assert_eq!(
242            sql.sql(),
243            r#"REFRESH MATERIALIZED VIEW "public"."user_stats""#
244        );
245    }
246}