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