Skip to main content

cratestack_sql/descriptor/
view.rs

1//! `ViewDescriptor` — the runtime sibling of [`super::ModelDescriptor`]
2//! for `view` blocks (ADR-0003).
3//!
4//! Views are read-only, so this descriptor implements only
5//! [`super::ReadSource`] — never [`super::WriteSource`]. The type
6//! system enforces read-only-ness: it is impossible to pass a
7//! `ViewDescriptor` to a write-path builder because the bound doesn't
8//! hold.
9//!
10//! The shape is intentionally narrower than `ModelDescriptor`. Views
11//! carry no:
12//!
13//! - create / update / delete policy slots
14//! - create defaults, emitted events, audit / retention / version /
15//!   PII / sensitive metadata
16//! - upsert-overwrite column list
17//! - soft-delete column (the view's SQL body is responsible for
18//!   filtering soft-deleted source rows — see ADR §"Delegate split")
19//! - relation includes (relation-follow off a view is deferred to a
20//!   future ADR)
21//!
22//! Extra fields specific to views:
23//!
24//! - `is_materialized` — `true` for `@@materialized` views. Picked up
25//!   by the macro to emit a `refresh()` method on the generated
26//!   delegate (server-only).
27//! - `source_tables` — the names of source tables / views the body
28//!   reads from. Carried so the migration diff engine can order
29//!   `CREATE VIEW` after its source `CREATE TABLE` and `DROP VIEW`
30//!   before its source `DROP TABLE`.
31
32use std::marker::PhantomData;
33
34use cratestack_policy::ReadPolicy;
35
36use super::{ModelColumn, ReadSource};
37
38#[derive(Debug, Clone, Copy)]
39pub struct ViewDescriptor<V, PK> {
40    pub schema_name: &'static str,
41    pub view_name: &'static str,
42    pub columns: &'static [ModelColumn],
43    /// SQL column name of the view's primary key. Empty string when
44    /// the view was declared `@@no_unique` — in that case the macro
45    /// also omits `find_unique` on the generated delegate.
46    pub primary_key: &'static str,
47    pub allowed_fields: &'static [&'static str],
48    pub allowed_sorts: &'static [&'static str],
49    pub read_allow_policies: &'static [ReadPolicy],
50    pub read_deny_policies: &'static [ReadPolicy],
51    pub detail_allow_policies: &'static [ReadPolicy],
52    pub detail_deny_policies: &'static [ReadPolicy],
53    /// `true` when the view was declared `@@materialized`. Embedded
54    /// builds reject this at macro expansion time (SQLite has no
55    /// materialized views); server builds emit a `refresh()` method.
56    pub is_materialized: bool,
57    /// Names of the models / views the SQL body reads from. Drives
58    /// migration ordering — `CREATE VIEW` lands after its sources,
59    /// `DROP VIEW` lands before them. Populated from the `from M, N`
60    /// declaration on the schema, not parsed out of the SQL body.
61    pub source_tables: &'static [&'static str],
62    _marker: PhantomData<fn() -> (V, PK)>,
63}
64
65impl<V, PK> ViewDescriptor<V, PK> {
66    #[allow(clippy::too_many_arguments)]
67    pub const fn new(
68        schema_name: &'static str,
69        view_name: &'static str,
70        columns: &'static [ModelColumn],
71        primary_key: &'static str,
72        allowed_fields: &'static [&'static str],
73        allowed_sorts: &'static [&'static str],
74        read_allow_policies: &'static [ReadPolicy],
75        read_deny_policies: &'static [ReadPolicy],
76        detail_allow_policies: &'static [ReadPolicy],
77        detail_deny_policies: &'static [ReadPolicy],
78        is_materialized: bool,
79        source_tables: &'static [&'static str],
80    ) -> Self {
81        Self {
82            schema_name,
83            view_name,
84            columns,
85            primary_key,
86            allowed_fields,
87            allowed_sorts,
88            read_allow_policies,
89            read_deny_policies,
90            detail_allow_policies,
91            detail_deny_policies,
92            is_materialized,
93            source_tables,
94            _marker: PhantomData,
95        }
96    }
97}
98
99impl<V, PK> ReadSource<V, PK> for ViewDescriptor<V, PK> {
100    fn schema_name(&self) -> &'static str {
101        self.schema_name
102    }
103    fn table_name(&self) -> &'static str {
104        // For views the "table" the read builder selects from is the
105        // view's SQL identifier — sqlx and rusqlite quote it the same
106        // way they would a real table.
107        self.view_name
108    }
109    fn columns(&self) -> &'static [ModelColumn] {
110        self.columns
111    }
112    fn primary_key(&self) -> &'static str {
113        self.primary_key
114    }
115    fn allowed_fields(&self) -> &'static [&'static str] {
116        self.allowed_fields
117    }
118    fn allowed_includes(&self) -> &'static [&'static str] {
119        // Relation-follow off views is deferred (ADR-0003 §"Deferred").
120        &[]
121    }
122    fn allowed_sorts(&self) -> &'static [&'static str] {
123        self.allowed_sorts
124    }
125    fn read_allow_policies(&self) -> &'static [ReadPolicy] {
126        self.read_allow_policies
127    }
128    fn read_deny_policies(&self) -> &'static [ReadPolicy] {
129        self.read_deny_policies
130    }
131    fn detail_allow_policies(&self) -> &'static [ReadPolicy] {
132        self.detail_allow_policies
133    }
134    fn detail_deny_policies(&self) -> &'static [ReadPolicy] {
135        self.detail_deny_policies
136    }
137    fn soft_delete_column(&self) -> Option<&'static str> {
138        // Views never carry soft-delete state — the source models do.
139        // The view's SQL body is responsible for filtering soft-
140        // deleted rows out of its projection.
141        None
142    }
143    // `select_projection` / `select_projection_subset` use the trait's
144    // default impls (they iterate `self.columns()`), which match
145    // `ModelDescriptor`'s behavior exactly.
146}
147