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}