1use super::{
9 SqliteStore,
10 integrity::{check_history_blocking, integrity_check_blocking},
11 lineage::{self, Seal},
12 load_blocking,
13 replay::replay_blocking,
14 runs::{self, RunStore, RunStoreReport},
15 snapshot::genesis_blocking,
16};
17use crate::{
18 StoreError,
19 error::database_error,
20 schema::{self, Layout},
21};
22use dpm_model::{LineageId, WorkspaceId};
23use files::{
24 ReadMode, canonical_blocking, create_target_blocking, discard_blocking, fingerprint_blocking,
25 open_read_only_blocking, read_mode_blocking, target_path_blocking,
26};
27use rusqlite::{
28 Connection,
29 backup::{Backup, StepResult},
30};
31use serde::Serialize;
32use std::{
33 path::{Path, PathBuf},
34 time::Duration,
35};
36
37pub(super) mod files;
38
39const BUSY_TIMEOUT: Duration = Duration::from_secs(5);
40const BUSY_RETRY_DELAY: Duration = Duration::from_millis(100);
41const BUSY_RETRIES: u32 = 50;
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45pub struct IntegrityReport {
46 pub path: PathBuf,
48 pub schema_version: i64,
50 pub workspace_id: WorkspaceId,
52 pub workspace_name: String,
54 pub lineage_id: LineageId,
56 pub archived: bool,
58 pub revision: u64,
60 pub operation_count: u64,
62 pub genesis_revision: u64,
64 #[serde(skip_serializing_if = "Option::is_none")]
68 pub runs: Option<RunStoreReport>,
69}
70
71impl SqliteStore {
72 pub fn backup_blocking(&self, to: &Path) -> Result<IntegrityReport, StoreError> {
78 let to = target_path_blocking(to)?;
79 let runs_to = RunStore::sidecar_path(&to);
80 refuse_existing_blocking(&runs_to)?;
81 let runs_from = RunStore::sidecar_path(&self.path);
82 let has_runs = nonempty_blocking(&runs_from)?;
83 create_target_blocking(&to)?;
84 if has_runs {
85 runs::recovery::create_target(&runs_to).inspect_err(|_| discard_blocking(&to))?;
86 }
87 let result = self
88 .copy_runs_blocking(&runs_from, &runs_to, has_runs)
89 .and_then(|()| {
90 copy_blocking(&self.connection, &to, "delete", |target| {
92 lineage::seal_blocking(target, &to, Seal::Archive)
93 })
94 })
95 .and_then(|()| verify_store_blocking(&to));
96 discard_all_on_error_blocking(&to, has_runs, result)
97 }
98
99 fn copy_runs_blocking(&self, from: &Path, to: &Path, has_runs: bool) -> Result<(), StoreError> {
107 if !has_runs {
108 return Ok(());
109 }
110 let workspace = self
111 .load_blocking()?
112 .ok_or_else(|| StoreError::NotInitialized(self.path.clone()))?
113 .workspace
114 .id;
115 RunStore::open_existing_blocking(from, workspace)
116 .map_err(run_store_error(from))?
117 .ok_or_else(|| missing_runs(from))?
118 .backup_to_blocking(to)
119 .map_err(run_store_error(to))
120 .map(drop)
121 }
122}
123
124pub fn restore_store_blocking(from: &Path, to: &Path) -> Result<IntegrityReport, StoreError> {
129 let from = canonical_blocking(from)?;
130 let to = target_path_blocking(to)?;
131 let source_report = verify_store_blocking(&from)?;
132 if !source_report.archived {
133 return Err(crate::LineageError::NotAnArchive { path: from }.into());
134 }
135 let runs_from = RunStore::sidecar_path(&from);
136 let runs_to = RunStore::sidecar_path(&to);
137 refuse_existing_blocking(&runs_to)?;
138 let has_runs = nonempty_blocking(&runs_from)?;
139 let mode = read_mode_blocking(&from)?;
140 let before = fingerprint_blocking(&from)?;
141 let source = open_read_only_blocking(&from, mode)?;
142 create_target_blocking(&to)?;
143 if has_runs {
144 runs::recovery::create_target(&runs_to).inspect_err(|_| discard_blocking(&to))?;
145 }
146 let fork = LineageId::new();
147 let result = copy_blocking(&source, &to, "wal", |target| {
148 lineage::seal_blocking(target, &to, Seal::Fork(fork))
149 })
150 .and_then(|()| {
151 if mode == ReadMode::Immutable && fingerprint_blocking(&from)? != before {
154 return Err(StoreError::BackupIncomplete {
155 path: to.clone(),
156 state: format!("{} changed during the restore", from.display()),
157 });
158 }
159 Ok(())
160 })
161 .and_then(|()| {
162 if has_runs {
163 runs::recovery::restore_blocking(
164 &runs_from,
165 &runs_to,
166 source_report.workspace_id,
167 fork,
168 source_report.revision,
169 )
170 .map_err(run_store_error(&runs_to))?;
171 }
172 verify_store_blocking(&to)
173 });
174 discard_all_on_error_blocking(&to, has_runs, result)
175}
176
177pub fn verify_store_blocking(path: &Path) -> Result<IntegrityReport, StoreError> {
187 let path = canonical_blocking(path)?;
188 let runs_path = RunStore::sidecar_path(&path);
189 if !nonempty_blocking(&runs_path)? {
190 return verify_project_blocking(&path, None);
191 }
192 let (runs, pair) = runs::recovery::verify_with_facts_blocking(&runs_path)
193 .map_err(run_store_error(&runs_path))?;
194 let report = verify_project_blocking(&path, Some(&pair))?;
195 Ok(IntegrityReport {
196 runs: Some(runs),
197 ..report
198 })
199}
200
201fn verify_project_blocking(
204 path: &Path,
205 pair: Option<&runs::pairing::PairFacts>,
206) -> Result<IntegrityReport, StoreError> {
207 let path = canonical_blocking(path)?;
208 if read_mode_blocking(&path)? == ReadMode::Immutable {
209 let before = fingerprint_blocking(&path)?;
210 let result = verify_with_blocking(&path, ReadMode::Immutable, pair);
211 if fingerprint_blocking(&path)? == before {
213 return result;
214 }
215 }
216 verify_with_blocking(&path, ReadMode::Shared, pair)
217}
218
219fn verify_with_blocking(
220 path: &Path,
221 mode: ReadMode,
222 pair: Option<&runs::pairing::PairFacts>,
223) -> Result<IntegrityReport, StoreError> {
224 let mut connection = open_read_only_blocking(path, mode)?;
225 let transaction = connection
227 .transaction()
228 .map_err(database_error(path, "begin verification"))?;
229 let schema_version = schema::stored_version_blocking(&transaction, path)?;
230 let layout = schema::check_blocking(&transaction, path)?;
231 if layout == Layout::Empty {
232 return Err(StoreError::UnrecognizedSchema {
233 path: path.to_path_buf(),
234 detail: "no DPM tables".into(),
235 });
236 }
237 integrity_check_blocking(&transaction, path)?;
238 let plan = load_blocking(&transaction, path)?
239 .ok_or_else(|| StoreError::NotInitialized(path.to_path_buf()))?;
240 let genesis =
241 genesis_blocking(&transaction, path)?.ok_or_else(|| StoreError::MissingGenesis {
242 path: path.to_path_buf(),
243 })?;
244 let genesis_revision = genesis.revision;
245 let store_lineage = lineage::read_blocking(&transaction, path)?;
246 let history = check_history_blocking(
247 &transaction,
248 path,
249 genesis.workspace.id,
250 (genesis_revision, plan.revision),
251 )?;
252 replay_blocking(&transaction, path, genesis, &plan)?;
253 if let Some(pair) = pair {
254 let identity = runs::pairing::ProjectIdentity {
255 workspace: plan.workspace.id,
256 lineage: store_lineage.lineage_id,
257 snapshot_revision: plan.revision,
258 genesis_revision,
259 };
260 runs::pairing::cross_check_blocking(&transaction, path, &identity, pair)
261 .map_err(run_store_error(&RunStore::sidecar_path(path)))?;
262 }
263 Ok(IntegrityReport {
264 path: path.to_path_buf(),
265 schema_version,
266 workspace_id: plan.workspace.id,
267 workspace_name: plan.workspace.name,
268 lineage_id: store_lineage.lineage_id,
269 archived: store_lineage.archived,
270 revision: plan.revision,
271 operation_count: history.count,
272 genesis_revision,
273 runs: None,
274 })
275}
276
277pub(in crate::sqlite) fn copy_blocking(
280 source: &Connection,
281 to: &Path,
282 journal_mode: &str,
283 seal: impl FnOnce(&Connection) -> Result<(), StoreError>,
284) -> Result<(), StoreError> {
285 let mut target = Connection::open(to).map_err(database_error(to, "open backup target"))?;
286 target
287 .busy_timeout(BUSY_TIMEOUT)
288 .map_err(database_error(to, "set busy timeout"))?;
289 {
290 let backup =
291 Backup::new(source, &mut target).map_err(database_error(to, "start backup"))?;
292 let mut retries = 0;
293 loop {
294 match backup.step(-1).map_err(database_error(to, "copy pages"))? {
296 StepResult::Done => break,
297 StepResult::Busy | StepResult::Locked if retries < BUSY_RETRIES => {
298 retries += 1;
299 std::thread::sleep(BUSY_RETRY_DELAY);
300 }
301 state => {
302 return Err(StoreError::BackupIncomplete {
303 path: to.to_path_buf(),
304 state: format!("{state:?}"),
305 });
306 }
307 }
308 }
309 }
310 seal(&target)?;
312 let mode: String = target
313 .query_row(
314 &format!("PRAGMA journal_mode = {journal_mode}"),
315 [],
316 |row| row.get(0),
317 )
318 .map_err(database_error(to, "set journal mode"))?;
319 if !mode.eq_ignore_ascii_case(journal_mode) {
320 return Err(StoreError::BackupIncomplete {
321 path: to.to_path_buf(),
322 state: format!("journal mode {mode} instead of {journal_mode}"),
323 });
324 }
325 target
326 .close()
327 .map_err(|(_, source)| database_error(to, "close backup target")(source))
328}
329
330fn discard_all_on_error_blocking(
332 to: &Path,
333 with_runs: bool,
334 result: Result<IntegrityReport, StoreError>,
335) -> Result<IntegrityReport, StoreError> {
336 if result.is_err() {
337 discard_blocking(to);
338 if with_runs {
339 runs::recovery::discard(&RunStore::sidecar_path(to));
340 }
341 }
342 result
343}
344
345fn nonempty_blocking(path: &Path) -> Result<bool, StoreError> {
348 match std::fs::metadata(path) {
349 Ok(metadata) => Ok(metadata.len() > 0),
350 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(false),
351 Err(source) => Err(StoreError::Io {
352 action: "inspect",
353 path: path.to_path_buf(),
354 source,
355 }),
356 }
357}
358
359fn refuse_existing_blocking(path: &Path) -> Result<(), StoreError> {
361 let exists = path.try_exists().map_err(|source| StoreError::Io {
362 action: "inspect",
363 path: path.to_path_buf(),
364 source,
365 })?;
366 if exists {
367 return Err(StoreError::TargetExists {
368 path: path.to_path_buf(),
369 });
370 }
371 Ok(())
372}
373
374fn run_store_error(path: &Path) -> impl FnOnce(crate::RunStoreError) -> StoreError {
378 let path = path.to_path_buf();
379 move |source| match source {
380 crate::RunStoreError::Store(error) => error,
381 other => StoreError::IntegrityCheckFailed {
382 path,
383 problems: vec![other.to_string()],
384 },
385 }
386}
387
388fn missing_runs(path: &Path) -> StoreError {
389 run_store_error(path)(crate::RunStoreError::Corrupt {
390 path: path.to_path_buf(),
391 detail: "the run store disappeared while it was being backed up".into(),
392 })
393}
394
395#[cfg(test)]
396mod tests;