1use super::{with_locked, Database};
4use crate::{sqlite::DatabaseConnection, DbError};
5use coven_format::path::{ObjectPath, PathError};
6use coven_foundation::id_source::{DeviceId, KeyId, ObjectId};
7use coven_foundation::locks::lock_unpoisoned;
8use std::collections::{BTreeMap, BTreeSet};
9use std::sync::{Arc, Mutex, Weak};
10
11#[path = "input_bytes.rs"]
12mod bytes;
13pub use bytes::RetainedBytes;
14#[path = "snapshot_staging.rs"]
15pub(crate) mod staging;
16#[cfg(any(test, feature = "test-utils"))]
17pub use staging::StagedDamage;
18pub use staging::{SnapshotStaging, StagedDigest};
19
20type InputConnection = Arc<Mutex<Option<DatabaseConnection>>>;
21
22fn read_local<R>(
23 reader: &InputConnection,
24 run: impl FnOnce(&DatabaseConnection) -> Result<R, DbError>,
25) -> Result<R, DbError> {
26 let guard = lock_unpoisoned(reader);
27 let reader = guard.as_ref().ok_or(DbError::StoreClosed)?;
28 reader.read_transaction(|| run(reader))
29}
30
31pub(crate) struct InputDatabase {
32 reader: InputConnection,
33 writer: Arc<Mutex<DatabaseConnection>>,
35 active: Arc<Mutex<BTreeMap<i64, Weak<InputRead>>>>,
36 stagings: std::sync::atomic::AtomicI64,
38 #[cfg(any(test, feature = "test-utils"))]
41 damage_staged: std::sync::atomic::AtomicBool,
42 #[cfg(any(test, feature = "test-utils"))]
46 damage_finishing: Mutex<Option<(staging::StagedDamage, u32)>>,
47 #[cfg(any(test, feature = "test-utils"))]
50 fail_removal: std::sync::atomic::AtomicBool,
51}
52
53struct InputRead {
54 id: i64,
55 active: Weak<Mutex<BTreeMap<i64, Weak<InputRead>>>>,
56 receiving: tokio::sync::Mutex<()>,
57}
58
59impl Drop for InputRead {
60 fn drop(&mut self) {
61 if let Some(active) = self.active.upgrade() {
62 let mut active = lock_unpoisoned(&active);
63 if active.get(&self.id).is_some_and(|saved| saved.strong_count() == 0) {
65 active.remove(&self.id);
66 }
67 }
68 }
69}
70
71#[derive(Clone)]
73pub struct RetainedInput {
74 database: Database,
75 id: i64,
76 reading: Arc<InputRead>,
77}
78
79pub struct RetainedRange {
81 pub offset: u64,
83 pub bytes: Vec<u8>,
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub struct InputBudget {
90 pub maximum: u64,
92 pub retained: u64,
94 pub evicted: u64,
96}
97
98fn copy_claim(inputs: &InputDatabase, source: &DatabaseConnection, id: i64) -> Result<(), DbError> {
101 inputs.write(|db| {
102 db.internal_execute("DELETE FROM _coven_input_ranges WHERE object=?1", [id])?;
103 crate::claim::read_attempt::<_, DbError>(source, |chunks| {
104 let mut offset = 0u64;
105 for chunk in chunks {
106 let bytes = chunk?;
107 let start = sql_offset(offset)?;
108 offset += bytes.len() as u64;
109 db.internal_execute(
110 "INSERT INTO _coven_input_ranges(object,start,end,bytes) VALUES(?1,?2,?3,?4)",
111 (id, start, sql_offset(offset)?, bytes),
112 )?;
113 }
114 Ok(())
115 })?;
116 db.internal_execute("UPDATE _coven_input_objects SET received=1 WHERE id=?1", [id])?;
117 evict(db, &inputs.active)
118 })
119}
120
121impl InputDatabase {
122 pub(crate) fn prepare<T>(
124 &self,
125 consume: impl FnOnce(&crate::input_preparation::PreparedInputs<'_>) -> Result<T, DbError>,
126 ) -> Result<T, DbError> {
127 with_locked(&self.writer, |writer| {
128 writer.transaction(|db| {
129 let inputs = crate::input_preparation::PreparedInputs::new(db)?;
130 let result = consume(&inputs)?;
131 inputs.finish()?;
132 Ok(result)
133 })
134 })
135 }
136 pub(crate) fn prepare_claim<T>(
137 &self,
138 attempt: crate::ClaimAttempt<impl std::io::Read>,
139 consume: impl FnOnce(
140 crate::claim_preparation::PreparedClaim<crate::claim_preparation::PreparedBytes<'_>>,
141 ) -> Result<T, DbError>,
142 ) -> Result<T, DbError> {
143 with_locked(&self.writer, |writer| {
144 writer.transaction(|db| crate::claim_preparation::prepare(db, attempt, consume))
145 })
146 }
147 fn discard(&self, id: i64) -> Result<(), DbError> {
150 self.write(|db| {
151 db.internal_execute("DELETE FROM _coven_input_ranges WHERE object=?1", [id])?;
152 db.internal_execute("UPDATE _coven_input_objects SET received=0 WHERE id=?1", [id])?;
153 Ok(())
154 })
155 }
156
157 fn release(&self, ids: Vec<i64>) -> Result<(), DbError> {
158 let pending = self.read(|db| {
159 let mut pending = Vec::new();
160 for id in ids {
161 if !db.query_row(
162 "SELECT released FROM _coven_input_objects WHERE id=?1",
163 [id],
164 |r| r.get::<_, bool>(0),
165 )? {
166 pending.push(id);
167 }
168 }
169 Ok(pending)
170 })?;
171 if pending.is_empty() {
172 return Ok(());
173 }
174 self.write(|db| {
175 for id in pending {
176 db.internal_execute(
177 "UPDATE _coven_input_objects SET released=1 WHERE id=?1",
178 [id],
179 )?;
180 }
181 Ok(())
182 })
183 }
184 #[cfg(any(test, feature = "test-utils"))]
185 pub(super) fn test_track_work(&self, counter: &crate::test_utils::WorkCounter) {
186 lock_unpoisoned(&self.reader)
187 .as_ref()
188 .expect("open input reader")
189 .test_track_work(counter.clone());
190 lock_unpoisoned(&self.writer).test_track_work(counter.clone());
191 }
192 pub(crate) fn new(
195 reader: DatabaseConnection,
196 writer: Arc<Mutex<DatabaseConnection>>,
197 ) -> Result<Self, DbError> {
198 with_locked(&writer, |writer| staging::create_tables(writer))?;
199 Ok(Self {
200 reader: Arc::new(Mutex::new(Some(reader))),
201 writer,
202 active: Arc::new(Mutex::new(BTreeMap::new())),
203 stagings: std::sync::atomic::AtomicI64::new(1),
204 #[cfg(any(test, feature = "test-utils"))]
205 damage_staged: std::sync::atomic::AtomicBool::new(false),
206 #[cfg(any(test, feature = "test-utils"))]
207 damage_finishing: Mutex::new(None),
208 #[cfg(any(test, feature = "test-utils"))]
209 fail_removal: std::sync::atomic::AtomicBool::new(false),
210 })
211 }
212
213 fn reading(&self, id: i64) -> Arc<InputRead> {
214 let mut active = lock_unpoisoned(&self.active);
215 if let Some(reading) = active.get(&id).and_then(Weak::upgrade) {
216 return reading;
217 }
218 let reading = Arc::new(InputRead {
219 id,
220 active: Arc::downgrade(&self.active),
221 receiving: tokio::sync::Mutex::new(()),
222 });
223 active.insert(id, Arc::downgrade(&reading));
224 reading
225 }
226
227 pub(crate) fn close(self) -> Vec<DbError> {
228 let reader = lock_unpoisoned(&self.reader).take().expect("open input reader");
229 let mut failures: Vec<DbError> = reader.close().err().into_iter().collect();
230 match Arc::try_unwrap(self.writer) {
232 Ok(writer) => {
233 if let Err(error) = coven_foundation::locks::into_unpoisoned(writer).close() {
234 failures.push(error);
235 }
236 }
237 Err(_held) => failures.push(DbError::Invariant {
238 what: "the input writer closes after its checkpointer stopped",
239 }),
240 }
241 failures
242 }
243
244 fn read<R>(
245 &self,
246 run: impl FnOnce(&DatabaseConnection) -> Result<R, DbError>,
247 ) -> Result<R, DbError> {
248 read_local(&self.reader, run)
249 }
250
251 fn write<R>(
252 &self,
253 run: impl FnOnce(&DatabaseConnection) -> Result<R, DbError>,
254 ) -> Result<R, DbError> {
255 with_locked(&self.writer, |writer| {
256 #[cfg(any(test, feature = "test-utils"))]
257 writer.record_work(|work| work.writer_holds += 1);
258 writer.transaction(run)
259 })
260 }
261}
262
263#[expect(
264 clippy::map_err_ignore,
265 reason = "a size past the target integer is TooLarge, which names the field and size"
266)]
267fn sql_offset(value: u64) -> Result<i64, DbError> {
268 i64::try_from(value).map_err(|_| DbError::TooLarge {
269 field: "retained input bytes",
270 actual: value,
271 maximum: i64::MAX as u64,
272 })
273}
274
275fn observation(db: &DatabaseConnection, path: &str) -> Result<Option<(i64, Vec<u8>)>, DbError> {
276 Ok(db
277 .query("SELECT id,identity FROM _coven_input_objects WHERE path=?1", [path], |r| {
278 Ok((r.get(0)?, r.get(1)?))
279 })?
280 .into_iter()
281 .next())
282}
283
284const RANGES: &str = "SELECT max(start,?2),substr(bytes,max(start,?2)-start+1,min(end,?3)-max(start,?2)) FROM _coven_input_ranges WHERE object=?1 AND start>=coalesce((SELECT start FROM _coven_input_ranges WHERE object=?1 AND start<=?2 ORDER BY start DESC LIMIT 1),?2) AND start<?3 AND end>?2 ORDER BY start";
287
288fn budget(db: &DatabaseConnection) -> Result<InputBudget, DbError> {
289 db.query_row(
290 "SELECT maximum,retained,evicted FROM _coven_input_budget WHERE singleton=1",
291 [],
292 |r| {
293 Ok(InputBudget {
294 maximum: r.get::<_, i64>(0)? as u64,
295 retained: r.get::<_, i64>(1)? as u64,
296 evicted: r.get::<_, i64>(2)? as u64,
297 })
298 },
299 )
300}
301
302fn evict(
303 db: &DatabaseConnection,
304 active: &Mutex<BTreeMap<i64, Weak<InputRead>>>,
305) -> Result<(), DbError> {
306 let active = lock_unpoisoned(active);
309 let before = budget(db)?;
310 let mut retained = before.retained;
311 let mut after = 0;
312 while retained > before.maximum {
313 let next = db.query(
314 "SELECT r.id,r.object FROM _coven_input_ranges r JOIN _coven_input_objects o ON o.id=r.object WHERE o.released=1 AND r.id>?1 ORDER BY r.id LIMIT 1",
315 [after], |r| Ok((r.get::<_,i64>(0)?, r.get::<_,i64>(1)?)),
316 )?.into_iter().next();
317 let Some((id, object)) = next else {
318 return Err(DbError::InputBudgetExceeded {
319 required: retained,
320 maximum: before.maximum,
321 });
322 };
323 after = id;
324 if active.get(&object).is_some_and(|input| input.strong_count() > 0) {
325 continue;
326 }
327 db.internal_execute("DELETE FROM _coven_input_ranges WHERE object=?1", [object])?;
330 retained = budget(db)?.retained;
331 }
332 let evicted = before.retained - retained;
333 if evicted > 0 {
334 db.internal_execute(
335 "UPDATE _coven_input_budget SET evicted=evicted+?1 WHERE singleton=1",
336 [sql_offset(evicted)?],
337 )?;
338 tracing::debug!(bytes = evicted, retained, "evicted retained remote input bytes");
339 }
340 Ok(())
341}
342
343impl InputDatabase {
344 fn forget(&self, select: &str, params: impl rusqlite::Params) -> Result<(), DbError> {
348 self.write(|db| {
349 let active = lock_unpoisoned(&self.active);
350 for object in db.query(select, params, |r| r.get::<_, i64>(0))? {
351 if active.get(&object).is_some_and(|input| input.strong_count() > 0) {
352 continue;
353 }
354 db.internal_execute("DELETE FROM _coven_input_objects WHERE id=?1", [object])?;
355 }
356 Ok(())
357 })
358 }
359
360 fn forget_slots_below(&self, first: u64) -> Result<(), DbError> {
361 self.forget("SELECT id FROM _coven_input_objects WHERE slot<?1", [sql_offset(first)?])
362 }
363
364 pub(crate) fn forget_path(&self, path: &str) -> Result<(), DbError> {
365 self.forget("SELECT id FROM _coven_input_objects WHERE path=?1", [path])
366 }
367}
368
369impl Database {
370 pub async fn forget_slot_inputs_below(
373 &self,
374 first: coven_format::value::Slot,
375 ) -> Result<(), DbError> {
376 self.call(move |inner| inner.inputs.forget_slots_below(first.get())).await
377 }
378
379 #[cfg(any(test, feature = "test-utils"))]
383 pub async fn claim_input(&self) -> Result<RetainedBytes, DbError> {
384 let (reader, reading, length) = self.call(|inner| {
385 inner.access.with_committed(|source| {
386 let upload = crate::claim::upload(source)?;
387 let id = inner.inputs.write(|db: &DatabaseConnection| {
388 db.internal_execute("DELETE FROM _coven_input_objects WHERE path='local-claim'", [])?;
389 db.query_row("INSERT INTO _coven_input_objects(path,identity,observation) VALUES('local-claim',x'',x'') RETURNING id", [], |row| row.get(0))
390 })?;
391 let reading = inner.inputs.reading(id);
392 copy_claim(&inner.inputs, source, id)?;
393 inner.inputs.release(vec![id])?;
394 Ok::<_, DbError>((inner.inputs.reader.clone(), reading, upload.size))
395 })
396 }).await?;
397 Ok(RetainedBytes::new(reader, reading, length))
398 }
399 pub async fn retained_observation(&self, path: String) -> Result<Option<Vec<u8>>, DbError> {
402 self.call(move |inner| {
403 inner.inputs.read(|db| {
404 Ok(db
405 .query(
406 "SELECT observation FROM _coven_input_objects WHERE path=?1",
407 [&path],
408 |r| r.get(0),
409 )?
410 .into_iter()
411 .next())
412 })
413 })
414 .await
415 }
416
417 pub async fn observe_input(
421 &self,
422 path: String,
423 identity: Vec<u8>,
424 observation_bytes: Vec<u8>,
425 replaceable: bool,
426 ) -> Result<Option<RetainedInput>, DbError> {
427 let slot = ObjectPath::parse(&path)
428 .map_err(|PathError| DbError::Invariant {
429 what: "an observed input names a storage path",
430 })?
431 .slot_number()
432 .map(|slot| sql_offset(slot.get()))
433 .transpose()?;
434 let id = self.call(move |inner| {
435 let previous = inner.inputs.read(|db| observation(db, &path))?;
436 match previous {
437 Some((id, saved)) if saved == identity => return Ok(Some(id)),
438 Some(_) if !replaceable => return Ok(None),
439 _ => (),
440 }
441 inner.inputs.write(|db: &DatabaseConnection| {
442 match observation(db, &path)? {
443 Some((id, saved)) if saved == identity => return Ok(Some(id)),
444 Some(_) if !replaceable => return Ok(None),
445 _ => (),
446 }
447 db.internal_execute("DELETE FROM _coven_input_objects WHERE path=?1", [&path])?;
448 db.query_row("INSERT INTO _coven_input_objects(path,slot,identity,observation) VALUES(?1,?2,?3,?4) RETURNING id", (&path, slot, &identity, &observation_bytes), |r| r.get(0)).map(Some)
449 })
450 }).await?;
451 match id {
452 None => Ok(None),
453 Some(id) => {
454 let reading =
455 self.call(move |inner| Ok::<_, DbError>(inner.inputs.reading(id))).await?;
456 Ok(Some(RetainedInput { database: self.clone(), id, reading }))
457 }
458 }
459 }
460
461 #[cfg(any(test, feature = "test-utils"))]
465 pub async fn set_input_budget(&self, maximum: u64) -> Result<InputBudget, DbError> {
466 self.call(move |inner| {
467 let previous = inner.inputs.read(budget)?;
468 if previous.maximum == maximum {
469 return Ok(previous);
470 }
471 inner.inputs.write(|db: &DatabaseConnection| {
472 db.internal_execute(
473 "UPDATE _coven_input_budget SET maximum=?1 WHERE singleton=1",
474 [sql_offset(maximum)?],
475 )?;
476 evict(db, &inner.inputs.active)?;
477 budget(db)
478 })
479 })
480 .await
481 }
482
483 #[cfg(any(test, feature = "test-utils"))]
485 pub async fn input_budget(&self) -> Result<InputBudget, DbError> {
486 self.call(|inner| inner.inputs.read(budget)).await
487 }
488
489 #[cfg(any(test, feature = "test-utils"))]
492 pub async fn input_peak(&self) -> Result<u64, DbError> {
493 self.call(|inner| {
494 with_locked(&inner.inputs.writer, |writer| {
495 writer.batch(
496 "CREATE TEMP TABLE IF NOT EXISTS _coven_input_peak(peak INTEGER NOT NULL);
497 INSERT INTO _coven_input_peak SELECT retained FROM main._coven_input_budget WHERE NOT EXISTS(SELECT 1 FROM _coven_input_peak);
498 CREATE TEMP TRIGGER IF NOT EXISTS _coven_input_peak_change AFTER UPDATE OF retained ON main._coven_input_budget BEGIN
499 UPDATE _coven_input_peak SET peak=max(peak,NEW.retained);
500 END;",
501 )?;
502 writer.query_row("SELECT peak FROM temp._coven_input_peak", [], |row| {
503 row.get::<_, i64>(0)
504 })
505 })
506 })
507 .await
508 .map(|peak| peak as u64)
509 }
510}
511
512impl RetainedInput {
513 pub async fn keep_claim_bytes(&self) -> Result<(), DbError> {
517 if self.received().await? {
518 return Ok(());
519 }
520 let id = self.id;
521 self.database
522 .call(move |inner| {
523 inner.access.with_committed(|source| copy_claim(&inner.inputs, source, id))
524 })
525 .await
526 }
527 pub async fn local_bytes(&self, length: u64) -> Result<RetainedBytes, DbError> {
531 let reader =
532 self.database.call(|inner| Ok::<_, DbError>(inner.inputs.reader.clone())).await?;
533 Ok(RetainedBytes::new(reader, self.reading.clone(), length))
534 }
535 pub async fn observation(&self) -> Result<Vec<u8>, DbError> {
538 self.read(|db, id| {
539 db.query_row("SELECT observation FROM _coven_input_objects WHERE id=?1", [id], |r| {
540 r.get(0)
541 })
542 })
543 .await
544 }
545 pub async fn receiving(&self) -> tokio::sync::MutexGuard<'_, ()> {
548 self.reading.receiving.lock().await
549 }
550 async fn read<R: Send + 'static>(
551 &self,
552 run: impl FnOnce(&DatabaseConnection, i64) -> Result<R, DbError> + Send + 'static,
553 ) -> Result<R, DbError> {
554 let id = self.id;
555 self.database.call(move |inner| inner.inputs.read(|db| run(db, id))).await
556 }
557
558 async fn write<R: Send + 'static>(
559 &self,
560 run: impl FnOnce(&DatabaseConnection, i64) -> Result<R, DbError> + Send + 'static,
561 ) -> Result<R, DbError> {
562 let id = self.id;
563 self.database.call(move |inner| inner.inputs.write(|db| run(db, id))).await
564 }
565
566 pub async fn ranges(&self, start: u64, end: u64) -> Result<Vec<RetainedRange>, DbError> {
568 let start = sql_offset(start)?;
569 let end = sql_offset(end)?;
570 self.read(move |db, id| {
571 db.query(RANGES, (id, start, end), |r| {
572 Ok(RetainedRange { offset: r.get::<_, i64>(0)? as u64, bytes: r.get(1)? })
573 })
574 })
575 .await
576 }
577
578 pub async fn keep_range(&self, offset: u64, bytes: Vec<u8>) -> Result<(), DbError> {
581 self.keep_received_range(offset, bytes, false).await
582 }
583
584 pub async fn received(&self) -> Result<bool, DbError> {
587 self.read(|db, id| {
588 db.query_row("SELECT received FROM _coven_input_objects WHERE id=?1", [id], |r| {
589 r.get(0)
590 })
591 })
592 .await
593 }
594
595 pub async fn retained_length(&self) -> Result<u64, DbError> {
598 self.read(|db, id| {
599 db.query_row(
600 "SELECT coalesce(sum(end-start),0) FROM _coven_input_ranges WHERE object=?1",
601 [id],
602 |r| r.get::<_, i64>(0),
603 )
604 })
605 .await
606 .map(|length| length as u64)
607 }
608
609 pub async fn keep_received_range(
612 &self,
613 offset: u64,
614 bytes: Vec<u8>,
615 complete: bool,
616 ) -> Result<(), DbError> {
617 if bytes.is_empty() && (!complete || self.received().await?) {
618 return Ok(());
619 }
620 let end = offset
621 .checked_add(bytes.len() as u64)
622 .ok_or(DbError::Invariant { what: "an input range's end fits u64" })?;
623 let offset = sql_offset(offset)?;
624 let end = sql_offset(end)?;
625 let id = self.id;
626 self.database.call(move |inner| inner.inputs.write(|db: &DatabaseConnection| {
627 if !bytes.is_empty() {
628 db.internal_execute("INSERT INTO _coven_input_ranges(object,start,end,bytes) VALUES(?1,?2,?3,?4)", (id,offset,end,bytes))?;
629 }
630 if complete {
631 db.internal_execute("UPDATE _coven_input_objects SET received=1 WHERE id=?1", [id])?;
632 }
633 evict(db, &inner.inputs.active)
634 })).await
635 }
636
637 pub async fn release_ranges(self) -> Result<(), DbError> {
641 let id = self.id;
642 self.database.call(move |inner| inner.inputs.release(vec![id])).await
643 }
644
645 pub async fn discard(self) -> Result<(), DbError> {
648 let id = self.id;
649 self.database.call(move |inner| inner.inputs.discard(id)).await
650 }
651
652 pub async fn files(&self) -> Result<Option<FileEvidence>, DbError> {
654 self.read(|db, id| {
655 let (complete, schema): (Option<bool>, Option<u32>) = db.query_row(
656 "SELECT files_complete,files_schema FROM _coven_input_objects WHERE id=?1",
657 [id],
658 |r| Ok((r.get(0)?, r.get(1)?)),
659 )?;
660 Ok(match (complete, schema) {
661 (None, _) => None,
662 (Some(false), Some(version)) => {
663 Some(FileEvidence::Unproven(UnprovenFiles::Schema(version)))
664 }
665 (Some(false), None) => {
666 let parts = db
667 .query(
668 "SELECT audience,key FROM _coven_input_unread WHERE object=?1",
669 [id],
670 |r| {
671 Ok((
672 crate::write_encoding::audience(r, 0)?,
673 r.get::<_, KeyId>(1)?,
674 ))
675 },
676 )?
677 .into_iter()
678 .collect::<BTreeSet<_>>();
679 if parts.is_empty() {
680 return Err(DbError::DamagedDatabase);
681 }
682 Some(FileEvidence::Unproven(UnprovenFiles::Parts(parts)))
683 }
684 (Some(true), _) => Some(FileEvidence::Complete(
685 db.query(
686 "SELECT device,stored_object FROM _coven_input_files WHERE object=?1 ORDER BY device,stored_object",
687 [id],
688 |r| Ok((r.get::<_, DeviceId>(0)?, r.get::<_, ObjectId>(1)?)),
689 )?
690 .into_iter()
691 .collect(),
692 )),
693 })
694 })
695 .await
696 }
697
698 pub async fn keep_files(&self, files: FileEvidence) -> Result<(), DbError> {
702 if self.files().await?.as_ref() == Some(&files) {
703 return Ok(());
704 }
705 self.write(move |db: &DatabaseConnection, id| {
706 db.internal_execute("DELETE FROM _coven_input_files WHERE object=?1", [id])?;
707 db.internal_execute("DELETE FROM _coven_input_unread WHERE object=?1", [id])?;
708 let (complete, schema) = match &files {
709 FileEvidence::Complete(_) => (true, None),
710 FileEvidence::Unproven(UnprovenFiles::Schema(version)) => (false, Some(*version)),
711 FileEvidence::Unproven(UnprovenFiles::Parts(_)) => (false, None),
712 };
713 db.internal_execute(
714 "UPDATE _coven_input_objects SET files_complete=?2,files_schema=?3 WHERE id=?1",
715 (id, complete, schema),
716 )?;
717 match files {
718 FileEvidence::Complete(files) => {
719 for (device, object) in files {
720 db.internal_execute(
721 "INSERT INTO _coven_input_files(object,device,stored_object) VALUES(?1,?2,?3)",
722 (id, device, object),
723 )?;
724 }
725 }
726 FileEvidence::Unproven(UnprovenFiles::Parts(parts)) => {
727 if parts.is_empty() {
728 return Err(DbError::Invariant {
729 what: "unproven file evidence names a part",
730 });
731 }
732 for (audience, key) in parts {
733 db.internal_execute(
734 "INSERT INTO _coven_input_unread(object,audience,key) VALUES(?1,?2,?3)",
735 (
736 id,
737 &audience,
738 key,
739 ),
740 )?;
741 }
742 }
743 FileEvidence::Unproven(UnprovenFiles::Schema(_)) => (),
744 }
745 Ok(())
746 })
747 .await
748 }
749}
750
751#[derive(Clone, Debug, PartialEq, Eq)]
754pub enum FileEvidence {
755 Complete(BTreeSet<(DeviceId, ObjectId)>),
757 Unproven(UnprovenFiles),
760}
761
762#[derive(Clone, Debug, PartialEq, Eq)]
764pub enum UnprovenFiles {
765 Parts(BTreeSet<(coven_merge::Audience, KeyId)>),
768 Schema(u32),
770}
771
772impl UnprovenFiles {
773 pub fn changed(
776 &self,
777 opens: impl Fn(&coven_merge::Audience, KeyId) -> bool,
778 versions: &std::ops::RangeInclusive<u32>,
779 ) -> bool {
780 match self {
781 Self::Parts(parts) => parts.iter().any(|(audience, key)| opens(audience, *key)),
782 Self::Schema(version) => versions.contains(version),
783 }
784 }
785}
786
787#[cfg(test)]
788#[path = "input_database_tests.rs"]
789mod tests;