Skip to main content

prolly_store_sqlite/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::collections::{hash_map::Entry, HashMap};
4use std::path::Path;
5use std::sync::Arc;
6use std::time::Duration;
7
8use ahash::AHashMap;
9use parking_lot::{Mutex, MutexGuard};
10#[cfg(unix)]
11use rusqlite::OpenFlags;
12use rusqlite::{params, Connection, OptionalExtension, TransactionBehavior};
13
14use prolly::{
15    BatchOp, Cid, Error, ManifestStore, ManifestStoreScan, ManifestUpdate, NamedRootManifest,
16    NodePublication, NodeStoreScan, PublicationOrigin, RootCondition, RootManifest, RootWrite,
17    Store, TransactionConflict, TransactionNodeWrite, TransactionUpdate, TransactionalStore,
18};
19
20struct NodeReadCache {
21    values: AHashMap<Vec<u8>, Arc<[u8]>>,
22    retained_bytes: usize,
23    max_bytes: usize,
24}
25
26impl NodeReadCache {
27    fn new(max_bytes: usize) -> Self {
28        Self {
29            values: AHashMap::new(),
30            retained_bytes: 0,
31            max_bytes,
32        }
33    }
34
35    fn get(&self, key: &[u8]) -> Option<Arc<[u8]>> {
36        self.values.get(key).cloned()
37    }
38
39    fn insert(&mut self, key: &[u8], value: Arc<[u8]>) {
40        if self.max_bytes == 0 || value.len() > self.max_bytes {
41            return;
42        }
43        let previous = self.values.remove(key);
44        self.retained_bytes = self
45            .retained_bytes
46            .saturating_sub(previous.as_ref().map_or(0, |value| value.len()));
47        if self.retained_bytes.saturating_add(value.len()) > self.max_bytes {
48            self.values.clear();
49            self.retained_bytes = 0;
50        }
51        self.retained_bytes = self.retained_bytes.saturating_add(value.len());
52        self.values.insert(key.to_vec(), value);
53    }
54
55    fn insert_immutable(&mut self, key: &[u8], value: Arc<[u8]>) {
56        if self.max_bytes == 0 || value.len() > self.max_bytes {
57            return;
58        }
59        if self.retained_bytes.saturating_add(value.len()) > self.max_bytes {
60            self.values.clear();
61            self.retained_bytes = 0;
62        }
63        if let Entry::Vacant(entry) = self.values.entry(key.to_vec()) {
64            self.retained_bytes = self.retained_bytes.saturating_add(value.len());
65            entry.insert(value);
66        }
67    }
68
69    fn remove(&mut self, key: &[u8]) {
70        if let Some(value) = self.values.remove(key) {
71            self.retained_bytes = self.retained_bytes.saturating_sub(value.len());
72        }
73    }
74}
75
76struct OrderedBatchReadPlan<'a> {
77    unique_keys: Vec<&'a [u8]>,
78    positions: Option<Vec<usize>>,
79}
80
81impl<'a> OrderedBatchReadPlan<'a> {
82    fn new(keys: &[&'a [u8]]) -> Self {
83        let mut unique_indexes = HashMap::with_capacity(keys.len());
84        let mut unique_keys = Vec::with_capacity(keys.len());
85        let mut positions = None;
86        for key in keys {
87            match unique_indexes.entry(*key) {
88                Entry::Occupied(entry) => positions
89                    .get_or_insert_with(|| (0..unique_keys.len()).collect::<Vec<_>>())
90                    .push(*entry.get()),
91                Entry::Vacant(entry) => {
92                    let index = unique_keys.len();
93                    unique_keys.push(*key);
94                    if let Some(positions) = positions.as_mut() {
95                        positions.push(index);
96                    }
97                    entry.insert(index);
98                }
99            }
100        }
101        Self {
102            unique_keys,
103            positions,
104        }
105    }
106
107    fn unique_keys(&self) -> &[&'a [u8]] {
108        &self.unique_keys
109    }
110
111    fn expand_owned<T: Clone>(&self, values: Vec<Option<T>>) -> Vec<Option<T>> {
112        match &self.positions {
113            Some(positions) => positions
114                .iter()
115                .map(|&index| values[index].clone())
116                .collect(),
117            None => values,
118        }
119    }
120}
121
122fn cid_from_store_key(key: &[u8], context: &str) -> Result<Cid, String> {
123    let bytes: [u8; 32] = key.try_into().map_err(|_| {
124        format!(
125            "{context} key has invalid CID length {}, expected 32",
126            key.len()
127        )
128    })?;
129    Ok(Cid(bytes))
130}
131
132fn sort_cids(cids: &mut [Cid]) {
133    cids.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
134}
135
136fn sort_named_root_manifests(roots: &mut [NamedRootManifest]) {
137    roots.sort_by(|left, right| left.name.cmp(&right.name));
138}
139
140const CREATE_TABLE_SQL: &str = "\
141CREATE TABLE IF NOT EXISTS prolly_nodes (
142    cid      BLOB PRIMARY KEY NOT NULL,
143    encoding INTEGER NOT NULL DEFAULT 0,
144    node     BLOB NOT NULL
145);";
146
147const CREATE_HINTS_TABLE_SQL: &str = "\
148CREATE TABLE IF NOT EXISTS prolly_hints (
149    namespace BLOB NOT NULL,
150    key       BLOB NOT NULL,
151    value     BLOB NOT NULL,
152    PRIMARY KEY (namespace, key)
153) WITHOUT ROWID;";
154
155const CREATE_ROOTS_TABLE_SQL: &str = "\
156CREATE TABLE IF NOT EXISTS prolly_roots (
157    name     BLOB PRIMARY KEY NOT NULL,
158    manifest BLOB NOT NULL
159) WITHOUT ROWID;";
160
161const SELECT_SQL: &str = "SELECT encoding, node FROM prolly_nodes WHERE cid = ?1";
162const MAX_BATCH_SELECT_KEYS: usize = 256;
163const SELECT_NODE_CIDS_SQL: &str = "SELECT cid FROM prolly_nodes ORDER BY cid";
164const UPSERT_SQL: &str = "\
165INSERT INTO prolly_nodes (cid, encoding, node)
166VALUES (?1, ?2, ?3)
167ON CONFLICT(cid) DO UPDATE SET encoding = excluded.encoding, node = excluded.node";
168const INSERT_IMMUTABLE_SQL: &str = "\
169INSERT OR IGNORE INTO prolly_nodes (cid, encoding, node)
170VALUES (?1, ?2, ?3)";
171const DELETE_SQL: &str = "DELETE FROM prolly_nodes WHERE cid = ?1";
172const UPSERT_HINT_SQL: &str = "\
173INSERT INTO prolly_hints (namespace, key, value)
174VALUES (?1, ?2, ?3)
175ON CONFLICT(namespace, key) DO UPDATE SET value = excluded.value";
176const SELECT_ROOT_SQL: &str = "SELECT manifest FROM prolly_roots WHERE name = ?1";
177const SELECT_ROOTS_SQL: &str = "SELECT name, manifest FROM prolly_roots ORDER BY name";
178const UPSERT_ROOT_SQL: &str = "\
179INSERT INTO prolly_roots (name, manifest)
180VALUES (?1, ?2)
181ON CONFLICT(name) DO UPDATE SET manifest = excluded.manifest";
182const DELETE_ROOT_SQL: &str = "DELETE FROM prolly_roots WHERE name = ?1";
183
184/// Configuration options for [`SqliteStore`].
185#[derive(Debug, Clone)]
186pub struct SqliteStoreConfig {
187    /// Busy timeout in milliseconds for contended SQLite locks.
188    pub busy_timeout_ms: u64,
189    /// Enable WAL journaling for file-backed databases.
190    pub enable_wal: bool,
191    /// Set SQLite synchronous mode to NORMAL when applying default pragmas.
192    pub synchronous_normal: bool,
193    /// Page size requested when creating a new SQLite database.
194    ///
195    /// Existing databases retain the page size stored in their file header.
196    pub page_size_bytes: u32,
197    /// Maximum bytes retained in SQLite's page cache for this connection.
198    ///
199    /// A larger cache prevents dirty B-tree pages from being spilled and
200    /// rewritten repeatedly during large prolly-node publications.
201    pub page_cache_size_bytes: u64,
202    /// Number of WAL pages that triggers an automatic checkpoint.
203    ///
204    /// This should be large enough that a normal node publication commits
205    /// without performing checkpoint I/O in its latency-sensitive path.
206    pub wal_autocheckpoint_pages: u32,
207    /// Maximum decoded node bytes retained by the adapter.
208    ///
209    /// This cache complements SQLite's encoded page cache and lets immutable
210    /// nodes written earlier in the process satisfy later shared reads without
211    /// another SQL lookup or decompression.
212    pub node_read_cache_size_bytes: usize,
213    /// Minimum serialized node size considered for LZ4 compression.
214    ///
215    /// Smaller values favor database size and cold I/O; larger values avoid
216    /// compression CPU for latency-sensitive in-process publication.
217    pub node_compression_min_bytes: usize,
218    /// Maximum database bytes SQLite may access through memory-mapped reads.
219    ///
220    /// This avoids per-page read syscalls for large immutable prolly nodes while
221    /// retaining SQLite's normal transactional and durability guarantees.
222    pub mmap_size_bytes: u64,
223}
224
225impl Default for SqliteStoreConfig {
226    fn default() -> Self {
227        Self {
228            busy_timeout_ms: 5_000,
229            enable_wal: true,
230            synchronous_normal: true,
231            page_size_bytes: 64 * 1024,
232            page_cache_size_bytes: 64 * 1024 * 1024,
233            wal_autocheckpoint_pages: 32 * 1024,
234            node_read_cache_size_bytes: 128 * 1024 * 1024,
235            node_compression_min_bytes: 8 * 1024,
236            mmap_size_bytes: 256 * 1024 * 1024,
237        }
238    }
239}
240
241/// Error type for SQLite store operations.
242#[derive(Debug)]
243pub struct SqliteStoreError {
244    message: String,
245    source: Option<rusqlite::Error>,
246}
247
248impl SqliteStoreError {
249    /// Create a new error with a message.
250    pub fn new(message: impl Into<String>) -> Self {
251        Self {
252            message: message.into(),
253            source: None,
254        }
255    }
256
257    /// Create a new error from a rusqlite error.
258    pub fn from_sqlite(err: rusqlite::Error, context: impl Into<String>) -> Self {
259        Self {
260            message: format!("{}: {}", context.into(), err),
261            source: Some(err),
262        }
263    }
264}
265
266impl std::fmt::Display for SqliteStoreError {
267    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268        write!(f, "SQLite error: {}", self.message)
269    }
270}
271
272impl std::error::Error for SqliteStoreError {
273    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
274        self.source
275            .as_ref()
276            .map(|e| e as &(dyn std::error::Error + 'static))
277    }
278}
279
280impl From<rusqlite::Error> for SqliteStoreError {
281    fn from(err: rusqlite::Error) -> Self {
282        Self {
283            message: err.to_string(),
284            source: Some(err),
285        }
286    }
287}
288
289/// SQLite-backed storage backend for Prolly Trees.
290///
291/// This store persists content-addressed nodes in a single SQLite table and
292/// supports atomic batch operations through transactions.
293pub struct SqliteStore {
294    conn: Mutex<Connection>,
295    node_read_cache: Mutex<NodeReadCache>,
296    node_compression_min_bytes: usize,
297}
298
299/// Identity obtained from SQLite's actual open main-database file descriptor.
300#[derive(Clone, Copy, Debug, Eq, PartialEq)]
301pub struct SqliteMainFileIdentity {
302    /// Filesystem device containing the open database file.
303    pub device: u64,
304    /// Filesystem inode of the open database file.
305    pub inode: u64,
306    /// Length observed through the open descriptor.
307    pub length: u64,
308}
309
310impl SqliteStore {
311    /// Open or create a SQLite database at the given path with default config.
312    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, SqliteStoreError> {
313        Self::open_with_config(path, SqliteStoreConfig::default())
314    }
315
316    /// Open or create a SQLite database with custom configuration.
317    pub fn open_with_config<P: AsRef<Path>>(
318        path: P,
319        config: SqliteStoreConfig,
320    ) -> Result<Self, SqliteStoreError> {
321        let conn = Connection::open(path.as_ref()).map_err(|e| {
322            SqliteStoreError::from_sqlite(
323                e,
324                format!("Failed to open database at {:?}", path.as_ref()),
325            )
326        })?;
327        Self::from_connection(conn, config)
328    }
329
330    /// Open an existing SQLite database with default runtime configuration.
331    ///
332    /// Unlike [`Self::open`], this never creates the database file and does not
333    /// execute schema DDL. Callers must validate the required schema before
334    /// using this path.
335    pub fn open_existing<P: AsRef<Path>>(path: P) -> Result<Self, SqliteStoreError> {
336        Self::open_existing_verified(path, |_| Ok(()))
337    }
338
339    /// Open an existing database and verify SQLite's actual main-file handle
340    /// before executing any pragma, schema statement, or other SQL.
341    #[cfg(unix)]
342    pub fn open_existing_verified<P, F>(path: P, verifier: F) -> Result<Self, SqliteStoreError>
343    where
344        P: AsRef<Path>,
345        F: FnOnce(SqliteMainFileIdentity) -> Result<(), SqliteStoreError>,
346    {
347        let conn = Connection::open_with_flags(
348            path.as_ref(),
349            OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX,
350        )
351        .map_err(|error| {
352            SqliteStoreError::from_sqlite(
353                error,
354                format!("Failed to open existing database at {:?}", path.as_ref()),
355            )
356        })?;
357        verifier(sqlite_main_file_identity(&conn)?)?;
358        Self::from_existing_connection(conn, SqliteStoreConfig::default())
359    }
360
361    /// Non-Unix platforms cannot currently prove the SQLite VFS handle's
362    /// identity before SQL runs, so verified opens fail closed there.
363    #[cfg(not(unix))]
364    pub fn open_existing_verified<P, F>(_path: P, _verifier: F) -> Result<Self, SqliteStoreError>
365    where
366        P: AsRef<Path>,
367        F: FnOnce(SqliteMainFileIdentity) -> Result<(), SqliteStoreError>,
368    {
369        Err(SqliteStoreError::new(
370            "verified existing SQLite opens are unsupported on this platform",
371        ))
372    }
373
374    /// Create an in-memory SQLite store.
375    pub fn open_in_memory() -> Result<Self, SqliteStoreError> {
376        let conn = Connection::open_in_memory()
377            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to open in-memory database"))?;
378        Self::from_connection(conn, SqliteStoreConfig::default())
379    }
380
381    fn from_connection(
382        conn: Connection,
383        config: SqliteStoreConfig,
384    ) -> Result<Self, SqliteStoreError> {
385        if !(512..=65_536).contains(&config.page_size_bytes)
386            || !config.page_size_bytes.is_power_of_two()
387        {
388            return Err(SqliteStoreError::new(
389                "page_size_bytes must be a power of two from 512 through 65536",
390            ));
391        }
392        // Applied before WAL or schema creation so new stores use pages large
393        // enough to pack several compressed prolly nodes per B-tree page.
394        conn.pragma_update(None, "page_size", config.page_size_bytes)
395            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set page_size"))?;
396        Self::apply_runtime_config(&conn, &config)?;
397        conn.execute_batch(CREATE_TABLE_SQL)
398            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to initialize schema"))?;
399        ensure_node_encoding_column(&conn)?;
400        conn.execute_batch(CREATE_HINTS_TABLE_SQL)
401            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to initialize hint schema"))?;
402        conn.execute_batch(CREATE_ROOTS_TABLE_SQL)
403            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to initialize root schema"))?;
404        Ok(Self {
405            conn: Mutex::new(conn),
406            node_read_cache: Mutex::new(NodeReadCache::new(config.node_read_cache_size_bytes)),
407            node_compression_min_bytes: config.node_compression_min_bytes,
408        })
409    }
410
411    fn from_existing_connection(
412        conn: Connection,
413        config: SqliteStoreConfig,
414    ) -> Result<Self, SqliteStoreError> {
415        Self::apply_runtime_config(&conn, &config)?;
416        Ok(Self {
417            conn: Mutex::new(conn),
418            node_read_cache: Mutex::new(NodeReadCache::new(config.node_read_cache_size_bytes)),
419            node_compression_min_bytes: config.node_compression_min_bytes,
420        })
421    }
422
423    fn apply_runtime_config(
424        conn: &Connection,
425        config: &SqliteStoreConfig,
426    ) -> Result<(), SqliteStoreError> {
427        conn.busy_timeout(Duration::from_millis(config.busy_timeout_ms))
428            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set busy timeout"))?;
429
430        if config.enable_wal {
431            conn.pragma_update(None, "journal_mode", "WAL")
432                .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to enable WAL mode"))?;
433        }
434        if config.synchronous_normal {
435            conn.pragma_update(None, "synchronous", "NORMAL")
436                .map_err(|e| {
437                    SqliteStoreError::from_sqlite(e, "Failed to set synchronous=NORMAL")
438                })?;
439        }
440        conn.pragma_update(None, "temp_store", "MEMORY")
441            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set temp_store=MEMORY"))?;
442        let cache_size_kib = config
443            .page_cache_size_bytes
444            .div_ceil(1024)
445            .min(i64::MAX as u64);
446        let cache_size_kib = -(cache_size_kib as i64);
447        conn.pragma_update(None, "cache_size", cache_size_kib)
448            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set cache_size"))?;
449        conn.pragma_update(None, "wal_autocheckpoint", config.wal_autocheckpoint_pages)
450            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set WAL autocheckpoint"))?;
451        conn.pragma_update(None, "mmap_size", config.mmap_size_bytes)
452            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to set mmap_size"))?;
453        Ok(())
454    }
455
456    fn connection(&self) -> Result<MutexGuard<'_, Connection>, SqliteStoreError> {
457        Ok(self.conn.lock())
458    }
459
460    fn node_read_cache(&self) -> Result<MutexGuard<'_, NodeReadCache>, SqliteStoreError> {
461        Ok(self.node_read_cache.lock())
462    }
463
464}
465
466fn ensure_node_encoding_column(conn: &Connection) -> Result<(), SqliteStoreError> {
467    let has_encoding = {
468        let mut stmt = conn
469            .prepare("PRAGMA table_info(prolly_nodes)")
470            .map_err(|error| {
471                SqliteStoreError::from_sqlite(error, "Failed to inspect node schema")
472            })?;
473        let columns = stmt
474            .query_map([], |row| row.get::<_, String>(1))
475            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to query node schema"))?;
476        let mut found = false;
477        for column in columns {
478            if column.map_err(|error| {
479                SqliteStoreError::from_sqlite(error, "Failed to read node schema")
480            })? == "encoding"
481            {
482                found = true;
483                break;
484            }
485        }
486        found
487    };
488    if !has_encoding {
489        conn.execute(
490            "ALTER TABLE prolly_nodes ADD COLUMN encoding INTEGER NOT NULL DEFAULT 0",
491            [],
492        )
493        .map_err(|error| {
494            SqliteStoreError::from_sqlite(error, "Failed to migrate node encoding schema")
495        })?;
496    }
497    Ok(())
498}
499
500const NODE_ENCODING_RAW: i64 = 0;
501const NODE_ENCODING_LZ4: i64 = 1;
502fn encode_stored_node(node: &[u8], min_compressible_bytes: usize) -> (i64, Vec<u8>) {
503    let mut scratch = Vec::new();
504    let (encoding, stored) =
505        encode_stored_node_into(node, &mut scratch, min_compressible_bytes);
506    (encoding, stored.to_vec())
507}
508
509fn encode_stored_node_into<'a>(
510    node: &'a [u8],
511    scratch: &'a mut Vec<u8>,
512    min_compressible_bytes: usize,
513) -> (i64, &'a [u8]) {
514    if node.len() < min_compressible_bytes || node.len() > u32::MAX as usize {
515        return (NODE_ENCODING_RAW, node);
516    }
517    let maximum = 4 + lz4_flex::block::get_maximum_output_size(node.len());
518    scratch.clear();
519    scratch.resize(maximum, 0);
520    scratch[..4].copy_from_slice(&(node.len() as u32).to_le_bytes());
521    let compressed_len = lz4_flex::block::compress_into(node, &mut scratch[4..])
522        .expect("maximum LZ4 output size is sufficient");
523    let stored_len = 4 + compressed_len;
524    if stored_len >= node.len() {
525        return (NODE_ENCODING_RAW, node);
526    }
527    scratch.truncate(stored_len);
528    (NODE_ENCODING_LZ4, scratch)
529}
530
531fn decode_stored_node_ref(encoding: i64, node: &[u8]) -> Result<Vec<u8>, SqliteStoreError> {
532    match encoding {
533        NODE_ENCODING_RAW => Ok(node.to_vec()),
534        NODE_ENCODING_LZ4 => lz4_flex::decompress_size_prepended(node)
535            .map_err(|error| SqliteStoreError::new(format!("failed to decompress node: {error}"))),
536        other => Err(SqliteStoreError::new(format!(
537            "unsupported node encoding {other}"
538        ))),
539    }
540}
541
542fn select_nodes_ordered_unique(
543    conn: &Connection,
544    keys: &[&[u8]],
545) -> Result<Vec<Option<Vec<u8>>>, SqliteStoreError> {
546    if keys.is_empty() {
547        return Ok(Vec::new());
548    }
549    if keys.len() == 1 {
550        let mut stmt = conn.prepare_cached(SELECT_SQL).map_err(|error| {
551            SqliteStoreError::from_sqlite(error, "Failed to prepare point read")
552        })?;
553        let mut rows = stmt
554            .query(params![keys[0]])
555            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to read key"))?;
556        let value = match rows
557            .next()
558            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to read key"))?
559        {
560            Some(row) => {
561                let encoding = row.get::<_, i64>(0).map_err(|error| {
562                    SqliteStoreError::from_sqlite(error, "Failed to read node encoding")
563                })?;
564                let node_value = row.get_ref(1).map_err(|error| {
565                    SqliteStoreError::from_sqlite(error, "Failed to borrow node bytes")
566                })?;
567                let node = node_value.as_blob().map_err(|error| {
568                    SqliteStoreError::new(format!("Failed to borrow node bytes: {error}"))
569                })?;
570                Some(decode_stored_node_ref(encoding, node)?)
571            }
572            None => None,
573        };
574        return Ok(vec![value]);
575    }
576
577    let positions = keys
578        .iter()
579        .enumerate()
580        .map(|(index, key)| (*key, index))
581        .collect::<HashMap<_, _>>();
582    let mut values = vec![None; keys.len()];
583
584    for chunk in keys.chunks(MAX_BATCH_SELECT_KEYS) {
585        let placeholders = (1..=chunk.len())
586            .map(|index| format!("?{index}"))
587            .collect::<Vec<_>>()
588            .join(",");
589        let sql =
590            format!("SELECT cid, encoding, node FROM prolly_nodes WHERE cid IN ({placeholders})");
591        let mut stmt = conn.prepare_cached(&sql).map_err(|error| {
592            SqliteStoreError::from_sqlite(error, "Failed to prepare multi-key read")
593        })?;
594        let mut rows = stmt
595            .query(rusqlite::params_from_iter(chunk.iter().copied()))
596            .map_err(|error| {
597                SqliteStoreError::from_sqlite(error, "Failed to execute multi-key read")
598            })?;
599        while let Some(row) = rows.next().map_err(|error| {
600            SqliteStoreError::from_sqlite(error, "Failed to read multi-key result")
601        })? {
602            let key_value = row.get_ref(0).map_err(|error| {
603                SqliteStoreError::from_sqlite(error, "Failed to read multi-key result")
604            })?;
605            let key = key_value.as_blob().map_err(|error| {
606                SqliteStoreError::new(format!("Failed to borrow node key: {error}"))
607            })?;
608            let encoding = row.get::<_, i64>(1).map_err(|error| {
609                SqliteStoreError::from_sqlite(error, "Failed to read node encoding")
610            })?;
611            let node_value = row.get_ref(2).map_err(|error| {
612                SqliteStoreError::from_sqlite(error, "Failed to borrow node bytes")
613            })?;
614            let node = node_value.as_blob().map_err(|error| {
615                SqliteStoreError::new(format!("Failed to borrow node bytes: {error}"))
616            })?;
617            let index = positions.get(key).copied().ok_or_else(|| {
618                SqliteStoreError::new("multi-key read returned an unrequested key")
619            })?;
620            values[index] = Some(decode_stored_node_ref(encoding, node)?);
621        }
622    }
623
624    Ok(values)
625}
626
627#[cfg(unix)]
628/// Inspect SQLite's actual open main-database descriptor without executing SQL.
629pub fn sqlite_main_file_identity(
630    conn: &Connection,
631) -> Result<SqliteMainFileIdentity, SqliteStoreError> {
632    use std::ffi::{c_int, c_void};
633    use std::fs::File;
634    use std::os::fd::BorrowedFd;
635    use std::os::unix::fs::MetadataExt;
636
637    // Every bundled Unix SQLite VFS begins its concrete `unixFile` with this
638    // stable prefix. `SQLITE_FCNTL_FILE_POINTER` returns the actual main-file
639    // object owned by this connection, not a pathname-derived approximation.
640    #[repr(C)]
641    struct UnixFilePrefix {
642        methods: *const rusqlite::ffi::sqlite3_io_methods,
643        vfs: *mut rusqlite::ffi::sqlite3_vfs,
644        inode: *mut c_void,
645        fd: c_int,
646    }
647
648    let mut sqlite_file: *mut rusqlite::ffi::sqlite3_file = std::ptr::null_mut();
649    // SAFETY: `conn` remains alive for the call, `main` is NUL terminated, and
650    // SQLite writes one sqlite3_file pointer into `sqlite_file` for this opcode.
651    let rc = unsafe {
652        rusqlite::ffi::sqlite3_file_control(
653            conn.handle(),
654            c"main".as_ptr(),
655            rusqlite::ffi::SQLITE_FCNTL_FILE_POINTER,
656            (&mut sqlite_file as *mut *mut rusqlite::ffi::sqlite3_file).cast(),
657        )
658    };
659    if rc != rusqlite::ffi::SQLITE_OK || sqlite_file.is_null() {
660        return Err(SqliteStoreError::new(format!(
661            "SQLite did not expose its main-file handle (code {rc})"
662        )));
663    }
664    // SAFETY: the bundled Unix VFS concrete file begins with UnixFilePrefix,
665    // and SQLite retains the descriptor for the lifetime of this connection.
666    let fd = unsafe { (*(sqlite_file.cast::<UnixFilePrefix>())).fd };
667    // Duplicate the descriptor so the temporary File cannot close SQLite's
668    // owned descriptor when it is dropped.
669    let borrowed = unsafe { BorrowedFd::borrow_raw(fd) };
670    let owned = borrowed.try_clone_to_owned().map_err(|error| {
671        SqliteStoreError::new(format!(
672            "failed to duplicate SQLite main-file handle: {error}"
673        ))
674    })?;
675    let metadata = File::from(owned).metadata().map_err(|error| {
676        SqliteStoreError::new(format!("failed to stat SQLite main-file handle: {error}"))
677    })?;
678    Ok(SqliteMainFileIdentity {
679        device: metadata.dev(),
680        inode: metadata.ino(),
681        length: metadata.len(),
682    })
683}
684
685impl Store for SqliteStore {
686    type Error = SqliteStoreError;
687
688    fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
689        if let Some(value) = self.node_read_cache()?.get(key) {
690            return Ok(Some(value.as_ref().to_vec()));
691        }
692        let conn = self.connection()?;
693        let mut stmt = conn
694            .prepare_cached(SELECT_SQL)
695            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to prepare point read"))?;
696        let mut rows = stmt
697            .query(params![key])
698            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read key"))?;
699        let Some(row) = rows
700            .next()
701            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read key"))?
702        else {
703            return Ok(None);
704        };
705        let encoding = row.get::<_, i64>(0).map_err(|error| {
706            SqliteStoreError::from_sqlite(error, "Failed to read node encoding")
707        })?;
708        let node_value = row
709            .get_ref(1)
710            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to borrow node bytes"))?;
711        let node = node_value.as_blob().map_err(|error| {
712            SqliteStoreError::new(format!("Failed to borrow node bytes: {error}"))
713        })?;
714        let decoded: Arc<[u8]> = Arc::from(decode_stored_node_ref(encoding, node)?);
715        self.node_read_cache()?.insert(key, decoded.clone());
716        Ok(Some(decoded.as_ref().to_vec()))
717    }
718
719    fn get_shared(&self, key: &[u8]) -> Result<Option<Arc<[u8]>>, Self::Error> {
720        if let Some(value) = self.node_read_cache()?.get(key) {
721            return Ok(Some(value));
722        }
723        let conn = self.connection()?;
724        let mut stmt = conn.prepare_cached(SELECT_SQL).map_err(|e| {
725            SqliteStoreError::from_sqlite(e, "Failed to prepare shared point read")
726        })?;
727        let mut rows = stmt
728            .query(params![key])
729            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read shared key"))?;
730        let Some(row) = rows
731            .next()
732            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read shared key"))?
733        else {
734            return Ok(None);
735        };
736        let encoding = row.get::<_, i64>(0).map_err(|error| {
737            SqliteStoreError::from_sqlite(error, "Failed to read node encoding")
738        })?;
739        let node_value = row
740            .get_ref(1)
741            .map_err(|error| SqliteStoreError::from_sqlite(error, "Failed to borrow node bytes"))?;
742        let node = node_value.as_blob().map_err(|error| {
743            SqliteStoreError::new(format!("Failed to borrow node bytes: {error}"))
744        })?;
745        let decoded: Arc<[u8]> = Arc::from(decode_stored_node_ref(encoding, node)?);
746        self.node_read_cache()?.insert(key, decoded.clone());
747        Ok(Some(decoded))
748    }
749
750    fn has_native_shared_reads(&self) -> bool {
751        true
752    }
753
754    fn put(&self, key: &[u8], value: &[u8]) -> Result<(), Self::Error> {
755        let conn = self.connection()?;
756        let (encoding, stored) =
757            encode_stored_node(value, self.node_compression_min_bytes);
758        conn.execute(UPSERT_SQL, params![key, encoding, stored])
759            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to write key"))?;
760        self.node_read_cache()?.insert(key, Arc::from(value));
761        Ok(())
762    }
763
764    fn delete(&self, key: &[u8]) -> Result<(), Self::Error> {
765        let conn = self.connection()?;
766        conn.execute(DELETE_SQL, params![key])
767            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to delete key"))?;
768        self.node_read_cache()?.remove(key);
769        Ok(())
770    }
771
772    fn batch(&self, ops: &[BatchOp]) -> Result<(), Self::Error> {
773        let mut conn = self.connection()?;
774        let tx = conn
775            .transaction_with_behavior(TransactionBehavior::Immediate)
776            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to start transaction"))?;
777
778        {
779            let mut upsert = tx
780                .prepare_cached(UPSERT_SQL)
781                .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to prepare batch write"))?;
782            let mut delete = tx
783                .prepare_cached(DELETE_SQL)
784                .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to prepare batch delete"))?;
785            let mut compression_scratch = Vec::new();
786
787            for op in ops {
788                match op {
789                    BatchOp::Upsert { key, value } => {
790                        let (encoding, value) = encode_stored_node_into(
791                            value,
792                            &mut compression_scratch,
793                            self.node_compression_min_bytes,
794                        );
795                        upsert.execute(params![key, encoding, value]).map_err(|e| {
796                            SqliteStoreError::from_sqlite(e, "Failed to write key in batch")
797                        })?;
798                    }
799                    BatchOp::Delete { key } => {
800                        delete.execute(params![key]).map_err(|e| {
801                            SqliteStoreError::from_sqlite(e, "Failed to delete key in batch")
802                        })?;
803                    }
804                }
805            }
806        }
807
808        tx.commit()
809            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to commit transaction"))?;
810        let mut cache = self.node_read_cache()?;
811        for op in ops {
812            match op {
813                BatchOp::Upsert { key, .. } | BatchOp::Delete { key } => cache.remove(key),
814            }
815        }
816        Ok(())
817    }
818
819    fn batch_get(&self, keys: &[&[u8]]) -> Result<HashMap<Vec<u8>, Vec<u8>>, Self::Error> {
820        let plan = OrderedBatchReadPlan::new(keys);
821        let values = self.batch_get_shared_ordered_unique(plan.unique_keys())?;
822        let mut results = HashMap::with_capacity(plan.unique_keys().len());
823        for (key, value) in plan.unique_keys().iter().zip(values) {
824            if let Some(value) = value {
825                results.insert(key.to_vec(), value.as_ref().to_vec());
826            }
827        }
828
829        Ok(results)
830    }
831
832    fn batch_get_ordered(&self, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>, Self::Error> {
833        let plan = OrderedBatchReadPlan::new(keys);
834        let unique_values = self
835            .batch_get_shared_ordered_unique(plan.unique_keys())?
836            .into_iter()
837            .map(|value| value.map(|value| value.as_ref().to_vec()))
838            .collect();
839        Ok(plan.expand_owned(unique_values))
840    }
841
842    fn batch_get_ordered_unique(
843        &self,
844        keys: &[&[u8]],
845    ) -> Result<Vec<Option<Vec<u8>>>, Self::Error> {
846        self.batch_get_shared_ordered_unique(keys).map(|values| {
847            values
848                .into_iter()
849                .map(|value| value.map(|value| value.as_ref().to_vec()))
850                .collect()
851        })
852    }
853
854    fn batch_get_shared_ordered_unique(
855        &self,
856        keys: &[&[u8]],
857    ) -> Result<Vec<Option<Arc<[u8]>>>, Self::Error> {
858        let mut values = vec![None; keys.len()];
859        let mut missing = Vec::new();
860        {
861            let cache = self.node_read_cache()?;
862            for (position, key) in keys.iter().enumerate() {
863                match cache.get(key) {
864                    Some(value) => values[position] = Some(value),
865                    None => missing.push((position, *key)),
866                }
867            }
868        }
869        if missing.is_empty() {
870            return Ok(values);
871        }
872        let missing_keys = missing.iter().map(|(_, key)| *key).collect::<Vec<_>>();
873        let conn = self.connection()?;
874        let loaded = select_nodes_ordered_unique(&conn, &missing_keys)?;
875        let mut cache = self.node_read_cache()?;
876        for ((position, key), loaded) in missing.into_iter().zip(loaded) {
877            if let Some(loaded) = loaded {
878                let loaded: Arc<[u8]> = Arc::from(loaded);
879                cache.insert(key, loaded.clone());
880                values[position] = Some(loaded);
881            }
882        }
883        Ok(values)
884    }
885
886    fn prefers_batch_reads(&self) -> bool {
887        true
888    }
889
890    fn batch_put(&self, entries: &[(&[u8], &[u8])]) -> Result<(), Self::Error> {
891        let mut conn = self.connection()?;
892        let tx = conn
893            .transaction_with_behavior(TransactionBehavior::Immediate)
894            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to start transaction"))?;
895
896        {
897            let mut stmt = tx.prepare_cached(UPSERT_SQL).map_err(|e| {
898                SqliteStoreError::from_sqlite(e, "Failed to prepare batch_put write")
899            })?;
900            let mut ordered = entries.iter().collect::<Vec<_>>();
901            ordered.sort_by(|left, right| left.0.cmp(right.0));
902            let mut compression_scratch = Vec::new();
903            for &&(key, value) in &ordered {
904                let (encoding, value) = encode_stored_node_into(
905                    value,
906                    &mut compression_scratch,
907                    self.node_compression_min_bytes,
908                );
909                stmt.execute(params![key, encoding, value]).map_err(|e| {
910                    SqliteStoreError::from_sqlite(e, "Failed to write key in batch_put")
911                })?;
912            }
913        }
914
915        tx.commit()
916            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to commit transaction"))?;
917        let mut cache = self.node_read_cache()?;
918        for &(key, _) in entries {
919            cache.remove(key);
920        }
921        Ok(())
922    }
923
924    fn supports_hints(&self) -> bool {
925        true
926    }
927
928    fn get_hint(&self, namespace: &[u8], key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
929        let conn = self.connection()?;
930        conn.query_row(
931            "SELECT value FROM prolly_hints WHERE namespace = ?1 AND key = ?2",
932            params![namespace, key],
933            |row| row.get(0),
934        )
935        .optional()
936        .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read hint"))
937    }
938
939    fn put_hint(&self, namespace: &[u8], key: &[u8], value: &[u8]) -> Result<(), Self::Error> {
940        let conn = self.connection()?;
941        conn.execute(
942            "\
943            INSERT INTO prolly_hints (namespace, key, value) \
944            VALUES (?1, ?2, ?3) \
945            ON CONFLICT(namespace, key) DO UPDATE SET value = excluded.value",
946            params![namespace, key, value],
947        )
948        .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to write hint"))?;
949        Ok(())
950    }
951
952    fn batch_put_with_hint(
953        &self,
954        entries: &[(&[u8], &[u8])],
955        namespace: &[u8],
956        key: &[u8],
957        value: &[u8],
958    ) -> Result<(), Self::Error> {
959        let mut conn = self.connection()?;
960        let tx = conn
961            .transaction_with_behavior(TransactionBehavior::Immediate)
962            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to start transaction"))?;
963
964        {
965            let mut upsert_node = tx.prepare_cached(UPSERT_SQL).map_err(|e| {
966                SqliteStoreError::from_sqlite(e, "Failed to prepare batch_put write")
967            })?;
968            let mut ordered = entries.iter().collect::<Vec<_>>();
969            ordered.sort_by(|left, right| left.0.cmp(right.0));
970            let mut compression_scratch = Vec::new();
971            for &&(key, value) in &ordered {
972                let (encoding, value) = encode_stored_node_into(
973                    value,
974                    &mut compression_scratch,
975                    self.node_compression_min_bytes,
976                );
977                upsert_node
978                    .execute(params![key, encoding, value])
979                    .map_err(|e| {
980                        SqliteStoreError::from_sqlite(e, "Failed to write key in batch_put")
981                    })?;
982            }
983        }
984
985        tx.execute(
986            "\
987            INSERT INTO prolly_hints (namespace, key, value) \
988            VALUES (?1, ?2, ?3) \
989            ON CONFLICT(namespace, key) DO UPDATE SET value = excluded.value",
990            params![namespace, key, value],
991        )
992        .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to write hint in batch_put"))?;
993
994        tx.commit()
995            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to commit transaction"))?;
996        let mut cache = self.node_read_cache()?;
997        for &(key, _) in entries {
998            cache.remove(key);
999        }
1000        Ok(())
1001    }
1002
1003    fn publish_nodes(&self, publication: NodePublication<'_>) -> Result<(), Self::Error> {
1004        let mut conn = self.connection()?;
1005        let tx = conn
1006            .transaction_with_behavior(TransactionBehavior::Immediate)
1007            .map_err(|error| {
1008                SqliteStoreError::from_sqlite(error, "Failed to start node publication")
1009            })?;
1010        {
1011            // Publication keys are content IDs for immutable nodes. Ignoring an
1012            // existing key avoids rewriting shared nodes carried into a new
1013            // tree while preserving the Store upsert contract on batch_put.
1014            let mut insert = tx.prepare_cached(INSERT_IMMUTABLE_SQL).map_err(|error| {
1015                SqliteStoreError::from_sqlite(error, "Failed to prepare node publication")
1016            })?;
1017            let mut ordered = publication.entries().iter().collect::<Vec<_>>();
1018            ordered.sort_by(|left, right| left.0.cmp(right.0));
1019            let mut compression_scratch = Vec::new();
1020            for &&(key, value) in &ordered {
1021                let (encoding, value) = encode_stored_node_into(
1022                    value,
1023                    &mut compression_scratch,
1024                    self.node_compression_min_bytes,
1025                );
1026                insert
1027                    .execute(params![key, encoding, value])
1028                    .map_err(|error| {
1029                        SqliteStoreError::from_sqlite(error, "Failed to publish immutable node")
1030                    })?;
1031            }
1032        }
1033        if let Some(hint) = publication.hint() {
1034            tx.execute(
1035                UPSERT_HINT_SQL,
1036                params![hint.namespace(), hint.key(), hint.value()],
1037            )
1038            .map_err(|error| {
1039                SqliteStoreError::from_sqlite(error, "Failed to write publication hint")
1040            })?;
1041        }
1042        tx.commit().map_err(|error| {
1043            SqliteStoreError::from_sqlite(error, "Failed to commit node publication")
1044        })?;
1045        // Batch and merge branches are commonly read immediately. A full tree
1046        // build already leaves its decoded nodes in the manager cache, so
1047        // duplicating every serialized node here only adds publication cost.
1048        if !matches!(
1049            publication.origin(),
1050            PublicationOrigin::TreeBuild | PublicationOrigin::Merge
1051        ) {
1052            let mut cache = self.node_read_cache()?;
1053            for &(key, value) in publication.entries() {
1054                cache.insert_immutable(key, Arc::from(value));
1055            }
1056        }
1057        Ok(())
1058    }
1059}
1060
1061impl NodeStoreScan for SqliteStore {
1062    type Error = SqliteStoreError;
1063
1064    fn list_node_cids(&self) -> Result<Vec<Cid>, Self::Error> {
1065        let conn = self.connection()?;
1066        let mut stmt = conn
1067            .prepare_cached(SELECT_NODE_CIDS_SQL)
1068            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to prepare node CID listing"))?;
1069        let rows = stmt
1070            .query_map([], |row| row.get::<_, Vec<u8>>(0))
1071            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to list node CIDs"))?;
1072
1073        let mut cids = Vec::new();
1074        for row in rows {
1075            let key = row
1076                .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read listed node CID"))?;
1077            cids.push(cid_from_store_key(&key, "SQLite node").map_err(SqliteStoreError::new)?);
1078        }
1079        sort_cids(&mut cids);
1080        Ok(cids)
1081    }
1082}
1083
1084impl ManifestStore for SqliteStore {
1085    type Error = SqliteStoreError;
1086
1087    fn get_root(&self, name: &[u8]) -> Result<Option<RootManifest>, Self::Error> {
1088        let conn = self.connection()?;
1089        let bytes = conn
1090            .query_row(SELECT_ROOT_SQL, params![name], |row| row.get(0))
1091            .optional()
1092            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read root manifest"))?;
1093        decode_root_manifest(bytes)
1094    }
1095
1096    fn put_root(&self, name: &[u8], manifest: &RootManifest) -> Result<(), Self::Error> {
1097        let conn = self.connection()?;
1098        let bytes = encode_root_manifest(manifest)?;
1099        conn.execute(UPSERT_ROOT_SQL, params![name, bytes])
1100            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to write root manifest"))?;
1101        Ok(())
1102    }
1103
1104    fn delete_root(&self, name: &[u8]) -> Result<(), Self::Error> {
1105        let conn = self.connection()?;
1106        conn.execute(DELETE_ROOT_SQL, params![name])
1107            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to delete root manifest"))?;
1108        Ok(())
1109    }
1110
1111    fn compare_and_swap_root(
1112        &self,
1113        name: &[u8],
1114        expected: Option<&RootManifest>,
1115        new: Option<&RootManifest>,
1116    ) -> Result<ManifestUpdate, Self::Error> {
1117        let expected_bytes = expected.map(encode_root_manifest).transpose()?;
1118        let new_bytes = new.map(encode_root_manifest).transpose()?;
1119
1120        let mut conn = self.connection()?;
1121        let tx = conn
1122            .transaction_with_behavior(TransactionBehavior::Immediate)
1123            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to start root transaction"))?;
1124
1125        let current_bytes = tx
1126            .query_row(SELECT_ROOT_SQL, params![name], |row| row.get(0))
1127            .optional()
1128            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to read root manifest"))?;
1129
1130        if current_bytes.as_deref() != expected_bytes.as_deref() {
1131            return Ok(ManifestUpdate::Conflict {
1132                current: decode_root_manifest(current_bytes)?,
1133            });
1134        }
1135
1136        match new_bytes {
1137            Some(bytes) => {
1138                tx.execute(UPSERT_ROOT_SQL, params![name, bytes])
1139                    .map_err(|e| {
1140                        SqliteStoreError::from_sqlite(e, "Failed to write root manifest")
1141                    })?;
1142            }
1143            None => {
1144                tx.execute(DELETE_ROOT_SQL, params![name]).map_err(|e| {
1145                    SqliteStoreError::from_sqlite(e, "Failed to delete root manifest")
1146                })?;
1147            }
1148        }
1149
1150        tx.commit()
1151            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to commit root transaction"))?;
1152        Ok(ManifestUpdate::Applied)
1153    }
1154}
1155
1156impl ManifestStoreScan for SqliteStore {
1157    fn list_roots(&self) -> Result<Vec<NamedRootManifest>, Self::Error> {
1158        let conn = self.connection()?;
1159        let mut stmt = conn.prepare_cached(SELECT_ROOTS_SQL).map_err(|e| {
1160            SqliteStoreError::from_sqlite(e, "Failed to prepare root manifest listing")
1161        })?;
1162        let rows = stmt
1163            .query_map([], |row| {
1164                Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?))
1165            })
1166            .map_err(|e| SqliteStoreError::from_sqlite(e, "Failed to list root manifests"))?;
1167
1168        let mut roots = Vec::new();
1169        for row in rows {
1170            let (name, bytes) = row.map_err(|e| {
1171                SqliteStoreError::from_sqlite(e, "Failed to read listed root manifest")
1172            })?;
1173            let manifest = RootManifest::from_bytes(&bytes)
1174                .map_err(|err| SqliteStoreError::new(err.to_string()))?;
1175            roots.push(NamedRootManifest::new(name, manifest));
1176        }
1177        sort_named_root_manifests(&mut roots);
1178        Ok(roots)
1179    }
1180}
1181
1182impl TransactionalStore for SqliteStore {
1183    fn supports_transactions(&self) -> bool {
1184        true
1185    }
1186
1187    fn commit_transaction(
1188        &self,
1189        node_writes: &[TransactionNodeWrite],
1190        root_conditions: &[RootCondition],
1191        root_writes: &[RootWrite],
1192    ) -> Result<TransactionUpdate, Error> {
1193        let mut conn = self
1194            .connection()
1195            .map_err(|err| Error::Store(Box::new(err)))?;
1196        let tx = conn
1197            .transaction_with_behavior(TransactionBehavior::Immediate)
1198            .map_err(|err| {
1199                Error::Store(Box::new(SqliteStoreError::from_sqlite(
1200                    err,
1201                    "Failed to start transaction commit",
1202                )))
1203            })?;
1204
1205        for condition in root_conditions {
1206            let current_bytes = tx
1207                .query_row(SELECT_ROOT_SQL, params![condition.name], |row| row.get(0))
1208                .optional()
1209                .map_err(|err| {
1210                    Error::Store(Box::new(SqliteStoreError::from_sqlite(
1211                        err,
1212                        "Failed to read root manifest during transaction commit",
1213                    )))
1214                })?;
1215            let current =
1216                decode_root_manifest(current_bytes).map_err(|err| Error::Store(Box::new(err)))?;
1217            if current != condition.expected {
1218                return Ok(TransactionUpdate::Conflict(Box::new(
1219                    TransactionConflict::new(
1220                        condition.name.clone(),
1221                        condition.expected.clone(),
1222                        current,
1223                    ),
1224                )));
1225            }
1226        }
1227
1228        {
1229            let mut upsert_node = tx.prepare_cached(UPSERT_SQL).map_err(|err| {
1230                Error::Store(Box::new(SqliteStoreError::from_sqlite(
1231                    err,
1232                    "Failed to prepare transaction node write",
1233                )))
1234            })?;
1235            let mut delete_node = tx.prepare_cached(DELETE_SQL).map_err(|err| {
1236                Error::Store(Box::new(SqliteStoreError::from_sqlite(
1237                    err,
1238                    "Failed to prepare transaction node delete",
1239                )))
1240            })?;
1241            for write in node_writes {
1242                match write {
1243                    TransactionNodeWrite::Upsert { key, value } => {
1244                        let (encoding, value) =
1245                            encode_stored_node(value, self.node_compression_min_bytes);
1246                        upsert_node
1247                            .execute(params![key, encoding, value])
1248                            .map_err(|err| {
1249                                Error::Store(Box::new(SqliteStoreError::from_sqlite(
1250                                    err,
1251                                    "Failed to write node during transaction commit",
1252                                )))
1253                            })?;
1254                    }
1255                    TransactionNodeWrite::Delete { key } => {
1256                        delete_node.execute(params![key]).map_err(|err| {
1257                            Error::Store(Box::new(SqliteStoreError::from_sqlite(
1258                                err,
1259                                "Failed to delete node during transaction commit",
1260                            )))
1261                        })?;
1262                    }
1263                }
1264            }
1265        }
1266
1267        {
1268            let mut upsert_root = tx.prepare_cached(UPSERT_ROOT_SQL).map_err(|err| {
1269                Error::Store(Box::new(SqliteStoreError::from_sqlite(
1270                    err,
1271                    "Failed to prepare transaction root write",
1272                )))
1273            })?;
1274            let mut delete_root = tx.prepare_cached(DELETE_ROOT_SQL).map_err(|err| {
1275                Error::Store(Box::new(SqliteStoreError::from_sqlite(
1276                    err,
1277                    "Failed to prepare transaction root delete",
1278                )))
1279            })?;
1280            for write in root_writes {
1281                match write {
1282                    RootWrite::Put { name, manifest } => {
1283                        let bytes = encode_root_manifest(manifest)
1284                            .map_err(|err| Error::Store(Box::new(err)))?;
1285                        upsert_root.execute(params![name, bytes]).map_err(|err| {
1286                            Error::Store(Box::new(SqliteStoreError::from_sqlite(
1287                                err,
1288                                "Failed to write root during transaction commit",
1289                            )))
1290                        })?;
1291                    }
1292                    RootWrite::Delete { name } => {
1293                        delete_root.execute(params![name]).map_err(|err| {
1294                            Error::Store(Box::new(SqliteStoreError::from_sqlite(
1295                                err,
1296                                "Failed to delete root during transaction commit",
1297                            )))
1298                        })?;
1299                    }
1300                }
1301            }
1302        }
1303
1304        tx.commit().map_err(|err| {
1305            Error::Store(Box::new(SqliteStoreError::from_sqlite(
1306                err,
1307                "Failed to commit transaction",
1308            )))
1309        })?;
1310        let mut cache = self
1311            .node_read_cache()
1312            .map_err(|err| Error::Store(Box::new(err)))?;
1313        for write in node_writes {
1314            match write {
1315                TransactionNodeWrite::Upsert { key, .. } | TransactionNodeWrite::Delete { key } => {
1316                    cache.remove(key)
1317                }
1318            }
1319        }
1320        Ok(TransactionUpdate::Applied {
1321            nodes_written: node_writes.len(),
1322            roots_written: root_writes.len(),
1323        })
1324    }
1325}
1326
1327fn encode_root_manifest(manifest: &RootManifest) -> Result<Vec<u8>, SqliteStoreError> {
1328    manifest
1329        .to_bytes()
1330        .map_err(|e| SqliteStoreError::new(format!("failed to encode root manifest: {e}")))
1331}
1332
1333fn decode_root_manifest(bytes: Option<Vec<u8>>) -> Result<Option<RootManifest>, SqliteStoreError> {
1334    bytes
1335        .as_deref()
1336        .map(RootManifest::from_bytes)
1337        .transpose()
1338        .map_err(|e| SqliteStoreError::new(format!("failed to decode root manifest: {e}")))
1339}
1340
1341#[cfg(test)]
1342mod tests {
1343    use super::*;
1344
1345    #[test]
1346    fn sqlite_store_put_get_delete() {
1347        let store = SqliteStore::open_in_memory().unwrap();
1348
1349        store.put(b"key", b"value").unwrap();
1350        assert_eq!(store.get(b"key").unwrap(), Some(b"value".to_vec()));
1351
1352        store.delete(b"key").unwrap();
1353        assert_eq!(store.get(b"key").unwrap(), None);
1354    }
1355
1356    #[test]
1357    fn sqlite_store_reuses_shared_node_reads() {
1358        let store = SqliteStore::open_in_memory().unwrap();
1359        store.put(b"key", b"value").unwrap();
1360
1361        let first = store.get_shared(b"key").unwrap().unwrap();
1362        let second = store.get_shared(b"key").unwrap().unwrap();
1363
1364        assert!(Arc::ptr_eq(&first, &second));
1365    }
1366
1367    #[test]
1368    fn sqlite_store_applies_publication_pragmas_and_rowid_schema() {
1369        let store = SqliteStore::open_in_memory().unwrap();
1370        let conn = store.connection().unwrap();
1371        let page_size: u32 = conn
1372            .query_row("PRAGMA page_size", [], |row| row.get(0))
1373            .unwrap();
1374        let cache_size: i64 = conn
1375            .query_row("PRAGMA cache_size", [], |row| row.get(0))
1376            .unwrap();
1377        let wal_autocheckpoint: u32 = conn
1378            .query_row("PRAGMA wal_autocheckpoint", [], |row| row.get(0))
1379            .unwrap();
1380        let schema: String = conn
1381            .query_row(
1382                "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'prolly_nodes'",
1383                [],
1384                |row| row.get(0),
1385            )
1386            .unwrap();
1387
1388        assert_eq!(page_size, 64 * 1024);
1389        assert_eq!(cache_size, -64 * 1024);
1390        assert_eq!(wal_autocheckpoint, 32 * 1024);
1391        assert!(!schema.to_ascii_uppercase().contains("WITHOUT ROWID"));
1392    }
1393
1394    #[test]
1395    fn sqlite_store_rejects_invalid_page_size() {
1396        let config = SqliteStoreConfig {
1397            page_size_bytes: 1_000,
1398            ..SqliteStoreConfig::default()
1399        };
1400        let error = SqliteStore::from_connection(Connection::open_in_memory().unwrap(), config)
1401            .err()
1402            .expect("invalid page size must fail");
1403        assert!(error.to_string().contains("page_size_bytes"));
1404    }
1405
1406    #[test]
1407    fn sqlite_store_batch_is_order_preserving_for_reads() {
1408        let store = SqliteStore::open_in_memory().unwrap();
1409        let ops = vec![
1410            BatchOp::Upsert {
1411                key: b"a",
1412                value: b"1",
1413            },
1414            BatchOp::Upsert {
1415                key: b"b",
1416                value: b"2",
1417            },
1418            BatchOp::Upsert {
1419                key: b"c",
1420                value: b"3",
1421            },
1422        ];
1423
1424        store.batch(&ops).unwrap();
1425
1426        let keys: Vec<&[u8]> = vec![b"c", b"missing", b"a", b"c", b"missing", b"b"];
1427        assert_eq!(
1428            store.batch_get_ordered(&keys).unwrap(),
1429            vec![
1430                Some(b"3".to_vec()),
1431                None,
1432                Some(b"1".to_vec()),
1433                Some(b"3".to_vec()),
1434                None,
1435                Some(b"2".to_vec())
1436            ]
1437        );
1438    }
1439
1440    #[test]
1441    fn sqlite_store_batch_put_updates_existing_keys() {
1442        let store = SqliteStore::open_in_memory().unwrap();
1443
1444        store.put(b"a", b"old").unwrap();
1445        store
1446            .batch_put(&[(b"a".as_slice(), b"new".as_slice()), (b"b", b"2")])
1447            .unwrap();
1448
1449        assert_eq!(store.get(b"a").unwrap(), Some(b"new".to_vec()));
1450        assert_eq!(store.get(b"b").unwrap(), Some(b"2".to_vec()));
1451    }
1452
1453    #[test]
1454    fn sqlite_store_persists_hints_separately_from_nodes() {
1455        let store = SqliteStore::open_in_memory().unwrap();
1456
1457        store.put_hint(b"rightmost", b"root", b"hint-v1").unwrap();
1458        assert_eq!(
1459            store.get_hint(b"rightmost", b"root").unwrap(),
1460            Some(b"hint-v1".to_vec())
1461        );
1462        assert_eq!(store.get_hint(b"rightmost", b"missing").unwrap(), None);
1463        assert_eq!(store.get(b"root").unwrap(), None);
1464
1465        store.put_hint(b"rightmost", b"root", b"hint-v2").unwrap();
1466        assert_eq!(
1467            store.get_hint(b"rightmost", b"root").unwrap(),
1468            Some(b"hint-v2".to_vec())
1469        );
1470    }
1471
1472    #[test]
1473    fn sqlite_store_compresses_repetitive_nodes_transparently() {
1474        let store = SqliteStore::open_in_memory().unwrap();
1475        let node = vec![b'x'; 16 * 1024];
1476
1477        store.put(b"compressed", &node).unwrap();
1478
1479        let conn = store.connection().unwrap();
1480        let (encoding, stored_bytes): (i64, usize) = conn
1481            .query_row(
1482                "SELECT encoding, length(node) FROM prolly_nodes WHERE cid = ?1",
1483                params![b"compressed"],
1484                |row| Ok((row.get(0)?, row.get(1)?)),
1485            )
1486            .unwrap();
1487        drop(conn);
1488        assert_eq!(encoding, NODE_ENCODING_LZ4);
1489        assert!(stored_bytes < node.len());
1490        assert_eq!(store.get(b"compressed").unwrap(), Some(node));
1491    }
1492
1493    #[test]
1494    fn sqlite_store_honors_node_compression_threshold() {
1495        let config = SqliteStoreConfig {
1496            node_compression_min_bytes: 32 * 1024,
1497            ..SqliteStoreConfig::default()
1498        };
1499        let store = SqliteStore::from_connection(Connection::open_in_memory().unwrap(), config)
1500            .unwrap();
1501        let node = vec![b'x'; 16 * 1024];
1502
1503        store.put(b"raw", &node).unwrap();
1504
1505        let conn = store.connection().unwrap();
1506        let encoding: i64 = conn
1507            .query_row(
1508                "SELECT encoding FROM prolly_nodes WHERE cid = ?1",
1509                params![b"raw"],
1510                |row| row.get(0),
1511            )
1512            .unwrap();
1513        assert_eq!(encoding, NODE_ENCODING_RAW);
1514    }
1515
1516    #[test]
1517    fn sqlite_store_migrates_legacy_raw_node_tables() {
1518        let conn = Connection::open_in_memory().unwrap();
1519        conn.execute_batch(
1520            "CREATE TABLE prolly_nodes (
1521                cid BLOB PRIMARY KEY NOT NULL,
1522                node BLOB NOT NULL
1523            ) WITHOUT ROWID;
1524            INSERT INTO prolly_nodes (cid, node) VALUES (x'6c6567616379', x'726177');",
1525        )
1526        .unwrap();
1527
1528        let store = SqliteStore::from_connection(conn, SqliteStoreConfig::default()).unwrap();
1529
1530        assert_eq!(store.get(b"legacy").unwrap(), Some(b"raw".to_vec()));
1531        let conn = store.connection().unwrap();
1532        let encoding: i64 = conn
1533            .query_row(
1534                "SELECT encoding FROM prolly_nodes WHERE cid = ?1",
1535                params![b"legacy"],
1536                |row| row.get(0),
1537            )
1538            .unwrap();
1539        assert_eq!(encoding, NODE_ENCODING_RAW);
1540    }
1541}