feat(smartdb): add operation log APIs, point-in-time revert support, and a web-based debug dashboard

This commit is contained in:
2026-04-02 17:02:03 +00:00
parent 943302f789
commit d34b8673e1
27 changed files with 3536 additions and 64 deletions

View File

@@ -18,5 +18,5 @@ pub use adapter::StorageAdapter;
pub use error::{StorageError, StorageResult};
pub use file::FileStorageAdapter;
pub use memory::MemoryStorageAdapter;
pub use oplog::{OpLog, OpLogEntry, OpType};
pub use oplog::{OpLog, OpLogEntry, OpLogStats, OpType};
pub use wal::{WalOp, WalRecord, WriteAheadLog};

View File

@@ -2,6 +2,8 @@
//!
//! The OpLog records every write operation so that changes can be replayed,
//! replicated, or used for change-stream style notifications.
//! Each entry stores both the new and previous document state, enabling
//! point-in-time replay and revert.
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
@@ -33,8 +35,21 @@ pub struct OpLogEntry {
pub collection: String,
/// Document id (hex string).
pub document_id: String,
/// The document snapshot (for insert/update; None for delete).
/// The new document snapshot (for insert/update; None for delete).
pub document: Option<Document>,
/// The previous document snapshot (for update/delete; None for insert).
pub previous_document: Option<Document>,
}
/// Aggregate statistics about the oplog.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpLogStats {
pub current_seq: u64,
pub total_entries: usize,
pub oldest_seq: u64,
pub inserts: usize,
pub updates: usize,
pub deletes: usize,
}
/// In-memory operation log.
@@ -61,6 +76,7 @@ impl OpLog {
collection: &str,
document_id: &str,
document: Option<Document>,
previous_document: Option<Document>,
) -> u64 {
let seq = self.next_seq.fetch_add(1, Ordering::SeqCst);
let entry = OpLogEntry {
@@ -74,11 +90,17 @@ impl OpLog {
collection: collection.to_string(),
document_id: document_id.to_string(),
document,
previous_document,
};
self.entries.insert(seq, entry);
seq
}
/// Get a single entry by sequence number.
pub fn get_entry(&self, seq: u64) -> Option<OpLogEntry> {
self.entries.get(&seq).map(|e| e.value().clone())
}
/// Get all entries with sequence number >= `since`.
pub fn entries_since(&self, since: u64) -> Vec<OpLogEntry> {
let mut result: Vec<_> = self
@@ -91,11 +113,72 @@ impl OpLog {
result
}
/// Get entries in range [from_seq, to_seq] inclusive, sorted by seq.
pub fn entries_range(&self, from_seq: u64, to_seq: u64) -> Vec<OpLogEntry> {
let mut result: Vec<_> = self
.entries
.iter()
.filter(|e| {
let k = *e.key();
k >= from_seq && k <= to_seq
})
.map(|e| e.value().clone())
.collect();
result.sort_by_key(|e| e.seq);
result
}
/// Remove all entries with seq > `after_seq` and reset the next_seq counter.
pub fn truncate_after(&self, after_seq: u64) {
let keys_to_remove: Vec<u64> = self
.entries
.iter()
.filter(|e| *e.key() > after_seq)
.map(|e| *e.key())
.collect();
for key in keys_to_remove {
self.entries.remove(&key);
}
self.next_seq.store(after_seq + 1, Ordering::SeqCst);
}
/// Get the current (latest) sequence number. Returns 0 if empty.
pub fn current_seq(&self) -> u64 {
self.next_seq.load(Ordering::SeqCst).saturating_sub(1)
}
/// Get aggregate statistics.
pub fn stats(&self) -> OpLogStats {
let mut inserts = 0usize;
let mut updates = 0usize;
let mut deletes = 0usize;
let mut oldest_seq = u64::MAX;
for entry in self.entries.iter() {
match entry.value().op {
OpType::Insert => inserts += 1,
OpType::Update => updates += 1,
OpType::Delete => deletes += 1,
}
if entry.value().seq < oldest_seq {
oldest_seq = entry.value().seq;
}
}
if oldest_seq == u64::MAX {
oldest_seq = 0;
}
OpLogStats {
current_seq: self.current_seq(),
total_entries: self.entries.len(),
oldest_seq,
inserts,
updates,
deletes,
}
}
/// Clear all entries.
pub fn clear(&self) {
self.entries.clear();