Skip to main content

appcore_sync_sqlite/
backup.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: backup.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/26 00:00:00 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/26 00:00:00 by dnettoRaw
8//      ###########      S: 2.0.0
9// =============================================================================
10
11//! Defines bounded backup contracts and behavior for this crate.
12
13use crate::store::{configure_memory, normalize_path};
14use crate::{
15    SqliteSyncError, SqliteSyncResult, SqliteSyncStore, SQLITE_SYNC_SCHEMA_V1,
16    SQLITE_SYNC_SCHEMA_V2, SQLITE_SYNC_SCHEMA_V3, SQLITE_SYNC_SCHEMA_V4, SQLITE_SYNC_SCHEMA_V5,
17};
18use rusqlite::backup::Backup;
19use rusqlite::{Connection, OpenFlags, TransactionBehavior};
20use sha2::{Digest, Sha256};
21use std::fs::{self, File, OpenOptions};
22use std::io::Read;
23use std::path::{Path, PathBuf};
24use std::time::Duration;
25
26/// Evidence returned after an integrity-checked online backup.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct SqliteBackupReport {
29    /// Internal schema version copied to the backup.
30    pub schema_version: u32,
31    /// Final backup file size in bytes.
32    pub bytes: u64,
33}
34
35/// Evidence returned after an integrity-checked backup of both attached databases.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct SqlitePairedBackupReport {
38    /// Backup evidence for complete local state.
39    pub local: SqliteBackupReport,
40    /// Backup evidence for the pending-only sync queue.
41    pub queue: SqliteBackupReport,
42    /// Number of unconfirmed operations captured in the same snapshot.
43    pub queue_entries: u64,
44    /// Combined size of the two SQLite files.
45    pub total_bytes: u64,
46}
47
48impl SqliteSyncStore {
49    /// Creates an integrity-checked online backup and atomically publishes it.
50    pub fn online_backup(
51        &self,
52        destination: impl AsRef<Path>,
53    ) -> SqliteSyncResult<SqliteBackupReport> {
54        if self.config().separate_outbox_path().is_some() {
55            return Err(SqliteSyncError::InvalidConfiguration(
56                "separate outbox requires a paired backup",
57            ));
58        }
59        let destination = normalize_backup_destination(destination.as_ref())?;
60        let temporary = reserve_temporary(&destination)?;
61        let result = self.copy_backup(&temporary).and_then(|report| {
62            sync_file(&temporary)?;
63            publish_temporary(&temporary, &destination)?;
64            Ok(report)
65        });
66        if result.is_err() {
67            let _ = fs::remove_file(&temporary);
68        }
69        result
70    }
71
72    /// Publishes a consistent bundle containing the local provider database and the
73    /// pending-only queue database. The destination directory must not exist.
74    pub fn paired_online_backup(
75        &self,
76        destination: impl AsRef<Path>,
77    ) -> SqliteSyncResult<SqlitePairedBackupReport> {
78        if self.config().separate_outbox_path().is_none() {
79            return Err(SqliteSyncError::InvalidConfiguration(
80                "paired backup requires a separate outbox",
81            ));
82        }
83        let destination = normalize_backup_destination(destination.as_ref())?;
84        let temporary = reserve_temporary_directory(&destination)?;
85        let local_name = safe_manifest_name(
86            self.config()
87                .path
88                .file_name()
89                .and_then(|name| name.to_str()),
90        )?;
91        let queue_name = safe_manifest_name(
92            self.config()
93                .separate_outbox_path()
94                .and_then(Path::file_name)
95                .and_then(|name| name.to_str()),
96        )?;
97        let result = self.with_connection(|connection| {
98            let transaction = connection
99                .transaction_with_behavior(TransactionBehavior::Exclusive)
100                .map_err(SqliteSyncError::database)?;
101            // Establish a read snapshot on both attached files before either copy.
102            let _: i64 = transaction
103                .query_row("SELECT count(*) FROM main.appcore_object_head", [], |row| row.get(0))
104                .map_err(SqliteSyncError::database)?;
105            let queue_entries: u64 = transaction
106                .query_row("SELECT count(*) FROM sync_queue.appcore_sync_outbox", [], |row| row.get(0))
107                .map_err(SqliteSyncError::database)?;
108            let local_path = temporary.join("local.sqlite3");
109            let queue_path = temporary.join("sync-queue.sqlite3");
110            fs::copy(self.config().path(), &local_path)
111                .map_err(|_| SqliteSyncError::DatabaseOperation)?;
112            fs::copy(
113                self.config().separate_outbox_path().ok_or(
114                    SqliteSyncError::InvalidConfiguration("separate outbox required"),
115                )?,
116                &queue_path,
117            )
118            .map_err(|_| SqliteSyncError::DatabaseOperation)?;
119            let local = validate_pair_database(&local_path, false)?;
120            let queue = validate_pair_database(&queue_path, true)?;
121            drop(transaction);
122            let manifest = format!(
123                "appcore-sync-pair-v1\nlocal_file={local_name}\nqueue_file={queue_name}\nlocal_schema={}\nlocal_bytes={}\nlocal_sha256={}\nqueue_schema={}\nqueue_bytes={}\nqueue_sha256={}\nqueue_entries={}\n",
124                local.schema_version, local.bytes, file_sha256(&local_path)?, queue.schema_version, queue.bytes, file_sha256(&queue_path)?, queue_entries
125            );
126            let manifest_path = temporary.join("manifest.txt");
127            fs::write(&manifest_path, manifest).map_err(|_| SqliteSyncError::DatabaseOperation)?;
128            sync_file(&local_path)?;
129            sync_file(&queue_path)?;
130            sync_file(&manifest_path)?;
131            sync_parent(&local_path)?;
132            let total_bytes = local.bytes.saturating_add(queue.bytes);
133            Ok(SqlitePairedBackupReport { local, queue, queue_entries, total_bytes })
134        });
135        let result = result.and_then(|report| {
136            publish_directory(&temporary, &destination)?;
137            Ok(report)
138        });
139        if result.is_err() {
140            let _ = fs::remove_dir_all(&temporary);
141        }
142        result
143    }
144
145    /// Restores a validated paired bundle into a new directory and publishes it atomically.
146    pub fn restore_paired_backup_to_new(
147        bundle: impl AsRef<Path>,
148        destination_directory: impl AsRef<Path>,
149    ) -> SqliteSyncResult<SqlitePairedBackupReport> {
150        let bundle = normalize_bundle_directory(bundle.as_ref())?;
151        let report = validate_paired_bundle(&bundle)?;
152        let destination = normalize_backup_destination(destination_directory.as_ref())?;
153        let temporary = reserve_temporary_directory(&destination)?;
154        let result = (|| {
155            let manifest = fs::read_to_string(bundle.join("manifest.txt"))
156                .map_err(|_| SqliteSyncError::IntegrityFailed)?;
157            let (local_name, queue_name) = manifest_names(&manifest)?;
158            for (source, name) in [
159                ("local.sqlite3", local_name),
160                ("sync-queue.sqlite3", queue_name),
161            ] {
162                fs::copy(bundle.join(source), temporary.join(&name))
163                    .map_err(|_| SqliteSyncError::DatabaseOperation)?;
164                sync_file(&temporary.join(name))?;
165            }
166            fs::copy(bundle.join("manifest.txt"), temporary.join("manifest.txt"))
167                .map_err(|_| SqliteSyncError::DatabaseOperation)?;
168            sync_file(&temporary.join("manifest.txt"))?;
169            validate_paired_bundle(&temporary)?;
170            sync_parent(&temporary.join("local.sqlite3"))?;
171            publish_directory(&temporary, &destination)?;
172            Ok(report)
173        })();
174        if result.is_err() {
175            let _ = fs::remove_dir_all(&temporary);
176        }
177        result
178    }
179
180    fn copy_backup(&self, temporary: &Path) -> SqliteSyncResult<SqliteBackupReport> {
181        self.with_connection(|source| {
182            copy_connection(source, temporary, self.config().backup_pages_per_step)
183        })
184    }
185
186    /// Restores a verified backup into a new, previously absent database path.
187    pub fn restore_backup_to_new(
188        backup_path: impl AsRef<Path>,
189        destination: impl AsRef<Path>,
190    ) -> SqliteSyncResult<SqliteBackupReport> {
191        let original_backup_path = backup_path.as_ref();
192        let backup_path = normalize_path(original_backup_path)?;
193        if !backup_path.is_file() {
194            return Err(SqliteSyncError::UnsafePath);
195        }
196        let destination = normalize_backup_destination(destination.as_ref())?;
197        let source = Connection::open_with_flags(
198            &backup_path,
199            OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_FULL_MUTEX,
200        )
201        .map_err(SqliteSyncError::database)?;
202        configure_memory(&source)?;
203        validate_backup_connection(&source)?;
204        let temporary = reserve_temporary(&destination)?;
205        let result = copy_connection(&source, &temporary, 128).and_then(|report| {
206            sync_file(&temporary)?;
207            publish_temporary(&temporary, &destination)?;
208            Ok(report)
209        });
210        if result.is_err() {
211            let _ = fs::remove_file(&temporary);
212        }
213        result
214    }
215}
216
217fn copy_connection(
218    source: &Connection,
219    temporary: &Path,
220    pages_per_step: i32,
221) -> SqliteSyncResult<SqliteBackupReport> {
222    let flags = OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_FULL_MUTEX;
223    let mut destination =
224        Connection::open_with_flags(temporary, flags).map_err(SqliteSyncError::database)?;
225    configure_memory(&destination)?;
226    let backup = Backup::new(source, &mut destination).map_err(SqliteSyncError::database)?;
227    backup
228        .run_to_completion(pages_per_step, Duration::from_millis(2), None)
229        .map_err(SqliteSyncError::database)?;
230    drop(backup);
231    let schema_version = validate_backup_connection(&destination)?;
232    drop(destination);
233    let bytes = fs::metadata(temporary)
234        .map_err(|_| SqliteSyncError::DatabaseOperation)?
235        .len();
236    Ok(SqliteBackupReport {
237        schema_version,
238        bytes,
239    })
240}
241
242fn reserve_temporary_directory(destination: &Path) -> SqliteSyncResult<PathBuf> {
243    let parent = destination.parent().unwrap_or_else(|| Path::new("."));
244    let name = destination
245        .file_name()
246        .and_then(|value| value.to_str())
247        .ok_or(SqliteSyncError::UnsafePath)?;
248    for attempt in 0..16u8 {
249        let candidate = parent.join(format!(".{name}.{}-{attempt}.tmp", std::process::id()));
250        match fs::create_dir(&candidate) {
251            Ok(()) => return Ok(candidate),
252            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
253            Err(_) => return Err(SqliteSyncError::DatabaseOperation),
254        }
255    }
256    Err(SqliteSyncError::CapacityExceeded("backup temporary"))
257}
258
259fn publish_directory(temporary: &Path, destination: &Path) -> SqliteSyncResult<()> {
260    fs::rename(temporary, destination).map_err(|_| SqliteSyncError::DatabaseOperation)?;
261    if sync_parent(destination).is_err() {
262        let _ = fs::rename(destination, temporary);
263        return Err(SqliteSyncError::DatabaseOperation);
264    }
265    Ok(())
266}
267
268fn validate_paired_bundle(bundle: &Path) -> SqliteSyncResult<SqlitePairedBackupReport> {
269    let manifest_path = bundle.join("manifest.txt");
270    let manifest =
271        fs::read_to_string(&manifest_path).map_err(|_| SqliteSyncError::IntegrityFailed)?;
272    let (local_name, queue_name) = manifest_names(&manifest)?;
273    let local_path = if bundle.join("local.sqlite3").is_file() {
274        bundle.join("local.sqlite3")
275    } else {
276        bundle.join(&local_name)
277    };
278    let queue_path = if bundle.join("sync-queue.sqlite3").is_file() {
279        bundle.join("sync-queue.sqlite3")
280    } else {
281        bundle.join(&queue_name)
282    };
283    let local = validate_pair_database(&local_path, false)?;
284    let queue = validate_pair_database(&queue_path, true)?;
285    let local_connection = Connection::open_with_flags(
286        &local_path,
287        OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_FULL_MUTEX,
288    )
289    .map_err(SqliteSyncError::database)?;
290    let recorded_queue_name: String = local_connection
291        .query_row(
292            "SELECT outbox_file FROM appcore_sync_storage_mode WHERE singleton=1",
293            [],
294            |row| row.get(0),
295        )
296        .map_err(SqliteSyncError::database)?;
297    if recorded_queue_name != queue_name {
298        return Err(SqliteSyncError::IntegrityFailed);
299    }
300    let connection = Connection::open_with_flags(
301        &queue_path,
302        OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_FULL_MUTEX,
303    )
304    .map_err(SqliteSyncError::database)?;
305    let queue_entries: u64 = connection
306        .query_row("SELECT count(*) FROM appcore_sync_outbox", [], |row| {
307            row.get(0)
308        })
309        .map_err(SqliteSyncError::database)?;
310    let expected = format!(
311        "appcore-sync-pair-v1\nlocal_file={local_name}\nqueue_file={queue_name}\nlocal_schema={}\nlocal_bytes={}\nlocal_sha256={}\nqueue_schema={}\nqueue_bytes={}\nqueue_sha256={}\nqueue_entries={}\n",
312        local.schema_version, local.bytes, file_sha256(&local_path)?, queue.schema_version, queue.bytes, file_sha256(&queue_path)?, queue_entries
313    );
314    if manifest != expected {
315        return Err(SqliteSyncError::IntegrityFailed);
316    }
317    Ok(SqlitePairedBackupReport {
318        total_bytes: local.bytes.saturating_add(queue.bytes),
319        local,
320        queue,
321        queue_entries,
322    })
323}
324
325fn manifest_names(manifest: &str) -> SqliteSyncResult<(String, String)> {
326    let mut lines = manifest.lines();
327    if lines.next() != Some("appcore-sync-pair-v1") {
328        return Err(SqliteSyncError::UpdateRequired);
329    }
330    let mut local = None;
331    let mut queue = None;
332    for line in lines {
333        if let Some(value) = line.strip_prefix("local_file=") {
334            local = Some(safe_manifest_name(Some(value))?);
335        }
336        if let Some(value) = line.strip_prefix("queue_file=") {
337            queue = Some(safe_manifest_name(Some(value))?);
338        }
339    }
340    Ok((
341        local.ok_or(SqliteSyncError::IntegrityFailed)?,
342        queue.ok_or(SqliteSyncError::IntegrityFailed)?,
343    ))
344}
345
346fn safe_manifest_name(value: Option<&str>) -> SqliteSyncResult<String> {
347    let value = value.ok_or(SqliteSyncError::UnsafePath)?;
348    if value.is_empty()
349        || value == "."
350        || value == ".."
351        || value.contains('/')
352        || value.contains('\\')
353        || value.bytes().any(|byte| byte.is_ascii_control())
354    {
355        return Err(SqliteSyncError::UnsafePath);
356    }
357    Ok(value.to_owned())
358}
359
360fn validate_pair_database(path: &Path, queue: bool) -> SqliteSyncResult<SqliteBackupReport> {
361    if !path.is_file() {
362        return Err(SqliteSyncError::UnsafePath);
363    }
364    let connection = Connection::open_with_flags(
365        path,
366        OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_FULL_MUTEX,
367    )
368    .map_err(SqliteSyncError::database)?;
369    let schema_version: u32 = connection
370        .pragma_query_value(None, "user_version", |row| row.get(0))
371        .map_err(SqliteSyncError::database)?;
372    let application_id: i64 = connection
373        .pragma_query_value(None, "application_id", |row| row.get(0))
374        .map_err(SqliteSyncError::database)?;
375    let expected_id = if queue { 0x4150_4351 } else { 0x4150_4353 };
376    if application_id != expected_id {
377        return Err(SqliteSyncError::UpdateRequired);
378    }
379    if queue {
380        let integrity: String = connection
381            .query_row("PRAGMA quick_check(1)", [], |row| row.get(0))
382            .map_err(SqliteSyncError::database)?;
383        if integrity != "ok" {
384            return Err(SqliteSyncError::IntegrityFailed);
385        }
386    } else if validate_backup_connection(&connection).is_err()
387        && !validate_local_backup_connection(&connection)?
388    {
389        return Err(SqliteSyncError::IntegrityFailed);
390    }
391    if queue {
392        let has_queue: bool = connection
393            .query_row("SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE type='table' AND name='appcore_sync_outbox')", [], |row| row.get(0))
394            .map_err(SqliteSyncError::database)?;
395        if !has_queue || schema_version != 1 {
396            return Err(SqliteSyncError::UpdateRequired);
397        }
398    }
399    let bytes = fs::metadata(path)
400        .map_err(|_| SqliteSyncError::DatabaseOperation)?
401        .len();
402    Ok(SqliteBackupReport {
403        schema_version,
404        bytes,
405    })
406}
407
408fn validate_local_backup_connection(connection: &Connection) -> SqliteSyncResult<bool> {
409    let version: u32 = connection
410        .pragma_query_value(None, "user_version", |row| row.get(0))
411        .map_err(SqliteSyncError::database)?;
412    if !(SQLITE_SYNC_SCHEMA_V1..=SQLITE_SYNC_SCHEMA_V5).contains(&version) {
413        return Ok(false);
414    }
415    let integrity: String = connection
416        .query_row("PRAGMA quick_check(1)", [], |row| row.get(0))
417        .map_err(SqliteSyncError::database)?;
418    let mode: String = connection
419        .query_row(
420            "SELECT outbox_mode FROM appcore_sync_storage_mode WHERE singleton=1",
421            [],
422            |row| row.get(0),
423        )
424        .map_err(SqliteSyncError::database)?;
425    Ok(integrity == "ok" && mode == "separate")
426}
427
428fn file_sha256(path: &Path) -> SqliteSyncResult<String> {
429    let mut file = File::open(path).map_err(|_| SqliteSyncError::DatabaseOperation)?;
430    let mut hasher = Sha256::new();
431    let mut buffer = [0u8; 64 * 1024];
432    loop {
433        let read = file
434            .read(&mut buffer)
435            .map_err(|_| SqliteSyncError::DatabaseOperation)?;
436        if read == 0 {
437            break;
438        }
439        hasher.update(&buffer[..read]);
440    }
441    Ok(hasher
442        .finalize()
443        .iter()
444        .map(|byte| format!("{byte:02x}"))
445        .collect())
446}
447
448fn normalize_backup_destination(path: &Path) -> SqliteSyncResult<PathBuf> {
449    if path.as_os_str().is_empty() {
450        return Err(SqliteSyncError::UnsafePath);
451    }
452    let normalized = normalize_path(path)?;
453    if normalized.exists() {
454        return Err(SqliteSyncError::UnsafePath);
455    }
456    Ok(normalized)
457}
458
459fn normalize_bundle_directory(path: &Path) -> SqliteSyncResult<PathBuf> {
460    let metadata = fs::symlink_metadata(path).map_err(|_| SqliteSyncError::UnsafePath)?;
461    if !metadata.is_dir() || metadata.file_type().is_symlink() {
462        return Err(SqliteSyncError::UnsafePath);
463    }
464    fs::canonicalize(path).map_err(|_| SqliteSyncError::UnsafePath)
465}
466
467fn reserve_temporary(destination: &Path) -> SqliteSyncResult<PathBuf> {
468    let parent = destination.parent().unwrap_or_else(|| Path::new("."));
469    let name = destination
470        .file_name()
471        .and_then(|value| value.to_str())
472        .ok_or(SqliteSyncError::UnsafePath)?;
473    for attempt in 0..16u8 {
474        let candidate = parent.join(format!(".{name}.{}-{attempt}.tmp", std::process::id()));
475        match OpenOptions::new()
476            .write(true)
477            .create_new(true)
478            .open(&candidate)
479        {
480            Ok(file) => {
481                drop(file);
482                return Ok(candidate);
483            }
484            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
485            Err(_) => return Err(SqliteSyncError::DatabaseOperation),
486        }
487    }
488    Err(SqliteSyncError::CapacityExceeded("backup temporary"))
489}
490
491fn publish_temporary(temporary: &Path, destination: &Path) -> SqliteSyncResult<()> {
492    fs::hard_link(temporary, destination).map_err(|_| SqliteSyncError::DatabaseOperation)?;
493    if fs::remove_file(temporary).is_err() {
494        let _ = fs::remove_file(destination);
495        return Err(SqliteSyncError::DatabaseOperation);
496    }
497    if sync_parent(destination).is_err() {
498        let _ = fs::remove_file(destination);
499        let _ = sync_parent(destination);
500        return Err(SqliteSyncError::DatabaseOperation);
501    }
502    Ok(())
503}
504
505fn validate_backup_connection(connection: &Connection) -> SqliteSyncResult<u32> {
506    let version: u32 = connection
507        .pragma_query_value(None, "user_version", |row| row.get(0))
508        .map_err(SqliteSyncError::database)?;
509    if version >= SQLITE_SYNC_SCHEMA_V5 {
510        let mode: String = connection
511            .query_row(
512                "SELECT outbox_mode FROM appcore_sync_storage_mode WHERE singleton=1",
513                [],
514                |row| row.get(0),
515            )
516            .map_err(SqliteSyncError::database)?;
517        if mode != "local" {
518            return Err(SqliteSyncError::UpdateRequired);
519        }
520    }
521    let application_id: i64 = connection
522        .pragma_query_value(None, "application_id", |row| row.get(0))
523        .map_err(SqliteSyncError::database)?;
524    if application_id != 0x4150_4353 {
525        return Err(SqliteSyncError::UpdateRequired);
526    }
527    let integrity: String = connection
528        .query_row("PRAGMA quick_check(1)", [], |row| row.get(0))
529        .map_err(SqliteSyncError::database)?;
530    if matches!(
531        version,
532        SQLITE_SYNC_SCHEMA_V1
533            | SQLITE_SYNC_SCHEMA_V2
534            | SQLITE_SYNC_SCHEMA_V3
535            | SQLITE_SYNC_SCHEMA_V4
536            | SQLITE_SYNC_SCHEMA_V5
537    ) && integrity == "ok"
538    {
539        Ok(version)
540    } else if !matches!(
541        version,
542        SQLITE_SYNC_SCHEMA_V1
543            | SQLITE_SYNC_SCHEMA_V2
544            | SQLITE_SYNC_SCHEMA_V3
545            | SQLITE_SYNC_SCHEMA_V4
546            | SQLITE_SYNC_SCHEMA_V5
547    ) {
548        Err(SqliteSyncError::UpdateRequired)
549    } else {
550        Err(SqliteSyncError::IntegrityFailed)
551    }
552}
553
554fn sync_file(path: &Path) -> SqliteSyncResult<()> {
555    let mut options = OpenOptions::new();
556    options.read(true);
557    #[cfg(windows)]
558    options.write(true);
559    options
560        .open(path)
561        .and_then(|file| file.sync_all())
562        .map_err(|_| SqliteSyncError::DatabaseOperation)
563}
564
565#[cfg(unix)]
566fn sync_parent(path: &Path) -> SqliteSyncResult<()> {
567    let parent = path.parent().unwrap_or_else(|| Path::new("."));
568    File::open(parent)
569        .and_then(|directory| directory.sync_all())
570        .map_err(|_| SqliteSyncError::DatabaseOperation)
571}
572
573#[cfg(windows)]
574fn sync_parent(path: &Path) -> SqliteSyncResult<()> {
575    use std::os::windows::fs::OpenOptionsExt;
576    let parent = path.parent().ok_or(SqliteSyncError::UnsafePath)?;
577    OpenOptions::new()
578        .read(true)
579        .write(true)
580        .custom_flags(windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS)
581        .open(parent)
582        .and_then(|directory| directory.sync_all())
583        .map_err(|_| SqliteSyncError::DatabaseOperation)
584}
585#[cfg(not(any(unix, windows)))]
586fn sync_parent(_path: &Path) -> SqliteSyncResult<()> {
587    Err(SqliteSyncError::DatabaseOperation)
588}