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