1use super::Database;
4use crate::file_protection::value as retain_value;
5use crate::{
6 DbError, DownloadedPartStream, DownloadedWriteStream, FileEvidence, Slot, UnprovenFiles,
7};
8use coven_format::{value::Value, write::RowChange};
9use coven_foundation::id_source::{DeviceId, ObjectId};
10use coven_merge::{ColumnValue, Operation};
11use rusqlite::params;
12use std::collections::{BTreeMap, BTreeSet};
13use std::ops::RangeInclusive;
14
15#[cfg(test)]
17pub(crate) struct FileRetention {
18 pub references: BTreeSet<(DeviceId, ObjectId)>,
21}
22
23impl Database {
24 pub async fn snapshot_absent(
26 &self,
27 id: coven_format::admin::SnapshotId,
28 ) -> Result<bool, DbError> {
29 self.call(move |inner| {
30 let reader = inner.access.readers.acquire_reader();
31 reader.with_reader(|db| db.query_row(
32 "SELECT EXISTS(SELECT 1 FROM _coven_absent_snapshots WHERE audience=?1 AND device=?2 AND number=?3)",
33 params![&id.audience, id.device, id.number.to_be_bytes()], |row| row.get(0)))
34 }).await
35 }
36
37 pub async fn record_absent_snapshot(
40 &self,
41 id: coven_format::admin::SnapshotId,
42 files: BTreeSet<(DeviceId, ObjectId)>,
43 ) -> Result<(), DbError> {
44 self.call(move |inner| {
45 let number = std::num::NonZeroU64::new(id.number).ok_or(DbError::DamagedDatabase)?;
46 let path = coven_format::path::ObjectPath::snapshot(
47 inner.access.directory.id(),
48 id.audience.clone(),
49 id.device,
50 number,
51 );
52 inner.access.with_writer(|writer| writer.transaction(|db| {
53 for (device, object) in files { insert_candidate(db, device, object)?; }
54 db.internal_execute("INSERT INTO _coven_absent_snapshots(audience,device,number) VALUES(?1,?2,?3) ON CONFLICT DO NOTHING",
55 params![&id.audience, id.device, id.number.to_be_bytes()])?;
56 Ok(())
57 }))?;
58 inner.inputs.forget_path(path.as_str())
59 }).await
60 }
61 pub async fn forget_announcements(
67 &self,
68 slots: BTreeSet<Slot>,
69 snapshots: BTreeSet<coven_format::admin::SnapshotId>,
70 listings: Vec<crate::NewOperation>,
71 ) -> Result<(), DbError> {
72 self.call(move |inner| {
73 inner.access.with_writer(|writer| {
74 writer.transaction(|db| {
75 crate::sequence_apply::forget_announcements(db, &slots)?;
76 for id in &snapshots {
77 db.internal_execute(
78 "DELETE FROM _coven_absent_snapshots WHERE audience=?1 AND device=?2 AND number=?3",
79 params![&id.audience, id.device, id.number.to_be_bytes()],
80 )?;
81 }
82 for listing in &listings {
83 crate::operation::insert(db, listing)?;
84 }
85 Ok(())
86 })
87 })
88 })
89 .await
90 }
91
92 #[cfg(test)]
94 pub(crate) async fn retained_files(&self) -> Result<FileRetention, DbError> {
95 self.call(move |inner| {
96 inner.access.with_committed(|db| {
97 let references = db.query("SELECT device,object FROM _coven_row_file_refs UNION SELECT device,object FROM _coven_published_file_refs UNION SELECT device,object FROM _coven_queued_file_refs UNION SELECT device,object FROM _coven_file_uploads", [], |r| Ok((r.get::<_, DeviceId>(0)?,r.get::<_, ObjectId>(1)?)))?.into_iter().collect();
98 Ok(FileRetention { references })
99 })
100 })
101 .await
102 }
103
104 pub async fn write_file_references<R: std::io::Read + Send + 'static>(
109 &self,
110 write: DownloadedWriteStream<R>,
111 skipped: BTreeSet<(coven_merge::Audience, coven_foundation::id_source::KeyId)>,
112 ) -> Result<FileEvidence, DbError> {
113 self.call(move |inner| {
114 let mut references = BTreeSet::new();
115 write.header.encode()?;
116 let version = write.header.header.schema_version;
117 if !inner.access.write_schema.versions.contains(&version) {
118 return Ok(FileEvidence::Unproven(UnprovenFiles::Schema(version)));
119 }
120 if write.header.parts.len() != write.parts.len() {
121 return Err(crate::SnapshotError::Inconsistent(
122 "write stream count differs from its header",
123 )
124 .into());
125 }
126 let opened = write
127 .parts
128 .iter()
129 .filter(|part| matches!(part, DownloadedPartStream::Opened(_)))
130 .count();
131 if (opened == write.parts.len()) != skipped.is_empty() {
132 return Err(crate::SnapshotError::Inconsistent(
133 "skipped parts disagree with the parts named skipped",
134 )
135 .into());
136 }
137 if !skipped.is_empty() {
138 return Ok(FileEvidence::Unproven(UnprovenFiles::Parts(skipped)));
139 }
140 for (header, part) in write.header.parts.into_iter().zip(write.parts) {
141 let DownloadedPartStream::Opened(input) = part else {
142 unreachable!("every part opened");
143 };
144 crate::download_stream::read_part(input, header, |frame| {
145 if let coven_format::dismissal::WriteFrame::Change(row) = frame {
146 retain_change(&row, &mut references)?;
147 }
148 Ok::<_, DbError>(())
149 })?;
150 }
151 Ok(FileEvidence::Complete(references))
152 })
153 .await
154 }
155
156 pub async fn write_schema_versions(&self) -> Result<RangeInclusive<u32>, DbError> {
158 self.call(move |inner| Ok(inner.access.write_schema.versions.clone())).await
159 }
160
161 pub async fn file_candidates(
165 &self,
166 uploaders: BTreeSet<DeviceId>,
167 ) -> Result<Vec<FileCandidate>, DbError> {
168 self.call(move |inner| {
169 inner.access.with_committed(|db| {
170 let mut candidates = Vec::new();
171 for device in uploaders {
172 candidates.extend(db.query(
173 "SELECT object,EXISTS(SELECT 1 FROM _coven_file_protection p WHERE p.device=c.device AND p.object=c.object),since FROM _coven_file_candidates c WHERE c.device=?1 ORDER BY c.object",
174 [device],
175 |r| {
176 Ok(FileCandidate {
177 device,
178 object: r.get(0)?,
179 protected: r.get(1)?,
180 since: crate::store_state_tables::slot(r, 2)?,
181 })
182 },
183 )?);
184 }
185 Ok(candidates)
186 })
187 })
188 .await
189 }
190
191 pub async fn published_reads(
195 &self,
196 devices: BTreeSet<DeviceId>,
197 ) -> Result<BTreeMap<DeviceId, u64>, DbError> {
198 self.call(move |inner| {
199 inner.access.with_committed(|db| {
200 let mut reads = BTreeMap::new();
201 for device in devices {
202 if let Some(read) = crate::write_metadata::last_landed_read(db, device)? {
203 reads.insert(device, read);
204 }
205 }
206 Ok(reads)
207 })
208 })
209 .await
210 }
211
212 pub async fn settle_file_candidates(
216 &self,
217 protected: Vec<(DeviceId, ObjectId)>,
218 deleted: Vec<(DeviceId, ObjectId)>,
219 ) -> Result<(), DbError> {
220 if protected.is_empty() && deleted.is_empty() {
221 return Ok(());
222 }
223 self.call(move |inner| {
224 inner.access.with_writer(|writer| {
225 writer.transaction(|db| {
226 for (device, object) in protected {
227 db.internal_execute(
228 "DELETE FROM _coven_file_candidates WHERE device=?1 AND object=?2 AND EXISTS(SELECT 1 FROM _coven_file_protection WHERE device=?1 AND object=?2)",
229 params![device, object],
230 )?;
231 }
232 for (device, object) in deleted {
233 db.internal_execute(
234 "DELETE FROM _coven_file_candidates WHERE device=?1 AND object=?2",
235 params![device, object],
236 )?;
237 }
238 Ok(())
239 })
240 })
241 })
242 .await
243 }
244
245 #[cfg(test)]
248 pub async fn add_file_candidates(
249 &self,
250 files: BTreeSet<(DeviceId, ObjectId)>,
251 ) -> Result<(), DbError> {
252 if files.is_empty() {
253 return Ok(());
254 }
255 self.call(move |inner| {
256 inner.access.with_writer(|writer| {
257 writer.transaction(|db| {
258 for (device, object) in files {
259 insert_candidate(db, device, object)?;
260 }
261 Ok(())
262 })
263 })
264 })
265 .await
266 }
267
268 pub async fn file_scan(&self, device: DeviceId) -> Result<Option<Slot>, DbError> {
271 self.call(move |inner| {
272 let reader = inner.access.readers.acquire_reader();
273 reader.with_reader(|db| {
274 Ok(db
275 .query("SELECT slot FROM _coven_file_scans WHERE device=?1", [device], |r| {
276 crate::store_state_tables::slot(r, 0)
277 })?
278 .pop())
279 })
280 })
281 .await
282 }
283
284 pub async fn complete_file_scan(
295 &self,
296 device: DeviceId,
297 stop: Slot,
298 objects: BTreeSet<ObjectId>,
299 ) -> Result<bool, DbError> {
300 self.call(move |inner| {
301 inner.access.with_writer(|writer| {
302 writer.transaction(|db| {
303 let kept: bool = db.query_row(
304 "SELECT EXISTS(SELECT 1 FROM _coven_cutoffs c JOIN _coven_devices d ON d.device=?1 WHERE c.slot=?2 AND (c.device=d.device OR c.member=d.member))",
305 params![device, stop.get().to_be_bytes()],
306 |r| r.get(0),
307 )?;
308 if !kept {
309 return Ok(false);
310 }
311 for object in objects {
312 insert_candidate(db, device, object)?;
313 }
314 db.internal_execute(
315 "INSERT INTO _coven_file_scans(device,slot) VALUES(?1,?2) ON CONFLICT(device) DO UPDATE SET slot=excluded.slot",
316 params![
317 device,
318 stop.get().to_be_bytes()
319 ],
320 )?;
321 Ok(true)
322 })
323 })
324 })
325 .await
326 }
327}
328
329#[derive(Clone, Copy, Debug, PartialEq, Eq)]
331pub struct FileCandidate {
332 pub device: DeviceId,
334 pub object: ObjectId,
336 pub protected: bool,
338 pub since: Slot,
342}
343
344fn insert_candidate(
345 db: &crate::sqlite::DatabaseConnection,
346 device: DeviceId,
347 object: ObjectId,
348) -> Result<(), DbError> {
349 db.internal_execute(
350 "INSERT INTO _coven_file_candidates(device,object,since) VALUES(?1,?2,(SELECT applied FROM _coven_sequence WHERE singleton=1)) ON CONFLICT DO UPDATE SET since=max(since,excluded.since)",
351 params![device, object],
352 )?;
353 Ok(())
354}
355
356pub(crate) fn count_candidates_from(
359 db: &crate::sqlite::DatabaseConnection,
360 slot: u64,
361) -> Result<(), DbError> {
362 db.internal_execute("UPDATE _coven_file_candidates SET since=?1", [slot.to_be_bytes()])?;
363 Ok(())
364}
365
366pub(crate) fn snapshot_references(
370 db: &crate::sqlite::DatabaseConnection,
371 schema: &crate::write_schema::WriteSchema,
372 audience: &coven_merge::Audience,
373) -> Result<BTreeSet<(DeviceId, ObjectId)>, DbError> {
374 let mut references = BTreeSet::new();
375 crate::snapshot_install::file_locations(db, schema, audience, |value| {
376 retain_value(value, &mut references)
377 })
378 .and_then(|()| {
379 crate::loss_record::visit(db, audience, |loss| {
380 crate::file_protection::loss(&loss, &mut references);
381 Ok::<_, DbError>(())
382 })
383 })
384 .map_err(|error| match error {
385 DbError::DamagedDatabase => {
386 crate::snapshot_error::invalid("snapshot file location is invalid")
387 }
388 error => error,
389 })?;
390 Ok(references)
391}
392
393fn retain_change(
398 row: &RowChange,
399 references: &mut BTreeSet<(DeviceId, ObjectId)>,
400) -> Result<(), DbError> {
401 if let Operation::Insert(columns) | Operation::Update(columns) = &row.change.operation {
402 retain_columns(&row.row.table, columns, references)?;
403 }
404 if let Some(column) = crate::file_protection::location(&row.row.table) {
405 if let Some(value) = row.old.get(column) {
406 retain_value(value, references)?;
407 }
408 }
409 Ok(())
410}
411
412fn retain_columns(
413 table: &str,
414 columns: &BTreeMap<String, ColumnValue<Value>>,
415 references: &mut BTreeSet<(DeviceId, ObjectId)>,
416) -> Result<(), DbError> {
417 if let Some(column) = crate::file_protection::location(table) {
418 if let Some(value) = columns.get(column) {
419 retain_value(&value.value, references)?;
420 }
421 }
422 Ok(())
423}
424
425#[cfg(test)]
426#[path = "database_file_retention_tests.rs"]
427pub(crate) mod tests;