tidaldb/tidal/src/storage/error.rs
jordan 29400d48db feat: implement Milestone 1 phases 1-3 — schema, WAL, and storage layer
Implements the foundation of tidalDB's data pipeline:

**Phase 1 – Schema primitives**
- EntityId newtype (u64, big-endian ordering)
- SignalTypeDefinition with pre-computed decay λ, deduped/sorted windows
- SchemaBuilder with full constraint validation (duplicates, identifiers,
  half-life, windows, velocity)
- LumenError wrapping all subsystems with required From impls

**Phase 2 – Write-Ahead Log**
- Length-prefixed, BLAKE3-protected entry format
- Group-commit writer (batch up to 100 events / 10 ms)
- Double-buffered content-hash deduplication
- Checkpoint, truncation, and crash-recovery with full replay
- Integration, property, and UAT tests (incl. 5,500-event deterministic UAT)
- Proptest coverage scaled to 10 000 events/run (was ≤500) to meet
  acceptance criterion; cases reduced 100→10 to keep runtime comparable

**Phase 3 – Storage engine**
- StorageEngine trait (get/put/delete/scan/batch/flush)
- Key encoding: [EntityId][0x00][Tag][suffix] with ordering/prefix helpers
- InMemoryBackend (BTreeMap + RwLock)
- FjallStorage with three isolated keyspaces and atomic batch helper
- Property tests for key ordering and round-trip correctness

Also adds planning docs for phases 4-5, research docs, architecture
overview, and roadmap updates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 16:43:24 -07:00

100 lines
2.7 KiB
Rust

use std::fmt;
/// Storage engine error types.
///
/// Replaces the stub `StorageError { message }` from Phase 1.1.
/// All storage backends surface errors through this enum.
#[derive(Debug)]
pub enum StorageError {
/// I/O error from the underlying filesystem or storage engine.
Io(std::io::Error),
/// Data corruption detected (checksum mismatch, invalid key encoding, etc.).
Corruption { message: String },
/// The storage engine has been closed and cannot service requests.
Closed,
/// A batch write conflicted with a concurrent operation.
BatchConflict,
}
impl fmt::Display for StorageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(source) => write!(f, "I/O error: {source}"),
Self::Corruption { message } => write!(f, "data corruption: {message}"),
Self::Closed => f.write_str("storage closed"),
Self::BatchConflict => f.write_str("batch conflict"),
}
}
}
impl std::error::Error for StorageError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(source) => Some(source),
_ => None,
}
}
}
impl From<std::io::Error> for StorageError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_io() {
let e = StorageError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found",
));
assert!(e.to_string().contains("I/O error"));
assert!(e.to_string().contains("file not found"));
}
#[test]
fn display_corruption() {
let e = StorageError::Corruption {
message: "bad checksum".into(),
};
assert_eq!(e.to_string(), "data corruption: bad checksum");
}
#[test]
fn display_closed() {
assert_eq!(StorageError::Closed.to_string(), "storage closed");
}
#[test]
fn display_batch_conflict() {
assert_eq!(StorageError::BatchConflict.to_string(), "batch conflict");
}
#[test]
fn from_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::Other, "disk full");
let storage_err: StorageError = io_err.into();
assert!(matches!(storage_err, StorageError::Io(_)));
}
#[test]
fn source_io() {
use std::error::Error;
let e = StorageError::Io(std::io::Error::new(std::io::ErrorKind::Other, "test"));
assert!(e.source().is_some());
}
#[test]
fn source_corruption_is_none() {
use std::error::Error;
let e = StorageError::Corruption {
message: "test".into(),
};
assert!(e.source().is_none());
}
}