1use std::{
5 collections::HashMap,
6 iter::repeat_n,
7 ops::Bound,
8 sync::{
9 Arc,
10 atomic::{AtomicU64, AtomicUsize, Ordering},
11 },
12};
13
14use reifydb_codec::{
15 key::encoded::EncodedKey,
16 row::bytes::{SHAPE_HEADER_SIZE, read_updated_at},
17};
18#[cfg(test)]
19use reifydb_core::metrics::scan::ScanCounters;
20use reifydb_core::{
21 common::CommitVersion, error::diagnostic::internal::internal, interface::store::EntryKind,
22 metrics::scan::record_page,
23};
24use reifydb_runtime::{
25 shutdown::Shutdown,
26 sync::{
27 map::Map,
28 mutex::{Mutex, MutexGuard},
29 },
30};
31use reifydb_sqlite::{
32 SqliteConfig, SqliteTempPathGuard,
33 connection::{connect, convert_flags, resolve_db_path},
34 memory::sweep_connection_cache,
35 pragma,
36};
37use reifydb_value::{
38 Result,
39 byte_size::ByteSize,
40 count::Count,
41 error, reifydb_assertions,
42 util::cowvec::CowVec,
43 value::{datetime::DateTime, duration::Duration},
44};
45use rusqlite::{
46 Connection, Error::QueryReturnedNoRows, Result as SqliteResult, Row, ToSql, Transaction, TransactionBehavior,
47 params, params_from_iter,
48};
49use tracing::{instrument, warn};
50
51use crate::{
52 MultiVersionScope,
53 tier::{
54 DisplacedValues, RangeBatch, RangeCursor, RawEntry, TierBackend, TierBatch, TierStorage,
55 VersionedGetResult,
56 persistent::sqlite::{
57 entry::{current_table_name, current_table_name_to_entry},
58 query::{
59 build_chunked_upsert_sql, build_create_current_sql, build_delete_below_version_sql,
60 build_delete_keys_sql, build_expired_keys_sql, build_get_current_sql,
61 build_get_many_current_sql, build_range_consistent_sql, build_range_current_sql,
62 build_reap_tombstones_sql, build_upsert_current_sql, prefix_upper_bound,
63 version_from_bytes, version_to_bytes,
64 },
65 },
66 },
67};
68
69const GET_MANY_CHUNK: usize = 900;
70
71const UPSERT_CHUNK: usize = 100;
72
73const GET_MANY_BUCKETS: [usize; 5] = [1, 8, 64, 512, GET_MANY_CHUNK];
74
75fn bucket_key_count(len: usize) -> usize {
76 for &bucket in GET_MANY_BUCKETS.iter() {
77 if len <= bucket {
78 return bucket;
79 }
80 }
81 GET_MANY_CHUNK
82}
83
84const BUSY_TIMEOUT: Duration = Duration::from_milliseconds_const(200);
85
86#[derive(Clone)]
87pub struct SqlitePersistentStorage {
88 inner: Arc<SqlitePersistentStorageInner>,
89}
90
91struct SqlitePersistentStorageInner {
92 conn: Mutex<Option<Connection>>,
93 readers: ReadPool,
94 table_sql: Map<EntryKind, Arc<TableSql>>,
95 cache_hits: AtomicU64,
96 cache_misses: AtomicU64,
97 reaped_high_water: Map<EntryKind, Arc<AtomicU64>>,
98 resurrections: AtomicU64,
99}
100
101#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
102pub struct SqlitePageCacheMetrics {
103 pub used: ByteSize,
104 pub hits: Count,
105 pub misses: Count,
106 pub connections_sampled: Count,
107 pub connections_total: Count,
108}
109
110struct TableSql {
111 table_name: String,
112 get_sql: String,
113 upsert_sql: String,
114 chunked_upsert_sql: String,
115 create_sql: String,
116}
117
118impl TableSql {
119 fn build(table: EntryKind) -> Self {
120 let table_name = current_table_name(table);
121 let get_sql = build_get_current_sql(&table_name);
122 let upsert_sql = build_upsert_current_sql(&table_name);
123 let chunked_upsert_sql = build_chunked_upsert_sql(&table_name, UPSERT_CHUNK);
124 let create_sql = build_create_current_sql(&table_name);
125 Self {
126 table_name,
127 get_sql,
128 upsert_sql,
129 chunked_upsert_sql,
130 create_sql,
131 }
132 }
133}
134
135struct ReadPool {
136 conns: Vec<Mutex<Option<Connection>>>,
137 next: AtomicUsize,
138}
139
140impl ReadPool {
141 fn acquire(&self) -> MutexGuard<'_, Option<Connection>> {
142 let n = self.conns.len();
143 let start = self.next.fetch_add(1, Ordering::Relaxed) % n;
144 for i in 0..n {
145 if let Some(guard) = self.conns[(start + i) % n].try_lock() {
146 return guard;
147 }
148 }
149 self.conns[start].lock()
150 }
151
152 fn shutdown(&self) {
153 for slot in &self.conns {
154 drop(slot.lock().take());
155 }
156 }
157}
158
159impl SqlitePersistentStorage {
160 #[instrument(name = "store::multi::persistent::sqlite::new", level = "debug", skip(config), fields(
161 db_path = ?config.path,
162 page_size = config.page_size.as_ref().map(|size| size.as_bytes()),
163 read_pool_size = config.read_pool_size,
164 journal_mode = config.journal_mode.as_ref().map(|mode| mode.as_str())
165 ))]
166 pub fn new(config: SqliteConfig) -> Self {
167 let db_path = resolve_db_path(config.path.clone(), "persistent.db");
168 let flags = convert_flags(&config.flags);
169
170 let conn = connect(&db_path, flags).expect("Failed to connect to persistent database");
171 pragma::apply(&conn, &config).expect("Failed to configure persistent SQLite pragmas");
172 conn.busy_timeout(BUSY_TIMEOUT.to_std()).expect("Failed to set persistent busy timeout");
173
174 let pool_size = config.read_pool_size.max(1) as usize;
175 let mut conns = Vec::with_capacity(pool_size);
176 for _ in 0..pool_size {
177 let reader = connect(&db_path, flags).expect("Failed to open persistent read connection");
178 pragma::apply_read_only(&reader, &config)
179 .expect("Failed to configure persistent read connection");
180 reader.busy_timeout(BUSY_TIMEOUT.to_std()).expect("Failed to set persistent read busy timeout");
181 conns.push(Mutex::new(Some(reader)));
182 }
183
184 Self {
185 inner: Arc::new(SqlitePersistentStorageInner {
186 conn: Mutex::new(Some(conn)),
187 readers: ReadPool {
188 conns,
189 next: AtomicUsize::new(0),
190 },
191 table_sql: Map::new(),
192 cache_hits: AtomicU64::new(0),
193 cache_misses: AtomicU64::new(0),
194 reaped_high_water: Map::new(),
195 resurrections: AtomicU64::new(0),
196 }),
197 }
198 }
199
200 pub fn page_cache_metrics(&self) -> SqlitePageCacheMetrics {
201 let mut used = 0u64;
202 let mut sampled = 0u64;
203 let mut sweep = |conn: &Connection| {
204 let swept = sweep_connection_cache(conn);
205 self.inner.cache_hits.fetch_add(swept.hits.as_u64(), Ordering::Relaxed);
206 self.inner.cache_misses.fetch_add(swept.misses.as_u64(), Ordering::Relaxed);
207 used += swept.used.as_bytes();
208 sampled += 1;
209 };
210 if let Some(guard) = self.inner.conn.try_lock()
211 && let Some(conn) = guard.as_ref()
212 {
213 sweep(conn);
214 }
215 for slot in &self.inner.readers.conns {
216 if let Some(guard) = slot.try_lock()
217 && let Some(conn) = guard.as_ref()
218 {
219 sweep(conn);
220 }
221 }
222 SqlitePageCacheMetrics {
223 used: ByteSize::from_bytes(used),
224 hits: Count::new(self.inner.cache_hits.load(Ordering::Relaxed)),
225 misses: Count::new(self.inner.cache_misses.load(Ordering::Relaxed)),
226 connections_sampled: Count::new(sampled),
227 connections_total: Count::new(1 + self.inner.readers.conns.len() as u64),
228 }
229 }
230
231 #[instrument(name = "store::multi::sqlite::conn_acquire", level = "debug", skip(self))]
232 fn lock_conn(&self) -> MutexGuard<'_, Option<Connection>> {
233 self.inner.conn.lock()
234 }
235
236 pub fn set_checkpoint_threshold(&self, frames: u32) {
237 let guard = self.lock_conn();
238 if let Some(conn) = guard.as_ref()
239 && let Err(e) = conn.pragma_update(None, "wal_autocheckpoint", frames)
240 {
241 warn!(error = %e, "failed to update wal_autocheckpoint pragma");
242 }
243 }
244
245 pub fn in_memory() -> (Self, SqliteTempPathGuard) {
246 let (config, guard) = SqliteConfig::in_memory();
247 (Self::new(config), guard)
248 }
249
250 fn table_sql(&self, table: EntryKind) -> Arc<TableSql> {
251 self.inner.table_sql.get_or_insert_with(table, || Arc::new(TableSql::build(table)))
252 }
253
254 pub fn count_current(&self, table: EntryKind) -> Result<u64> {
255 let table_sql = self.table_sql(table);
256 let guard = self.inner.readers.acquire();
257 let Some(conn) = guard.as_ref() else {
258 return Ok(0);
259 };
260 let sql = format!("SELECT COUNT(*) FROM \"{}\"", table_sql.table_name);
261 match conn.query_row(&sql, [], |row| row.get::<_, i64>(0)) {
262 Ok(c) => Ok(c as u64),
263 Err(e) if e.to_string().contains("no such table") => Ok(0),
264 Err(e) => Err(error!(internal(format!("Failed to count persistent current: {}", e)))),
265 }
266 }
267
268 pub fn delete_below_version(
269 &self,
270 table: EntryKind,
271 cutoff_version: CommitVersion,
272 prefix: Option<&[u8]>,
273 cursor: Option<&[u8]>,
274 limit: usize,
275 ) -> Result<(Vec<EncodedKey>, Option<EncodedKey>)> {
276 if limit == 0 {
277 return Ok((Vec::new(), None));
278 }
279 let limit = limit.min(i64::MAX as usize);
280 let table_sql = self.table_sql(table);
281 let sql = build_delete_below_version_sql(
282 &table_sql.table_name,
283 prefix.is_some(),
284 cursor.is_some(),
285 limit,
286 );
287 let cutoff = version_to_bytes(cutoff_version);
288 let upper = prefix.map(prefix_upper_bound);
289 let guard = self.lock_conn();
290 let Some(conn) = guard.as_ref() else {
291 return Ok((Vec::new(), None));
292 };
293 let mut stmt = match conn.prepare_cached(&sql) {
294 Ok(stmt) => stmt,
295 Err(e) if e.to_string().contains("no such table") => return Ok((Vec::new(), None)),
296 Err(e) => {
297 return Err(error!(internal(format!(
298 "Failed to prepare delete expired for {}: {}",
299 table_sql.table_name, e
300 ))));
301 }
302 };
303 let mut binds: Vec<&[u8]> = Vec::with_capacity(4);
304 binds.push(cutoff.as_slice());
305 if let Some(prefix) = prefix {
306 binds.push(prefix);
307 binds.push(upper.as_deref().expect("upper bound is present when a prefix is present"));
308 }
309 if let Some(cursor) = cursor {
310 binds.push(cursor);
311 }
312 let map_key = |row: &Row| row.get::<_, Vec<u8>>(0);
313 let rows = match stmt.query_map(params_from_iter(binds), map_key) {
314 Ok(rows) => rows,
315 Err(e) if e.to_string().contains("no such table") => return Ok((Vec::new(), None)),
316 Err(e) => {
317 return Err(error!(internal(format!(
318 "Failed to delete expired persistent rows from {}: {}",
319 table_sql.table_name, e
320 ))));
321 }
322 };
323 let mut deleted = Vec::new();
324 for row in rows {
325 match row {
326 Ok(key) => deleted.push(EncodedKey::new(key)),
327 Err(e) => {
328 return Err(error!(internal(format!(
329 "Failed to read deleted key from {}: {}",
330 table_sql.table_name, e
331 ))));
332 }
333 }
334 }
335 let next_cursor = if deleted.len() == limit {
336 deleted.iter().max().cloned()
337 } else {
338 None
339 };
340 Ok((deleted, next_cursor))
341 }
342
343 pub fn delete_keys(&self, table: EntryKind, keys: &[EncodedKey]) -> Result<u64> {
344 if keys.is_empty() {
345 return Ok(0);
346 }
347 let table_sql = self.table_sql(table);
348 let guard = self.lock_conn();
349 let Some(conn) = guard.as_ref() else {
350 return Ok(0);
351 };
352 let mut total = 0u64;
353 for chunk in keys.chunks(GET_MANY_CHUNK) {
354 let sql = build_delete_keys_sql(&table_sql.table_name, chunk.len());
355 match conn.execute(&sql, params_from_iter(chunk.iter().map(|k| k.as_slice()))) {
356 Ok(n) => total += n as u64,
357 Err(e) if e.to_string().contains("no such table") => return Ok(total),
358 Err(e) => {
359 return Err(error!(internal(format!(
360 "Failed to delete keys from {}: {}",
361 table_sql.table_name, e
362 ))));
363 }
364 }
365 }
366 Ok(total)
367 }
368
369 pub fn list_current_entries(&self) -> Result<Vec<EntryKind>> {
370 let guard = self.lock_conn();
371 let Some(conn) = guard.as_ref() else {
372 return Ok(Vec::new());
373 };
374 let mut stmt = conn
375 .prepare_cached(
376 "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
377 )
378 .map_err(|e| error!(internal(format!("Failed to prepare table listing: {}", e))))?;
379 let names = stmt
380 .query_map([], |row| row.get::<_, String>(0))
381 .map_err(|e| error!(internal(format!("Failed to list current tables: {}", e))))?;
382 let mut out = Vec::new();
383 for name in names {
384 let name = name.map_err(|e| error!(internal(format!("Failed to read table name: {}", e))))?;
385 if let Some(kind) = current_table_name_to_entry(&name) {
386 out.push(kind);
387 }
388 }
389 Ok(out)
390 }
391
392 pub fn reap_tombstones(
393 &self,
394 kind: EntryKind,
395 cutoff_version: CommitVersion,
396 limit: usize,
397 ) -> Result<(u64, bool)> {
398 if limit == 0 {
399 return Ok((0, false));
400 }
401 let limit = limit.min(i64::MAX as usize);
402 let table_name = ¤t_table_name(kind);
403 let sql = build_reap_tombstones_sql(table_name, limit);
404 let cutoff = version_to_bytes(cutoff_version);
405 let guard = self.lock_conn();
406 let Some(conn) = guard.as_ref() else {
407 return Ok((0, false));
408 };
409 let reaped = match conn.execute(&sql, params![cutoff.as_slice()]) {
410 Ok(n) => n as u64,
411 Err(e) if e.to_string().contains("no such table") => return Ok((0, false)),
412 Err(e) => {
413 return Err(error!(internal(format!(
414 "Failed to reap tombstones from {}: {}",
415 table_name, e
416 ))));
417 }
418 };
419 self.inner
420 .reaped_high_water
421 .get_or_insert_with(kind, || Arc::new(AtomicU64::new(0)))
422 .fetch_max(cutoff_version.0, Ordering::Relaxed);
423
424 Ok((reaped, reaped == limit as u64))
425 }
426
427 pub fn expired_keys(
428 &self,
429 table: EntryKind,
430 cutoff: DateTime,
431 cursor: Option<(DateTime, &[u8])>,
432 limit: usize,
433 ) -> Result<Vec<(EncodedKey, DateTime)>> {
434 if limit == 0 {
435 return Ok(Vec::new());
436 }
437 let table_sql = self.table_sql(table);
438 let sql = build_expired_keys_sql(&table_sql.table_name, cursor.is_some(), limit.min(i64::MAX as usize));
439 let guard = self.inner.readers.acquire();
440 let Some(conn) = guard.as_ref() else {
441 return Ok(Vec::new());
442 };
443 let mut stmt = match conn.prepare_cached(&sql) {
444 Ok(stmt) => stmt,
445 Err(e) if e.to_string().contains("no such table") => return Ok(Vec::new()),
446 Err(e) => {
447 return Err(error!(internal(format!(
448 "Failed to prepare expired keys for {}: {}",
449 table_sql.table_name, e
450 ))));
451 }
452 };
453 let mut params: Vec<Box<dyn ToSql>> = vec![Box::new(cutoff.to_nanos() as i64)];
454 if let Some((at, key)) = cursor {
455 params.push(Box::new(at.to_nanos() as i64));
456 params.push(Box::new(key.to_vec()));
457 }
458 let rows = match stmt.query_map(params_from_iter(params), |row| {
459 let key: Vec<u8> = row.get(0)?;
460 let nanos: i64 = row.get(1)?;
461 Ok((EncodedKey::new(key), DateTime::from_nanos(nanos as u64)))
462 }) {
463 Ok(rows) => rows,
464 Err(e) if e.to_string().contains("no such table") => return Ok(Vec::new()),
465 Err(e) => {
466 return Err(error!(internal(format!(
467 "Failed to scan expired keys from {}: {}",
468 table_sql.table_name, e
469 ))));
470 }
471 };
472 let mut out = Vec::new();
473 for row in rows {
474 out.push(row.map_err(|e| {
475 error!(internal(format!(
476 "Failed to read expired key from {}: {}",
477 table_sql.table_name, e
478 )))
479 })?);
480 }
481 Ok(out)
482 }
483
484 pub fn resurrections(&self) -> u64 {
485 self.inner.resurrections.load(Ordering::Relaxed)
486 }
487
488 #[cfg(reifydb_assertions)]
489 fn assert_no_resurrection(
490 &self,
491 tx: &Transaction,
492 kind: EntryKind,
493 table_sql: &TableSql,
494 key: &EncodedKey,
495 version: CommitVersion,
496 ) {
497 let Some(high_water) = self.inner.reaped_high_water.get(&kind) else {
498 return;
499 };
500 let high_water = high_water.load(Ordering::Relaxed);
501 if version.0 > high_water {
502 return;
503 }
504 let exists = tx
505 .prepare_cached(&table_sql.get_sql)
506 .and_then(|mut check| check.exists(params![key.as_slice()]))
507 .unwrap_or(true);
508 if !exists {
509 self.inner.resurrections.fetch_add(1, Ordering::Relaxed);
510 }
511 assert!(
512 exists,
513 "resurrection: flush inserted an absent key into {} at version {} at or below that entry's own \
514 reaped-tombstone high-water {}; every version <= a reap cutoff was already durable for that \
515 entry when the reap ran (TombstoneReap floors on the per-kind flush watermark), so this write \
516 can only rematerialize a reaped removal - the floor contract or flush monotonicity is broken \
517 (key={:?})",
518 table_sql.table_name,
519 version.0,
520 high_water,
521 key.as_slice()
522 );
523 }
524
525 fn upsert_entries_collecting_accepted(
526 &self,
527 tx: &Transaction,
528 table: EntryKind,
529 table_sql: &TableSql,
530 version: CommitVersion,
531 entries: &[(EncodedKey, Option<CowVec<u8>>)],
532 accepted: &mut Vec<EncodedKey>,
533 ) -> Result<()> {
534 let new_version_bytes = version_to_bytes(version);
535 let mut chunk_stmt = tx
536 .prepare_cached(&table_sql.chunked_upsert_sql)
537 .map_err(|e| error!(internal(format!("Failed to prepare chunked persistent upsert: {}", e))))?;
538 let mut single_stmt = tx
539 .prepare_cached(&table_sql.upsert_sql)
540 .map_err(|e| error!(internal(format!("Failed to prepare persistent upsert: {}", e))))?;
541
542 let mut chunks = entries.chunks_exact(UPSERT_CHUNK);
543 for chunk in chunks.by_ref() {
544 let mut boxed: Vec<Box<dyn ToSql>> = Vec::with_capacity(chunk.len() * 4);
545 for (key, value) in chunk {
546 reifydb_assertions! {
547 self.assert_no_resurrection(tx, table, table_sql, key, version);
548 }
549 boxed.push(Box::new(key.as_slice().to_vec()));
550 boxed.push(Box::new(new_version_bytes.to_vec()));
551 boxed.push(Box::new(value.as_ref().map(|v| v.as_slice().to_vec())));
552 boxed.push(Box::new(
553 expiry_stamp(table, value.as_ref()).map(|at| at.to_nanos() as i64),
554 ));
555 }
556 let flat: Vec<&dyn ToSql> = boxed.iter().map(|p| p.as_ref()).collect();
557 let returned = chunk_stmt
558 .query_map(params_from_iter(flat), |row| row.get::<_, Vec<u8>>(0))
559 .map_err(|e| error!(internal(format!("Failed to upsert persistent rows: {}", e))))?;
560 for key_bytes in returned {
561 let key_bytes = key_bytes.map_err(|e| {
562 error!(internal(format!("Failed to read accepted persistent key: {}", e)))
563 })?;
564 accepted.push(EncodedKey::new(key_bytes));
565 }
566 }
567
568 for (key, value) in chunks.remainder() {
569 reifydb_assertions! {
570 self.assert_no_resurrection(tx, table, table_sql, key, version);
571 }
572 let value_slice = value.as_ref().map(|v| v.as_slice());
573 let affected = single_stmt
574 .execute(params![
575 key.as_slice(),
576 new_version_bytes.as_slice(),
577 value_slice,
578 expiry_stamp(table, value.as_ref()).map(|at| at.to_nanos() as i64)
579 ])
580 .map_err(|e| error!(internal(format!("Failed to upsert persistent row: {}", e))))?;
581 if affected > 0 {
582 accepted.push(key.clone());
583 }
584 }
585
586 Ok(())
587 }
588
589 #[instrument(name = "store::multi::persistent::sqlite::set", level = "debug", skip(self, batches), fields(table_count = batches.len(), version = version.0))]
590 pub fn set_collecting_accepted(&self, version: CommitVersion, batches: TierBatch) -> Result<Vec<EncodedKey>> {
591 let mut accepted = Vec::new();
592 if batches.is_empty() {
593 return Ok(accepted);
594 }
595
596 let guard = self.lock_conn();
597 let Some(conn) = guard.as_ref() else {
598 return Ok(accepted);
599 };
600 let tx = Transaction::new_unchecked(conn, TransactionBehavior::Immediate)
601 .map_err(|e| error!(internal(format!("Failed to start persistent transaction: {}", e))))?;
602
603 for (table, entries) in batches {
604 let table_sql = self.table_sql(table);
605 Self::create_table_if_needed(&tx, &table_sql.create_sql)
606 .map_err(|e| error!(internal(format!("Failed to ensure persistent table: {}", e))))?;
607
608 self.upsert_entries_collecting_accepted(
609 &tx,
610 table,
611 &table_sql,
612 version,
613 &entries,
614 &mut accepted,
615 )?;
616 }
617
618 tx.commit().map_err(|e| error!(internal(format!("Failed to commit persistent transaction: {}", e))))?;
619 Ok(accepted)
620 }
621
622 #[instrument(name = "store::multi::persistent::sqlite::persist_sweep", level = "debug", skip(self, batches), fields(batch_count = batches.len()))]
623 pub fn persist_sweep(&self, batches: Vec<(CommitVersion, TierBatch)>) -> Result<Vec<EncodedKey>> {
624 let mut accepted = Vec::new();
625 if batches.iter().all(|(_, batch)| batch.is_empty()) {
626 return Ok(accepted);
627 }
628
629 let guard = self.lock_conn();
630 let Some(conn) = guard.as_ref() else {
631 return Err(error!(internal(
632 "Persistent storage is shut down; refusing to acknowledge a flush sweep whose \
633 writes would then be dropped from the commit buffer unpersisted"
634 .to_string()
635 )));
636 };
637 let tx = Transaction::new_unchecked(conn, TransactionBehavior::Immediate)
638 .map_err(|e| error!(internal(format!("Failed to start persistent transaction: {}", e))))?;
639
640 for (version, batch) in batches {
641 for (table, entries) in batch {
642 let table_sql = self.table_sql(table);
643 Self::create_table_if_needed(&tx, &table_sql.create_sql).map_err(|e| {
644 error!(internal(format!("Failed to ensure persistent table: {}", e)))
645 })?;
646
647 self.upsert_entries_collecting_accepted(
648 &tx,
649 table,
650 &table_sql,
651 version,
652 &entries,
653 &mut accepted,
654 )?;
655 }
656 }
657
658 tx.commit().map_err(|e| error!(internal(format!("Failed to commit persistent transaction: {}", e))))?;
659 Ok(accepted)
660 }
661
662 fn create_table_if_needed(conn: &Connection, create_sql: &str) -> SqliteResult<()> {
663 conn.execute_batch(create_sql)?;
664 Ok(())
665 }
666
667 fn range_chunk(&self, cursor: &mut RangeCursor, req: RangeChunkRequest<'_>) -> Result<RangeBatch> {
668 if cursor.exhausted {
669 return Ok(RangeBatch::empty());
670 }
671
672 let table_sql = self.table_sql(req.table);
673 let guard = self.inner.readers.acquire();
674 let Some(conn) = guard.as_ref() else {
675 cursor.exhausted = true;
676 return Ok(RangeBatch::empty());
677 };
678
679 let sql = build_range_current_sql(
680 &table_sql.table_name,
681 bound_shape(req.start),
682 bound_shape(req.end),
683 cursor.last_key.is_some(),
684 req.descending,
685 );
686
687 let mut stmt = match conn.prepare_cached(&sql) {
688 Ok(s) => s,
689 Err(e) if e.to_string().contains("no such table") => {
690 cursor.exhausted = true;
691 return Ok(RangeBatch::empty());
692 }
693 Err(e) => return Err(error!(internal(format!("Failed to prepare persistent range: {}", e)))),
694 };
695
696 let version_bytes = version_to_bytes(req.scope.read()).to_vec();
697 let limit_i64 = req.batch_size as i64;
698 let mut params: Vec<Box<dyn ToSql>> = Vec::new();
699 match req.start {
700 Bound::Included(s) | Bound::Excluded(s) => params.push(Box::new(s.to_vec())),
701 Bound::Unbounded => {}
702 }
703 match req.end {
704 Bound::Included(e) | Bound::Excluded(e) => params.push(Box::new(e.to_vec())),
705 Bound::Unbounded => {}
706 }
707 if let Some(k) = cursor.last_key.as_deref() {
708 params.push(Box::new(k.to_vec()));
709 }
710 params.push(Box::new(version_bytes));
711 params.push(Box::new(limit_i64));
712
713 let raw: Vec<RawEntry> = match stmt.query_map(params_from_iter(params), |row| {
714 let key: Vec<u8> = row.get(0)?;
715 let version_blob: Vec<u8> = row.get(1)?;
716 let value: Option<Vec<u8>> = row.get(2)?;
717 Ok(RawEntry {
718 key: EncodedKey::new(key),
719 version: version_from_bytes(&version_blob),
720 value: value.map(CowVec::new),
721 })
722 }) {
723 Ok(rows) => rows
724 .collect::<SqliteResult<Vec<_>>>()
725 .map_err(|e| error!(internal(format!("Failed to read persistent row: {}", e))))?,
726 Err(e) if e.to_string().contains("no such table") => {
727 cursor.exhausted = true;
728 return Ok(RangeBatch::empty());
729 }
730 Err(e) => return Err(error!(internal(format!("Failed to scan persistent range: {}", e)))),
731 };
732 let page_was_full = raw.len() >= req.batch_size;
733 let last_scanned = raw.last().map(|e| e.key.clone());
734 record_page(raw.len() as u64, raw.iter().filter(|e| e.value.is_none()).count() as u64);
735 let entries: Vec<RawEntry> = raw.into_iter().filter(|e| req.scope.contains(e.version)).collect();
736
737 if !page_was_full {
738 cursor.exhausted = true;
739 }
740 if let Some(last) = last_scanned {
741 cursor.last_key = Some(last);
742 }
743
744 let has_more = !cursor.exhausted;
745 Ok(RangeBatch {
746 entries,
747 has_more,
748 })
749 }
750
751 #[instrument(name = "store::multi::persistent::sqlite::load_consistent", level = "debug", skip_all, fields(table = ?table))]
752 pub fn load_range_consistent(
753 &self,
754 table: EntryKind,
755 start: Bound<&[u8]>,
756 end: Bound<&[u8]>,
757 read: CommitVersion,
758 limit: Option<usize>,
759 ) -> Result<Vec<RawEntry>> {
760 let table_sql = self.table_sql(table);
761 let guard = self.inner.readers.acquire();
762 let Some(conn) = guard.as_ref() else {
763 return Ok(Vec::new());
764 };
765
766 let sql = build_range_consistent_sql(&table_sql.table_name, bound_shape(start), bound_shape(end));
767
768 let mut stmt = match conn.prepare_cached(&sql) {
769 Ok(s) => s,
770 Err(e) if e.to_string().contains("no such table") => return Ok(Vec::new()),
771 Err(e) => {
772 return Err(error!(internal(format!(
773 "Failed to prepare persistent consistent range: {}",
774 e
775 ))));
776 }
777 };
778
779 let version_bytes = version_to_bytes(read).to_vec();
780 let mut params: Vec<Box<dyn ToSql>> = Vec::new();
781 match start {
782 Bound::Included(s) | Bound::Excluded(s) => params.push(Box::new(s.to_vec())),
783 Bound::Unbounded => {}
784 }
785 match end {
786 Bound::Included(e) | Bound::Excluded(e) => params.push(Box::new(e.to_vec())),
787 Bound::Unbounded => {}
788 }
789 params.push(Box::new(version_bytes));
790
791 let raw: Vec<RawEntry> = match stmt.query_map(params_from_iter(params), |row| {
792 let key: Vec<u8> = row.get(0)?;
793 let version_blob: Vec<u8> = row.get(1)?;
794 let value: Option<Vec<u8>> = row.get(2)?;
795 Ok(RawEntry {
796 key: EncodedKey::new(key),
797 version: version_from_bytes(&version_blob),
798 value: value.map(CowVec::new),
799 })
800 }) {
801 Ok(rows) => {
802 let mut collected = Vec::new();
803 for row in rows {
804 let entry = row.map_err(|e| {
805 error!(internal(format!(
806 "Failed to read persistent consistent row: {}",
807 e
808 )))
809 })?;
810 collected.push(entry);
811 if limit.is_some_and(|l| collected.len() >= l) {
812 break;
813 }
814 }
815 collected
816 }
817 Err(e) if e.to_string().contains("no such table") => return Ok(Vec::new()),
818 Err(e) => {
819 return Err(error!(internal(format!(
820 "Failed to scan persistent consistent range: {}",
821 e
822 ))));
823 }
824 };
825
826 Ok(raw)
827 }
828}
829
830fn expiry_stamp(table: EntryKind, value: Option<&CowVec<u8>>) -> Option<DateTime> {
831 match (table, value) {
832 (EntryKind::Source(_) | EntryKind::PartitionedSource(_), Some(row))
833 if row.len() >= SHAPE_HEADER_SIZE =>
834 {
835 Some(read_updated_at(row))
836 }
837 _ => None,
838 }
839}
840
841fn bound_shape(b: Bound<&[u8]>) -> Bound<()> {
842 match b {
843 Bound::Included(_) => Bound::Included(()),
844 Bound::Excluded(_) => Bound::Excluded(()),
845 Bound::Unbounded => Bound::Unbounded,
846 }
847}
848
849struct RangeChunkRequest<'a> {
850 table: EntryKind,
851 start: Bound<&'a [u8]>,
852 end: Bound<&'a [u8]>,
853 scope: MultiVersionScope,
854 batch_size: usize,
855 descending: bool,
856}
857
858impl SqlitePersistentStorage {
859 #[instrument(name = "store::multi::persistent::sqlite::get::source", level = "trace", skip(self), fields(key_len = key.len(), version = version.0))]
860 fn get_source(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<VersionedGetResult> {
861 self.get_impl(table, key, version)
862 }
863
864 #[instrument(name = "store::multi::persistent::sqlite::get::multi", level = "trace", skip(self), fields(key_len = key.len(), version = version.0))]
865 fn get_multi(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<VersionedGetResult> {
866 self.get_impl(table, key, version)
867 }
868
869 fn get_impl(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<VersionedGetResult> {
870 let table_sql = self.table_sql(table);
871 let guard = self.inner.readers.acquire();
872 let Some(conn) = guard.as_ref() else {
873 return Ok(VersionedGetResult::NotFound);
874 };
875
876 let result = match conn.prepare_cached(&table_sql.get_sql) {
877 Ok(mut stmt) => stmt.query_row(params![key], |row| {
878 let version_bytes: Vec<u8> = row.get(0)?;
879 let value: Option<Vec<u8>> = row.get(1)?;
880 Ok((version_from_bytes(&version_bytes), value))
881 }),
882 Err(e) if e.to_string().contains("no such table") => Err(QueryReturnedNoRows),
883 Err(e) => return Err(error!(internal(format!("Failed to prepare persistent get: {}", e)))),
884 };
885
886 match result {
887 Ok((stored_version, value)) if stored_version <= version => Ok(match value {
888 Some(v) => VersionedGetResult::Value {
889 value: CowVec::new(v),
890 version: stored_version,
891 },
892 None => VersionedGetResult::Tombstone,
893 }),
894 Ok(_) => Ok(VersionedGetResult::NotFound),
895 Err(QueryReturnedNoRows) => Ok(VersionedGetResult::NotFound),
896 Err(e) if e.to_string().contains("no such table") => Ok(VersionedGetResult::NotFound),
897 Err(e) => Err(error!(internal(format!("Failed to read persistent: {}", e)))),
898 }
899 }
900
901 #[instrument(name = "store::multi::persistent::sqlite::get_many::source", level = "trace", skip(self, keys), fields(key_count = keys.len(), version = version.0))]
902 fn get_many_source(
903 &self,
904 table: EntryKind,
905 keys: &[&[u8]],
906 version: CommitVersion,
907 ) -> Result<Vec<VersionedGetResult>> {
908 self.get_many_impl(table, keys, version)
909 }
910
911 #[instrument(name = "store::multi::persistent::sqlite::get_many::multi", level = "trace", skip(self, keys), fields(key_count = keys.len(), version = version.0))]
912 fn get_many_multi(
913 &self,
914 table: EntryKind,
915 keys: &[&[u8]],
916 version: CommitVersion,
917 ) -> Result<Vec<VersionedGetResult>> {
918 self.get_many_impl(table, keys, version)
919 }
920
921 fn get_many_impl(
922 &self,
923 table: EntryKind,
924 keys: &[&[u8]],
925 version: CommitVersion,
926 ) -> Result<Vec<VersionedGetResult>> {
927 let mut out = vec![VersionedGetResult::NotFound; keys.len()];
928 if keys.is_empty() {
929 return Ok(out);
930 }
931
932 let index: HashMap<&[u8], usize> = keys.iter().enumerate().map(|(i, &k)| (k, i)).collect();
933 let table_sql = self.table_sql(table);
934 let guard = self.inner.readers.acquire();
935 let Some(conn) = guard.as_ref() else {
936 return Ok(out);
937 };
938
939 for chunk in keys.chunks(GET_MANY_CHUNK) {
940 let bucket = bucket_key_count(chunk.len());
941 let sql = build_get_many_current_sql(&table_sql.table_name, bucket);
942 let mut stmt = match conn.prepare_cached(&sql) {
943 Ok(stmt) => stmt,
944 Err(e) if e.to_string().contains("no such table") => return Ok(out),
945 Err(e) => {
946 return Err(error!(internal(format!(
947 "Failed to prepare persistent get_many: {}",
948 e
949 ))));
950 }
951 };
952
953 let pad_key = chunk[0];
954 let padded = chunk.iter().copied().chain(repeat_n(pad_key, bucket - chunk.len()));
955 let mut rows = stmt
956 .query(params_from_iter(padded))
957 .map_err(|e| error!(internal(format!("Failed to query persistent get_many: {}", e))))?;
958
959 while let Some(row) = rows.next().map_err(|e| {
960 error!(internal(format!("Failed to read persistent get_many row: {}", e)))
961 })? {
962 let key_ref = row.get_ref(0).map_err(|e| {
963 error!(internal(format!("Failed to read persistent get_many key: {}", e)))
964 })?;
965 let key = key_ref.as_blob().map_err(|e| {
966 error!(internal(format!("Failed to decode persistent get_many key: {}", e)))
967 })?;
968 let Some(&i) = index.get(key) else {
969 continue;
970 };
971 let version_ref = row.get_ref(1).map_err(|e| {
972 error!(internal(format!("Failed to read persistent get_many version: {}", e)))
973 })?;
974 let version_bytes = version_ref.as_blob().map_err(|e| {
975 error!(internal(format!("Failed to decode persistent get_many version: {}", e)))
976 })?;
977 let stored_version = version_from_bytes(version_bytes);
978 if stored_version > version {
979 continue;
980 }
981 let value: Option<Vec<u8>> = row.get(2).map_err(|e| {
982 error!(internal(format!("Failed to read persistent get_many value: {}", e)))
983 })?;
984 out[i] = match value {
985 Some(v) => VersionedGetResult::Value {
986 value: CowVec::new(v),
987 version: stored_version,
988 },
989 None => VersionedGetResult::Tombstone,
990 };
991 }
992 }
993
994 Ok(out)
995 }
996}
997
998impl TierStorage for SqlitePersistentStorage {
999 fn get(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<VersionedGetResult> {
1000 match table {
1001 EntryKind::Source(_) => self.get_source(table, key, version),
1002 _ => self.get_multi(table, key, version),
1003 }
1004 }
1005
1006 fn get_many(
1007 &self,
1008 table: EntryKind,
1009 keys: &[&[u8]],
1010 version: CommitVersion,
1011 ) -> Result<Vec<VersionedGetResult>> {
1012 match table {
1013 EntryKind::Source(_) => self.get_many_source(table, keys, version),
1014 _ => self.get_many_multi(table, keys, version),
1015 }
1016 }
1017
1018 fn set(&self, version: CommitVersion, batches: TierBatch) -> Result<DisplacedValues> {
1019 self.set_collecting_accepted(version, batches)?;
1020 Ok(DisplacedValues::new())
1021 }
1022
1023 #[instrument(name = "store::multi::persistent::sqlite::range", level = "trace", skip(self, cursor, start, end), fields(table = ?table, batch_size = batch_size))]
1024 fn range_next(
1025 &self,
1026 table: EntryKind,
1027 cursor: &mut RangeCursor,
1028 start: Bound<&[u8]>,
1029 end: Bound<&[u8]>,
1030 scope: MultiVersionScope,
1031 batch_size: usize,
1032 ) -> Result<RangeBatch> {
1033 self.range_chunk(
1034 cursor,
1035 RangeChunkRequest {
1036 table,
1037 start,
1038 end,
1039 scope,
1040 batch_size,
1041 descending: false,
1042 },
1043 )
1044 }
1045
1046 #[instrument(name = "store::multi::persistent::sqlite::range_rev", level = "trace", skip(self, cursor, start, end), fields(table = ?table, batch_size = batch_size))]
1047 fn range_rev_next(
1048 &self,
1049 table: EntryKind,
1050 cursor: &mut RangeCursor,
1051 start: Bound<&[u8]>,
1052 end: Bound<&[u8]>,
1053 scope: MultiVersionScope,
1054 batch_size: usize,
1055 ) -> Result<RangeBatch> {
1056 self.range_chunk(
1057 cursor,
1058 RangeChunkRequest {
1059 table,
1060 start,
1061 end,
1062 scope,
1063 batch_size,
1064 descending: true,
1065 },
1066 )
1067 }
1068
1069 fn ensure_table(&self, table: EntryKind) -> Result<()> {
1070 let table_sql = self.table_sql(table);
1071 let guard = self.inner.conn.lock();
1072 let Some(conn) = guard.as_ref() else {
1073 return Ok(());
1074 };
1075 Self::create_table_if_needed(conn, &table_sql.create_sql)
1076 .map_err(|e| error!(internal(format!("Failed to ensure persistent table: {}", e))))
1077 }
1078
1079 fn clear_table(&self, table: EntryKind) -> Result<()> {
1080 let table_sql = self.table_sql(table);
1081 let guard = self.inner.conn.lock();
1082 let Some(conn) = guard.as_ref() else {
1083 return Ok(());
1084 };
1085 let result = conn.execute(&format!("DELETE FROM \"{}\"", table_sql.table_name), []);
1086 if let Err(e) = result
1087 && !e.to_string().contains("no such table")
1088 {
1089 return Err(error!(internal(format!(
1090 "Failed to clear persistent {}: {}",
1091 table_sql.table_name, e
1092 ))));
1093 }
1094 Ok(())
1095 }
1096}
1097
1098impl TierBackend for SqlitePersistentStorage {}
1099
1100impl Shutdown for SqlitePersistentStorage {
1101 fn shutdown(&self) {
1102 if let Some(conn) = self.inner.conn.lock().take() {
1103 if let Err(e) = pragma::shutdown(&conn) {
1104 warn!(error = %e, "persistent close: pragma shutdown failed");
1105 }
1106 drop(conn);
1107 }
1108 self.inner.readers.shutdown();
1109 }
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114 use std::collections::HashMap;
1115
1116 use reifydb_core::interface::catalog::{id::TableId, storage::StorageId};
1117
1118 use super::*;
1119
1120 fn table() -> EntryKind {
1121 EntryKind::Source(StorageId::Table(TableId(1)))
1122 }
1123
1124 fn key(n: u64) -> EncodedKey {
1125 EncodedKey::new(n.to_be_bytes())
1126 }
1127
1128 fn row(payload: &[u8]) -> CowVec<u8> {
1129 CowVec::new(payload.to_vec())
1130 }
1131
1132 fn stamped(nanos: u64) -> CowVec<u8> {
1133 let mut bytes = vec![0u8; SHAPE_HEADER_SIZE];
1135 bytes[16..24].copy_from_slice(&nanos.to_le_bytes());
1136 CowVec::new(bytes)
1137 }
1138
1139 fn at(nanos: u64) -> DateTime {
1140 DateTime::from_nanos(nanos)
1141 }
1142
1143 fn expired_at(s: &SqlitePersistentStorage, kind: EntryKind, cutoff: u64) -> Vec<u64> {
1144 s.expired_keys(kind, at(cutoff), None, 100)
1145 .unwrap()
1146 .into_iter()
1147 .map(|(key, _)| u64::from_be_bytes(key.as_slice().try_into().unwrap()))
1148 .collect()
1149 }
1150
1151 fn reap(s: &SqlitePersistentStorage, keys: &[u64]) -> u64 {
1152 let keys: Vec<EncodedKey> = keys.iter().map(|n| key(*n)).collect();
1153 s.delete_keys(table(), &keys).unwrap()
1154 }
1155
1156 fn stored_keys(s: &SqlitePersistentStorage) -> Vec<u64> {
1157 let table_name = s.table_sql(table()).table_name.clone();
1159 let guard = s.inner.conn.lock();
1160 let conn = guard.as_ref().expect("write connection is present");
1161 let mut stmt = conn.prepare(&format!("SELECT key FROM \"{}\" ORDER BY key", table_name)).unwrap();
1162 let keys: Vec<u64> = stmt
1163 .query_map([], |row| row.get::<_, Vec<u8>>(0))
1164 .unwrap()
1165 .map(|key| u64::from_be_bytes(key.unwrap().as_slice().try_into().unwrap()))
1166 .collect();
1167 keys
1168 }
1169
1170 fn visible(s: &SqlitePersistentStorage, k: &EncodedKey) -> bool {
1171 s.get(table(), k.as_slice(), CommitVersion(u64::MAX)).unwrap().value().is_some()
1172 }
1173
1174 #[test]
1175 fn a_range_does_not_fetch_the_tombstones_it_would_only_discard() {
1176 let (s, _guard) = SqlitePersistentStorage::in_memory();
1182 let mut writes = Vec::new();
1183 for i in 1..=50u64 {
1184 writes.push((key(i), Some(row(b"doomed"))));
1185 }
1186 s.set(CommitVersion(1), HashMap::from([(table(), writes)])).unwrap();
1187 let mut deletes = Vec::new();
1188 for i in 1..=50u64 {
1189 deletes.push((key(i), None));
1190 }
1191 s.set(CommitVersion(2), HashMap::from([(table(), deletes)])).unwrap();
1192 s.set(CommitVersion(3), HashMap::from([(table(), vec![(key(200), Some(row(b"alive")))])])).unwrap();
1193 assert_eq!(s.count_current(table()).unwrap(), 51, "precondition: 50 tombstones are physically present");
1194
1195 let before = ScanCounters::sample();
1196 let mut cursor = RangeCursor::default();
1197 let batch = s
1198 .range_next(
1199 table(),
1200 &mut cursor,
1201 Bound::Unbounded,
1202 Bound::Unbounded,
1203 MultiVersionScope::AsOf {
1204 read: CommitVersion(10),
1205 },
1206 1024,
1207 )
1208 .unwrap();
1209 let scanned = before.since();
1210
1211 assert_eq!(
1212 batch.entries.iter().map(|e| e.key.clone()).collect::<Vec<_>>(),
1213 vec![key(200)],
1214 "a tombstoned key must not surface, and the one live row must"
1215 );
1216 assert_eq!(scanned.fetched, 1, "the 50 tombstones must never cross into Rust");
1217 assert_eq!(scanned.tombstones, 0);
1218 }
1219
1220 #[test]
1221 fn a_page_the_scope_filter_empties_does_not_end_the_scan() {
1222 let (s, _guard) = SqlitePersistentStorage::in_memory();
1229 s.set(
1230 CommitVersion(1),
1231 HashMap::from([(table(), vec![(key(1), Some(row(b"a"))), (key(2), Some(row(b"b")))])]),
1232 )
1233 .unwrap();
1234 s.set(
1235 CommitVersion(5),
1236 HashMap::from([(table(), vec![(key(3), Some(row(b"c"))), (key(4), Some(row(b"d")))])]),
1237 )
1238 .unwrap();
1239
1240 let scope = MultiVersionScope::Between {
1241 after: CommitVersion(1),
1242 read: CommitVersion(10),
1243 };
1244 let mut cursor = RangeCursor::default();
1245 let mut seen: Vec<EncodedKey> = Vec::new();
1246 loop {
1247 let batch = s
1248 .range_next(table(), &mut cursor, Bound::Unbounded, Bound::Unbounded, scope, 2)
1249 .unwrap();
1250 seen.extend(batch.entries.iter().map(|e| e.key.clone()));
1251 if !batch.has_more {
1252 break;
1253 }
1254 }
1255
1256 assert_eq!(
1257 seen,
1258 vec![key(3), key(4)],
1259 "rows newer than `after` must survive a page that filtered out entirely"
1260 );
1261 }
1262
1263 #[test]
1264 fn page_cache_metrics_accumulates_hits_and_misses_across_sweeps() {
1265 let (s, _guard) = SqlitePersistentStorage::in_memory();
1268 s.set(CommitVersion(1), HashMap::from([(table(), vec![(key(1), Some(row(b"a")))])])).unwrap();
1269 assert!(visible(&s, &key(1)));
1270
1271 let first = s.page_cache_metrics();
1272 assert_eq!(
1273 first.connections_sampled, first.connections_total,
1274 "an idle pool must have every connection sampled"
1275 );
1276 assert!(
1277 first.hits.as_u64() + first.misses.as_u64() > 0,
1278 "writing and reading a row must touch the page cache, got {first:?}"
1279 );
1280 assert!(first.used.as_bytes() > 0, "connections holding pages must report used bytes");
1281
1282 assert!(visible(&s, &key(1)));
1283 let second = s.page_cache_metrics();
1284 assert!(
1285 second.hits.as_u64() >= first.hits.as_u64(),
1286 "hit totals must accumulate, got {} then {}",
1287 first.hits.as_u64(),
1288 second.hits.as_u64()
1289 );
1290 assert!(
1291 second.misses.as_u64() >= first.misses.as_u64(),
1292 "miss totals must accumulate, got {} then {}",
1293 first.misses.as_u64(),
1294 second.misses.as_u64()
1295 );
1296 }
1297
1298 #[test]
1299 fn delete_below_version_removes_rows_at_or_below_cutoff() {
1300 let (s, _guard) = SqlitePersistentStorage::in_memory();
1301 s.set(CommitVersion(1), HashMap::from([(table(), vec![(key(1), Some(row(b"a")))])])).unwrap();
1303 s.set(CommitVersion(2), HashMap::from([(table(), vec![(key(2), Some(row(b"b")))])])).unwrap();
1304 s.set(CommitVersion(3), HashMap::from([(table(), vec![(key(3), Some(row(b"c")))])])).unwrap();
1305 assert_eq!(s.count_current(table()).unwrap(), 3);
1306
1307 let (deleted, _) = s.delete_below_version(table(), CommitVersion(2), None, None, usize::MAX).unwrap();
1308
1309 assert_eq!(deleted.len(), 2, "rows whose version is <= cutoff(2) must be physically deleted");
1310 assert_eq!(
1311 s.count_current(table()).unwrap(),
1312 1,
1313 "deletion must reclaim sqlite rows, not tombstone them"
1314 );
1315 assert!(!visible(&s, &key(1)));
1316 assert!(!visible(&s, &key(2)));
1317 assert!(visible(&s, &key(3)), "a row written after the cutoff version must survive");
1318 }
1319
1320 #[test]
1321 fn create_table_indexes_the_version_column() {
1322 let (s, _guard) = SqlitePersistentStorage::in_memory();
1325 s.set(CommitVersion(1), HashMap::from([(table(), vec![(key(1), Some(row(b"a")))])])).unwrap();
1326
1327 let table_name = s.table_sql(table()).table_name.clone();
1328 let guard = s.inner.conn.lock();
1329 let conn = guard.as_ref().expect("write connection is present");
1330
1331 let indices: Vec<String> = conn
1332 .prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = ?1")
1333 .unwrap()
1334 .query_map([table_name.as_str()], |r| r.get::<_, String>(0))
1335 .unwrap()
1336 .map(|r| r.unwrap())
1337 .collect();
1338
1339 assert!(
1340 indices.contains(&format!("{table_name}__version")),
1341 "the version column must be indexed so the TTL delete seeks instead of scanning, got {indices:?}"
1342 );
1343 assert!(
1344 !indices.iter().any(|n| n.ends_with("__created_nanos") || n.ends_with("__updated_nanos")),
1345 "the dropped timestamp indices must not be recreated, got {indices:?}"
1346 );
1347 }
1348
1349 #[test]
1350 fn delete_below_version_keeps_rows_written_after_the_cutoff() {
1351 let (s, _guard) = SqlitePersistentStorage::in_memory();
1352 s.set(CommitVersion(2), HashMap::from([(table(), vec![(key(2), Some(row(b"stale")))])])).unwrap();
1353 s.set(CommitVersion(5), HashMap::from([(table(), vec![(key(1), Some(row(b"fresh")))])])).unwrap();
1354
1355 let (deleted, _) = s.delete_below_version(table(), CommitVersion(3), None, None, usize::MAX).unwrap();
1356
1357 assert_eq!(deleted.len(), 1, "only the row whose last write is at or below the cutoff is evicted");
1358 assert!(visible(&s, &key(1)), "a row written after the cutoff version must NOT be evicted");
1359 assert!(!visible(&s, &key(2)));
1360 }
1361
1362 #[test]
1363 fn delete_below_version_boundary_is_inclusive() {
1364 let (s, _guard) = SqlitePersistentStorage::in_memory();
1365 s.set(CommitVersion(5), HashMap::from([(table(), vec![(key(1), Some(row(b"v5")))])])).unwrap();
1366
1367 let (deleted, _) = s.delete_below_version(table(), CommitVersion(5), None, None, usize::MAX).unwrap();
1368 assert_eq!(
1369 deleted.len(),
1370 1,
1371 "a row whose version equals the cutoff is evicted (the bound is inclusive)"
1372 );
1373 assert!(!visible(&s, &key(1)));
1374 }
1375
1376 #[test]
1377 fn delete_below_version_on_missing_table_is_noop() {
1378 let (s, _guard) = SqlitePersistentStorage::in_memory();
1379 let (deleted, _) = s
1380 .delete_below_version(
1381 EntryKind::Source(StorageId::Table(TableId(999))),
1382 CommitVersion(100),
1383 None,
1384 None,
1385 usize::MAX,
1386 )
1387 .unwrap();
1388 assert_eq!(deleted.len(), 0);
1389 }
1390
1391 #[test]
1392 fn delete_below_version_with_prefix_only_touches_matching_keys() {
1393 let (s, _guard) = SqlitePersistentStorage::in_memory();
1394 let left = EncodedKey::new(vec![0x01, 0xAA]);
1395 let right = EncodedKey::new(vec![0x02, 0xBB]);
1396 s.set(
1397 CommitVersion(1),
1398 HashMap::from([(
1399 table(),
1400 vec![(left.clone(), Some(row(b"l"))), (right.clone(), Some(row(b"r")))],
1401 )]),
1402 )
1403 .unwrap();
1404
1405 let (deleted, _) =
1406 s.delete_below_version(table(), CommitVersion(2), Some(&[0x01]), None, usize::MAX).unwrap();
1407
1408 assert_eq!(deleted.len(), 1, "only the 0x01-prefixed (left) row should be deleted");
1409 assert!(!visible(&s, &left));
1410 assert!(visible(&s, &right), "the 0x02-prefixed (right) row must survive a left-only prefix sweep");
1411 }
1412
1413 #[test]
1414 fn delete_below_version_returns_exactly_the_deleted_keys() {
1415 let (s, _guard) = SqlitePersistentStorage::in_memory();
1418 s.set(CommitVersion(1), HashMap::from([(table(), vec![(key(1), Some(row(b"a")))])])).unwrap();
1419 s.set(CommitVersion(2), HashMap::from([(table(), vec![(key(2), Some(row(b"b")))])])).unwrap();
1420 s.set(CommitVersion(3), HashMap::from([(table(), vec![(key(3), Some(row(b"c")))])])).unwrap();
1421
1422 let mut got: Vec<Vec<u8>> = s
1423 .delete_below_version(table(), CommitVersion(2), None, None, usize::MAX)
1424 .unwrap()
1425 .0
1426 .iter()
1427 .map(|k| k.to_vec())
1428 .collect();
1429 got.sort();
1430 let mut want = vec![key(1).to_vec(), key(2).to_vec()];
1431 want.sort();
1432 assert_eq!(
1433 got, want,
1434 "delete_below_version must return every key it physically deleted, and only those"
1435 );
1436 assert!(visible(&s, &key(3)), "the row newer than the cutoff must neither be deleted nor returned");
1437 }
1438
1439 #[test]
1440 fn delete_below_version_caps_one_call_and_reports_a_resume_cursor() {
1441 let (s, _guard) = SqlitePersistentStorage::in_memory();
1444 for n in 1..=5u64 {
1445 s.set(CommitVersion(n), HashMap::from([(table(), vec![(key(n), Some(row(b"x")))])])).unwrap();
1446 }
1447 assert_eq!(s.count_current(table()).unwrap(), 5);
1448
1449 let (deleted, cursor) = s.delete_below_version(table(), CommitVersion(5), None, None, 2).unwrap();
1450
1451 assert_eq!(deleted.len(), 2, "a limit of 2 must delete exactly two rows in one call");
1452 assert_eq!(s.count_current(table()).unwrap(), 3, "only the two capped rows may be physically gone");
1453 assert_eq!(
1454 cursor,
1455 Some(key(2)),
1456 "hitting the cap must return the largest deleted key so the next slice resumes above it"
1457 );
1458 assert!(!visible(&s, &key(1)));
1459 assert!(!visible(&s, &key(2)));
1460 assert!(visible(&s, &key(3)), "the first uncapped key must still be present");
1461 }
1462
1463 #[test]
1464 fn delete_below_version_resumes_from_cursor_and_drains_without_gaps() {
1465 let (s, _guard) = SqlitePersistentStorage::in_memory();
1468 for n in 1..=5u64 {
1469 s.set(CommitVersion(n), HashMap::from([(table(), vec![(key(n), Some(row(b"x")))])])).unwrap();
1470 }
1471
1472 let mut cursor = None;
1473 let mut all: Vec<Vec<u8>> = Vec::new();
1474 let mut calls = 0;
1475 loop {
1476 let (deleted, next) =
1477 s.delete_below_version(table(), CommitVersion(5), None, cursor.as_deref(), 2).unwrap();
1478 calls += 1;
1479 all.extend(deleted.iter().map(|k| k.to_vec()));
1480 match next {
1481 Some(k) => cursor = Some(k.to_vec()),
1482 None => break,
1483 }
1484 }
1485
1486 let mut want = (1..=5u64).map(|n| key(n).to_vec()).collect::<Vec<_>>();
1487 want.sort();
1488 all.sort();
1489 assert_eq!(all, want, "resuming from the cursor must delete every eligible key exactly once, no gaps");
1490 assert_eq!(calls, 3, "5 rows at limit 2 must drain in ceil(5/2) = 3 calls (2 + 2 + 1)");
1491 assert_eq!(s.count_current(table()).unwrap(), 0);
1492 }
1493
1494 #[test]
1495 fn reap_then_flush_of_newer_versions_records_no_resurrection() {
1496 let (s, _guard) = SqlitePersistentStorage::in_memory();
1499 s.set(CommitVersion(2), HashMap::from([(table(), vec![(key(1), None)])])).unwrap();
1500 s.reap_tombstones(table(), CommitVersion(2), 100).unwrap();
1501
1502 s.set(
1503 CommitVersion(3),
1504 HashMap::from([(table(), vec![(key(1), Some(row(b"back"))), (key(2), Some(row(b"new")))])]),
1505 )
1506 .unwrap();
1507
1508 assert_eq!(
1509 s.resurrections(),
1510 0,
1511 "writes above the reap high-water are ordinary flushes; counting them would make the tripwire \
1512 fire on every healthy re-insert"
1513 );
1514 assert!(visible(&s, &key(1)), "a fresh write after a reaped removal must land");
1515 }
1516
1517 #[test]
1518 fn a_reap_high_water_does_not_carry_across_entry_kinds() {
1519 let other = EntryKind::Source(StorageId::Table(TableId(2)));
1522 let (s, _guard) = SqlitePersistentStorage::in_memory();
1523 s.set(CommitVersion(5), HashMap::from([(other, vec![(key(1), None)])])).unwrap();
1524 s.reap_tombstones(other, CommitVersion(5), 100).unwrap();
1525
1526 s.set(CommitVersion(4), HashMap::from([(table(), vec![(key(9), Some(row(b"fresh")))])])).unwrap();
1527
1528 assert_eq!(
1529 s.resurrections(),
1530 0,
1531 "a first-ever insert into an unreaped entry is absent by definition; charging it against \
1532 another entry's cutoff makes the tripwire fire on healthy writes"
1533 );
1534 assert!(visible(&s, &key(9)), "the fresh row must land");
1535 }
1536
1537 #[test]
1538 fn a_chunked_upsert_batch_reports_exactly_the_keys_that_won_their_cas() {
1539 let (s, _guard) = SqlitePersistentStorage::in_memory();
1541
1542 let evens: Vec<_> = (0..170u64).step_by(2).map(|i| (key(i), Some(row(b"seed-even")))).collect();
1543 let odds: Vec<_> = (1..170u64).step_by(2).map(|i| (key(i), Some(row(b"seed-odd")))).collect();
1544 s.set_collecting_accepted(CommitVersion(200), HashMap::from([(table(), evens)])).unwrap();
1545 s.set_collecting_accepted(CommitVersion(50), HashMap::from([(table(), odds)])).unwrap();
1546
1547 let attempt: Vec<_> = (0..170u64).map(|i| (key(i), Some(row(b"attempt")))).collect();
1548 let accepted =
1549 s.set_collecting_accepted(CommitVersion(150), HashMap::from([(table(), attempt)])).unwrap();
1550
1551 let mut accepted_ids: Vec<u64> =
1552 accepted.iter().map(|k| u64::from_be_bytes(k.as_slice().try_into().unwrap())).collect();
1553 accepted_ids.sort();
1554 let expected_odds: Vec<u64> = (1..170u64).step_by(2).collect();
1555 assert_eq!(
1556 accepted_ids, expected_odds,
1557 "only the keys whose stored version (50) lost to this batch's version (150) may be reported \
1558 accepted"
1559 );
1560
1561 for i in (0..170u64).step_by(2) {
1562 let value = s.get(table(), key(i).as_slice(), CommitVersion(u64::MAX)).unwrap().value();
1563 assert_eq!(
1564 value.as_ref().map(|v| v.as_slice()),
1565 Some(&b"seed-even"[..]),
1566 "key {i} lost its CAS (stored version 200 >= batch version 150) and must be untouched"
1567 );
1568 }
1569 for i in (1..170u64).step_by(2) {
1570 let value = s.get(table(), key(i).as_slice(), CommitVersion(u64::MAX)).unwrap().value();
1571 assert_eq!(
1572 value.as_ref().map(|v| v.as_slice()),
1573 Some(&b"attempt"[..]),
1574 "key {i} won its CAS (stored version 50 < batch version 150) and must carry this batch's \
1575 value"
1576 );
1577 }
1578 }
1579
1580 #[cfg(reifydb_assertions)]
1581 #[test]
1582 #[should_panic(expected = "resurrection")]
1583 fn flush_below_the_reap_high_water_of_an_absent_key_trips_the_resurrection_assertion() {
1584 let (s, _guard) = SqlitePersistentStorage::in_memory();
1587 s.set(CommitVersion(5), HashMap::from([(table(), vec![(key(1), None)])])).unwrap();
1588 s.reap_tombstones(table(), CommitVersion(5), 100).unwrap();
1589
1590 s.set(CommitVersion(4), HashMap::from([(table(), vec![(key(1), Some(row(b"ghost")))])])).unwrap();
1591 }
1592
1593 #[cfg(reifydb_assertions)]
1594 #[test]
1595 #[should_panic(expected = "resurrection")]
1596 fn sweep_below_the_reap_high_water_of_an_absent_key_trips_the_resurrection_assertion() {
1597 let (s, _guard) = SqlitePersistentStorage::in_memory();
1600 s.set(CommitVersion(5), HashMap::from([(table(), vec![(key(1), None)])])).unwrap();
1601 s.reap_tombstones(table(), CommitVersion(5), 100).unwrap();
1602
1603 s.persist_sweep(vec![(
1604 CommitVersion(4),
1605 HashMap::from([(table(), vec![(key(1), Some(row(b"ghost")))])]),
1606 )])
1607 .unwrap();
1608 }
1609
1610 #[test]
1611 fn reap_tombstones_removes_null_valued_rows_and_leaves_live_rows() {
1612 let (s, _guard) = SqlitePersistentStorage::in_memory();
1615 s.set(CommitVersion(1), HashMap::from([(table(), vec![(key(1), Some(row(b"live")))])])).unwrap();
1616 s.set(CommitVersion(2), HashMap::from([(table(), vec![(key(2), None)])])).unwrap();
1617 s.set(CommitVersion(3), HashMap::from([(table(), vec![(key(3), Some(row(b"live")))])])).unwrap();
1618 assert_eq!(s.count_current(table()).unwrap(), 3, "the tombstone counts as a physical row until reaped");
1619
1620 let (reaped, more) = s.reap_tombstones(table(), CommitVersion(10), 100).unwrap();
1621
1622 assert_eq!(reaped, 1, "only the NULL-valued row is a tombstone");
1623 assert!(!more, "a batch below the limit reports no remaining backlog");
1624 assert_eq!(s.count_current(table()).unwrap(), 2, "the tombstone row must be physically gone");
1625 assert!(visible(&s, &key(1)), "a live row must never be reaped");
1626 assert!(visible(&s, &key(3)), "a live row above the tombstone must never be reaped");
1627 }
1628
1629 #[test]
1630 fn reap_tombstones_respects_the_cutoff() {
1631 let (s, _guard) = SqlitePersistentStorage::in_memory();
1634 s.set(CommitVersion(5), HashMap::from([(table(), vec![(key(1), None)])])).unwrap();
1635
1636 let (below, _) = s.reap_tombstones(table(), CommitVersion(4), 100).unwrap();
1637 assert_eq!(below, 0, "a tombstone at version 5 must not be reaped under a cutoff of 4");
1638 assert_eq!(s.count_current(table()).unwrap(), 1, "the tombstone must still be present");
1639
1640 let (at, _) = s.reap_tombstones(table(), CommitVersion(5), 100).unwrap();
1641 assert_eq!(at, 1, "the cutoff is inclusive: version 5 is reapable at cutoff 5");
1642 assert_eq!(s.count_current(table()).unwrap(), 0);
1643 }
1644
1645 #[test]
1646 fn reap_tombstones_is_bounded_by_limit_and_reports_more() {
1647 let (s, _guard) = SqlitePersistentStorage::in_memory();
1650 for n in 1..=3u64 {
1651 s.set(CommitVersion(n), HashMap::from([(table(), vec![(key(n), None)])])).unwrap();
1652 }
1653
1654 let (first, more_first) = s.reap_tombstones(table(), CommitVersion(10), 2).unwrap();
1655 assert_eq!(first, 2, "a limit of 2 caps the first call at two tombstones");
1656 assert!(more_first, "hitting the limit must report backlog remaining");
1657
1658 let (second, more_second) = s.reap_tombstones(table(), CommitVersion(10), 2).unwrap();
1659 assert_eq!(second, 1, "the third tombstone drains on the next call");
1660 assert!(!more_second, "a sub-limit batch reports no further backlog");
1661 assert_eq!(s.count_current(table()).unwrap(), 0);
1662 }
1663
1664 #[test]
1665 fn expired_keys_returns_rows_at_or_below_the_cutoff_oldest_first() {
1666 let (s, _guard) = SqlitePersistentStorage::in_memory();
1668 s.set(
1669 CommitVersion(1),
1670 HashMap::from([(
1671 table(),
1672 vec![
1673 (key(1), Some(stamped(300))),
1674 (key(2), Some(stamped(100))),
1675 (key(3), Some(stamped(200))),
1676 (key(4), Some(stamped(500))),
1677 ],
1678 )]),
1679 )
1680 .unwrap();
1681
1682 assert_eq!(
1683 expired_at(&s, table(), 300),
1684 vec![2, 3, 1],
1685 "candidates must come back ordered by their own stamp, oldest first, cutoff inclusive"
1686 );
1687 assert_eq!(expired_at(&s, table(), 99), Vec::<u64>::new(), "a cutoff below every stamp yields nothing");
1688 }
1689
1690 #[test]
1691 fn expired_keys_never_returns_a_tombstone() {
1692 let (s, _guard) = SqlitePersistentStorage::in_memory();
1694 s.set(CommitVersion(1), HashMap::from([(table(), vec![(key(1), Some(stamped(100)))])])).unwrap();
1695 s.set(CommitVersion(2), HashMap::from([(table(), vec![(key(1), None)])])).unwrap();
1696
1697 assert_eq!(
1698 expired_at(&s, table(), 1_000),
1699 Vec::<u64>::new(),
1700 "a valueless row must not surface as an expiry candidate"
1701 );
1702 }
1703
1704 #[test]
1705 fn a_fresh_write_clears_an_earlier_expiry_stamp() {
1706 let (s, _guard) = SqlitePersistentStorage::in_memory();
1708 s.set(CommitVersion(1), HashMap::from([(table(), vec![(key(1), Some(stamped(100)))])])).unwrap();
1709 assert_eq!(expired_at(&s, table(), 150), vec![1], "precondition: the row starts out expired");
1710
1711 s.set(CommitVersion(2), HashMap::from([(table(), vec![(key(1), Some(stamped(900)))])])).unwrap();
1712
1713 assert_eq!(
1714 expired_at(&s, table(), 150),
1715 Vec::<u64>::new(),
1716 "rewriting the row must carry its new stamp into the index, not leave the stale one"
1717 );
1718 assert_eq!(expired_at(&s, table(), 900), vec![1], "and the row expires again against the new stamp");
1719 }
1720
1721 #[test]
1722 fn expired_keys_ignores_entries_whose_rows_carry_no_stamp() {
1723 let (s, _guard) = SqlitePersistentStorage::in_memory();
1725 s.set(CommitVersion(1), HashMap::from([(EntryKind::Multi, vec![(key(1), Some(stamped(100)))])]))
1726 .unwrap();
1727
1728 assert_eq!(
1729 expired_at(&s, EntryKind::Multi, 1_000),
1730 Vec::<u64>::new(),
1731 "a non-row entry must never produce expiry candidates, whatever its bytes look like"
1732 );
1733 }
1734
1735 #[test]
1736 fn expired_keys_resumes_from_the_cursor_without_gaps_or_repeats() {
1737 let (s, _guard) = SqlitePersistentStorage::in_memory();
1739 s.set(
1740 CommitVersion(1),
1741 HashMap::from([(
1742 table(),
1743 vec![
1744 (key(1), Some(stamped(100))),
1745 (key(2), Some(stamped(100))),
1746 (key(3), Some(stamped(200))),
1747 ],
1748 )]),
1749 )
1750 .unwrap();
1751
1752 let mut seen = Vec::new();
1753 let mut cursor: Option<(DateTime, EncodedKey)> = None;
1754 loop {
1755 let batch = s
1756 .expired_keys(table(), at(1_000), cursor.as_ref().map(|(a, k)| (*a, k.as_slice())), 1)
1757 .unwrap();
1758 let Some((k, a)) = batch.last().cloned() else {
1759 break;
1760 };
1761 seen.push(u64::from_be_bytes(k.as_slice().try_into().unwrap()));
1762 cursor = Some((a, k));
1763 }
1764
1765 assert_eq!(
1766 seen,
1767 vec![1, 2, 3],
1768 "threading the cursor must walk every candidate exactly once, in order"
1769 );
1770 }
1771
1772 #[test]
1773 fn delete_keys_removes_the_row_outright_leaving_no_tombstone() {
1774 let (s, _guard) = SqlitePersistentStorage::in_memory();
1776 s.set(
1777 CommitVersion(1),
1778 HashMap::from([(
1779 table(),
1780 vec![
1781 (key(1), Some(stamped(100))),
1782 (key(2), Some(stamped(200))),
1783 (key(3), Some(stamped(500))),
1784 ],
1785 )]),
1786 )
1787 .unwrap();
1788
1789 assert_eq!(reap(&s, &[1, 2]), 2, "every named key must be removed");
1790 assert_eq!(stored_keys(&s), vec![3], "a reaped key must leave no row behind, not even a tombstone");
1791 assert_eq!(
1792 expired_at(&s, table(), 1_000),
1793 vec![3],
1794 "a reaped key must not resurface as an expiry candidate"
1795 );
1796 }
1797
1798 #[test]
1799 fn expiry_discovery_uses_the_partial_index() {
1800 let (s, _guard) = SqlitePersistentStorage::in_memory();
1802 for n in 1..=200u64 {
1803 s.set(CommitVersion(n), HashMap::from([(table(), vec![(key(n), Some(stamped(n)))])])).unwrap();
1804 }
1805 let table_name = s.table_sql(table()).table_name.clone();
1806
1807 let guard = s.inner.conn.lock();
1808 let conn = guard.as_ref().expect("write connection is present");
1809 conn.execute_batch("ANALYZE").unwrap();
1810 let sql = format!("EXPLAIN QUERY PLAN {}", build_expired_keys_sql(&table_name, false, 100));
1811 let details: Vec<String> = conn
1812 .prepare(&sql)
1813 .unwrap()
1814 .query_map([0i64], |r| r.get::<_, String>(3))
1815 .unwrap()
1816 .map(|r| r.unwrap())
1817 .collect();
1818
1819 assert!(
1820 details.iter().any(|d| d.contains(&format!("{table_name}__expiry"))),
1821 "expiry discovery must use the partial expiry index; query plan was {details:?}"
1822 );
1823 }
1824
1825 #[test]
1826 fn tombstone_discovery_uses_the_partial_index() {
1827 let (s, _guard) = SqlitePersistentStorage::in_memory();
1831 for n in 1..=200u64 {
1832 s.set(CommitVersion(n), HashMap::from([(table(), vec![(key(n), Some(row(b"live")))])]))
1833 .unwrap();
1834 }
1835 for n in 201..=203u64 {
1836 s.set(CommitVersion(n), HashMap::from([(table(), vec![(key(n), None)])])).unwrap();
1837 }
1838 let table_name = s.table_sql(table()).table_name.clone();
1839
1840 let guard = s.inner.conn.lock();
1841 let conn = guard.as_ref().expect("write connection is present");
1842 conn.execute_batch("ANALYZE").unwrap();
1843 let sql = format!(
1844 "EXPLAIN QUERY PLAN SELECT key FROM \"{0}\" WHERE value IS NULL AND version <= ?1 LIMIT 100",
1845 table_name
1846 );
1847 let zero = [0u8; 8];
1848 let details: Vec<String> = conn
1849 .prepare(&sql)
1850 .unwrap()
1851 .query_map([zero.as_slice()], |r| r.get::<_, String>(3))
1852 .unwrap()
1853 .map(|r| r.unwrap())
1854 .collect();
1855
1856 assert!(
1857 details.iter().any(|d| d.contains(&format!("{table_name}__tombstone"))),
1858 "reap discovery must use the partial tombstone index; query plan was {details:?}"
1859 );
1860 }
1861}