Skip to main content

reifydb_store_multi/tier/persistent/sqlite/
storage.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use 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	// `table()` backs the narrow row schema, so every key built against it must decode as a RowKey.
2378	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		// A body shorter than a full shape header must never be read as carrying an expiry stamp.
2396		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		// The narrow row schema stores the row number descending in `key`, never an encoded RowKey,
2428		// so reading the column raw would report the inverted integer rather than the row.
2429		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		// The timer probe asks for one row and used to receive every dead row in the prefix,
2448		// because LIMIT applies after the WHERE: measured at 1,837 rows fetched per probe, 100%
2449		// of them dead. A removal now deletes the row outright rather than rewriting it to a
2450		// none value, so the dead rows are not there to be scanned past in the first place.
2451		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		// The SQL only bounds version <= read, but Between also demands version > after, so the
2498		// surviving-row count is not evidence about whether sqlite has more rows. Deciding
2499		// exhaustion from it stops the scan on the first page that filters out, silently dropping
2500		// every later match; resuming from the last surviving key instead of the last scanned key
2501		// would re-read the filtered rows forever. Keys 1-2 fill a whole page and all fail the
2502		// filter, so a correct cursor must still reach keys 3-4.
2503		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		// A sweep drains the per-connection counters take-and-reset into store totals, so the reported
2541		// counts must be monotone; raw per-connection reads would report a sawtooth, not a hit rate.
2542		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		// No query plan selects a bare version index, so recreating it costs a b-tree write per row on every
2575		// source table for nothing.
2576		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		// A removal must leave no row behind, or the parked one wins the CAS and strands the key absent.
2608		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		// a key marked accepted despite losing its CAS is evicted from memory while never persisted
2624		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		// Batches reach this tier unordered, so a lower version after a removal is an ordinary write.
2666		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		// The sweep hands over the only copy of a row: the commit buffer drops it on the strength of this
2677		// call returning Ok. A shut-down storage that reported success would lose the row silently.
2678		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		// Eviction drains from the head, so youngest-first would strand the oldest rows forever.
2692		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		// Otherwise the evictor re-deletes a dead key forever and never advances past it.
2718		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		// Without this the index is unsound under UPDATE: a row rewritten inside its ttl still dies.
2732		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		// Otherwise catalog bytes read as a timestamp hand the evictor arbitrary rows to delete.
2749		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		// A candidate the evictor cannot remove must never stall every older row behind it.
2763		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		// A reaped row must vanish, not become a tombstone the reaper would then have to clear again.
2801		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		// The narrow series table names its key columns variant_tag, key and sequence. Every statement the
2888		// storage builds for it has to name those columns too: a single one left on the blob shape either
2889		// errors outright or, worse, matches nothing.
2890		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		// Only a batch past UPSERT_CHUNK reaches the chunked statements, which are the only ones that
2966		// RETURN their key columns. Those columns are read back positionally and rebuilt into an encoded
2967		// key, so a wrong column order or a wrong count here hands the caller keys for rows it never wrote.
2968		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		// The cursor forms of the key and expiry statements are the only ones that bind key columns after a
3006		// timestamp, so their placeholder numbering differs from every other statement. A cursor bound to
3007		// the wrong slot silently restarts the walk or skips a page of it.
3008		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		// When several rows carry the same stamp the cursor can only make progress on the key columns, so
3060		// the tie clause is the only thing keeping a page boundary inside a shared stamp from looping on the
3061		// same rows forever, or from stepping over the rest of them.
3062		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		// The partitioned narrow table carries two more key columns. If a bound or a parameter list drops
3098		// them, every partition collapses into one and a scan of one partition returns another's rows.
3099		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		// Series rows are reachable two ways: the generic byte path, which translates encoded bounds into
3142		// columns, and the typed path, which is handed narrow keys directly. Both run against the same
3143		// table, so a disagreement between them means one of the two is reading a different window.
3144		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		// A narrow series table also owns a column named `key`, so blob SQL against a narrow table returns
3222		// zero rows without erroring, and narrow SQL against a blob table errors on a missing column. Data
3223		// written before the narrow schema existed must therefore keep resolving to the blob schema.
3224		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(&current_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, &current_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		// Falling back to the blob schema here would make every read of that table return zero rows with no
3285		// error at all, which is the exact failure this whole probe exists to prevent.
3286		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		// Forward order visits the largest partition/row tuple first, so a real cursor is that tuple.
3502		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		// The row schema pins this tie-break; the partitioned schema did not, so reversing its cursor
3552		// predicate and its ORDER BY together passed every test while walking candidates backwards.
3553		// The evictor threads this cursor across ticks, so the index must hand back candidates in the
3554		// same order the key space has, or a candidate it cannot remove strands the rows behind it.
3555		// The two partitions straddle the sign bit, which is where the halves last disagreed.
3556		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			// xorshift64*, mimicking the scattered sign bits real xxh3_128 partition hashes produce.
3617			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		// The count-only pagination tests pass under a reversal that flips the ORDER BY and the cursor
3663		// predicate together, because every row is still reached, just backwards. Callers merge this
3664		// stream with the in-memory tiers on encoded-key order, so the narrow partitioned columns must
3665		// reproduce that order exactly, not merely reach every row. The partitions below straddle the
3666		// sign bit in both halves, so a half that inverts alone or not at all reorders the sequence.
3667		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		// A batch of 2 against 12 rows forces the cursor to carry the direction across six pages, so a
3715		// cursor predicate pointing the wrong way drops or repeats rows instead of paging cleanly.
3716		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		// Without the index this full-scans the live set on every batch, the exact cost it removes.
3741		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}