Skip to main content

coven_database/
declaration.rs

1//! The application's declarations, independent of any SQLite connection.
2
3/// How independently inserted rows choose their identity (§9.2).
4#[derive(Clone, Debug, Eq, PartialEq)]
5pub enum RowIdentity {
6    /// A canonical lowercase UUID, version 4 or 7, in a primary-key component.
7    IndependentUuid,
8    /// Equal derived keys identify one row on every device.
9    SharedKey,
10}
11
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub(crate) enum AudienceSource {
14    Store,
15    Column(String),
16    ForeignKey(String),
17    Both { column: String, foreign_key: String },
18}
19
20/// A synced table's key, audience, file references, shared triggers and whether only
21/// each row's original creator writes it directly (E2).
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct SyncedTable {
24    pub(crate) name: String,
25    pub(crate) identity: RowIdentity,
26    pub(crate) key: Vec<String>,
27    pub(crate) audience: AudienceSource,
28    pub(crate) files: Vec<FileColumn>,
29    pub(crate) shared_triggers: Vec<String>,
30    pub(crate) owner_only: bool,
31}
32
33impl SyncedTable {
34    /// The table name exactly as supplied to the declaration.
35    pub fn name(&self) -> &str {
36        &self.name
37    }
38
39    /// The declared kind of row identity.
40    pub fn identity(&self) -> &RowIdentity {
41        &self.identity
42    }
43
44    /// The declared primary-key columns, in order; defaults to `id`.
45    pub fn get_key_columns(&self) -> &[String] {
46        &self.key
47    }
48
49    /// Whether direct writes are restricted to each row's original creator.
50    pub fn is_owner_only(&self) -> bool {
51        self.owner_only
52    }
53
54    /// The root audience column, including when inheritance was also declared.
55    /// Opening refuses a declaration with both audience sources.
56    pub fn get_audience_column(&self) -> Option<&str> {
57        match &self.audience {
58            AudienceSource::Column(column) | AudienceSource::Both { column, .. } => Some(column),
59            AudienceSource::Store | AudienceSource::ForeignKey(_) => None,
60        }
61    }
62
63    /// The audience foreign key, including when a root column was also declared.
64    /// Opening refuses a declaration with both audience sources.
65    pub fn get_audience_from(&self) -> Option<&str> {
66        match &self.audience {
67            AudienceSource::ForeignKey(foreign_key) | AudienceSource::Both { foreign_key, .. } => {
68                Some(foreign_key)
69            }
70            AudienceSource::Store | AudienceSource::Column(_) => None,
71        }
72    }
73
74    /// The declared file reference columns, in declaration order.
75    pub fn file_columns(&self) -> &[FileColumn] {
76        &self.files
77    }
78
79    /// The shared trigger names, in declaration order, including repetitions.
80    pub fn shared_triggers(&self) -> &[String] {
81        &self.shared_triggers
82    }
83
84    /// Declare a synced table with the one-column primary key `id`.
85    pub fn new(name: impl Into<String>, identity: RowIdentity) -> Self {
86        Self {
87            name: name.into(),
88            identity,
89            key: vec!["id".into()],
90            audience: AudienceSource::Store,
91            files: Vec::new(),
92            shared_triggers: Vec::new(),
93            owner_only: false,
94        }
95    }
96
97    /// Only each row's original creating member may directly update or
98    /// delete it (§9.6). Shared-trigger targets count as direct writes;
99    /// foreign-key actions from authorized changes may still reach the row.
100    /// No app owner column is needed. Changing this declaration for an
101    /// existing table is a breaking schema change (§18.1).
102    pub fn owner_only(mut self) -> Self {
103        self.owner_only = true;
104        self
105    }
106
107    /// The columns in PRIMARY KEY order.
108    pub fn key_columns<I, S>(mut self, columns: I) -> Self
109    where
110        I: IntoIterator<Item = S>,
111        S: Into<String>,
112    {
113        self.key = columns.into_iter().map(Into::into).collect();
114        self
115    }
116
117    /// Set the root's non-null text audience column. Declaring inheritance too is an error.
118    pub fn audience_column(mut self, column: impl Into<String>) -> Self {
119        let column = column.into();
120        self.audience = match self.audience {
121            AudienceSource::ForeignKey(foreign_key) | AudienceSource::Both { foreign_key, .. } => {
122                AudienceSource::Both { column, foreign_key }
123            }
124            AudienceSource::Store | AudienceSource::Column(_) => AudienceSource::Column(column),
125        };
126        self
127    }
128
129    /// Inherit the audience through this foreign key. Declaring a root column too is an error.
130    pub fn audience_from(mut self, foreign_key: impl Into<String>) -> Self {
131        let foreign_key = foreign_key.into();
132        self.audience = match self.audience {
133            AudienceSource::Column(column) | AudienceSource::Both { column, .. } => {
134                AudienceSource::Both { column, foreign_key }
135            }
136            AudienceSource::Store | AudienceSource::ForeignKey(_) => {
137                AudienceSource::ForeignKey(foreign_key)
138            }
139        };
140        self
141    }
142
143    /// Declare a column holding a file reference. Opening and migrating check
144    /// the column against this synced table's actual schema.
145    pub fn file_column(mut self, column: FileColumn) -> Self {
146        self.files.push(column);
147        self
148    }
149
150    /// Declare a trigger as shared; all other triggers are local.
151    pub fn shared_trigger(mut self, name: impl Into<String>) -> Self {
152        self.shared_triggers.push(name.into());
153        self
154    }
155}
156
157/// A namespace's kind: whose bytes its files are (§17.1).
158#[derive(Clone, Debug, Eq, PartialEq)]
159pub enum Provenance {
160    /// The user's original, which coven never modifies or deletes.
161    UserProvided,
162    /// Bytes that coven owns.
163    AppProvided,
164}
165
166/// When a write setting a file reference goes out (§17.4, E2).
167#[derive(Clone, Debug, Eq, PartialEq)]
168pub enum Binding {
169    /// Once its file is stored, together with it: the write and every later
170    /// write of this device wait for the upload. Its files are app files of
171    /// at most `limit` bytes.
172    Bound {
173        /// The largest file the reference may name, in bytes.
174        limit: u64,
175    },
176    /// At once; the file is on its uploader only until it is stored.
177    Trailing,
178}
179
180/// The size limit of a bound reference that declares none: 1.5 MiB (§17.4).
181pub const BOUND_FILE_LIMIT: u64 = 3 << 19;
182
183/// A column holding a file's id, or NULL (§17.2, E2). It references the file
184/// as a foreign key references its parent.
185#[derive(Clone, Debug, Eq, PartialEq)]
186pub struct FileColumn {
187    pub(crate) column: String,
188    pub(crate) write_once: bool,
189    pub(crate) binding: Binding,
190}
191
192impl FileColumn {
193    /// A trailing file reference held in `column`.
194    pub fn new(column: impl Into<String>) -> Self {
195        Self { column: column.into(), write_once: false, binding: Binding::Trailing }
196    }
197
198    /// Bound, naming files of at most `BOUND_FILE_LIMIT` bytes (§17.4).
199    pub fn bound(self) -> Self {
200        self.bound_with_limit(BOUND_FILE_LIMIT)
201    }
202
203    /// Bound, naming files of at most `limit` bytes (§17.4).
204    pub fn bound_with_limit(mut self, limit: u64) -> Self {
205        self.binding = Binding::Bound { limit };
206        self
207    }
208
209    /// Refuse a write that changes the reference once it names a file.
210    pub fn write_once(mut self) -> Self {
211        self.write_once = true;
212        self
213    }
214
215    /// The column exactly as declared.
216    pub fn column(&self) -> &str {
217        &self.column
218    }
219
220    /// When a write setting the reference goes out.
221    pub fn binding(&self) -> &Binding {
222        &self.binding
223    }
224
225    /// Whether a reference, once it names a file, can't change.
226    pub fn is_write_once(&self) -> bool {
227        self.write_once
228    }
229}
230
231/// A kind of file with its cache budget, declared when the store opens
232/// (§17.1, §17.8, E1).
233#[derive(Clone, Debug, Eq, PartialEq)]
234pub struct Namespace {
235    pub(crate) name: String,
236    pub(crate) kind: Provenance,
237    pub(crate) cache_budget: u64,
238}
239
240impl Namespace {
241    /// A namespace of this kind. The budget applies when the namespace is
242    /// first declared; `set_cache_budget` changes it after.
243    pub fn new(name: impl Into<String>, kind: Provenance, cache_budget: u64) -> Self {
244        Self { name: name.into(), kind, cache_budget }
245    }
246
247    /// The name exactly as declared.
248    pub fn name(&self) -> &str {
249        &self.name
250    }
251
252    /// Whose bytes its files are.
253    pub fn kind(&self) -> &Provenance {
254        &self.kind
255    }
256
257    /// The initial cache budget in bytes, as declared, not its current
258    /// setting. Pinned files' bytes don't count toward it.
259    pub fn cache_budget(&self) -> u64 {
260        self.cache_budget
261    }
262}
263
264#[cfg(test)]
265#[path = "declaration_tests.rs"]
266mod tests;