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