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