coven_database/write.rs
1//! Transaction-scoped app SQL. The connection remains inside its owner.
2
3use rusqlite::{Params, Row};
4
5/// App SQL inside one local write, and coven's steps beside it (§4).
6/// Returning an error rolls back every statement and step. This borrowed
7/// context cannot change the transaction or access coven's tables.
8pub struct SqlContext<'connection, 'write> {
9 files: &'write crate::file_write::FileWrite<'connection>,
10}
11
12impl<'connection, 'write> SqlContext<'connection, 'write> {
13 pub(crate) fn new(files: &'write crate::file_write::FileWrite<'connection>) -> Self {
14 Self { files }
15 }
16
17 /// Record a move of these files of this device, on it only, to the
18 /// cloud (§17.3). It survives restarts; once every file is stored, one
19 /// write switches them all to the cloud, changing no app row.
20 pub fn move_to_cloud(
21 &self,
22 files: Vec<crate::MoveFile>,
23 ) -> Result<crate::MoveId, crate::DbError> {
24 self.files.step(|| self.files.move_to_cloud(files))
25 }
26
27 /// Cancel a recorded move: every file stays on this device only, and
28 /// the move's record ends (§17.3).
29 pub fn cancel_move(&self, id: crate::MoveId) -> Result<(), crate::DbError> {
30 self.files.step(|| self.files.cancel_move(id))
31 }
32
33 /// Remove these files' copies from this device in this write: every
34 /// cache entry of their stored bytes, pinned or not, the partial
35 /// download a pin left, and the pin requests coven keeps for them
36 /// (§17.8). Nothing else is evicted. A file this device has no record
37 /// of, or one on one device only, has nothing to remove. The bytes go
38 /// once the write has committed.
39 pub fn unpin(
40 &self,
41 files: &[coven_foundation::id_source::FileId],
42 ) -> Result<(), crate::DbError> {
43 self.files.step(|| self.files.unpin(files))
44 }
45
46 /// Remove a stored file's cached copies, pinned or not, in this write,
47 /// with no storage operation (§17.8). A file this device has no record
48 /// of, or one on one device only, has none. The bytes go once the write
49 /// has committed.
50 pub fn evict_file(
51 &self,
52 file: coven_foundation::id_source::FileId,
53 ) -> Result<(), crate::DbError> {
54 self.files.step(|| self.files.evict(file))
55 }
56
57 /// Clear a declared namespace's cache in this write (§17.8): every
58 /// unpinned file and chunk goes. Pinned copies, the partial downloads
59 /// pins left and other namespaces stay. An undeclared namespace fails
60 /// with `UndeclaredNamespace`. The bytes go once the write has
61 /// committed.
62 pub fn clear_cache(&self, namespace: &str) -> Result<(), crate::DbError> {
63 self.files.step(|| self.files.clear_cache(namespace))
64 }
65
66 /// Set a declared namespace's cache budget and evict its unpinned
67 /// entries until they are within it, in this write (§17.8). Pinned
68 /// bytes don't count toward the budget. An undeclared namespace
69 /// fails with `UndeclaredNamespace`. The evicted bytes go once the write
70 /// has committed.
71 pub fn set_cache_budget(&self, namespace: &str, bytes: u64) -> Result<(), crate::DbError> {
72 self.files.step(|| self.files.set_cache_budget(namespace, bytes))
73 }
74
75 /// Dismiss lost values the app has dealt with, in this write (§8.4): its
76 /// record carries the acknowledgements and the removed rows' deletes,
77 /// with their cascades, after the app's own changes, so every device
78 /// drops them. A removed row is deleted for good. A value no longer lost
79 /// as it was, such as a row restored since, is left alone. The write
80 /// fails with `DismissedRowChanged` if it also changes a row its
81 /// dismissals delete.
82 pub fn dismiss_lost_values(&self, values: &[crate::LostValue]) -> Result<(), crate::DbError> {
83 self.files.step(|| {
84 self.files.dismiss(values);
85 Ok(())
86 })
87 }
88
89 /// Pause trailing uploads, or resume them, in this write (§17.9). A
90 /// paused upload keeps its place and its provider session.
91 pub fn set_uploads_paused(&self, paused: bool) -> Result<(), crate::DbError> {
92 self.files.step(|| self.files.set_uploads_paused(paused))
93 }
94
95 /// Move files only on unmetered networks, or on any, in this write
96 /// (§17.9).
97 pub fn set_unmetered_only(&self, unmetered_only: bool) -> Result<(), crate::DbError> {
98 self.files.step(|| self.files.set_unmetered_only(unmetered_only))
99 }
100
101 /// Run `effect` once this write has committed, as the in-memory side of
102 /// one of coven's steps: nothing runs for a write that rolls back. The
103 /// write returns only once every effect has run.
104 pub fn after_commit(&self, effect: impl FnOnce() + 'static) {
105 self.files.after_commit(Box::new(effect));
106 }
107
108 /// Put files `stage_files` staged into this write, and return what its
109 /// batch returned, such as their ids, for this write's rows to name
110 /// (§17.2). Files staged by another open store, or by this one before it
111 /// closed, fail with `PreparedElsewhere`. A file no row names fails the
112 /// write with `FileUnreferenced`.
113 pub fn put_staged<T>(&self, staged: crate::StagedFiles<T>) -> Result<T, crate::DbError> {
114 self.files.step(|| self.files.put_staged(staged))
115 }
116
117 /// Stage a user's original, read whole once already, as a new file in a
118 /// user namespace, and return its id for a row to reference (§17.1,
119 /// §17.3). Coven records the original's path, size, modification time and
120 /// the hashes its read found; it never reads it here. An undeclared
121 /// namespace fails with `UndeclaredNamespace`, an app one with
122 /// `NamespaceKind`.
123 pub fn put_user_file(
124 &self,
125 namespace: &str,
126 original: crate::PreparedUserFile,
127 place: crate::Place,
128 ) -> Result<coven_foundation::id_source::FileId, crate::DbError> {
129 self.files.step(|| self.files.put_user_file(namespace, original, place))
130 }
131
132 /// Stage a new file over an existing file's bytes, for a row in any
133 /// audience, and return its id (§17.2). The write fails with
134 /// `FileAbsent` if this device has no record of `from`, and with
135 /// `NotThisDevice` if `from` is on another device only.
136 pub fn existing_file(
137 &self,
138 from: coven_foundation::id_source::FileId,
139 ) -> coven_foundation::id_source::FileId {
140 self.files.existing_file(from)
141 }
142
143 /// Pin a prepared pin's files in this write (§17.8): each whole copy
144 /// becomes the file's pinned cache entry, and each copy already kept
145 /// stays. Files prepared in another open store, or in this one before
146 /// it closed, fail with `PreparedElsewhere`; a file whose record left
147 /// its stored bytes since with `NoLongerStored`; and a kept copy evicted
148 /// or unpinned since with `PinChanged`.
149 pub fn pin(&self, prepared: crate::PreparedPin) -> Result<(), crate::DbError> {
150 self.files.step(|| self.files.pin(prepared))
151 }
152
153 /// End the pin request `id`, in this write, returning whether it still
154 /// waited (§17.8).
155 pub(crate) fn end_pin_request(&self, id: i64) -> Result<bool, crate::DbError> {
156 self.files.step(|| self.files.end_pin_request(id))
157 }
158
159 /// Switch a prepared call's files to device-only on this device, each to
160 /// its copy, which becomes the original this device records, in this
161 /// write (§17.3). Each must still be one this device may make
162 /// device-only, and still stored where its copy came from
163 /// (`NotInCloud`); copies prepared in another open store, or in this
164 /// one before it closed, fail with `PreparedElsewhere`. A write that
165 /// also removes such a file's last reference fails with
166 /// `DeviceOnlyUnreferenced`.
167 pub fn make_device_only(
168 &self,
169 prepared: crate::PreparedDeviceOnly,
170 ) -> Result<(), crate::DbError> {
171 self.files.step(|| self.files.make_prepared_device_only(prepared))
172 }
173
174 /// Acknowledge a finished operation the app's writes journaled, in this
175 /// write: it leaves `coven_operations` (§11.4). One still running fails
176 /// with `OperationRunning`; one not listed there with
177 /// `OperationUnknown`.
178 pub fn acknowledge_operation(
179 &self,
180 operation: crate::OperationId,
181 ) -> Result<(), crate::DbError> {
182 self.journal_step(|journal| journal.acknowledge(operation))
183 }
184
185 /// End the new person's name the store a join published keeps, in this
186 /// write, which records the app's profile of that person, or its request
187 /// for one (§13.2). With no name kept, it changes nothing.
188 pub fn acknowledge_joined_invitee(&self) -> Result<(), crate::DbError> {
189 self.files.step(|| self.files.acknowledge_joined_invitee())
190 }
191
192 /// Run one of sync's steps on the journal in this write (§4, §11). A
193 /// failed step fails the write, even if the closure goes on and returns
194 /// success, since it may have made only part of its change.
195 pub fn journal_step<T, E>(
196 &self,
197 step: impl FnOnce(&crate::Journal<'_>) -> Result<T, E>,
198 ) -> Result<T, E> {
199 self.files.step(|| step(&crate::Journal::new(self.files)))
200 }
201
202 /// Execute one app statement with parameters.
203 pub fn execute<P: Params>(&self, sql: &str, params: P) -> rusqlite::Result<usize> {
204 self.files.execute(sql, params)
205 }
206
207 /// Execute app statements within this write's transaction.
208 pub fn execute_batch(&self, sql: &str) -> rusqlite::Result<()> {
209 self.files.execute_batch(sql)
210 }
211
212 /// Map one app row, including changes already made by this write.
213 pub fn query_row<T, P, F>(&self, sql: &str, params: P, map: F) -> rusqlite::Result<T>
214 where
215 P: Params,
216 F: FnOnce(&Row<'_>) -> rusqlite::Result<T>,
217 {
218 self.files.query_row(sql, params, map)
219 }
220
221 /// Map all app rows produced by a query within this write.
222 pub fn query<T, P, F>(&self, sql: &str, params: P, map: F) -> rusqlite::Result<Vec<T>>
223 where
224 P: Params,
225 F: FnMut(&Row<'_>) -> rusqlite::Result<T>,
226 {
227 self.files.query(sql, params, map)
228 }
229}
230
231/// App SQL inside one bulk write (§4): it changes device-local tables
232/// alone, and may read any table app SQL reads. Returning an error rolls back
233/// every statement. This borrowed context cannot change the transaction,
234/// access coven's tables or write a synced one.
235pub struct BulkSqlContext<'connection> {
236 database: &'connection crate::sqlite::DatabaseConnection,
237}
238
239impl<'connection> BulkSqlContext<'connection> {
240 pub(crate) fn new(database: &'connection crate::sqlite::DatabaseConnection) -> Self {
241 Self { database }
242 }
243
244 /// Execute one app statement with parameters.
245 pub fn execute<P: Params>(&self, sql: &str, params: P) -> rusqlite::Result<usize> {
246 self.database.app_execute(sql, params)
247 }
248
249 /// Execute app statements within this write's transaction.
250 pub fn execute_batch(&self, sql: &str) -> rusqlite::Result<()> {
251 self.database.app_batch(sql)
252 }
253
254 /// Map one app row, including changes already made by this write.
255 pub fn query_row<T, P, F>(&self, sql: &str, params: P, map: F) -> rusqlite::Result<T>
256 where
257 P: Params,
258 F: FnOnce(&Row<'_>) -> rusqlite::Result<T>,
259 {
260 self.database.app_query_row(sql, params, map)
261 }
262
263 /// Map all app rows produced by a query within this write.
264 pub fn query<T, P, F>(&self, sql: &str, params: P, map: F) -> rusqlite::Result<Vec<T>>
265 where
266 P: Params,
267 F: FnMut(&Row<'_>) -> rusqlite::Result<T>,
268 {
269 self.database.app_query(sql, params, map)
270 }
271}
272
273#[cfg(test)]
274#[path = "write_tests.rs"]
275pub(crate) mod tests;