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