drizzle_types/postgres/ddl/
view.rs

1//! PostgreSQL View DDL types
2//!
3//! This module provides two complementary types:
4//! - [`ViewDef`] - A const-friendly definition type for compile-time schema definitions
5//! - [`View`] - A runtime type for serde serialization/deserialization
6
7#[cfg(feature = "std")]
8use std::borrow::Cow;
9
10#[cfg(all(feature = "alloc", not(feature = "std")))]
11use alloc::borrow::Cow;
12
13#[cfg(feature = "serde")]
14use crate::serde_helpers::{cow_from_string, cow_option_from_string};
15
16// =============================================================================
17// ViewWithOption Types
18// =============================================================================
19
20/// Const-friendly view WITH options definition
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub struct ViewWithOptionDef {
23    /// CHECK OPTION ('local' | 'cascaded')
24    pub check_option: Option<&'static str>,
25    /// Security barrier flag
26    pub security_barrier: bool,
27    /// Security invoker flag
28    pub security_invoker: bool,
29    /// Fillfactor (for materialized views)
30    pub fillfactor: Option<i32>,
31    /// Toast tuple target (for materialized views)
32    pub toast_tuple_target: Option<i32>,
33    /// Parallel workers (for materialized views)
34    pub parallel_workers: Option<i32>,
35    /// Autovacuum enabled (for materialized views)
36    pub autovacuum_enabled: Option<bool>,
37    /// Vacuum index cleanup (for materialized views): 'auto' | 'on' | 'off'
38    pub vacuum_index_cleanup: Option<&'static str>,
39    /// Vacuum truncate (for materialized views)
40    pub vacuum_truncate: Option<bool>,
41    /// Autovacuum vacuum threshold (for materialized views)
42    pub autovacuum_vacuum_threshold: Option<i32>,
43    /// Autovacuum vacuum scale factor (for materialized views)
44    pub autovacuum_vacuum_scale_factor: Option<i32>,
45    /// Autovacuum vacuum cost delay (for materialized views)
46    pub autovacuum_vacuum_cost_delay: Option<i32>,
47    /// Autovacuum vacuum cost limit (for materialized views)
48    pub autovacuum_vacuum_cost_limit: Option<i32>,
49    /// Autovacuum freeze min age (for materialized views)
50    pub autovacuum_freeze_min_age: Option<i64>,
51    /// Autovacuum freeze max age (for materialized views)
52    pub autovacuum_freeze_max_age: Option<i64>,
53    /// Autovacuum freeze table age (for materialized views)
54    pub autovacuum_freeze_table_age: Option<i64>,
55    /// Autovacuum multixact freeze min age (for materialized views)
56    pub autovacuum_multixact_freeze_min_age: Option<i64>,
57    /// Autovacuum multixact freeze max age (for materialized views)
58    pub autovacuum_multixact_freeze_max_age: Option<i64>,
59    /// Autovacuum multixact freeze table age (for materialized views)
60    pub autovacuum_multixact_freeze_table_age: Option<i64>,
61    /// Log autovacuum min duration (for materialized views)
62    pub log_autovacuum_min_duration: Option<i32>,
63    /// User catalog table (for materialized views)
64    pub user_catalog_table: Option<bool>,
65}
66
67impl ViewWithOptionDef {
68    /// Create a new view WITH options definition
69    #[must_use]
70    pub const fn new() -> Self {
71        Self {
72            check_option: None,
73            security_barrier: false,
74            security_invoker: false,
75            fillfactor: None,
76            toast_tuple_target: None,
77            parallel_workers: None,
78            autovacuum_enabled: None,
79            vacuum_index_cleanup: None,
80            vacuum_truncate: None,
81            autovacuum_vacuum_threshold: None,
82            autovacuum_vacuum_scale_factor: None,
83            autovacuum_vacuum_cost_delay: None,
84            autovacuum_vacuum_cost_limit: None,
85            autovacuum_freeze_min_age: None,
86            autovacuum_freeze_max_age: None,
87            autovacuum_freeze_table_age: None,
88            autovacuum_multixact_freeze_min_age: None,
89            autovacuum_multixact_freeze_max_age: None,
90            autovacuum_multixact_freeze_table_age: None,
91            log_autovacuum_min_duration: None,
92            user_catalog_table: None,
93        }
94    }
95
96    /// Set CHECK OPTION
97    #[must_use]
98    pub const fn check_option(self, option: &'static str) -> Self {
99        Self {
100            check_option: Some(option),
101            ..self
102        }
103    }
104
105    /// Set security barrier
106    #[must_use]
107    pub const fn security_barrier(self) -> Self {
108        Self {
109            security_barrier: true,
110            ..self
111        }
112    }
113
114    /// Set security invoker
115    #[must_use]
116    pub const fn security_invoker(self) -> Self {
117        Self {
118            security_invoker: true,
119            ..self
120        }
121    }
122
123    /// Convert to runtime type
124    #[must_use]
125    pub const fn into_view_with_option(self) -> ViewWithOption {
126        ViewWithOption {
127            check_option: match self.check_option {
128                Some(s) => Some(Cow::Borrowed(s)),
129                None => None,
130            },
131            security_barrier: if self.security_barrier {
132                Some(true)
133            } else {
134                None
135            },
136            security_invoker: if self.security_invoker {
137                Some(true)
138            } else {
139                None
140            },
141            fillfactor: self.fillfactor,
142            toast_tuple_target: self.toast_tuple_target,
143            parallel_workers: self.parallel_workers,
144            autovacuum_enabled: self.autovacuum_enabled,
145            vacuum_index_cleanup: match self.vacuum_index_cleanup {
146                Some(s) => Some(Cow::Borrowed(s)),
147                None => None,
148            },
149            vacuum_truncate: self.vacuum_truncate,
150            autovacuum_vacuum_threshold: self.autovacuum_vacuum_threshold,
151            autovacuum_vacuum_scale_factor: self.autovacuum_vacuum_scale_factor,
152            autovacuum_vacuum_cost_delay: self.autovacuum_vacuum_cost_delay,
153            autovacuum_vacuum_cost_limit: self.autovacuum_vacuum_cost_limit,
154            autovacuum_freeze_min_age: self.autovacuum_freeze_min_age,
155            autovacuum_freeze_max_age: self.autovacuum_freeze_max_age,
156            autovacuum_freeze_table_age: self.autovacuum_freeze_table_age,
157            autovacuum_multixact_freeze_min_age: self.autovacuum_multixact_freeze_min_age,
158            autovacuum_multixact_freeze_max_age: self.autovacuum_multixact_freeze_max_age,
159            autovacuum_multixact_freeze_table_age: self.autovacuum_multixact_freeze_table_age,
160            log_autovacuum_min_duration: self.log_autovacuum_min_duration,
161            user_catalog_table: self.user_catalog_table,
162        }
163    }
164}
165
166impl Default for ViewWithOptionDef {
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172/// Runtime view WITH options entity
173#[derive(Clone, Debug, PartialEq, Eq)]
174#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
175#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
176pub struct ViewWithOption {
177    /// CHECK OPTION ('local' | 'cascaded')
178    #[cfg_attr(
179        feature = "serde",
180        serde(
181            skip_serializing_if = "Option::is_none",
182            deserialize_with = "cow_option_from_string"
183        )
184    )]
185    pub check_option: Option<Cow<'static, str>>,
186
187    /// Security barrier flag
188    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
189    pub security_barrier: Option<bool>,
190
191    /// Security invoker flag
192    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
193    pub security_invoker: Option<bool>,
194
195    /// Fillfactor (for materialized views)
196    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
197    pub fillfactor: Option<i32>,
198
199    /// Toast tuple target (for materialized views)
200    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
201    pub toast_tuple_target: Option<i32>,
202
203    /// Parallel workers (for materialized views)
204    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
205    pub parallel_workers: Option<i32>,
206
207    /// Autovacuum enabled (for materialized views)
208    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
209    pub autovacuum_enabled: Option<bool>,
210
211    /// Vacuum index cleanup (for materialized views): 'auto' | 'on' | 'off'
212    #[cfg_attr(
213        feature = "serde",
214        serde(
215            skip_serializing_if = "Option::is_none",
216            deserialize_with = "cow_option_from_string"
217        )
218    )]
219    pub vacuum_index_cleanup: Option<Cow<'static, str>>,
220
221    /// Vacuum truncate (for materialized views)
222    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
223    pub vacuum_truncate: Option<bool>,
224
225    /// Autovacuum vacuum threshold (for materialized views)
226    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
227    pub autovacuum_vacuum_threshold: Option<i32>,
228
229    /// Autovacuum vacuum scale factor (for materialized views)
230    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
231    pub autovacuum_vacuum_scale_factor: Option<i32>,
232
233    /// Autovacuum vacuum cost delay (for materialized views)
234    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
235    pub autovacuum_vacuum_cost_delay: Option<i32>,
236
237    /// Autovacuum vacuum cost limit (for materialized views)
238    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
239    pub autovacuum_vacuum_cost_limit: Option<i32>,
240
241    /// Autovacuum freeze min age (for materialized views)
242    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
243    pub autovacuum_freeze_min_age: Option<i64>,
244
245    /// Autovacuum freeze max age (for materialized views)
246    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
247    pub autovacuum_freeze_max_age: Option<i64>,
248
249    /// Autovacuum freeze table age (for materialized views)
250    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
251    pub autovacuum_freeze_table_age: Option<i64>,
252
253    /// Autovacuum multixact freeze min age (for materialized views)
254    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
255    pub autovacuum_multixact_freeze_min_age: Option<i64>,
256
257    /// Autovacuum multixact freeze max age (for materialized views)
258    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
259    pub autovacuum_multixact_freeze_max_age: Option<i64>,
260
261    /// Autovacuum multixact freeze table age (for materialized views)
262    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
263    pub autovacuum_multixact_freeze_table_age: Option<i64>,
264
265    /// Log autovacuum min duration (for materialized views)
266    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
267    pub log_autovacuum_min_duration: Option<i32>,
268
269    /// User catalog table (for materialized views)
270    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
271    pub user_catalog_table: Option<bool>,
272}
273
274impl Default for ViewWithOption {
275    fn default() -> Self {
276        ViewWithOptionDef::new().into_view_with_option()
277    }
278}
279
280impl From<ViewWithOptionDef> for ViewWithOption {
281    fn from(def: ViewWithOptionDef) -> Self {
282        def.into_view_with_option()
283    }
284}
285
286// =============================================================================
287// Const-friendly Definition Type
288// =============================================================================
289
290/// Const-friendly view definition
291#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
292pub struct ViewDef {
293    /// Schema name
294    pub schema: &'static str,
295    /// View name
296    pub name: &'static str,
297    /// View definition (AS SELECT ...)
298    pub definition: Option<&'static str>,
299    /// Is this a materialized view?
300    pub materialized: bool,
301    /// WITH options
302    pub with: Option<ViewWithOptionDef>,
303    /// Whether this is an existing view (not managed by drizzle)
304    pub is_existing: bool,
305    /// WITH NO DATA (for materialized views)
306    pub with_no_data: bool,
307    /// USING clause (for materialized views)
308    pub using: Option<&'static str>,
309    /// Tablespace (for materialized views)
310    pub tablespace: Option<&'static str>,
311}
312
313impl ViewDef {
314    /// Create a new view definition
315    #[must_use]
316    pub const fn new(schema: &'static str, name: &'static str) -> Self {
317        Self {
318            schema,
319            name,
320            definition: None,
321            materialized: false,
322            with: None,
323            is_existing: false,
324            with_no_data: false,
325            using: None,
326            tablespace: None,
327        }
328    }
329
330    /// Set the view definition
331    #[must_use]
332    pub const fn definition(self, sql: &'static str) -> Self {
333        Self {
334            definition: Some(sql),
335            ..self
336        }
337    }
338
339    /// Mark as materialized view
340    #[must_use]
341    pub const fn materialized(self) -> Self {
342        Self {
343            materialized: true,
344            ..self
345        }
346    }
347
348    /// Set WITH options
349    #[must_use]
350    pub const fn with_options(self, options: ViewWithOptionDef) -> Self {
351        Self {
352            with: Some(options),
353            ..self
354        }
355    }
356
357    /// Mark as existing (not managed by drizzle)
358    #[must_use]
359    pub const fn existing(self) -> Self {
360        Self {
361            is_existing: true,
362            ..self
363        }
364    }
365
366    /// Set WITH NO DATA
367    #[must_use]
368    pub const fn with_no_data(self) -> Self {
369        Self {
370            with_no_data: true,
371            ..self
372        }
373    }
374
375    /// Set USING clause
376    #[must_use]
377    pub const fn using(self, clause: &'static str) -> Self {
378        Self {
379            using: Some(clause),
380            ..self
381        }
382    }
383
384    /// Set tablespace
385    #[must_use]
386    pub const fn tablespace(self, space: &'static str) -> Self {
387        Self {
388            tablespace: Some(space),
389            ..self
390        }
391    }
392
393    /// Convert to runtime [`View`] type
394    ///
395    /// Note: This method cannot be const because it needs to convert nested Option types
396    /// (with options) which require runtime method calls.
397    #[must_use]
398    pub fn into_view(self) -> View {
399        View {
400            schema: Cow::Borrowed(self.schema),
401            name: Cow::Borrowed(self.name),
402            definition: match self.definition {
403                Some(s) => Some(Cow::Borrowed(s)),
404                None => None,
405            },
406            materialized: self.materialized,
407            with: self.with.map(|w| w.into_view_with_option()),
408            is_existing: self.is_existing,
409            with_no_data: if self.with_no_data { Some(true) } else { None },
410            using: match self.using {
411                Some(s) => Some(Cow::Borrowed(s)),
412                None => None,
413            },
414            tablespace: match self.tablespace {
415                Some(s) => Some(Cow::Borrowed(s)),
416                None => None,
417            },
418        }
419    }
420}
421
422impl Default for ViewDef {
423    fn default() -> Self {
424        Self::new("public", "")
425    }
426}
427
428// =============================================================================
429// Runtime Type for Serde
430// =============================================================================
431
432/// Runtime view entity
433#[derive(Clone, Debug, PartialEq, Eq)]
434#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
435#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
436pub struct View {
437    /// Schema name
438    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
439    pub schema: Cow<'static, str>,
440
441    /// View name
442    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
443    pub name: Cow<'static, str>,
444
445    /// View definition (AS SELECT ...)
446    #[cfg_attr(
447        feature = "serde",
448        serde(
449            default,
450            skip_serializing_if = "Option::is_none",
451            deserialize_with = "cow_option_from_string"
452        )
453    )]
454    pub definition: Option<Cow<'static, str>>,
455
456    /// Is this a materialized view?
457    #[cfg_attr(feature = "serde", serde(default))]
458    pub materialized: bool,
459
460    /// WITH options
461    #[cfg_attr(
462        feature = "serde",
463        serde(skip_serializing_if = "Option::is_none", rename = "with")
464    )]
465    pub with: Option<ViewWithOption>,
466
467    /// Whether this is an existing view (not managed by drizzle)
468    #[cfg_attr(feature = "serde", serde(default))]
469    pub is_existing: bool,
470
471    /// WITH NO DATA (for materialized views)
472    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
473    pub with_no_data: Option<bool>,
474
475    /// USING clause (for materialized views)
476    #[cfg_attr(
477        feature = "serde",
478        serde(
479            skip_serializing_if = "Option::is_none",
480            deserialize_with = "cow_option_from_string"
481        )
482    )]
483    pub using: Option<Cow<'static, str>>,
484
485    /// Tablespace (for materialized views)
486    #[cfg_attr(
487        feature = "serde",
488        serde(
489            skip_serializing_if = "Option::is_none",
490            deserialize_with = "cow_option_from_string"
491        )
492    )]
493    pub tablespace: Option<Cow<'static, str>>,
494}
495
496impl View {
497    /// Create a new view
498    #[must_use]
499    pub fn new(schema: impl Into<Cow<'static, str>>, name: impl Into<Cow<'static, str>>) -> Self {
500        Self {
501            schema: schema.into(),
502            name: name.into(),
503            definition: None,
504            materialized: false,
505            with: None,
506            is_existing: false,
507            with_no_data: None,
508            using: None,
509            tablespace: None,
510        }
511    }
512
513    /// Get the schema name
514    #[inline]
515    #[must_use]
516    pub fn schema(&self) -> &str {
517        &self.schema
518    }
519
520    /// Get the view name
521    #[inline]
522    #[must_use]
523    pub fn name(&self) -> &str {
524        &self.name
525    }
526}
527
528impl Default for View {
529    fn default() -> Self {
530        Self::new("public", "")
531    }
532}
533
534impl From<ViewDef> for View {
535    fn from(def: ViewDef) -> Self {
536        def.into_view()
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543
544    #[test]
545    fn test_const_view_def() {
546        const VIEW: ViewDef = ViewDef::new("public", "active_users")
547            .definition("SELECT * FROM users WHERE active = 1");
548
549        assert_eq!(VIEW.name, "active_users");
550        assert_eq!(VIEW.schema, "public");
551    }
552
553    #[test]
554    fn test_materialized_view_def() {
555        const MAT_VIEW: ViewDef = ViewDef::new("public", "user_stats")
556            .materialized()
557            .with_no_data();
558
559        assert!(MAT_VIEW.materialized);
560    }
561
562    #[test]
563    fn test_view_def_to_view() {
564        const DEF: ViewDef = ViewDef::new("public", "view").definition("SELECT 1");
565        let view = DEF.into_view();
566        assert_eq!(view.name(), "view");
567        assert_eq!(view.schema(), "public");
568    }
569}