1use std::{
5 collections::HashMap,
6 iter::repeat_n,
7 ops::Bound,
8 sync::{
9 Arc,
10 atomic::{AtomicU32, AtomicUsize, Ordering},
11 },
12};
13
14use reifydb_codec::key::encoded::EncodedKey;
15use reifydb_core::{common::CommitVersion, error::diagnostic::internal::internal, interface::store::EntryKind};
16use reifydb_runtime::{
17 shutdown::Shutdown,
18 sync::{
19 map::Map,
20 mutex::{Mutex, MutexGuard},
21 },
22};
23use reifydb_sqlite::{
24 SqliteConfig, SqliteTempPathGuard,
25 connection::{connect, convert_flags, resolve_db_path},
26 pragma,
27};
28use reifydb_value::{Result, error, util::cowvec::CowVec, value::duration::Duration};
29use rusqlite::{
30 Connection, Error::QueryReturnedNoRows, Result as SqliteResult, Row, ToSql, Transaction, TransactionBehavior,
31 params, params_from_iter,
32};
33use tracing::{instrument, warn};
34
35use crate::{
36 MultiVersionScope,
37 tier::{
38 HistoricalCursor, RangeBatch, RangeCursor, RawEntry, TierBackend, TierBatch, TierStorage,
39 VersionedGetResult,
40 persistent::{
41 CheckpointOutcome,
42 sqlite::{
43 entry::current_table_name,
44 query::{
45 build_create_current_sql, build_delete_below_version_sql,
46 build_delete_key_through_sql, build_delete_keys_sql, build_get_current_sql,
47 build_get_many_current_sql, build_range_consistent_sql,
48 build_range_current_sql, build_upsert_current_sql, prefix_upper_bound,
49 version_from_bytes, version_to_bytes,
50 },
51 },
52 },
53 },
54};
55
56const GET_MANY_CHUNK: usize = 900;
57
58const GET_MANY_BUCKETS: [usize; 5] = [1, 8, 64, 512, GET_MANY_CHUNK];
59
60fn bucket_key_count(len: usize) -> usize {
61 for &bucket in GET_MANY_BUCKETS.iter() {
62 if len <= bucket {
63 return bucket;
64 }
65 }
66 GET_MANY_CHUNK
67}
68
69const BUSY_TIMEOUT: Duration = Duration::from_milliseconds_const(200);
70
71#[derive(Clone)]
72pub struct SqlitePersistentStorage {
73 inner: Arc<SqlitePersistentStorageInner>,
74}
75
76struct SqlitePersistentStorageInner {
77 conn: Mutex<Option<Connection>>,
78 readers: ReadPool,
79 checkpoint_threshold_frames: AtomicU32,
80 table_sql: Map<EntryKind, Arc<TableSql>>,
81}
82
83struct TableSql {
84 table_name: String,
85 get_sql: String,
86 upsert_sql: String,
87 create_sql: String,
88}
89
90impl TableSql {
91 fn build(table: EntryKind) -> Self {
92 let table_name = current_table_name(table);
93 let get_sql = build_get_current_sql(&table_name);
94 let upsert_sql = build_upsert_current_sql(&table_name);
95 let create_sql = build_create_current_sql(&table_name);
96 Self {
97 table_name,
98 get_sql,
99 upsert_sql,
100 create_sql,
101 }
102 }
103}
104
105struct ReadPool {
106 conns: Vec<Mutex<Option<Connection>>>,
107 next: AtomicUsize,
108}
109
110impl ReadPool {
111 fn acquire(&self) -> MutexGuard<'_, Option<Connection>> {
112 let n = self.conns.len();
113 let start = self.next.fetch_add(1, Ordering::Relaxed) % n;
114 for i in 0..n {
115 if let Some(guard) = self.conns[(start + i) % n].try_lock() {
116 return guard;
117 }
118 }
119 self.conns[start].lock()
120 }
121
122 fn shutdown(&self) {
123 for slot in &self.conns {
124 drop(slot.lock().take());
125 }
126 }
127}
128
129impl SqlitePersistentStorage {
130 #[instrument(name = "store::multi::persistent::sqlite::new", level = "debug", skip(config), fields(
131 db_path = ?config.path,
132 page_size = config.page_size.as_bytes(),
133 journal_mode = %config.journal_mode.as_str()
134 ))]
135 pub fn new(config: SqliteConfig) -> Self {
136 let db_path = resolve_db_path(config.path.clone(), "persistent.db");
137 let flags = convert_flags(&config.flags);
138
139 let conn = connect(&db_path, flags).expect("Failed to connect to persistent database");
140 pragma::apply(&conn, &config).expect("Failed to configure persistent SQLite pragmas");
141 conn.busy_timeout(BUSY_TIMEOUT.to_std()).expect("Failed to set persistent busy timeout");
142
143 let pool_size = config.read_pool_size.max(1) as usize;
144 let mut conns = Vec::with_capacity(pool_size);
145 for _ in 0..pool_size {
146 let reader = connect(&db_path, flags).expect("Failed to open persistent read connection");
147 pragma::apply_read_only(&reader, &config)
148 .expect("Failed to configure persistent read connection");
149 reader.busy_timeout(BUSY_TIMEOUT.to_std()).expect("Failed to set persistent read busy timeout");
150 conns.push(Mutex::new(Some(reader)));
151 }
152
153 Self {
154 inner: Arc::new(SqlitePersistentStorageInner {
155 conn: Mutex::new(Some(conn)),
156 readers: ReadPool {
157 conns,
158 next: AtomicUsize::new(0),
159 },
160 checkpoint_threshold_frames: AtomicU32::new(config.wal_autocheckpoint),
161 table_sql: Map::new(),
162 }),
163 }
164 }
165
166 #[instrument(name = "store::multi::sqlite::conn_acquire", level = "debug", skip(self))]
167 fn lock_conn(&self) -> MutexGuard<'_, Option<Connection>> {
168 self.inner.conn.lock()
169 }
170
171 #[instrument(name = "store::multi::sqlite::checkpoint", level = "debug", skip(self))]
172 pub fn maybe_checkpoint(&self) -> Result<CheckpointOutcome> {
173 let guard = self.lock_conn();
174 let Some(conn) = guard.as_ref() else {
175 return Ok(CheckpointOutcome {
176 log_frames: 0,
177 restarted: false,
178 });
179 };
180
181 let mut log_frames: i64 = 0;
182 conn.pragma(None, "wal_checkpoint", "PASSIVE", |row| {
183 log_frames = row.get(1)?;
184 Ok(())
185 })
186 .map_err(|e| error!(internal(format!("Failed to query persistent WAL size: {}", e))))?;
187
188 let log_frames = log_frames.max(0) as u32;
189 if log_frames <= self.inner.checkpoint_threshold_frames.load(Ordering::Relaxed) {
190 return Ok(CheckpointOutcome {
191 log_frames,
192 restarted: false,
193 });
194 }
195
196 let mut busy: i64 = 1;
197 if let Err(e) = conn.pragma(None, "wal_checkpoint", "RESTART", |row| {
198 busy = row.get(0)?;
199 Ok(())
200 }) {
201 warn!(error = %e, "persistent checkpoint: RESTART failed");
202 }
203
204 Ok(CheckpointOutcome {
205 log_frames,
206 restarted: busy == 0,
207 })
208 }
209
210 pub fn set_checkpoint_threshold(&self, frames: u32) {
211 self.inner.checkpoint_threshold_frames.store(frames, Ordering::Relaxed);
212 let guard = self.lock_conn();
213 if let Some(conn) = guard.as_ref()
214 && let Err(e) = conn.pragma_update(None, "wal_autocheckpoint", frames)
215 {
216 warn!(error = %e, "failed to update wal_autocheckpoint pragma");
217 }
218 }
219
220 pub fn in_memory() -> (Self, SqliteTempPathGuard) {
221 let (config, guard) = SqliteConfig::in_memory();
222 (Self::new(config), guard)
223 }
224
225 fn table_sql(&self, table: EntryKind) -> Arc<TableSql> {
226 self.inner.table_sql.get_or_insert_with(table, || Arc::new(TableSql::build(table)))
227 }
228
229 pub fn count_current(&self, table: EntryKind) -> Result<u64> {
230 let table_sql = self.table_sql(table);
231 let guard = self.inner.readers.acquire();
232 let Some(conn) = guard.as_ref() else {
233 return Ok(0);
234 };
235 let sql = format!("SELECT COUNT(*) FROM \"{}\"", table_sql.table_name);
236 match conn.query_row(&sql, [], |row| row.get::<_, i64>(0)) {
237 Ok(c) => Ok(c as u64),
238 Err(e) if e.to_string().contains("no such table") => Ok(0),
239 Err(e) => Err(error!(internal(format!("Failed to count persistent current: {}", e)))),
240 }
241 }
242
243 pub fn delete_below_version(
244 &self,
245 table: EntryKind,
246 cutoff_version: CommitVersion,
247 prefix: Option<&[u8]>,
248 ) -> Result<Vec<EncodedKey>> {
249 let table_sql = self.table_sql(table);
250 let sql = build_delete_below_version_sql(&table_sql.table_name, prefix.is_some());
251 let cutoff = version_to_bytes(cutoff_version);
252 let guard = self.lock_conn();
253 let Some(conn) = guard.as_ref() else {
254 return Ok(Vec::new());
255 };
256 let mut stmt = match conn.prepare_cached(&sql) {
257 Ok(stmt) => stmt,
258 Err(e) if e.to_string().contains("no such table") => return Ok(Vec::new()),
259 Err(e) => {
260 return Err(error!(internal(format!(
261 "Failed to prepare delete expired for {}: {}",
262 table_sql.table_name, e
263 ))));
264 }
265 };
266 let map_key = |row: &Row| row.get::<_, Vec<u8>>(0);
267 let rows = match prefix {
268 Some(prefix) => {
269 let upper = prefix_upper_bound(prefix);
270 stmt.query_map(params![cutoff.as_slice(), prefix, upper.as_slice()], map_key)
271 }
272 None => stmt.query_map(params![cutoff.as_slice()], map_key),
273 };
274 let rows = match rows {
275 Ok(rows) => rows,
276 Err(e) if e.to_string().contains("no such table") => return Ok(Vec::new()),
277 Err(e) => {
278 return Err(error!(internal(format!(
279 "Failed to delete expired persistent rows from {}: {}",
280 table_sql.table_name, e
281 ))));
282 }
283 };
284 let mut deleted = Vec::new();
285 for row in rows {
286 match row {
287 Ok(key) => deleted.push(EncodedKey::new(key)),
288 Err(e) => {
289 return Err(error!(internal(format!(
290 "Failed to read deleted key from {}: {}",
291 table_sql.table_name, e
292 ))));
293 }
294 }
295 }
296 Ok(deleted)
297 }
298
299 pub fn delete_keys(&self, table: EntryKind, keys: &[EncodedKey]) -> Result<u64> {
300 if keys.is_empty() {
301 return Ok(0);
302 }
303 let table_sql = self.table_sql(table);
304 let guard = self.lock_conn();
305 let Some(conn) = guard.as_ref() else {
306 return Ok(0);
307 };
308 let mut total = 0u64;
309 for chunk in keys.chunks(GET_MANY_CHUNK) {
310 let sql = build_delete_keys_sql(&table_sql.table_name, chunk.len());
311 match conn.execute(&sql, params_from_iter(chunk.iter().map(|k| k.as_slice()))) {
312 Ok(n) => total += n as u64,
313 Err(e) if e.to_string().contains("no such table") => return Ok(total),
314 Err(e) => {
315 return Err(error!(internal(format!(
316 "Failed to delete keys from {}: {}",
317 table_sql.table_name, e
318 ))));
319 }
320 }
321 }
322 Ok(total)
323 }
324
325 #[instrument(name = "store::multi::persistent::sqlite::delete_through", level = "debug", skip(self, keys), fields(key_count = keys.len()))]
326 pub fn delete_keys_through(&self, table: EntryKind, keys: &[(EncodedKey, CommitVersion)]) -> Result<u64> {
327 if keys.is_empty() {
328 return Ok(0);
329 }
330 let table_sql = self.table_sql(table);
331 let guard = self.lock_conn();
332 let Some(conn) = guard.as_ref() else {
333 return Ok(0);
334 };
335 let tx = Transaction::new_unchecked(conn, TransactionBehavior::Immediate)
336 .map_err(|e| error!(internal(format!("Failed to start persistent transaction: {}", e))))?;
337 let mut total = 0u64;
338 {
339 let sql = build_delete_key_through_sql(&table_sql.table_name);
340 let mut stmt = match tx.prepare_cached(&sql) {
341 Ok(s) => s,
342 Err(e) if e.to_string().contains("no such table") => return Ok(0),
343 Err(e) => {
344 return Err(error!(internal(format!(
345 "Failed to prepare persistent delete through: {}",
346 e
347 ))));
348 }
349 };
350 for (key, version) in keys {
351 let version_bytes = version_to_bytes(*version);
352 let affected = stmt
353 .execute(params![key.as_slice(), version_bytes.as_slice()])
354 .map_err(|e| {
355 error!(internal(format!(
356 "Failed to delete key through version from {}: {}",
357 table_sql.table_name, e
358 )))
359 })?;
360 total += affected as u64;
361 }
362 }
363 tx.commit().map_err(|e| error!(internal(format!("Failed to commit persistent transaction: {}", e))))?;
364 Ok(total)
365 }
366
367 #[instrument(name = "store::multi::persistent::sqlite::set", level = "debug", skip(self, batches), fields(table_count = batches.len(), version = version.0))]
368 pub fn set_collecting_accepted(&self, version: CommitVersion, batches: TierBatch) -> Result<Vec<EncodedKey>> {
369 let mut accepted = Vec::new();
370 if batches.is_empty() {
371 return Ok(accepted);
372 }
373
374 let guard = self.lock_conn();
375 let Some(conn) = guard.as_ref() else {
376 return Ok(accepted);
377 };
378 let tx = Transaction::new_unchecked(conn, TransactionBehavior::Immediate)
379 .map_err(|e| error!(internal(format!("Failed to start persistent transaction: {}", e))))?;
380
381 let new_version_bytes = version_to_bytes(version);
382
383 for (table, entries) in batches {
384 let table_sql = self.table_sql(table);
385 Self::create_table_if_needed(&tx, &table_sql.create_sql)
386 .map_err(|e| error!(internal(format!("Failed to ensure persistent table: {}", e))))?;
387
388 let mut stmt = tx
389 .prepare_cached(&table_sql.upsert_sql)
390 .map_err(|e| error!(internal(format!("Failed to prepare persistent upsert: {}", e))))?;
391
392 for (key, value) in entries {
393 let value_slice = value.as_ref().map(|v| v.as_slice());
394 let affected = stmt
395 .execute(params![key.as_slice(), new_version_bytes.as_slice(), value_slice])
396 .map_err(|e| {
397 error!(internal(format!("Failed to upsert persistent row: {}", e)))
398 })?;
399 if affected > 0 {
400 accepted.push(key);
401 }
402 }
403 }
404
405 tx.commit().map_err(|e| error!(internal(format!("Failed to commit persistent transaction: {}", e))))?;
406 Ok(accepted)
407 }
408
409 #[instrument(name = "store::multi::persistent::sqlite::persist_sweep", level = "debug", skip(self, batches), fields(batch_count = batches.len()))]
410 pub fn persist_sweep(&self, batches: Vec<(CommitVersion, TierBatch)>) -> Result<Vec<EncodedKey>> {
411 let mut accepted = Vec::new();
412 if batches.iter().all(|(_, batch)| batch.is_empty()) {
413 return Ok(accepted);
414 }
415
416 let guard = self.lock_conn();
417 let Some(conn) = guard.as_ref() else {
418 return Err(error!(internal(
419 "Persistent storage is shut down; refusing to acknowledge a flush sweep whose \
420 writes would then be dropped from the commit buffer unpersisted"
421 .to_string()
422 )));
423 };
424 let tx = Transaction::new_unchecked(conn, TransactionBehavior::Immediate)
425 .map_err(|e| error!(internal(format!("Failed to start persistent transaction: {}", e))))?;
426
427 for (version, batch) in batches {
428 let new_version_bytes = version_to_bytes(version);
429 for (table, entries) in batch {
430 let table_sql = self.table_sql(table);
431 Self::create_table_if_needed(&tx, &table_sql.create_sql).map_err(|e| {
432 error!(internal(format!("Failed to ensure persistent table: {}", e)))
433 })?;
434
435 let mut stmt = tx.prepare_cached(&table_sql.upsert_sql).map_err(|e| {
436 error!(internal(format!("Failed to prepare persistent upsert: {}", e)))
437 })?;
438
439 for (key, value) in entries {
440 let value_slice = value.as_ref().map(|v| v.as_slice());
441 let affected = stmt
442 .execute(params![
443 key.as_slice(),
444 new_version_bytes.as_slice(),
445 value_slice
446 ])
447 .map_err(|e| {
448 error!(internal(format!(
449 "Failed to upsert persistent row: {}",
450 e
451 )))
452 })?;
453 if affected > 0 {
454 accepted.push(key);
455 }
456 }
457 }
458 }
459
460 tx.commit().map_err(|e| error!(internal(format!("Failed to commit persistent transaction: {}", e))))?;
461 Ok(accepted)
462 }
463
464 fn create_table_if_needed(conn: &Connection, create_sql: &str) -> SqliteResult<()> {
465 conn.execute_batch(create_sql)?;
466 Ok(())
467 }
468
469 fn range_chunk(&self, cursor: &mut RangeCursor, req: RangeChunkRequest<'_>) -> Result<RangeBatch> {
470 if cursor.exhausted {
471 return Ok(RangeBatch::empty());
472 }
473
474 let table_sql = self.table_sql(req.table);
475 let guard = self.inner.readers.acquire();
476 let Some(conn) = guard.as_ref() else {
477 cursor.exhausted = true;
478 return Ok(RangeBatch::empty());
479 };
480
481 let sql = build_range_current_sql(
482 &table_sql.table_name,
483 bound_shape(req.start),
484 bound_shape(req.end),
485 cursor.last_key.is_some(),
486 req.descending,
487 );
488
489 let mut stmt = match conn.prepare_cached(&sql) {
490 Ok(s) => s,
491 Err(e) if e.to_string().contains("no such table") => {
492 cursor.exhausted = true;
493 return Ok(RangeBatch::empty());
494 }
495 Err(e) => return Err(error!(internal(format!("Failed to prepare persistent range: {}", e)))),
496 };
497
498 let version_bytes = version_to_bytes(req.scope.read()).to_vec();
499 let limit_i64 = req.batch_size as i64;
500 let mut params: Vec<Box<dyn ToSql>> = Vec::new();
501 match req.start {
502 Bound::Included(s) | Bound::Excluded(s) => params.push(Box::new(s.to_vec())),
503 Bound::Unbounded => {}
504 }
505 match req.end {
506 Bound::Included(e) | Bound::Excluded(e) => params.push(Box::new(e.to_vec())),
507 Bound::Unbounded => {}
508 }
509 if let Some(k) = cursor.last_key.as_deref() {
510 params.push(Box::new(k.to_vec()));
511 }
512 params.push(Box::new(version_bytes));
513 params.push(Box::new(limit_i64));
514
515 let raw: Vec<RawEntry> = match stmt.query_map(params_from_iter(params), |row| {
516 let key: Vec<u8> = row.get(0)?;
517 let version_blob: Vec<u8> = row.get(1)?;
518 let value: Option<Vec<u8>> = row.get(2)?;
519 Ok(RawEntry {
520 key: EncodedKey::new(key),
521 version: version_from_bytes(&version_blob),
522 value: value.map(CowVec::new),
523 })
524 }) {
525 Ok(rows) => rows
526 .collect::<SqliteResult<Vec<_>>>()
527 .map_err(|e| error!(internal(format!("Failed to read persistent row: {}", e))))?,
528 Err(e) if e.to_string().contains("no such table") => {
529 cursor.exhausted = true;
530 return Ok(RangeBatch::empty());
531 }
532 Err(e) => return Err(error!(internal(format!("Failed to scan persistent range: {}", e)))),
533 };
534 let entries: Vec<RawEntry> = raw.into_iter().filter(|e| req.scope.contains(e.version)).collect();
535
536 if entries.len() < req.batch_size {
537 cursor.exhausted = true;
538 }
539 if let Some(last) = entries.last() {
540 cursor.last_key = Some(last.key.clone());
541 }
542
543 let has_more = !cursor.exhausted;
544 Ok(RangeBatch {
545 entries,
546 has_more,
547 })
548 }
549
550 #[instrument(name = "store::multi::persistent::sqlite::load_consistent", level = "debug", skip_all, fields(table = ?table))]
551 pub fn load_range_consistent(
552 &self,
553 table: EntryKind,
554 start: Bound<&[u8]>,
555 end: Bound<&[u8]>,
556 read: CommitVersion,
557 limit: Option<usize>,
558 ) -> Result<Vec<RawEntry>> {
559 let table_sql = self.table_sql(table);
560 let guard = self.inner.readers.acquire();
561 let Some(conn) = guard.as_ref() else {
562 return Ok(Vec::new());
563 };
564
565 let sql = build_range_consistent_sql(&table_sql.table_name, bound_shape(start), bound_shape(end));
566
567 let mut stmt = match conn.prepare_cached(&sql) {
568 Ok(s) => s,
569 Err(e) if e.to_string().contains("no such table") => return Ok(Vec::new()),
570 Err(e) => {
571 return Err(error!(internal(format!(
572 "Failed to prepare persistent consistent range: {}",
573 e
574 ))));
575 }
576 };
577
578 let version_bytes = version_to_bytes(read).to_vec();
579 let mut params: Vec<Box<dyn ToSql>> = Vec::new();
580 match start {
581 Bound::Included(s) | Bound::Excluded(s) => params.push(Box::new(s.to_vec())),
582 Bound::Unbounded => {}
583 }
584 match end {
585 Bound::Included(e) | Bound::Excluded(e) => params.push(Box::new(e.to_vec())),
586 Bound::Unbounded => {}
587 }
588 params.push(Box::new(version_bytes));
589
590 let raw: Vec<RawEntry> = match stmt.query_map(params_from_iter(params), |row| {
591 let key: Vec<u8> = row.get(0)?;
592 let version_blob: Vec<u8> = row.get(1)?;
593 let value: Option<Vec<u8>> = row.get(2)?;
594 Ok(RawEntry {
595 key: EncodedKey::new(key),
596 version: version_from_bytes(&version_blob),
597 value: value.map(CowVec::new),
598 })
599 }) {
600 Ok(rows) => {
601 let mut collected = Vec::new();
602 for row in rows {
603 let entry = row.map_err(|e| {
604 error!(internal(format!(
605 "Failed to read persistent consistent row: {}",
606 e
607 )))
608 })?;
609 collected.push(entry);
610 if limit.is_some_and(|l| collected.len() >= l) {
611 break;
612 }
613 }
614 collected
615 }
616 Err(e) if e.to_string().contains("no such table") => return Ok(Vec::new()),
617 Err(e) => {
618 return Err(error!(internal(format!(
619 "Failed to scan persistent consistent range: {}",
620 e
621 ))));
622 }
623 };
624
625 Ok(raw)
626 }
627}
628
629fn bound_shape(b: Bound<&[u8]>) -> Bound<()> {
630 match b {
631 Bound::Included(_) => Bound::Included(()),
632 Bound::Excluded(_) => Bound::Excluded(()),
633 Bound::Unbounded => Bound::Unbounded,
634 }
635}
636
637struct RangeChunkRequest<'a> {
638 table: EntryKind,
639 start: Bound<&'a [u8]>,
640 end: Bound<&'a [u8]>,
641 scope: MultiVersionScope,
642 batch_size: usize,
643 descending: bool,
644}
645
646impl SqlitePersistentStorage {
647 #[instrument(name = "store::multi::persistent::sqlite::get::operator", level = "trace", skip(self), fields(key_len = key.len(), version = version.0))]
648 fn get_operator(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<VersionedGetResult> {
649 self.get_impl(table, key, version)
650 }
651
652 #[instrument(name = "store::multi::persistent::sqlite::get::source", level = "trace", skip(self), fields(key_len = key.len(), version = version.0))]
653 fn get_source(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<VersionedGetResult> {
654 self.get_impl(table, key, version)
655 }
656
657 #[instrument(name = "store::multi::persistent::sqlite::get::operator_internal", level = "trace", skip(self), fields(key_len = key.len(), version = version.0))]
658 fn get_operator_internal(
659 &self,
660 table: EntryKind,
661 key: &[u8],
662 version: CommitVersion,
663 ) -> Result<VersionedGetResult> {
664 self.get_impl(table, key, version)
665 }
666
667 #[instrument(name = "store::multi::persistent::sqlite::get::multi", level = "trace", skip(self), fields(key_len = key.len(), version = version.0))]
668 fn get_multi(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<VersionedGetResult> {
669 self.get_impl(table, key, version)
670 }
671
672 fn get_impl(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<VersionedGetResult> {
673 let table_sql = self.table_sql(table);
674 let guard = self.inner.readers.acquire();
675 let Some(conn) = guard.as_ref() else {
676 return Ok(VersionedGetResult::NotFound);
677 };
678
679 let result = match conn.prepare_cached(&table_sql.get_sql) {
680 Ok(mut stmt) => stmt.query_row(params![key], |row| {
681 let version_bytes: Vec<u8> = row.get(0)?;
682 let value: Option<Vec<u8>> = row.get(1)?;
683 Ok((version_from_bytes(&version_bytes), value))
684 }),
685 Err(e) if e.to_string().contains("no such table") => Err(QueryReturnedNoRows),
686 Err(e) => return Err(error!(internal(format!("Failed to prepare persistent get: {}", e)))),
687 };
688
689 match result {
690 Ok((stored_version, value)) if stored_version <= version => Ok(match value {
691 Some(v) => VersionedGetResult::Value {
692 value: CowVec::new(v),
693 version: stored_version,
694 },
695 None => VersionedGetResult::Tombstone,
696 }),
697 Ok(_) => Ok(VersionedGetResult::NotFound),
698 Err(QueryReturnedNoRows) => Ok(VersionedGetResult::NotFound),
699 Err(e) if e.to_string().contains("no such table") => Ok(VersionedGetResult::NotFound),
700 Err(e) => Err(error!(internal(format!("Failed to read persistent: {}", e)))),
701 }
702 }
703
704 #[instrument(name = "store::multi::persistent::sqlite::get_many::operator", level = "trace", skip(self, keys), fields(key_count = keys.len(), version = version.0))]
705 fn get_many_operator(
706 &self,
707 table: EntryKind,
708 keys: &[&[u8]],
709 version: CommitVersion,
710 ) -> Result<Vec<VersionedGetResult>> {
711 self.get_many_impl(table, keys, version)
712 }
713
714 #[instrument(name = "store::multi::persistent::sqlite::get_many::source", level = "trace", skip(self, keys), fields(key_count = keys.len(), version = version.0))]
715 fn get_many_source(
716 &self,
717 table: EntryKind,
718 keys: &[&[u8]],
719 version: CommitVersion,
720 ) -> Result<Vec<VersionedGetResult>> {
721 self.get_many_impl(table, keys, version)
722 }
723
724 #[instrument(name = "store::multi::persistent::sqlite::get_many::operator_internal", level = "trace", skip(self, keys), fields(key_count = keys.len(), version = version.0))]
725 fn get_many_operator_internal(
726 &self,
727 table: EntryKind,
728 keys: &[&[u8]],
729 version: CommitVersion,
730 ) -> Result<Vec<VersionedGetResult>> {
731 self.get_many_impl(table, keys, version)
732 }
733
734 #[instrument(name = "store::multi::persistent::sqlite::get_many::multi", level = "trace", skip(self, keys), fields(key_count = keys.len(), version = version.0))]
735 fn get_many_multi(
736 &self,
737 table: EntryKind,
738 keys: &[&[u8]],
739 version: CommitVersion,
740 ) -> Result<Vec<VersionedGetResult>> {
741 self.get_many_impl(table, keys, version)
742 }
743
744 fn get_many_impl(
745 &self,
746 table: EntryKind,
747 keys: &[&[u8]],
748 version: CommitVersion,
749 ) -> Result<Vec<VersionedGetResult>> {
750 let mut out = vec![VersionedGetResult::NotFound; keys.len()];
751 if keys.is_empty() {
752 return Ok(out);
753 }
754
755 let index: HashMap<&[u8], usize> = keys.iter().enumerate().map(|(i, &k)| (k, i)).collect();
756 let table_sql = self.table_sql(table);
757 let guard = self.inner.readers.acquire();
758 let Some(conn) = guard.as_ref() else {
759 return Ok(out);
760 };
761
762 for chunk in keys.chunks(GET_MANY_CHUNK) {
763 let bucket = bucket_key_count(chunk.len());
764 let sql = build_get_many_current_sql(&table_sql.table_name, bucket);
765 let mut stmt = match conn.prepare_cached(&sql) {
766 Ok(stmt) => stmt,
767 Err(e) if e.to_string().contains("no such table") => return Ok(out),
768 Err(e) => {
769 return Err(error!(internal(format!(
770 "Failed to prepare persistent get_many: {}",
771 e
772 ))));
773 }
774 };
775
776 let pad_key = chunk[0];
777 let padded = chunk.iter().copied().chain(repeat_n(pad_key, bucket - chunk.len()));
778 let mut rows = stmt
779 .query(params_from_iter(padded))
780 .map_err(|e| error!(internal(format!("Failed to query persistent get_many: {}", e))))?;
781
782 while let Some(row) = rows.next().map_err(|e| {
783 error!(internal(format!("Failed to read persistent get_many row: {}", e)))
784 })? {
785 let key_ref = row.get_ref(0).map_err(|e| {
786 error!(internal(format!("Failed to read persistent get_many key: {}", e)))
787 })?;
788 let key = key_ref.as_blob().map_err(|e| {
789 error!(internal(format!("Failed to decode persistent get_many key: {}", e)))
790 })?;
791 let Some(&i) = index.get(key) else {
792 continue;
793 };
794 let version_ref = row.get_ref(1).map_err(|e| {
795 error!(internal(format!("Failed to read persistent get_many version: {}", e)))
796 })?;
797 let version_bytes = version_ref.as_blob().map_err(|e| {
798 error!(internal(format!("Failed to decode persistent get_many version: {}", e)))
799 })?;
800 let stored_version = version_from_bytes(version_bytes);
801 if stored_version > version {
802 continue;
803 }
804 let value: Option<Vec<u8>> = row.get(2).map_err(|e| {
805 error!(internal(format!("Failed to read persistent get_many value: {}", e)))
806 })?;
807 out[i] = match value {
808 Some(v) => VersionedGetResult::Value {
809 value: CowVec::new(v),
810 version: stored_version,
811 },
812 None => VersionedGetResult::Tombstone,
813 };
814 }
815 }
816
817 Ok(out)
818 }
819}
820
821impl TierStorage for SqlitePersistentStorage {
822 fn get(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<VersionedGetResult> {
823 match table {
824 EntryKind::Operator(_) => self.get_operator(table, key, version),
825 EntryKind::OperatorInternal(_) => self.get_operator_internal(table, key, version),
826 EntryKind::Source(_) => self.get_source(table, key, version),
827 _ => self.get_multi(table, key, version),
828 }
829 }
830
831 fn get_many(
832 &self,
833 table: EntryKind,
834 keys: &[&[u8]],
835 version: CommitVersion,
836 ) -> Result<Vec<VersionedGetResult>> {
837 match table {
838 EntryKind::Operator(_) => self.get_many_operator(table, keys, version),
839 EntryKind::OperatorInternal(_) => self.get_many_operator_internal(table, keys, version),
840 EntryKind::Source(_) => self.get_many_source(table, keys, version),
841 _ => self.get_many_multi(table, keys, version),
842 }
843 }
844
845 fn set(&self, version: CommitVersion, batches: TierBatch) -> Result<()> {
846 self.set_collecting_accepted(version, batches)?;
847 Ok(())
848 }
849
850 #[instrument(name = "store::multi::persistent::sqlite::range", level = "trace", skip(self, cursor, start, end), fields(table = ?table, batch_size = batch_size))]
851 fn range_next(
852 &self,
853 table: EntryKind,
854 cursor: &mut RangeCursor,
855 start: Bound<&[u8]>,
856 end: Bound<&[u8]>,
857 scope: MultiVersionScope,
858 batch_size: usize,
859 ) -> Result<RangeBatch> {
860 self.range_chunk(
861 cursor,
862 RangeChunkRequest {
863 table,
864 start,
865 end,
866 scope,
867 batch_size,
868 descending: false,
869 },
870 )
871 }
872
873 #[instrument(name = "store::multi::persistent::sqlite::range_rev", level = "trace", skip(self, cursor, start, end), fields(table = ?table, batch_size = batch_size))]
874 fn range_rev_next(
875 &self,
876 table: EntryKind,
877 cursor: &mut RangeCursor,
878 start: Bound<&[u8]>,
879 end: Bound<&[u8]>,
880 scope: MultiVersionScope,
881 batch_size: usize,
882 ) -> Result<RangeBatch> {
883 self.range_chunk(
884 cursor,
885 RangeChunkRequest {
886 table,
887 start,
888 end,
889 scope,
890 batch_size,
891 descending: true,
892 },
893 )
894 }
895
896 fn ensure_table(&self, table: EntryKind) -> Result<()> {
897 let table_sql = self.table_sql(table);
898 let guard = self.inner.conn.lock();
899 let Some(conn) = guard.as_ref() else {
900 return Ok(());
901 };
902 Self::create_table_if_needed(conn, &table_sql.create_sql)
903 .map_err(|e| error!(internal(format!("Failed to ensure persistent table: {}", e))))
904 }
905
906 fn clear_table(&self, table: EntryKind) -> Result<()> {
907 let table_sql = self.table_sql(table);
908 let guard = self.inner.conn.lock();
909 let Some(conn) = guard.as_ref() else {
910 return Ok(());
911 };
912 let result = conn.execute(&format!("DELETE FROM \"{}\"", table_sql.table_name), []);
913 if let Err(e) = result
914 && !e.to_string().contains("no such table")
915 {
916 return Err(error!(internal(format!(
917 "Failed to clear persistent {}: {}",
918 table_sql.table_name, e
919 ))));
920 }
921 Ok(())
922 }
923
924 fn drop(&self, _batches: HashMap<EntryKind, Vec<(EncodedKey, CommitVersion)>>) -> Result<()> {
925 panic!("SqlitePersistentStorage::drop: persistent tier has no historical chain to drop versions from");
928 }
929
930 #[instrument(name = "store::multi::persistent::sqlite::get_all_versions", level = "trace", skip(self, key), fields(table = ?table, key_len = key.len()))]
931 fn get_all_versions(&self, table: EntryKind, key: &[u8]) -> Result<Vec<(CommitVersion, Option<CowVec<u8>>)>> {
932 let table_sql = self.table_sql(table);
935 let guard = self.inner.readers.acquire();
936 let Some(conn) = guard.as_ref() else {
937 return Ok(Vec::new());
938 };
939
940 let result = match conn.prepare_cached(&table_sql.get_sql) {
941 Ok(mut stmt) => stmt.query_row(params![key], |row| {
942 let version_bytes: Vec<u8> = row.get(0)?;
943 let value: Option<Vec<u8>> = row.get(1)?;
944 Ok((version_from_bytes(&version_bytes), value.map(CowVec::new)))
945 }),
946 Err(e) if e.to_string().contains("no such table") => return Ok(Vec::new()),
947 Err(e) => {
948 return Err(error!(internal(format!(
949 "Failed to prepare persistent get_all_versions: {}",
950 e
951 ))));
952 }
953 };
954
955 match result {
956 Ok(row) => Ok(vec![row]),
957 Err(QueryReturnedNoRows) => Ok(Vec::new()),
958 Err(e) if e.to_string().contains("no such table") => Ok(Vec::new()),
959 Err(e) => Err(error!(internal(format!("Failed to read persistent versions: {}", e)))),
960 }
961 }
962
963 fn scan_historical_below(
964 &self,
965 _table: EntryKind,
966 _cutoff: CommitVersion,
967 _cursor: &mut HistoricalCursor,
968 _batch_size: usize,
969 ) -> Result<Vec<(EncodedKey, CommitVersion)>> {
970 panic!("SqlitePersistentStorage::scan_historical_below: persistent tier has no historical chain");
973 }
974}
975
976impl TierBackend for SqlitePersistentStorage {}
977
978impl Shutdown for SqlitePersistentStorage {
979 fn shutdown(&self) {
980 if let Some(conn) = self.inner.conn.lock().take() {
981 if let Err(e) = pragma::shutdown(&conn) {
982 warn!(error = %e, "persistent close: pragma shutdown failed");
983 }
984 drop(conn);
985 }
986 self.inner.readers.shutdown();
987 }
988}
989
990#[cfg(test)]
991mod tests {
992 use std::collections::HashMap;
993
994 use reifydb_core::interface::catalog::{id::TableId, shape::ShapeId};
995
996 use super::*;
997
998 fn table() -> EntryKind {
999 EntryKind::Source(ShapeId::Table(TableId(1)))
1000 }
1001
1002 fn key(n: u64) -> EncodedKey {
1003 EncodedKey::new(n.to_be_bytes().to_vec())
1004 }
1005
1006 fn row(payload: &[u8]) -> CowVec<u8> {
1007 CowVec::new(payload.to_vec())
1008 }
1009
1010 fn visible(s: &SqlitePersistentStorage, k: &EncodedKey) -> bool {
1011 s.get(table(), k.as_slice(), CommitVersion(u64::MAX)).unwrap().value().is_some()
1012 }
1013
1014 #[test]
1015 fn delete_below_version_removes_rows_at_or_below_cutoff() {
1016 let (s, _guard) = SqlitePersistentStorage::in_memory();
1017 s.set(CommitVersion(1), HashMap::from([(table(), vec![(key(1), Some(row(b"a")))])])).unwrap();
1019 s.set(CommitVersion(2), HashMap::from([(table(), vec![(key(2), Some(row(b"b")))])])).unwrap();
1020 s.set(CommitVersion(3), HashMap::from([(table(), vec![(key(3), Some(row(b"c")))])])).unwrap();
1021 assert_eq!(s.count_current(table()).unwrap(), 3);
1022
1023 let deleted = s.delete_below_version(table(), CommitVersion(2), None).unwrap();
1024
1025 assert_eq!(deleted.len(), 2, "rows whose version is <= cutoff(2) must be physically deleted");
1026 assert_eq!(
1027 s.count_current(table()).unwrap(),
1028 1,
1029 "deletion must reclaim sqlite rows, not tombstone them"
1030 );
1031 assert!(!visible(&s, &key(1)));
1032 assert!(!visible(&s, &key(2)));
1033 assert!(visible(&s, &key(3)), "a row written after the cutoff version must survive");
1034 }
1035
1036 #[test]
1037 fn create_table_indexes_the_version_column() {
1038 let (s, _guard) = SqlitePersistentStorage::in_memory();
1042 s.set(CommitVersion(1), HashMap::from([(table(), vec![(key(1), Some(row(b"a")))])])).unwrap();
1043
1044 let table_name = s.table_sql(table()).table_name.clone();
1045 let guard = s.inner.conn.lock();
1046 let conn = guard.as_ref().expect("write connection is present");
1047
1048 let indices: Vec<String> = conn
1049 .prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = ?1")
1050 .unwrap()
1051 .query_map([table_name.as_str()], |r| r.get::<_, String>(0))
1052 .unwrap()
1053 .map(|r| r.unwrap())
1054 .collect();
1055
1056 assert!(
1057 indices.contains(&format!("{table_name}__version")),
1058 "the version column must be indexed so the TTL delete seeks instead of scanning, got {indices:?}"
1059 );
1060 assert!(
1061 !indices.iter().any(|n| n.ends_with("__created_nanos") || n.ends_with("__updated_nanos")),
1062 "the dropped timestamp indices must not be recreated, got {indices:?}"
1063 );
1064 }
1065
1066 #[test]
1067 fn delete_below_version_keeps_rows_written_after_the_cutoff() {
1068 let (s, _guard) = SqlitePersistentStorage::in_memory();
1069 s.set(CommitVersion(2), HashMap::from([(table(), vec![(key(2), Some(row(b"stale")))])])).unwrap();
1070 s.set(CommitVersion(5), HashMap::from([(table(), vec![(key(1), Some(row(b"fresh")))])])).unwrap();
1071
1072 let deleted = s.delete_below_version(table(), CommitVersion(3), None).unwrap();
1073
1074 assert_eq!(deleted.len(), 1, "only the row whose last write is at or below the cutoff is evicted");
1075 assert!(visible(&s, &key(1)), "a row written after the cutoff version must NOT be evicted");
1076 assert!(!visible(&s, &key(2)));
1077 }
1078
1079 #[test]
1080 fn delete_below_version_boundary_is_inclusive() {
1081 let (s, _guard) = SqlitePersistentStorage::in_memory();
1082 s.set(CommitVersion(5), HashMap::from([(table(), vec![(key(1), Some(row(b"v5")))])])).unwrap();
1083
1084 let deleted = s.delete_below_version(table(), CommitVersion(5), None).unwrap();
1086 assert_eq!(
1087 deleted.len(),
1088 1,
1089 "a row whose version equals the cutoff is evicted (the bound is inclusive)"
1090 );
1091 assert!(!visible(&s, &key(1)));
1092 }
1093
1094 #[test]
1095 fn delete_below_version_on_missing_table_is_noop() {
1096 let (s, _guard) = SqlitePersistentStorage::in_memory();
1097 let deleted = s
1098 .delete_below_version(EntryKind::Source(ShapeId::Table(TableId(999))), CommitVersion(100), None)
1099 .unwrap();
1100 assert_eq!(deleted.len(), 0);
1101 }
1102
1103 #[test]
1104 fn delete_below_version_with_prefix_only_touches_matching_keys() {
1105 let (s, _guard) = SqlitePersistentStorage::in_memory();
1106 let left = EncodedKey::new(vec![0x01, 0xAA]);
1108 let right = EncodedKey::new(vec![0x02, 0xBB]);
1109 s.set(
1110 CommitVersion(1),
1111 HashMap::from([(
1112 table(),
1113 vec![(left.clone(), Some(row(b"l"))), (right.clone(), Some(row(b"r")))],
1114 )]),
1115 )
1116 .unwrap();
1117
1118 let deleted = s.delete_below_version(table(), CommitVersion(2), Some(&[0x01])).unwrap();
1119
1120 assert_eq!(deleted.len(), 1, "only the 0x01-prefixed (left) row should be deleted");
1121 assert!(!visible(&s, &left));
1122 assert!(visible(&s, &right), "the 0x02-prefixed (right) row must survive a left-only prefix sweep");
1123 }
1124
1125 #[test]
1126 fn delete_below_version_returns_exactly_the_deleted_keys() {
1127 let (s, _guard) = SqlitePersistentStorage::in_memory();
1128 s.set(CommitVersion(1), HashMap::from([(table(), vec![(key(1), Some(row(b"a")))])])).unwrap();
1129 s.set(CommitVersion(2), HashMap::from([(table(), vec![(key(2), Some(row(b"b")))])])).unwrap();
1130 s.set(CommitVersion(3), HashMap::from([(table(), vec![(key(3), Some(row(b"c")))])])).unwrap();
1131
1132 let mut got: Vec<Vec<u8>> = s
1136 .delete_below_version(table(), CommitVersion(2), None)
1137 .unwrap()
1138 .iter()
1139 .map(|k| k.to_vec())
1140 .collect();
1141 got.sort();
1142 let mut want = vec![key(1).to_vec(), key(2).to_vec()];
1143 want.sort();
1144 assert_eq!(
1145 got, want,
1146 "delete_below_version must return every key it physically deleted, and only those"
1147 );
1148 assert!(visible(&s, &key(3)), "the row newer than the cutoff must neither be deleted nor returned");
1149 }
1150}