Skip to main content

rightkit_db/
lib.rs

1// Unsafe is confined to the host-name FFI in `host`.
2#![deny(unsafe_code)]
3
4#[cfg(all(feature = "sqlcipher", feature = "sqlcipher-system"))]
5compile_error!("rightkit-db features `sqlcipher` and `sqlcipher-system` are mutually exclusive; enable exactly one");
6
7use rusqlite::{Connection, OpenFlags, Transaction, TransactionBehavior};
8use std::ffi::OsString;
9use std::ops::Deref;
10use std::path::{Path, PathBuf};
11use std::sync::{Arc, Condvar, Mutex, MutexGuard, Weak};
12use std::time::{Duration, Instant};
13/// The zeroizing wrapper that the SQLCipher key fields and key candidates use, re-exported so
14/// callers build keys without declaring their own `zeroize` dependency.
15pub use zeroize::Zeroizing;
16
17mod backup;
18mod checkpoint_job;
19#[allow(unsafe_code)]
20mod host;
21pub use host::per_host_path;
22/// The exact `rusqlite` this crate links (`links = "sqlite3"` allows one per build). Apps name
23/// `Connection`, `params!`, `Row`, `OptionalExtension`, `TransactionBehavior` through this path
24/// instead of declaring their own `rusqlite` dependency.
25pub use rusqlite;
26mod migrate;
27mod recover;
28mod restore;
29mod unreadable;
30
31#[cfg(all(feature = "encrypted-lifecycle", not(feature = "cipher")))]
32compile_error!("rightkit-db feature `encrypted-lifecycle` needs a SQLCipher backend; also enable `sqlcipher` or `sqlcipher-system`");
33
34/// The encrypted-store lifecycle: key selection, rotation, recovery, restore and relocation behind
35/// one cross-process guard. Needs `encrypted-lifecycle` and a SQLCipher backend.
36#[cfg(all(feature = "encrypted-lifecycle", feature = "cipher"))]
37pub mod encrypted_lifecycle;
38
39pub use backup::{
40    online_backup_to, remove_database_files, replace_database, replace_database_locked,
41    restore_from, store_lock_path, BackupProgress, StoreLock,
42};
43pub use checkpoint_job::{
44    WalCheckpointJob, WalCheckpointMode, WalCheckpointPolicy, WalCheckpointRun,
45};
46pub use migrate::{
47    add_column_if_missing, column_exists, migrate, migration_version, pending_marker,
48    pending_marker_started_at, rollback_to, rollback_to_with_storage, stored_version_is_newer,
49    user_version, write_pending_marker, Degraded, LegacyVersionTable, MigrateOptions, Migration,
50    MigrationReport, OpenOrDegrade, PendingMarker, Step, VersionStorage,
51};
52#[cfg(feature = "cipher")]
53pub use recover::rekey;
54pub use recover::{
55    quarantine_database, quarantine_database_opts, quarantine_database_with, MoveMode, OpenOutcome,
56    QuarantineOptions, QuarantineStamp, RecoveryPolicy, SidecarPolicy,
57};
58
59pub use restore::{
60    recover_interrupted_restore, recover_interrupted_restore_locked, restore_database,
61    restore_database_locked, InterruptedRestore, RestoreOptions, RestoreReceipt, RestoreVerify,
62};
63
64pub use unreadable::{
65    classify_open_failure, unreadable_counter_path, RecoveryAction, StorageProblem,
66    UnreadableCause, UnreadablePolicy, UnreadableRecovery,
67};
68
69const BUSY_TIMEOUT_MS: u64 = 5_000;
70const JOURNAL_SIZE_LIMIT: i64 = 67_108_864;
71
72pub type Result<T> = std::result::Result<T, DbError>;
73
74#[derive(Debug, thiserror::Error)]
75#[non_exhaustive]
76pub enum DbError {
77    #[error("SQLite error: {0}")]
78    Sqlite(#[from] rusqlite::Error),
79    #[error("a SQLCipher key requires the rightkit-db `sqlcipher` or `sqlcipher-system` feature")]
80    SqlcipherFeatureRequired,
81    #[error("SQLCipher key contains characters that are unsafe for PRAGMA interpolation")]
82    InvalidSqlcipherKey,
83    /// An open-time `quick_check` or `integrity_check` failed. Open-time recovery treats it as an
84    /// unreadable database (see `DbOptions::open_writer_recovering`).
85    #[error("database integrity check failed: {0}")]
86    Integrity(String),
87    #[error("quarantine suffix {0:?} must be non-empty and contain no path separator or NUL")]
88    InvalidQuarantineSuffix(String),
89    #[error("read-pool capacity must be greater than zero")]
90    InvalidPoolCapacity,
91    #[error("reader cache size must be between 1 and 2147483647 KiB, got {0}")]
92    InvalidReaderCacheSize(u32),
93    /// Checkout after [`ReadPool::close`] or [`ReadPool::close_and_wait`].
94    #[error("read pool is closed")]
95    ReadPoolClosed,
96    /// [`ReadPool::close_and_wait`] timed out. `outstanding` counts readers still checked out or
97    /// opening.
98    #[error("read pool close timed out after {timeout:?} with {outstanding} reader(s) still out")]
99    ReadPoolCloseTimeout {
100        outstanding: usize,
101        timeout: Duration,
102    },
103    /// A reader did not come up read-only: `PRAGMA query_only` did not stay on, or a read-only
104    /// handle reported a writable main database.
105    #[error("reader connection is not read-only: {0}")]
106    ReadOnlyNotEffective(String),
107    #[error("database path has no file name")]
108    InvalidDatabasePath,
109    #[error("database is at schema v{current}, newer than this build understands (v{max}) and not forward compatible")]
110    NewerDatabase { current: i64, max: i64 },
111    #[error("invalid migration list: {0}")]
112    InvalidMigrations(String),
113    #[error("migration v{version} ({name}) failed and was rolled back: {source}")]
114    MigrationFailed {
115        version: i64,
116        name: &'static str,
117        #[source]
118        source: rusqlite::Error,
119    },
120    #[error("cannot roll back: {0}")]
121    Rollback(String),
122    #[error("database backup failed: {0}")]
123    Backup(String),
124    /// [`restore_database`] failed. `previous` is where the previous store is kept when it is not
125    /// at the live path (never deleted); `failed_copy` is where the restored copy was set aside.
126    /// `rolled_back` is true when the live path holds the previous store again (or was never
127    /// touched); false means the rollback was incomplete, a restore journal remains, and
128    /// [`recover_interrupted_restore`] finishes it.
129    #[error("database restore failed: {message}")]
130    RestoreFailed {
131        message: String,
132        previous: Option<PathBuf>,
133        failed_copy: Option<PathBuf>,
134        rolled_back: bool,
135    },
136    #[error("no candidate key opened the database")]
137    NoKeyOpened,
138    #[error(transparent)]
139    Fs(#[from] rightkit_fs::FsError),
140    /// The database file could not be read (permission or I/O failure) or its storage is
141    /// unavailable. Not corruption: the file was not touched unless `quarantine_after` was
142    /// reached, in which case recovery succeeds instead of returning this. See
143    /// [`UnreadablePolicy`].
144    #[error(
145        "database {} cannot be read: {cause} ({consecutive_failures} consecutive failed open(s)): {source}",
146        .path.display()
147    )]
148    Unreadable {
149        path: PathBuf,
150        cause: UnreadableCause,
151        /// Consecutive failed opens recorded so far (0 under [`UnreadablePolicy::Fail`] and for
152        /// storage problems, which are never counted).
153        consecutive_failures: u32,
154        /// The count at which [`UnreadablePolicy::QuarantineAfter`] would set the file aside.
155        quarantine_after: Option<u32>,
156        #[source]
157        source: Box<DbError>,
158    },
159}
160
161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub enum CacheProfile {
163    Default,
164    LargeWorkingSet,
165}
166
167#[derive(Clone, Copy, Debug, Eq, PartialEq)]
168pub enum DurabilityProfile {
169    Full,
170    Normal,
171}
172
173/// How the journal mode is chosen. WAL needs working shared-memory locking, which network
174/// filesystems often lack and which SQLite will silently downgrade; the default therefore
175/// *verifies* the mode that took effect and falls back to a rollback journal.
176#[derive(Clone, Copy, Debug, Eq, PartialEq)]
177#[non_exhaustive]
178pub enum JournalSelect {
179    /// WAL when the filesystem honors it and is not a detected network mount; else a
180    /// rollback journal (TRUNCATE with [`DbOptions::per_host_fallback`], else DELETE).
181    WalWithFallback,
182    /// WAL or fail (no fallback).
183    WalStrict,
184    /// Rollback journal, deleted after each transaction.
185    Delete,
186    /// Rollback journal, truncated after each transaction.
187    Truncate,
188}
189
190impl JournalSelect {
191    fn is_rollback(self) -> bool {
192        matches!(self, JournalSelect::Delete | JournalSelect::Truncate)
193    }
194}
195
196/// Environment kill-switch (`wal` | `delete` | `truncate`) for a misclassified mount in the
197/// field. An app's own variable ([`DbOptions::journal_env`]) is checked first.
198pub const JOURNAL_ENV: &str = "RIGHTKIT_SQLITE_JOURNAL_MODE";
199
200/// Post-key, pre-pragma per-connection initializer (register functions, collations, ...).
201/// Open-time integrity check, selected by [`DbOptions::integrity`].
202#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
203pub enum IntegrityCheck {
204    /// No check at open (default).
205    #[default]
206    Off,
207    /// `PRAGMA quick_check`: structural page checks, without the index-to-table cross-check. It
208    /// costs far less than `Full` on large databases.
209    Quick,
210    /// `PRAGMA integrity_check`, or `PRAGMA cipher_integrity_check` for a keyed database.
211    Full,
212}
213
214pub type ConnInit = Arc<dyn Fn(&Connection) -> rusqlite::Result<()> + Send + Sync>;
215
216#[derive(Clone)]
217pub struct DbOptions {
218    pub path: PathBuf,
219    /// SQLite busy wait, installed before key verification or any bootstrap statement.
220    /// Default: five seconds.
221    pub busy_timeout: Duration,
222    /// SQLCipher passphrase-style key (base64-ish charset). See also [`DbOptions::raw_key`].
223    pub key: Option<Zeroizing<String>>,
224    /// SQLCipher raw 256-bit key, applied as `x'<hex>'` (no KDF). Takes precedence over `key`.
225    pub raw_key: Option<Zeroizing<[u8; 32]>>,
226    pub profile: CacheProfile,
227    pub durability: DurabilityProfile,
228    pub foreign_keys: bool,
229    /// Legacy switch: `true` runs a full check on every open. Ignored when `integrity` is not `Off`.
230    pub integrity_check_on_open: bool,
231    /// Open-time integrity check (default `Off`). A failed check is [`DbError::Integrity`], which
232    /// `open_writer_recovering` treats as unreadable.
233    pub integrity: IntegrityCheck,
234    pub journal: JournalSelect,
235    /// Narrow the database and its sidecars to owner-only (0600) on unix. Default on. Set it to
236    /// `false` when sibling processes must read the file (for example a helper running as another
237    /// user); that is the intended switch, not a bug.
238    pub owner_only: bool,
239    pub init: Option<ConnInit>,
240    /// The app's own kill-switch variable (e.g. `CODERIGHT_SQLITE_JOURNAL_MODE`), checked
241    /// before [`JOURNAL_ENV`].
242    pub journal_env: Option<String>,
243    /// When a rollback journal is in force (network mount, kill switch, or WAL refused),
244    /// open the per-host sibling [`per_host_path`] instead of the requested file, so hosts
245    /// sharing a network directory never share one rollback-journaled database. The
246    /// requested file is not created. Default off.
247    pub per_host_fallback: bool,
248    /// Sets `PRAGMA secure_delete` on every connection at open: `Some(true)` overwrites freed
249    /// pages, `Some(false)` does not, `None` (default) leaves SQLite's compile-time default.
250    pub secure_delete: Option<bool>,
251    /// Run `PRAGMA optimize` on each writer connection at open (default `true`). Set it to `false`
252    /// to leave planner statistics alone at open, for example when the file is opened only to
253    /// inspect it. Readers never run it, whatever this is set to.
254    pub optimize_on_open: bool,
255}
256
257impl DbOptions {
258    pub fn new(path: impl Into<PathBuf>) -> Self {
259        Self {
260            path: path.into(),
261            busy_timeout: Duration::from_millis(BUSY_TIMEOUT_MS),
262            key: None,
263            raw_key: None,
264            profile: CacheProfile::Default,
265            durability: DurabilityProfile::Normal,
266            foreign_keys: false,
267            integrity_check_on_open: false,
268            integrity: IntegrityCheck::Off,
269            journal: JournalSelect::WalWithFallback,
270            owner_only: true,
271            init: None,
272            journal_env: None,
273            per_host_fallback: false,
274            secure_delete: None,
275            optimize_on_open: true,
276        }
277    }
278
279    pub fn with_journal_env(mut self, name: impl Into<String>) -> Self {
280        self.journal_env = Some(name.into());
281        self
282    }
283
284    pub fn with_per_host_fallback(mut self, on: bool) -> Self {
285        self.per_host_fallback = on;
286        self
287    }
288
289    /// Turn `PRAGMA optimize` off at writer open (see [`DbOptions::optimize_on_open`]).
290    pub fn without_optimize_on_open(mut self) -> Self {
291        self.optimize_on_open = false;
292        self
293    }
294
295    /// The file connections actually open: the requested path, or its per-host sibling when
296    /// [`Self::per_host_fallback`] applies (a rollback journal is in force, or a writer
297    /// already fell back there).
298    pub fn effective_path(&self) -> PathBuf {
299        if !self.per_host_fallback {
300            return self.path.clone();
301        }
302        let sibling = per_host_path(&self.path);
303        if effective_journal(self).is_rollback() || (sibling.exists() && !self.path.exists()) {
304            sibling
305        } else {
306            self.path.clone()
307        }
308    }
309
310    pub fn with_passphrase(mut self, key: impl Into<String>) -> Self {
311        self.key = Some(Zeroizing::new(key.into()));
312        self.raw_key = None;
313        self
314    }
315
316    pub fn with_raw_key(mut self, key: [u8; 32]) -> Self {
317        self.raw_key = Some(Zeroizing::new(key));
318        self.key = None;
319        self
320    }
321
322    pub fn is_keyed(&self) -> bool {
323        self.key.is_some() || self.raw_key.is_some()
324    }
325
326    pub fn open_writer(&self) -> Result<Connection> {
327        self.open(ConnectionKind::Writer)
328    }
329
330    pub fn open_reader(&self) -> Result<Connection> {
331        self.open(ConnectionKind::Reader { cache_kib: None })
332    }
333
334    /// Open exactly `path` read-only, without creating it or running mutating bootstrap.
335    /// URI parameters are passed to SQLite unchanged. Journal fallback, permissions,
336    /// `init`, optimization, and migrations are skipped; key and timeout still apply.
337    pub fn open_read_only(&self) -> Result<Connection> {
338        self.open_existing_at(&self.path, OpenFlags::SQLITE_OPEN_READ_ONLY)
339    }
340
341    /// Open exactly `path` read-write without CREATE or mutating bootstrap.
342    /// Uses the same minimal setup as [`Self::open_read_only`].
343    pub fn open_existing_writer(&self) -> Result<Connection> {
344        self.open_existing_at(&self.path, OpenFlags::SQLITE_OPEN_READ_WRITE)
345    }
346
347    /// Open a private, ephemeral database. `path`, filesystem permissions, journal
348    /// selection/fallback, and mmap are ignored; other bootstrap options still apply.
349    pub fn open_in_memory(&self) -> Result<Connection> {
350        let key = self.key_pragma_value()?;
351        let conn = Connection::open_in_memory()?;
352        self.initialize(&conn, key.as_ref().map(|k| k.as_str()))?;
353        configure_connection(&conn, self, ConnectionKind::Memory, None)?;
354        self.check_on_open(&conn)?;
355        Ok(conn)
356    }
357
358    fn open_existing_at(&self, path: &Path, access: OpenFlags) -> Result<Connection> {
359        let key = self.key_pragma_value()?;
360        let conn = Connection::open_with_flags(
361            path,
362            access | OpenFlags::SQLITE_OPEN_URI | OpenFlags::SQLITE_OPEN_NO_MUTEX,
363        )?;
364        conn.busy_timeout(self.busy_timeout)?;
365        apply_key_first(&conn, key.as_ref().map(|k| k.as_str()))?;
366        verify_key(&conn)?;
367        self.check_on_open(&conn)?;
368        Ok(conn)
369    }
370
371    fn initialize(&self, conn: &Connection, key: Option<&str>) -> Result<()> {
372        conn.busy_timeout(self.busy_timeout)?;
373        apply_key_first(conn, key)?;
374        verify_key(conn)?;
375        if let Some(init) = &self.init {
376            init(conn)?;
377        }
378        Ok(())
379    }
380
381    /// A read pool with [`ReadPoolOptions::new`]: read-write file handles, with `query_only` set
382    /// and verified on every reader. Same as `open_read_pool_with(cap, ReadPoolOptions::new())`.
383    pub fn open_read_pool(&self, cap: usize) -> Result<ReadPool> {
384        ReadPool::open(self.clone(), cap, ReadPoolOptions::new())
385    }
386
387    /// A read pool configured by [`ReadPoolOptions`]. A read-only pool needs the database to exist.
388    pub fn open_read_pool_with(&self, cap: usize, pool: ReadPoolOptions) -> Result<ReadPool> {
389        ReadPool::open(self.clone(), cap, pool)
390    }
391
392    fn open_pool_reader(&self, pool: ReadPoolOptions) -> Result<Connection> {
393        if pool.read_only {
394            self.open_pool_read_only(pool.cache_kib)
395        } else {
396            self.open(ConnectionKind::Reader {
397                cache_kib: pool.cache_kib,
398            })
399        }
400    }
401
402    /// A pool reader opened with `SQLITE_OPEN_READ_ONLY`, so the file refuses writes even after
403    /// `query_only` is turned off. It never creates the file, changes the journal mode, narrows
404    /// permissions, or runs `optimize`; the writer owns those. `init` runs on each reader.
405    fn open_pool_read_only(&self, cache_kib: Option<u32>) -> Result<Connection> {
406        let path = self.effective_path();
407        let key = self.key_pragma_value()?;
408        let conn = Connection::open_with_flags(
409            &path,
410            OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
411        )?;
412        conn.busy_timeout(self.busy_timeout)?;
413        apply_key_first(&conn, key.as_ref().map(|k| k.as_str()))?;
414        verify_key(&conn)?;
415        conn.pragma_update(None, "temp_store", "MEMORY")?;
416        if self.foreign_keys {
417            conn.pragma_update(None, "foreign_keys", true)?;
418        }
419        apply_cache_settings(&conn, self, ConnectionKind::Reader { cache_kib })?;
420        // Per-connection setup (`init`) runs on every reader, read-only ones included, as it does
421        // on the other connection kinds. An `init` that writes fails here with the SQLite error.
422        if let Some(init) = &self.init {
423            init(&conn)?;
424        }
425        enforce_query_only(&conn)?;
426        if !conn.is_readonly("main")? {
427            return Err(DbError::ReadOnlyNotEffective(
428                "SQLITE_OPEN_READ_ONLY handle reports a writable main database".into(),
429            ));
430        }
431        self.check_on_open(&conn)?;
432        Ok(conn)
433    }
434
435    fn open(&self, kind: ConnectionKind) -> Result<Connection> {
436        let path = self.effective_path();
437        // The per-host sibling is always rollback-journaled, whoever opens it.
438        let forced = (path != self.path).then(|| match effective_journal(self) {
439            mode if mode.is_rollback() => mode,
440            _ => fallback_mode(self),
441        });
442        let (conn, journal) = self.open_at(&path, kind, forced)?;
443        // WAL refused at runtime: with per-host fallback, the rollback journal belongs on the
444        // per-host sibling. The main file this open touched stays where it is, with every
445        // `-wal`/`-shm`/`-journal` beside it. Deleting it is never safe: file identity proves
446        // origin, not exclusive lifetime, and another connection may have committed
447        // rollback-journal data into that same file (and closed) before this point, so any
448        // check-then-remove can destroy a committed write. A created-empty main is harmless
449        // (it never holds data this open wrote, and a later open falls back again).
450        if journal == Opened::FellBack && self.per_host_fallback && path == self.path {
451            drop(conn);
452            let sibling = per_host_path(&self.path);
453            return Ok(self
454                .open_at(&sibling, kind, Some(JournalSelect::Truncate))?
455                .0);
456        }
457        Ok(conn)
458    }
459
460    fn open_at(
461        &self,
462        path: &Path,
463        kind: ConnectionKind,
464        forced: Option<JournalSelect>,
465    ) -> Result<(Connection, Opened)> {
466        let key = self.key_pragma_value()?;
467        let flags = OpenFlags::SQLITE_OPEN_READ_WRITE
468            | OpenFlags::SQLITE_OPEN_CREATE
469            | OpenFlags::SQLITE_OPEN_NO_MUTEX;
470        let conn = Connection::open_with_flags(path, flags)?;
471        // Narrow the main file before any write so WAL/SHM sidecars inherit owner-only.
472        if self.owner_only {
473            rightkit_fs::restrict_owner_only(path)?;
474        }
475        self.initialize(&conn, key.as_ref().map(|k| k.as_str()))?;
476        if let Some(on) = self.secure_delete {
477            conn.pragma_update(None, "secure_delete", if on { "ON" } else { "OFF" })?;
478        }
479        let journal = configure_connection(&conn, self, kind, forced)?;
480        self.check_on_open(&conn)?;
481        if self.owner_only {
482            harden_sidecars(path)?;
483        }
484        Ok((conn, journal))
485    }
486
487    /// Run the open-time check selected by `integrity`; `integrity_check_on_open` alone means `Full`.
488    fn check_on_open(&self, conn: &Connection) -> Result<()> {
489        let mode = match self.integrity {
490            IntegrityCheck::Off if self.integrity_check_on_open => IntegrityCheck::Full,
491            mode => mode,
492        };
493        match (mode, self.is_keyed()) {
494            (IntegrityCheck::Off, _) => Ok(()),
495            (IntegrityCheck::Quick, _) => run_integrity_pragma(conn, "PRAGMA quick_check"),
496            (IntegrityCheck::Full, true) => {
497                run_integrity_pragma(conn, "PRAGMA cipher_integrity_check")
498            }
499            (IntegrityCheck::Full, false) => run_integrity_pragma(conn, "PRAGMA integrity_check"),
500        }
501    }
502
503    /// The `PRAGMA key` right-hand side (already quoted), validated against injection.
504    fn key_pragma_value(&self) -> Result<Option<Zeroizing<String>>> {
505        if let Some(raw) = &self.raw_key {
506            #[cfg(not(feature = "cipher"))]
507            {
508                let _ = raw;
509                return Err(DbError::SqlcipherFeatureRequired);
510            }
511            #[cfg(feature = "cipher")]
512            {
513                let hex: String = raw.iter().map(|b| format!("{b:02x}")).collect();
514                return Ok(Some(Zeroizing::new(format!("\"x'{hex}'\""))));
515            }
516        }
517        let key = self.key.as_ref().map(|key| key.as_str());
518        validate_key(key)?;
519        Ok(key.map(|k| Zeroizing::new(format!("'{k}'"))))
520    }
521}
522
523/// Default-options read-only open; never creates a missing database.
524pub fn open_read_only(path: impl AsRef<Path>) -> Result<Connection> {
525    DbOptions::new(path.as_ref()).open_read_only()
526}
527
528/// Default-options read-write open; never creates a missing database.
529pub fn open_existing_writer(path: impl AsRef<Path>) -> Result<Connection> {
530    DbOptions::new(path.as_ref()).open_existing_writer()
531}
532
533/// Default-options private in-memory database.
534pub fn open_in_memory() -> Result<Connection> {
535    DbOptions::new(":memory:").open_in_memory()
536}
537
538fn harden_sidecars(db: &Path) -> Result<()> {
539    for suffix in ["-wal", "-shm"] {
540        let mut name = OsString::from(db.file_name().ok_or(DbError::InvalidDatabasePath)?);
541        name.push(suffix);
542        rightkit_fs::restrict_owner_only(&db.with_file_name(name))?;
543    }
544    Ok(())
545}
546
547#[derive(Clone, Copy)]
548enum ConnectionKind {
549    Writer,
550    /// `cache_kib` overrides the reader cache size; see [`ReadPoolOptions::with_reader_cache_kib`].
551    Reader {
552        cache_kib: Option<u32>,
553    },
554    Memory,
555}
556
557impl ConnectionKind {
558    fn reader_cache_kib(self) -> Option<u32> {
559        match self {
560            ConnectionKind::Reader { cache_kib } => cache_kib,
561            ConnectionKind::Writer | ConnectionKind::Memory => None,
562        }
563    }
564}
565
566/// What journal setup did.
567#[derive(Clone, Copy, Debug, Eq, PartialEq)]
568enum Opened {
569    AsRequested,
570    /// WAL was wanted but not honored; a rollback journal is in force.
571    FellBack,
572}
573
574fn configure_connection(
575    conn: &Connection,
576    options: &DbOptions,
577    kind: ConnectionKind,
578    forced: Option<JournalSelect>,
579) -> Result<Opened> {
580    let opened = if matches!(kind, ConnectionKind::Memory) {
581        Opened::AsRequested
582    } else {
583        configure_journal(conn, options, kind, forced)?
584    };
585    let synchronous = match options.durability {
586        DurabilityProfile::Full => "FULL",
587        DurabilityProfile::Normal => "NORMAL",
588    };
589    conn.pragma_update(None, "synchronous", synchronous)?;
590    conn.pragma_update(None, "journal_size_limit", JOURNAL_SIZE_LIMIT)?;
591    conn.pragma_update(None, "temp_store", "MEMORY")?;
592    if options.foreign_keys {
593        conn.pragma_update(None, "foreign_keys", true)?;
594    }
595    apply_cache_settings(conn, options, kind)?;
596    if matches!(kind, ConnectionKind::Reader { .. }) {
597        // Readers never run `PRAGMA optimize`: it can write statistics, and the writer runs it
598        // when it opens. `query_only` is set after every other reader setup statement.
599        enforce_query_only(conn)?;
600    } else if options.optimize_on_open {
601        optimize_on_open(conn)?;
602    }
603    Ok(opened)
604}
605
606/// Profile cache and mmap sizes, then the reader override, which wins over the profile on readers
607/// only. Writers and in-memory connections keep the profile size.
608fn apply_cache_settings(
609    conn: &Connection,
610    options: &DbOptions,
611    kind: ConnectionKind,
612) -> Result<()> {
613    if options.profile == CacheProfile::LargeWorkingSet {
614        conn.pragma_update(None, "cache_size", -65_536_i64)?;
615        if !matches!(kind, ConnectionKind::Memory) {
616            conn.pragma_update(None, "mmap_size", 268_435_456_i64)?;
617        }
618    }
619    if let Some(kib) = kind.reader_cache_kib() {
620        conn.pragma_update(None, "cache_size", -i64::from(kib))?;
621    }
622    Ok(())
623}
624
625/// Sets `PRAGMA query_only = ON` on a reader and reads it back, so a reader that did not come up
626/// query-only is never handed out.
627fn enforce_query_only(conn: &Connection) -> Result<()> {
628    conn.pragma_update(None, "query_only", true)?;
629    let on: i64 = conn.query_row("PRAGMA query_only", [], |row| row.get(0))?;
630    if on != 1 {
631        return Err(DbError::ReadOnlyNotEffective(
632            "PRAGMA query_only did not stay ON".into(),
633        ));
634    }
635    Ok(())
636}
637
638fn set_rollback(conn: &Connection, mode: JournalSelect) -> Result<()> {
639    let pragma = if mode == JournalSelect::Truncate {
640        "PRAGMA journal_mode=TRUNCATE"
641    } else {
642        "PRAGMA journal_mode=DELETE"
643    };
644    conn.query_row(pragma, [], |row| row.get::<_, String>(0))?;
645    Ok(())
646}
647
648fn configure_journal(
649    conn: &Connection,
650    options: &DbOptions,
651    kind: ConnectionKind,
652    forced: Option<JournalSelect>,
653) -> Result<Opened> {
654    let wanted = forced.unwrap_or_else(|| effective_journal(options));
655    if wanted.is_rollback() {
656        // Rollback modes are per connection: a reader left in the default DELETE mode would
657        // try to delete a TRUNCATE writer's open journal (IOERR_DELETE on Windows).
658        set_rollback(conn, wanted)?;
659        return Ok(Opened::AsRequested);
660    }
661    let honored = conn
662        .query_row("PRAGMA journal_mode=WAL", [], |row| row.get::<_, String>(0))
663        .map(|mode| mode.eq_ignore_ascii_case("wal"))
664        .unwrap_or(false)
665        // `journal_mode=WAL` can report success before the -shm/-wal sidecars are usable;
666        // one empty write transaction proves the sidecars work before anything depends on them.
667        && (matches!(kind, ConnectionKind::Reader { .. })
668            || conn.execute_batch("BEGIN IMMEDIATE; ROLLBACK;").is_ok());
669    if honored {
670        return Ok(Opened::AsRequested);
671    }
672    if wanted == JournalSelect::WalStrict {
673        return Err(DbError::Sqlite(rusqlite::Error::SqliteFailure(
674            rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_IOERR),
675            Some("WAL journal mode was not honored by this filesystem".into()),
676        )));
677    }
678    let _ = conn.execute_batch("ROLLBACK;");
679    set_rollback(conn, fallback_mode(options))?;
680    Ok(Opened::FellBack)
681}
682
683/// The rollback journal used when WAL is unavailable: TRUNCATE with per-host fallback
684/// (CodeRight's network mode), DELETE otherwise.
685fn fallback_mode(options: &DbOptions) -> JournalSelect {
686    if options.per_host_fallback {
687        JournalSelect::Truncate
688    } else {
689        JournalSelect::Delete
690    }
691}
692
693/// Kill switches (the app's own variable first, then [`JOURNAL_ENV`]); set-but-empty or
694/// unrecognized values are ignored.
695fn env_journal(options: &DbOptions) -> Option<JournalSelect> {
696    options
697        .journal_env
698        .iter()
699        .map(String::as_str)
700        .chain([JOURNAL_ENV])
701        .find_map(|name| {
702            match std::env::var(name)
703                .ok()?
704                .trim()
705                .to_ascii_lowercase()
706                .as_str()
707            {
708                "wal" => Some(JournalSelect::WalStrict),
709                "delete" => Some(JournalSelect::Delete),
710                "truncate" => Some(JournalSelect::Truncate),
711                _ => None,
712            }
713        })
714}
715
716fn effective_journal(options: &DbOptions) -> JournalSelect {
717    if let Some(mode) = env_journal(options) {
718        return mode;
719    }
720    if options.journal == JournalSelect::WalWithFallback
721        && rightkit_fs::is_network_filesystem(&options.path)
722    {
723        return fallback_mode(options);
724    }
725    options.journal
726}
727
728/// The journal mode SQLite is actually running (`wal`, `delete`, ...).
729pub fn journal_mode(conn: &Connection) -> Result<String> {
730    Ok(conn.query_row("PRAGMA journal_mode", [], |row| row.get::<_, String>(0))?)
731}
732
733/// Start a transaction that reserves SQLite's single writer slot up front.
734/// This keeps read-then-write work out of the unretryable BUSY_SNAPSHOT path.
735pub fn write_txn(conn: &mut Connection) -> Result<Transaction<'_>> {
736    conn.transaction_with_behavior(TransactionBehavior::Immediate)
737        .map_err(Into::into)
738}
739
740/// [`write_txn`] for a caller that holds only `&Connection`, such as a closure-style helper.
741/// It runs `BEGIN IMMEDIATE` through `Transaction::new_unchecked`, so the reserved writer slot is
742/// the same. The borrow checker does not stop a nested transaction here: SQLite refuses the
743/// `BEGIN` instead. The transaction rolls back when dropped unless it is committed.
744pub fn write_txn_unchecked(conn: &Connection) -> Result<Transaction<'_>> {
745    Transaction::new_unchecked(conn, TransactionBehavior::Immediate).map_err(Into::into)
746}
747
748#[cfg(feature = "cipher")]
749fn apply_key_first(conn: &Connection, key: Option<&str>) -> Result<()> {
750    if let Some(key) = key {
751        let statement = Zeroizing::new(format!("PRAGMA key = {key};"));
752        conn.execute_batch(statement.as_str())?;
753    }
754    Ok(())
755}
756
757#[cfg(not(feature = "cipher"))]
758fn apply_key_first(_conn: &Connection, key: Option<&str>) -> Result<()> {
759    if key.is_some() {
760        return Err(DbError::SqlcipherFeatureRequired);
761    }
762    Ok(())
763}
764
765fn validate_key(key: Option<&str>) -> Result<()> {
766    #[cfg(not(feature = "cipher"))]
767    if key.is_some() {
768        return Err(DbError::SqlcipherFeatureRequired);
769    }
770    if let Some(key) = key {
771        let safe = !key.is_empty()
772            && key.len() <= 4_096
773            && key.bytes().all(|byte| {
774                byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/' | b'=' | b'-' | b'_')
775            });
776        if !safe {
777            return Err(DbError::InvalidSqlcipherKey);
778        }
779    }
780    Ok(())
781}
782
783fn verify_key(conn: &Connection) -> Result<()> {
784    conn.query_row("SELECT count(*) FROM sqlite_master", [], |row| {
785        row.get::<_, i64>(0)
786    })?;
787    Ok(())
788}
789
790fn run_integrity_pragma(conn: &Connection, pragma: &str) -> Result<()> {
791    let mut statement = conn.prepare(pragma)?;
792    let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
793    let messages = rows.collect::<std::result::Result<Vec<_>, _>>()?;
794    if messages.is_empty()
795        || messages
796            .iter()
797            .all(|message| message.eq_ignore_ascii_case("ok"))
798    {
799        return Ok(());
800    }
801    Err(DbError::Integrity(messages.join("; ")))
802}
803
804/// Options for a [`ReadPool`]. The default ([`ReadPoolOptions::new`]) matches
805/// [`DbOptions::open_read_pool`]: read-write file handles, with `query_only` set and verified on
806/// every reader. Build with the `with_*` methods; the fields are private.
807#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
808pub struct ReadPoolOptions {
809    read_only: bool,
810    cache_kib: Option<u32>,
811}
812
813impl ReadPoolOptions {
814    pub fn new() -> Self {
815        Self::default()
816    }
817
818    /// Opens each reader with `SQLITE_OPEN_READ_ONLY`, so writes fail even if `query_only` is later
819    /// turned off. The database must already exist: the pool never creates it, changes its journal
820    /// mode, narrows its permissions, or runs `optimize`. Each reader still runs the `init` hook,
821    /// as on the other connections. A rollback journal left by a crash is recovered by a writer,
822    /// not by a read-only reader. Default off.
823    pub fn with_read_only(mut self, on: bool) -> Self {
824        self.read_only = on;
825        self
826    }
827
828    /// Page cache size for each reader connection, in KiB. It is applied as `PRAGMA cache_size`
829    /// set to `-kib`, so the size does not depend on the page size. Valid values are 1 to
830    /// 2147483647; the pool checks this when it opens. It overrides
831    /// [`CacheProfile::LargeWorkingSet`] on readers only. Writers and [`DbOptions::open_reader`]
832    /// keep the profile size. Default: the profile's size.
833    pub fn with_reader_cache_kib(mut self, kib: u32) -> Self {
834        self.cache_kib = Some(kib);
835        self
836    }
837}
838
839/// A bounded pool of reader connections. Clones share one pool.
840#[derive(Clone)]
841pub struct ReadPool {
842    inner: Arc<ReadPoolInner>,
843}
844
845/// Opens one reader connection. A pool built from [`DbOptions`] uses that database's reader
846/// setup; [`ReadPool::from_opener`] supplies its own.
847type ReaderOpener = Box<dyn Fn() -> Result<Connection> + Send + Sync>;
848
849struct ReadPoolInner {
850    open: ReaderOpener,
851    cap: usize,
852    state: Mutex<ReadPoolState>,
853    /// Wakes a `get` waiting for capacity, and every waiter when the pool closes.
854    available: Condvar,
855    /// Wakes `close_and_wait` when a connection is released after the pool closed.
856    drained: Condvar,
857}
858
859#[derive(Default)]
860struct ReadPoolState {
861    idle: Vec<Connection>,
862    /// Idle, checked-out, and still-opening connections.
863    total: usize,
864    closed: bool,
865}
866
867impl ReadPoolInner {
868    fn open_reader(&self) -> Result<Connection> {
869        (self.open)()
870    }
871}
872
873pub struct PooledReadConnection {
874    conn: Option<Connection>,
875    pool: Weak<ReadPoolInner>,
876    // Managed lifecycle checkout retains its exact generation/guard until connection return.
877    _lifecycle_lease: Option<Arc<dyn Send + Sync>>,
878}
879
880impl PooledReadConnection {
881    #[cfg(feature = "encrypted-lifecycle")]
882    pub(crate) fn with_lifecycle_lease(mut self, lease: Arc<dyn Send + Sync>) -> Self {
883        self._lifecycle_lease = Some(lease);
884        self
885    }
886}
887
888impl Deref for PooledReadConnection {
889    type Target = Connection;
890
891    fn deref(&self) -> &Self::Target {
892        self.conn.as_ref().expect("pooled connection is present")
893    }
894}
895
896impl Drop for PooledReadConnection {
897    fn drop(&mut self) {
898        let Some(conn) = self.conn.take() else {
899            return;
900        };
901        let Some(pool) = self.pool.upgrade() else {
902            return;
903        };
904        let mut state = lock_recover(&pool.state);
905        if state.closed {
906            // Close the file before the count drops, so `close_and_wait` returns only once it is
907            // released. Checked under the same lock as `close`, so no connection is parked idle
908            // after the pool has closed.
909            drop(conn);
910            state.total -= 1;
911            pool.drained.notify_all();
912        } else {
913            state.idle.push(conn);
914            pool.available.notify_one();
915        }
916    }
917}
918
919impl ReadPool {
920    fn open(options: DbOptions, cap: usize, pool_options: ReadPoolOptions) -> Result<Self> {
921        if cap == 0 {
922            return Err(DbError::InvalidPoolCapacity);
923        }
924        if let Some(kib) = pool_options.cache_kib {
925            if kib == 0 || kib > i32::MAX.unsigned_abs() {
926                return Err(DbError::InvalidReaderCacheSize(kib));
927            }
928        }
929        Self::with_opener(
930            cap,
931            Box::new(move || options.open_pool_reader(pool_options)),
932        )
933    }
934
935    /// A pool whose readers come from `open`, called once now to check it works and again for
936    /// each new reader. The pool enforces `cap` and the closed and wait rules; `query_only` and
937    /// other reader setup are the opener's job. Intended for tests that need a pool over a
938    /// connection they build themselves. Use [`DbOptions::open_read_pool`] for a real database.
939    pub fn from_opener(
940        cap: usize,
941        open: impl Fn() -> Result<Connection> + Send + Sync + 'static,
942    ) -> Result<Self> {
943        Self::with_opener(cap, Box::new(open))
944    }
945
946    fn with_opener(cap: usize, open: ReaderOpener) -> Result<Self> {
947        if cap == 0 {
948            return Err(DbError::InvalidPoolCapacity);
949        }
950        let pool = Self {
951            inner: Arc::new(ReadPoolInner {
952                open,
953                cap,
954                state: Mutex::new(ReadPoolState::default()),
955                available: Condvar::new(),
956                drained: Condvar::new(),
957            }),
958        };
959        let conn = pool.inner.open_reader()?;
960        let mut state = lock_recover(&pool.inner.state);
961        state.total = 1;
962        state.idle.push(conn);
963        drop(state);
964        Ok(pool)
965    }
966
967    /// Checks out an idle reader, opens a new one while under the cap, or waits for a return.
968    /// Fails with [`DbError::ReadPoolClosed`] once the pool is closed, including while waiting.
969    /// The wait has no bound; use [`Self::get_timeout`] to give up after a deadline.
970    pub fn get(&self) -> Result<PooledReadConnection> {
971        match self.checkout(None)? {
972            Some(reader) => Ok(reader),
973            // Unreachable: an unbounded wait ends only with a reader or an error.
974            None => Err(DbError::ReadPoolClosed),
975        }
976    }
977
978    /// [`Self::get`] with a bounded wait. Returns `Ok(Some(reader))` as soon as a reader is idle
979    /// or can be opened under the cap, and `Ok(None)` if none frees up within `timeout`. A zero
980    /// `timeout` never waits: it returns an idle or newly opened reader, or `Ok(None)`. If the
981    /// pool is closed, or closes while waiting, the result is [`DbError::ReadPoolClosed`], never
982    /// `Ok(None)`. Opening a new reader is not bounded by `timeout`. A timeout too large for the
983    /// clock waits in bounded slices until a reader frees up or the pool closes.
984    pub fn get_timeout(&self, timeout: Duration) -> Result<Option<PooledReadConnection>> {
985        self.checkout(Some(timeout))
986    }
987
988    /// Shared checkout. `wait` is `None` for no deadline.
989    fn checkout(&self, wait: Option<Duration>) -> Result<Option<PooledReadConnection>> {
990        let started = Instant::now();
991        let deadline = wait.and_then(|wait| started.checked_add(wait));
992        let mut state = lock_recover(&self.inner.state);
993        loop {
994            if state.closed {
995                return Err(DbError::ReadPoolClosed);
996            }
997            if let Some(conn) = state.idle.pop() {
998                return Ok(Some(PooledReadConnection {
999                    conn: Some(conn),
1000                    pool: Arc::downgrade(&self.inner),
1001                    _lifecycle_lease: None,
1002                }));
1003            }
1004            if state.total < self.inner.cap {
1005                state.total += 1;
1006                drop(state);
1007                return self.open_checked_out().map(Some);
1008            }
1009            state = match (wait, deadline) {
1010                (None, _) => match self.inner.available.wait(state) {
1011                    Ok(state) => state,
1012                    Err(poisoned) => poisoned.into_inner(),
1013                },
1014                (Some(_), Some(at)) => {
1015                    let remaining = at.saturating_duration_since(Instant::now());
1016                    if remaining.is_zero() {
1017                        return Ok(None);
1018                    }
1019                    match self.inner.available.wait_timeout(state, remaining) {
1020                        Ok((state, _)) => state,
1021                        Err(poisoned) => poisoned.into_inner().0,
1022                    }
1023                }
1024                (Some(_), None) => {
1025                    match self
1026                        .inner
1027                        .available
1028                        .wait_timeout(state, Duration::from_secs(60))
1029                    {
1030                        Ok((state, _)) => state,
1031                        Err(poisoned) => poisoned.into_inner().0,
1032                    }
1033                }
1034            };
1035        }
1036    }
1037
1038    /// Opens a reader for a `get` that reserved a slot. If the pool closed meanwhile, the new
1039    /// reader is dropped and the caller gets [`DbError::ReadPoolClosed`].
1040    fn open_checked_out(&self) -> Result<PooledReadConnection> {
1041        match self.inner.open_reader() {
1042            Ok(conn) => {
1043                let mut state = lock_recover(&self.inner.state);
1044                if state.closed {
1045                    drop(conn);
1046                    state.total -= 1;
1047                    self.inner.drained.notify_all();
1048                    return Err(DbError::ReadPoolClosed);
1049                }
1050                Ok(PooledReadConnection {
1051                    conn: Some(conn),
1052                    pool: Arc::downgrade(&self.inner),
1053                    _lifecycle_lease: None,
1054                })
1055            }
1056            Err(error) => {
1057                let mut state = lock_recover(&self.inner.state);
1058                state.total -= 1;
1059                self.inner.available.notify_one();
1060                self.inner.drained.notify_all();
1061                Err(error)
1062            }
1063        }
1064    }
1065
1066    /// Stops handing out readers and drops the idle ones now. A reader still checked out closes
1067    /// when it is returned. Calling it again does nothing.
1068    pub fn close(&self) {
1069        let idle = {
1070            let mut state = lock_recover(&self.inner.state);
1071            if state.closed {
1072                return;
1073            }
1074            state.closed = true;
1075            self.inner.available.notify_all();
1076            std::mem::take(&mut state.idle)
1077        };
1078        let closed = idle.len();
1079        // Files close before the count drops, so `close_and_wait` sees them released.
1080        drop(idle);
1081        let mut state = lock_recover(&self.inner.state);
1082        state.total -= closed;
1083        self.inner.drained.notify_all();
1084    }
1085
1086    /// [`Self::close`], then waits up to `timeout` for every checked-out reader to be returned.
1087    /// `Ok(())` means every connection is closed and its database file released. Otherwise it
1088    /// returns [`DbError::ReadPoolCloseTimeout`] with the number still out. Do not call it while
1089    /// holding a reader from this pool: that reader cannot come back, so the call times out. A
1090    /// timeout too large for the clock waits in bounded slices until the readers return.
1091    pub fn close_and_wait(&self, timeout: Duration) -> Result<()> {
1092        self.close();
1093        let deadline = Instant::now().checked_add(timeout);
1094        let mut state = lock_recover(&self.inner.state);
1095        loop {
1096            if state.total == 0 {
1097                return Ok(());
1098            }
1099            let remaining = match deadline {
1100                Some(at) => at.saturating_duration_since(Instant::now()),
1101                None => Duration::from_secs(60),
1102            };
1103            if remaining.is_zero() {
1104                return Err(DbError::ReadPoolCloseTimeout {
1105                    outstanding: state.total,
1106                    timeout,
1107                });
1108            }
1109            let (next, _) = match self.inner.drained.wait_timeout(state, remaining) {
1110                Ok(pair) => pair,
1111                Err(poisoned) => poisoned.into_inner(),
1112            };
1113            state = next;
1114        }
1115    }
1116
1117    pub fn capacity(&self) -> usize {
1118        self.inner.cap
1119    }
1120
1121    pub fn idle_len(&self) -> usize {
1122        lock_recover(&self.inner.state).idle.len()
1123    }
1124
1125    pub fn total_len(&self) -> usize {
1126        lock_recover(&self.inner.state).total
1127    }
1128}
1129
1130fn lock_recover<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
1131    mutex
1132        .lock()
1133        .unwrap_or_else(|poisoned| poisoned.into_inner())
1134}
1135
1136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1137pub struct CheckpointStatus {
1138    pub busy: i64,
1139    pub log_frames: i64,
1140    pub checkpointed_frames: i64,
1141}
1142
1143pub fn checkpoint_truncate(conn: &Connection) -> Result<CheckpointStatus> {
1144    checkpoint(conn, "PRAGMA wal_checkpoint(TRUNCATE)")
1145}
1146
1147/// `PRAGMA wal_checkpoint(RESTART)`: copies the log into the main file like PASSIVE, waits (under
1148/// the busy timeout) for readers still using the log, then the next writer starts it again from
1149/// the beginning. The log file keeps its size. Use it when the database is quiet and the log is
1150/// large; [`WalCheckpointJob`] decides when.
1151pub fn checkpoint_restart(conn: &Connection) -> Result<CheckpointStatus> {
1152    checkpoint(conn, "PRAGMA wal_checkpoint(RESTART)")
1153}
1154
1155pub fn checkpoint_if_wal_larger_than(
1156    conn: &Connection,
1157    db: &Path,
1158    bytes: u64,
1159) -> Result<Option<CheckpointStatus>> {
1160    let wal = wal_path(db)?;
1161    let size = match std::fs::metadata(wal) {
1162        Ok(metadata) => metadata.len(),
1163        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1164        Err(_) => return Ok(None),
1165    };
1166    if size <= bytes {
1167        return Ok(None);
1168    }
1169    checkpoint(conn, "PRAGMA wal_checkpoint(PASSIVE)").map(Some)
1170}
1171
1172fn checkpoint(conn: &Connection, pragma: &str) -> Result<CheckpointStatus> {
1173    conn.query_row(pragma, [], |row| {
1174        Ok(CheckpointStatus {
1175            busy: row.get(0)?,
1176            log_frames: row.get(1)?,
1177            checkpointed_frames: row.get(2)?,
1178        })
1179    })
1180    .map_err(Into::into)
1181}
1182
1183fn wal_path(db: &Path) -> Result<PathBuf> {
1184    let file_name = db.file_name().ok_or(DbError::InvalidDatabasePath)?;
1185    let mut wal_name = OsString::from(file_name);
1186    wal_name.push("-wal");
1187    Ok(db.with_file_name(wal_name))
1188}
1189
1190pub fn optimize_on_open(conn: &Connection) -> Result<()> {
1191    conn.execute_batch("PRAGMA optimize=0x10002;")?;
1192    Ok(())
1193}
1194
1195pub fn optimize_boundary(conn: &Connection) -> Result<()> {
1196    conn.execute_batch("PRAGMA optimize;")?;
1197    Ok(())
1198}