1use std::{
5 collections::{HashMap, HashSet},
6 iter::repeat_n,
7 ops::Bound,
8 sync::{
9 Arc, OnceLock,
10 atomic::{AtomicU64, 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,
22 error::diagnostic::internal::internal,
23 interface::{catalog::storage::StorageId, store::EntryKind},
24 key::{
25 row::{StoragePartitionedRowKey, StorageRowKey},
26 series::{
27 PartitionedSeriesKeyColumns, SeriesKeyColumns, StoragePartitionedSeriesKey, StorageSeriesKey,
28 },
29 },
30 metrics::scan::record_page,
31};
32use reifydb_runtime::{
33 shutdown::Shutdown,
34 sync::{
35 map::Map,
36 mutex::{Mutex, MutexGuard},
37 },
38};
39use reifydb_sqlite::{SqliteConfig, SqliteTempPathGuard, pragma};
40use reifydb_store::{
41 coverage::cursor::Cursor,
42 filter::KeyFilter,
43 metrics::PageCacheMetrics,
44 sqlite::{OpenMessages, open, page_cache_metrics, pool::ReadPool},
45};
46use reifydb_store_commit::{
47 MultiVersionScope, RangeBatch, RangeCursor, RangeStop, RawEntry, TierBatch, VersionedGetResult,
48};
49use reifydb_value::{
50 Result, error,
51 util::cowvec::CowVec,
52 value::{datetime::DateTime, row_number::RowNumber},
53};
54use rusqlite::{
55 Connection, Error::QueryReturnedNoRows, Result as SqliteResult, Row, ToSql, Transaction, TransactionBehavior,
56 params_from_iter,
57};
58use tracing::{instrument, warn};
59
60use super::schema::row_from_sql;
61use crate::{
62 filter::{ARMED_CAPACITY_KEYS, MultiKeys},
63 tier::{
64 TierStorage,
65 persistent::{
66 NarrowRangeRequest,
67 sqlite::{
68 entry::{
69 SqliteSchema, current_table_name, current_table_name_to_entry,
70 narrow_series_schema, series_schema_from_columns, sqlite_schema,
71 },
72 query::{
73 build_chunked_upsert_sql, build_chunked_upsert_sql_keyed,
74 build_chunked_upsert_sql_partitioned, build_chunked_upsert_sql_row,
75 build_create_current_sql, build_create_current_sql_partitioned,
76 build_create_current_sql_partitioned_series, build_create_current_sql_row,
77 build_create_current_sql_series, build_current_exists_sql,
78 build_current_keys_sql, build_current_keys_sql_keyed,
79 build_current_keys_sql_partitioned, build_current_keys_sql_row,
80 build_delete_current_sql, build_delete_current_sql_keyed,
81 build_delete_current_sql_partitioned, build_delete_current_sql_row,
82 build_delete_keys_sql, build_delete_keys_sql_keyed,
83 build_delete_keys_sql_partitioned, build_delete_keys_sql_row,
84 build_expired_keys_sql, build_expired_keys_sql_keyed,
85 build_expired_keys_sql_partitioned, build_expired_keys_sql_row,
86 build_get_current_sql, build_get_current_sql_keyed,
87 build_get_current_sql_partitioned, build_get_current_sql_row,
88 build_get_many_current_sql, build_get_many_current_sql_keyed,
89 build_get_many_current_sql_partitioned, build_get_many_current_sql_row,
90 build_max_version_sql, build_range_current_sql,
91 build_range_current_sql_partitioned, build_range_current_sql_partitioned_exact,
92 build_range_current_sql_partitioned_series, build_range_current_sql_row,
93 build_range_current_sql_series, build_upsert_current_sql,
94 build_upsert_current_sql_keyed, build_upsert_current_sql_partitioned,
95 build_upsert_current_sql_row, version_from_bytes, version_to_bytes,
96 },
97 schema::{
98 PartitionedRangeBounds, SeriesRangeBounds, partition_half_from_sql,
99 partition_half_to_sql, partitioned_ident_of, partitioned_key_for,
100 partitioned_range_bounds, partitioned_series_ident_of,
101 partitioned_series_key_for, row_ident_of, row_key_for, row_range_bounds,
102 row_to_sql, series_ident_of, series_key_for, series_range_bounds,
103 series_storage_header, series_suffix_widths,
104 },
105 },
106 },
107 },
108};
109
110const GET_MANY_CHUNK: usize = 900;
111
112const UPSERT_CHUNK: usize = 100;
113
114const GET_MANY_BUCKETS: [usize; 5] = [1, 8, 64, 512, GET_MANY_CHUNK];
115
116fn bucket_key_count(len: usize) -> usize {
117 for &bucket in GET_MANY_BUCKETS.iter() {
118 if len <= bucket {
119 return bucket;
120 }
121 }
122 GET_MANY_CHUNK
123}
124
125#[derive(Clone)]
126pub struct SqlitePersistentStorage {
127 inner: Arc<SqlitePersistentStorageInner>,
128}
129
130struct SqlitePersistentStorageInner {
131 conn: Mutex<Option<Connection>>,
132 readers: ReadPool,
133 table_sql: Map<EntryKind, Arc<TableSql>>,
134 cache_hits: AtomicU64,
135 cache_misses: AtomicU64,
136 filter: KeyFilter<MultiKeys>,
137
138 written_high_water: AtomicU64,
139 opened_high_water: OnceLock<u64>,
140}
141
142struct TableSql {
143 table_name: String,
144 schema: SqliteSchema,
145 get_sql: String,
146 upsert_sql: String,
147 chunked_upsert_sql: String,
148 delete_sql: String,
149 chunked_delete_sql: String,
150 create_sql: String,
151}
152
153impl TableSql {
154 fn build(table: EntryKind, schema: SqliteSchema) -> Self {
155 let table_name = current_table_name(table);
156 let (get_sql, upsert_sql, chunked_upsert_sql, delete_sql, chunked_delete_sql, create_sql) = match schema
157 {
158 SqliteSchema::Blob => (
159 build_get_current_sql(&table_name),
160 build_upsert_current_sql(&table_name),
161 build_chunked_upsert_sql(&table_name, UPSERT_CHUNK),
162 build_delete_current_sql(&table_name, 1, false),
163 build_delete_current_sql(&table_name, UPSERT_CHUNK, true),
164 build_create_current_sql(&table_name),
165 ),
166 SqliteSchema::Row => (
167 build_get_current_sql_row(&table_name),
168 build_upsert_current_sql_row(&table_name),
169 build_chunked_upsert_sql_row(&table_name, UPSERT_CHUNK),
170 build_delete_current_sql_row(&table_name, 1, false),
171 build_delete_current_sql_row(&table_name, UPSERT_CHUNK, true),
172 build_create_current_sql_row(&table_name),
173 ),
174 SqliteSchema::Partitioned => (
175 build_get_current_sql_partitioned(&table_name),
176 build_upsert_current_sql_partitioned(&table_name),
177 build_chunked_upsert_sql_partitioned(&table_name, UPSERT_CHUNK),
178 build_delete_current_sql_partitioned(&table_name, 1, false),
179 build_delete_current_sql_partitioned(&table_name, UPSERT_CHUNK, true),
180 build_create_current_sql_partitioned(&table_name),
181 ),
182 SqliteSchema::Series | SqliteSchema::PartitionedSeries => {
183 let columns = series_columns(schema);
184 let create = if schema == SqliteSchema::Series {
185 build_create_current_sql_series(&table_name)
186 } else {
187 build_create_current_sql_partitioned_series(&table_name)
188 };
189 (
190 build_get_current_sql_keyed(&table_name, columns),
191 build_upsert_current_sql_keyed(&table_name, columns),
192 build_chunked_upsert_sql_keyed(&table_name, columns, UPSERT_CHUNK),
193 build_delete_current_sql_keyed(&table_name, columns, 1, false),
194 build_delete_current_sql_keyed(&table_name, columns, UPSERT_CHUNK, true),
195 create,
196 )
197 }
198 };
199 Self {
200 table_name,
201 schema,
202 get_sql,
203 upsert_sql,
204 chunked_upsert_sql,
205 delete_sql,
206 chunked_delete_sql,
207 create_sql,
208 }
209 }
210}
211
212const OPEN_MESSAGES: OpenMessages = OpenMessages {
213 connect: "Failed to connect to persistent database",
214 pragmas: "Failed to configure persistent SQLite pragmas",
215 busy_timeout: "Failed to set persistent busy timeout",
216 read_connect: "Failed to open persistent read connection",
217 read_pragmas: "Failed to configure persistent read connection",
218 read_busy_timeout: "Failed to set persistent read busy timeout",
219};
220
221impl SqlitePersistentStorage {
222 #[instrument(name = "store::multi::persistent::sqlite::new", level = "debug", skip(config), fields(
223 db_path = ?config.path,
224 page_size = config.page_size.as_ref().map(|size| size.as_bytes()),
225 read_pool_size = config.read_pool_size,
226 journal_mode = config.journal_mode.as_ref().map(|mode| mode.as_str())
227 ))]
228 pub fn new(config: SqliteConfig) -> Self {
229 let (conn, readers) = open(&config, "persistent.db", &OPEN_MESSAGES);
230
231 let filter = if any_current_row(&conn) {
232 KeyFilter::<MultiKeys>::new()
233 } else {
234 KeyFilter::<MultiKeys>::armed(ARMED_CAPACITY_KEYS)
235 };
236
237 Self {
238 inner: Arc::new(SqlitePersistentStorageInner {
239 conn: Mutex::new(Some(conn)),
240 readers,
241 table_sql: Map::new(),
242 cache_hits: AtomicU64::new(0),
243 cache_misses: AtomicU64::new(0),
244 filter,
245 written_high_water: AtomicU64::new(0),
246 opened_high_water: OnceLock::new(),
247 }),
248 }
249 }
250
251 pub fn install_floor(&self) -> Result<CommitVersion> {
252 let opened = match self.inner.opened_high_water.get() {
253 Some(opened) => *opened,
254 None => {
255 let probed = self.probe_high_water()?;
256 *self.inner.opened_high_water.get_or_init(|| probed)
257 }
258 };
259 Ok(CommitVersion(opened.max(self.inner.written_high_water.load(Ordering::SeqCst))))
260 }
261
262 fn probe_high_water(&self) -> Result<u64> {
263 let guard = self.inner.readers.acquire();
264 let Some(conn) = guard.as_ref() else {
265 return Err(error!(internal(
266 "Persistent storage is shut down; refusing to probe the install floor, whose \
267 fallback would gate coverage materializes on a version no read ever observed"
268 .to_string()
269 )));
270 };
271 Ok(highest_current_version(conn))
272 }
273
274 fn record_written_version(&self, version: CommitVersion) {
275 self.inner.written_high_water.fetch_max(version.0, Ordering::SeqCst);
276 }
277
278 pub fn filter(&self) -> &KeyFilter<MultiKeys> {
279 &self.inner.filter
280 }
281
282 pub(crate) fn current_key_slice(
283 &self,
284 table: EntryKind,
285 cursor: Option<&EncodedKey>,
286 budget: usize,
287 ) -> Result<Vec<EncodedKey>> {
288 if budget == 0 {
289 return Ok(Vec::new());
290 }
291 let guard = self.inner.readers.acquire();
292 let Some(conn) = guard.as_ref() else {
293 return Ok(Vec::new());
294 };
295 let table_sql = self.table_sql(conn, table)?;
296 let storage = source_storage(table);
297 let limit = budget.min(i64::MAX as usize) as i64;
298
299 let sql = match table_sql.schema {
300 SqliteSchema::Blob => build_current_keys_sql(&table_sql.table_name, cursor.is_some()),
301 SqliteSchema::Row => build_current_keys_sql_row(&table_sql.table_name, cursor.is_some()),
302 SqliteSchema::Partitioned => {
303 build_current_keys_sql_partitioned(&table_sql.table_name, cursor.is_some())
304 }
305 SqliteSchema::Series | SqliteSchema::PartitionedSeries => build_current_keys_sql_keyed(
306 &table_sql.table_name,
307 series_columns(table_sql.schema),
308 cursor.is_some(),
309 ),
310 };
311
312 let mut params: Vec<Box<dyn ToSql>> = Vec::new();
313 if let Some(key) = cursor {
314 match table_sql.schema {
315 SqliteSchema::Blob => params.push(Box::new(key.to_vec())),
316 _ => {
317 let ints = key_ints(table_sql.schema, key.as_slice()).ok_or_else(|| {
318 error!(internal(
319 "a current-key cursor does not decode under its own table's \
320 narrow schema"
321 .to_string()
322 ))
323 })?;
324 for i in ints {
325 params.push(Box::new(i));
326 }
327 }
328 }
329 }
330 params.push(Box::new(limit));
331
332 let mut stmt = match conn.prepare_cached(&sql) {
333 Ok(stmt) => stmt,
334 Err(e) if e.to_string().contains("no such table") => return Ok(Vec::new()),
335 Err(e) => {
336 return Err(error!(internal(format!("Failed to prepare current key scan: {}", e))));
337 }
338 };
339 let flat: Vec<&dyn ToSql> = params.iter().map(|p| p.as_ref()).collect();
340 let mut rows = stmt
341 .query(params_from_iter(flat))
342 .map_err(|e| error!(internal(format!("Failed to scan current keys: {}", e))))?;
343
344 let mut out = Vec::with_capacity(budget);
345 while let Some(row) =
346 rows.next().map_err(|e| error!(internal(format!("Failed to read current key: {}", e))))?
347 {
348 let returned = read_returned_key(table_sql.schema, row)
349 .map_err(|e| error!(internal(format!("Failed to decode current key: {}", e))))?;
350 out.push(returned.into_encoded_key(storage));
351 }
352 Ok(out)
353 }
354
355 pub fn page_cache_metrics(&self) -> PageCacheMetrics {
356 page_cache_metrics(
357 &self.inner.conn,
358 &self.inner.readers,
359 &self.inner.cache_hits,
360 &self.inner.cache_misses,
361 )
362 }
363
364 #[instrument(name = "store::multi::sqlite::conn_acquire", level = "debug", skip(self))]
365 fn lock_conn(&self) -> MutexGuard<'_, Option<Connection>> {
366 self.inner.conn.lock()
367 }
368
369 pub fn set_checkpoint_threshold(&self, frames: u32) {
370 let guard = self.lock_conn();
371 if let Some(conn) = guard.as_ref()
372 && let Err(e) = conn.pragma_update(None, "wal_autocheckpoint", frames)
373 {
374 warn!(error = %e, "failed to update wal_autocheckpoint pragma");
375 }
376 }
377
378 pub fn in_memory() -> (Self, SqliteTempPathGuard) {
379 let (config, guard) = SqliteConfig::in_memory();
380 (Self::new(config), guard)
381 }
382
383 fn table_sql(&self, conn: &Connection, table: EntryKind) -> Result<Arc<TableSql>> {
384 if let Some(cached) = self.inner.table_sql.get(&table) {
385 return Ok(cached);
386 }
387 let built = Arc::new(TableSql::build(table, resolve_schema(conn, table)?));
388 self.inner.table_sql.insert(table, built.clone());
389 Ok(built)
390 }
391
392 pub fn count_current(&self, table: EntryKind) -> Result<u64> {
393 let guard = self.inner.readers.acquire();
394 let Some(conn) = guard.as_ref() else {
395 return Ok(0);
396 };
397 let table_sql = self.table_sql(conn, table)?;
398 let sql = format!("SELECT COUNT(*) FROM \"{}\"", table_sql.table_name);
399 match conn.query_row(&sql, [], |row| row.get::<_, i64>(0)) {
400 Ok(c) => Ok(c as u64),
401 Err(e) if e.to_string().contains("no such table") => Ok(0),
402 Err(e) => Err(error!(internal(format!("Failed to count persistent current: {}", e)))),
403 }
404 }
405 pub fn delete_keys(&self, table: EntryKind, keys: &[EncodedKey]) -> Result<u64> {
406 if keys.is_empty() {
407 return Ok(0);
408 }
409 let guard = self.lock_conn();
410 let Some(conn) = guard.as_ref() else {
411 return Ok(0);
412 };
413 let table_sql = self.table_sql(conn, table)?;
414 let mut total = 0u64;
415 for chunk in keys.chunks(GET_MANY_CHUNK) {
416 let sql = match table_sql.schema {
417 SqliteSchema::Blob => build_delete_keys_sql(&table_sql.table_name, chunk.len()),
418 SqliteSchema::Row => build_delete_keys_sql_row(&table_sql.table_name, chunk.len()),
419 SqliteSchema::Partitioned => {
420 build_delete_keys_sql_partitioned(&table_sql.table_name, chunk.len())
421 }
422 SqliteSchema::Series | SqliteSchema::PartitionedSeries => build_delete_keys_sql_keyed(
423 &table_sql.table_name,
424 series_columns(table_sql.schema),
425 chunk.len(),
426 ),
427 };
428 let mut boxed: Vec<Box<dyn ToSql>> =
429 Vec::with_capacity(chunk.len() * table_sql.schema.key_column_count());
430 for key in chunk {
431 push_key_params(table_sql.schema, key.as_slice(), &mut boxed)?;
432 }
433 let flat: Vec<&dyn ToSql> = boxed.iter().map(|p| p.as_ref()).collect();
434 match conn.execute(&sql, params_from_iter(flat)) {
435 Ok(n) => total += n as u64,
436 Err(e) if e.to_string().contains("no such table") => return Ok(total),
437 Err(e) => {
438 return Err(error!(internal(format!(
439 "Failed to delete keys from {}: {}",
440 table_sql.table_name, e
441 ))));
442 }
443 }
444 }
445 Ok(total)
446 }
447
448 pub fn list_current_entries(&self) -> Result<Vec<EntryKind>> {
449 let guard = self.lock_conn();
450 let Some(conn) = guard.as_ref() else {
451 return Ok(Vec::new());
452 };
453 let mut stmt = conn
454 .prepare_cached(
455 "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
456 )
457 .map_err(|e| error!(internal(format!("Failed to prepare table listing: {}", e))))?;
458 let names = stmt
459 .query_map([], |row| row.get::<_, String>(0))
460 .map_err(|e| error!(internal(format!("Failed to list current tables: {}", e))))?;
461 let mut out = Vec::new();
462 for name in names {
463 let name = name.map_err(|e| error!(internal(format!("Failed to read table name: {}", e))))?;
464 if let Some(kind) = current_table_name_to_entry(&name) {
465 out.push(kind);
466 }
467 }
468 Ok(out)
469 }
470
471 pub fn expired_keys(
472 &self,
473 table: EntryKind,
474 cutoff: DateTime,
475 cursor: Option<(DateTime, &[u8])>,
476 limit: usize,
477 ) -> Result<Vec<(EncodedKey, DateTime)>> {
478 if limit == 0 {
479 return Ok(Vec::new());
480 }
481 let guard = self.inner.readers.acquire();
482 let Some(conn) = guard.as_ref() else {
483 return Ok(Vec::new());
484 };
485 let table_sql = self.table_sql(conn, table)?;
486 let storage = source_storage(table);
487 let limit = limit.min(i64::MAX as usize);
488 let sql = match table_sql.schema {
489 SqliteSchema::Blob => build_expired_keys_sql(&table_sql.table_name, cursor.is_some(), limit),
490 SqliteSchema::Row => build_expired_keys_sql_row(&table_sql.table_name, cursor.is_some(), limit),
491 SqliteSchema::Partitioned => {
492 build_expired_keys_sql_partitioned(&table_sql.table_name, cursor.is_some(), limit)
493 }
494 SqliteSchema::Series | SqliteSchema::PartitionedSeries => build_expired_keys_sql_keyed(
495 &table_sql.table_name,
496 series_columns(table_sql.schema),
497 cursor.is_some(),
498 limit,
499 ),
500 };
501 let mut stmt = match conn.prepare_cached(&sql) {
502 Ok(stmt) => stmt,
503 Err(e) if e.to_string().contains("no such table") => return Ok(Vec::new()),
504 Err(e) => {
505 return Err(error!(internal(format!(
506 "Failed to prepare expired keys for {}: {}",
507 table_sql.table_name, e
508 ))));
509 }
510 };
511 let mut params: Vec<Box<dyn ToSql>> = vec![Box::new(cutoff.to_nanos() as i64)];
512 if let Some((at, key)) = cursor {
513 params.push(Box::new(at.to_nanos() as i64));
514 match table_sql.schema {
515 SqliteSchema::Blob => params.push(Box::new(key.to_vec())),
516 _ => {
517 let ints = key_ints(table_sql.schema, key).ok_or_else(|| {
518 error!(internal(
519 "an expired-keys cursor does not decode under its own \
520 table's narrow schema"
521 .to_string()
522 ))
523 })?;
524 for i in ints {
525 params.push(Box::new(i));
526 }
527 }
528 }
529 }
530 let key_columns = table_sql.schema.key_column_count();
531 let flat: Vec<&dyn ToSql> = params.iter().map(|p| p.as_ref()).collect();
532 let rows = match stmt.query_map(params_from_iter(flat), |row| {
533 let returned = read_returned_key(table_sql.schema, row)?;
534 let nanos: i64 = row.get(key_columns)?;
535 Ok((returned, DateTime::from_nanos(nanos as u64)))
536 }) {
537 Ok(rows) => rows,
538 Err(e) if e.to_string().contains("no such table") => return Ok(Vec::new()),
539 Err(e) => {
540 return Err(error!(internal(format!(
541 "Failed to scan expired keys from {}: {}",
542 table_sql.table_name, e
543 ))));
544 }
545 };
546 let mut out = Vec::new();
547 for row in rows {
548 let (returned, at) = row.map_err(|e| {
549 error!(internal(format!(
550 "Failed to read expired key from {}: {}",
551 table_sql.table_name, e
552 )))
553 })?;
554 out.push((returned.into_encoded_key(storage), at));
555 }
556 Ok(out)
557 }
558
559 fn upsert_entries_collecting_accepted(
560 &self,
561 tx: &Transaction,
562 table: EntryKind,
563 table_sql: &TableSql,
564 version: CommitVersion,
565 entries: &[(EncodedKey, Option<CowVec<u8>>)],
566 accepted: &mut Vec<EncodedKey>,
567 ) -> Result<()> {
568 self.record_written_version(version);
569 let new_version_bytes = version_to_bytes(version);
570 let mut chunk_stmt = tx
571 .prepare_cached(&table_sql.chunked_upsert_sql)
572 .map_err(|e| error!(internal(format!("Failed to prepare chunked persistent upsert: {}", e))))?;
573 let mut single_stmt = tx
574 .prepare_cached(&table_sql.upsert_sql)
575 .map_err(|e| error!(internal(format!("Failed to prepare persistent upsert: {}", e))))?;
576
577 let mut sets: Vec<&(EncodedKey, Option<CowVec<u8>>)> = Vec::with_capacity(entries.len());
578 let mut removals: Vec<&EncodedKey> = Vec::new();
579 for entry in entries {
580 if entry.1.is_some() {
581 sets.push(entry);
582 } else {
583 removals.push(&entry.0);
584 }
585 }
586
587 let storage = source_storage(table);
588 let key_columns = table_sql.schema.key_column_count();
589
590 let mut chunks = sets.chunks_exact(UPSERT_CHUNK);
591 for chunk in chunks.by_ref() {
592 let mut boxed: Vec<Box<dyn ToSql>> = Vec::with_capacity(chunk.len() * (key_columns + 3));
593 for (key, value) in chunk.iter().copied() {
594 self.inner.filter.add((table, key));
595 push_key_params(table_sql.schema, key.as_slice(), &mut boxed)?;
596 boxed.push(Box::new(new_version_bytes.to_vec()));
597 boxed.push(Box::new(value.as_ref().map(|v| v.as_slice().to_vec())));
598 boxed.push(Box::new(
599 expiry_stamp(table, value.as_ref()).map(|at| at.to_nanos() as i64),
600 ));
601 }
602 let flat: Vec<&dyn ToSql> = boxed.iter().map(|p| p.as_ref()).collect();
603 let returned = chunk_stmt
604 .query_map(params_from_iter(flat), |row| read_returned_key(table_sql.schema, row))
605 .map_err(|e| error!(internal(format!("Failed to upsert persistent rows: {}", e))))?;
606 for key in returned {
607 let key = key
608 .map_err(|e| {
609 error!(internal(format!(
610 "Failed to read accepted persistent key: {}",
611 e
612 )))
613 })?
614 .into_encoded_key(storage);
615 accepted.push(key);
616 }
617 }
618
619 for (key, value) in chunks.remainder().iter().copied() {
620 self.inner.filter.add((table, key));
621 let mut boxed: Vec<Box<dyn ToSql>> = Vec::with_capacity(key_columns + 3);
622 push_key_params(table_sql.schema, key.as_slice(), &mut boxed)?;
623 boxed.push(Box::new(new_version_bytes.to_vec()));
624 boxed.push(Box::new(value.as_ref().map(|v| v.as_slice().to_vec())));
625 boxed.push(Box::new(expiry_stamp(table, value.as_ref()).map(|at| at.to_nanos() as i64)));
626 let flat: Vec<&dyn ToSql> = boxed.iter().map(|p| p.as_ref()).collect();
627 let affected = single_stmt
628 .execute(params_from_iter(flat))
629 .map_err(|e| error!(internal(format!("Failed to upsert persistent row: {}", e))))?;
630 if affected > 0 {
631 accepted.push(key.clone());
632 }
633 }
634
635 self.delete_entries_collecting_accepted(tx, table, table_sql, &new_version_bytes, &removals, accepted)?;
636
637 Ok(())
638 }
639
640 fn delete_entries_collecting_accepted(
641 &self,
642 tx: &Transaction,
643 table: EntryKind,
644 table_sql: &TableSql,
645 version_bytes: &[u8],
646 removals: &[&EncodedKey],
647 accepted: &mut Vec<EncodedKey>,
648 ) -> Result<()> {
649 if removals.is_empty() {
650 return Ok(());
651 }
652 let mut chunk_stmt = tx
653 .prepare_cached(&table_sql.chunked_delete_sql)
654 .map_err(|e| error!(internal(format!("Failed to prepare chunked persistent delete: {}", e))))?;
655 let mut single_stmt = tx
656 .prepare_cached(&table_sql.delete_sql)
657 .map_err(|e| error!(internal(format!("Failed to prepare persistent delete: {}", e))))?;
658
659 let storage = source_storage(table);
660 let key_columns = table_sql.schema.key_column_count();
661
662 let mut chunks = removals.chunks_exact(UPSERT_CHUNK);
663 for chunk in chunks.by_ref() {
664 let mut boxed: Vec<Box<dyn ToSql>> = Vec::with_capacity(chunk.len() * key_columns + 1);
665 for key in chunk.iter() {
666 push_key_params(table_sql.schema, key.as_slice(), &mut boxed)?;
667 }
668 boxed.push(Box::new(version_bytes.to_vec()));
669 let flat: Vec<&dyn ToSql> = boxed.iter().map(|p| p.as_ref()).collect();
670 let returned = chunk_stmt
671 .query_map(params_from_iter(flat), |row| read_returned_key(table_sql.schema, row))
672 .map_err(|e| error!(internal(format!("Failed to delete persistent rows: {}", e))))?;
673 for key in returned {
674 let key = key
675 .map_err(|e| {
676 error!(internal(format!(
677 "Failed to read deleted persistent key: {}",
678 e
679 )))
680 })?
681 .into_encoded_key(storage);
682 accepted.push(key);
683 }
684 }
685
686 for key in chunks.remainder().iter().copied() {
687 let mut boxed: Vec<Box<dyn ToSql>> = Vec::with_capacity(key_columns + 1);
688 push_key_params(table_sql.schema, key.as_slice(), &mut boxed)?;
689 boxed.push(Box::new(version_bytes.to_vec()));
690 let flat: Vec<&dyn ToSql> = boxed.iter().map(|p| p.as_ref()).collect();
691 let affected = single_stmt
692 .execute(params_from_iter(flat))
693 .map_err(|e| error!(internal(format!("Failed to delete persistent row: {}", e))))?;
694 if affected > 0 {
695 accepted.push((*key).clone());
696 }
697 }
698
699 Ok(())
700 }
701
702 #[instrument(name = "store::multi::persistent::sqlite::set", level = "debug", skip(self, batches), fields(table_count = batches.len(), version = version.0))]
703 pub fn set_collecting_accepted(&self, version: CommitVersion, batches: TierBatch) -> Result<Vec<EncodedKey>> {
704 let mut accepted = Vec::new();
705 if batches.is_empty() {
706 return Ok(accepted);
707 }
708
709 let guard = self.lock_conn();
710 let Some(conn) = guard.as_ref() else {
711 return Ok(accepted);
712 };
713 let tx = Transaction::new_unchecked(conn, TransactionBehavior::Immediate)
714 .map_err(|e| error!(internal(format!("Failed to start persistent transaction: {}", e))))?;
715
716 for (table, entries) in batches {
717 let table_sql = self.table_sql(&tx, table)?;
718 Self::create_table_if_needed(&tx, &table_sql.create_sql)
719 .map_err(|e| error!(internal(format!("Failed to ensure persistent table: {}", e))))?;
720
721 self.upsert_entries_collecting_accepted(
722 &tx,
723 table,
724 &table_sql,
725 version,
726 &entries,
727 &mut accepted,
728 )?;
729 }
730
731 tx.commit().map_err(|e| error!(internal(format!("Failed to commit persistent transaction: {}", e))))?;
732 Ok(accepted)
733 }
734
735 #[instrument(name = "store::multi::persistent::sqlite::persist_sweep", level = "debug", skip(self, batches), fields(batch_count = batches.len()))]
736 pub fn persist_sweep(&self, batches: Vec<(CommitVersion, TierBatch)>) -> Result<Vec<EncodedKey>> {
737 let mut accepted = Vec::new();
738 if batches.iter().all(|(_, batch)| batch.is_empty()) {
739 return Ok(accepted);
740 }
741
742 let guard = self.lock_conn();
743 let Some(conn) = guard.as_ref() else {
744 return Err(error!(internal(
745 "Persistent storage is shut down; refusing to acknowledge a flush sweep whose \
746 writes would then be dropped from the commit buffer unpersisted"
747 .to_string()
748 )));
749 };
750 let tx = Transaction::new_unchecked(conn, TransactionBehavior::Immediate)
751 .map_err(|e| error!(internal(format!("Failed to start persistent transaction: {}", e))))?;
752
753 let mut ensured: HashSet<EntryKind> = HashSet::new();
754 for (version, batch) in batches {
755 for (table, entries) in batch {
756 let table_sql = self.table_sql(&tx, table)?;
757 if ensured.insert(table) {
758 Self::create_table_if_needed(&tx, &table_sql.create_sql).map_err(|e| {
759 error!(internal(format!("Failed to ensure persistent table: {}", e)))
760 })?;
761 }
762
763 self.upsert_entries_collecting_accepted(
764 &tx,
765 table,
766 &table_sql,
767 version,
768 &entries,
769 &mut accepted,
770 )?;
771 }
772 }
773
774 tx.commit().map_err(|e| error!(internal(format!("Failed to commit persistent transaction: {}", e))))?;
775 Ok(accepted)
776 }
777
778 fn create_table_if_needed(conn: &Connection, create_sql: &str) -> SqliteResult<()> {
779 conn.execute_batch(create_sql)?;
780 Ok(())
781 }
782
783 pub(crate) fn range_chunk_row(
784 &self,
785 cursor: &mut Cursor<RangeStop, StorageRowKey>,
786 req: NarrowRangeRequest<'_, StorageRowKey>,
787 ) -> Result<RangeBatch<StorageRowKey>> {
788 if cursor.is_exhausted() {
789 return Ok(RangeBatch::empty());
790 }
791
792 let guard = self.inner.readers.acquire();
793 let Some(conn) = guard.as_ref() else {
794 return Err(error!(internal(
795 "Persistent storage is shut down; refusing to report a range chunk exhausted \
796 having read nothing, which hands the caller a short scan reported as a \
797 complete one"
798 .to_string()
799 )));
800 };
801 let table_sql = self.table_sql(conn, req.table)?;
802
803 let version_bytes = version_to_bytes(req.scope.read()).to_vec();
804 let limit_i64 = req.batch_size as i64;
805
806 let to_sql_bound = |bound: Bound<&StorageRowKey>| match bound {
807 Bound::Included(key) => Bound::Included(row_to_sql(key.row().0)),
808 Bound::Excluded(key) => Bound::Excluded(row_to_sql(key.row().0)),
809 Bound::Unbounded => Bound::Unbounded,
810 };
811 let lower = to_sql_bound(req.start);
812 let upper = to_sql_bound(req.end);
813 let last_row = cursor.last_key().map(|key| row_to_sql(key.row().0));
814
815 let sql = build_range_current_sql_row(
816 &table_sql.table_name,
817 bound_shape_of(&lower),
818 bound_shape_of(&upper),
819 last_row.is_some(),
820 req.descending,
821 );
822 let mut stmt = match conn.prepare_cached(&sql) {
823 Ok(s) => s,
824 Err(e) if e.to_string().contains("no such table") => {
825 cursor.finish_with(RangeStop::AbsentTable);
826 return Ok(RangeBatch::empty());
827 }
828 Err(e) => {
829 return Err(error!(internal(format!("Failed to prepare persistent range: {}", e))));
830 }
831 };
832
833 let mut params: Vec<Box<dyn ToSql>> = Vec::new();
834 if let Some(v) = bound_value(lower) {
835 params.push(Box::new(v));
836 }
837 if let Some(v) = bound_value(upper) {
838 params.push(Box::new(v));
839 }
840 if let Some(v) = last_row {
841 params.push(Box::new(v));
842 }
843 params.push(Box::new(version_bytes));
844 params.push(Box::new(limit_i64));
845 let flat: Vec<&dyn ToSql> = params.iter().map(|p| p.as_ref()).collect();
846
847 let raw: Vec<RawEntry<StorageRowKey>> = match stmt.query_map(params_from_iter(flat), |row| {
848 let r: i64 = row.get(0)?;
849 let version_blob: Vec<u8> = row.get(1)?;
850 let value: Option<Vec<u8>> = row.get(2)?;
851 Ok(RawEntry {
852 key: StorageRowKey::new(RowNumber(row_from_sql(r))),
853 version: version_from_bytes(&version_blob),
854 value: value.map(CowVec::new),
855 })
856 }) {
857 Ok(rows) => rows
858 .collect::<SqliteResult<Vec<_>>>()
859 .map_err(|e| error!(internal(format!("Failed to read persistent row: {}", e))))?,
860 Err(e) if e.to_string().contains("no such table") => {
861 cursor.finish_with(RangeStop::AbsentTable);
862 return Ok(RangeBatch::empty());
863 }
864 Err(e) => {
865 return Err(error!(internal(format!("Failed to scan persistent range: {}", e))));
866 }
867 };
868
869 record_page(raw.len() as u64, raw.iter().filter(|e| e.value.is_none()).count() as u64);
870 let has_more = raw.len() == req.batch_size;
871 if let Some(last) = raw.last() {
872 cursor.advance(last.key);
873 }
874 if !has_more {
875 cursor.finish_with(RangeStop::Scanned);
876 }
877 Ok(RangeBatch {
878 entries: raw,
879 has_more,
880 })
881 }
882
883 pub(crate) fn range_chunk_partitioned(
884 &self,
885 cursor: &mut Cursor<RangeStop, StoragePartitionedRowKey>,
886 req: NarrowRangeRequest<'_, StoragePartitionedRowKey>,
887 ) -> Result<RangeBatch<StoragePartitionedRowKey>> {
888 if cursor.is_exhausted() {
889 return Ok(RangeBatch::empty());
890 }
891
892 let guard = self.inner.readers.acquire();
893 let Some(conn) = guard.as_ref() else {
894 return Err(error!(internal(
895 "Persistent storage is shut down; refusing to report a range chunk exhausted \
896 having read nothing, which hands the caller a short scan reported as a \
897 complete one"
898 .to_string()
899 )));
900 };
901 let table_sql = self.table_sql(conn, req.table)?;
902
903 let version_bytes = version_to_bytes(req.scope.read()).to_vec();
904 let limit_i64 = req.batch_size as i64;
905
906 let to_sql_bound = |bound: Bound<&StoragePartitionedRowKey>| match bound {
907 Bound::Included(key) => Bound::Included(partitioned_triple(key)),
908 Bound::Excluded(key) => Bound::Excluded(partitioned_triple(key)),
909 Bound::Unbounded => Bound::Unbounded,
910 };
911 let lower = to_sql_bound(req.start);
912 let upper = to_sql_bound(req.end);
913 let last_triple = cursor.last_key().map(partitioned_triple);
914
915 let sql = build_range_current_sql_partitioned(
916 &table_sql.table_name,
917 bound_shape_of(&lower),
918 bound_shape_of(&upper),
919 last_triple.is_some(),
920 req.descending,
921 );
922 let mut stmt = match conn.prepare_cached(&sql) {
923 Ok(s) => s,
924 Err(e) if e.to_string().contains("no such table") => {
925 cursor.finish_with(RangeStop::AbsentTable);
926 return Ok(RangeBatch::empty());
927 }
928 Err(e) => {
929 return Err(error!(internal(format!("Failed to prepare persistent range: {}", e))));
930 }
931 };
932
933 let mut params: Vec<Box<dyn ToSql>> = Vec::new();
934 if let Some((hi, lo, row)) = bound_value(lower) {
935 params.push(Box::new(hi));
936 params.push(Box::new(lo));
937 params.push(Box::new(row));
938 }
939 if let Some((hi, lo, row)) = bound_value(upper) {
940 params.push(Box::new(hi));
941 params.push(Box::new(lo));
942 params.push(Box::new(row));
943 }
944 if let Some((hi, lo, row)) = last_triple {
945 params.push(Box::new(hi));
946 params.push(Box::new(lo));
947 params.push(Box::new(row));
948 }
949 params.push(Box::new(version_bytes));
950 params.push(Box::new(limit_i64));
951 let flat: Vec<&dyn ToSql> = params.iter().map(|p| p.as_ref()).collect();
952
953 let raw: Vec<RawEntry<StoragePartitionedRowKey>> = match stmt.query_map(params_from_iter(flat), |row| {
954 let hi: i64 = row.get(0)?;
955 let lo: i64 = row.get(1)?;
956 let r: i64 = row.get(2)?;
957 let version_blob: Vec<u8> = row.get(3)?;
958 let value: Option<Vec<u8>> = row.get(4)?;
959 Ok(RawEntry {
960 key: StoragePartitionedRowKey::from_halves(
961 partition_half_from_sql(hi),
962 partition_half_from_sql(lo),
963 RowNumber(row_from_sql(r)),
964 ),
965 version: version_from_bytes(&version_blob),
966 value: value.map(CowVec::new),
967 })
968 }) {
969 Ok(rows) => rows
970 .collect::<SqliteResult<Vec<_>>>()
971 .map_err(|e| error!(internal(format!("Failed to read persistent row: {}", e))))?,
972 Err(e) if e.to_string().contains("no such table") => {
973 cursor.finish_with(RangeStop::AbsentTable);
974 return Ok(RangeBatch::empty());
975 }
976 Err(e) => {
977 return Err(error!(internal(format!("Failed to scan persistent range: {}", e))));
978 }
979 };
980
981 record_page(raw.len() as u64, raw.iter().filter(|e| e.value.is_none()).count() as u64);
982 let has_more = raw.len() == req.batch_size;
983 if let Some(last) = raw.last() {
984 cursor.advance(last.key);
985 }
986 if !has_more {
987 cursor.finish_with(RangeStop::Scanned);
988 }
989 Ok(RangeBatch {
990 entries: raw,
991 has_more,
992 })
993 }
994
995 pub(crate) fn range_chunk_series(
996 &self,
997 cursor: &mut Cursor<RangeStop, StorageSeriesKey>,
998 req: NarrowRangeRequest<'_, StorageSeriesKey>,
999 ) -> Result<RangeBatch<StorageSeriesKey>> {
1000 if cursor.is_exhausted() {
1001 return Ok(RangeBatch::empty());
1002 }
1003
1004 let guard = self.inner.readers.acquire();
1005 let Some(conn) = guard.as_ref() else {
1006 return Err(error!(internal(
1007 "Persistent storage is shut down; refusing to report a range chunk exhausted \
1008 having read nothing, which hands the caller a short scan reported as a \
1009 complete one"
1010 .to_string()
1011 )));
1012 };
1013 let table_sql = self.table_sql(conn, req.table)?;
1014
1015 let version_bytes = version_to_bytes(req.scope.read()).to_vec();
1016 let limit_i64 = req.batch_size as i64;
1017
1018 let to_sql_bound = |bound: Bound<&StorageSeriesKey>| match bound {
1019 Bound::Included(key) => Bound::Included(series_triple(key)),
1020 Bound::Excluded(key) => Bound::Excluded(series_triple(key)),
1021 Bound::Unbounded => Bound::Unbounded,
1022 };
1023 let lower = to_sql_bound(req.start);
1024 let upper = to_sql_bound(req.end);
1025 let last_triple = cursor.last_key().map(series_triple);
1026
1027 let sql = build_range_current_sql_series(
1028 &table_sql.table_name,
1029 bound_shape_of(&lower),
1030 bound_shape_of(&upper),
1031 last_triple.is_some(),
1032 req.descending,
1033 );
1034 let mut stmt = match conn.prepare_cached(&sql) {
1035 Ok(s) => s,
1036 Err(e) if e.to_string().contains("no such table") => {
1037 cursor.finish_with(RangeStop::AbsentTable);
1038 return Ok(RangeBatch::empty());
1039 }
1040 Err(e) => {
1041 return Err(error!(internal(format!("Failed to prepare persistent range: {}", e))));
1042 }
1043 };
1044
1045 let mut params: Vec<Box<dyn ToSql>> = Vec::new();
1046 for triple in [bound_value(lower), bound_value(upper), last_triple].into_iter().flatten() {
1047 params.push(Box::new(triple.0));
1048 params.push(Box::new(triple.1));
1049 params.push(Box::new(triple.2));
1050 }
1051 params.push(Box::new(version_bytes));
1052 params.push(Box::new(limit_i64));
1053 let flat: Vec<&dyn ToSql> = params.iter().map(|p| p.as_ref()).collect();
1054
1055 let raw: Vec<RawEntry<StorageSeriesKey>> = match stmt.query_map(params_from_iter(flat), |row| {
1056 let variant_tag: i64 = row.get(0)?;
1057 let key: i64 = row.get(1)?;
1058 let sequence: i64 = row.get(2)?;
1059 let version_blob: Vec<u8> = row.get(3)?;
1060 let value: Option<Vec<u8>> = row.get(4)?;
1061 Ok(RawEntry {
1062 key: StorageSeriesKey::from_sql_columns(SeriesKeyColumns {
1063 variant_tag,
1064 key,
1065 sequence,
1066 }),
1067 version: version_from_bytes(&version_blob),
1068 value: value.map(CowVec::new),
1069 })
1070 }) {
1071 Ok(rows) => rows
1072 .collect::<SqliteResult<Vec<_>>>()
1073 .map_err(|e| error!(internal(format!("Failed to read persistent row: {}", e))))?,
1074 Err(e) if e.to_string().contains("no such table") => {
1075 cursor.finish_with(RangeStop::AbsentTable);
1076 return Ok(RangeBatch::empty());
1077 }
1078 Err(e) => {
1079 return Err(error!(internal(format!("Failed to scan persistent range: {}", e))));
1080 }
1081 };
1082
1083 record_page(raw.len() as u64, raw.iter().filter(|e| e.value.is_none()).count() as u64);
1084 let has_more = raw.len() == req.batch_size;
1085 if let Some(last) = raw.last() {
1086 cursor.advance(last.key);
1087 }
1088 if !has_more {
1089 cursor.finish_with(RangeStop::Scanned);
1090 }
1091 Ok(RangeBatch {
1092 entries: raw,
1093 has_more,
1094 })
1095 }
1096
1097 pub(crate) fn range_chunk_partitioned_series(
1098 &self,
1099 cursor: &mut Cursor<RangeStop, StoragePartitionedSeriesKey>,
1100 req: NarrowRangeRequest<'_, StoragePartitionedSeriesKey>,
1101 ) -> Result<RangeBatch<StoragePartitionedSeriesKey>> {
1102 if cursor.is_exhausted() {
1103 return Ok(RangeBatch::empty());
1104 }
1105
1106 let guard = self.inner.readers.acquire();
1107 let Some(conn) = guard.as_ref() else {
1108 return Err(error!(internal(
1109 "Persistent storage is shut down; refusing to report a range chunk exhausted \
1110 having read nothing, which hands the caller a short scan reported as a \
1111 complete one"
1112 .to_string()
1113 )));
1114 };
1115 let table_sql = self.table_sql(conn, req.table)?;
1116
1117 let version_bytes = version_to_bytes(req.scope.read()).to_vec();
1118 let limit_i64 = req.batch_size as i64;
1119
1120 let to_sql_bound = |bound: Bound<&StoragePartitionedSeriesKey>| match bound {
1121 Bound::Included(key) => Bound::Included(partitioned_series_columns(key)),
1122 Bound::Excluded(key) => Bound::Excluded(partitioned_series_columns(key)),
1123 Bound::Unbounded => Bound::Unbounded,
1124 };
1125 let lower = to_sql_bound(req.start);
1126 let upper = to_sql_bound(req.end);
1127 let last_columns = cursor.last_key().map(partitioned_series_columns);
1128
1129 let sql = build_range_current_sql_partitioned_series(
1130 &table_sql.table_name,
1131 bound_shape_of(&lower),
1132 bound_shape_of(&upper),
1133 last_columns.is_some(),
1134 req.descending,
1135 );
1136 let mut stmt = match conn.prepare_cached(&sql) {
1137 Ok(s) => s,
1138 Err(e) if e.to_string().contains("no such table") => {
1139 cursor.finish_with(RangeStop::AbsentTable);
1140 return Ok(RangeBatch::empty());
1141 }
1142 Err(e) => {
1143 return Err(error!(internal(format!("Failed to prepare persistent range: {}", e))));
1144 }
1145 };
1146
1147 let mut params: Vec<Box<dyn ToSql>> = Vec::new();
1148 for columns in [bound_value(lower), bound_value(upper), last_columns].into_iter().flatten() {
1149 params.push(Box::new(columns.0));
1150 params.push(Box::new(columns.1));
1151 params.push(Box::new(columns.2));
1152 params.push(Box::new(columns.3));
1153 params.push(Box::new(columns.4));
1154 }
1155 params.push(Box::new(version_bytes));
1156 params.push(Box::new(limit_i64));
1157 let flat: Vec<&dyn ToSql> = params.iter().map(|p| p.as_ref()).collect();
1158
1159 let raw: Vec<RawEntry<StoragePartitionedSeriesKey>> =
1160 match stmt.query_map(params_from_iter(flat), |row| {
1161 let partition_hi: i64 = row.get(0)?;
1162 let partition_lo: i64 = row.get(1)?;
1163 let variant_tag: i64 = row.get(2)?;
1164 let key: i64 = row.get(3)?;
1165 let sequence: i64 = row.get(4)?;
1166 let version_blob: Vec<u8> = row.get(5)?;
1167 let value: Option<Vec<u8>> = row.get(6)?;
1168 Ok(RawEntry {
1169 key: StoragePartitionedSeriesKey::from_sql_columns(
1170 PartitionedSeriesKeyColumns {
1171 partition_hi,
1172 partition_lo,
1173 variant_tag,
1174 key,
1175 sequence,
1176 },
1177 ),
1178 version: version_from_bytes(&version_blob),
1179 value: value.map(CowVec::new),
1180 })
1181 }) {
1182 Ok(rows) => rows.collect::<SqliteResult<Vec<_>>>().map_err(|e| {
1183 error!(internal(format!("Failed to read persistent row: {}", e)))
1184 })?,
1185 Err(e) if e.to_string().contains("no such table") => {
1186 cursor.finish_with(RangeStop::AbsentTable);
1187 return Ok(RangeBatch::empty());
1188 }
1189 Err(e) => {
1190 return Err(error!(internal(format!(
1191 "Failed to scan persistent range: {}",
1192 e
1193 ))));
1194 }
1195 };
1196
1197 record_page(raw.len() as u64, raw.iter().filter(|e| e.value.is_none()).count() as u64);
1198 let has_more = raw.len() == req.batch_size;
1199 if let Some(last) = raw.last() {
1200 cursor.advance(last.key);
1201 }
1202 if !has_more {
1203 cursor.finish_with(RangeStop::Scanned);
1204 }
1205 Ok(RangeBatch {
1206 entries: raw,
1207 has_more,
1208 })
1209 }
1210
1211 fn range_chunk(&self, cursor: &mut RangeCursor, req: RangeChunkRequest<'_>) -> Result<RangeBatch> {
1212 if cursor.is_exhausted() {
1213 return Ok(RangeBatch::empty());
1214 }
1215
1216 let storage = source_storage(req.table);
1217 let guard = self.inner.readers.acquire();
1218 let Some(conn) = guard.as_ref() else {
1219 return Err(error!(internal(
1220 "Persistent storage is shut down; refusing to report a range chunk exhausted \
1221 having read nothing, which hands the caller a short scan reported as a \
1222 complete one"
1223 .to_string()
1224 )));
1225 };
1226 let table_sql = self.table_sql(conn, req.table)?;
1227
1228 let version_bytes = version_to_bytes(req.scope.read()).to_vec();
1229 let limit_i64 = req.batch_size as i64;
1230
1231 let raw: Vec<RawEntry> = match table_sql.schema {
1232 SqliteSchema::Blob => {
1233 let sql = build_range_current_sql(
1234 &table_sql.table_name,
1235 bound_shape(req.start),
1236 bound_shape(req.end),
1237 cursor.last_key().is_some(),
1238 req.descending,
1239 );
1240 let mut stmt = match conn.prepare_cached(&sql) {
1241 Ok(s) => s,
1242 Err(e) if e.to_string().contains("no such table") => {
1243 cursor.finish_with(RangeStop::AbsentTable);
1244 return Ok(RangeBatch::empty());
1245 }
1246 Err(e) => {
1247 return Err(error!(internal(format!(
1248 "Failed to prepare persistent range: {}",
1249 e
1250 ))));
1251 }
1252 };
1253 let mut params: Vec<Box<dyn ToSql>> = Vec::new();
1254 match req.start {
1255 Bound::Included(s) | Bound::Excluded(s) => params.push(Box::new(s.to_vec())),
1256 Bound::Unbounded => {}
1257 }
1258 match req.end {
1259 Bound::Included(e) | Bound::Excluded(e) => params.push(Box::new(e.to_vec())),
1260 Bound::Unbounded => {}
1261 }
1262 if let Some(k) = cursor.last_key().map(|k| k.as_slice()) {
1263 params.push(Box::new(k.to_vec()));
1264 }
1265 params.push(Box::new(version_bytes.clone()));
1266 params.push(Box::new(limit_i64));
1267 match stmt.query_map(params_from_iter(params), |row| {
1268 let key: Vec<u8> = row.get(0)?;
1269 let version_blob: Vec<u8> = row.get(1)?;
1270 let value: Option<Vec<u8>> = row.get(2)?;
1271 Ok(RawEntry {
1272 key: EncodedKey::new(key),
1273 version: version_from_bytes(&version_blob),
1274 value: value.map(CowVec::new),
1275 })
1276 }) {
1277 Ok(rows) => rows.collect::<SqliteResult<Vec<_>>>().map_err(|e| {
1278 error!(internal(format!("Failed to read persistent row: {}", e)))
1279 })?,
1280 Err(e) if e.to_string().contains("no such table") => {
1281 cursor.finish_with(RangeStop::AbsentTable);
1282 return Ok(RangeBatch::empty());
1283 }
1284 Err(e) => {
1285 return Err(error!(internal(format!(
1286 "Failed to scan persistent range: {}",
1287 e
1288 ))));
1289 }
1290 }
1291 }
1292 SqliteSchema::Row => {
1293 let bounds = row_range_bounds(req.start, req.end);
1294 let last_row =
1295 cursor.last_key()
1296 .map(|k| {
1297 row_ident_of(k.as_slice())
1298 .map(|ident| row_to_sql(ident.row().0))
1299 .ok_or_else(|| {
1300 error!(internal("a range cursor does not decode as a RowKey".to_string()))
1301 })
1302 })
1303 .transpose()?;
1304 let sql = build_range_current_sql_row(
1305 &table_sql.table_name,
1306 bound_shape_of(&bounds.lower),
1307 bound_shape_of(&bounds.upper),
1308 last_row.is_some(),
1309 req.descending,
1310 );
1311 let mut stmt = match conn.prepare_cached(&sql) {
1312 Ok(s) => s,
1313 Err(e) if e.to_string().contains("no such table") => {
1314 cursor.finish_with(RangeStop::AbsentTable);
1315 return Ok(RangeBatch::empty());
1316 }
1317 Err(e) => {
1318 return Err(error!(internal(format!(
1319 "Failed to prepare persistent range: {}",
1320 e
1321 ))));
1322 }
1323 };
1324 let mut params: Vec<Box<dyn ToSql>> = Vec::new();
1325 if let Some(v) = bound_value(bounds.lower) {
1326 params.push(Box::new(v));
1327 }
1328 if let Some(v) = bound_value(bounds.upper) {
1329 params.push(Box::new(v));
1330 }
1331 if let Some(v) = last_row {
1332 params.push(Box::new(v));
1333 }
1334 params.push(Box::new(version_bytes.clone()));
1335 params.push(Box::new(limit_i64));
1336 let storage_id = storage.expect("row schema entry kinds always carry a storage id");
1337 let flat: Vec<&dyn ToSql> = params.iter().map(|p| p.as_ref()).collect();
1338 match stmt.query_map(params_from_iter(flat), |row| {
1339 let r: i64 = row.get(0)?;
1340 let version_blob: Vec<u8> = row.get(1)?;
1341 let value: Option<Vec<u8>> = row.get(2)?;
1342 Ok(RawEntry {
1343 key: row_key_for(storage_id, r),
1344 version: version_from_bytes(&version_blob),
1345 value: value.map(CowVec::new),
1346 })
1347 }) {
1348 Ok(rows) => rows.collect::<SqliteResult<Vec<_>>>().map_err(|e| {
1349 error!(internal(format!("Failed to read persistent row: {}", e)))
1350 })?,
1351 Err(e) if e.to_string().contains("no such table") => {
1352 cursor.finish_with(RangeStop::AbsentTable);
1353 return Ok(RangeBatch::empty());
1354 }
1355 Err(e) => {
1356 return Err(error!(internal(format!(
1357 "Failed to scan persistent range: {}",
1358 e
1359 ))));
1360 }
1361 }
1362 }
1363 SqliteSchema::Partitioned => {
1364 let bounds = partitioned_range_bounds(req.start, req.end);
1365 let storage_id =
1366 storage.expect("partitioned schema entry kinds always carry a storage id");
1367 match bounds {
1368 PartitionedRangeBounds::ExactPartition {
1369 partition_hi,
1370 partition_lo,
1371 lower_row,
1372 upper_row,
1373 } => {
1374 let last_row = cursor
1375 .last_key()
1376 .map(|k| {
1377 partitioned_ident_of(k.as_slice())
1378 .map(|ident| row_to_sql(ident.row().0))
1379 .ok_or_else(|| {
1380 error!(internal(
1381 "a range cursor does not decode as a \
1382 PartitionedRowKey"
1383 .to_string()
1384 ))
1385 })
1386 })
1387 .transpose()?;
1388 let sql = build_range_current_sql_partitioned_exact(
1389 &table_sql.table_name,
1390 bound_shape_of(&lower_row),
1391 bound_shape_of(&upper_row),
1392 last_row.is_some(),
1393 req.descending,
1394 );
1395 let mut stmt = match conn.prepare_cached(&sql) {
1396 Ok(s) => s,
1397 Err(e) if e.to_string().contains("no such table") => {
1398 cursor.finish_with(RangeStop::AbsentTable);
1399 return Ok(RangeBatch::empty());
1400 }
1401 Err(e) => {
1402 return Err(error!(internal(format!(
1403 "Failed to prepare persistent range: {}",
1404 e
1405 ))));
1406 }
1407 };
1408 let mut params: Vec<Box<dyn ToSql>> =
1409 vec![Box::new(partition_hi), Box::new(partition_lo)];
1410 if let Some(v) = bound_value(lower_row) {
1411 params.push(Box::new(v));
1412 }
1413 if let Some(v) = bound_value(upper_row) {
1414 params.push(Box::new(v));
1415 }
1416 if let Some(v) = last_row {
1417 params.push(Box::new(v));
1418 }
1419 params.push(Box::new(version_bytes.clone()));
1420 params.push(Box::new(limit_i64));
1421 let flat: Vec<&dyn ToSql> = params.iter().map(|p| p.as_ref()).collect();
1422 match stmt.query_map(params_from_iter(flat), |row| {
1423 let hi: i64 = row.get(0)?;
1424 let lo: i64 = row.get(1)?;
1425 let r: i64 = row.get(2)?;
1426 let version_blob: Vec<u8> = row.get(3)?;
1427 let value: Option<Vec<u8>> = row.get(4)?;
1428 Ok(RawEntry {
1429 key: partitioned_key_for(storage_id, hi, lo, r),
1430 version: version_from_bytes(&version_blob),
1431 value: value.map(CowVec::new),
1432 })
1433 }) {
1434 Ok(rows) => {
1435 rows.collect::<SqliteResult<Vec<_>>>().map_err(|e| {
1436 error!(internal(format!(
1437 "Failed to read persistent row: {}",
1438 e
1439 )))
1440 })?
1441 }
1442 Err(e) if e.to_string().contains("no such table") => {
1443 cursor.finish_with(RangeStop::AbsentTable);
1444 return Ok(RangeBatch::empty());
1445 }
1446 Err(e) => {
1447 return Err(error!(internal(format!(
1448 "Failed to scan persistent range: {}",
1449 e
1450 ))));
1451 }
1452 }
1453 }
1454 PartitionedRangeBounds::Open {
1455 lower,
1456 upper,
1457 } => {
1458 let last_triple =
1459 cursor.last_key()
1460 .map(|k| {
1461 partitioned_ident_of(k.as_slice())
1462 .map(|ident| {
1463 (
1464 partition_half_to_sql(ident.partition_hi()),
1465 partition_half_to_sql(ident.partition_lo()),
1466 row_to_sql(ident.row().0),
1467 )
1468 })
1469 .ok_or_else(|| {
1470 error!(internal(
1471 "a range cursor does not decode as a \
1472 PartitionedRowKey"
1473 .to_string()
1474 ))
1475 })
1476 })
1477 .transpose()?;
1478 let sql = build_range_current_sql_partitioned(
1479 &table_sql.table_name,
1480 bound_shape_of(&lower),
1481 bound_shape_of(&upper),
1482 last_triple.is_some(),
1483 req.descending,
1484 );
1485 let mut stmt = match conn.prepare_cached(&sql) {
1486 Ok(s) => s,
1487 Err(e) if e.to_string().contains("no such table") => {
1488 cursor.finish_with(RangeStop::AbsentTable);
1489 return Ok(RangeBatch::empty());
1490 }
1491 Err(e) => {
1492 return Err(error!(internal(format!(
1493 "Failed to prepare persistent range: {}",
1494 e
1495 ))));
1496 }
1497 };
1498 let mut params: Vec<Box<dyn ToSql>> = Vec::new();
1499 if let Some((hi, lo, r)) = bound_value(lower) {
1500 params.push(Box::new(hi));
1501 params.push(Box::new(lo));
1502 params.push(Box::new(r));
1503 }
1504 if let Some((hi, lo, r)) = bound_value(upper) {
1505 params.push(Box::new(hi));
1506 params.push(Box::new(lo));
1507 params.push(Box::new(r));
1508 }
1509 if let Some((hi, lo, r)) = last_triple {
1510 params.push(Box::new(hi));
1511 params.push(Box::new(lo));
1512 params.push(Box::new(r));
1513 }
1514 params.push(Box::new(version_bytes.clone()));
1515 params.push(Box::new(limit_i64));
1516 let flat: Vec<&dyn ToSql> = params.iter().map(|p| p.as_ref()).collect();
1517 match stmt.query_map(params_from_iter(flat), |row| {
1518 let hi: i64 = row.get(0)?;
1519 let lo: i64 = row.get(1)?;
1520 let r: i64 = row.get(2)?;
1521 let version_blob: Vec<u8> = row.get(3)?;
1522 let value: Option<Vec<u8>> = row.get(4)?;
1523 Ok(RawEntry {
1524 key: partitioned_key_for(storage_id, hi, lo, r),
1525 version: version_from_bytes(&version_blob),
1526 value: value.map(CowVec::new),
1527 })
1528 }) {
1529 Ok(rows) => {
1530 rows.collect::<SqliteResult<Vec<_>>>().map_err(|e| {
1531 error!(internal(format!(
1532 "Failed to read persistent row: {}",
1533 e
1534 )))
1535 })?
1536 }
1537 Err(e) if e.to_string().contains("no such table") => {
1538 cursor.finish_with(RangeStop::AbsentTable);
1539 return Ok(RangeBatch::empty());
1540 }
1541 Err(e) => {
1542 return Err(error!(internal(format!(
1543 "Failed to scan persistent range: {}",
1544 e
1545 ))));
1546 }
1547 }
1548 }
1549 }
1550 }
1551 SqliteSchema::Series | SqliteSchema::PartitionedSeries => {
1552 let storage_id = storage.expect("series schema entry kinds always carry a storage id");
1553 let widths = series_suffix_widths(table_sql.schema)
1554 .expect("only the series schemas reach a series range");
1555 let header = series_storage_header(table_sql.schema, storage_id)
1556 .expect("only the series schemas reach a series range");
1557 let bounds = series_range_bounds(header.as_slice(), widths, req.start, req.end)
1558 .ok_or_else(|| {
1559 error!(internal(
1560 "a range bound is not a key of the series table it was \
1561 routed to"
1562 .to_string()
1563 ))
1564 })?;
1565 let (lower, upper) = match bounds {
1566 SeriesRangeBounds::Empty => {
1567 cursor.finish_with(RangeStop::Scanned);
1568 return Ok(RangeBatch::empty());
1569 }
1570 SeriesRangeBounds::Range {
1571 lower,
1572 upper,
1573 } => (lower, upper),
1574 };
1575 let last_columns = cursor
1576 .last_key()
1577 .map(|k| {
1578 key_ints(table_sql.schema, k.as_slice()).ok_or_else(|| {
1579 error!(internal(
1580 "a range cursor does not decode as a key of its \
1581 own series table"
1582 .to_string()
1583 ))
1584 })
1585 })
1586 .transpose()?;
1587 let partitioned = table_sql.schema == SqliteSchema::PartitionedSeries;
1588 let sql = if partitioned {
1589 build_range_current_sql_partitioned_series(
1590 &table_sql.table_name,
1591 bound_shape_of(&lower),
1592 bound_shape_of(&upper),
1593 last_columns.is_some(),
1594 req.descending,
1595 )
1596 } else {
1597 build_range_current_sql_series(
1598 &table_sql.table_name,
1599 bound_shape_of(&lower),
1600 bound_shape_of(&upper),
1601 last_columns.is_some(),
1602 req.descending,
1603 )
1604 };
1605 let mut stmt = match conn.prepare_cached(&sql) {
1606 Ok(s) => s,
1607 Err(e) if e.to_string().contains("no such table") => {
1608 cursor.finish_with(RangeStop::AbsentTable);
1609 return Ok(RangeBatch::empty());
1610 }
1611 Err(e) => {
1612 return Err(error!(internal(format!(
1613 "Failed to prepare persistent range: {}",
1614 e
1615 ))));
1616 }
1617 };
1618 let mut params: Vec<Box<dyn ToSql>> = Vec::new();
1619 for columns in [bound_value_ref(&lower), bound_value_ref(&upper), last_columns.as_ref()]
1620 .into_iter()
1621 .flatten()
1622 {
1623 for column in columns {
1624 params.push(Box::new(*column));
1625 }
1626 }
1627 params.push(Box::new(version_bytes.clone()));
1628 params.push(Box::new(limit_i64));
1629 let key_columns = table_sql.schema.key_column_count();
1630 let flat: Vec<&dyn ToSql> = params.iter().map(|p| p.as_ref()).collect();
1631 match stmt.query_map(params_from_iter(flat), |row| {
1632 let mut columns = Vec::with_capacity(key_columns);
1633 for column in 0..key_columns {
1634 columns.push(row.get::<_, i64>(column)?);
1635 }
1636 let version_blob: Vec<u8> = row.get(key_columns)?;
1637 let value: Option<Vec<u8>> = row.get(key_columns + 1)?;
1638 let key = if partitioned {
1639 partitioned_series_key_for(
1640 storage_id, columns[0], columns[1], columns[2], columns[3],
1641 columns[4],
1642 )
1643 } else {
1644 series_key_for(storage_id, columns[0], columns[1], columns[2])
1645 };
1646 Ok(RawEntry {
1647 key,
1648 version: version_from_bytes(&version_blob),
1649 value: value.map(CowVec::new),
1650 })
1651 }) {
1652 Ok(rows) => rows.collect::<SqliteResult<Vec<_>>>().map_err(|e| {
1653 error!(internal(format!("Failed to read persistent row: {}", e)))
1654 })?,
1655 Err(e) if e.to_string().contains("no such table") => {
1656 cursor.finish_with(RangeStop::AbsentTable);
1657 return Ok(RangeBatch::empty());
1658 }
1659 Err(e) => {
1660 return Err(error!(internal(format!(
1661 "Failed to scan persistent range: {}",
1662 e
1663 ))));
1664 }
1665 }
1666 }
1667 };
1668 let page_was_full = raw.len() >= req.batch_size;
1669 let last_scanned = raw.last().map(|e| e.key.clone());
1670 record_page(raw.len() as u64, raw.iter().filter(|e| e.value.is_none()).count() as u64);
1671 let entries: Vec<RawEntry> = raw.into_iter().filter(|e| req.scope.contains(e.version)).collect();
1672
1673 if let Some(last) = last_scanned {
1674 cursor.advance(last);
1675 }
1676 if !page_was_full {
1677 cursor.finish_with(RangeStop::Scanned);
1678 }
1679
1680 let has_more = !cursor.is_exhausted();
1681 Ok(RangeBatch {
1682 entries,
1683 has_more,
1684 })
1685 }
1686}
1687
1688fn any_current_row(conn: &Connection) -> bool {
1689 let mut stmt = conn
1690 .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'")
1691 .expect("persistent table listing could not be prepared");
1692 let names: Vec<String> = stmt
1693 .query_map([], |row| row.get::<_, String>(0))
1694 .expect("persistent table listing failed")
1695 .collect::<SqliteResult<Vec<String>>>()
1696 .expect("persistent table name could not be read");
1697 drop(stmt);
1698
1699 for name in names {
1700 if current_table_name_to_entry(&name).is_none() {
1701 continue;
1702 }
1703 let exists: i64 = conn
1704 .query_row(&build_current_exists_sql(&name), [], |row| row.get(0))
1705 .expect("persistent existence probe failed");
1706 if exists != 0 {
1707 return true;
1708 }
1709 }
1710 false
1711}
1712
1713fn highest_current_version(conn: &Connection) -> u64 {
1714 let mut stmt = conn
1715 .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'")
1716 .expect("persistent table listing could not be prepared");
1717 let names: Vec<String> = stmt
1718 .query_map([], |row| row.get::<_, String>(0))
1719 .expect("persistent table listing failed")
1720 .collect::<SqliteResult<Vec<String>>>()
1721 .expect("persistent table name could not be read");
1722 drop(stmt);
1723
1724 let mut highest = 0u64;
1725 for name in names {
1726 if current_table_name_to_entry(&name).is_none() {
1727 continue;
1728 }
1729 let blob: Option<Vec<u8>> = conn
1730 .query_row(&build_max_version_sql(&name), [], |row| row.get(0))
1731 .expect("persistent high water probe failed");
1732 if let Some(blob) = blob {
1733 highest = highest.max(version_from_bytes(&blob).0);
1734 }
1735 }
1736 highest
1737}
1738
1739fn series_columns(schema: SqliteSchema) -> &'static [&'static str] {
1740 schema.series_key_columns().expect("only the series schemas reach a series query builder")
1741}
1742
1743fn read_table_columns(conn: &Connection, table_name: &str) -> Result<Vec<(String, String)>> {
1744 let mut stmt = conn
1745 .prepare(&format!("PRAGMA table_info(\"{}\")", table_name))
1746 .map_err(|e| error!(internal(format!("Failed to prepare a layout probe for {}: {}", table_name, e))))?;
1747 let rows = stmt
1748 .query_map([], |row| Ok((row.get::<_, String>(1)?, row.get::<_, String>(2)?)))
1749 .map_err(|e| error!(internal(format!("Failed to probe the layout of {}: {}", table_name, e))))?;
1750 rows.collect::<SqliteResult<Vec<_>>>()
1751 .map_err(|e| error!(internal(format!("Failed to read the layout of {}: {}", table_name, e))))
1752}
1753
1754fn resolve_schema(conn: &Connection, table: EntryKind) -> Result<SqliteSchema> {
1755 let Some(narrow) = narrow_series_schema(table) else {
1756 return Ok(sqlite_schema(table));
1757 };
1758 let table_name = current_table_name(table);
1759 let columns = read_table_columns(conn, &table_name)?;
1760 series_schema_from_columns(narrow, &columns).ok_or_else(|| {
1761 error!(internal(format!(
1762 "Persistent table {} carries neither the blob series layout nor the narrow one; \
1763 refusing to read it under a guessed schema, which would return zero rows silently",
1764 table_name
1765 )))
1766 })
1767}
1768
1769fn source_storage(table: EntryKind) -> Option<StorageId> {
1770 match table {
1771 EntryKind::Source(storage, _) | EntryKind::PartitionedSource(storage, _) => Some(storage),
1772 EntryKind::Multi => None,
1773 }
1774}
1775
1776fn push_key_params(schema: SqliteSchema, key: &[u8], boxed: &mut Vec<Box<dyn ToSql>>) -> Result<()> {
1777 match schema {
1778 SqliteSchema::Blob => {
1779 boxed.push(Box::new(key.to_vec()));
1780 }
1781 SqliteSchema::Row => {
1782 let ident = row_ident_of(key).ok_or_else(|| {
1783 error!(internal(
1784 "a row-schema table received a key that does not decode as a RowKey"
1785 .to_string()
1786 ))
1787 })?;
1788 boxed.push(Box::new(row_to_sql(ident.row().0)));
1789 }
1790 SqliteSchema::Partitioned => {
1791 let ident = partitioned_ident_of(key).ok_or_else(|| {
1792 error!(internal(
1793 "a partitioned-schema table received a key that does not decode as a \
1794 PartitionedRowKey"
1795 .to_string()
1796 ))
1797 })?;
1798 boxed.push(Box::new(partition_half_to_sql(ident.partition_hi())));
1799 boxed.push(Box::new(partition_half_to_sql(ident.partition_lo())));
1800 boxed.push(Box::new(row_to_sql(ident.row().0)));
1801 }
1802 SqliteSchema::Series => {
1803 let ident = series_ident_of(key).ok_or_else(|| {
1804 error!(internal(
1805 "a series-schema table received a key that does not decode as a \
1806 SeriesRowKey"
1807 .to_string()
1808 ))
1809 })?;
1810 let columns = ident.to_sql_columns();
1811 boxed.push(Box::new(columns.variant_tag));
1812 boxed.push(Box::new(columns.key));
1813 boxed.push(Box::new(columns.sequence));
1814 }
1815 SqliteSchema::PartitionedSeries => {
1816 let ident = partitioned_series_ident_of(key).ok_or_else(|| {
1817 error!(internal(
1818 "a partitioned-series-schema table received a key that does not decode \
1819 as a PartitionedSeriesRowKey"
1820 .to_string()
1821 ))
1822 })?;
1823 let columns = ident.to_sql_columns();
1824 boxed.push(Box::new(columns.partition_hi));
1825 boxed.push(Box::new(columns.partition_lo));
1826 boxed.push(Box::new(columns.variant_tag));
1827 boxed.push(Box::new(columns.key));
1828 boxed.push(Box::new(columns.sequence));
1829 }
1830 }
1831 Ok(())
1832}
1833
1834enum ReturnedKey {
1835 Blob(Vec<u8>),
1836 Row(i64),
1837 Partitioned(i64, i64, i64),
1838 Series(i64, i64, i64),
1839 PartitionedSeries(i64, i64, i64, i64, i64),
1840}
1841
1842impl ReturnedKey {
1843 fn into_encoded_key(self, storage: Option<StorageId>) -> EncodedKey {
1844 match self {
1845 ReturnedKey::Blob(bytes) => EncodedKey::new(bytes),
1846 ReturnedKey::Row(row) => row_key_for(
1847 storage.expect("a row-schema table's entry kind always carries a storage id"),
1848 row,
1849 ),
1850 ReturnedKey::Partitioned(hi, lo, row) => partitioned_key_for(
1851 storage.expect("a partitioned-schema table's entry kind always carries a storage id"),
1852 hi,
1853 lo,
1854 row,
1855 ),
1856 ReturnedKey::Series(variant_tag, key, sequence) => series_key_for(
1857 storage.expect("a series-schema table's entry kind always carries a storage id"),
1858 variant_tag,
1859 key,
1860 sequence,
1861 ),
1862 ReturnedKey::PartitionedSeries(hi, lo, variant_tag, key, sequence) => {
1863 partitioned_series_key_for(
1864 storage.expect(
1865 "a partitioned-series-schema table's entry kind always carries a \
1866 storage id",
1867 ),
1868 hi,
1869 lo,
1870 variant_tag,
1871 key,
1872 sequence,
1873 )
1874 }
1875 }
1876 }
1877}
1878
1879fn key_ints(schema: SqliteSchema, key: &[u8]) -> Option<Vec<i64>> {
1880 match schema {
1881 SqliteSchema::Blob => None,
1882 SqliteSchema::Row => row_ident_of(key).map(|ident| vec![row_to_sql(ident.row().0)]),
1883 SqliteSchema::Partitioned => partitioned_ident_of(key).map(|ident| {
1884 vec![
1885 partition_half_to_sql(ident.partition_hi()),
1886 partition_half_to_sql(ident.partition_lo()),
1887 row_to_sql(ident.row().0),
1888 ]
1889 }),
1890 SqliteSchema::Series => series_ident_of(key).map(|ident| {
1891 let columns = ident.to_sql_columns();
1892 vec![columns.variant_tag, columns.key, columns.sequence]
1893 }),
1894 SqliteSchema::PartitionedSeries => partitioned_series_ident_of(key).map(|ident| {
1895 let columns = ident.to_sql_columns();
1896 vec![
1897 columns.partition_hi,
1898 columns.partition_lo,
1899 columns.variant_tag,
1900 columns.key,
1901 columns.sequence,
1902 ]
1903 }),
1904 }
1905}
1906
1907fn read_returned_key(schema: SqliteSchema, row: &Row) -> SqliteResult<ReturnedKey> {
1908 match schema {
1909 SqliteSchema::Blob => Ok(ReturnedKey::Blob(row.get::<_, Vec<u8>>(0)?)),
1910 SqliteSchema::Row => Ok(ReturnedKey::Row(row.get::<_, i64>(0)?)),
1911 SqliteSchema::Partitioned => Ok(ReturnedKey::Partitioned(row.get(0)?, row.get(1)?, row.get(2)?)),
1912 SqliteSchema::Series => Ok(ReturnedKey::Series(row.get(0)?, row.get(1)?, row.get(2)?)),
1913 SqliteSchema::PartitionedSeries => Ok(ReturnedKey::PartitionedSeries(
1914 row.get(0)?,
1915 row.get(1)?,
1916 row.get(2)?,
1917 row.get(3)?,
1918 row.get(4)?,
1919 )),
1920 }
1921}
1922
1923fn expiry_stamp(table: EntryKind, value: Option<&CowVec<u8>>) -> Option<DateTime> {
1924 match (table, value) {
1925 (EntryKind::Source(_, _) | EntryKind::PartitionedSource(_, _), Some(row))
1926 if row.len() >= SHAPE_HEADER_SIZE =>
1927 {
1928 Some(read_updated_at(row))
1929 }
1930 _ => None,
1931 }
1932}
1933
1934fn bound_shape(b: Bound<&[u8]>) -> Bound<()> {
1935 match b {
1936 Bound::Included(_) => Bound::Included(()),
1937 Bound::Excluded(_) => Bound::Excluded(()),
1938 Bound::Unbounded => Bound::Unbounded,
1939 }
1940}
1941
1942fn bound_shape_of<T>(b: &Bound<T>) -> Bound<()> {
1943 match b {
1944 Bound::Included(_) => Bound::Included(()),
1945 Bound::Excluded(_) => Bound::Excluded(()),
1946 Bound::Unbounded => Bound::Unbounded,
1947 }
1948}
1949
1950fn partitioned_triple(key: &StoragePartitionedRowKey) -> (i64, i64, i64) {
1951 (partition_half_to_sql(key.partition_hi()), partition_half_to_sql(key.partition_lo()), row_to_sql(key.row().0))
1952}
1953
1954fn series_triple(key: &StorageSeriesKey) -> (i64, i64, i64) {
1955 let columns = key.to_sql_columns();
1956 (columns.variant_tag, columns.key, columns.sequence)
1957}
1958
1959fn partitioned_series_columns(key: &StoragePartitionedSeriesKey) -> (i64, i64, i64, i64, i64) {
1960 let columns = key.to_sql_columns();
1961 (columns.partition_hi, columns.partition_lo, columns.variant_tag, columns.key, columns.sequence)
1962}
1963
1964fn bound_value_ref<T>(b: &Bound<T>) -> Option<&T> {
1965 match b {
1966 Bound::Included(v) | Bound::Excluded(v) => Some(v),
1967 Bound::Unbounded => None,
1968 }
1969}
1970
1971fn bound_value<T: Copy>(b: Bound<T>) -> Option<T> {
1972 match b {
1973 Bound::Included(v) | Bound::Excluded(v) => Some(v),
1974 Bound::Unbounded => None,
1975 }
1976}
1977
1978struct RangeChunkRequest<'a> {
1979 table: EntryKind,
1980 start: Bound<&'a [u8]>,
1981 end: Bound<&'a [u8]>,
1982 scope: MultiVersionScope,
1983 batch_size: usize,
1984 descending: bool,
1985}
1986
1987impl SqlitePersistentStorage {
1988 #[instrument(name = "store::multi::persistent::sqlite::get::source", level = "trace", skip(self), fields(key_len = key.len(), version = version.0))]
1989 fn get_source(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<VersionedGetResult> {
1990 self.get_impl(table, key, version)
1991 }
1992
1993 #[instrument(name = "store::multi::persistent::sqlite::get::multi", level = "trace", skip(self), fields(key_len = key.len(), version = version.0))]
1994 fn get_multi(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<VersionedGetResult> {
1995 self.get_impl(table, key, version)
1996 }
1997
1998 fn get_impl(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<VersionedGetResult> {
1999 let guard = self.inner.readers.acquire();
2000 let Some(conn) = guard.as_ref() else {
2001 return Ok(VersionedGetResult::NotFound);
2002 };
2003 let table_sql = self.table_sql(conn, table)?;
2004
2005 let mut boxed: Vec<Box<dyn ToSql>> = Vec::with_capacity(table_sql.schema.key_column_count());
2006 if push_key_params(table_sql.schema, key, &mut boxed).is_err() {
2007 return Ok(VersionedGetResult::NotFound);
2008 }
2009 let flat: Vec<&dyn ToSql> = boxed.iter().map(|p| p.as_ref()).collect();
2010
2011 let result = match conn.prepare_cached(&table_sql.get_sql) {
2012 Ok(mut stmt) => stmt.query_row(params_from_iter(flat), |row| {
2013 let version_bytes: Vec<u8> = row.get(0)?;
2014 let value: Option<Vec<u8>> = row.get(1)?;
2015 Ok((version_from_bytes(&version_bytes), value))
2016 }),
2017 Err(e) if e.to_string().contains("no such table") => Err(QueryReturnedNoRows),
2018 Err(e) => return Err(error!(internal(format!("Failed to prepare persistent get: {}", e)))),
2019 };
2020
2021 match result {
2022 Ok((stored_version, value)) if stored_version <= version => Ok(match value {
2023 Some(v) => VersionedGetResult::Value {
2024 value: CowVec::new(v),
2025 version: stored_version,
2026 },
2027 None => VersionedGetResult::Tombstone,
2028 }),
2029 Ok(_) => Ok(VersionedGetResult::NotFound),
2030 Err(QueryReturnedNoRows) => Ok(VersionedGetResult::NotFound),
2031 Err(e) if e.to_string().contains("no such table") => Ok(VersionedGetResult::NotFound),
2032 Err(e) => Err(error!(internal(format!("Failed to read persistent: {}", e)))),
2033 }
2034 }
2035
2036 #[instrument(name = "store::multi::persistent::sqlite::get_many::source", level = "trace", skip(self, keys), fields(key_count = keys.len(), version = version.0))]
2037 fn get_many_source(
2038 &self,
2039 table: EntryKind,
2040 keys: &[&[u8]],
2041 version: CommitVersion,
2042 ) -> Result<Vec<VersionedGetResult>> {
2043 self.get_many_impl(table, keys, version)
2044 }
2045
2046 #[instrument(name = "store::multi::persistent::sqlite::get_many::multi", level = "trace", skip(self, keys), fields(key_count = keys.len(), version = version.0))]
2047 fn get_many_multi(
2048 &self,
2049 table: EntryKind,
2050 keys: &[&[u8]],
2051 version: CommitVersion,
2052 ) -> Result<Vec<VersionedGetResult>> {
2053 self.get_many_impl(table, keys, version)
2054 }
2055
2056 fn get_many_impl(
2057 &self,
2058 table: EntryKind,
2059 keys: &[&[u8]],
2060 version: CommitVersion,
2061 ) -> Result<Vec<VersionedGetResult>> {
2062 let mut out = vec![VersionedGetResult::NotFound; keys.len()];
2063 if keys.is_empty() {
2064 return Ok(out);
2065 }
2066
2067 let guard = self.inner.readers.acquire();
2068 let Some(conn) = guard.as_ref() else {
2069 return Ok(out);
2070 };
2071 let table_sql = self.table_sql(conn, table)?;
2072 if table_sql.schema == SqliteSchema::Blob {
2073 drop(guard);
2074 return self.get_many_blob(&table_sql, keys, version, &mut out).map(|()| out);
2075 }
2076
2077 let key_columns = table_sql.schema.key_column_count();
2078 let mut index: HashMap<Vec<i64>, usize> = HashMap::with_capacity(keys.len());
2079 let mut per_key_params: Vec<Vec<i64>> = Vec::with_capacity(keys.len());
2080 for (i, &k) in keys.iter().enumerate() {
2081 let params = key_ints(table_sql.schema, k).ok_or_else(|| {
2082 error!(internal(
2083 "a get_many key does not decode under its own table's narrow schema"
2084 .to_string()
2085 ))
2086 })?;
2087 debug_assert_eq!(params.len(), key_columns);
2088 index.insert(params.clone(), i);
2089 per_key_params.push(params);
2090 }
2091
2092 for chunk in per_key_params.chunks(GET_MANY_CHUNK) {
2093 let bucket = bucket_key_count(chunk.len());
2094 let sql = match table_sql.schema {
2095 SqliteSchema::Row => build_get_many_current_sql_row(&table_sql.table_name, bucket),
2096 SqliteSchema::Partitioned => {
2097 build_get_many_current_sql_partitioned(&table_sql.table_name, bucket)
2098 }
2099 SqliteSchema::Series | SqliteSchema::PartitionedSeries => {
2100 build_get_many_current_sql_keyed(
2101 &table_sql.table_name,
2102 series_columns(table_sql.schema),
2103 bucket,
2104 )
2105 }
2106 SqliteSchema::Blob => unreachable!("blob schema handled by get_many_blob"),
2107 };
2108 let mut stmt = match conn.prepare_cached(&sql) {
2109 Ok(stmt) => stmt,
2110 Err(e) if e.to_string().contains("no such table") => return Ok(out),
2111 Err(e) => {
2112 return Err(error!(internal(format!(
2113 "Failed to prepare persistent get_many: {}",
2114 e
2115 ))));
2116 }
2117 };
2118
2119 let pad = chunk[0].clone();
2120 let mut padded: Vec<i64> = Vec::with_capacity(bucket * key_columns);
2121 for p in chunk {
2122 padded.extend_from_slice(p);
2123 }
2124 for _ in chunk.len()..bucket {
2125 padded.extend_from_slice(&pad);
2126 }
2127 let mut rows = stmt
2128 .query(params_from_iter(padded))
2129 .map_err(|e| error!(internal(format!("Failed to query persistent get_many: {}", e))))?;
2130
2131 while let Some(row) = rows.next().map_err(|e| {
2132 error!(internal(format!("Failed to read persistent get_many row: {}", e)))
2133 })? {
2134 let key_params: Vec<i64> = (0..key_columns)
2135 .map(|c| {
2136 row.get::<_, i64>(c).map_err(|e| {
2137 error!(internal(format!(
2138 "Failed to read persistent get_many key: {}",
2139 e
2140 )))
2141 })
2142 })
2143 .collect::<Result<Vec<_>>>()?;
2144 let Some(&i) = index.get(&key_params) else {
2145 continue;
2146 };
2147 let version_bytes: Vec<u8> = row.get(key_columns).map_err(|e| {
2148 error!(internal(format!("Failed to read persistent get_many version: {}", e)))
2149 })?;
2150 let stored_version = version_from_bytes(&version_bytes);
2151 if stored_version > version {
2152 continue;
2153 }
2154 let value: Option<Vec<u8>> = row.get(key_columns + 1).map_err(|e| {
2155 error!(internal(format!("Failed to read persistent get_many value: {}", e)))
2156 })?;
2157 out[i] = match value {
2158 Some(v) => VersionedGetResult::Value {
2159 value: CowVec::new(v),
2160 version: stored_version,
2161 },
2162 None => VersionedGetResult::Tombstone,
2163 };
2164 }
2165 }
2166
2167 Ok(out)
2168 }
2169
2170 fn get_many_blob(
2171 &self,
2172 table_sql: &TableSql,
2173 keys: &[&[u8]],
2174 version: CommitVersion,
2175 out: &mut [VersionedGetResult],
2176 ) -> Result<()> {
2177 let index: HashMap<&[u8], usize> = keys.iter().enumerate().map(|(i, &k)| (k, i)).collect();
2178 let guard = self.inner.readers.acquire();
2179 let Some(conn) = guard.as_ref() else {
2180 return Ok(());
2181 };
2182
2183 for chunk in keys.chunks(GET_MANY_CHUNK) {
2184 let bucket = bucket_key_count(chunk.len());
2185 let sql = build_get_many_current_sql(&table_sql.table_name, bucket);
2186 let mut stmt = match conn.prepare_cached(&sql) {
2187 Ok(stmt) => stmt,
2188 Err(e) if e.to_string().contains("no such table") => return Ok(()),
2189 Err(e) => {
2190 return Err(error!(internal(format!(
2191 "Failed to prepare persistent get_many: {}",
2192 e
2193 ))));
2194 }
2195 };
2196
2197 let pad_key = chunk[0];
2198 let padded = chunk.iter().copied().chain(repeat_n(pad_key, bucket - chunk.len()));
2199 let mut rows = stmt
2200 .query(params_from_iter(padded))
2201 .map_err(|e| error!(internal(format!("Failed to query persistent get_many: {}", e))))?;
2202
2203 while let Some(row) = rows.next().map_err(|e| {
2204 error!(internal(format!("Failed to read persistent get_many row: {}", e)))
2205 })? {
2206 let key_ref = row.get_ref(0).map_err(|e| {
2207 error!(internal(format!("Failed to read persistent get_many key: {}", e)))
2208 })?;
2209 let key = key_ref.as_blob().map_err(|e| {
2210 error!(internal(format!("Failed to decode persistent get_many key: {}", e)))
2211 })?;
2212 let Some(&i) = index.get(key) else {
2213 continue;
2214 };
2215 let version_ref = row.get_ref(1).map_err(|e| {
2216 error!(internal(format!("Failed to read persistent get_many version: {}", e)))
2217 })?;
2218 let version_bytes = version_ref.as_blob().map_err(|e| {
2219 error!(internal(format!("Failed to decode persistent get_many version: {}", e)))
2220 })?;
2221 let stored_version = version_from_bytes(version_bytes);
2222 if stored_version > version {
2223 continue;
2224 }
2225 let value: Option<Vec<u8>> = row.get(2).map_err(|e| {
2226 error!(internal(format!("Failed to read persistent get_many value: {}", e)))
2227 })?;
2228 out[i] = match value {
2229 Some(v) => VersionedGetResult::Value {
2230 value: CowVec::new(v),
2231 version: stored_version,
2232 },
2233 None => VersionedGetResult::Tombstone,
2234 };
2235 }
2236 }
2237
2238 Ok(())
2239 }
2240}
2241
2242impl TierStorage for SqlitePersistentStorage {
2243 fn get(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<VersionedGetResult> {
2244 match table {
2245 EntryKind::Source(_, _) => self.get_source(table, key, version),
2246 _ => self.get_multi(table, key, version),
2247 }
2248 }
2249
2250 fn get_many(
2251 &self,
2252 table: EntryKind,
2253 keys: &[&[u8]],
2254 version: CommitVersion,
2255 ) -> Result<Vec<VersionedGetResult>> {
2256 match table {
2257 EntryKind::Source(_, _) => self.get_many_source(table, keys, version),
2258 _ => self.get_many_multi(table, keys, version),
2259 }
2260 }
2261
2262 fn set(&self, version: CommitVersion, batches: TierBatch) -> Result<()> {
2263 self.set_collecting_accepted(version, batches)?;
2264 Ok(())
2265 }
2266
2267 #[instrument(name = "store::multi::persistent::sqlite::range", level = "trace", skip(self, cursor, start, end), fields(table = ?table, batch_size = batch_size))]
2268 fn range_next(
2269 &self,
2270 table: EntryKind,
2271 cursor: &mut RangeCursor,
2272 start: Bound<&[u8]>,
2273 end: Bound<&[u8]>,
2274 scope: MultiVersionScope,
2275 batch_size: usize,
2276 ) -> Result<RangeBatch> {
2277 self.range_chunk(
2278 cursor,
2279 RangeChunkRequest {
2280 table,
2281 start,
2282 end,
2283 scope,
2284 batch_size,
2285 descending: false,
2286 },
2287 )
2288 }
2289
2290 #[instrument(name = "store::multi::persistent::sqlite::range_rev", level = "trace", skip(self, cursor, start, end), fields(table = ?table, batch_size = batch_size))]
2291 fn range_rev_next(
2292 &self,
2293 table: EntryKind,
2294 cursor: &mut RangeCursor,
2295 start: Bound<&[u8]>,
2296 end: Bound<&[u8]>,
2297 scope: MultiVersionScope,
2298 batch_size: usize,
2299 ) -> Result<RangeBatch> {
2300 self.range_chunk(
2301 cursor,
2302 RangeChunkRequest {
2303 table,
2304 start,
2305 end,
2306 scope,
2307 batch_size,
2308 descending: true,
2309 },
2310 )
2311 }
2312
2313 fn ensure_table(&self, table: EntryKind) -> Result<()> {
2314 let guard = self.inner.conn.lock();
2315 let Some(conn) = guard.as_ref() else {
2316 return Ok(());
2317 };
2318 let table_sql = self.table_sql(conn, table)?;
2319 Self::create_table_if_needed(conn, &table_sql.create_sql)
2320 .map_err(|e| error!(internal(format!("Failed to ensure persistent table: {}", e))))
2321 }
2322
2323 fn clear_table(&self, table: EntryKind) -> Result<()> {
2324 let guard = self.inner.conn.lock();
2325 let Some(conn) = guard.as_ref() else {
2326 return Ok(());
2327 };
2328 let table_sql = self.table_sql(conn, table)?;
2329 let result = conn.execute(&format!("DELETE FROM \"{}\"", table_sql.table_name), []);
2330 if let Err(e) = result
2331 && !e.to_string().contains("no such table")
2332 {
2333 return Err(error!(internal(format!(
2334 "Failed to clear persistent {}: {}",
2335 table_sql.table_name, e
2336 ))));
2337 }
2338 Ok(())
2339 }
2340}
2341
2342impl Shutdown for SqlitePersistentStorage {
2343 fn shutdown(&self) {
2344 if let Some(conn) = self.inner.conn.lock().take() {
2345 if let Err(e) = pragma::shutdown(&conn) {
2346 warn!(error = %e, "persistent close: pragma shutdown failed");
2347 }
2348 drop(conn);
2349 }
2350 self.inner.readers.shutdown();
2351 }
2352}
2353
2354#[cfg(test)]
2355mod tests {
2356 use std::collections::HashMap;
2357
2358 use reifydb_codec::key::encoded::EncodedKeyRange;
2359 use reifydb_core::{
2360 interface::{
2361 catalog::{id::TableId, storage::StorageId},
2362 store::EntryLayout,
2363 },
2364 key::{
2365 any::TaggedKey,
2366 row::{PartitionedRowKey, RowKey, RowKeyRange},
2367 series::{
2368 PartitionedSeriesRowKey, PartitionedSeriesRowKeyRange, SeriesRowKey, SeriesRowKeyRange,
2369 },
2370 },
2371 };
2372 use reifydb_value::value::{partition::Partition, row_number::RowNumber};
2373
2374 use super::*;
2375 use crate::tier::persistent::sqlite::schema::row_from_sql;
2376
2377 fn table() -> EntryKind {
2379 EntryKind::Source(StorageId::Table(TableId(1)), EntryLayout::Row)
2380 }
2381
2382 fn key(n: u64) -> EncodedKey {
2383 RowKey::encoded(StorageId::Table(TableId(1)), RowNumber(n))
2384 }
2385
2386 fn row_cursor(n: u64) -> TaggedKey {
2387 TaggedKey::from(RowKey::new(StorageId::Table(TableId(1)), RowNumber(n)))
2388 }
2389
2390 fn row(payload: &[u8]) -> CowVec<u8> {
2391 CowVec::new(payload.to_vec())
2392 }
2393
2394 fn stamped(nanos: u64) -> CowVec<u8> {
2395 let mut bytes = vec![0u8; SHAPE_HEADER_SIZE];
2397 bytes[16..24].copy_from_slice(&nanos.to_le_bytes());
2398 CowVec::new(bytes)
2399 }
2400
2401 fn at(nanos: u64) -> DateTime {
2402 DateTime::from_nanos(nanos)
2403 }
2404
2405 fn expired_at(s: &SqlitePersistentStorage, kind: EntryKind, cutoff: u64) -> Vec<u64> {
2406 s.expired_keys(kind, at(cutoff), None, 100)
2407 .unwrap()
2408 .into_iter()
2409 .map(|(key, _)| RowKey::decode(&key).unwrap().row.0)
2410 .collect()
2411 }
2412
2413 fn partitioned_expired_at(s: &SqlitePersistentStorage, kind: EntryKind, cutoff: u64) -> Vec<u64> {
2414 s.expired_keys(kind, at(cutoff), None, 100)
2415 .unwrap()
2416 .into_iter()
2417 .map(|(key, _)| PartitionedRowKey::decode(&key).unwrap().row.0)
2418 .collect()
2419 }
2420
2421 fn reap(s: &SqlitePersistentStorage, keys: &[u64]) -> u64 {
2422 let keys: Vec<EncodedKey> = keys.iter().map(|n| key(*n)).collect();
2423 s.delete_keys(table(), &keys).unwrap()
2424 }
2425
2426 fn stored_keys(s: &SqlitePersistentStorage) -> Vec<u64> {
2427 let guard = s.inner.conn.lock();
2430 let conn = guard.as_ref().expect("write connection is present");
2431 let table_name = s.table_sql(conn, table()).unwrap().table_name.clone();
2432 let mut stmt = conn.prepare(&format!("SELECT key FROM \"{}\" ORDER BY key", table_name)).unwrap();
2433 let keys: Vec<u64> = stmt
2434 .query_map([], |row| row.get::<_, i64>(0))
2435 .unwrap()
2436 .map(|key| row_from_sql(key.unwrap()))
2437 .collect();
2438 keys
2439 }
2440
2441 fn visible(s: &SqlitePersistentStorage, k: &EncodedKey) -> bool {
2442 s.get(table(), k.as_slice(), CommitVersion(u64::MAX)).unwrap().value().is_some()
2443 }
2444
2445 #[test]
2446 fn a_range_does_not_fetch_deleted_rows_because_the_delete_removed_them() {
2447 let (s, _guard) = SqlitePersistentStorage::in_memory();
2452 let mut writes = Vec::new();
2453 for i in 1..=50u64 {
2454 writes.push((key(i), Some(row(b"doomed"))));
2455 }
2456 s.set(CommitVersion(1), HashMap::from([(table(), writes)])).unwrap();
2457 let mut deletes = Vec::new();
2458 for i in 1..=50u64 {
2459 deletes.push((key(i), None));
2460 }
2461 s.set(CommitVersion(2), HashMap::from([(table(), deletes)])).unwrap();
2462 s.set(CommitVersion(3), HashMap::from([(table(), vec![(key(200), Some(row(b"alive")))])])).unwrap();
2463 assert_eq!(
2464 s.count_current(table()).unwrap(),
2465 1,
2466 "the 50 removals must delete their rows outright; rewriting them to a none value \
2467 leaves 50 dead rows for every later scan to page through and discard"
2468 );
2469
2470 let before = ScanCounters::sample();
2471 let mut cursor = RangeCursor::default();
2472 let batch = s
2473 .range_next(
2474 table(),
2475 &mut cursor,
2476 Bound::Unbounded,
2477 Bound::Unbounded,
2478 MultiVersionScope::AsOf {
2479 read: CommitVersion(10),
2480 },
2481 1024,
2482 )
2483 .unwrap();
2484 let scanned = before.since();
2485
2486 assert_eq!(
2487 batch.entries.iter().map(|e| e.key.clone()).collect::<Vec<_>>(),
2488 vec![key(200)],
2489 "a deleted key must not surface, and the one live row must"
2490 );
2491 assert_eq!(scanned.fetched, 1, "the 50 deleted rows must never cross into Rust");
2492 assert_eq!(scanned.tombstones, 0);
2493 }
2494
2495 #[test]
2496 fn a_page_the_scope_filter_empties_does_not_end_the_scan() {
2497 let (s, _guard) = SqlitePersistentStorage::in_memory();
2504 s.set(
2505 CommitVersion(1),
2506 HashMap::from([(table(), vec![(key(1), Some(row(b"a"))), (key(2), Some(row(b"b")))])]),
2507 )
2508 .unwrap();
2509 s.set(
2510 CommitVersion(5),
2511 HashMap::from([(table(), vec![(key(3), Some(row(b"c"))), (key(4), Some(row(b"d")))])]),
2512 )
2513 .unwrap();
2514
2515 let scope = MultiVersionScope::Between {
2516 after: CommitVersion(1),
2517 read: CommitVersion(10),
2518 };
2519 let mut cursor = RangeCursor::default();
2520 let mut seen: Vec<EncodedKey> = Vec::new();
2521 loop {
2522 let batch = s
2523 .range_next(table(), &mut cursor, Bound::Unbounded, Bound::Unbounded, scope, 2)
2524 .unwrap();
2525 seen.extend(batch.entries.iter().map(|e| e.key.clone()));
2526 if !batch.has_more {
2527 break;
2528 }
2529 }
2530
2531 assert_eq!(
2532 seen,
2533 vec![key(4), key(3)],
2534 "rows newer than `after` must survive a page that filtered out entirely"
2535 );
2536 }
2537
2538 #[test]
2539 fn page_cache_metrics_accumulates_hits_and_misses_across_sweeps() {
2540 let (s, _guard) = SqlitePersistentStorage::in_memory();
2543 s.set(CommitVersion(1), HashMap::from([(table(), vec![(key(1), Some(row(b"a")))])])).unwrap();
2544 assert!(visible(&s, &key(1)));
2545
2546 let first = s.page_cache_metrics();
2547 assert_eq!(
2548 first.connections_sampled, first.connections_total,
2549 "an idle pool must have every connection sampled"
2550 );
2551 assert!(
2552 first.hits.as_u64() + first.misses.as_u64() > 0,
2553 "writing and reading a row must touch the page cache, got {first:?}"
2554 );
2555 assert!(first.used.as_bytes() > 0, "connections holding pages must report used bytes");
2556
2557 assert!(visible(&s, &key(1)));
2558 let second = s.page_cache_metrics();
2559 assert!(
2560 second.hits.as_u64() >= first.hits.as_u64(),
2561 "hit totals must accumulate, got {} then {}",
2562 first.hits.as_u64(),
2563 second.hits.as_u64()
2564 );
2565 assert!(
2566 second.misses.as_u64() >= first.misses.as_u64(),
2567 "miss totals must accumulate, got {} then {}",
2568 first.misses.as_u64(),
2569 second.misses.as_u64()
2570 );
2571 }
2572 #[test]
2573 fn create_table_leaves_the_version_column_unindexed() {
2574 let (s, _guard) = SqlitePersistentStorage::in_memory();
2577 s.set(CommitVersion(1), HashMap::from([(table(), vec![(key(1), Some(row(b"a")))])])).unwrap();
2578
2579 let guard = s.inner.conn.lock();
2580 let conn = guard.as_ref().expect("write connection is present");
2581 let table_name = s.table_sql(conn, table()).unwrap().table_name.clone();
2582
2583 let indices: Vec<String> = conn
2584 .prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = ?1")
2585 .unwrap()
2586 .query_map([table_name.as_str()], |r| r.get::<_, String>(0))
2587 .unwrap()
2588 .map(|r| r.unwrap())
2589 .collect();
2590
2591 assert!(
2592 !indices.contains(&format!("{table_name}__version")),
2593 "the bare version index must not be created; it is written on every row and read by no query plan, got {indices:?}"
2594 );
2595 assert!(
2596 !indices.contains(&format!("{table_name}__tombstone")),
2597 "the tombstone index must not be created; a removal deletes its row outright, so nothing \
2598 writes a valueless row for it to index, got {indices:?}"
2599 );
2600 assert!(
2601 !indices.iter().any(|n| n.ends_with("__created_nanos") || n.ends_with("__updated_nanos")),
2602 "the dropped timestamp indices must not be recreated, got {indices:?}"
2603 );
2604 }
2605 #[test]
2606 fn a_key_written_again_after_a_removal_comes_back() {
2607 let (s, _guard) = SqlitePersistentStorage::in_memory();
2609 s.set(CommitVersion(2), HashMap::from([(table(), vec![(key(1), None)])])).unwrap();
2610
2611 s.set(
2612 CommitVersion(3),
2613 HashMap::from([(table(), vec![(key(1), Some(row(b"back"))), (key(2), Some(row(b"new")))])]),
2614 )
2615 .unwrap();
2616
2617 assert!(visible(&s, &key(1)), "a fresh write after a removal must land");
2618 assert!(visible(&s, &key(2)), "an unrelated key in the same batch must land too");
2619 }
2620
2621 #[test]
2622 fn a_chunked_upsert_batch_reports_exactly_the_keys_that_won_their_cas() {
2623 let (s, _guard) = SqlitePersistentStorage::in_memory();
2625
2626 let evens: Vec<_> = (0..170u64).step_by(2).map(|i| (key(i), Some(row(b"seed-even")))).collect();
2627 let odds: Vec<_> = (1..170u64).step_by(2).map(|i| (key(i), Some(row(b"seed-odd")))).collect();
2628 s.set_collecting_accepted(CommitVersion(200), HashMap::from([(table(), evens)])).unwrap();
2629 s.set_collecting_accepted(CommitVersion(50), HashMap::from([(table(), odds)])).unwrap();
2630
2631 let attempt: Vec<_> = (0..170u64).map(|i| (key(i), Some(row(b"attempt")))).collect();
2632 let accepted =
2633 s.set_collecting_accepted(CommitVersion(150), HashMap::from([(table(), attempt)])).unwrap();
2634
2635 let mut accepted_ids: Vec<u64> = accepted.iter().map(|k| RowKey::decode(k).unwrap().row.0).collect();
2636 accepted_ids.sort();
2637 let expected_odds: Vec<u64> = (1..170u64).step_by(2).collect();
2638 assert_eq!(
2639 accepted_ids, expected_odds,
2640 "only the keys whose stored version (50) lost to this batch's version (150) may be reported \
2641 accepted"
2642 );
2643
2644 for i in (0..170u64).step_by(2) {
2645 let value = s.get(table(), key(i).as_slice(), CommitVersion(u64::MAX)).unwrap().value();
2646 assert_eq!(
2647 value.as_ref().map(|v| v.as_slice()),
2648 Some(&b"seed-even"[..]),
2649 "key {i} lost its CAS (stored version 200 >= batch version 150) and must be untouched"
2650 );
2651 }
2652 for i in (1..170u64).step_by(2) {
2653 let value = s.get(table(), key(i).as_slice(), CommitVersion(u64::MAX)).unwrap().value();
2654 assert_eq!(
2655 value.as_ref().map(|v| v.as_slice()),
2656 Some(&b"attempt"[..]),
2657 "key {i} won its CAS (stored version 50 < batch version 150) and must carry this batch's \
2658 value"
2659 );
2660 }
2661 }
2662
2663 #[test]
2664 fn a_write_below_a_removals_version_still_lands_because_versions_arrive_out_of_order() {
2665 let (s, _guard) = SqlitePersistentStorage::in_memory();
2667 s.set(CommitVersion(5), HashMap::from([(table(), vec![(key(1), None)])])).unwrap();
2668
2669 s.set(CommitVersion(4), HashMap::from([(table(), vec![(key(1), Some(row(b"earlier")))])])).unwrap();
2670
2671 assert!(visible(&s, &key(1)), "the row must land: nothing on disk outranks it after the removal");
2672 }
2673
2674 #[test]
2675 fn persist_sweep_errors_when_storage_is_shut_down() {
2676 let (s, _guard) = SqlitePersistentStorage::in_memory();
2679 s.shutdown();
2680
2681 let batches = vec![(CommitVersion(1), HashMap::from([(table(), vec![(key(1), Some(row(b"v")))])]))];
2682
2683 assert!(
2684 s.persist_sweep(batches).is_err(),
2685 "a shut-down persistent tier must refuse the sweep loudly so the buffer is not dropped"
2686 );
2687 }
2688
2689 #[test]
2690 fn expired_keys_returns_rows_at_or_below_the_cutoff_oldest_first() {
2691 let (s, _guard) = SqlitePersistentStorage::in_memory();
2693 s.set(
2694 CommitVersion(1),
2695 HashMap::from([(
2696 table(),
2697 vec![
2698 (key(1), Some(stamped(300))),
2699 (key(2), Some(stamped(100))),
2700 (key(3), Some(stamped(200))),
2701 (key(4), Some(stamped(500))),
2702 ],
2703 )]),
2704 )
2705 .unwrap();
2706
2707 assert_eq!(
2708 expired_at(&s, table(), 300),
2709 vec![2, 3, 1],
2710 "candidates must come back ordered by their own stamp, oldest first, cutoff inclusive"
2711 );
2712 assert_eq!(expired_at(&s, table(), 99), Vec::<u64>::new(), "a cutoff below every stamp yields nothing");
2713 }
2714
2715 #[test]
2716 fn expired_keys_never_returns_a_tombstone() {
2717 let (s, _guard) = SqlitePersistentStorage::in_memory();
2719 s.set(CommitVersion(1), HashMap::from([(table(), vec![(key(1), Some(stamped(100)))])])).unwrap();
2720 s.set(CommitVersion(2), HashMap::from([(table(), vec![(key(1), None)])])).unwrap();
2721
2722 assert_eq!(
2723 expired_at(&s, table(), 1_000),
2724 Vec::<u64>::new(),
2725 "a valueless row must not surface as an expiry candidate"
2726 );
2727 }
2728
2729 #[test]
2730 fn a_fresh_write_clears_an_earlier_expiry_stamp() {
2731 let (s, _guard) = SqlitePersistentStorage::in_memory();
2733 s.set(CommitVersion(1), HashMap::from([(table(), vec![(key(1), Some(stamped(100)))])])).unwrap();
2734 assert_eq!(expired_at(&s, table(), 150), vec![1], "precondition: the row starts out expired");
2735
2736 s.set(CommitVersion(2), HashMap::from([(table(), vec![(key(1), Some(stamped(900)))])])).unwrap();
2737
2738 assert_eq!(
2739 expired_at(&s, table(), 150),
2740 Vec::<u64>::new(),
2741 "rewriting the row must carry its new stamp into the index, not leave the stale one"
2742 );
2743 assert_eq!(expired_at(&s, table(), 900), vec![1], "and the row expires again against the new stamp");
2744 }
2745
2746 #[test]
2747 fn expired_keys_ignores_entries_whose_rows_carry_no_stamp() {
2748 let (s, _guard) = SqlitePersistentStorage::in_memory();
2750 s.set(CommitVersion(1), HashMap::from([(EntryKind::Multi, vec![(key(1), Some(stamped(100)))])]))
2751 .unwrap();
2752
2753 assert_eq!(
2754 expired_at(&s, EntryKind::Multi, 1_000),
2755 Vec::<u64>::new(),
2756 "a non-row entry must never produce expiry candidates, whatever its bytes look like"
2757 );
2758 }
2759
2760 #[test]
2761 fn expired_keys_resumes_from_the_cursor_without_gaps_or_repeats() {
2762 let (s, _guard) = SqlitePersistentStorage::in_memory();
2764 s.set(
2765 CommitVersion(1),
2766 HashMap::from([(
2767 table(),
2768 vec![
2769 (key(1), Some(stamped(100))),
2770 (key(2), Some(stamped(100))),
2771 (key(3), Some(stamped(200))),
2772 ],
2773 )]),
2774 )
2775 .unwrap();
2776
2777 let mut seen = Vec::new();
2778 let mut cursor: Option<(DateTime, EncodedKey)> = None;
2779 loop {
2780 let batch = s
2781 .expired_keys(table(), at(1_000), cursor.as_ref().map(|(a, k)| (*a, k.as_slice())), 1)
2782 .unwrap();
2783 let Some((k, a)) = batch.last().cloned() else {
2784 break;
2785 };
2786 seen.push(RowKey::decode(&k).unwrap().row.0);
2787 cursor = Some((a, k));
2788 }
2789
2790 assert_eq!(
2791 seen,
2792 vec![2, 1, 3],
2793 "threading the cursor must walk every candidate exactly once, in order: rows 1 and 2 share a \
2794 stamp, so the key breaks the tie descending, and only then does the later stamp follow"
2795 );
2796 }
2797
2798 #[test]
2799 fn delete_keys_removes_the_row_outright_leaving_no_tombstone() {
2800 let (s, _guard) = SqlitePersistentStorage::in_memory();
2802 s.set(
2803 CommitVersion(1),
2804 HashMap::from([(
2805 table(),
2806 vec![
2807 (key(1), Some(stamped(100))),
2808 (key(2), Some(stamped(200))),
2809 (key(3), Some(stamped(500))),
2810 ],
2811 )]),
2812 )
2813 .unwrap();
2814
2815 assert_eq!(reap(&s, &[1, 2]), 2, "every named key must be removed");
2816 assert_eq!(stored_keys(&s), vec![3], "a reaped key must leave no row behind, not even a tombstone");
2817 assert_eq!(
2818 expired_at(&s, table(), 1_000),
2819 vec![3],
2820 "a reaped key must not resurface as an expiry candidate"
2821 );
2822 }
2823
2824 fn series_storage() -> StorageId {
2825 StorageId::series(1)
2826 }
2827
2828 fn series_table() -> EntryKind {
2829 EntryKind::Source(series_storage(), EntryLayout::Series)
2830 }
2831
2832 fn series_key(variant_tag: Option<u8>, key: u64, sequence: u64) -> EncodedKey {
2833 SeriesRowKey {
2834 storage: series_storage(),
2835 variant_tag,
2836 key,
2837 sequence,
2838 }
2839 .encode()
2840 }
2841
2842 fn partitioned_series_table() -> EntryKind {
2843 EntryKind::PartitionedSource(series_storage(), EntryLayout::Series)
2844 }
2845
2846 fn partitioned_series_key(partition: u128, key: u64, sequence: u64) -> EncodedKey {
2847 PartitionedSeriesRowKey::encoded(series_storage(), Partition(partition), None, key, sequence)
2848 }
2849
2850 fn resolved_schema(s: &SqlitePersistentStorage, kind: EntryKind) -> SqliteSchema {
2851 let guard = s.inner.readers.acquire();
2852 let conn = guard.as_ref().expect("read connection is present");
2853 s.table_sql(conn, kind).unwrap().schema
2854 }
2855
2856 fn scan_forward(s: &SqlitePersistentStorage, kind: EntryKind, range: &EncodedKeyRange) -> Vec<EncodedKey> {
2857 let start = match &range.start {
2858 Bound::Included(k) => Bound::Included(k.as_slice()),
2859 Bound::Excluded(k) => Bound::Excluded(k.as_slice()),
2860 Bound::Unbounded => Bound::Unbounded,
2861 };
2862 let end = match &range.end {
2863 Bound::Included(k) => Bound::Included(k.as_slice()),
2864 Bound::Excluded(k) => Bound::Excluded(k.as_slice()),
2865 Bound::Unbounded => Bound::Unbounded,
2866 };
2867 let mut cursor = RangeCursor::default();
2868 s.range_next(
2869 kind,
2870 &mut cursor,
2871 start,
2872 end,
2873 MultiVersionScope::AsOf {
2874 read: CommitVersion(100),
2875 },
2876 1024,
2877 )
2878 .unwrap()
2879 .entries
2880 .into_iter()
2881 .map(|e| e.key)
2882 .collect()
2883 }
2884
2885 #[test]
2886 fn a_fresh_series_table_is_created_narrow_and_serves_every_read_path() {
2887 let (s, _guard) = SqlitePersistentStorage::in_memory();
2891 let t = series_table();
2892 s.set(
2893 CommitVersion(1),
2894 HashMap::from([(
2895 t,
2896 vec![
2897 (series_key(None, 10, 0), Some(stamped(100))),
2898 (series_key(Some(3), 10, 1), Some(stamped(200))),
2899 (series_key(Some(3), 20, 0), Some(stamped(300))),
2900 ],
2901 )]),
2902 )
2903 .unwrap();
2904
2905 assert_eq!(resolved_schema(&s, t), SqliteSchema::Series);
2906 assert_eq!(s.count_current(t).unwrap(), 3);
2907
2908 let got = s.get(t, series_key(Some(3), 20, 0).as_slice(), CommitVersion(10)).unwrap();
2909 assert!(matches!(got, VersionedGetResult::Value { .. }), "a narrow series row must be readable by key");
2910
2911 let keys = [series_key(Some(3), 10, 1), series_key(None, 10, 0), series_key(Some(9), 1, 1)];
2912 let refs: Vec<&[u8]> = keys.iter().map(|k| k.as_slice()).collect();
2913 let many = s.get_many(t, &refs, CommitVersion(10)).unwrap();
2914 assert!(matches!(many[0], VersionedGetResult::Value { .. }));
2915 assert!(matches!(many[1], VersionedGetResult::Value { .. }));
2916 assert!(
2917 matches!(many[2], VersionedGetResult::NotFound),
2918 "a key that was never written must not be answered out of another row"
2919 );
2920
2921 let scanned = scan_forward(&s, t, &SeriesRowKeyRange::full_scan(series_storage(), None).encode());
2922 assert_eq!(
2923 scanned,
2924 vec![series_key(Some(3), 20, 0), series_key(Some(3), 10, 1), series_key(None, 10, 0)],
2925 "a narrow scan must reproduce the encoded key order: tag ascending, then key and sequence \
2926 descending, with the untagged rows last"
2927 );
2928
2929 let current: Vec<EncodedKey> = s.current_key_slice(t, None, 100).unwrap();
2930 assert_eq!(current, scanned, "the current key slice must walk the same order the scan does");
2931
2932 let expired: Vec<EncodedKey> =
2933 s.expired_keys(t, at(250), None, 100).unwrap().into_iter().map(|(key, _)| key).collect();
2934 assert_eq!(
2935 expired,
2936 vec![series_key(None, 10, 0), series_key(Some(3), 10, 1)],
2937 "expiry discovery must return the stamped narrow rows oldest first, decoded back to keys"
2938 );
2939
2940 assert_eq!(s.delete_keys(t, &[series_key(Some(3), 10, 1)]).unwrap(), 1);
2941 assert_eq!(s.count_current(t).unwrap(), 2, "a reaped narrow row must actually leave the table");
2942
2943 s.set(CommitVersion(2), HashMap::from([(t, vec![(series_key(None, 10, 0), None)])])).unwrap();
2944 assert!(
2945 matches!(
2946 s.get(t, series_key(None, 10, 0).as_slice(), CommitVersion(10)).unwrap(),
2947 VersionedGetResult::NotFound
2948 ),
2949 "a removal must delete the row the narrow delete names"
2950 );
2951 assert_eq!(
2952 s.count_current(t).unwrap(),
2953 1,
2954 "a removal must delete exactly one narrow row, not every row sharing a key column"
2955 );
2956 assert_eq!(
2957 scan_forward(&s, t, &SeriesRowKeyRange::full_scan(series_storage(), None).encode()),
2958 vec![series_key(Some(3), 20, 0)],
2959 "the row the removal did not name must survive it"
2960 );
2961 }
2962
2963 #[test]
2964 fn a_series_batch_larger_than_a_chunk_reports_back_the_keys_it_actually_wrote() {
2965 let (s, _guard) = SqlitePersistentStorage::in_memory();
2969 let t = series_table();
2970 let written: Vec<EncodedKey> = (0u64..250).map(|n| series_key(Some((n % 4) as u8), n, n % 3)).collect();
2971 let accepted = s
2972 .set_collecting_accepted(
2973 CommitVersion(1),
2974 HashMap::from([(
2975 t,
2976 written.iter().map(|k| (k.clone(), Some(row(b"v")))).collect::<Vec<_>>(),
2977 )]),
2978 )
2979 .unwrap();
2980
2981 let mut accepted_sorted = accepted.clone();
2982 accepted_sorted.sort();
2983 let mut written_sorted = written.clone();
2984 written_sorted.sort();
2985 assert_eq!(accepted_sorted, written_sorted, "every written key must be reported back exactly once");
2986 assert_eq!(s.count_current(t).unwrap(), 250);
2987
2988 let removals: Vec<EncodedKey> = written.iter().take(150).cloned().collect();
2989 let removed = s
2990 .set_collecting_accepted(
2991 CommitVersion(2),
2992 HashMap::from([(t, removals.iter().map(|k| (k.clone(), None)).collect::<Vec<_>>())]),
2993 )
2994 .unwrap();
2995 let mut removed_sorted = removed.clone();
2996 removed_sorted.sort();
2997 let mut removals_sorted = removals.clone();
2998 removals_sorted.sort();
2999 assert_eq!(removed_sorted, removals_sorted, "every removed key must be reported back exactly once");
3000 assert_eq!(s.count_current(t).unwrap(), 100);
3001 }
3002
3003 #[test]
3004 fn a_series_cursor_resumes_a_key_walk_and_an_expiry_walk_without_gaps_or_repeats() {
3005 let (s, _guard) = SqlitePersistentStorage::in_memory();
3009 let t = series_table();
3010 let written: Vec<EncodedKey> = (0u64..12).map(|n| series_key(Some((n % 3) as u8), n, 0)).collect();
3011 s.set(
3012 CommitVersion(1),
3013 HashMap::from([(
3014 t,
3015 written.iter()
3016 .enumerate()
3017 .map(|(i, k)| (k.clone(), Some(stamped(i as u64 + 1))))
3018 .collect(),
3019 )]),
3020 )
3021 .unwrap();
3022
3023 let mut walked: Vec<EncodedKey> = Vec::new();
3024 loop {
3025 let page = s.current_key_slice(t, walked.last(), 5).unwrap();
3026 if page.is_empty() {
3027 break;
3028 }
3029 walked.extend(page);
3030 }
3031 let mut expected = written.clone();
3032 expected.sort_by_key(|k| k.as_slice().to_vec());
3033 assert_eq!(walked, expected, "a paged key walk must reach every row once, in encoded key order");
3034
3035 let mut expired: Vec<EncodedKey> = Vec::new();
3036 let mut cursor: Option<(DateTime, EncodedKey)> = None;
3037 loop {
3038 let page = s
3039 .expired_keys(t, at(100), cursor.as_ref().map(|(when, key)| (*when, key.as_slice())), 5)
3040 .unwrap();
3041 if page.is_empty() {
3042 break;
3043 }
3044 cursor = page.last().map(|(key, when)| (*when, key.clone()));
3045 expired.extend(page.into_iter().map(|(key, _)| key));
3046 }
3047 let mut expired_sorted = expired.clone();
3048 expired_sorted.sort();
3049 let mut written_sorted = written.clone();
3050 written_sorted.sort();
3051 assert_eq!(
3052 expired_sorted, written_sorted,
3053 "a paged expiry walk must reach every stamped row exactly once"
3054 );
3055 }
3056
3057 #[test]
3058 fn a_series_expiry_resume_breaks_a_shared_stamp_tie_on_the_key_columns() {
3059 let (s, _guard) = SqlitePersistentStorage::in_memory();
3063 let t = series_table();
3064 let written: Vec<EncodedKey> = (0u64..6).map(|n| series_key(Some(1), n, 0)).collect();
3065 s.set(
3066 CommitVersion(1),
3067 HashMap::from([(
3068 t,
3069 written.iter().map(|k| (k.clone(), Some(stamped(50)))).collect::<Vec<_>>(),
3070 )]),
3071 )
3072 .unwrap();
3073
3074 let mut seen: Vec<EncodedKey> = Vec::new();
3075 let mut cursor: Option<(DateTime, EncodedKey)> = None;
3076 for _ in 0..10 {
3077 let page = s
3078 .expired_keys(t, at(100), cursor.as_ref().map(|(when, key)| (*when, key.as_slice())), 2)
3079 .unwrap();
3080 if page.is_empty() {
3081 break;
3082 }
3083 cursor = page.last().map(|(key, when)| (*when, key.clone()));
3084 seen.extend(page.into_iter().map(|(key, _)| key));
3085 }
3086
3087 let mut expected = written.clone();
3088 expected.sort_by_key(|k| k.as_slice().to_vec());
3089 assert_eq!(
3090 seen, expected,
3091 "rows sharing one stamp must be walked once each, in encoded key order, across pages"
3092 );
3093 }
3094
3095 #[test]
3096 fn a_fresh_partitioned_series_table_is_created_narrow_and_keeps_its_partitions_apart() {
3097 let (s, _guard) = SqlitePersistentStorage::in_memory();
3100 let t = partitioned_series_table();
3101 s.set(
3102 CommitVersion(1),
3103 HashMap::from([(
3104 t,
3105 vec![
3106 (partitioned_series_key(1, 10, 0), Some(row(b"a"))),
3107 (partitioned_series_key(1, 20, 0), Some(row(b"b"))),
3108 (partitioned_series_key(2, 10, 0), Some(row(b"c"))),
3109 ],
3110 )]),
3111 )
3112 .unwrap();
3113
3114 assert_eq!(resolved_schema(&s, t), SqliteSchema::PartitionedSeries);
3115
3116 let all = scan_forward(&s, t, &PartitionedSeriesRowKeyRange::full_scan(series_storage()).encode());
3117 assert_eq!(all.len(), 3, "a full scan must reach every partition");
3118
3119 let one = scan_forward(
3120 &s,
3121 t,
3122 &PartitionedSeriesRowKeyRange::partition_range(series_storage(), Partition(1)).encode(),
3123 );
3124 assert_eq!(
3125 one,
3126 vec![partitioned_series_key(1, 20, 0), partitioned_series_key(1, 10, 0)],
3127 "a partition range must return exactly its own partition, key descending"
3128 );
3129
3130 assert!(
3131 matches!(
3132 s.get(t, partitioned_series_key(2, 10, 0).as_slice(), CommitVersion(10)).unwrap(),
3133 VersionedGetResult::Value { .. }
3134 ),
3135 "a partitioned narrow get must find the row under its own partition"
3136 );
3137 }
3138
3139 #[test]
3140 fn the_typed_series_range_reads_the_same_rows_the_byte_range_does() {
3141 let (s, _guard) = SqlitePersistentStorage::in_memory();
3145 let t = series_table();
3146 let written: Vec<EncodedKey> = (0u64..8).map(|n| series_key(Some((n % 2) as u8), n, n % 2)).collect();
3147 s.set(
3148 CommitVersion(1),
3149 HashMap::from([(t, written.iter().map(|k| (k.clone(), Some(row(b"v")))).collect::<Vec<_>>())]),
3150 )
3151 .unwrap();
3152
3153 let mut cursor: Cursor<RangeStop, StorageSeriesKey> = Cursor::default();
3154 let typed = s
3155 .range_chunk_series(
3156 &mut cursor,
3157 NarrowRangeRequest {
3158 table: t,
3159 start: Bound::Unbounded,
3160 end: Bound::Unbounded,
3161 scope: MultiVersionScope::AsOf {
3162 read: CommitVersion(100),
3163 },
3164 batch_size: 1024,
3165 descending: false,
3166 },
3167 )
3168 .unwrap();
3169 let typed_keys: Vec<EncodedKey> =
3170 typed.entries.iter().map(|e| e.key.with_storage(series_storage()).encode()).collect();
3171
3172 assert_eq!(
3173 typed_keys,
3174 scan_forward(&s, t, &SeriesRowKeyRange::full_scan(series_storage(), None).encode()),
3175 "the typed narrow scan and the translated byte scan must agree row for row"
3176 );
3177 assert_eq!(typed_keys.len(), written.len());
3178 }
3179
3180 #[test]
3181 fn the_typed_partitioned_series_range_reads_the_same_rows_the_byte_range_does() {
3182 let (s, _guard) = SqlitePersistentStorage::in_memory();
3183 let t = partitioned_series_table();
3184 let written: Vec<EncodedKey> =
3185 (0u64..6).map(|n| partitioned_series_key((n % 2) as u128 + 1, n, 0)).collect();
3186 s.set(
3187 CommitVersion(1),
3188 HashMap::from([(t, written.iter().map(|k| (k.clone(), Some(row(b"v")))).collect::<Vec<_>>())]),
3189 )
3190 .unwrap();
3191
3192 let mut cursor: Cursor<RangeStop, StoragePartitionedSeriesKey> = Cursor::default();
3193 let typed = s
3194 .range_chunk_partitioned_series(
3195 &mut cursor,
3196 NarrowRangeRequest {
3197 table: t,
3198 start: Bound::Unbounded,
3199 end: Bound::Unbounded,
3200 scope: MultiVersionScope::AsOf {
3201 read: CommitVersion(100),
3202 },
3203 batch_size: 1024,
3204 descending: false,
3205 },
3206 )
3207 .unwrap();
3208 let typed_keys: Vec<EncodedKey> =
3209 typed.entries.iter().map(|e| e.key.with_storage(series_storage()).encode()).collect();
3210
3211 assert_eq!(
3212 typed_keys,
3213 scan_forward(&s, t, &PartitionedSeriesRowKeyRange::full_scan(series_storage()).encode()),
3214 "the typed partitioned scan and the translated byte scan must agree row for row"
3215 );
3216 assert_eq!(typed_keys.len(), written.len());
3217 }
3218
3219 #[test]
3220 fn an_existing_blob_series_table_keeps_being_read_and_written_as_a_blob_table() {
3221 let (s, guard) = SqlitePersistentStorage::in_memory();
3225 let t = series_table();
3226 {
3227 let conn_guard = s.inner.conn.lock();
3228 let conn = conn_guard.as_ref().expect("write connection is present");
3229 conn.execute_batch(&build_create_current_sql(¤t_table_name(t))).unwrap();
3230 }
3231
3232 assert_eq!(
3233 resolved_schema(&s, t),
3234 SqliteSchema::Blob,
3235 "a table already carrying a key BLOB primary key must stay on the blob schema"
3236 );
3237
3238 s.set(
3239 CommitVersion(1),
3240 HashMap::from([(
3241 t,
3242 vec![
3243 (series_key(None, 10, 0), Some(stamped(100))),
3244 (series_key(Some(3), 20, 0), Some(stamped(300))),
3245 ],
3246 )]),
3247 )
3248 .unwrap();
3249
3250 assert_eq!(s.count_current(t).unwrap(), 2, "the write must land in the pre-existing blob table");
3251 assert!(
3252 matches!(
3253 s.get(t, series_key(Some(3), 20, 0).as_slice(), CommitVersion(10)).unwrap(),
3254 VersionedGetResult::Value { .. }
3255 ),
3256 "a blob series row must still be readable, not silently answered as absent"
3257 );
3258 assert_eq!(
3259 scan_forward(&s, t, &SeriesRowKeyRange::full_scan(series_storage(), None).encode()),
3260 vec![series_key(Some(3), 20, 0), series_key(None, 10, 0)],
3261 "a blob series scan must keep returning its rows"
3262 );
3263 assert_eq!(
3264 s.expired_keys(t, at(200), None, 100).unwrap().len(),
3265 1,
3266 "expiry discovery must keep working against the blob table"
3267 );
3268 assert_eq!(s.delete_keys(t, &[series_key(None, 10, 0)]).unwrap(), 1);
3269
3270 let columns = {
3271 let conn_guard = s.inner.conn.lock();
3272 let conn = conn_guard.as_ref().expect("write connection is present");
3273 read_table_columns(conn, ¤t_table_name(t)).unwrap()
3274 };
3275 assert!(
3276 columns.iter().all(|(name, _)| name != "variant_tag"),
3277 "the blob table must never be widened into a narrow one behind the caller's back"
3278 );
3279 drop(guard);
3280 }
3281
3282 #[test]
3283 fn a_series_table_whose_layout_the_probe_cannot_place_is_refused_loudly() {
3284 let (s, _guard) = SqlitePersistentStorage::in_memory();
3287 let t = series_table();
3288 {
3289 let conn_guard = s.inner.conn.lock();
3290 let conn = conn_guard.as_ref().expect("write connection is present");
3291 conn.execute_batch(&format!(
3292 "CREATE TABLE \"{}\" (key INTEGER PRIMARY KEY, version BLOB NOT NULL, value BLOB, \
3293 updated_at INTEGER);",
3294 current_table_name(t)
3295 ))
3296 .unwrap();
3297 }
3298
3299 let err = s.count_current(t).unwrap_err();
3300 assert!(
3301 err.to_string().contains("neither the blob series layout nor the narrow one"),
3302 "the probe must name what it could not place, got {err}"
3303 );
3304 }
3305
3306 #[test]
3307 fn row_schema_full_scan_preserves_the_blob_schemas_descending_ascending_order() {
3308 let (s, _guard) = SqlitePersistentStorage::in_memory();
3309 let t = table();
3310 let mut writes = Vec::new();
3311 for n in 1u64..=5 {
3312 writes.push((key(n), Some(row(b"x"))));
3313 }
3314 s.set(CommitVersion(1), HashMap::from([(t, writes)])).unwrap();
3315
3316 let range = RowKey::full_scan(StorageId::Table(TableId(1))).encode();
3317 let (start, end) = match (&range.start, &range.end) {
3318 (Bound::Included(s), Bound::Excluded(e)) => (s.as_slice(), e.as_slice()),
3319 _ => panic!("expected an included prefix start and an excluded prefix end"),
3320 };
3321
3322 let mut cursor = RangeCursor::default();
3323 let batch = s
3324 .range_next(
3325 t,
3326 &mut cursor,
3327 Bound::Included(start),
3328 Bound::Excluded(end),
3329 MultiVersionScope::AsOf {
3330 read: CommitVersion(10),
3331 },
3332 1024,
3333 )
3334 .unwrap();
3335 let forward: Vec<u64> = batch.entries.iter().map(|e| RowKey::decode(&e.key).unwrap().row.0).collect();
3336 assert_eq!(
3337 forward,
3338 vec![5, 4, 3, 2, 1],
3339 "range_next must reproduce the old byte-ascending-is-row-descending order of the BLOB schema"
3340 );
3341
3342 let mut cursor2 = RangeCursor::default();
3343 let batch2 = s
3344 .range_rev_next(
3345 t,
3346 &mut cursor2,
3347 Bound::Included(start),
3348 Bound::Excluded(end),
3349 MultiVersionScope::AsOf {
3350 read: CommitVersion(10),
3351 },
3352 1024,
3353 )
3354 .unwrap();
3355 let reverse: Vec<u64> = batch2.entries.iter().map(|e| RowKey::decode(&e.key).unwrap().row.0).collect();
3356 assert_eq!(
3357 reverse,
3358 vec![1, 2, 3, 4, 5],
3359 "range_rev_next must reproduce the old byte-descending-is-row-ascending order of the BLOB schema"
3360 );
3361 }
3362
3363 #[test]
3364 fn row_schema_scan_range_mixes_a_full_cursor_bound_with_a_prefix_only_end_bound() {
3365 let (s, _guard) = SqlitePersistentStorage::in_memory();
3366 let t = table();
3367 let mut writes = Vec::new();
3368 for n in 1u64..=5 {
3369 writes.push((key(n), Some(row(b"x"))));
3370 }
3371 s.set(CommitVersion(1), HashMap::from([(t, writes)])).unwrap();
3372
3373 let range = RowKeyRange::scan_range(StorageId::Table(TableId(1)), Some(&row_cursor(4))).encode();
3374 let (start, end) = match (&range.start, &range.end) {
3375 (Bound::Excluded(s), Bound::Excluded(e)) => (s.as_slice(), e.as_slice()),
3376 other => panic!(
3377 "expected an excluded cursor start and an excluded prefix-only end, got {other:?}"
3378 ),
3379 };
3380
3381 let mut cursor = RangeCursor::default();
3382 let batch = s
3383 .range_next(
3384 t,
3385 &mut cursor,
3386 Bound::Excluded(start),
3387 Bound::Excluded(end),
3388 MultiVersionScope::AsOf {
3389 read: CommitVersion(10),
3390 },
3391 1024,
3392 )
3393 .unwrap();
3394 let got: Vec<u64> = batch.entries.iter().map(|e| RowKey::decode(&e.key).unwrap().row.0).collect();
3395 assert_eq!(
3396 got,
3397 vec![3, 2, 1],
3398 "resuming after row 4 must yield every remaining row, oldest scan order preserved"
3399 );
3400 }
3401
3402 #[test]
3403 fn row_schema_expired_keys_scan_finds_rows() {
3404 let (s, _guard) = SqlitePersistentStorage::in_memory();
3405 s.set(
3406 CommitVersion(1),
3407 HashMap::from([(table(), vec![(key(1), Some(stamped(100))), (key(2), Some(stamped(200)))])]),
3408 )
3409 .unwrap();
3410
3411 assert_eq!(
3412 expired_at(&s, table(), 150),
3413 vec![1],
3414 "the narrow row schema must still surface expiry candidates through its own key column"
3415 );
3416 }
3417
3418 fn partitioned_table() -> EntryKind {
3419 EntryKind::PartitionedSource(StorageId::Table(TableId(2)), EntryLayout::Row)
3420 }
3421
3422 fn partitioned_key(partition: u128, n: u64) -> EncodedKey {
3423 PartitionedRowKey::encoded(StorageId::Table(TableId(2)), Partition(partition), RowNumber(n))
3424 }
3425
3426 fn partitioned_cursor(partition: u128, n: u64) -> TaggedKey {
3427 TaggedKey::from(PartitionedRowKey::new(
3428 StorageId::Table(TableId(2)),
3429 Partition(partition),
3430 RowNumber(n),
3431 ))
3432 }
3433
3434 #[test]
3435 fn partitioned_schema_get_after_insert_is_exact() {
3436 let (s, _guard) = SqlitePersistentStorage::in_memory();
3437 let k = partitioned_key(7, 3);
3438 s.set(CommitVersion(1), HashMap::from([(partitioned_table(), vec![(k.clone(), Some(row(b"v")))])]))
3439 .unwrap();
3440
3441 let got = s.get(partitioned_table(), k.as_slice(), CommitVersion(u64::MAX)).unwrap();
3442 assert_eq!(got.value().as_ref().map(|v| v.as_slice()), Some(&b"v"[..]));
3443 }
3444
3445 #[test]
3446 fn partitioned_schema_full_scan_with_both_bounds_prefix_only() {
3447 let (s, _guard) = SqlitePersistentStorage::in_memory();
3448 let t = partitioned_table();
3449 s.set(
3450 CommitVersion(1),
3451 HashMap::from([(
3452 t,
3453 vec![
3454 (partitioned_key(1, 1), Some(row(b"a"))),
3455 (partitioned_key(1, 2), Some(row(b"b"))),
3456 (partitioned_key(2, 1), Some(row(b"c"))),
3457 ],
3458 )]),
3459 )
3460 .unwrap();
3461
3462 let range = PartitionedRowKey::full_scan(StorageId::Table(TableId(2))).encode();
3463 let (start, end) = match (&range.start, &range.end) {
3464 (Bound::Included(s), Bound::Excluded(e)) => (s.as_slice(), e.as_slice()),
3465 other => panic!("expected an included prefix start and an excluded prefix end, got {other:?}"),
3466 };
3467
3468 let mut cursor = RangeCursor::default();
3469 let batch = s
3470 .range_next(
3471 t,
3472 &mut cursor,
3473 Bound::Included(start),
3474 Bound::Excluded(end),
3475 MultiVersionScope::AsOf {
3476 read: CommitVersion(10),
3477 },
3478 1024,
3479 )
3480 .unwrap();
3481 assert_eq!(batch.entries.len(), 3, "a full-table scan across partitions must reach every row");
3482 }
3483
3484 #[test]
3485 fn partitioned_schema_scan_range_mixes_a_full_cursor_bound_with_a_prefix_only_end_bound() {
3486 let (s, _guard) = SqlitePersistentStorage::in_memory();
3487 let t = partitioned_table();
3488 s.set(
3489 CommitVersion(1),
3490 HashMap::from([(
3491 t,
3492 vec![
3493 (partitioned_key(1, 1), Some(row(b"a"))),
3494 (partitioned_key(1, 2), Some(row(b"b"))),
3495 (partitioned_key(2, 1), Some(row(b"c"))),
3496 ],
3497 )]),
3498 )
3499 .unwrap();
3500
3501 let range =
3503 PartitionedRowKey::scan_range(StorageId::Table(TableId(2)), Some(&partitioned_cursor(2, 1)))
3504 .encode();
3505 let (start, end) = match (&range.start, &range.end) {
3506 (Bound::Excluded(s), Bound::Excluded(e)) => (s.as_slice(), e.as_slice()),
3507 other => panic!(
3508 "expected an excluded cursor start and an excluded prefix-only end, got {other:?}"
3509 ),
3510 };
3511
3512 let mut cursor = RangeCursor::default();
3513 let batch = s
3514 .range_next(
3515 t,
3516 &mut cursor,
3517 Bound::Excluded(start),
3518 Bound::Excluded(end),
3519 MultiVersionScope::AsOf {
3520 read: CommitVersion(10),
3521 },
3522 1024,
3523 )
3524 .unwrap();
3525 assert_eq!(batch.entries.len(), 2, "resuming past the first row must still reach the other two");
3526 }
3527 #[test]
3528 fn partitioned_schema_expired_keys_scan_finds_rows() {
3529 let (s, _guard) = SqlitePersistentStorage::in_memory();
3530 s.set(
3531 CommitVersion(1),
3532 HashMap::from([(
3533 partitioned_table(),
3534 vec![
3535 (partitioned_key(1, 1), Some(stamped(100))),
3536 (partitioned_key(1, 2), Some(stamped(200))),
3537 ],
3538 )]),
3539 )
3540 .unwrap();
3541
3542 assert_eq!(
3543 partitioned_expired_at(&s, partitioned_table(), 150),
3544 vec![1],
3545 "the narrow partitioned schema must still surface expiry candidates through its own columns"
3546 );
3547 }
3548
3549 #[test]
3550 fn partitioned_expired_keys_resume_breaks_a_shared_stamp_tie_in_encoded_key_order() {
3551 let (s, _guard) = SqlitePersistentStorage::in_memory();
3557 let low = 1u128;
3558 let high = (1u128 << 127) | 3;
3559 s.set(
3560 CommitVersion(1),
3561 HashMap::from([(
3562 partitioned_table(),
3563 vec![
3564 (partitioned_key(low, 1), Some(stamped(100))),
3565 (partitioned_key(low, 2), Some(stamped(100))),
3566 (partitioned_key(high, 1), Some(stamped(100))),
3567 (partitioned_key(low, 3), Some(stamped(200))),
3568 ],
3569 )]),
3570 )
3571 .unwrap();
3572
3573 let mut tied: Vec<(u128, u64)> = vec![(low, 1), (low, 2), (high, 1)];
3574 tied.sort_by(|a, b| partitioned_key(a.0, a.1).as_slice().cmp(partitioned_key(b.0, b.1).as_slice()));
3575 let mut expected = tied.clone();
3576 expected.push((low, 3));
3577
3578 let mut seen = Vec::new();
3579 let mut cursor: Option<(DateTime, EncodedKey)> = None;
3580 loop {
3581 let batch = s
3582 .expired_keys(
3583 partitioned_table(),
3584 at(1_000),
3585 cursor.as_ref().map(|(a, k)| (*a, k.as_slice())),
3586 1,
3587 )
3588 .unwrap();
3589 let Some((k, a)) = batch.last().cloned() else {
3590 break;
3591 };
3592 let decoded = PartitionedRowKey::decode(&k).unwrap();
3593 seen.push((decoded.partition.0, decoded.row.0));
3594 cursor = Some((a, k));
3595 }
3596
3597 assert_eq!(
3598 seen, expected,
3599 "threading the cursor must walk every candidate once, oldest stamp first and ties broken \
3600 exactly as the encoded keys sort"
3601 );
3602 assert_eq!(
3603 seen[0],
3604 (high, 1),
3605 "the larger partition encodes lower, so it must lead the tie, not trail it"
3606 );
3607 }
3608
3609 #[test]
3610 fn partitioned_schema_paginated_full_scan_reaches_every_row_across_many_partitions() {
3611 let (s, _guard) = SqlitePersistentStorage::in_memory();
3612 let t = partitioned_table();
3613 let mut writes = Vec::new();
3614 let mut seed = 0x9E3779B97F4A7C15u64;
3615 for _ in 1u128..=64 {
3616 seed ^= seed << 13;
3618 seed ^= seed >> 7;
3619 seed ^= seed << 17;
3620 let hi = seed as u128;
3621 seed ^= seed << 13;
3622 seed ^= seed >> 7;
3623 seed ^= seed << 17;
3624 let lo = seed as u128;
3625 let p = (hi << 64) | lo;
3626 writes.push((partitioned_key(p, 1), Some(row(b"a"))));
3627 writes.push((partitioned_key(p, 2), Some(row(b"b"))));
3628 }
3629 s.set(CommitVersion(1), HashMap::from([(t, writes)])).unwrap();
3630
3631 let range = PartitionedRowKey::full_scan(StorageId::Table(TableId(2))).encode();
3632 let (start, end) = match (&range.start, &range.end) {
3633 (Bound::Included(s), Bound::Excluded(e)) => (s.as_slice(), e.as_slice()),
3634 other => panic!("expected an included prefix start and an excluded prefix end, got {other:?}"),
3635 };
3636
3637 let mut cursor = RangeCursor::default();
3638 let mut total = 0usize;
3639 loop {
3640 let batch = s
3641 .range_next(
3642 t,
3643 &mut cursor,
3644 Bound::Included(start),
3645 Bound::Excluded(end),
3646 MultiVersionScope::AsOf {
3647 read: CommitVersion(10),
3648 },
3649 4,
3650 )
3651 .unwrap();
3652 total += batch.entries.len();
3653 if cursor.is_exhausted() {
3654 break;
3655 }
3656 }
3657 assert_eq!(total, 128, "a paginated full-table scan with a small batch size must reach every row");
3658 }
3659
3660 #[test]
3661 fn partitioned_schema_paginated_scan_yields_exactly_the_encoded_key_order_in_both_directions() {
3662 let (s, _guard) = SqlitePersistentStorage::in_memory();
3668 let t = partitioned_table();
3669 let partitions: [u128; 4] =
3670 [1, (1u128 << 64) | 7, (1u128 << 127) | 3, (1u128 << 127) | (1u128 << 63) | 9];
3671 let mut written = Vec::new();
3672 let mut writes = Vec::new();
3673 for p in partitions {
3674 for r in 1u64..=3 {
3675 let k = partitioned_key(p, r);
3676 written.push((k.clone(), p, r));
3677 writes.push((k, Some(row(b"a"))));
3678 }
3679 }
3680 s.set(CommitVersion(1), HashMap::from([(t, writes)])).unwrap();
3681
3682 let mut forward_expected = written.clone();
3683 forward_expected.sort_by(|a, b| a.0.as_slice().cmp(b.0.as_slice()));
3684 let forward_expected: Vec<(u128, u64)> = forward_expected.iter().map(|(_, p, r)| (*p, *r)).collect();
3685 let mut reverse_expected = forward_expected.clone();
3686 reverse_expected.reverse();
3687
3688 let range = PartitionedRowKey::full_scan(StorageId::Table(TableId(2))).encode();
3689 let (start, end) = match (&range.start, &range.end) {
3690 (Bound::Included(s), Bound::Excluded(e)) => (s.as_slice(), e.as_slice()),
3691 other => panic!("expected an included prefix start and an excluded prefix end, got {other:?}"),
3692 };
3693
3694 let forward = paginate_partitioned(&s, t, start, end, false);
3695 assert_eq!(
3696 forward, forward_expected,
3697 "a paginated forward scan must walk the encoded keys ascending, partition then row"
3698 );
3699
3700 let reverse = paginate_partitioned(&s, t, start, end, true);
3701 assert_eq!(
3702 reverse, reverse_expected,
3703 "a paginated reverse scan must walk the same encoded keys descending, the exact mirror"
3704 );
3705 }
3706
3707 fn paginate_partitioned(
3708 s: &SqlitePersistentStorage,
3709 t: EntryKind,
3710 start: &[u8],
3711 end: &[u8],
3712 reverse: bool,
3713 ) -> Vec<(u128, u64)> {
3714 let mut cursor = RangeCursor::default();
3717 let mut out = Vec::new();
3718 loop {
3719 let scope = MultiVersionScope::AsOf {
3720 read: CommitVersion(10),
3721 };
3722 let batch = if reverse {
3723 s.range_rev_next(t, &mut cursor, Bound::Included(start), Bound::Excluded(end), scope, 2)
3724 } else {
3725 s.range_next(t, &mut cursor, Bound::Included(start), Bound::Excluded(end), scope, 2)
3726 }
3727 .unwrap();
3728 for entry in &batch.entries {
3729 let decoded = PartitionedRowKey::decode(&entry.key).unwrap();
3730 out.push((decoded.partition.0, decoded.row.0));
3731 }
3732 if cursor.is_exhausted() {
3733 return out;
3734 }
3735 }
3736 }
3737
3738 #[test]
3739 fn expiry_discovery_uses_the_partial_index() {
3740 let (s, _guard) = SqlitePersistentStorage::in_memory();
3742 for n in 1..=200u64 {
3743 s.set(CommitVersion(n), HashMap::from([(table(), vec![(key(n), Some(stamped(n)))])])).unwrap();
3744 }
3745 let guard = s.inner.conn.lock();
3746 let conn = guard.as_ref().expect("write connection is present");
3747 let table_name = s.table_sql(conn, table()).unwrap().table_name.clone();
3748 conn.execute_batch("ANALYZE").unwrap();
3749 let sql = format!("EXPLAIN QUERY PLAN {}", build_expired_keys_sql(&table_name, false, 100));
3750 let details: Vec<String> = conn
3751 .prepare(&sql)
3752 .unwrap()
3753 .query_map([0i64], |r| r.get::<_, String>(3))
3754 .unwrap()
3755 .map(|r| r.unwrap())
3756 .collect();
3757
3758 assert!(
3759 details.iter().any(|d| d.contains(&format!("{table_name}__expiry"))),
3760 "expiry discovery must use the partial expiry index; query plan was {details:?}"
3761 );
3762 }
3763}