Skip to main content

coven_database/
input_database.rs

1//! Durable remote observations, independently committed while application writes run.
2
3use 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    /// Shared with the input database's checkpointer (§4).
34    writer: Arc<Mutex<DatabaseConnection>>,
35    active: Arc<Mutex<BTreeMap<i64, Weak<InputRead>>>>,
36    /// The next snapshot staging's number; 0 is the installing snapshot's.
37    stagings: std::sync::atomic::AtomicI64,
38    /// A test's request to change a staged record before the next load or
39    /// file check reads it, as damage on this device's disk would.
40    #[cfg(any(test, feature = "test-utils"))]
41    damage_staged: std::sync::atomic::AtomicBool,
42    /// A test's request to damage the next stagings that finish, and how
43    /// many, before finishing checks them, as damage on this device's disk
44    /// would.
45    #[cfg(any(test, feature = "test-utils"))]
46    damage_finishing: Mutex<Option<(staging::StagedDamage, u32)>>,
47    /// A test's request to fail the next removal of the stagings a failed
48    /// load or file check ends, as a disk error would.
49    #[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            // A concurrent new reader may already have replaced this expired weak entry.
64            if active.get(&self.id).is_some_and(|saved| saved.strong_count() == 0) {
65                active.remove(&self.id);
66            }
67        }
68    }
69}
70
71/// One observed object version. A replacement invalidates the preceding handle.
72#[derive(Clone)]
73pub struct RetainedInput {
74    database: Database,
75    id: i64,
76    reading: Arc<InputRead>,
77}
78
79/// Encrypted bytes received for one provider observation; completion is separate.
80pub struct RetainedRange {
81    /// Byte offset in the encrypted object.
82    pub offset: u64,
83    /// The exact provider bytes.
84    pub bytes: Vec<u8>,
85}
86
87/// Retained encrypted bytes have a maintained total and explicit eviction count.
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub struct InputBudget {
90    /// Maximum retained encrypted bytes, excluding checked metadata and references.
91    pub maximum: u64,
92    /// Currently retained encrypted bytes.
93    pub retained: u64,
94    /// Bytes explicitly evicted since this catalog was created.
95    pub evicted: u64,
96}
97
98/// Copy the current claim attempt's exact bytes from the store connection into
99/// the input catalog, so applying a landed claim reads no provider bytes.
100fn 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    /// Prepare inputs for one slot's application, in one input transaction.
123    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    /// Drop an object's received bytes and its receipt, so its next read
148    /// fetches it again.
149    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    /// `writer` is shared with the input database's checkpointer (§4). Its
193    /// temporary storage, on disk, holds prepared inputs and staged snapshots.
194    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        // The checkpointer stopped before, and let go of the writer.
231        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
284// Nonoverlapping ranges need at most the immediately preceding range plus
285// ranges beginning inside the requested interval. Seek to that boundary.
286const 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    // A released object can be borrowed by another consumer. Keep its bytes
307    // until that consumer drops its handle, including across network awaits.
308    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        // Evict a complete input together: retaining only its suffix would
328        // turn the next read into separate requests around an internal gap.
329        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    /// Delete the records of the objects `select` picks, except those a live
345    /// handle holds: a reader in flight keeps its rows. The cascades take each
346    /// record's ranges, file evidence and unread parts.
347    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    /// Forget the input records of the slots below a checked floor's `first`:
371    /// nothing reads them again (§16.4). A record a live read holds stays.
372    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    /// A seekable local input for applying the current claim without a provider
380    /// reread after successful creation. One cache identity is reused across
381    /// claims; the journal remains the authoritative copy until settlement.
382    #[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    /// The retained provider observation of an exact path, if any. A drained
400    /// writer's discovered objects are found here instead of by probing (§6.1).
401    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    /// Retain a provider observation before reading it. Immutable identity changes
418    /// return `None` and preserve the preceding evidence. A replacement discards
419    /// only that path's preceding version. Equal observations acquire no writer.
420    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    /// Configure retained encrypted input independently of application file caches.
462    /// Lowering the budget evicts oldest completed inputs atomically with accounting.
463    /// Checked facts and file protection are never evicted with their bytes.
464    #[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    /// Report byte retention and explicit eviction without scanning cached chunks.
484    #[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    /// The most retained bytes any input write has required since this was
490    /// first called, before its evictions, sampled on every change.
491    #[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    /// Retain the confirmed current claim under its provider observation without
514    /// downloading bytes already owned by the journal. The caller has settled
515    /// publication and checked the floor before associating these bytes.
516    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    /// A seekable reader over received bytes, pinned against eviction. It never
528    /// fetches missing bytes or acquires the application writer. Consumers may
529    /// use it inside a slot transaction after completing transport and floor checks.
530    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    /// The first completed provider observation for this identity, including
536    /// native metadata whose representation can differ in later listings.
537    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    /// Share an in-flight range across consumers of the same durable observation.
546    /// The guard spans checking retained ranges, fetching gaps, and committing them.
547    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    /// Cached portions of a requested interval, clipped so memory is bounded by it.
567    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    /// Commit newly fetched, nonoverlapping bytes. Refilling an evicted range is
579    /// a new insertion, while retained bytes must be consumed without fetching.
580    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    /// Whether a whole response (or all of its bounded ranges) reached checked
585    /// transport EOF. This records receipt, never content authentication.
586    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    /// How many of the object's bytes are retained: its ranges never overlap,
596    /// so the object is held whole when this equals its size.
597    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    /// Commit received bytes and, at successful EOF, the transport completion
610    /// fact in one transaction. Neither fact can survive a failed byte commit.
611    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    /// The complete input has been applied (with reversible inputs committed),
638    /// or permanently refused. Release its encrypted ranges for explicit eviction;
639    /// checked metadata, file protection, and the refusal remain durable.
640    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    /// Drop the received bytes, which a second read of the object
646    /// contradicted, so the next read fetches the object again (§19.1).
647    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    /// The checked file evidence of these bytes, if any (§17.10).
653    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    /// Record references only after complete authentication. Unproven evidence
699    /// proves no file absent and names what must change before these bytes
700    /// are checked again.
701    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/// What checked retained bytes establish about the uploaded files they name
752/// (§17.10).
753#[derive(Clone, Debug, PartialEq, Eq)]
754pub enum FileEvidence {
755    /// Every fixed file path the bytes name, after complete authentication.
756    Complete(BTreeSet<(DeviceId, ObjectId)>),
757    /// The bytes cannot prove any file absent, and are not read again for that
758    /// purpose until this prerequisite changes.
759    Unproven(UnprovenFiles),
760}
761
762/// Why retained bytes cannot yet name every file they refer to (§17.10).
763#[derive(Clone, Debug, PartialEq, Eq)]
764pub enum UnprovenFiles {
765    /// Parts sealed for these audiences under these keys, which this device
766    /// could not open: it lacked the key or could not read the audience.
767    Parts(BTreeSet<(coven_merge::Audience, KeyId)>),
768    /// A schema version this version cannot interpret.
769    Schema(u32),
770}
771
772impl UnprovenFiles {
773    /// Whether reading the same bytes again could now succeed: one of the
774    /// parts now opens, or the schema version is now supported.
775    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;