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