Skip to main content

prolly_store_sqlite/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::cell::Cell;
4use std::collections::{hash_map::Entry, HashMap};
5use std::ops::Deref;
6use std::path::{Path, PathBuf};
7use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
8use std::sync::{mpsc, Arc, OnceLock};
9use std::thread::{self, JoinHandle};
10use std::time::{Duration, Instant};
11
12use lru::LruCache;
13use parking_lot::{Condvar, Mutex, MutexGuard};
14use rusqlite::OpenFlags;
15use rusqlite::{params, Connection, OptionalExtension, TransactionBehavior};
16
17use prolly::{
18    BatchOp, Cid, Error, ManifestStore, ManifestStoreScan, ManifestUpdate, NamedRootManifest,
19    NodePublication, NodeStoreScan, PublicationOrigin, RootCondition, RootManifest, RootWrite,
20    Store, TransactionConflict, TransactionNodeWrite, TransactionUpdate, TransactionalStore,
21};
22
23const DEFAULT_NODE_CACHE_SHARDS: usize = 16;
24static NEXT_SQLITE_READER_SLOT: AtomicUsize = AtomicUsize::new(0);
25
26thread_local! {
27    static SQLITE_READER_SLOT: Cell<usize> = const { Cell::new(usize::MAX) };
28}
29
30struct NodeReadCacheShard {
31    values: LruCache<Vec<u8>, Arc<[u8]>>,
32    retained_bytes: usize,
33    max_bytes: usize,
34}
35
36impl NodeReadCacheShard {
37    fn new(max_bytes: usize) -> Self {
38        Self {
39            values: LruCache::unbounded(),
40            retained_bytes: 0,
41            max_bytes,
42        }
43    }
44
45    fn get(&mut self, key: &[u8]) -> Option<Arc<[u8]>> {
46        self.values.get(key).cloned()
47    }
48
49    fn insert(&mut self, key: &[u8], value: Arc<[u8]>) -> usize {
50        if self.max_bytes == 0 || value.len() > self.max_bytes {
51            return 0;
52        }
53        let mut evictions = 0usize;
54        let previous = self.values.pop(key);
55        self.retained_bytes = self
56            .retained_bytes
57            .saturating_sub(previous.as_ref().map_or(0, |entry| entry.len()));
58        let target_bytes = self.max_bytes.saturating_sub(value.len());
59        while self.retained_bytes > target_bytes {
60            if let Some((_, evicted)) = self.values.pop_lru() {
61                self.retained_bytes = self.retained_bytes.saturating_sub(evicted.len());
62                evictions = evictions.saturating_add(1);
63            } else {
64                break;
65            }
66        }
67        self.retained_bytes = self.retained_bytes.saturating_add(value.len());
68        self.values.put(key.to_vec(), value);
69        evictions
70    }
71
72    fn insert_immutable(&mut self, key: &[u8], value: Arc<[u8]>) -> usize {
73        if self.max_bytes == 0 || value.len() > self.max_bytes {
74            return 0;
75        }
76        if self.get(key).is_some() {
77            return 0;
78        }
79        self.insert(key, value)
80    }
81
82    fn remove(&mut self, key: &[u8]) {
83        if let Some(value) = self.values.pop(key) {
84            self.retained_bytes = self.retained_bytes.saturating_sub(value.len());
85        }
86    }
87}
88
89struct ShardedNodeReadCache {
90    shards: Box<[Mutex<NodeReadCacheShard>]>,
91    metrics: Arc<SqliteMetricsInner>,
92}
93
94impl ShardedNodeReadCache {
95    fn new(max_bytes: usize, shard_count: usize, metrics: Arc<SqliteMetricsInner>) -> Self {
96        let shard_count = if max_bytes == 0 {
97            1
98        } else {
99            shard_count.max(1)
100        };
101        let shard_bytes = max_bytes.div_ceil(shard_count);
102        let shards = (0..shard_count)
103            .map(|_| Mutex::new(NodeReadCacheShard::new(shard_bytes)))
104            .collect::<Vec<_>>()
105            .into_boxed_slice();
106        Self { shards, metrics }
107    }
108
109    fn shard_index(&self, key: &[u8]) -> usize {
110        let hash = key
111            .iter()
112            .take(8)
113            .fold(0xcbf29ce484222325u64, |hash, byte| {
114                (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3)
115            });
116        hash as usize % self.shards.len()
117    }
118
119    fn get(&self, key: &[u8]) -> Option<Arc<[u8]>> {
120        let value = self.shards[self.shard_index(key)].lock().get(key);
121        if value.is_some() {
122            self.metrics.node_cache_hits.fetch_add(1, Ordering::Relaxed);
123        } else {
124            self.metrics
125                .node_cache_misses
126                .fetch_add(1, Ordering::Relaxed);
127        }
128        value
129    }
130
131    fn insert(&self, key: &[u8], value: Arc<[u8]>) {
132        let evictions = self.shards[self.shard_index(key)].lock().insert(key, value);
133        self.metrics
134            .node_cache_evictions
135            .fetch_add(evictions as u64, Ordering::Relaxed);
136    }
137
138    fn insert_immutable(&self, key: &[u8], value: Arc<[u8]>) {
139        let evictions = self.shards[self.shard_index(key)]
140            .lock()
141            .insert_immutable(key, value);
142        self.metrics
143            .node_cache_evictions
144            .fetch_add(evictions as u64, Ordering::Relaxed);
145    }
146
147    fn remove(&self, key: &[u8]) {
148        self.shards[self.shard_index(key)].lock().remove(key);
149    }
150
151    fn retained_bytes(&self) -> usize {
152        self.shards
153            .iter()
154            .map(|shard| shard.lock().retained_bytes)
155            .sum()
156    }
157
158    fn entries(&self) -> usize {
159        self.shards
160            .iter()
161            .map(|shard| shard.lock().values.len())
162            .sum()
163    }
164}
165
166#[derive(Default)]
167struct SqliteMetricsInner {
168    node_cache_hits: AtomicU64,
169    node_cache_misses: AtomicU64,
170    node_cache_evictions: AtomicU64,
171    sql_reads: AtomicU64,
172    write_transactions: AtomicU64,
173    published_nodes: AtomicU64,
174    grouped_publications: AtomicU64,
175    checkpoint_attempts: AtomicU64,
176    checkpoint_busy: AtomicU64,
177    checkpointed_frames: AtomicU64,
178    compressed_input_bytes: AtomicU64,
179    compressed_stored_bytes: AtomicU64,
180}
181
182/// Runtime counters for the SQLite adapter and SQLite pager.
183#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
184pub struct SqliteStoreMetrics {
185    /// Decoded-node reads served by the adapter cache.
186    pub node_cache_hits: u64,
187    /// Decoded-node reads not found in the adapter cache.
188    pub node_cache_misses: u64,
189    /// Decoded nodes removed to keep cache shards within their bounds.
190    pub node_cache_evictions: u64,
191    /// Decoded nodes currently retained by all cache shards.
192    pub node_cache_entries: usize,
193    /// Decoded node payload bytes currently retained by all cache shards.
194    pub node_cache_retained_bytes: usize,
195    /// SQLite node-read statements executed after cache misses.
196    pub sql_reads: u64,
197    /// Successfully committed SQLite write transactions.
198    pub write_transactions: u64,
199    /// Immutable nodes submitted by successfully committed publications.
200    pub published_nodes: u64,
201    /// Publications committed alongside another publication by group commit.
202    pub grouped_publications: u64,
203    /// Explicit and background WAL checkpoint attempts.
204    pub checkpoint_attempts: u64,
205    /// Checkpoint attempts that could not complete because a reader was active.
206    pub checkpoint_busy: u64,
207    /// Frames reported as checkpointed by the latest checkpoint attempt.
208    pub checkpointed_frames: u64,
209    /// Uncompressed input bytes considered by successful node publications.
210    pub compressed_input_bytes: u64,
211    /// Stored bytes produced for those node publications.
212    pub compressed_stored_bytes: u64,
213    /// SQLite page-cache hits reported across all open connections.
214    pub sqlite_page_cache_hits: u64,
215    /// SQLite page-cache misses reported across all open connections.
216    pub sqlite_page_cache_misses: u64,
217    /// SQLite page-cache writes reported across all open connections.
218    pub sqlite_page_cache_writes: u64,
219    /// SQLite page-cache spills reported across all open connections.
220    pub sqlite_page_cache_spills: u64,
221}
222
223struct OrderedBatchReadPlan<'a> {
224    unique_keys: Vec<&'a [u8]>,
225    positions: Option<Vec<usize>>,
226}
227
228impl<'a> OrderedBatchReadPlan<'a> {
229    fn new(keys: &[&'a [u8]]) -> Self {
230        let mut unique_indexes = HashMap::with_capacity(keys.len());
231        let mut unique_keys = Vec::with_capacity(keys.len());
232        let mut positions = None;
233        for key in keys {
234            match unique_indexes.entry(*key) {
235                Entry::Occupied(entry) => positions
236                    .get_or_insert_with(|| (0..unique_keys.len()).collect::<Vec<_>>())
237                    .push(*entry.get()),
238                Entry::Vacant(entry) => {
239                    let index = unique_keys.len();
240                    unique_keys.push(*key);
241                    if let Some(positions) = positions.as_mut() {
242                        positions.push(index);
243                    }
244                    entry.insert(index);
245                }
246            }
247        }
248        Self {
249            unique_keys,
250            positions,
251        }
252    }
253
254    fn unique_keys(&self) -> &[&'a [u8]] {
255        &self.unique_keys
256    }
257
258    fn expand_owned<T: Clone>(&self, values: Vec<Option<T>>) -> Vec<Option<T>> {
259        match &self.positions {
260            Some(positions) => positions
261                .iter()
262                .map(|&index| values[index].clone())
263                .collect(),
264            None => values,
265        }
266    }
267}
268
269fn cid_from_store_key(key: &[u8], context: &str) -> Result<Cid, String> {
270    let bytes: [u8; 32] = key.try_into().map_err(|_| {
271        format!(
272            "{context} key has invalid CID length {}, expected 32",
273            key.len()
274        )
275    })?;
276    Ok(Cid(bytes))
277}
278
279fn sort_cids(cids: &mut [Cid]) {
280    cids.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
281}
282
283fn sort_named_root_manifests(roots: &mut [NamedRootManifest]) {
284    roots.sort_by(|left, right| left.name.cmp(&right.name));
285}
286
287const CREATE_TABLE_SQL: &str = "\
288CREATE TABLE IF NOT EXISTS prolly_nodes (
289    cid      BLOB PRIMARY KEY NOT NULL,
290    encoding INTEGER NOT NULL DEFAULT 0,
291    node     BLOB NOT NULL
292);";
293
294const CREATE_HINTS_TABLE_SQL: &str = "\
295CREATE TABLE IF NOT EXISTS prolly_hints (
296    namespace BLOB NOT NULL,
297    key       BLOB NOT NULL,
298    value     BLOB NOT NULL,
299    PRIMARY KEY (namespace, key)
300) WITHOUT ROWID;";
301
302const CREATE_ROOTS_TABLE_SQL: &str = "\
303CREATE TABLE IF NOT EXISTS prolly_roots (
304    name     BLOB PRIMARY KEY NOT NULL,
305    manifest BLOB NOT NULL
306) WITHOUT ROWID;";
307
308const SELECT_SQL: &str = "SELECT encoding, node FROM prolly_nodes WHERE cid = ?1";
309const SELECT_NODE_CIDS_SQL: &str = "SELECT cid FROM prolly_nodes ORDER BY cid";
310const UPSERT_SQL: &str = "\
311INSERT INTO prolly_nodes (cid, encoding, node)
312VALUES (?1, ?2, ?3)
313ON CONFLICT(cid) DO UPDATE SET encoding = excluded.encoding, node = excluded.node";
314const INSERT_IMMUTABLE_SQL: &str = "\
315INSERT OR IGNORE INTO prolly_nodes (cid, encoding, node)
316VALUES (?1, ?2, ?3)";
317const DELETE_SQL: &str = "DELETE FROM prolly_nodes WHERE cid = ?1";
318const UPSERT_HINT_SQL: &str = "\
319INSERT INTO prolly_hints (namespace, key, value)
320VALUES (?1, ?2, ?3)
321ON CONFLICT(namespace, key) DO UPDATE SET value = excluded.value";
322const SELECT_ROOT_SQL: &str = "SELECT manifest FROM prolly_roots WHERE name = ?1";
323const SELECT_ROOTS_SQL: &str = "SELECT name, manifest FROM prolly_roots ORDER BY name";
324const UPSERT_ROOT_SQL: &str = "\
325INSERT INTO prolly_roots (name, manifest)
326VALUES (?1, ?2)
327ON CONFLICT(name) DO UPDATE SET manifest = excluded.manifest";
328const DELETE_ROOT_SQL: &str = "DELETE FROM prolly_roots WHERE name = ?1";
329
330/// Configuration options for [`SqliteStore`].
331#[derive(Debug, Clone)]
332pub struct SqliteStoreConfig {
333    /// Busy timeout in milliseconds for contended SQLite locks.
334    pub busy_timeout_ms: u64,
335    /// Enable WAL journaling for file-backed databases.
336    pub enable_wal: bool,
337    /// Set SQLite synchronous mode to NORMAL when applying default pragmas.
338    pub synchronous_normal: bool,
339    /// Page size requested when creating a new SQLite database.
340    ///
341    /// Existing databases retain the page size stored in their file header.
342    pub page_size_bytes: u32,
343    /// Maximum bytes retained in SQLite's page cache for this connection.
344    ///
345    /// A larger cache prevents dirty B-tree pages from being spilled and
346    /// rewritten repeatedly during large prolly-node publications.
347    pub page_cache_size_bytes: u64,
348    /// Number of WAL pages that triggers an automatic checkpoint.
349    ///
350    /// This should be large enough that a normal node publication commits
351    /// without performing checkpoint I/O in its latency-sensitive path.
352    pub wal_autocheckpoint_pages: u32,
353    /// Maximum decoded node bytes retained by the adapter.
354    ///
355    /// This cache complements SQLite's encoded page cache and lets immutable
356    /// nodes written earlier in the process satisfy later shared reads without
357    /// another SQL lookup or decompression.
358    pub node_read_cache_size_bytes: usize,
359    /// Minimum serialized node size considered for LZ4 compression.
360    ///
361    /// Smaller values favor database size and cold I/O; larger values avoid
362    /// compression CPU for latency-sensitive in-process publication.
363    pub node_compression_min_bytes: usize,
364    /// Maximum database bytes SQLite may access through memory-mapped reads.
365    ///
366    /// This avoids per-page read syscalls for large immutable prolly nodes while
367    /// retaining SQLite's normal transactional and durability guarantees.
368    pub mmap_size_bytes: u64,
369    /// Number of read-only SQLite connections used for concurrent cache misses,
370    /// scans, and manifest reads. In-memory and externally supplied connections
371    /// use the writer connection regardless of this value.
372    pub reader_connections: usize,
373    /// Let the first thread that reads from a file-backed store reuse the writer
374    /// connection. This preserves the single-thread page and statement cache
375    /// fast path while other threads use the read-only pool.
376    pub primary_reader_uses_writer: bool,
377    /// Number of independently locked decoded-node cache shards.
378    pub node_read_cache_shards: usize,
379    /// Maximum SQLite variables used by one native multi-key read statement.
380    pub max_batch_select_keys: usize,
381    /// Run passive WAL checkpoints on a dedicated connection instead of in the
382    /// latency-sensitive commit path.
383    pub background_checkpoints: bool,
384    /// Run a passive checkpoint after the WAL file reaches this allocation.
385    pub checkpoint_wal_bytes: u64,
386    /// Maximum delay between background checkpoint inspections.
387    pub checkpoint_interval_ms: u64,
388    /// Maximum retained WAL allocation after the log is reset.
389    pub journal_size_limit_bytes: u64,
390    /// Optional window used to combine concurrent immutable-node publications
391    /// into one durable SQLite transaction. Zero disables group commit.
392    pub group_commit_delay_micros: u64,
393    /// Maximum number of nodes combined in one grouped publication transaction.
394    pub group_commit_max_nodes: usize,
395}
396
397impl Default for SqliteStoreConfig {
398    fn default() -> Self {
399        Self {
400            busy_timeout_ms: 5_000,
401            enable_wal: true,
402            synchronous_normal: true,
403            page_size_bytes: 64 * 1024,
404            page_cache_size_bytes: 64 * 1024 * 1024,
405            wal_autocheckpoint_pages: 32 * 1024,
406            node_read_cache_size_bytes: 128 * 1024 * 1024,
407            node_compression_min_bytes: 8 * 1024,
408            mmap_size_bytes: 256 * 1024 * 1024,
409            reader_connections: 4,
410            primary_reader_uses_writer: true,
411            node_read_cache_shards: DEFAULT_NODE_CACHE_SHARDS,
412            max_batch_select_keys: 128,
413            background_checkpoints: true,
414            checkpoint_wal_bytes: 64 * 1024 * 1024,
415            checkpoint_interval_ms: 1_000,
416            journal_size_limit_bytes: 256 * 1024 * 1024,
417            group_commit_delay_micros: 0,
418            group_commit_max_nodes: 16_384,
419        }
420    }
421}
422
423/// Error type for SQLite store operations.
424#[derive(Debug)]
425pub struct SqliteStoreError {
426    message: String,
427    source: Option<rusqlite::Error>,
428}
429
430impl SqliteStoreError {
431    /// Create a new error with a message.
432    pub fn new(message: impl Into<String>) -> Self {
433        Self {
434            message: message.into(),
435            source: None,
436        }
437    }
438
439    /// Create a new error from a rusqlite error.
440    pub fn from_sqlite(err: rusqlite::Error, context: impl Into<String>) -> Self {
441        Self {
442            message: format!("{}: {}", context.into(), err),
443            source: Some(err),
444        }
445    }
446}
447
448impl std::fmt::Display for SqliteStoreError {
449    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450        write!(f, "SQLite error: {}", self.message)
451    }
452}
453
454impl std::error::Error for SqliteStoreError {
455    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
456        self.source
457            .as_ref()
458            .map(|e| e as &(dyn std::error::Error + 'static))
459    }
460}
461
462impl From<rusqlite::Error> for SqliteStoreError {
463    fn from(err: rusqlite::Error) -> Self {
464        Self {
465            message: err.to_string(),
466            source: Some(err),
467        }
468    }
469}
470
471enum ReadConnectionGuard<'a> {
472    Writer(MutexGuard<'a, Connection>),
473    Reader(MutexGuard<'a, Connection>),
474}
475
476impl Deref for ReadConnectionGuard<'_> {
477    type Target = Connection;
478
479    fn deref(&self) -> &Self::Target {
480        match self {
481            Self::Writer(conn) | Self::Reader(conn) => conn,
482        }
483    }
484}
485
486/// WAL checkpoint modes exposed by [`SqliteStore::checkpoint`].
487#[derive(Clone, Copy, Debug, Eq, PartialEq)]
488pub enum SqliteCheckpointMode {
489    Passive,
490    Full,
491    Restart,
492    Truncate,
493}
494
495impl SqliteCheckpointMode {
496    const fn sql(self) -> &'static str {
497        match self {
498            Self::Passive => "PRAGMA wal_checkpoint(PASSIVE)",
499            Self::Full => "PRAGMA wal_checkpoint(FULL)",
500            Self::Restart => "PRAGMA wal_checkpoint(RESTART)",
501            Self::Truncate => "PRAGMA wal_checkpoint(TRUNCATE)",
502        }
503    }
504}
505
506/// Result returned by a WAL checkpoint operation.
507#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
508pub struct SqliteCheckpointStats {
509    /// Whether SQLite reported a busy reader or writer during the checkpoint.
510    pub busy: bool,
511    /// Frames present in the WAL when SQLite inspected it.
512    pub log_frames: u64,
513    /// Frames already copied back to the database file.
514    pub checkpointed_frames: u64,
515}
516
517/// File and freelist statistics useful for maintenance decisions.
518#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
519pub struct SqliteStorageStats {
520    /// Configured database page size.
521    pub page_size_bytes: u64,
522    /// Total pages currently allocated in the database file.
523    pub page_count: u64,
524    /// Allocated pages currently present on SQLite's freelist.
525    pub freelist_pages: u64,
526    /// Logical database allocation (`page_size_bytes * page_count`).
527    pub database_bytes: u64,
528    /// Reclaimable database allocation (`page_size_bytes * freelist_pages`).
529    pub free_bytes: u64,
530    /// Bytes currently allocated to the WAL file, including retained capacity.
531    pub wal_bytes: u64,
532}
533
534enum CheckpointSignal {
535    WriteCommitted,
536    Shutdown,
537}
538
539struct CheckpointWorker {
540    sender: mpsc::Sender<CheckpointSignal>,
541    handle: Option<JoinHandle<()>>,
542}
543
544impl CheckpointWorker {
545    fn notify_write(&self) {
546        let _ = self.sender.send(CheckpointSignal::WriteCommitted);
547    }
548}
549
550impl Drop for CheckpointWorker {
551    fn drop(&mut self) {
552        let _ = self.sender.send(CheckpointSignal::Shutdown);
553        if let Some(handle) = self.handle.take() {
554            let _ = handle.join();
555        }
556    }
557}
558
559struct OwnedPublication {
560    entries: Vec<(Vec<u8>, Vec<u8>)>,
561    hint: Option<(Vec<u8>, Vec<u8>, Vec<u8>)>,
562    origin: PublicationOrigin,
563}
564
565struct PublicationCompletion {
566    result: Mutex<Option<Result<(), String>>>,
567    ready: Condvar,
568}
569
570struct PendingPublication {
571    publication: OwnedPublication,
572    completion: Arc<PublicationCompletion>,
573}
574
575#[derive(Default)]
576struct PublicationGroupState {
577    queue: Vec<PendingPublication>,
578    flushing: bool,
579}
580
581struct PublicationGroupCommit {
582    delay: Duration,
583    max_nodes: usize,
584    state: Mutex<PublicationGroupState>,
585}
586
587/// SQLite-backed storage backend for Prolly Trees.
588///
589/// This store persists content-addressed nodes in a single SQLite table and
590/// supports atomic batch operations through transactions.
591pub struct SqliteStore {
592    writer: Mutex<Connection>,
593    readers: Box<[Mutex<Connection>]>,
594    primary_reader_thread: OnceLock<thread::ThreadId>,
595    primary_reader_uses_writer: bool,
596    node_read_cache: ShardedNodeReadCache,
597    node_compression_min_bytes: usize,
598    max_batch_select_keys: usize,
599    metrics: Arc<SqliteMetricsInner>,
600    checkpoint_worker: Option<CheckpointWorker>,
601    group_commit: Option<PublicationGroupCommit>,
602    wal_path: Option<PathBuf>,
603}
604
605/// Identity obtained from SQLite's actual open main-database file descriptor.
606#[derive(Clone, Copy, Debug, Eq, PartialEq)]
607pub struct SqliteMainFileIdentity {
608    /// Filesystem device containing the open database file.
609    pub device: u64,
610    /// Filesystem inode of the open database file.
611    pub inode: u64,
612    /// Length observed through the open descriptor.
613    pub length: u64,
614}
615
616impl SqliteStore {
617    /// Open or create a SQLite database at the given path with default config.
618    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, SqliteStoreError> {
619        Self::open_with_config(path, SqliteStoreConfig::default())
620    }
621
622    /// Open or create a SQLite database with custom configuration.
623    pub fn open_with_config<P: AsRef<Path>>(
624        path: P,
625        config: SqliteStoreConfig,
626    ) -> Result<Self, SqliteStoreError> {
627        let path = path.as_ref().to_path_buf();
628        let conn = Connection::open_with_flags(
629            &path,
630            OpenFlags::SQLITE_OPEN_READ_WRITE
631                | OpenFlags::SQLITE_OPEN_CREATE
632                | OpenFlags::SQLITE_OPEN_NO_MUTEX,
633        )
634        .map_err(|e| {
635            SqliteStoreError::from_sqlite(e, format!("Failed to open database at {path:?}"))
636        })?;
637        Self::from_connection(conn, config.clone())?.attach_file_runtime(path, &config, None)
638    }
639
640    /// Open an existing SQLite database with default runtime configuration.
641    ///
642    /// Unlike [`Self::open`], this never creates the database file and does not
643    /// execute schema DDL. Callers must validate the required schema before
644    /// using this path.
645    pub fn open_existing<P: AsRef<Path>>(path: P) -> Result<Self, SqliteStoreError> {
646        Self::open_existing_verified(path, |_| Ok(()))
647    }
648
649    /// Open an existing database with explicit runtime configuration.
650    pub fn open_existing_with_config<P: AsRef<Path>>(
651        path: P,
652        config: SqliteStoreConfig,
653    ) -> Result<Self, SqliteStoreError> {
654        Self::open_existing_verified_with_config(path, config, |_| Ok(()))
655    }
656
657    /// Open an existing database and verify SQLite's actual main-file handle
658    /// before executing any pragma, schema statement, or other SQL.
659    #[cfg(unix)]
660    pub fn open_existing_verified<P, F>(path: P, verifier: F) -> Result<Self, SqliteStoreError>
661    where
662        P: AsRef<Path>,
663        F: FnOnce(SqliteMainFileIdentity) -> Result<(), SqliteStoreError>,
664    {
665        Self::open_existing_verified_with_config(path, SqliteStoreConfig::default(), verifier)
666    }
667
668    /// Open and verify an existing database with explicit runtime tuning.
669    #[cfg(unix)]
670    pub fn open_existing_verified_with_config<P, F>(
671        path: P,
672        config: SqliteStoreConfig,
673        verifier: F,
674    ) -> Result<Self, SqliteStoreError>
675    where
676        P: AsRef<Path>,
677        F: FnOnce(SqliteMainFileIdentity) -> Result<(), SqliteStoreError>,
678    {
679        let path = path.as_ref().to_path_buf();
680        let conn = Connection::open_with_flags(
681            &path,
682            OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX,
683        )
684        .map_err(|error| {
685            SqliteStoreError::from_sqlite(
686                error,
687                format!("Failed to open existing database at {path:?}"),
688            )
689        })?;
690        let identity = sqlite_main_file_identity(&conn)?;
691        verifier(identity)?;
692        Self::from_existing_connection(conn, config.clone())?.attach_file_runtime(
693            path,
694            &config,
695            Some(identity),
696        )
697    }
698
699    /// Non-Unix platforms cannot currently prove the SQLite VFS handle's
700    /// identity before SQL runs, so verified opens fail closed there.
701    #[cfg(not(unix))]
702    pub fn open_existing_verified<P, F>(_path: P, _verifier: F) -> Result<Self, SqliteStoreError>
703    where
704        P: AsRef<Path>,
705        F: FnOnce(SqliteMainFileIdentity) -> Result<(), SqliteStoreError>,
706    {
707        Err(SqliteStoreError::new(
708            "verified existing SQLite opens are unsupported on this platform",
709        ))
710    }
711
712    /// Non-Unix platforms cannot verify SQLite's actual main-file handle.
713    #[cfg(not(unix))]
714    pub fn open_existing_verified_with_config<P, F>(
715        _path: P,
716        _config: SqliteStoreConfig,
717        _verifier: F,
718    ) -> Result<Self, SqliteStoreError>
719    where
720        P: AsRef<Path>,
721        F: FnOnce(SqliteMainFileIdentity) -> Result<(), SqliteStoreError>,
722    {
723        Err(SqliteStoreError::new(
724            "verified existing SQLite opens are unsupported on this platform",
725        ))
726    }
727
728    /// Create an in-memory SQLite store.
729    pub fn open_in_memory() -> Result<Self, SqliteStoreError> {
730        let conn = Connection::open_in_memory()
731            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to open in-memory database"))?;
732        Self::from_connection(conn, SqliteStoreConfig::default())
733    }
734
735    fn from_connection(
736        conn: Connection,
737        config: SqliteStoreConfig,
738    ) -> Result<Self, SqliteStoreError> {
739        Self::validate_config(&config)?;
740        // Applied before WAL or schema creation so new stores use pages large
741        // enough to pack several compressed prolly nodes per B-tree page.
742        conn.pragma_update(None, "page_size", config.page_size_bytes)
743            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set page_size"))?;
744        Self::apply_runtime_config(&conn, &config)?;
745        conn.execute_batch(CREATE_TABLE_SQL)
746            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to initialize schema"))?;
747        ensure_node_encoding_column(&conn)?;
748        conn.execute_batch(CREATE_HINTS_TABLE_SQL)
749            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to initialize hint schema"))?;
750        conn.execute_batch(CREATE_ROOTS_TABLE_SQL)
751            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to initialize root schema"))?;
752        let metrics = Arc::new(SqliteMetricsInner::default());
753        Ok(Self {
754            writer: Mutex::new(conn),
755            readers: Box::new([]),
756            primary_reader_thread: OnceLock::new(),
757            primary_reader_uses_writer: config.primary_reader_uses_writer,
758            node_read_cache: ShardedNodeReadCache::new(
759                config.node_read_cache_size_bytes,
760                config.node_read_cache_shards,
761                metrics.clone(),
762            ),
763            node_compression_min_bytes: config.node_compression_min_bytes,
764            max_batch_select_keys: config.max_batch_select_keys.max(1),
765            metrics,
766            checkpoint_worker: None,
767            group_commit: (config.group_commit_delay_micros > 0).then(|| PublicationGroupCommit {
768                delay: Duration::from_micros(config.group_commit_delay_micros),
769                max_nodes: config.group_commit_max_nodes.max(1),
770                state: Mutex::new(PublicationGroupState::default()),
771            }),
772            wal_path: None,
773        })
774    }
775
776    fn from_existing_connection(
777        conn: Connection,
778        config: SqliteStoreConfig,
779    ) -> Result<Self, SqliteStoreError> {
780        Self::validate_config(&config)?;
781        Self::apply_runtime_config(&conn, &config)?;
782        let metrics = Arc::new(SqliteMetricsInner::default());
783        Ok(Self {
784            writer: Mutex::new(conn),
785            readers: Box::new([]),
786            primary_reader_thread: OnceLock::new(),
787            primary_reader_uses_writer: config.primary_reader_uses_writer,
788            node_read_cache: ShardedNodeReadCache::new(
789                config.node_read_cache_size_bytes,
790                config.node_read_cache_shards,
791                metrics.clone(),
792            ),
793            node_compression_min_bytes: config.node_compression_min_bytes,
794            max_batch_select_keys: config.max_batch_select_keys.max(1),
795            metrics,
796            checkpoint_worker: None,
797            group_commit: (config.group_commit_delay_micros > 0).then(|| PublicationGroupCommit {
798                delay: Duration::from_micros(config.group_commit_delay_micros),
799                max_nodes: config.group_commit_max_nodes.max(1),
800                state: Mutex::new(PublicationGroupState::default()),
801            }),
802            wal_path: None,
803        })
804    }
805
806    fn validate_config(config: &SqliteStoreConfig) -> Result<(), SqliteStoreError> {
807        if !(512..=65_536).contains(&config.page_size_bytes)
808            || !config.page_size_bytes.is_power_of_two()
809        {
810            return Err(SqliteStoreError::new(
811                "page_size_bytes must be a power of two from 512 through 65536",
812            ));
813        }
814        if config.reader_connections > 64 {
815            return Err(SqliteStoreError::new(
816                "reader_connections must not exceed 64",
817            ));
818        }
819        if !(1..=256).contains(&config.node_read_cache_shards) {
820            return Err(SqliteStoreError::new(
821                "node_read_cache_shards must be from 1 through 256",
822            ));
823        }
824        if config.max_batch_select_keys == 0 {
825            return Err(SqliteStoreError::new(
826                "max_batch_select_keys must be greater than zero",
827            ));
828        }
829        if config.group_commit_delay_micros > 0 && config.group_commit_max_nodes == 0 {
830            return Err(SqliteStoreError::new(
831                "group_commit_max_nodes must be greater than zero when group commit is enabled",
832            ));
833        }
834        Ok(())
835    }
836
837    fn attach_file_runtime(
838        mut self,
839        path: PathBuf,
840        config: &SqliteStoreConfig,
841        expected_identity: Option<SqliteMainFileIdentity>,
842    ) -> Result<Self, SqliteStoreError> {
843        let mut readers = Vec::with_capacity(config.reader_connections);
844        for _ in 0..config.reader_connections {
845            let reader = Connection::open_with_flags(
846                &path,
847                OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
848            )
849            .map_err(|error| {
850                SqliteStoreError::from_sqlite(error, "Failed to open SQLite read connection")
851            })?;
852            #[cfg(unix)]
853            if let Some(expected) = expected_identity {
854                let actual = sqlite_main_file_identity(&reader)?;
855                if actual.device != expected.device || actual.inode != expected.inode {
856                    return Err(SqliteStoreError::new(
857                        "SQLite read connection resolved to a different database file",
858                    ));
859                }
860            }
861            Self::apply_reader_runtime_config(&reader, config)?;
862            readers.push(Mutex::new(reader));
863        }
864        self.readers = readers.into_boxed_slice();
865        self.wal_path = Some(wal_path_for(&path));
866        if config.enable_wal && config.background_checkpoints {
867            self.writer
868                .lock()
869                .pragma_update(None, "wal_autocheckpoint", 0)
870                .map_err(|error| {
871                    SqliteStoreError::from_sqlite(error, "Failed to disable writer autocheckpoint")
872                })?;
873            self.checkpoint_worker =
874                Some(spawn_checkpoint_worker(path, config, self.metrics.clone())?);
875        }
876        Ok(self)
877    }
878
879    fn apply_runtime_config(
880        conn: &Connection,
881        config: &SqliteStoreConfig,
882    ) -> Result<(), SqliteStoreError> {
883        conn.busy_timeout(Duration::from_millis(config.busy_timeout_ms))
884            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set busy timeout"))?;
885
886        if config.enable_wal {
887            conn.pragma_update(None, "journal_mode", "WAL")
888                .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to enable WAL mode"))?;
889        }
890        if config.synchronous_normal {
891            conn.pragma_update(None, "synchronous", "NORMAL")
892                .map_err(|e| {
893                    SqliteStoreError::from_sqlite(e, "Failed to set synchronous=NORMAL")
894                })?;
895        }
896        conn.pragma_update(None, "temp_store", "MEMORY")
897            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set temp_store=MEMORY"))?;
898        let cache_size_kib = config
899            .page_cache_size_bytes
900            .div_ceil(1024)
901            .min(i64::MAX as u64);
902        let cache_size_kib = -(cache_size_kib as i64);
903        conn.pragma_update(None, "cache_size", cache_size_kib)
904            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set cache_size"))?;
905        conn.pragma_update(None, "wal_autocheckpoint", config.wal_autocheckpoint_pages)
906            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set WAL autocheckpoint"))?;
907        conn.pragma_update(
908            None,
909            "journal_size_limit",
910            config.journal_size_limit_bytes.min(i64::MAX as u64) as i64,
911        )
912        .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set journal_size_limit"))?;
913        conn.pragma_update(None, "mmap_size", config.mmap_size_bytes)
914            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set mmap_size"))?;
915        Ok(())
916    }
917
918    fn apply_reader_runtime_config(
919        conn: &Connection,
920        config: &SqliteStoreConfig,
921    ) -> Result<(), SqliteStoreError> {
922        conn.busy_timeout(Duration::from_millis(config.busy_timeout_ms))
923            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set reader busy timeout"))?;
924        conn.pragma_update(None, "query_only", true)
925            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to enable query_only"))?;
926        let reader_cache_bytes = config
927            .page_cache_size_bytes
928            .checked_div(config.reader_connections.max(1) as u64)
929            .unwrap_or(config.page_cache_size_bytes)
930            .max(1024 * 1024);
931        let cache_kib = -(reader_cache_bytes.div_ceil(1024).min(i64::MAX as u64) as i64);
932        conn.pragma_update(None, "cache_size", cache_kib)
933            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set reader cache_size"))?;
934        conn.pragma_update(None, "mmap_size", config.mmap_size_bytes)
935            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set reader mmap_size"))?;
936        Ok(())
937    }
938
939    fn connection(&self) -> Result<MutexGuard<'_, Connection>, SqliteStoreError> {
940        Ok(self.writer.lock())
941    }
942
943    fn read_connection(&self) -> Result<ReadConnectionGuard<'_>, SqliteStoreError> {
944        if self.readers.is_empty() {
945            return Ok(ReadConnectionGuard::Writer(self.writer.lock()));
946        }
947        if self.primary_reader_uses_writer {
948            let current = thread::current().id();
949            let primary = self.primary_reader_thread.get_or_init(|| current);
950            if *primary == current {
951                return Ok(ReadConnectionGuard::Writer(self.writer.lock()));
952            }
953        }
954        let index = SQLITE_READER_SLOT.with(|slot| {
955            let current = slot.get();
956            if current == usize::MAX {
957                let assigned = NEXT_SQLITE_READER_SLOT.fetch_add(1, Ordering::Relaxed);
958                slot.set(assigned);
959                assigned
960            } else {
961                current
962            }
963        }) % self.readers.len();
964        Ok(ReadConnectionGuard::Reader(self.readers[index].lock()))
965    }
966
967    fn write_committed(&self) {
968        self.metrics
969            .write_transactions
970            .fetch_add(1, Ordering::Relaxed);
971        if let Some(worker) = &self.checkpoint_worker {
972            worker.notify_write();
973        }
974    }
975
976    fn record_compression(&self, input_len: usize, encoding: i64, stored_len: usize) {
977        if encoding == NODE_ENCODING_LZ4 {
978            self.metrics
979                .compressed_input_bytes
980                .fetch_add(input_len as u64, Ordering::Relaxed);
981            self.metrics
982                .compressed_stored_bytes
983                .fetch_add(stored_len as u64, Ordering::Relaxed);
984        }
985    }
986
987    fn own_publication(publication: NodePublication<'_>) -> OwnedPublication {
988        OwnedPublication {
989            entries: publication
990                .entries()
991                .iter()
992                .map(|(key, value)| (key.to_vec(), value.to_vec()))
993                .collect(),
994            hint: publication.hint().map(|hint| {
995                (
996                    hint.namespace().to_vec(),
997                    hint.key().to_vec(),
998                    hint.value().to_vec(),
999                )
1000            }),
1001            origin: publication.origin(),
1002        }
1003    }
1004
1005    fn publish_owned_batch(
1006        &self,
1007        publications: &[&OwnedPublication],
1008    ) -> Result<(), SqliteStoreError> {
1009        let mut conn = self.connection()?;
1010        let tx = conn
1011            .transaction_with_behavior(TransactionBehavior::Immediate)
1012            .map_err(|error| {
1013                SqliteStoreError::from_sqlite(error, "Failed to start grouped node publication")
1014            })?;
1015        {
1016            let mut insert = tx.prepare_cached(INSERT_IMMUTABLE_SQL).map_err(|error| {
1017                SqliteStoreError::from_sqlite(error, "Failed to prepare grouped node publication")
1018            })?;
1019            let mut compression_scratch = Vec::new();
1020            for publication in publications {
1021                let mut ordered = publication.entries.iter().collect::<Vec<_>>();
1022                ordered.sort_by(|left, right| left.0.cmp(&right.0));
1023                for (key, input) in ordered {
1024                    let (encoding, stored) = encode_stored_node_into(
1025                        input,
1026                        &mut compression_scratch,
1027                        self.node_compression_min_bytes,
1028                    );
1029                    self.record_compression(input.len(), encoding, stored.len());
1030                    insert
1031                        .execute(params![key, encoding, stored])
1032                        .map_err(|error| {
1033                            SqliteStoreError::from_sqlite(
1034                                error,
1035                                "Failed to publish grouped immutable node",
1036                            )
1037                        })?;
1038                }
1039            }
1040        }
1041        for publication in publications {
1042            if let Some((namespace, key, value)) = &publication.hint {
1043                tx.execute(UPSERT_HINT_SQL, params![namespace, key, value])
1044                    .map_err(|error| {
1045                        SqliteStoreError::from_sqlite(
1046                            error,
1047                            "Failed to write grouped publication hint",
1048                        )
1049                    })?;
1050            }
1051        }
1052        tx.commit().map_err(|error| {
1053            SqliteStoreError::from_sqlite(error, "Failed to commit grouped node publication")
1054        })?;
1055        drop(conn);
1056        self.write_committed();
1057        self.metrics.grouped_publications.fetch_add(
1058            publications.len().saturating_sub(1) as u64,
1059            Ordering::Relaxed,
1060        );
1061        self.metrics.published_nodes.fetch_add(
1062            publications
1063                .iter()
1064                .map(|publication| publication.entries.len() as u64)
1065                .sum(),
1066            Ordering::Relaxed,
1067        );
1068        for publication in publications {
1069            if !matches!(
1070                publication.origin,
1071                PublicationOrigin::TreeBuild | PublicationOrigin::Merge
1072            ) {
1073                for (key, value) in &publication.entries {
1074                    self.node_read_cache
1075                        .insert_immutable(key, Arc::from(value.as_slice()));
1076                }
1077            }
1078        }
1079        Ok(())
1080    }
1081
1082    fn publish_grouped(&self, publication: NodePublication<'_>) -> Result<(), SqliteStoreError> {
1083        let group = self
1084            .group_commit
1085            .as_ref()
1086            .expect("grouped publication requires configured coordinator");
1087        let completion = Arc::new(PublicationCompletion {
1088            result: Mutex::new(None),
1089            ready: Condvar::new(),
1090        });
1091        let leader = {
1092            let mut state = group.state.lock();
1093            state.queue.push(PendingPublication {
1094                publication: Self::own_publication(publication),
1095                completion: completion.clone(),
1096            });
1097            if state.flushing {
1098                false
1099            } else {
1100                state.flushing = true;
1101                true
1102            }
1103        };
1104
1105        if leader {
1106            thread::sleep(group.delay);
1107            loop {
1108                let pending = {
1109                    let mut state = group.state.lock();
1110                    if state.queue.is_empty() {
1111                        state.flushing = false;
1112                        break;
1113                    }
1114                    let mut nodes = 0usize;
1115                    let take = state
1116                        .queue
1117                        .iter()
1118                        .take_while(|pending| {
1119                            if nodes > 0
1120                                && nodes.saturating_add(pending.publication.entries.len())
1121                                    > group.max_nodes
1122                            {
1123                                return false;
1124                            }
1125                            nodes = nodes.saturating_add(pending.publication.entries.len());
1126                            true
1127                        })
1128                        .count()
1129                        .max(1);
1130                    state.queue.drain(..take).collect::<Vec<_>>()
1131                };
1132                let publications = pending
1133                    .iter()
1134                    .map(|pending| &pending.publication)
1135                    .collect::<Vec<_>>();
1136                let result = self
1137                    .publish_owned_batch(&publications)
1138                    .map_err(|error| error.to_string());
1139                for pending in pending {
1140                    let mut slot = pending.completion.result.lock();
1141                    *slot = Some(result.clone());
1142                    pending.completion.ready.notify_one();
1143                }
1144            }
1145        }
1146
1147        let mut result = completion.result.lock();
1148        while result.is_none() {
1149            completion.ready.wait(&mut result);
1150        }
1151        result
1152            .take()
1153            .expect("group publication completion must be populated")
1154            .map_err(SqliteStoreError::new)
1155    }
1156
1157    /// Number of read-only connections available for concurrent reads.
1158    pub fn reader_connection_count(&self) -> usize {
1159        self.readers.len()
1160    }
1161
1162    /// Return adapter and SQLite pager counters without resetting them.
1163    pub fn metrics(&self) -> Result<SqliteStoreMetrics, SqliteStoreError> {
1164        let mut pager = SqlitePagerMetrics::default();
1165        {
1166            let writer = self.writer.lock();
1167            pager.add_connection(&writer)?;
1168        }
1169        for reader in &self.readers {
1170            pager.add_connection(&reader.lock())?;
1171        }
1172        Ok(SqliteStoreMetrics {
1173            node_cache_hits: self.metrics.node_cache_hits.load(Ordering::Relaxed),
1174            node_cache_misses: self.metrics.node_cache_misses.load(Ordering::Relaxed),
1175            node_cache_evictions: self.metrics.node_cache_evictions.load(Ordering::Relaxed),
1176            node_cache_entries: self.node_read_cache.entries(),
1177            node_cache_retained_bytes: self.node_read_cache.retained_bytes(),
1178            sql_reads: self.metrics.sql_reads.load(Ordering::Relaxed),
1179            write_transactions: self.metrics.write_transactions.load(Ordering::Relaxed),
1180            published_nodes: self.metrics.published_nodes.load(Ordering::Relaxed),
1181            grouped_publications: self.metrics.grouped_publications.load(Ordering::Relaxed),
1182            checkpoint_attempts: self.metrics.checkpoint_attempts.load(Ordering::Relaxed),
1183            checkpoint_busy: self.metrics.checkpoint_busy.load(Ordering::Relaxed),
1184            checkpointed_frames: self.metrics.checkpointed_frames.load(Ordering::Relaxed),
1185            compressed_input_bytes: self.metrics.compressed_input_bytes.load(Ordering::Relaxed),
1186            compressed_stored_bytes: self.metrics.compressed_stored_bytes.load(Ordering::Relaxed),
1187            sqlite_page_cache_hits: pager.hits,
1188            sqlite_page_cache_misses: pager.misses,
1189            sqlite_page_cache_writes: pager.writes,
1190            sqlite_page_cache_spills: pager.spills,
1191        })
1192    }
1193
1194    /// Run an explicit WAL checkpoint and return SQLite's frame counters.
1195    pub fn checkpoint(
1196        &self,
1197        mode: SqliteCheckpointMode,
1198    ) -> Result<SqliteCheckpointStats, SqliteStoreError> {
1199        let conn = self.connection()?;
1200        let stats = run_checkpoint(&conn, mode)?;
1201        record_checkpoint_metrics(&self.metrics, stats);
1202        Ok(stats)
1203    }
1204
1205    /// Return database size, freelist, and WAL allocation statistics.
1206    pub fn storage_stats(&self) -> Result<SqliteStorageStats, SqliteStoreError> {
1207        let conn = self.read_connection()?;
1208        let page_size: u64 = conn
1209            .query_row("PRAGMA page_size", [], |row| row.get(0))
1210            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to read page_size"))?;
1211        let page_count: u64 = conn
1212            .query_row("PRAGMA page_count", [], |row| row.get(0))
1213            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to read page_count"))?;
1214        let freelist_pages: u64 = conn
1215            .query_row("PRAGMA freelist_count", [], |row| row.get(0))
1216            .map_err(|error| {
1217                SqliteStoreError::from_sqlite(error, "Failed to read freelist_count")
1218            })?;
1219        let wal_bytes = self
1220            .wal_path
1221            .as_ref()
1222            .and_then(|path| std::fs::metadata(path).ok())
1223            .map_or(0, |metadata| metadata.len());
1224        Ok(SqliteStorageStats {
1225            page_size_bytes: page_size,
1226            page_count,
1227            freelist_pages,
1228            database_bytes: page_size.saturating_mul(page_count),
1229            free_bytes: page_size.saturating_mul(freelist_pages),
1230            wal_bytes,
1231        })
1232    }
1233
1234    /// Create a consistent online backup using SQLite's backup API.
1235    pub fn backup_to<P: AsRef<Path>>(&self, path: P) -> Result<(), SqliteStoreError> {
1236        let conn = self.read_connection()?;
1237        conn.backup(rusqlite::DatabaseName::Main, path, None)
1238            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to back up database"))
1239    }
1240
1241    /// Rebuild the database to reclaim free pages and reduce fragmentation.
1242    /// Callers should quiesce application traffic before invoking this method.
1243    pub fn compact(&self) -> Result<(), SqliteStoreError> {
1244        let conn = self.connection()?;
1245        conn.execute_batch("VACUUM")
1246            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to compact database"))?;
1247        drop(conn);
1248        self.write_committed();
1249        Ok(())
1250    }
1251
1252    /// Ask SQLite to perform bounded planner and connection maintenance.
1253    pub fn optimize(&self) -> Result<(), SqliteStoreError> {
1254        let conn = self.connection()?;
1255        conn.execute_batch("PRAGMA optimize")
1256            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to optimize database"))
1257    }
1258
1259    /// Run SQLite's fast structural consistency check.
1260    pub fn quick_check(&self) -> Result<(), SqliteStoreError> {
1261        let conn = self.read_connection()?;
1262        let result: String = conn
1263            .query_row("PRAGMA quick_check", [], |row| row.get(0))
1264            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to check database"))?;
1265        if result == "ok" {
1266            Ok(())
1267        } else {
1268            Err(SqliteStoreError::new(format!(
1269                "SQLite quick_check failed: {result}"
1270            )))
1271        }
1272    }
1273}
1274
1275#[derive(Default)]
1276struct SqlitePagerMetrics {
1277    hits: u64,
1278    misses: u64,
1279    writes: u64,
1280    spills: u64,
1281}
1282
1283impl SqlitePagerMetrics {
1284    fn add_connection(&mut self, conn: &Connection) -> Result<(), SqliteStoreError> {
1285        self.hits = self.hits.saturating_add(connection_db_status(
1286            conn,
1287            rusqlite::ffi::SQLITE_DBSTATUS_CACHE_HIT,
1288        )?);
1289        self.misses = self.misses.saturating_add(connection_db_status(
1290            conn,
1291            rusqlite::ffi::SQLITE_DBSTATUS_CACHE_MISS,
1292        )?);
1293        self.writes = self.writes.saturating_add(connection_db_status(
1294            conn,
1295            rusqlite::ffi::SQLITE_DBSTATUS_CACHE_WRITE,
1296        )?);
1297        self.spills = self.spills.saturating_add(connection_db_status(
1298            conn,
1299            rusqlite::ffi::SQLITE_DBSTATUS_CACHE_SPILL,
1300        )?);
1301        Ok(())
1302    }
1303}
1304
1305fn connection_db_status(conn: &Connection, operation: i32) -> Result<u64, SqliteStoreError> {
1306    let mut current = 0;
1307    let mut highwater = 0;
1308    // SAFETY: the connection remains locked for the call and both output
1309    // pointers refer to initialized integers valid for the duration of it.
1310    let code = unsafe {
1311        rusqlite::ffi::sqlite3_db_status(conn.handle(), operation, &mut current, &mut highwater, 0)
1312    };
1313    if code == rusqlite::ffi::SQLITE_OK {
1314        Ok(current.max(0) as u64)
1315    } else {
1316        Err(SqliteStoreError::new(format!(
1317            "sqlite3_db_status failed with code {code}"
1318        )))
1319    }
1320}
1321
1322fn run_checkpoint(
1323    conn: &Connection,
1324    mode: SqliteCheckpointMode,
1325) -> Result<SqliteCheckpointStats, SqliteStoreError> {
1326    let (busy, log, checkpointed): (i64, i64, i64) = conn
1327        .query_row(mode.sql(), [], |row| {
1328            Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1329        })
1330        .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to checkpoint WAL"))?;
1331    Ok(SqliteCheckpointStats {
1332        busy: busy != 0,
1333        log_frames: log.max(0) as u64,
1334        checkpointed_frames: checkpointed.max(0) as u64,
1335    })
1336}
1337
1338fn record_checkpoint_metrics(metrics: &SqliteMetricsInner, stats: SqliteCheckpointStats) {
1339    metrics.checkpoint_attempts.fetch_add(1, Ordering::Relaxed);
1340    if stats.busy {
1341        metrics.checkpoint_busy.fetch_add(1, Ordering::Relaxed);
1342    }
1343    metrics
1344        .checkpointed_frames
1345        .store(stats.checkpointed_frames, Ordering::Relaxed);
1346}
1347
1348fn spawn_checkpoint_worker(
1349    path: PathBuf,
1350    config: &SqliteStoreConfig,
1351    metrics: Arc<SqliteMetricsInner>,
1352) -> Result<CheckpointWorker, SqliteStoreError> {
1353    let conn = Connection::open_with_flags(
1354        &path,
1355        OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX,
1356    )
1357    .map_err(|error| {
1358        SqliteStoreError::from_sqlite(error, "Failed to open checkpoint connection")
1359    })?;
1360    conn.busy_timeout(Duration::from_millis(config.busy_timeout_ms))
1361        .map_err(|error| {
1362            SqliteStoreError::from_sqlite(error, "Failed to configure checkpoint timeout")
1363        })?;
1364    conn.pragma_update(None, "wal_autocheckpoint", 0)
1365        .map_err(|error| {
1366            SqliteStoreError::from_sqlite(error, "Failed to disable checkpoint autocheckpoint")
1367        })?;
1368    conn.pragma_update(
1369        None,
1370        "journal_size_limit",
1371        config.journal_size_limit_bytes.min(i64::MAX as u64) as i64,
1372    )
1373    .map_err(|error| {
1374        SqliteStoreError::from_sqlite(error, "Failed to configure checkpoint journal limit")
1375    })?;
1376    let wal_path = wal_path_for(&path);
1377    let threshold = config.checkpoint_wal_bytes;
1378    let interval = Duration::from_millis(config.checkpoint_interval_ms.max(1));
1379    let (sender, receiver) = mpsc::channel();
1380    let handle = thread::Builder::new()
1381        .name("prolly-sqlite-checkpoint".to_string())
1382        .spawn(move || {
1383            let mut writes_pending = false;
1384            let mut next_inspection = Instant::now() + interval;
1385            loop {
1386                let signal = receiver
1387                    .recv_timeout(next_inspection.saturating_duration_since(Instant::now()));
1388                if matches!(signal, Ok(CheckpointSignal::Shutdown)) {
1389                    if let Ok(stats) = run_checkpoint(&conn, SqliteCheckpointMode::Truncate) {
1390                        record_checkpoint_metrics(&metrics, stats);
1391                    }
1392                    break;
1393                }
1394                if matches!(signal, Ok(CheckpointSignal::WriteCommitted)) {
1395                    writes_pending = true;
1396                }
1397                while let Ok(signal) = receiver.try_recv() {
1398                    if matches!(signal, CheckpointSignal::Shutdown) {
1399                        if let Ok(stats) = run_checkpoint(&conn, SqliteCheckpointMode::Truncate) {
1400                            record_checkpoint_metrics(&metrics, stats);
1401                        }
1402                        return;
1403                    }
1404                    writes_pending = true;
1405                }
1406                if Instant::now() < next_inspection {
1407                    continue;
1408                }
1409                next_inspection = Instant::now() + interval;
1410                if !writes_pending {
1411                    continue;
1412                }
1413                let wal_bytes = std::fs::metadata(&wal_path)
1414                    .map(|metadata| metadata.len())
1415                    .unwrap_or(0);
1416                if wal_bytes >= threshold {
1417                    if let Ok(stats) = run_checkpoint(&conn, SqliteCheckpointMode::Passive) {
1418                        record_checkpoint_metrics(&metrics, stats);
1419                        writes_pending = stats.busy || stats.checkpointed_frames < stats.log_frames;
1420                    }
1421                }
1422            }
1423        })
1424        .map_err(|error| {
1425            SqliteStoreError::new(format!("failed to spawn checkpoint worker: {error}"))
1426        })?;
1427    Ok(CheckpointWorker {
1428        sender,
1429        handle: Some(handle),
1430    })
1431}
1432
1433fn wal_path_for(path: &Path) -> PathBuf {
1434    let mut wal_path = path.as_os_str().to_os_string();
1435    wal_path.push("-wal");
1436    PathBuf::from(wal_path)
1437}
1438
1439fn ensure_node_encoding_column(conn: &Connection) -> Result<(), SqliteStoreError> {
1440    let has_encoding = {
1441        let mut stmt = conn
1442            .prepare("PRAGMA table_info(prolly_nodes)")
1443            .map_err(|error| {
1444                SqliteStoreError::from_sqlite(error, "Failed to inspect node schema")
1445            })?;
1446        let columns = stmt
1447            .query_map([], |row| row.get::<_, String>(1))
1448            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to query node schema"))?;
1449        let mut found = false;
1450        for column in columns {
1451            if column.map_err(|error| {
1452                SqliteStoreError::from_sqlite(error, "Failed to read node schema")
1453            })? == "encoding"
1454            {
1455                found = true;
1456                break;
1457            }
1458        }
1459        found
1460    };
1461    if !has_encoding {
1462        conn.execute(
1463            "ALTER TABLE prolly_nodes ADD COLUMN encoding INTEGER NOT NULL DEFAULT 0",
1464            [],
1465        )
1466        .map_err(|error| {
1467            SqliteStoreError::from_sqlite(error, "Failed to migrate node encoding schema")
1468        })?;
1469    }
1470    Ok(())
1471}
1472
1473const NODE_ENCODING_RAW: i64 = 0;
1474const NODE_ENCODING_LZ4: i64 = 1;
1475fn encode_stored_node(node: &[u8], min_compressible_bytes: usize) -> (i64, Vec<u8>) {
1476    let mut scratch = Vec::new();
1477    let (encoding, stored) = encode_stored_node_into(node, &mut scratch, min_compressible_bytes);
1478    (encoding, stored.to_vec())
1479}
1480
1481fn encode_stored_node_into<'a>(
1482    node: &'a [u8],
1483    scratch: &'a mut Vec<u8>,
1484    min_compressible_bytes: usize,
1485) -> (i64, &'a [u8]) {
1486    if node.len() < min_compressible_bytes || node.len() > u32::MAX as usize {
1487        return (NODE_ENCODING_RAW, node);
1488    }
1489    let maximum = 4 + lz4_flex::block::get_maximum_output_size(node.len());
1490    scratch.clear();
1491    scratch.resize(maximum, 0);
1492    scratch[..4].copy_from_slice(&(node.len() as u32).to_le_bytes());
1493    let compressed_len = lz4_flex::block::compress_into(node, &mut scratch[4..])
1494        .expect("maximum LZ4 output size is sufficient");
1495    let stored_len = 4 + compressed_len;
1496    if stored_len >= node.len() {
1497        return (NODE_ENCODING_RAW, node);
1498    }
1499    scratch.truncate(stored_len);
1500    (NODE_ENCODING_LZ4, scratch)
1501}
1502
1503fn decode_stored_node_ref(encoding: i64, node: &[u8]) -> Result<Vec<u8>, SqliteStoreError> {
1504    match encoding {
1505        NODE_ENCODING_RAW => Ok(node.to_vec()),
1506        NODE_ENCODING_LZ4 => lz4_flex::decompress_size_prepended(node)
1507            .map_err(|error| SqliteStoreError::new(format!("failed to decompress node: {error}"))),
1508        other => Err(SqliteStoreError::new(format!(
1509            "unsupported node encoding {other}"
1510        ))),
1511    }
1512}
1513
1514fn select_nodes_ordered_unique(
1515    conn: &Connection,
1516    keys: &[&[u8]],
1517    configured_max_keys: usize,
1518) -> Result<Vec<Option<Vec<u8>>>, SqliteStoreError> {
1519    if keys.is_empty() {
1520        return Ok(Vec::new());
1521    }
1522    if keys.len() == 1 {
1523        let mut stmt = conn.prepare_cached(SELECT_SQL).map_err(|error| {
1524            SqliteStoreError::from_sqlite(error, "Failed to prepare point read")
1525        })?;
1526        let mut rows = stmt
1527            .query(params![keys[0]])
1528            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to read key"))?;
1529        let value = match rows
1530            .next()
1531            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to read key"))?
1532        {
1533            Some(row) => {
1534                let encoding = row.get::<_, i64>(0).map_err(|error| {
1535                    SqliteStoreError::from_sqlite(error, "Failed to read node encoding")
1536                })?;
1537                let node_value = row.get_ref(1).map_err(|error| {
1538                    SqliteStoreError::from_sqlite(error, "Failed to borrow node bytes")
1539                })?;
1540                let node = node_value.as_blob().map_err(|error| {
1541                    SqliteStoreError::new(format!("Failed to borrow node bytes: {error}"))
1542                })?;
1543                Some(decode_stored_node_ref(encoding, node)?)
1544            }
1545            None => None,
1546        };
1547        return Ok(vec![value]);
1548    }
1549
1550    let positions = keys
1551        .iter()
1552        .enumerate()
1553        .map(|(index, key)| (*key, index))
1554        .collect::<HashMap<_, _>>();
1555    let mut values = vec![None; keys.len()];
1556
1557    // SAFETY: the connection remains locked for this call and sqlite3_limit
1558    // only reads the current per-connection variable bound for a negative value.
1559    let sqlite_max_keys = unsafe {
1560        rusqlite::ffi::sqlite3_limit(
1561            conn.handle(),
1562            rusqlite::ffi::SQLITE_LIMIT_VARIABLE_NUMBER,
1563            -1,
1564        )
1565    }
1566    .max(1) as usize;
1567    let max_keys = configured_max_keys.max(1).min(sqlite_max_keys);
1568    for chunk in keys.chunks(max_keys) {
1569        let placeholders = (1..=chunk.len())
1570            .map(|index| format!("?{index}"))
1571            .collect::<Vec<_>>()
1572            .join(",");
1573        let sql =
1574            format!("SELECT cid, encoding, node FROM prolly_nodes WHERE cid IN ({placeholders})");
1575        let mut stmt = conn.prepare_cached(&sql).map_err(|error| {
1576            SqliteStoreError::from_sqlite(error, "Failed to prepare multi-key read")
1577        })?;
1578        let mut rows = stmt
1579            .query(rusqlite::params_from_iter(chunk.iter().copied()))
1580            .map_err(|error| {
1581                SqliteStoreError::from_sqlite(error, "Failed to execute multi-key read")
1582            })?;
1583        while let Some(row) = rows.next().map_err(|error| {
1584            SqliteStoreError::from_sqlite(error, "Failed to read multi-key result")
1585        })? {
1586            let key_value = row.get_ref(0).map_err(|error| {
1587                SqliteStoreError::from_sqlite(error, "Failed to read multi-key result")
1588            })?;
1589            let key = key_value.as_blob().map_err(|error| {
1590                SqliteStoreError::new(format!("Failed to borrow node key: {error}"))
1591            })?;
1592            let encoding = row.get::<_, i64>(1).map_err(|error| {
1593                SqliteStoreError::from_sqlite(error, "Failed to read node encoding")
1594            })?;
1595            let node_value = row.get_ref(2).map_err(|error| {
1596                SqliteStoreError::from_sqlite(error, "Failed to borrow node bytes")
1597            })?;
1598            let node = node_value.as_blob().map_err(|error| {
1599                SqliteStoreError::new(format!("Failed to borrow node bytes: {error}"))
1600            })?;
1601            let index = positions.get(key).copied().ok_or_else(|| {
1602                SqliteStoreError::new("multi-key read returned an unrequested key")
1603            })?;
1604            values[index] = Some(decode_stored_node_ref(encoding, node)?);
1605        }
1606    }
1607
1608    Ok(values)
1609}
1610
1611#[cfg(unix)]
1612/// Inspect SQLite's actual open main-database descriptor without executing SQL.
1613pub fn sqlite_main_file_identity(
1614    conn: &Connection,
1615) -> Result<SqliteMainFileIdentity, SqliteStoreError> {
1616    use std::ffi::{c_int, c_void};
1617    use std::fs::File;
1618    use std::os::fd::BorrowedFd;
1619    use std::os::unix::fs::MetadataExt;
1620
1621    // Every bundled Unix SQLite VFS begins its concrete `unixFile` with this
1622    // stable prefix. `SQLITE_FCNTL_FILE_POINTER` returns the actual main-file
1623    // object owned by this connection, not a pathname-derived approximation.
1624    #[repr(C)]
1625    struct UnixFilePrefix {
1626        methods: *const rusqlite::ffi::sqlite3_io_methods,
1627        vfs: *mut rusqlite::ffi::sqlite3_vfs,
1628        inode: *mut c_void,
1629        fd: c_int,
1630    }
1631
1632    let mut sqlite_file: *mut rusqlite::ffi::sqlite3_file = std::ptr::null_mut();
1633    // SAFETY: `conn` remains alive for the call, `main` is NUL terminated, and
1634    // SQLite writes one sqlite3_file pointer into `sqlite_file` for this opcode.
1635    let rc = unsafe {
1636        rusqlite::ffi::sqlite3_file_control(
1637            conn.handle(),
1638            c"main".as_ptr(),
1639            rusqlite::ffi::SQLITE_FCNTL_FILE_POINTER,
1640            (&mut sqlite_file as *mut *mut rusqlite::ffi::sqlite3_file).cast(),
1641        )
1642    };
1643    if rc != rusqlite::ffi::SQLITE_OK || sqlite_file.is_null() {
1644        return Err(SqliteStoreError::new(format!(
1645            "SQLite did not expose its main-file handle (code {rc})"
1646        )));
1647    }
1648    // SAFETY: the bundled Unix VFS concrete file begins with UnixFilePrefix,
1649    // and SQLite retains the descriptor for the lifetime of this connection.
1650    let fd = unsafe { (*(sqlite_file.cast::<UnixFilePrefix>())).fd };
1651    // Duplicate the descriptor so the temporary File cannot close SQLite's
1652    // owned descriptor when it is dropped.
1653    let borrowed = unsafe { BorrowedFd::borrow_raw(fd) };
1654    let owned = borrowed.try_clone_to_owned().map_err(|error| {
1655        SqliteStoreError::new(format!(
1656            "failed to duplicate SQLite main-file handle: {error}"
1657        ))
1658    })?;
1659    let metadata = File::from(owned).metadata().map_err(|error| {
1660        SqliteStoreError::new(format!("failed to stat SQLite main-file handle: {error}"))
1661    })?;
1662    Ok(SqliteMainFileIdentity {
1663        device: metadata.dev(),
1664        inode: metadata.ino(),
1665        length: metadata.len(),
1666    })
1667}
1668
1669impl Store for SqliteStore {
1670    type Error = SqliteStoreError;
1671
1672    fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
1673        if let Some(value) = self.node_read_cache.get(key) {
1674            return Ok(Some(value.as_ref().to_vec()));
1675        }
1676        let conn = self.read_connection()?;
1677        self.metrics.sql_reads.fetch_add(1, Ordering::Relaxed);
1678        let mut stmt = conn
1679            .prepare_cached(SELECT_SQL)
1680            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to prepare point read"))?;
1681        let mut rows = stmt
1682            .query(params![key])
1683            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read key"))?;
1684        let Some(row) = rows
1685            .next()
1686            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read key"))?
1687        else {
1688            return Ok(None);
1689        };
1690        let encoding = row.get::<_, i64>(0).map_err(|error| {
1691            SqliteStoreError::from_sqlite(error, "Failed to read node encoding")
1692        })?;
1693        let node_value = row
1694            .get_ref(1)
1695            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to borrow node bytes"))?;
1696        let node = node_value.as_blob().map_err(|error| {
1697            SqliteStoreError::new(format!("Failed to borrow node bytes: {error}"))
1698        })?;
1699        let decoded: Arc<[u8]> = Arc::from(decode_stored_node_ref(encoding, node)?);
1700        self.node_read_cache.insert(key, decoded.clone());
1701        Ok(Some(decoded.as_ref().to_vec()))
1702    }
1703
1704    fn get_shared(&self, key: &[u8]) -> Result<Option<Arc<[u8]>>, Self::Error> {
1705        if let Some(value) = self.node_read_cache.get(key) {
1706            return Ok(Some(value));
1707        }
1708        let conn = self.read_connection()?;
1709        self.metrics.sql_reads.fetch_add(1, Ordering::Relaxed);
1710        let mut stmt = conn
1711            .prepare_cached(SELECT_SQL)
1712            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to prepare shared point read"))?;
1713        let mut rows = stmt
1714            .query(params![key])
1715            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read shared key"))?;
1716        let Some(row) = rows
1717            .next()
1718            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read shared key"))?
1719        else {
1720            return Ok(None);
1721        };
1722        let encoding = row.get::<_, i64>(0).map_err(|error| {
1723            SqliteStoreError::from_sqlite(error, "Failed to read node encoding")
1724        })?;
1725        let node_value = row
1726            .get_ref(1)
1727            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to borrow node bytes"))?;
1728        let node = node_value.as_blob().map_err(|error| {
1729            SqliteStoreError::new(format!("Failed to borrow node bytes: {error}"))
1730        })?;
1731        let decoded: Arc<[u8]> = Arc::from(decode_stored_node_ref(encoding, node)?);
1732        self.node_read_cache.insert(key, decoded.clone());
1733        Ok(Some(decoded))
1734    }
1735
1736    fn has_native_shared_reads(&self) -> bool {
1737        true
1738    }
1739
1740    fn put(&self, key: &[u8], value: &[u8]) -> Result<(), Self::Error> {
1741        let conn = self.connection()?;
1742        let (encoding, stored) = encode_stored_node(value, self.node_compression_min_bytes);
1743        self.record_compression(value.len(), encoding, stored.len());
1744        conn.execute(UPSERT_SQL, params![key, encoding, stored])
1745            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to write key"))?;
1746        drop(conn);
1747        self.write_committed();
1748        self.node_read_cache.insert(key, Arc::from(value));
1749        Ok(())
1750    }
1751
1752    fn delete(&self, key: &[u8]) -> Result<(), Self::Error> {
1753        let conn = self.connection()?;
1754        conn.execute(DELETE_SQL, params![key])
1755            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to delete key"))?;
1756        drop(conn);
1757        self.write_committed();
1758        self.node_read_cache.remove(key);
1759        Ok(())
1760    }
1761
1762    fn batch(&self, ops: &[BatchOp]) -> Result<(), Self::Error> {
1763        let mut conn = self.connection()?;
1764        let tx = conn
1765            .transaction_with_behavior(TransactionBehavior::Immediate)
1766            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to start transaction"))?;
1767
1768        {
1769            let mut upsert = tx
1770                .prepare_cached(UPSERT_SQL)
1771                .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to prepare batch write"))?;
1772            let mut delete = tx
1773                .prepare_cached(DELETE_SQL)
1774                .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to prepare batch delete"))?;
1775            let mut compression_scratch = Vec::new();
1776
1777            for op in ops {
1778                match op {
1779                    BatchOp::Upsert { key, value } => {
1780                        let input_len = value.len();
1781                        let (encoding, stored) = encode_stored_node_into(
1782                            value,
1783                            &mut compression_scratch,
1784                            self.node_compression_min_bytes,
1785                        );
1786                        self.record_compression(input_len, encoding, stored.len());
1787                        upsert
1788                            .execute(params![key, encoding, stored])
1789                            .map_err(|e| {
1790                                SqliteStoreError::from_sqlite(e, "Failed to write key in batch")
1791                            })?;
1792                    }
1793                    BatchOp::Delete { key } => {
1794                        delete.execute(params![key]).map_err(|e| {
1795                            SqliteStoreError::from_sqlite(e, "Failed to delete key in batch")
1796                        })?;
1797                    }
1798                }
1799            }
1800        }
1801
1802        tx.commit()
1803            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to commit transaction"))?;
1804        drop(conn);
1805        self.write_committed();
1806        for op in ops {
1807            match op {
1808                BatchOp::Upsert { key, .. } | BatchOp::Delete { key } => {
1809                    self.node_read_cache.remove(key)
1810                }
1811            }
1812        }
1813        Ok(())
1814    }
1815
1816    fn batch_get(&self, keys: &[&[u8]]) -> Result<HashMap<Vec<u8>, Vec<u8>>, Self::Error> {
1817        let plan = OrderedBatchReadPlan::new(keys);
1818        let values = self.batch_get_shared_ordered_unique(plan.unique_keys())?;
1819        let mut results = HashMap::with_capacity(plan.unique_keys().len());
1820        for (key, value) in plan.unique_keys().iter().zip(values) {
1821            if let Some(value) = value {
1822                results.insert(key.to_vec(), value.as_ref().to_vec());
1823            }
1824        }
1825
1826        Ok(results)
1827    }
1828
1829    fn batch_get_ordered(&self, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>, Self::Error> {
1830        let plan = OrderedBatchReadPlan::new(keys);
1831        let unique_values = self
1832            .batch_get_shared_ordered_unique(plan.unique_keys())?
1833            .into_iter()
1834            .map(|value| value.map(|value| value.as_ref().to_vec()))
1835            .collect();
1836        Ok(plan.expand_owned(unique_values))
1837    }
1838
1839    fn batch_get_ordered_unique(
1840        &self,
1841        keys: &[&[u8]],
1842    ) -> Result<Vec<Option<Vec<u8>>>, Self::Error> {
1843        self.batch_get_shared_ordered_unique(keys).map(|values| {
1844            values
1845                .into_iter()
1846                .map(|value| value.map(|value| value.as_ref().to_vec()))
1847                .collect()
1848        })
1849    }
1850
1851    fn batch_get_shared_ordered_unique(
1852        &self,
1853        keys: &[&[u8]],
1854    ) -> Result<Vec<Option<Arc<[u8]>>>, Self::Error> {
1855        let mut values = vec![None; keys.len()];
1856        let mut missing = Vec::new();
1857        for (position, key) in keys.iter().enumerate() {
1858            match self.node_read_cache.get(key) {
1859                Some(value) => values[position] = Some(value),
1860                None => missing.push((position, *key)),
1861            }
1862        }
1863        if missing.is_empty() {
1864            return Ok(values);
1865        }
1866        let missing_keys = missing.iter().map(|(_, key)| *key).collect::<Vec<_>>();
1867        let conn = self.read_connection()?;
1868        self.metrics.sql_reads.fetch_add(1, Ordering::Relaxed);
1869        let loaded = select_nodes_ordered_unique(&conn, &missing_keys, self.max_batch_select_keys)?;
1870        drop(conn);
1871        for ((position, key), loaded) in missing.into_iter().zip(loaded) {
1872            if let Some(loaded) = loaded {
1873                let loaded: Arc<[u8]> = Arc::from(loaded);
1874                self.node_read_cache.insert(key, loaded.clone());
1875                values[position] = Some(loaded);
1876            }
1877        }
1878        Ok(values)
1879    }
1880
1881    fn prefers_batch_reads(&self) -> bool {
1882        true
1883    }
1884
1885    fn batch_put(&self, entries: &[(&[u8], &[u8])]) -> Result<(), Self::Error> {
1886        let mut conn = self.connection()?;
1887        let tx = conn
1888            .transaction_with_behavior(TransactionBehavior::Immediate)
1889            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to start transaction"))?;
1890
1891        {
1892            let mut stmt = tx.prepare_cached(UPSERT_SQL).map_err(|e| {
1893                SqliteStoreError::from_sqlite(e, "Failed to prepare batch_put write")
1894            })?;
1895            let mut ordered = entries.iter().collect::<Vec<_>>();
1896            ordered.sort_by(|left, right| left.0.cmp(right.0));
1897            let mut compression_scratch = Vec::new();
1898            for &&(key, value) in &ordered {
1899                let input_len = value.len();
1900                let (encoding, stored) = encode_stored_node_into(
1901                    value,
1902                    &mut compression_scratch,
1903                    self.node_compression_min_bytes,
1904                );
1905                self.record_compression(input_len, encoding, stored.len());
1906                stmt.execute(params![key, encoding, stored]).map_err(|e| {
1907                    SqliteStoreError::from_sqlite(e, "Failed to write key in batch_put")
1908                })?;
1909            }
1910        }
1911
1912        tx.commit()
1913            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to commit transaction"))?;
1914        drop(conn);
1915        self.write_committed();
1916        for &(key, _) in entries {
1917            self.node_read_cache.remove(key);
1918        }
1919        Ok(())
1920    }
1921
1922    fn supports_hints(&self) -> bool {
1923        true
1924    }
1925
1926    fn get_hint(&self, namespace: &[u8], key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
1927        let conn = self.read_connection()?;
1928        self.metrics.sql_reads.fetch_add(1, Ordering::Relaxed);
1929        conn.query_row(
1930            "SELECT value FROM prolly_hints WHERE namespace = ?1 AND key = ?2",
1931            params![namespace, key],
1932            |row| row.get(0),
1933        )
1934        .optional()
1935        .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read hint"))
1936    }
1937
1938    fn put_hint(&self, namespace: &[u8], key: &[u8], value: &[u8]) -> Result<(), Self::Error> {
1939        let conn = self.connection()?;
1940        conn.execute(
1941            "\
1942            INSERT INTO prolly_hints (namespace, key, value) \
1943            VALUES (?1, ?2, ?3) \
1944            ON CONFLICT(namespace, key) DO UPDATE SET value = excluded.value",
1945            params![namespace, key, value],
1946        )
1947        .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to write hint"))?;
1948        drop(conn);
1949        self.write_committed();
1950        Ok(())
1951    }
1952
1953    fn batch_put_with_hint(
1954        &self,
1955        entries: &[(&[u8], &[u8])],
1956        namespace: &[u8],
1957        key: &[u8],
1958        value: &[u8],
1959    ) -> Result<(), Self::Error> {
1960        let mut conn = self.connection()?;
1961        let tx = conn
1962            .transaction_with_behavior(TransactionBehavior::Immediate)
1963            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to start transaction"))?;
1964
1965        {
1966            let mut upsert_node = tx.prepare_cached(UPSERT_SQL).map_err(|e| {
1967                SqliteStoreError::from_sqlite(e, "Failed to prepare batch_put write")
1968            })?;
1969            let mut ordered = entries.iter().collect::<Vec<_>>();
1970            ordered.sort_by(|left, right| left.0.cmp(right.0));
1971            let mut compression_scratch = Vec::new();
1972            for &&(key, value) in &ordered {
1973                let input_len = value.len();
1974                let (encoding, stored) = encode_stored_node_into(
1975                    value,
1976                    &mut compression_scratch,
1977                    self.node_compression_min_bytes,
1978                );
1979                self.record_compression(input_len, encoding, stored.len());
1980                upsert_node
1981                    .execute(params![key, encoding, stored])
1982                    .map_err(|e| {
1983                        SqliteStoreError::from_sqlite(e, "Failed to write key in batch_put")
1984                    })?;
1985            }
1986        }
1987
1988        tx.execute(
1989            "\
1990            INSERT INTO prolly_hints (namespace, key, value) \
1991            VALUES (?1, ?2, ?3) \
1992            ON CONFLICT(namespace, key) DO UPDATE SET value = excluded.value",
1993            params![namespace, key, value],
1994        )
1995        .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to write hint in batch_put"))?;
1996
1997        tx.commit()
1998            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to commit transaction"))?;
1999        drop(conn);
2000        self.write_committed();
2001        for &(key, _) in entries {
2002            self.node_read_cache.remove(key);
2003        }
2004        Ok(())
2005    }
2006
2007    fn publish_nodes(&self, publication: NodePublication<'_>) -> Result<(), Self::Error> {
2008        if self.group_commit.is_some() {
2009            return self.publish_grouped(publication);
2010        }
2011        let mut conn = self.connection()?;
2012        let tx = conn
2013            .transaction_with_behavior(TransactionBehavior::Immediate)
2014            .map_err(|error| {
2015                SqliteStoreError::from_sqlite(error, "Failed to start node publication")
2016            })?;
2017        {
2018            // Publication keys are content IDs for immutable nodes. Ignoring an
2019            // existing key avoids rewriting shared nodes carried into a new
2020            // tree while preserving the Store upsert contract on batch_put.
2021            let mut insert = tx.prepare_cached(INSERT_IMMUTABLE_SQL).map_err(|error| {
2022                SqliteStoreError::from_sqlite(error, "Failed to prepare node publication")
2023            })?;
2024            let mut ordered = publication.entries().iter().collect::<Vec<_>>();
2025            ordered.sort_by(|left, right| left.0.cmp(right.0));
2026            let mut compression_scratch = Vec::new();
2027            for &&(key, value) in &ordered {
2028                let input_len = value.len();
2029                let (encoding, stored) = encode_stored_node_into(
2030                    value,
2031                    &mut compression_scratch,
2032                    self.node_compression_min_bytes,
2033                );
2034                self.record_compression(input_len, encoding, stored.len());
2035                insert
2036                    .execute(params![key, encoding, stored])
2037                    .map_err(|error| {
2038                        SqliteStoreError::from_sqlite(error, "Failed to publish immutable node")
2039                    })?;
2040            }
2041        }
2042        if let Some(hint) = publication.hint() {
2043            tx.execute(
2044                UPSERT_HINT_SQL,
2045                params![hint.namespace(), hint.key(), hint.value()],
2046            )
2047            .map_err(|error| {
2048                SqliteStoreError::from_sqlite(error, "Failed to write publication hint")
2049            })?;
2050        }
2051        tx.commit().map_err(|error| {
2052            SqliteStoreError::from_sqlite(error, "Failed to commit node publication")
2053        })?;
2054        drop(conn);
2055        self.write_committed();
2056        self.metrics
2057            .published_nodes
2058            .fetch_add(publication.entries().len() as u64, Ordering::Relaxed);
2059        // Batch and merge branches are commonly read immediately. A full tree
2060        // build already leaves its decoded nodes in the manager cache, so
2061        // duplicating every serialized node here only adds publication cost.
2062        if !matches!(
2063            publication.origin(),
2064            PublicationOrigin::TreeBuild | PublicationOrigin::Merge
2065        ) {
2066            for &(key, value) in publication.entries() {
2067                self.node_read_cache.insert_immutable(key, Arc::from(value));
2068            }
2069        }
2070        Ok(())
2071    }
2072}
2073
2074impl NodeStoreScan for SqliteStore {
2075    type Error = SqliteStoreError;
2076
2077    fn list_node_cids(&self) -> Result<Vec<Cid>, Self::Error> {
2078        let conn = self.read_connection()?;
2079        self.metrics.sql_reads.fetch_add(1, Ordering::Relaxed);
2080        let mut stmt = conn
2081            .prepare_cached(SELECT_NODE_CIDS_SQL)
2082            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to prepare node CID listing"))?;
2083        let rows = stmt
2084            .query_map([], |row| row.get::<_, Vec<u8>>(0))
2085            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to list node CIDs"))?;
2086
2087        let mut cids = Vec::new();
2088        for row in rows {
2089            let key = row
2090                .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read listed node CID"))?;
2091            cids.push(cid_from_store_key(&key, "SQLite node").map_err(SqliteStoreError::new)?);
2092        }
2093        sort_cids(&mut cids);
2094        Ok(cids)
2095    }
2096}
2097
2098impl ManifestStore for SqliteStore {
2099    type Error = SqliteStoreError;
2100
2101    fn get_root(&self, name: &[u8]) -> Result<Option<RootManifest>, Self::Error> {
2102        let conn = self.read_connection()?;
2103        self.metrics.sql_reads.fetch_add(1, Ordering::Relaxed);
2104        let bytes = conn
2105            .query_row(SELECT_ROOT_SQL, params![name], |row| row.get(0))
2106            .optional()
2107            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read root manifest"))?;
2108        decode_root_manifest(bytes)
2109    }
2110
2111    fn put_root(&self, name: &[u8], manifest: &RootManifest) -> Result<(), Self::Error> {
2112        let conn = self.connection()?;
2113        let bytes = encode_root_manifest(manifest)?;
2114        conn.execute(UPSERT_ROOT_SQL, params![name, bytes])
2115            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to write root manifest"))?;
2116        drop(conn);
2117        self.write_committed();
2118        Ok(())
2119    }
2120
2121    fn delete_root(&self, name: &[u8]) -> Result<(), Self::Error> {
2122        let conn = self.connection()?;
2123        conn.execute(DELETE_ROOT_SQL, params![name])
2124            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to delete root manifest"))?;
2125        drop(conn);
2126        self.write_committed();
2127        Ok(())
2128    }
2129
2130    fn compare_and_swap_root(
2131        &self,
2132        name: &[u8],
2133        expected: Option<&RootManifest>,
2134        new: Option<&RootManifest>,
2135    ) -> Result<ManifestUpdate, Self::Error> {
2136        let expected_bytes = expected.map(encode_root_manifest).transpose()?;
2137        let new_bytes = new.map(encode_root_manifest).transpose()?;
2138
2139        let mut conn = self.connection()?;
2140        let tx = conn
2141            .transaction_with_behavior(TransactionBehavior::Immediate)
2142            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to start root transaction"))?;
2143
2144        let current_bytes = tx
2145            .query_row(SELECT_ROOT_SQL, params![name], |row| row.get(0))
2146            .optional()
2147            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read root manifest"))?;
2148
2149        if current_bytes.as_deref() != expected_bytes.as_deref() {
2150            return Ok(ManifestUpdate::Conflict {
2151                current: decode_root_manifest(current_bytes)?,
2152            });
2153        }
2154
2155        match new_bytes {
2156            Some(bytes) => {
2157                tx.execute(UPSERT_ROOT_SQL, params![name, bytes])
2158                    .map_err(|e| {
2159                        SqliteStoreError::from_sqlite(e, "Failed to write root manifest")
2160                    })?;
2161            }
2162            None => {
2163                tx.execute(DELETE_ROOT_SQL, params![name]).map_err(|e| {
2164                    SqliteStoreError::from_sqlite(e, "Failed to delete root manifest")
2165                })?;
2166            }
2167        }
2168
2169        tx.commit()
2170            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to commit root transaction"))?;
2171        drop(conn);
2172        self.write_committed();
2173        Ok(ManifestUpdate::Applied)
2174    }
2175}
2176
2177impl ManifestStoreScan for SqliteStore {
2178    fn list_roots(&self) -> Result<Vec<NamedRootManifest>, Self::Error> {
2179        let conn = self.read_connection()?;
2180        self.metrics.sql_reads.fetch_add(1, Ordering::Relaxed);
2181        let mut stmt = conn.prepare_cached(SELECT_ROOTS_SQL).map_err(|e| {
2182            SqliteStoreError::from_sqlite(e, "Failed to prepare root manifest listing")
2183        })?;
2184        let rows = stmt
2185            .query_map([], |row| {
2186                Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?))
2187            })
2188            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to list root manifests"))?;
2189
2190        let mut roots = Vec::new();
2191        for row in rows {
2192            let (name, bytes) = row.map_err(|e| {
2193                SqliteStoreError::from_sqlite(e, "Failed to read listed root manifest")
2194            })?;
2195            let manifest = RootManifest::from_bytes(&bytes)
2196                .map_err(|err| SqliteStoreError::new(err.to_string()))?;
2197            roots.push(NamedRootManifest::new(name, manifest));
2198        }
2199        sort_named_root_manifests(&mut roots);
2200        Ok(roots)
2201    }
2202}
2203
2204impl TransactionalStore for SqliteStore {
2205    fn supports_transactions(&self) -> bool {
2206        true
2207    }
2208
2209    fn commit_transaction(
2210        &self,
2211        node_writes: &[TransactionNodeWrite],
2212        root_conditions: &[RootCondition],
2213        root_writes: &[RootWrite],
2214    ) -> Result<TransactionUpdate, Error> {
2215        let mut conn = self
2216            .connection()
2217            .map_err(|err| Error::Store(Box::new(err)))?;
2218        let tx = conn
2219            .transaction_with_behavior(TransactionBehavior::Immediate)
2220            .map_err(|err| {
2221                Error::Store(Box::new(SqliteStoreError::from_sqlite(
2222                    err,
2223                    "Failed to start transaction commit",
2224                )))
2225            })?;
2226
2227        for condition in root_conditions {
2228            let current_bytes = tx
2229                .query_row(SELECT_ROOT_SQL, params![condition.name], |row| row.get(0))
2230                .optional()
2231                .map_err(|err| {
2232                    Error::Store(Box::new(SqliteStoreError::from_sqlite(
2233                        err,
2234                        "Failed to read root manifest during transaction commit",
2235                    )))
2236                })?;
2237            let current =
2238                decode_root_manifest(current_bytes).map_err(|err| Error::Store(Box::new(err)))?;
2239            if current != condition.expected {
2240                return Ok(TransactionUpdate::Conflict(Box::new(
2241                    TransactionConflict::new(
2242                        condition.name.clone(),
2243                        condition.expected.clone(),
2244                        current,
2245                    ),
2246                )));
2247            }
2248        }
2249
2250        {
2251            let mut upsert_node = tx.prepare_cached(UPSERT_SQL).map_err(|err| {
2252                Error::Store(Box::new(SqliteStoreError::from_sqlite(
2253                    err,
2254                    "Failed to prepare transaction node write",
2255                )))
2256            })?;
2257            let mut delete_node = tx.prepare_cached(DELETE_SQL).map_err(|err| {
2258                Error::Store(Box::new(SqliteStoreError::from_sqlite(
2259                    err,
2260                    "Failed to prepare transaction node delete",
2261                )))
2262            })?;
2263            for write in node_writes {
2264                match write {
2265                    TransactionNodeWrite::Upsert { key, value } => {
2266                        let (encoding, stored) =
2267                            encode_stored_node(value, self.node_compression_min_bytes);
2268                        self.record_compression(value.len(), encoding, stored.len());
2269                        upsert_node
2270                            .execute(params![key, encoding, stored])
2271                            .map_err(|err| {
2272                                Error::Store(Box::new(SqliteStoreError::from_sqlite(
2273                                    err,
2274                                    "Failed to write node during transaction commit",
2275                                )))
2276                            })?;
2277                    }
2278                    TransactionNodeWrite::Delete { key } => {
2279                        delete_node.execute(params![key]).map_err(|err| {
2280                            Error::Store(Box::new(SqliteStoreError::from_sqlite(
2281                                err,
2282                                "Failed to delete node during transaction commit",
2283                            )))
2284                        })?;
2285                    }
2286                }
2287            }
2288        }
2289
2290        {
2291            let mut upsert_root = tx.prepare_cached(UPSERT_ROOT_SQL).map_err(|err| {
2292                Error::Store(Box::new(SqliteStoreError::from_sqlite(
2293                    err,
2294                    "Failed to prepare transaction root write",
2295                )))
2296            })?;
2297            let mut delete_root = tx.prepare_cached(DELETE_ROOT_SQL).map_err(|err| {
2298                Error::Store(Box::new(SqliteStoreError::from_sqlite(
2299                    err,
2300                    "Failed to prepare transaction root delete",
2301                )))
2302            })?;
2303            for write in root_writes {
2304                match write {
2305                    RootWrite::Put { name, manifest } => {
2306                        let bytes = encode_root_manifest(manifest)
2307                            .map_err(|err| Error::Store(Box::new(err)))?;
2308                        upsert_root.execute(params![name, bytes]).map_err(|err| {
2309                            Error::Store(Box::new(SqliteStoreError::from_sqlite(
2310                                err,
2311                                "Failed to write root during transaction commit",
2312                            )))
2313                        })?;
2314                    }
2315                    RootWrite::Delete { name } => {
2316                        delete_root.execute(params![name]).map_err(|err| {
2317                            Error::Store(Box::new(SqliteStoreError::from_sqlite(
2318                                err,
2319                                "Failed to delete root during transaction commit",
2320                            )))
2321                        })?;
2322                    }
2323                }
2324            }
2325        }
2326
2327        tx.commit().map_err(|err| {
2328            Error::Store(Box::new(SqliteStoreError::from_sqlite(
2329                err,
2330                "Failed to commit transaction",
2331            )))
2332        })?;
2333        drop(conn);
2334        self.write_committed();
2335        for write in node_writes {
2336            match write {
2337                TransactionNodeWrite::Upsert { key, .. } | TransactionNodeWrite::Delete { key } => {
2338                    self.node_read_cache.remove(key)
2339                }
2340            }
2341        }
2342        Ok(TransactionUpdate::Applied {
2343            nodes_written: node_writes.len(),
2344            roots_written: root_writes.len(),
2345        })
2346    }
2347}
2348
2349fn encode_root_manifest(manifest: &RootManifest) -> Result<Vec<u8>, SqliteStoreError> {
2350    manifest
2351        .to_bytes()
2352        .map_err(|e| SqliteStoreError::new(format!("failed to encode root manifest: {e}")))
2353}
2354
2355fn decode_root_manifest(bytes: Option<Vec<u8>>) -> Result<Option<RootManifest>, SqliteStoreError> {
2356    bytes
2357        .as_deref()
2358        .map(RootManifest::from_bytes)
2359        .transpose()
2360        .map_err(|e| SqliteStoreError::new(format!("failed to decode root manifest: {e}")))
2361}
2362
2363#[cfg(test)]
2364mod tests {
2365    use super::*;
2366
2367    #[test]
2368    fn sqlite_store_put_get_delete() {
2369        let store = SqliteStore::open_in_memory().unwrap();
2370
2371        store.put(b"key", b"value").unwrap();
2372        assert_eq!(store.get(b"key").unwrap(), Some(b"value".to_vec()));
2373
2374        store.delete(b"key").unwrap();
2375        assert_eq!(store.get(b"key").unwrap(), None);
2376    }
2377
2378    #[test]
2379    fn sqlite_store_reuses_shared_node_reads() {
2380        let store = SqliteStore::open_in_memory().unwrap();
2381        store.put(b"key", b"value").unwrap();
2382
2383        let first = store.get_shared(b"key").unwrap().unwrap();
2384        let second = store.get_shared(b"key").unwrap().unwrap();
2385
2386        assert!(Arc::ptr_eq(&first, &second));
2387    }
2388
2389    #[test]
2390    fn sqlite_store_applies_publication_pragmas_and_rowid_schema() {
2391        let store = SqliteStore::open_in_memory().unwrap();
2392        let conn = store.connection().unwrap();
2393        let page_size: u32 = conn
2394            .query_row("PRAGMA page_size", [], |row| row.get(0))
2395            .unwrap();
2396        let cache_size: i64 = conn
2397            .query_row("PRAGMA cache_size", [], |row| row.get(0))
2398            .unwrap();
2399        let wal_autocheckpoint: u32 = conn
2400            .query_row("PRAGMA wal_autocheckpoint", [], |row| row.get(0))
2401            .unwrap();
2402        let schema: String = conn
2403            .query_row(
2404                "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'prolly_nodes'",
2405                [],
2406                |row| row.get(0),
2407            )
2408            .unwrap();
2409
2410        assert_eq!(page_size, 64 * 1024);
2411        assert_eq!(cache_size, -64 * 1024);
2412        assert_eq!(wal_autocheckpoint, 32 * 1024);
2413        assert!(!schema.to_ascii_uppercase().contains("WITHOUT ROWID"));
2414    }
2415
2416    #[test]
2417    fn sqlite_store_rejects_invalid_page_size() {
2418        let config = SqliteStoreConfig {
2419            page_size_bytes: 1_000,
2420            ..SqliteStoreConfig::default()
2421        };
2422        let error = SqliteStore::from_connection(Connection::open_in_memory().unwrap(), config)
2423            .err()
2424            .expect("invalid page size must fail");
2425        assert!(error.to_string().contains("page_size_bytes"));
2426    }
2427
2428    #[test]
2429    fn sqlite_store_batch_is_order_preserving_for_reads() {
2430        let store = SqliteStore::open_in_memory().unwrap();
2431        let ops = vec![
2432            BatchOp::Upsert {
2433                key: b"a",
2434                value: b"1",
2435            },
2436            BatchOp::Upsert {
2437                key: b"b",
2438                value: b"2",
2439            },
2440            BatchOp::Upsert {
2441                key: b"c",
2442                value: b"3",
2443            },
2444        ];
2445
2446        store.batch(&ops).unwrap();
2447
2448        let keys: Vec<&[u8]> = vec![b"c", b"missing", b"a", b"c", b"missing", b"b"];
2449        assert_eq!(
2450            store.batch_get_ordered(&keys).unwrap(),
2451            vec![
2452                Some(b"3".to_vec()),
2453                None,
2454                Some(b"1".to_vec()),
2455                Some(b"3".to_vec()),
2456                None,
2457                Some(b"2".to_vec())
2458            ]
2459        );
2460    }
2461
2462    #[test]
2463    fn sqlite_store_batch_put_updates_existing_keys() {
2464        let store = SqliteStore::open_in_memory().unwrap();
2465
2466        store.put(b"a", b"old").unwrap();
2467        store
2468            .batch_put(&[(b"a".as_slice(), b"new".as_slice()), (b"b", b"2")])
2469            .unwrap();
2470
2471        assert_eq!(store.get(b"a").unwrap(), Some(b"new".to_vec()));
2472        assert_eq!(store.get(b"b").unwrap(), Some(b"2".to_vec()));
2473    }
2474
2475    #[test]
2476    fn sqlite_store_persists_hints_separately_from_nodes() {
2477        let store = SqliteStore::open_in_memory().unwrap();
2478
2479        store.put_hint(b"rightmost", b"root", b"hint-v1").unwrap();
2480        assert_eq!(
2481            store.get_hint(b"rightmost", b"root").unwrap(),
2482            Some(b"hint-v1".to_vec())
2483        );
2484        assert_eq!(store.get_hint(b"rightmost", b"missing").unwrap(), None);
2485        assert_eq!(store.get(b"root").unwrap(), None);
2486
2487        store.put_hint(b"rightmost", b"root", b"hint-v2").unwrap();
2488        assert_eq!(
2489            store.get_hint(b"rightmost", b"root").unwrap(),
2490            Some(b"hint-v2".to_vec())
2491        );
2492    }
2493
2494    #[test]
2495    fn sqlite_store_compresses_repetitive_nodes_transparently() {
2496        let store = SqliteStore::open_in_memory().unwrap();
2497        let node = vec![b'x'; 16 * 1024];
2498
2499        store.put(b"compressed", &node).unwrap();
2500
2501        let conn = store.connection().unwrap();
2502        let (encoding, stored_bytes): (i64, usize) = conn
2503            .query_row(
2504                "SELECT encoding, length(node) FROM prolly_nodes WHERE cid = ?1",
2505                params![b"compressed"],
2506                |row| Ok((row.get(0)?, row.get(1)?)),
2507            )
2508            .unwrap();
2509        drop(conn);
2510        assert_eq!(encoding, NODE_ENCODING_LZ4);
2511        assert!(stored_bytes < node.len());
2512        assert_eq!(store.get(b"compressed").unwrap(), Some(node));
2513    }
2514
2515    #[test]
2516    fn sqlite_store_honors_node_compression_threshold() {
2517        let config = SqliteStoreConfig {
2518            node_compression_min_bytes: 32 * 1024,
2519            ..SqliteStoreConfig::default()
2520        };
2521        let store =
2522            SqliteStore::from_connection(Connection::open_in_memory().unwrap(), config).unwrap();
2523        let node = vec![b'x'; 16 * 1024];
2524
2525        store.put(b"raw", &node).unwrap();
2526
2527        let conn = store.connection().unwrap();
2528        let encoding: i64 = conn
2529            .query_row(
2530                "SELECT encoding FROM prolly_nodes WHERE cid = ?1",
2531                params![b"raw"],
2532                |row| row.get(0),
2533            )
2534            .unwrap();
2535        assert_eq!(encoding, NODE_ENCODING_RAW);
2536    }
2537
2538    #[test]
2539    fn sqlite_store_migrates_legacy_raw_node_tables() {
2540        let conn = Connection::open_in_memory().unwrap();
2541        conn.execute_batch(
2542            "CREATE TABLE prolly_nodes (
2543                cid BLOB PRIMARY KEY NOT NULL,
2544                node BLOB NOT NULL
2545            ) WITHOUT ROWID;
2546            INSERT INTO prolly_nodes (cid, node) VALUES (x'6c6567616379', x'726177');",
2547        )
2548        .unwrap();
2549
2550        let store = SqliteStore::from_connection(conn, SqliteStoreConfig::default()).unwrap();
2551
2552        assert_eq!(store.get(b"legacy").unwrap(), Some(b"raw".to_vec()));
2553        let conn = store.connection().unwrap();
2554        let encoding: i64 = conn
2555            .query_row(
2556                "SELECT encoding FROM prolly_nodes WHERE cid = ?1",
2557                params![b"legacy"],
2558                |row| row.get(0),
2559            )
2560            .unwrap();
2561        assert_eq!(encoding, NODE_ENCODING_RAW);
2562    }
2563
2564    #[test]
2565    fn file_store_opens_reader_pool_and_disables_writer_autocheckpoint() {
2566        let directory = tempfile::tempdir().unwrap();
2567        let config = SqliteStoreConfig {
2568            reader_connections: 3,
2569            background_checkpoints: true,
2570            ..SqliteStoreConfig::default()
2571        };
2572        let store =
2573            SqliteStore::open_with_config(directory.path().join("store.db"), config).unwrap();
2574
2575        assert_eq!(store.reader_connection_count(), 3);
2576        let writer = store.connection().unwrap();
2577        let autocheckpoint: u32 = writer
2578            .query_row("PRAGMA wal_autocheckpoint", [], |row| row.get(0))
2579            .unwrap();
2580        assert_eq!(autocheckpoint, 0);
2581    }
2582
2583    #[test]
2584    fn primary_reader_reuses_writer_and_other_threads_use_the_pool() {
2585        let directory = tempfile::tempdir().unwrap();
2586        let config = SqliteStoreConfig {
2587            background_checkpoints: false,
2588            reader_connections: 2,
2589            ..SqliteStoreConfig::default()
2590        };
2591        let store = Arc::new(
2592            SqliteStore::open_with_config(directory.path().join("store.db"), config).unwrap(),
2593        );
2594
2595        assert!(matches!(
2596            store.read_connection().unwrap(),
2597            ReadConnectionGuard::Writer(_)
2598        ));
2599        let other = {
2600            let store = store.clone();
2601            thread::spawn(move || {
2602                matches!(
2603                    store.read_connection().unwrap(),
2604                    ReadConnectionGuard::Reader(_)
2605                )
2606            })
2607        };
2608        assert!(other.join().unwrap());
2609    }
2610
2611    #[test]
2612    fn reader_pool_serves_reads_while_the_writer_commits() {
2613        let directory = tempfile::tempdir().unwrap();
2614        let config = SqliteStoreConfig {
2615            background_checkpoints: false,
2616            node_read_cache_size_bytes: 0,
2617            reader_connections: 4,
2618            ..SqliteStoreConfig::default()
2619        };
2620        let store = Arc::new(
2621            SqliteStore::open_with_config(directory.path().join("store.db"), config).unwrap(),
2622        );
2623        store.put(b"stable", b"value").unwrap();
2624        let barrier = Arc::new(std::sync::Barrier::new(5));
2625        let readers = (0..4)
2626            .map(|_| {
2627                let store = store.clone();
2628                let barrier = barrier.clone();
2629                thread::spawn(move || {
2630                    barrier.wait();
2631                    for _ in 0..100 {
2632                        assert_eq!(store.get(b"stable").unwrap(), Some(b"value".to_vec()));
2633                    }
2634                })
2635            })
2636            .collect::<Vec<_>>();
2637        barrier.wait();
2638        for index in 0u32..100 {
2639            store
2640                .put(&index.to_be_bytes(), &index.wrapping_add(1).to_be_bytes())
2641                .unwrap();
2642        }
2643        for reader in readers {
2644            reader.join().unwrap();
2645        }
2646
2647        assert!(store.metrics().unwrap().sql_reads >= 400);
2648    }
2649
2650    #[test]
2651    fn background_checkpoint_worker_processes_committed_wal_frames() {
2652        let directory = tempfile::tempdir().unwrap();
2653        let config = SqliteStoreConfig {
2654            background_checkpoints: true,
2655            checkpoint_interval_ms: 5,
2656            checkpoint_wal_bytes: 1,
2657            reader_connections: 1,
2658            ..SqliteStoreConfig::default()
2659        };
2660        let store =
2661            SqliteStore::open_with_config(directory.path().join("store.db"), config).unwrap();
2662        store.put(b"key", b"value").unwrap();
2663
2664        let deadline = Instant::now() + Duration::from_secs(2);
2665        while store.metrics().unwrap().checkpoint_attempts == 0 && Instant::now() < deadline {
2666            thread::sleep(Duration::from_millis(5));
2667        }
2668
2669        let metrics = store.metrics().unwrap();
2670        assert!(metrics.checkpoint_attempts >= 1);
2671        assert!(metrics.checkpointed_frames >= 1);
2672    }
2673
2674    #[test]
2675    fn adaptive_batch_reads_cross_the_legacy_256_key_boundary() {
2676        let directory = tempfile::tempdir().unwrap();
2677        let config = SqliteStoreConfig {
2678            background_checkpoints: false,
2679            max_batch_select_keys: 512,
2680            node_read_cache_size_bytes: 0,
2681            reader_connections: 2,
2682            ..SqliteStoreConfig::default()
2683        };
2684        let store =
2685            SqliteStore::open_with_config(directory.path().join("store.db"), config).unwrap();
2686        let keys = (0u32..600)
2687            .map(|index| index.to_be_bytes().to_vec())
2688            .collect::<Vec<_>>();
2689        let values = (0u32..600)
2690            .map(|index| format!("value-{index}").into_bytes())
2691            .collect::<Vec<_>>();
2692        let entries = keys
2693            .iter()
2694            .zip(&values)
2695            .map(|(key, value)| (key.as_slice(), value.as_slice()))
2696            .collect::<Vec<_>>();
2697        store.batch_put(&entries).unwrap();
2698        let key_refs = keys.iter().map(Vec::as_slice).collect::<Vec<_>>();
2699
2700        let observed = store.batch_get_ordered_unique(&key_refs).unwrap();
2701
2702        assert_eq!(observed.len(), values.len());
2703        assert!(observed
2704            .iter()
2705            .zip(values)
2706            .all(|(observed, expected)| observed.as_deref() == Some(expected.as_slice())));
2707    }
2708
2709    #[test]
2710    fn sharded_cache_eviction_and_sqlite_metrics_are_reported() {
2711        let config = SqliteStoreConfig {
2712            node_read_cache_size_bytes: 16,
2713            node_read_cache_shards: 1,
2714            ..SqliteStoreConfig::default()
2715        };
2716        let store =
2717            SqliteStore::from_connection(Connection::open_in_memory().unwrap(), config).unwrap();
2718        store.put(b"a", b"123456789012").unwrap();
2719        store.put(b"b", b"abcdefghijkl").unwrap();
2720        assert_eq!(store.get(b"a").unwrap(), Some(b"123456789012".to_vec()));
2721
2722        let metrics = store.metrics().unwrap();
2723        assert!(metrics.node_cache_evictions >= 1);
2724        assert!(metrics.node_cache_misses >= 1);
2725        assert!(metrics.sql_reads >= 1);
2726        assert!(metrics.sqlite_page_cache_hits + metrics.sqlite_page_cache_misses > 0);
2727    }
2728
2729    #[test]
2730    fn group_commit_combines_concurrent_publications() {
2731        let directory = tempfile::tempdir().unwrap();
2732        let config = SqliteStoreConfig {
2733            background_checkpoints: false,
2734            group_commit_delay_micros: 20_000,
2735            group_commit_max_nodes: 64,
2736            reader_connections: 2,
2737            ..SqliteStoreConfig::default()
2738        };
2739        let store = Arc::new(
2740            SqliteStore::open_with_config(directory.path().join("store.db"), config).unwrap(),
2741        );
2742        let barrier = Arc::new(std::sync::Barrier::new(8));
2743        let threads = (0u8..8)
2744            .map(|index| {
2745                let store = store.clone();
2746                let barrier = barrier.clone();
2747                thread::spawn(move || {
2748                    let key = vec![index; 32];
2749                    let value = vec![index.wrapping_add(1); 128];
2750                    let entries = [(key.as_slice(), value.as_slice())];
2751                    barrier.wait();
2752                    store
2753                        .publish_nodes(NodePublication::new(
2754                            &entries,
2755                            PublicationOrigin::PointUpsert,
2756                        ))
2757                        .unwrap();
2758                })
2759            })
2760            .collect::<Vec<_>>();
2761        for thread in threads {
2762            thread.join().unwrap();
2763        }
2764
2765        let metrics = store.metrics().unwrap();
2766        assert_eq!(metrics.published_nodes, 8);
2767        assert!(metrics.grouped_publications >= 1);
2768        assert!(metrics.write_transactions < 8);
2769    }
2770
2771    #[test]
2772    fn checkpoint_backup_compaction_and_quick_check_are_available() {
2773        let directory = tempfile::tempdir().unwrap();
2774        let database = directory.path().join("store.db");
2775        let backup = directory.path().join("backup.db");
2776        let config = SqliteStoreConfig {
2777            background_checkpoints: false,
2778            reader_connections: 2,
2779            ..SqliteStoreConfig::default()
2780        };
2781        let store = SqliteStore::open_with_config(&database, config).unwrap();
2782        store.put(b"key", b"value").unwrap();
2783
2784        let checkpoint = store.checkpoint(SqliteCheckpointMode::Passive).unwrap();
2785        assert!(checkpoint.checkpointed_frames <= checkpoint.log_frames);
2786        store.quick_check().unwrap();
2787        store.backup_to(&backup).unwrap();
2788        store.compact().unwrap();
2789        let stats = store.storage_stats().unwrap();
2790        assert!(stats.page_count > 0);
2791
2792        let backup_store = SqliteStore::open_existing(&backup).unwrap();
2793        assert_eq!(backup_store.get(b"key").unwrap(), Some(b"value".to_vec()));
2794    }
2795}