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