coven_database/
staged_damage.rs1#[cfg(any(test, feature = "test-utils"))]
6use super::table;
7use super::{SnapshotStaging, StagedTables};
8use crate::snapshot_error::SnapshotError;
9use crate::sqlite::DatabaseConnection;
10use crate::DbError;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub struct StagedDigest([u8; 32]);
18
19fn digest(db: &DatabaseConnection, tables: &StagedTables) -> Result<StagedDigest, DbError> {
22 let mut hasher = coven_crypto::ContentHasher::new();
23 let mut table = |name: &str, select: &str, params: &[&dyn rusqlite::ToSql]| {
24 let mut rows = Vec::new();
25 db.for_each(select, params, |row| {
26 rows.push(encoded(row)?);
27 Ok::<_, DbError>(())
28 })?;
29 rows.sort_unstable();
30 hasher.update(&(name.len() as u64).to_be_bytes());
31 hasher.update(name.as_bytes());
32 hasher.update(&(rows.len() as u64).to_be_bytes());
33 for row in rows {
34 hasher.update(&row);
35 }
36 Ok::<_, DbError>(())
37 };
38 let staging = format!("_coven_staged_{}_", tables.id);
40 for name in &tables.tables {
41 table(&name.replacen(&staging, "", 1), &format!("SELECT * FROM {name}"), &[])?;
42 }
43 table(
44 "_coven_snapshot_writes",
45 "SELECT device,number,timestamp,read_through,slot FROM temp._coven_snapshot_writes WHERE source=?1",
46 &[&tables.id],
47 )?;
48 Ok(StagedDigest(*hasher.finish().as_bytes()))
49}
50
51fn encoded(row: &rusqlite::Row<'_>) -> Result<Vec<u8>, DbError> {
54 use rusqlite::types::ValueRef;
55 let mut bytes = Vec::new();
56 for i in 0..row.as_ref().column_count() {
57 match row.get_ref(i)? {
58 ValueRef::Null => bytes.push(0),
59 ValueRef::Integer(value) => {
60 bytes.push(1);
61 bytes.extend_from_slice(&value.to_be_bytes());
62 }
63 ValueRef::Real(value) => {
64 bytes.push(2);
65 bytes.extend_from_slice(&value.to_bits().to_be_bytes());
66 }
67 ValueRef::Text(value) | ValueRef::Blob(value) => {
68 bytes.push(3);
69 bytes.extend_from_slice(&(value.len() as u64).to_be_bytes());
70 bytes.extend_from_slice(value);
71 }
72 }
73 }
74 Ok(bytes)
75}
76
77pub(super) fn with_staged_digest(
82 db: &DatabaseConnection,
83 stagings: &[SnapshotStaging],
84 error: DbError,
85) -> DbError {
86 let DbError::Snapshot(SnapshotError::Body { snapshot, error, staged: None }) = error else {
87 return error;
88 };
89 let tables = stagings
90 .iter()
91 .find(|staging| staging.expected == snapshot)
92 .and_then(|staging| staging.tables.as_ref());
93 match tables.map(|tables| digest(db, tables)) {
94 Some(Ok(staged)) => SnapshotError::Body { snapshot, error, staged: Some(staged) }.into(),
95 Some(Err(failure)) => failure,
96 None => SnapshotError::Body { snapshot, error, staged: None }.into(),
97 }
98}
99
100#[cfg(any(test, feature = "test-utils"))]
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub enum StagedDamage {
105 Record,
108 Writes,
111}
112
113#[cfg(any(test, feature = "test-utils"))]
115pub(super) fn damage(
116 db: &DatabaseConnection,
117 staging: i64,
118 damage: StagedDamage,
119) -> Result<(), DbError> {
120 let changed = match damage {
121 StagedDamage::Record => {
122 let records = table(staging, "records");
123 db.internal_execute(
124 &format!(
125 "UPDATE {records} SET record=substr(record,1,1) WHERE rowid=(SELECT min(rowid) FROM {records})"
126 ),
127 [],
128 )?
129 }
130 StagedDamage::Writes => db.internal_execute(
131 "UPDATE temp._coven_snapshot_writes SET read_through=slot WHERE source=?1",
132 [staging],
133 )?,
134 };
135 match changed {
136 0 => {
137 Err(DbError::Invariant { what: "a test damages a staging that holds what it changes" })
138 }
139 _ => Ok(()),
140 }
141}
142
143#[cfg(any(test, feature = "test-utils"))]
145pub(super) fn removal_failure() -> DbError {
146 DbError::Sqlite(rusqlite::Error::SqliteFailure(
147 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_IOERR),
148 Some("injected staging removal failure".into()),
149 ))
150}
151
152impl crate::Database {
153 #[cfg(any(test, feature = "test-utils"))]
157 pub async fn fail_next_staging_removal(&self) -> Result<(), DbError> {
158 self.call(|inner| {
159 inner.inputs.fail_removal.store(true, std::sync::atomic::Ordering::SeqCst);
160 Ok::<_, DbError>(())
161 })
162 .await
163 }
164}
165
166impl SnapshotStaging {
167 pub async fn digest(&self) -> Result<StagedDigest, DbError> {
169 self.id()?;
170 let tables = self.tables.as_ref().map(|tables| (tables.id, tables.tables.clone()));
171 let (id, tables) = tables.expect("a staging with an id holds its tables");
172 self.database
173 .call(move |inner| inner.inputs.write(|db| digest(db, &StagedTables { id, tables })))
174 .await
175 }
176}