#![allow(dead_code)]
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use parking_lot::Mutex;
use rusqlite::{params, Connection};
use crate::backend::{clamped, Backend, ReadCursor, StagingCursor};
use crate::gc::{EngineWindowState, WindowCandidate, WINDOW_TAKE_BATCH};
use crate::Error;
fn db_err(e: rusqlite::Error) -> Error {
Error::Io(format!("sqlite: {e}"))
}
fn is_no_such_table(e: &rusqlite::Error) -> bool {
e.to_string().contains("no such table")
}
fn size_u64(raw: i64) -> u64 {
raw.max(0) as u64
}
fn time_to_nanos(t: SystemTime) -> i64 {
match t.duration_since(UNIX_EPOCH) {
Ok(d) => d.as_nanos() as i64,
Err(e) => -(e.duration().as_nanos() as i64),
}
}
fn nanos_to_time(n: i64) -> SystemTime {
if n >= 0 {
UNIX_EPOCH + std::time::Duration::from_nanos(n as u64)
} else {
UNIX_EPOCH - std::time::Duration::from_nanos((-n) as u64)
}
}
async fn blocking<F, T>(body: F) -> Result<T, Error>
where
F: FnOnce() -> Result<T, Error> + Send + 'static,
T: Send + 'static,
{
match tokio::task::spawn_blocking(body).await {
Ok(result) => result,
Err(e) => Err(Error::Io(format!("blocking task joint failure: {e}"))),
}
}
#[derive(Clone)]
pub(crate) struct SqliteBackend {
conn: Arc<Mutex<Connection>>,
#[cfg(test)]
fail_commit: bool,
#[cfg(test)]
fail_window_stage: bool,
}
impl SqliteBackend {
pub(crate) async fn new(path: impl Into<PathBuf>) -> Result<Self, Error> {
let path: Arc<PathBuf> = Arc::new(path.into());
let conn = blocking(move || {
let conn = Connection::open(&*path).map_err(db_err)?;
let mode: String = conn
.query_row("PRAGMA journal_mode = WAL", [], |row| row.get(0))
.map_err(db_err)?;
if !mode.eq_ignore_ascii_case("wal") {
return Err(Error::Io(format!(
"sqlite: journal_mode is {mode}, WAL unavailable"
)));
}
conn.execute_batch(
"PRAGMA synchronous = NORMAL;
PRAGMA busy_timeout = 5000;",
)
.map_err(db_err)?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS blobs (
key BLOB PRIMARY KEY,
value BLOB,
size INTEGER NOT NULL
);",
)
.map_err(db_err)?;
Ok(conn)
})
.await?;
Ok(Self {
conn: Arc::new(Mutex::new(conn)),
#[cfg(test)]
fail_commit: false,
#[cfg(test)]
fail_window_stage: false,
})
}
#[cfg(test)]
async fn new_tableless(path: impl Into<PathBuf>) -> Result<Self, Error> {
let path: Arc<PathBuf> = Arc::new(path.into());
let conn = blocking(move || Connection::open(&*path).map_err(db_err)).await?;
Ok(Self {
conn: Arc::new(Mutex::new(conn)),
#[cfg(test)]
fail_commit: false,
#[cfg(test)]
fail_window_stage: false,
})
}
#[cfg(test)]
pub(crate) async fn table_exists(&self, name: &str) -> Result<bool, Error> {
let conn = Arc::clone(&self.conn);
let name = name.to_owned();
blocking(move || {
let guard = conn.lock();
let probed: rusqlite::Result<bool> = guard
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?1",
params![name],
|row| row.get::<_, i64>(0),
)
.map(|count| count != 0);
probed.map_err(db_err)
})
.await
}
#[cfg(test)]
async fn pragma(&self, name: &str) -> Result<rusqlite::types::Value, Error> {
let conn = Arc::clone(&self.conn);
let name = name.to_owned();
blocking(move || {
let guard = conn.lock();
let value: rusqlite::types::Value = guard
.query_row(&format!("PRAGMA {name}"), [], |row| row.get(0))
.map_err(db_err)?;
Ok(value)
})
.await
}
}
pub(crate) struct SqliteCursor {
value: Vec<u8>,
len: u64,
}
impl ReadCursor for SqliteCursor {
fn len(&self) -> u64 {
self.len
}
async fn read_at(&self, offset: u64, len: u64) -> Result<Vec<u8>, Error> {
let (start, end) = clamped(offset, len, self.value.len());
Ok(self.value[start..end].to_vec())
}
}
pub(crate) struct SqliteStaging {
staged: Vec<u8>,
conn: Arc<Mutex<Connection>>,
#[cfg(test)]
fail_commit: bool,
}
impl ReadCursor for SqliteStaging {
fn len(&self) -> u64 {
self.staged.len() as u64
}
async fn read_at(&self, offset: u64, len: u64) -> Result<Vec<u8>, Error> {
let (start, end) = clamped(offset, len, self.staged.len());
Ok(self.staged[start..end].to_vec())
}
}
impl StagingCursor for SqliteStaging {
async fn append(&mut self, chunk: &[u8]) -> Result<(), Error> {
self.staged.extend_from_slice(chunk);
Ok(())
}
async fn commit(mut self, key: &[u8]) -> Result<(), Error> {
let value = std::mem::take(&mut self.staged);
let conn = Arc::clone(&self.conn);
let key = key.to_vec();
#[cfg(test)]
let fail_commit = self.fail_commit;
blocking(move || {
#[cfg(test)]
if fail_commit {
return Err(Error::Io(
"engine commit failure (sqlite, injected)".to_owned(),
));
}
let guard = conn.lock();
let size = value.len() as i64;
let written: rusqlite::Result<()> = (|| {
let mut stmt = guard.prepare_cached(
"INSERT OR IGNORE INTO blobs (key, value, size) VALUES (?1, ?2, ?3)",
)?;
stmt.execute(params![key, value, size])?;
Ok(())
})();
written.map_err(db_err)
})
.await
}
async fn discard(mut self) {
self.staged.clear();
}
}
impl Backend for SqliteBackend {
type Cursor = SqliteCursor;
type Staging = SqliteStaging;
fn name(&self) -> &'static str {
"sqlite"
}
async fn has(&self, key: &[u8]) -> Result<bool, Error> {
let conn = Arc::clone(&self.conn);
let key = key.to_vec();
blocking(move || {
let guard = conn.lock();
let probed: rusqlite::Result<bool> = (|| {
let mut stmt =
guard.prepare_cached("SELECT EXISTS(SELECT 1 FROM blobs WHERE key = ?1)")?;
Ok(stmt.query_row(params![key], |row| row.get::<_, i64>(0))? != 0)
})();
match probed {
Ok(present) => Ok(present),
Err(e) if is_no_such_table(&e) => Ok(false),
Err(e) => Err(db_err(e)),
}
})
.await
}
async fn get(&self, key: &[u8]) -> Result<Option<Self::Cursor>, Error> {
let conn = Arc::clone(&self.conn);
let key = key.to_vec();
blocking(move || {
let guard = conn.lock();
let loaded: rusqlite::Result<Option<SqliteCursor>> = (|| {
let mut stmt =
guard.prepare_cached("SELECT value, size FROM blobs WHERE key = ?1")?;
let row = stmt.query_row(params![key], |row| {
Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, i64>(1)?))
});
match row {
Ok((value, size)) => Ok(Some(SqliteCursor {
value,
len: size_u64(size),
})),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e),
}
})();
match loaded {
Ok(cursor) => Ok(cursor),
Err(e) if is_no_such_table(&e) => Ok(None),
Err(e) => Err(db_err(e)),
}
})
.await
}
async fn put(&self, key: &[u8], value: &[u8]) -> Result<(), Error> {
let conn = Arc::clone(&self.conn);
let key = key.to_vec();
let value = value.to_vec();
let size = value.len() as i64;
blocking(move || {
let guard = conn.lock();
let written: rusqlite::Result<()> = (|| {
let mut stmt = guard.prepare_cached(
"INSERT OR IGNORE INTO blobs (key, value, size) VALUES (?1, ?2, ?3)",
)?;
stmt.execute(params![key, value, size])?;
Ok(())
})();
written.map_err(db_err)
})
.await
}
async fn put_staged(&self) -> Result<Self::Staging, Error> {
Ok(SqliteStaging {
staged: Vec::new(),
conn: Arc::clone(&self.conn),
#[cfg(test)]
fail_commit: self.fail_commit,
})
}
async fn delete(&self, key: &[u8]) -> Result<(), Error> {
let conn = Arc::clone(&self.conn);
let key = key.to_vec();
blocking(move || {
let guard = conn.lock();
let removed: rusqlite::Result<usize> = (|| {
let mut stmt = guard.prepare_cached("DELETE FROM blobs WHERE key = ?1")?;
stmt.execute(params![key])
})();
match removed {
Ok(0) => Err(Error::Missing),
Ok(_) => Ok(()),
Err(e) if is_no_such_table(&e) => Err(Error::Missing),
Err(e) => Err(db_err(e)),
}
})
.await
}
async fn list(&self) -> Result<Vec<Vec<u8>>, Error> {
let conn = Arc::clone(&self.conn);
blocking(move || {
let guard = conn.lock();
let listed: rusqlite::Result<Vec<Vec<u8>>> = (|| {
let mut stmt = guard.prepare_cached("SELECT key FROM blobs")?;
let rows = stmt.query_map([], |row| row.get::<_, Vec<u8>>(0))?;
let mut keys = Vec::new();
for row in rows {
keys.push(row?);
}
Ok(keys)
})();
match listed {
Ok(keys) => Ok(keys),
Err(e) if is_no_such_table(&e) => Ok(Vec::new()),
Err(e) => Err(db_err(e)),
}
})
.await
}
async fn size(&self, key: &[u8]) -> Result<Option<u64>, Error> {
let conn = Arc::clone(&self.conn);
let key = key.to_vec();
blocking(move || {
let guard = conn.lock();
let probed: rusqlite::Result<Option<i64>> = (|| {
let mut stmt = guard.prepare_cached("SELECT size FROM blobs WHERE key = ?1")?;
let row = stmt.query_row(params![key], |row| row.get::<_, i64>(0));
match row {
Ok(size) => Ok(Some(size)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e),
}
})();
match probed {
Ok(size) => Ok(size.map(size_u64)),
Err(e) if is_no_such_table(&e) => Ok(None),
Err(e) => Err(db_err(e)),
}
})
.await
}
}
impl EngineWindowState for SqliteBackend {
async fn ensure_gc_schema(&self) -> Result<(), Error> {
let conn = Arc::clone(&self.conn);
blocking(move || {
let guard = conn.lock();
guard
.execute_batch(
"CREATE TABLE IF NOT EXISTS gc_window (
key BLOB PRIMARY KEY,
visible_after INTEGER
);",
)
.map_err(db_err)
})
.await
}
async fn window_stage(&self, candidates: &[WindowCandidate]) -> Result<(), Error> {
if candidates.is_empty() {
return Ok(());
}
let conn = Arc::clone(&self.conn);
#[cfg(test)]
let fail_window_stage = self.fail_window_stage;
let rows: Vec<(Vec<u8>, i64)> = candidates
.iter()
.map(|c| (c.key.clone(), time_to_nanos(c.visible_after)))
.collect();
blocking(move || {
let mut guard = conn.lock();
let staged: Result<(), Error> = (|| {
let tx = guard.transaction().map_err(db_err)?;
{
let mut stmt = tx
.prepare_cached(
"INSERT INTO gc_window (key, visible_after) VALUES (?1, ?2) \
ON CONFLICT(key) DO UPDATE SET \
visible_after = max(visible_after, excluded.visible_after)",
)
.map_err(db_err)?;
for (key, visible_after) in rows.iter() {
#[cfg(test)]
if fail_window_stage && rows.last().is_some_and(|(last, _)| last == key) {
return Err(Error::Io(
"sqlite: window staging failure (mid-loop, injected)".to_owned(),
));
}
stmt.execute(params![key, visible_after]).map_err(db_err)?;
}
}
tx.commit().map_err(db_err)
})();
staged
})
.await
}
async fn window_take(&self, now: SystemTime) -> Result<Vec<Vec<u8>>, Error> {
let conn = Arc::clone(&self.conn);
let horizon = time_to_nanos(now);
blocking(move || {
let guard = conn.lock();
let taken: rusqlite::Result<Vec<Vec<u8>>> = (|| {
let mut stmt = guard.prepare_cached(
"DELETE FROM gc_window WHERE key IN ( \
SELECT key FROM gc_window WHERE visible_after <= ?1 \
ORDER BY visible_after LIMIT ?2 \
) RETURNING key",
)?;
let rows = stmt.query_map(params![horizon, WINDOW_TAKE_BATCH as i64], |row| {
row.get::<_, Vec<u8>>(0)
})?;
let mut keys = Vec::new();
for row in rows {
keys.push(row?);
}
Ok(keys)
})();
match taken {
Ok(mut keys) => {
keys.sort();
Ok(keys)
}
Err(e) if is_no_such_table(&e) => Ok(Vec::new()),
Err(e) => Err(db_err(e)),
}
})
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
const CONTENT: &[u8] = b"the quick brown fox jumps over the lazy dog";
struct TempDb(PathBuf);
fn temp_db(tag: &str) -> TempDb {
static SEQ: AtomicU64 = AtomicU64::new(0);
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!(
"alkblobs-sqlite-{tag}-{}-{nanos:x}-{seq}.db",
std::process::id()
));
TempDb(path)
}
impl TempDb {
fn path(&self) -> &Path {
&self.0
}
fn drop_sidecar(&self, suffix: &str) {
let base = self.0.to_string_lossy().into_owned();
let _ = std::fs::remove_file(format!("{base}{suffix}"));
}
}
impl Drop for TempDb {
fn drop(&mut self) {
self.drop_sidecar("-wal");
self.drop_sidecar("-shm");
let _ = std::fs::remove_file(&self.0);
}
}
async fn new_backend() -> (TempDb, SqliteBackend) {
let db = temp_db("engine");
let backend = SqliteBackend::new(db.path()).await.unwrap();
(db, backend)
}
async fn new_window_backend() -> (TempDb, SqliteBackend) {
let (db, backend) = new_backend().await;
backend.ensure_gc_schema().await.unwrap();
(db, backend)
}
async fn stored_value(backend: &SqliteBackend, key: &[u8]) -> Option<Vec<u8>> {
let cursor = backend.get(key).await.expect("get succeeds")?;
let (bytes, _) = cursor.read_range(0, cursor.len()).await.expect("read");
Some(bytes)
}
async fn staged(backend: &SqliteBackend, key: &[u8], content: &[u8]) -> Result<(), Error> {
let mut staging = backend.put_staged().await?;
staging.append(content).await?;
staging.commit(key).await
}
#[tokio::test]
async fn backend_method_contract() {
let (_db, backend) = new_backend().await;
let key = b"\x01opaque-key-bytes".to_vec();
assert_eq!(backend.name(), "sqlite", "reporting identity");
assert!(
!backend.has(&key).await.unwrap(),
"virgin store: has is a no-op read"
);
assert!(
backend.get(&key).await.unwrap().is_none(),
"virgin store: get is a no-op read"
);
assert_eq!(
backend.size(&key).await.unwrap(),
None,
"virgin store: size is a no-op read"
);
assert!(
backend.list().await.unwrap().is_empty(),
"virgin store: list is empty"
);
assert_eq!(
backend.delete(&key).await.unwrap_err(),
Error::Missing,
"virgin store: delete reports Missing"
);
backend.put(&key, CONTENT).await.unwrap();
assert!(backend.has(&key).await.unwrap(), "put lands");
assert_eq!(
backend.size(&key).await.unwrap(),
Some(CONTENT.len() as u64),
"size is metadata-only and exact"
);
backend.put(&key, b"overwritten").await.unwrap();
assert_eq!(
stored_value(&backend, &key).await.as_deref(),
Some(CONTENT),
"CAS first-writer wins: re-put is a no-op"
);
assert_eq!(
backend.list().await.unwrap(),
vec![key.clone()],
"list is complete, keys not values"
);
backend.delete(&key).await.unwrap();
assert!(!backend.has(&key).await.unwrap(), "delete lands");
assert_eq!(
backend.delete(&key).await.unwrap_err(),
Error::Missing,
"delete reports Missing when absent"
);
}
#[tokio::test]
async fn list_is_complete_and_well_formed() {
let (_db, backend) = new_backend().await;
let keys = [
b"\x02b-key".to_vec(),
b"\x02a-key".to_vec(),
b"\x02c-key".to_vec(),
];
for key in &keys {
backend.put(key, b"value").await.unwrap();
}
let mut listed = backend.list().await.unwrap();
let mut expected = keys.to_vec();
listed.sort();
expected.sort();
assert_eq!(listed, expected, "list is complete and well-formed");
for key in &expected {
assert!(
backend.has(key).await.unwrap(),
"no spurious or mangled keys"
);
}
}
#[tokio::test]
async fn cursor_contract_materialized_body() {
let (_db, backend) = new_backend().await;
let key = b"\x01cursor-key".to_vec();
backend.put(&key, CONTENT).await.unwrap();
let cursor = backend
.get(&key)
.await
.expect("get succeeds")
.expect("entry present");
let len = CONTENT.len() as u64;
assert_eq!(cursor.len(), len, "len is the entry length");
assert_eq!(cursor.read_at(0, len).await.unwrap(), CONTENT, "full read");
assert_eq!(
cursor.read_at(0, len + 17).await.unwrap(),
CONTENT,
"over-read clamps at EOF, pread-style"
);
let mid = (CONTENT.len() / 2) as u64;
assert_eq!(
cursor.read_at(mid, len).await.unwrap(),
&CONTENT[mid as usize..],
"tail read clamps"
);
assert_eq!(
cursor.read_at(len, 5).await.unwrap(),
Vec::<u8>::new(),
"read at EOF is empty, never an error"
);
assert_eq!(
cursor.read_at(0, 0).await.unwrap(),
Vec::<u8>::new(),
"zero-length read"
);
let (slice, digest) = cursor.read_range(mid, 7).await.unwrap();
assert_eq!(slice, &CONTENT[mid as usize..(mid as usize + 7)]);
assert_eq!(digest, crate::backend::slice_digest(&slice));
}
#[tokio::test]
async fn empty_entry_round_trips() {
let (_db, backend) = new_backend().await;
let key = b"\x01empty-key".to_vec();
backend.put(&key, b"").await.unwrap();
assert!(backend.has(&key).await.unwrap(), "empty value still lands");
assert_eq!(backend.size(&key).await.unwrap(), Some(0), "exact zero");
let cursor = backend.get(&key).await.unwrap().expect("entry present");
assert_eq!(cursor.len(), 0);
assert_eq!(cursor.read_at(0, 8).await.unwrap(), Vec::<u8>::new());
let staged_key = b"\x01empty-staged-key".to_vec();
let mut staging = backend.put_staged().await.unwrap();
staging.append(b"").await.unwrap();
assert_eq!(staging.len(), 0, "empty append stays empty");
staging
.commit(&staged_key)
.await
.expect("empty commit publishes");
assert!(
backend.has(&staged_key).await.unwrap(),
"empty entry landed"
);
assert_eq!(backend.size(&staged_key).await.unwrap(), Some(0));
let committed = backend
.get(&staged_key)
.await
.expect("get succeeds")
.expect("entry present");
assert_eq!(
committed.read_at(0, 8).await.unwrap(),
Vec::<u8>::new(),
"committed empty body reads empty"
);
}
#[tokio::test]
async fn staged_put_round_trips_through_the_read_seam() {
let (_db, backend) = new_backend().await;
let key = b"\x01staged-key".to_vec();
let mut staging = backend.put_staged().await.unwrap();
staging.append(&CONTENT[..8]).await.unwrap();
staging.append(&CONTENT[8..16]).await.unwrap();
staging.append(&CONTENT[16..]).await.unwrap();
assert_eq!(
staging.len() as usize,
CONTENT.len(),
"staged length is the appended length"
);
let (bytes, digest) = staging
.read_range(0, staging.len())
.await
.expect("staged bytes readable");
assert_eq!(bytes, CONTENT, "staged artifact readable as a whole");
assert_eq!(digest, crate::backend::slice_digest(CONTENT));
staging.commit(&key).await.expect("commit publishes");
assert!(backend.has(&key).await.unwrap(), "entry published");
let cursor = backend
.get(&key)
.await
.expect("get succeeds")
.expect("entry present");
let (bytes, digest) = cursor
.read_range(0, cursor.len())
.await
.expect("committed read");
assert_eq!(bytes, CONTENT);
assert_eq!(digest, crate::backend::slice_digest(CONTENT));
backend.put(&key, b"overwritten").await.unwrap();
assert_eq!(
stored_value(&backend, &key).await.as_deref(),
Some(CONTENT),
"whole-value re-put does not overwrite"
);
staged(&backend, &key, b"overwritten-by-stage")
.await
.expect("staged re-commit is dedup, not failure");
assert_eq!(
stored_value(&backend, &key).await.as_deref(),
Some(CONTENT),
"staged re-commit does not overwrite"
);
}
#[tokio::test]
async fn staged_discard_leaves_zero_residue() {
let (_db, backend) = new_backend().await;
let key = b"\x01discard-key".to_vec();
let mut staging = backend.put_staged().await.unwrap();
staging.append(b"residue").await.unwrap();
staging.discard().await;
assert!(
!backend.has(&key).await.unwrap(),
"discarded stage published nothing"
);
assert_eq!(backend.size(&key).await.unwrap(), None, "no entry landed");
assert!(
backend.list().await.unwrap().is_empty(),
"the table holds exactly zero entries after discard"
);
}
#[tokio::test]
async fn commit_failure_self_discards() {
let (_db, backend) = new_backend().await;
let failing = SqliteBackend {
conn: Arc::clone(&backend.conn),
#[cfg(test)]
fail_commit: true,
#[cfg(test)]
fail_window_stage: false,
};
let key = b"\x01failure-key".to_vec();
let mut staging = failing.put_staged().await.unwrap();
staging.append(CONTENT).await.unwrap();
let err = staging.commit(&key).await.unwrap_err();
assert!(matches!(err, Error::Io(_)), "engine media failure");
assert!(
!backend.has(&key).await.unwrap(),
"failed commit published nothing"
);
assert_eq!(backend.size(&key).await.unwrap(), None, "zero residue");
assert!(
backend.list().await.unwrap().is_empty(),
"zero residue in the table itself"
);
}
#[tokio::test]
async fn opens_with_wal_synchronous_normal_and_busy_timeout() {
let (_db, backend) = new_backend().await;
match backend.pragma("journal_mode").await.unwrap() {
rusqlite::types::Value::Text(mode) => {
assert_eq!(mode.to_lowercase(), "wal", "journal_mode is WAL");
}
other => panic!("journal_mode probe returned {other:?}"),
}
match backend.pragma("synchronous").await.unwrap() {
rusqlite::types::Value::Integer(sync) => {
assert_eq!(sync, 1, "synchronous is NORMAL (1)");
}
other => panic!("synchronous probe returned {other:?}"),
}
match backend.pragma("busy_timeout").await.unwrap() {
rusqlite::types::Value::Integer(timeout) => {
assert_eq!(
timeout, 5000,
"busy_timeout is the pinned 5000 ms contention posture"
);
}
other => panic!("busy_timeout probe returned {other:?}"),
}
}
#[tokio::test]
async fn construction_rejects_a_database_that_cannot_hold_wal() {
let opened = SqliteBackend::new(":memory:").await;
assert!(
opened.is_err(),
"in-memory databases cannot hold the WAL posture"
);
}
#[tokio::test]
async fn size_probe_is_exact_and_rides_the_key_index() {
let (_db, backend) = new_backend().await;
let key = b"\x01size-probe-key".to_vec();
let large_value = vec![0xAB_u8; 256 * 1024];
backend.put(&key, &large_value).await.unwrap();
assert_eq!(
backend.size(&key).await.unwrap(),
Some(large_value.len() as u64),
"size is metadata-only and exact"
);
assert_eq!(backend.size(b"\x01absent").await.unwrap(), None);
let conn = Arc::clone(&backend.conn);
let plan = blocking(move || {
let guard = conn.lock();
let planned: rusqlite::Result<Vec<String>> = (|| {
let mut stmt = guard
.prepare_cached("EXPLAIN QUERY PLAN SELECT size FROM blobs WHERE key = ?1")?;
let rows = stmt.query_map(params![key], |row| row.get::<_, String>(3))?;
let mut details = Vec::new();
for row in rows {
details.push(row?);
}
Ok(details)
})();
planned.map_err(db_err)
})
.await
.unwrap();
let joined = plan.join(" ");
assert!(
joined.contains("USING INDEX"),
"the size probe searches the key's index: {joined}"
);
}
#[tokio::test]
async fn virgin_store_no_tables_yet_reads_are_noops() {
let db = temp_db("tableless");
let backend = SqliteBackend::new_tableless(db.path()).await.unwrap();
let key = b"\x01opaque-key".to_vec();
assert!(
!backend.has(&key).await.unwrap(),
"has on a tableless db is a no-op read"
);
assert!(
backend.get(&key).await.unwrap().is_none(),
"get is a no-op read"
);
assert_eq!(
backend.size(&key).await.unwrap(),
None,
"size is a no-op read"
);
assert!(
backend.list().await.unwrap().is_empty(),
"list is empty on a tableless db"
);
assert_eq!(
backend.delete(&key).await.unwrap_err(),
Error::Missing,
"delete on a tableless db reports Missing"
);
assert!(
backend.put(&key, CONTENT).await.is_err(),
"the tolerance is read-only: writes surface the engine error"
);
assert!(
backend.list().await.unwrap().is_empty(),
"the failed write left zero residue"
);
}
fn at(nanos: i64) -> SystemTime {
nanos_to_time(nanos)
}
async fn window_rows(backend: &SqliteBackend) -> Vec<(Vec<u8>, i64)> {
let conn = Arc::clone(&backend.conn);
blocking(move || {
let guard = conn.lock();
let listed: rusqlite::Result<Vec<(Vec<u8>, i64)>> = (|| {
let mut stmt = guard
.prepare_cached("SELECT key, visible_after FROM gc_window")
.unwrap();
let rows = stmt.query_map([], |row| {
Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, i64>(1)?))
})?;
let mut out: Vec<(Vec<u8>, i64)> = rows.collect::<Result<_, _>>()?;
out.sort();
Ok(out)
})();
listed.map_err(db_err)
})
.await
.unwrap()
}
async fn table_exists(backend: &SqliteBackend, name: &str) -> bool {
backend.table_exists(name).await.unwrap()
}
fn stages(key: &[u8], visible_after: SystemTime) -> WindowCandidate {
WindowCandidate {
key: key.to_vec(),
visible_after,
}
}
#[tokio::test]
async fn gc_schema_creates_window_table_only() {
let (_db, backend) = new_backend().await;
backend.ensure_gc_schema().await.unwrap();
backend.ensure_gc_schema().await.unwrap();
assert!(table_exists(&backend, "gc_window").await, "window table");
assert!(
!table_exists(&backend, "gc_pins").await,
"gc_pins is never created by sqlite (dead pin machinery dropped)"
);
let db = temp_db("tableless-window");
let tableless = SqliteBackend::new_tableless(db.path()).await.unwrap();
assert!(!table_exists(&tableless, "gc_window").await);
tableless.ensure_gc_schema().await.unwrap();
assert!(table_exists(&tableless, "gc_window").await);
assert!(!table_exists(&tableless, "gc_pins").await);
}
#[tokio::test]
async fn window_stage_is_idempotent_and_restage_moves_the_deadline_forward_only() {
let (_db, backend) = new_window_backend().await;
let key = b"\x01window-key".to_vec();
let deadline = at(1_000_000_000);
backend
.window_stage(&[stages(&key, deadline)])
.await
.unwrap();
backend
.window_stage(&[stages(&key, deadline)])
.await
.unwrap();
assert_eq!(
window_rows(&backend).await,
vec![(key.clone(), 1_000_000_000)],
"re-stage at the same deadline is one idempotent row"
);
backend
.window_stage(&[stages(&key, at(2_000_000_000))])
.await
.unwrap();
assert_eq!(
window_rows(&backend).await,
vec![(key.clone(), 2_000_000_000)],
"a re-stage at a later deadline moves the row forward"
);
backend
.window_stage(&[stages(&key, at(1_000_000_000))])
.await
.unwrap();
assert_eq!(
window_rows(&backend).await,
vec![(key.clone(), 2_000_000_000)],
"a re-stage at an earlier deadline never moves visible_after backwards"
);
let other = b"\x01window-other".to_vec();
backend
.window_stage(&[stages(&other, deadline)])
.await
.unwrap();
assert_eq!(
window_rows(&backend).await.len(),
2,
"distinct keys are distinct rows"
);
}
#[tokio::test]
async fn window_stage_mid_loop_failure_stages_nothing() {
let (_db, mut backend) = new_window_backend().await;
let prior = b"\x01window-prior".to_vec();
backend
.window_stage(&[stages(&prior, at(1_000_000_000))])
.await
.unwrap();
backend.fail_window_stage = true;
let aborted_a = b"\x01window-aborted-a".to_vec();
let aborted_b = b"\x01window-aborted-b".to_vec();
let err = backend
.window_stage(&[
stages(&aborted_a, at(2_000_000_000)),
stages(&aborted_b, at(2_000_000_000)),
])
.await
.unwrap_err();
assert!(
matches!(err, Error::Io(ref m) if m.contains("mid-loop")),
"the injected stage failure surfaces as Io: {err}"
);
assert_eq!(
window_rows(&backend).await,
vec![(prior.clone(), 1_000_000_000)],
"the aborted call staged nothing — the prior row is the only residue"
);
backend.fail_window_stage = false;
backend
.window_stage(&[
stages(&aborted_a, at(2_000_000_000)),
stages(&aborted_b, at(2_000_000_000)),
])
.await
.unwrap();
assert_eq!(
window_rows(&backend).await.len(),
3,
"the cleared backend stages cleanly again"
);
}
#[tokio::test]
async fn window_take_is_bounded_across_the_overdue_backlog() {
let (_db, backend) = new_window_backend().await;
let total = WINDOW_TAKE_BATCH + 7;
let candidates: Vec<WindowCandidate> = (0..total)
.map(|i| {
stages(
format!("\x01win-backlog-{i:05}").as_bytes(),
at(i as i64 * 1_000_000),
)
})
.collect();
backend.window_stage(&candidates).await.unwrap();
let first = backend
.window_take(at(total as i64 * 1_000_000))
.await
.unwrap();
let mut expected_first: Vec<Vec<u8>> = (0..WINDOW_TAKE_BATCH)
.map(|i| format!("\x01win-backlog-{i:05}").into_bytes())
.collect();
expected_first.sort();
assert_eq!(
first, expected_first,
"the first take hands out exactly the batch, oldest deadlines first"
);
let second = backend
.window_take(at(total as i64 * 1_000_000))
.await
.unwrap();
let mut expected_rest: Vec<Vec<u8>> = (WINDOW_TAKE_BATCH..total)
.map(|i| format!("\x01win-backlog-{i:05}").into_bytes())
.collect();
expected_rest.sort();
assert_eq!(second, expected_rest, "the next take drains the remainder");
assert_eq!(
backend
.window_take(at(total as i64 * 1_000_000))
.await
.unwrap(),
Vec::<Vec<u8>>::new(),
"the drained window takes empty"
);
}
#[tokio::test]
async fn window_take_returns_the_overdue_set_and_leaves_future_rows() {
let (_db, backend) = new_window_backend().await;
let past_pre_epoch = b"\x01win-past-pre-epoch".to_vec();
let past = b"\x01win-past".to_vec();
let future = b"\x01win-future".to_vec();
let far_future = b"\x01win-far-future".to_vec();
backend
.window_stage(&[
stages(&past_pre_epoch, at(-1_000_000_000)),
stages(&past, at(1_000_000_000)),
stages(&future, at(2_000_000_000)),
stages(&far_future, at(3_600_000_000_000)),
])
.await
.unwrap();
let taken = backend.window_take(at(1_000_000_001)).await.unwrap();
let mut expected = vec![past_pre_epoch.clone(), past.clone()];
expected.sort();
assert_eq!(taken, expected, "exactly the overdue set");
assert_eq!(
backend.window_take(at(1_000_000_001)).await.unwrap(),
Vec::<Vec<u8>>::new(),
"the rows left the table: never handed out twice"
);
assert_eq!(
window_rows(&backend).await,
vec![
(far_future.clone(), 3_600_000_000_000),
(future.clone(), 2_000_000_000)
],
"future rows left in place"
);
}
#[tokio::test]
async fn window_take_boundary_visible_after_equal_now_is_overdue() {
let (_db, backend) = new_window_backend().await;
let equal = b"\x01win-equal-boundary".to_vec();
let just_later = b"\x01win-nanosecond-later".to_vec();
backend
.window_stage(&[
stages(&equal, at(1_500_000_000)),
stages(&just_later, at(1_500_000_001)),
])
.await
.unwrap();
assert_eq!(
backend.window_take(at(1_500_000_000)).await.unwrap(),
vec![equal.clone()],
"visible_after == now is overdue (the `<=` boundary)"
);
assert_eq!(
backend.window_take(at(1_500_000_000)).await.unwrap(),
Vec::<Vec<u8>>::new(),
"the not-yet-overdue row stays (one nanosecond of slack)"
);
assert_eq!(
backend.window_take(at(1_500_000_001)).await.unwrap(),
vec![just_later.clone()],
"the horizon itself moved, so the row is now overdue"
);
}
#[tokio::test]
async fn window_virgin_store_shapes() {
let db = temp_db("tableless-window-take");
let tableless = SqliteBackend::new_tableless(db.path()).await.unwrap();
let key = b"\x01virgin-window-key".to_vec();
assert_eq!(
tableless.window_take(at(1_000_000_000)).await.unwrap(),
Vec::<Vec<u8>>::new(),
"tableless take is a no-op read"
);
assert_eq!(
tableless.window_take(at(1_000_000_000)).await.unwrap(),
Vec::<Vec<u8>>::new(),
"still empty on a repeat"
);
let err = tableless.window_stage(&[stages(&key, at(1))]).await;
match err {
Err(Error::Io(message)) => {
assert!(
message.contains("no such table"),
"the write surfaces the engine error, not a silent schema create"
)
}
other => panic!("tableless stage is the engine error, got {other:?}"),
}
let (_db, backend) = new_window_backend().await;
assert_eq!(
backend.window_take(SystemTime::now()).await.unwrap(),
Vec::<Vec<u8>>::new(),
"empty window table takes empty"
);
}
}