Skip to main content

ipfrs_storage/
wal.rs

1//! Write-Ahead Log (WAL) for crash-recovery of block store writes.
2//!
3//! Operations are written to a sequential log before being applied,
4//! so interrupted writes can be replayed on restart.
5
6use std::collections::VecDeque;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Mutex;
9
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12
13// ---------------------------------------------------------------------------
14// FNV-1a 32-bit hash
15// ---------------------------------------------------------------------------
16
17/// Computes FNV-1a 32-bit hash of `data`.
18pub fn fnv1a_32(data: &[u8]) -> u32 {
19    const OFFSET_BASIS: u32 = 2_166_136_261;
20    const PRIME: u32 = 16_777_619;
21
22    let mut hash = OFFSET_BASIS;
23    for &byte in data {
24        hash ^= u32::from(byte);
25        hash = hash.wrapping_mul(PRIME);
26    }
27    hash
28}
29
30// ---------------------------------------------------------------------------
31// Errors
32// ---------------------------------------------------------------------------
33
34/// Errors that can occur during WAL operations.
35#[derive(Debug, Error)]
36pub enum WalError {
37    /// The WAL has reached its maximum entry capacity.
38    #[error("WAL capacity exceeded: current={current}, max={max}")]
39    CapacityExceeded { current: usize, max: usize },
40
41    /// A checkpoint operation failed.
42    #[error("Checkpoint failed: {0}")]
43    CheckpointFailed(String),
44
45    /// Serialization or deserialization error.
46    #[error("Serialization error: {0}")]
47    Serialization(String),
48}
49
50// ---------------------------------------------------------------------------
51// WalOp
52// ---------------------------------------------------------------------------
53
54/// A WAL operation that can be recorded.
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub enum WalOp {
57    /// Put a single block by CID.
58    Put {
59        /// Content identifier of the block.
60        cid: String,
61        /// Length of the data in bytes.
62        data_len: u64,
63    },
64    /// Delete a single block by CID.
65    Delete {
66        /// Content identifier of the block to delete.
67        cid: String,
68    },
69    /// Put a batch of blocks.
70    BatchPut {
71        /// Content identifiers of the blocks.
72        cids: Vec<String>,
73        /// Total bytes in the batch.
74        total_bytes: u64,
75    },
76    /// A checkpoint marking a stable recovery point.
77    Checkpoint {
78        /// Sequence number up to which all entries are checkpointed.
79        sequence: u64,
80    },
81}
82
83impl WalOp {
84    /// Serializes the op to bytes for CRC computation.
85    fn to_bytes_for_crc(&self) -> Result<Vec<u8>, WalError> {
86        serde_json::to_vec(self).map_err(|e| WalError::Serialization(e.to_string()))
87    }
88}
89
90// ---------------------------------------------------------------------------
91// WalEntry
92// ---------------------------------------------------------------------------
93
94/// A single entry in the write-ahead log.
95#[derive(Debug, Clone)]
96pub struct WalEntry {
97    /// Monotonically increasing sequence number.
98    pub sequence: u64,
99    /// The operation recorded.
100    pub op: WalOp,
101    /// CRC-32 (FNV-1a) of the serialized op bytes.
102    pub crc32: u32,
103    /// Unix timestamp in milliseconds when the entry was created.
104    pub timestamp_ms: u64,
105}
106
107// ---------------------------------------------------------------------------
108// WalStats
109// ---------------------------------------------------------------------------
110
111/// Atomic statistics for a [`WriteAheadLog`].
112#[derive(Debug, Default)]
113pub struct WalStats {
114    /// Total number of entries ever appended.
115    pub total_appended: AtomicU64,
116    /// Total number of checkpoints ever taken.
117    pub total_checkpoints: AtomicU64,
118    /// Total number of entries ever truncated.
119    pub total_truncated: AtomicU64,
120    /// Total number of entries ever returned by replay.
121    pub total_replayed: AtomicU64,
122}
123
124/// A point-in-time snapshot of [`WalStats`].
125#[derive(Debug, Clone, PartialEq)]
126pub struct WalStatsSnapshot {
127    pub total_appended: u64,
128    pub total_checkpoints: u64,
129    pub total_truncated: u64,
130    pub total_replayed: u64,
131}
132
133impl WalStats {
134    /// Returns a consistent point-in-time snapshot of the stats.
135    pub fn snapshot(&self) -> WalStatsSnapshot {
136        WalStatsSnapshot {
137            total_appended: self.total_appended.load(Ordering::Relaxed),
138            total_checkpoints: self.total_checkpoints.load(Ordering::Relaxed),
139            total_truncated: self.total_truncated.load(Ordering::Relaxed),
140            total_replayed: self.total_replayed.load(Ordering::Relaxed),
141        }
142    }
143}
144
145// ---------------------------------------------------------------------------
146// WriteAheadLog
147// ---------------------------------------------------------------------------
148
149/// In-memory write-ahead log providing crash-recovery for block store writes.
150///
151/// # Example
152/// ```
153/// use ipfrs_storage::wal::{WriteAheadLog, WalOp};
154///
155/// let wal = WriteAheadLog::new(1000);
156/// let seq = wal.append(WalOp::Put { cid: "bafytest".into(), data_len: 128 }).unwrap();
157/// assert_eq!(seq, 1);
158/// let ops = wal.replay_ops();
159/// assert_eq!(ops.len(), 1);
160/// ```
161pub struct WriteAheadLog {
162    entries: Mutex<Vec<WalEntry>>,
163    next_sequence: AtomicU64,
164    checkpoint_sequence: AtomicU64,
165    /// Maximum number of entries allowed in the log before truncation is required.
166    pub max_entries: usize,
167    /// Live statistics.
168    pub stats: WalStats,
169}
170
171impl WriteAheadLog {
172    /// Creates a new [`WriteAheadLog`] with the given maximum entry count.
173    pub fn new(max_entries: usize) -> Self {
174        Self {
175            entries: Mutex::new(Vec::new()),
176            next_sequence: AtomicU64::new(1),
177            checkpoint_sequence: AtomicU64::new(0),
178            max_entries,
179            stats: WalStats::default(),
180        }
181    }
182
183    /// Creates a [`WriteAheadLog`] with the default maximum of 10 000 entries.
184    pub fn with_defaults() -> Self {
185        Self::new(10_000)
186    }
187
188    /// Returns the current timestamp in milliseconds since the Unix epoch.
189    fn now_ms() -> u64 {
190        use std::time::{SystemTime, UNIX_EPOCH};
191        SystemTime::now()
192            .duration_since(UNIX_EPOCH)
193            .unwrap_or_default()
194            .as_millis() as u64
195    }
196
197    /// Appends an operation to the WAL.
198    ///
199    /// Assigns the next sequence number, computes the CRC-32 (FNV-1a) of the
200    /// serialized op, and stores the entry.  Returns the assigned sequence number.
201    ///
202    /// # Errors
203    /// Returns [`WalError::CapacityExceeded`] when the number of stored entries
204    /// already equals or exceeds `max_entries`.
205    pub fn append(&self, op: WalOp) -> Result<u64, WalError> {
206        let mut entries = self
207            .entries
208            .lock()
209            .map_err(|_| WalError::CheckpointFailed("lock poisoned during append".into()))?;
210
211        let current = entries.len();
212        if current >= self.max_entries {
213            return Err(WalError::CapacityExceeded {
214                current,
215                max: self.max_entries,
216            });
217        }
218
219        let crc32 = {
220            let bytes = op.to_bytes_for_crc()?;
221            fnv1a_32(&bytes)
222        };
223
224        let sequence = self.next_sequence.fetch_add(1, Ordering::SeqCst);
225
226        entries.push(WalEntry {
227            sequence,
228            op,
229            crc32,
230            timestamp_ms: Self::now_ms(),
231        });
232
233        self.stats.total_appended.fetch_add(1, Ordering::Relaxed);
234        Ok(sequence)
235    }
236
237    /// Appends a `WalOp::Checkpoint` entry and updates `checkpoint_sequence`.
238    ///
239    /// Returns the sequence number of the checkpoint entry.
240    pub fn checkpoint(&self) -> Result<u64, WalError> {
241        // Determine the sequence the checkpoint covers — it will be the sequence
242        // of the checkpoint entry itself (the log is up-to-date at that point).
243        let checkpoint_seq = self.next_sequence.load(Ordering::SeqCst);
244
245        let seq = self.append(WalOp::Checkpoint {
246            sequence: checkpoint_seq,
247        })?;
248
249        self.checkpoint_sequence.store(seq, Ordering::SeqCst);
250        self.stats.total_checkpoints.fetch_add(1, Ordering::Relaxed);
251        Ok(seq)
252    }
253
254    /// Returns all entries whose sequence number is strictly greater than the
255    /// last checkpointed sequence.
256    pub fn entries_since_checkpoint(&self) -> Vec<WalEntry> {
257        let checkpoint = self.checkpoint_sequence.load(Ordering::SeqCst);
258        let entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
259        entries
260            .iter()
261            .filter(|e| e.sequence > checkpoint)
262            .cloned()
263            .collect()
264    }
265
266    /// Removes all entries whose sequence number is strictly less than
267    /// `sequence`.  Returns the number of entries removed.
268    pub fn truncate_before(&self, sequence: u64) -> usize {
269        let mut entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
270        let before = entries.len();
271        entries.retain(|e| e.sequence >= sequence);
272        let removed = before - entries.len();
273        self.stats
274            .total_truncated
275            .fetch_add(removed as u64, Ordering::Relaxed);
276        removed
277    }
278
279    /// Returns the number of entries currently in the log.
280    pub fn entry_count(&self) -> usize {
281        let entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
282        entries.len()
283    }
284
285    /// Returns the ops from entries since the last checkpoint, excluding
286    /// `WalOp::Checkpoint` ops themselves (those are metadata, not data ops).
287    pub fn replay_ops(&self) -> Vec<WalOp> {
288        let ops: Vec<WalOp> = self
289            .entries_since_checkpoint()
290            .into_iter()
291            .filter_map(|e| match e.op {
292                WalOp::Checkpoint { .. } => None,
293                other => Some(other),
294            })
295            .collect();
296
297        self.stats
298            .total_replayed
299            .fetch_add(ops.len() as u64, Ordering::Relaxed);
300        ops
301    }
302
303    /// Returns the current checkpoint sequence number.
304    pub fn checkpoint_sequence(&self) -> u64 {
305        self.checkpoint_sequence.load(Ordering::SeqCst)
306    }
307
308    /// Returns the next sequence number that will be assigned (without
309    /// consuming it).
310    pub fn next_sequence(&self) -> u64 {
311        self.next_sequence.load(Ordering::SeqCst)
312    }
313}
314
315impl std::fmt::Debug for WriteAheadLog {
316    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317        f.debug_struct("WriteAheadLog")
318            .field("max_entries", &self.max_entries)
319            .field("next_sequence", &self.next_sequence.load(Ordering::Relaxed))
320            .field(
321                "checkpoint_sequence",
322                &self.checkpoint_sequence.load(Ordering::Relaxed),
323            )
324            .field("entry_count", &self.entry_count())
325            .finish()
326    }
327}
328
329// ---------------------------------------------------------------------------
330// StorageWriteAheadLog — in-memory WAL for crash-safe block operations
331// ---------------------------------------------------------------------------
332
333/// The kind of operation recorded in a [`StorageWalEntry`].
334#[derive(Debug, Clone, Copy, PartialEq, Eq)]
335pub enum WalEntryKind {
336    /// A block put operation.
337    Put,
338    /// A block delete operation.
339    Delete,
340    /// A block update (overwrite) operation.
341    Update,
342}
343
344/// A single entry in the [`StorageWriteAheadLog`].
345#[derive(Debug, Clone)]
346pub struct StorageWalEntry {
347    /// Monotonically increasing sequence number.
348    pub sequence: u64,
349    /// The kind of operation.
350    pub kind: WalEntryKind,
351    /// The key (e.g. CID) this operation targets.
352    pub key: String,
353    /// The data payload (empty for deletes).
354    pub data: Vec<u8>,
355    /// Logical tick at the time the entry was appended.
356    pub timestamp_tick: u64,
357}
358
359/// Configuration for [`StorageWriteAheadLog`].
360#[derive(Debug, Clone)]
361pub struct WalConfig {
362    /// Maximum number of entries before the oldest must be truncated.
363    pub max_entries: usize,
364    /// Maximum total data bytes across all entries.
365    pub max_data_bytes: u64,
366    /// Number of ticks between automatic checkpoint hints.
367    pub checkpoint_interval: u64,
368}
369
370impl Default for WalConfig {
371    fn default() -> Self {
372        Self {
373            max_entries: 10_000,
374            max_data_bytes: 100_000_000, // 100 MB
375            checkpoint_interval: 50,
376        }
377    }
378}
379
380/// Point-in-time statistics for a [`StorageWriteAheadLog`].
381#[derive(Debug, Clone)]
382pub struct StorageWalStats {
383    /// Number of entries currently in the log.
384    pub entry_count: usize,
385    /// Total data bytes across all entries.
386    pub data_bytes: u64,
387    /// Number of checkpoints recorded.
388    pub checkpoint_count: usize,
389    /// The next sequence number that will be assigned.
390    pub next_sequence: u64,
391    /// Sequence number of the oldest entry, if any.
392    pub oldest_sequence: Option<u64>,
393}
394
395/// In-memory write-ahead log for crash-safe block operations.
396///
397/// Unlike [`WriteAheadLog`] which is CID-centric and thread-safe via
398/// `Mutex`/`AtomicU64`, `StorageWriteAheadLog` is a single-threaded,
399/// tick-driven WAL that tracks total data bytes and supports indexed
400/// checkpoint replay.
401pub struct StorageWriteAheadLog {
402    config: WalConfig,
403    entries: VecDeque<StorageWalEntry>,
404    next_sequence: u64,
405    current_tick: u64,
406    total_data_bytes: u64,
407    checkpoints: Vec<u64>,
408    last_checkpoint_tick: u64,
409}
410
411impl StorageWriteAheadLog {
412    /// Creates a new `StorageWriteAheadLog` with the given configuration.
413    pub fn new(config: WalConfig) -> Self {
414        Self {
415            config,
416            entries: VecDeque::new(),
417            next_sequence: 1,
418            current_tick: 0,
419            total_data_bytes: 0,
420            checkpoints: Vec::new(),
421            last_checkpoint_tick: 0,
422        }
423    }
424
425    /// Appends a new entry to the WAL.
426    ///
427    /// If appending would exceed `max_entries`, the oldest entry is evicted
428    /// first. Returns the assigned sequence number.
429    ///
430    /// # Errors
431    /// Returns an error if adding `data` would exceed `max_data_bytes`.
432    pub fn append(&mut self, kind: WalEntryKind, key: &str, data: Vec<u8>) -> Result<u64, String> {
433        let new_data_len = data.len() as u64;
434
435        // Check data-byte budget (after potential eviction the freed bytes
436        // might make room, so we calculate the *would-be* total).
437        let would_be = self.total_data_bytes + new_data_len;
438        if would_be > self.config.max_data_bytes {
439            return Err(format!(
440                "WAL data limit exceeded: would be {} bytes, max {} bytes",
441                would_be, self.config.max_data_bytes
442            ));
443        }
444
445        // Evict oldest if at capacity
446        if self.entries.len() >= self.config.max_entries {
447            if let Some(evicted) = self.entries.pop_front() {
448                self.total_data_bytes = self
449                    .total_data_bytes
450                    .saturating_sub(evicted.data.len() as u64);
451            }
452        }
453
454        let seq = self.next_sequence;
455        self.next_sequence += 1;
456
457        self.entries.push_back(StorageWalEntry {
458            sequence: seq,
459            kind,
460            key: key.to_string(),
461            data,
462            timestamp_tick: self.current_tick,
463        });
464        self.total_data_bytes += new_data_len;
465
466        Ok(seq)
467    }
468
469    /// Returns a reference to the entry with the given sequence number, if present.
470    pub fn get_entry(&self, sequence: u64) -> Option<&StorageWalEntry> {
471        // The deque is ordered by sequence. We can binary-search if the front
472        // sequence is known.
473        let front_seq = self.entries.front().map(|e| e.sequence)?;
474        if sequence < front_seq {
475            return None;
476        }
477        let idx = (sequence - front_seq) as usize;
478        self.entries.get(idx).filter(|e| e.sequence == sequence)
479    }
480
481    /// Returns references to all entries with sequence numbers strictly
482    /// greater than `sequence`.
483    pub fn entries_since(&self, sequence: u64) -> Vec<&StorageWalEntry> {
484        self.entries
485            .iter()
486            .filter(|e| e.sequence > sequence)
487            .collect()
488    }
489
490    /// Records the current head sequence as a checkpoint and returns it.
491    pub fn create_checkpoint(&mut self) -> u64 {
492        let cp_seq = if let Some(last) = self.entries.back() {
493            last.sequence
494        } else {
495            self.next_sequence.saturating_sub(1)
496        };
497        self.checkpoints.push(cp_seq);
498        self.last_checkpoint_tick = self.current_tick;
499        cp_seq
500    }
501
502    /// Removes all entries with sequence numbers strictly less than `sequence`,
503    /// updating `total_data_bytes` accordingly.
504    pub fn truncate_before(&mut self, sequence: u64) {
505        while let Some(front) = self.entries.front() {
506            if front.sequence < sequence {
507                let evicted = self
508                    .entries
509                    .pop_front()
510                    .expect("front existed in condition");
511                self.total_data_bytes = self
512                    .total_data_bytes
513                    .saturating_sub(evicted.data.len() as u64);
514            } else {
515                break;
516            }
517        }
518    }
519
520    /// Returns all entries from the given checkpoint index to the end.
521    ///
522    /// # Errors
523    /// Returns an error if `checkpoint_idx` is out of range.
524    pub fn replay_from_checkpoint(
525        &self,
526        checkpoint_idx: usize,
527    ) -> Result<Vec<&StorageWalEntry>, String> {
528        let cp_seq = self.checkpoints.get(checkpoint_idx).ok_or_else(|| {
529            format!(
530                "checkpoint index {} out of range (have {})",
531                checkpoint_idx,
532                self.checkpoints.len()
533            )
534        })?;
535        Ok(self.entries_since(*cp_seq))
536    }
537
538    /// Returns `true` if the number of ticks since the last checkpoint is
539    /// at least `checkpoint_interval`.
540    pub fn should_checkpoint(&self) -> bool {
541        self.current_tick.saturating_sub(self.last_checkpoint_tick)
542            >= self.config.checkpoint_interval
543    }
544
545    /// Advances the logical clock by one tick.
546    pub fn tick(&mut self) {
547        self.current_tick += 1;
548    }
549
550    /// Returns the number of entries currently in the log.
551    pub fn entry_count(&self) -> usize {
552        self.entries.len()
553    }
554
555    /// Returns the total data bytes across all entries.
556    pub fn data_bytes(&self) -> u64 {
557        self.total_data_bytes
558    }
559
560    /// Returns a point-in-time statistics snapshot.
561    pub fn stats(&self) -> StorageWalStats {
562        StorageWalStats {
563            entry_count: self.entries.len(),
564            data_bytes: self.total_data_bytes,
565            checkpoint_count: self.checkpoints.len(),
566            next_sequence: self.next_sequence,
567            oldest_sequence: self.entries.front().map(|e| e.sequence),
568        }
569    }
570}
571
572impl std::fmt::Debug for StorageWriteAheadLog {
573    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
574        f.debug_struct("StorageWriteAheadLog")
575            .field("next_sequence", &self.next_sequence)
576            .field("current_tick", &self.current_tick)
577            .field("entry_count", &self.entries.len())
578            .field("total_data_bytes", &self.total_data_bytes)
579            .field("checkpoint_count", &self.checkpoints.len())
580            .finish()
581    }
582}
583
584// ---------------------------------------------------------------------------
585// Tests
586// ---------------------------------------------------------------------------
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591
592    // ------------------------------------------------------------------
593    // Helper
594    // ------------------------------------------------------------------
595
596    fn make_put(cid: &str, data_len: u64) -> WalOp {
597        WalOp::Put {
598            cid: cid.to_string(),
599            data_len,
600        }
601    }
602
603    fn make_delete(cid: &str) -> WalOp {
604        WalOp::Delete {
605            cid: cid.to_string(),
606        }
607    }
608
609    fn make_batch_put(cids: &[&str], total_bytes: u64) -> WalOp {
610        WalOp::BatchPut {
611            cids: cids.iter().map(|s| s.to_string()).collect(),
612            total_bytes,
613        }
614    }
615
616    // ------------------------------------------------------------------
617    // 1. Append single entry, sequence increments
618    // ------------------------------------------------------------------
619
620    #[test]
621    fn test_append_single_entry_sequence_starts_at_one() {
622        let wal = WriteAheadLog::new(100);
623        let seq = wal.append(make_put("bafya", 64)).expect("append failed");
624        assert_eq!(seq, 1, "first sequence should be 1");
625        assert_eq!(wal.entry_count(), 1);
626    }
627
628    #[test]
629    fn test_append_multiple_entries_sequence_increments() {
630        let wal = WriteAheadLog::new(100);
631        let s1 = wal.append(make_put("cid1", 10)).expect("append 1");
632        let s2 = wal.append(make_delete("cid1")).expect("append 2");
633        let s3 = wal.append(make_put("cid2", 20)).expect("append 3");
634
635        assert_eq!(s1, 1);
636        assert_eq!(s2, 2);
637        assert_eq!(s3, 3);
638        assert_eq!(wal.entry_count(), 3);
639    }
640
641    // ------------------------------------------------------------------
642    // 2. Append exceeds max_entries → error
643    // ------------------------------------------------------------------
644
645    #[test]
646    fn test_append_exceeds_max_entries_returns_error() {
647        let wal = WriteAheadLog::new(2);
648        wal.append(make_put("c1", 1)).expect("first append");
649        wal.append(make_put("c2", 2)).expect("second append");
650
651        let result = wal.append(make_put("c3", 3));
652        match result {
653            Err(WalError::CapacityExceeded { current, max }) => {
654                assert_eq!(current, 2);
655                assert_eq!(max, 2);
656            }
657            other => panic!("expected CapacityExceeded, got {:?}", other),
658        }
659    }
660
661    // ------------------------------------------------------------------
662    // 3. Checkpoint records sequence
663    // ------------------------------------------------------------------
664
665    #[test]
666    fn test_checkpoint_records_sequence() {
667        let wal = WriteAheadLog::new(100);
668        wal.append(make_put("x", 1)).expect("append");
669        wal.append(make_put("y", 2)).expect("append");
670
671        let cp_seq = wal.checkpoint().expect("checkpoint");
672        assert!(cp_seq >= 3, "checkpoint entry should get next sequence (3)");
673        assert_eq!(wal.checkpoint_sequence(), cp_seq);
674    }
675
676    // ------------------------------------------------------------------
677    // 4. entries_since_checkpoint returns only new entries
678    // ------------------------------------------------------------------
679
680    #[test]
681    fn test_entries_since_checkpoint_returns_only_new() {
682        let wal = WriteAheadLog::new(100);
683        wal.append(make_put("before", 1)).expect("append");
684        wal.checkpoint().expect("checkpoint");
685
686        wal.append(make_put("after1", 2)).expect("append after");
687        wal.append(make_put("after2", 3)).expect("append after");
688
689        let since = wal.entries_since_checkpoint();
690        assert_eq!(since.len(), 2);
691        for entry in &since {
692            match &entry.op {
693                WalOp::Put { cid, .. } => {
694                    assert!(cid.starts_with("after"), "unexpected CID: {}", cid);
695                }
696                other => panic!("unexpected op: {:?}", other),
697            }
698        }
699    }
700
701    // ------------------------------------------------------------------
702    // 5. After checkpoint, entries_since_checkpoint is empty (excl. Checkpoint)
703    // ------------------------------------------------------------------
704
705    #[test]
706    fn test_entries_since_checkpoint_empty_after_fresh_checkpoint() {
707        let wal = WriteAheadLog::new(100);
708        wal.append(make_put("a", 1)).expect("a");
709        wal.append(make_put("b", 2)).expect("b");
710        wal.checkpoint().expect("checkpoint");
711
712        // entries_since_checkpoint includes the Checkpoint entry itself only
713        // if its sequence > checkpoint_sequence — but checkpoint_sequence IS set
714        // to the checkpoint entry's sequence, so nothing should appear.
715        let since = wal.entries_since_checkpoint();
716        assert!(
717            since.is_empty(),
718            "expected empty, got {} entries",
719            since.len()
720        );
721    }
722
723    // ------------------------------------------------------------------
724    // 6. truncate_before removes correct entries
725    // ------------------------------------------------------------------
726
727    #[test]
728    fn test_truncate_before_removes_correct_entries() {
729        let wal = WriteAheadLog::new(100);
730        for i in 0..5u64 {
731            wal.append(make_put(&format!("c{i}"), i)).expect("append");
732        }
733        // Sequences 1..=5 in log. Remove < 3 (i.e. 1 and 2).
734        let removed = wal.truncate_before(3);
735        assert_eq!(removed, 2, "should remove exactly 2 entries");
736        assert_eq!(wal.entry_count(), 3, "3 entries should remain");
737    }
738
739    #[test]
740    fn test_truncate_before_nothing_to_remove() {
741        let wal = WriteAheadLog::new(100);
742        wal.append(make_put("a", 1)).expect("append");
743        let removed = wal.truncate_before(1); // seq >= 1 kept, nothing < 1
744        assert_eq!(removed, 0);
745        assert_eq!(wal.entry_count(), 1);
746    }
747
748    // ------------------------------------------------------------------
749    // 7. replay_ops excludes Checkpoint ops
750    // ------------------------------------------------------------------
751
752    #[test]
753    fn test_replay_ops_excludes_checkpoint_ops() {
754        let wal = WriteAheadLog::new(100);
755        // Before checkpoint
756        wal.append(make_put("old", 1)).expect("append old");
757        wal.checkpoint().expect("cp1");
758
759        // After checkpoint: 2 data ops + another checkpoint
760        wal.append(make_put("new1", 2)).expect("new1");
761        wal.append(make_delete("new1")).expect("del");
762        wal.checkpoint().expect("cp2");
763
764        // replay_ops returns only data ops since last checkpoint
765        let ops = wal.replay_ops();
766        assert!(
767            ops.is_empty(),
768            "after second checkpoint there are no data ops to replay, got {:?}",
769            ops
770        );
771    }
772
773    #[test]
774    fn test_replay_ops_returns_data_ops_since_checkpoint() {
775        let wal = WriteAheadLog::new(100);
776        wal.append(make_put("a", 1)).expect("a");
777        wal.checkpoint().expect("cp");
778
779        wal.append(make_put("b", 2)).expect("b");
780        wal.append(make_delete("a")).expect("del a");
781        wal.append(make_batch_put(&["c", "d"], 500)).expect("batch");
782
783        let ops = wal.replay_ops();
784        assert_eq!(ops.len(), 3);
785        assert!(matches!(ops[0], WalOp::Put { ref cid, .. } if cid == "b"));
786        assert!(matches!(ops[1], WalOp::Delete { ref cid } if cid == "a"));
787        assert!(matches!(ops[2], WalOp::BatchPut { .. }));
788    }
789
790    // ------------------------------------------------------------------
791    // 8. CRC-32 computed (non-zero for non-empty data)
792    // ------------------------------------------------------------------
793
794    #[test]
795    fn test_crc32_non_zero_for_non_empty_data() {
796        let data = b"hello ipfrs wal";
797        let crc = fnv1a_32(data);
798        assert_ne!(
799            crc, 0,
800            "FNV-1a should produce non-zero hash for non-empty input"
801        );
802    }
803
804    #[test]
805    fn test_crc32_entry_stored_correctly() {
806        let wal = WriteAheadLog::new(100);
807        let op = make_put("bafytest123", 256);
808        let seq = wal.append(op.clone()).expect("append");
809
810        let entries = wal
811            .entries_since_checkpoint()
812            .into_iter()
813            .find(|e| e.sequence == seq)
814            .expect("entry not found");
815
816        let expected_crc = fnv1a_32(&serde_json::to_vec(&op).unwrap());
817        assert_eq!(entries.crc32, expected_crc);
818        assert_ne!(entries.crc32, 0);
819    }
820
821    // ------------------------------------------------------------------
822    // 9. Stats accumulation
823    // ------------------------------------------------------------------
824
825    #[test]
826    fn test_stats_total_appended() {
827        let wal = WriteAheadLog::new(100);
828        wal.append(make_put("x", 1)).expect("x");
829        wal.append(make_delete("x")).expect("del x");
830        let snap = wal.stats.snapshot();
831        // The checkpoint() itself calls append(), so here just 2
832        assert_eq!(snap.total_appended, 2);
833    }
834
835    #[test]
836    fn test_stats_total_checkpoints() {
837        let wal = WriteAheadLog::new(100);
838        wal.append(make_put("a", 1)).expect("a");
839        wal.checkpoint().expect("cp1");
840        wal.checkpoint().expect("cp2");
841        let snap = wal.stats.snapshot();
842        assert_eq!(snap.total_checkpoints, 2);
843        // checkpoint also calls append internally → 3 appends total
844        assert_eq!(snap.total_appended, 3);
845    }
846
847    #[test]
848    fn test_stats_total_truncated() {
849        let wal = WriteAheadLog::new(100);
850        for i in 0..6u64 {
851            wal.append(make_put(&format!("c{i}"), i)).expect("append");
852        }
853        wal.truncate_before(4); // removes seq 1,2,3
854        let snap = wal.stats.snapshot();
855        assert_eq!(snap.total_truncated, 3);
856    }
857
858    #[test]
859    fn test_stats_total_replayed() {
860        let wal = WriteAheadLog::new(100);
861        wal.append(make_put("a", 1)).expect("a");
862        wal.checkpoint().expect("cp");
863        wal.append(make_put("b", 2)).expect("b");
864        wal.append(make_delete("a")).expect("del");
865
866        let ops = wal.replay_ops();
867        assert_eq!(ops.len(), 2);
868
869        let snap = wal.stats.snapshot();
870        assert_eq!(snap.total_replayed, 2);
871    }
872
873    // ------------------------------------------------------------------
874    // 10. Multiple Put/Delete/BatchPut ops
875    // ------------------------------------------------------------------
876
877    #[test]
878    fn test_multiple_op_types_roundtrip() {
879        let wal = WriteAheadLog::new(100);
880
881        let s1 = wal.append(make_put("cid-put-1", 100)).expect("put1");
882        let s2 = wal.append(make_put("cid-put-2", 200)).expect("put2");
883        let s3 = wal
884            .append(make_batch_put(&["cid-a", "cid-b", "cid-c"], 750))
885            .expect("batch");
886        let s4 = wal.append(make_delete("cid-put-1")).expect("del");
887
888        assert!(s1 < s2 && s2 < s3 && s3 < s4, "sequences must be monotonic");
889        assert_eq!(wal.entry_count(), 4);
890
891        let ops = wal.replay_ops();
892        assert_eq!(ops.len(), 4);
893
894        assert!(matches!(&ops[0], WalOp::Put { cid, data_len: 100 } if cid == "cid-put-1"));
895        assert!(matches!(&ops[1], WalOp::Put { cid, data_len: 200 } if cid == "cid-put-2"));
896        assert!(matches!(&ops[2], WalOp::BatchPut { cids, total_bytes: 750 } if cids.len() == 3));
897        assert!(matches!(&ops[3], WalOp::Delete { cid } if cid == "cid-put-1"));
898    }
899
900    // ------------------------------------------------------------------
901    // 11. FNV-1a determinism and known value
902    // ------------------------------------------------------------------
903
904    #[test]
905    fn test_fnv1a_deterministic() {
906        let data = b"ipfrs-wal";
907        let h1 = fnv1a_32(data);
908        let h2 = fnv1a_32(data);
909        assert_eq!(h1, h2, "FNV-1a must be deterministic");
910    }
911
912    #[test]
913    fn test_fnv1a_empty_input_is_offset_basis() {
914        let h = fnv1a_32(b"");
915        assert_eq!(h, 2_166_136_261u32, "empty input should equal offset basis");
916    }
917
918    // ------------------------------------------------------------------
919    // 12. Concurrent appends
920    // ------------------------------------------------------------------
921
922    #[test]
923    fn test_concurrent_appends_unique_sequences() {
924        use std::sync::Arc;
925        use std::thread;
926
927        let wal = Arc::new(WriteAheadLog::new(10_000));
928        let threads = 8usize;
929        let per_thread = 50usize;
930
931        let handles: Vec<_> = (0..threads)
932            .map(|t| {
933                let w = Arc::clone(&wal);
934                thread::spawn(move || {
935                    (0..per_thread)
936                        .map(|i| {
937                            w.append(make_put(&format!("t{t}-c{i}"), i as u64))
938                                .expect("concurrent append")
939                        })
940                        .collect::<Vec<_>>()
941                })
942            })
943            .collect();
944
945        let mut all_seqs: Vec<u64> = handles
946            .into_iter()
947            .flat_map(|h| h.join().expect("thread panicked"))
948            .collect();
949
950        all_seqs.sort_unstable();
951        all_seqs.dedup();
952        assert_eq!(
953            all_seqs.len(),
954            threads * per_thread,
955            "all sequences must be unique"
956        );
957        assert_eq!(wal.entry_count(), threads * per_thread);
958    }
959
960    // ------------------------------------------------------------------
961    // 13. Debug impl smoke test
962    // ------------------------------------------------------------------
963
964    #[test]
965    fn test_debug_impl_does_not_panic() {
966        let wal = WriteAheadLog::with_defaults();
967        wal.append(make_put("dbg", 1)).expect("append");
968        let _ = format!("{:?}", wal);
969    }
970
971    // ------------------------------------------------------------------
972    // 14. WalError display
973    // ------------------------------------------------------------------
974
975    #[test]
976    fn test_wal_error_display() {
977        let e = WalError::CapacityExceeded { current: 5, max: 5 };
978        let msg = e.to_string();
979        assert!(
980            msg.contains("current=5") && msg.contains("max=5"),
981            "{}",
982            msg
983        );
984
985        let e2 = WalError::CheckpointFailed("disk full".into());
986        assert!(e2.to_string().contains("disk full"));
987    }
988
989    // ==================================================================
990    // StorageWriteAheadLog tests
991    // ==================================================================
992
993    fn swal_config() -> WalConfig {
994        WalConfig {
995            max_entries: 100,
996            max_data_bytes: 10_000,
997            checkpoint_interval: 5,
998        }
999    }
1000
1001    fn swal_default() -> StorageWriteAheadLog {
1002        StorageWriteAheadLog::new(swal_config())
1003    }
1004
1005    // ------------------------------------------------------------------
1006    // 15. Append entries and verify sequence numbers
1007    // ------------------------------------------------------------------
1008
1009    #[test]
1010    fn test_swal_append_sequence_starts_at_one() {
1011        let mut wal = swal_default();
1012        let seq = wal
1013            .append(WalEntryKind::Put, "key1", vec![1, 2, 3])
1014            .expect("append");
1015        assert_eq!(seq, 1);
1016        assert_eq!(wal.entry_count(), 1);
1017    }
1018
1019    #[test]
1020    fn test_swal_append_multiple_sequences_increment() {
1021        let mut wal = swal_default();
1022        let s1 = wal.append(WalEntryKind::Put, "k1", vec![1]).expect("s1");
1023        let s2 = wal.append(WalEntryKind::Delete, "k1", vec![]).expect("s2");
1024        let s3 = wal.append(WalEntryKind::Update, "k2", vec![9]).expect("s3");
1025        assert_eq!(s1, 1);
1026        assert_eq!(s2, 2);
1027        assert_eq!(s3, 3);
1028        assert_eq!(wal.entry_count(), 3);
1029    }
1030
1031    // ------------------------------------------------------------------
1032    // 16. get_entry by sequence
1033    // ------------------------------------------------------------------
1034
1035    #[test]
1036    fn test_swal_get_entry_existing() {
1037        let mut wal = swal_default();
1038        let seq = wal.append(WalEntryKind::Put, "abc", vec![10]).expect("a");
1039        let entry = wal.get_entry(seq).expect("entry should exist");
1040        assert_eq!(entry.key, "abc");
1041        assert_eq!(entry.data, vec![10]);
1042        assert_eq!(entry.kind, WalEntryKind::Put);
1043    }
1044
1045    #[test]
1046    fn test_swal_get_entry_missing() {
1047        let wal = swal_default();
1048        assert!(wal.get_entry(999).is_none());
1049    }
1050
1051    #[test]
1052    fn test_swal_get_entry_after_truncation() {
1053        let mut wal = swal_default();
1054        wal.append(WalEntryKind::Put, "a", vec![1]).expect("a");
1055        wal.append(WalEntryKind::Put, "b", vec![2]).expect("b");
1056        wal.truncate_before(2);
1057        assert!(wal.get_entry(1).is_none());
1058        assert!(wal.get_entry(2).is_some());
1059    }
1060
1061    // ------------------------------------------------------------------
1062    // 17. entries_since returns correct slice
1063    // ------------------------------------------------------------------
1064
1065    #[test]
1066    fn test_swal_entries_since() {
1067        let mut wal = swal_default();
1068        for i in 0..5 {
1069            wal.append(WalEntryKind::Put, &format!("k{i}"), vec![i as u8])
1070                .expect("append");
1071        }
1072        let since = wal.entries_since(3);
1073        assert_eq!(since.len(), 2);
1074        assert_eq!(since[0].sequence, 4);
1075        assert_eq!(since[1].sequence, 5);
1076    }
1077
1078    #[test]
1079    fn test_swal_entries_since_zero_returns_all() {
1080        let mut wal = swal_default();
1081        wal.append(WalEntryKind::Put, "x", vec![]).expect("x");
1082        wal.append(WalEntryKind::Delete, "y", vec![]).expect("y");
1083        let all = wal.entries_since(0);
1084        assert_eq!(all.len(), 2);
1085    }
1086
1087    // ------------------------------------------------------------------
1088    // 18. Checkpoint creation
1089    // ------------------------------------------------------------------
1090
1091    #[test]
1092    fn test_swal_create_checkpoint() {
1093        let mut wal = swal_default();
1094        wal.append(WalEntryKind::Put, "a", vec![1]).expect("a");
1095        wal.append(WalEntryKind::Put, "b", vec![2]).expect("b");
1096        let cp = wal.create_checkpoint();
1097        assert_eq!(cp, 2); // last entry sequence
1098        assert_eq!(wal.stats().checkpoint_count, 1);
1099    }
1100
1101    #[test]
1102    fn test_swal_create_multiple_checkpoints() {
1103        let mut wal = swal_default();
1104        wal.append(WalEntryKind::Put, "a", vec![]).expect("a");
1105        let cp1 = wal.create_checkpoint();
1106        wal.append(WalEntryKind::Put, "b", vec![]).expect("b");
1107        wal.append(WalEntryKind::Put, "c", vec![]).expect("c");
1108        let cp2 = wal.create_checkpoint();
1109        assert!(cp2 > cp1);
1110        assert_eq!(wal.stats().checkpoint_count, 2);
1111    }
1112
1113    // ------------------------------------------------------------------
1114    // 19. truncate_before removes old entries and updates data bytes
1115    // ------------------------------------------------------------------
1116
1117    #[test]
1118    fn test_swal_truncate_before() {
1119        let mut wal = swal_default();
1120        wal.append(WalEntryKind::Put, "a", vec![1, 2, 3])
1121            .expect("a");
1122        wal.append(WalEntryKind::Put, "b", vec![4, 5]).expect("b");
1123        wal.append(WalEntryKind::Put, "c", vec![6]).expect("c");
1124
1125        let bytes_before = wal.data_bytes();
1126        assert_eq!(bytes_before, 6);
1127
1128        wal.truncate_before(3); // remove seq 1, 2
1129        assert_eq!(wal.entry_count(), 1);
1130        assert_eq!(wal.data_bytes(), 1); // only "c" data remains
1131    }
1132
1133    #[test]
1134    fn test_swal_truncate_before_nothing() {
1135        let mut wal = swal_default();
1136        wal.append(WalEntryKind::Put, "x", vec![1]).expect("x");
1137        wal.truncate_before(1); // seq 1 is NOT less than 1
1138        assert_eq!(wal.entry_count(), 1);
1139    }
1140
1141    #[test]
1142    fn test_swal_truncate_before_all() {
1143        let mut wal = swal_default();
1144        wal.append(WalEntryKind::Put, "a", vec![1]).expect("a");
1145        wal.append(WalEntryKind::Put, "b", vec![2]).expect("b");
1146        wal.truncate_before(100);
1147        assert_eq!(wal.entry_count(), 0);
1148        assert_eq!(wal.data_bytes(), 0);
1149    }
1150
1151    // ------------------------------------------------------------------
1152    // 20. replay_from_checkpoint
1153    // ------------------------------------------------------------------
1154
1155    #[test]
1156    fn test_swal_replay_from_checkpoint() {
1157        let mut wal = swal_default();
1158        wal.append(WalEntryKind::Put, "old", vec![1]).expect("old");
1159        wal.create_checkpoint(); // checkpoint 0 at seq 1
1160        wal.append(WalEntryKind::Put, "new1", vec![2]).expect("n1");
1161        wal.append(WalEntryKind::Delete, "old", vec![]).expect("n2");
1162
1163        let replayed = wal.replay_from_checkpoint(0).expect("replay");
1164        assert_eq!(replayed.len(), 2);
1165        assert_eq!(replayed[0].key, "new1");
1166        assert_eq!(replayed[1].key, "old");
1167    }
1168
1169    #[test]
1170    fn test_swal_replay_from_checkpoint_invalid_index() {
1171        let wal = swal_default();
1172        let result = wal.replay_from_checkpoint(0);
1173        assert!(result.is_err());
1174    }
1175
1176    #[test]
1177    fn test_swal_replay_from_second_checkpoint() {
1178        let mut wal = swal_default();
1179        wal.append(WalEntryKind::Put, "a", vec![]).expect("a");
1180        wal.create_checkpoint(); // cp 0
1181        wal.append(WalEntryKind::Put, "b", vec![]).expect("b");
1182        wal.create_checkpoint(); // cp 1
1183        wal.append(WalEntryKind::Put, "c", vec![]).expect("c");
1184
1185        let replayed = wal.replay_from_checkpoint(1).expect("replay from cp1");
1186        assert_eq!(replayed.len(), 1);
1187        assert_eq!(replayed[0].key, "c");
1188    }
1189
1190    // ------------------------------------------------------------------
1191    // 21. max_entries overflow → oldest evicted
1192    // ------------------------------------------------------------------
1193
1194    #[test]
1195    fn test_swal_max_entries_overflow_evicts_oldest() {
1196        let cfg = WalConfig {
1197            max_entries: 3,
1198            max_data_bytes: 100_000,
1199            checkpoint_interval: 50,
1200        };
1201        let mut wal = StorageWriteAheadLog::new(cfg);
1202        wal.append(WalEntryKind::Put, "a", vec![1]).expect("a");
1203        wal.append(WalEntryKind::Put, "b", vec![2]).expect("b");
1204        wal.append(WalEntryKind::Put, "c", vec![3]).expect("c");
1205        // Now at capacity, next append evicts "a"
1206        wal.append(WalEntryKind::Put, "d", vec![4]).expect("d");
1207
1208        assert_eq!(wal.entry_count(), 3);
1209        assert!(wal.get_entry(1).is_none(), "seq 1 should be evicted");
1210        assert!(wal.get_entry(2).is_some());
1211        assert!(wal.get_entry(4).is_some());
1212    }
1213
1214    // ------------------------------------------------------------------
1215    // 22. max_data_bytes enforcement
1216    // ------------------------------------------------------------------
1217
1218    #[test]
1219    fn test_swal_max_data_bytes_enforced() {
1220        let cfg = WalConfig {
1221            max_entries: 100,
1222            max_data_bytes: 10,
1223            checkpoint_interval: 50,
1224        };
1225        let mut wal = StorageWriteAheadLog::new(cfg);
1226        wal.append(WalEntryKind::Put, "a", vec![0; 8]).expect("a");
1227        // 8 bytes used; adding 5 more → 13 > 10 → error
1228        let result = wal.append(WalEntryKind::Put, "b", vec![0; 5]);
1229        assert!(result.is_err());
1230        assert!(result
1231            .as_ref()
1232            .err()
1233            .is_some_and(|e| e.contains("data limit exceeded")));
1234    }
1235
1236    #[test]
1237    fn test_swal_max_data_bytes_exact_limit() {
1238        let cfg = WalConfig {
1239            max_entries: 100,
1240            max_data_bytes: 10,
1241            checkpoint_interval: 50,
1242        };
1243        let mut wal = StorageWriteAheadLog::new(cfg);
1244        // Exactly 10 bytes should succeed
1245        wal.append(WalEntryKind::Put, "a", vec![0; 10])
1246            .expect("exact limit");
1247        assert_eq!(wal.data_bytes(), 10);
1248        // One more byte should fail
1249        let result = wal.append(WalEntryKind::Put, "b", vec![1]);
1250        assert!(result.is_err());
1251    }
1252
1253    // ------------------------------------------------------------------
1254    // 23. should_checkpoint timing
1255    // ------------------------------------------------------------------
1256
1257    #[test]
1258    fn test_swal_should_checkpoint_timing() {
1259        let cfg = WalConfig {
1260            max_entries: 100,
1261            max_data_bytes: 100_000,
1262            checkpoint_interval: 3,
1263        };
1264        let mut wal = StorageWriteAheadLog::new(cfg);
1265        // At tick 0, last_checkpoint_tick is 0, interval is 3
1266        // 0 - 0 = 0 < 3
1267        assert!(!wal.should_checkpoint());
1268        wal.tick(); // tick 1
1269        assert!(!wal.should_checkpoint());
1270        wal.tick(); // tick 2
1271        assert!(!wal.should_checkpoint());
1272        wal.tick(); // tick 3
1273        assert!(wal.should_checkpoint());
1274
1275        wal.create_checkpoint(); // resets last_checkpoint_tick to 3
1276        assert!(!wal.should_checkpoint());
1277        wal.tick(); // 4
1278        wal.tick(); // 5
1279        wal.tick(); // 6
1280        assert!(wal.should_checkpoint());
1281    }
1282
1283    // ------------------------------------------------------------------
1284    // 24. stats accuracy
1285    // ------------------------------------------------------------------
1286
1287    #[test]
1288    fn test_swal_stats_accuracy() {
1289        let mut wal = swal_default();
1290        wal.append(WalEntryKind::Put, "a", vec![1, 2]).expect("a");
1291        wal.append(WalEntryKind::Delete, "b", vec![]).expect("b");
1292        wal.create_checkpoint();
1293
1294        let s = wal.stats();
1295        assert_eq!(s.entry_count, 2);
1296        assert_eq!(s.data_bytes, 2);
1297        assert_eq!(s.checkpoint_count, 1);
1298        assert_eq!(s.next_sequence, 3);
1299        assert_eq!(s.oldest_sequence, Some(1));
1300    }
1301
1302    #[test]
1303    fn test_swal_stats_after_truncate() {
1304        let mut wal = swal_default();
1305        wal.append(WalEntryKind::Put, "a", vec![1]).expect("a");
1306        wal.append(WalEntryKind::Put, "b", vec![2, 3]).expect("b");
1307        wal.truncate_before(2);
1308        let s = wal.stats();
1309        assert_eq!(s.entry_count, 1);
1310        assert_eq!(s.data_bytes, 2);
1311        assert_eq!(s.oldest_sequence, Some(2));
1312    }
1313
1314    // ------------------------------------------------------------------
1315    // 25. Empty WAL behavior
1316    // ------------------------------------------------------------------
1317
1318    #[test]
1319    fn test_swal_empty_wal() {
1320        let wal = swal_default();
1321        assert_eq!(wal.entry_count(), 0);
1322        assert_eq!(wal.data_bytes(), 0);
1323        assert!(wal.get_entry(1).is_none());
1324        assert!(wal.entries_since(0).is_empty());
1325        let s = wal.stats();
1326        assert_eq!(s.entry_count, 0);
1327        assert_eq!(s.oldest_sequence, None);
1328        assert_eq!(s.checkpoint_count, 0);
1329    }
1330
1331    // ------------------------------------------------------------------
1332    // 26. Mixed Put/Delete/Update entries
1333    // ------------------------------------------------------------------
1334
1335    #[test]
1336    fn test_swal_mixed_entry_kinds() {
1337        let mut wal = swal_default();
1338        wal.append(WalEntryKind::Put, "file1", vec![10, 20])
1339            .expect("put");
1340        wal.append(WalEntryKind::Update, "file1", vec![30, 40])
1341            .expect("update");
1342        wal.append(WalEntryKind::Delete, "file1", vec![])
1343            .expect("delete");
1344
1345        assert_eq!(wal.entry_count(), 3);
1346        let e1 = wal.get_entry(1).expect("e1");
1347        assert_eq!(e1.kind, WalEntryKind::Put);
1348        let e2 = wal.get_entry(2).expect("e2");
1349        assert_eq!(e2.kind, WalEntryKind::Update);
1350        let e3 = wal.get_entry(3).expect("e3");
1351        assert_eq!(e3.kind, WalEntryKind::Delete);
1352    }
1353
1354    // ------------------------------------------------------------------
1355    // 27. Tick advances clock
1356    // ------------------------------------------------------------------
1357
1358    #[test]
1359    fn test_swal_tick_advances_clock() {
1360        let mut wal = swal_default();
1361        wal.tick();
1362        wal.tick();
1363        wal.append(WalEntryKind::Put, "t", vec![]).expect("t");
1364        let entry = wal.get_entry(1).expect("entry");
1365        assert_eq!(entry.timestamp_tick, 2);
1366    }
1367
1368    // ------------------------------------------------------------------
1369    // 28. data_bytes tracks correctly through append + truncate
1370    // ------------------------------------------------------------------
1371
1372    #[test]
1373    fn test_swal_data_bytes_tracking() {
1374        let mut wal = swal_default();
1375        wal.append(WalEntryKind::Put, "a", vec![0; 100]).expect("a");
1376        assert_eq!(wal.data_bytes(), 100);
1377        wal.append(WalEntryKind::Put, "b", vec![0; 50]).expect("b");
1378        assert_eq!(wal.data_bytes(), 150);
1379        wal.truncate_before(2);
1380        assert_eq!(wal.data_bytes(), 50);
1381    }
1382
1383    // ------------------------------------------------------------------
1384    // 29. Overflow eviction updates data_bytes
1385    // ------------------------------------------------------------------
1386
1387    #[test]
1388    fn test_swal_overflow_eviction_updates_data_bytes() {
1389        let cfg = WalConfig {
1390            max_entries: 2,
1391            max_data_bytes: 100_000,
1392            checkpoint_interval: 50,
1393        };
1394        let mut wal = StorageWriteAheadLog::new(cfg);
1395        wal.append(WalEntryKind::Put, "a", vec![0; 30]).expect("a");
1396        wal.append(WalEntryKind::Put, "b", vec![0; 20]).expect("b");
1397        assert_eq!(wal.data_bytes(), 50);
1398
1399        // Evicts "a" (30 bytes), adds "c" (10 bytes)
1400        wal.append(WalEntryKind::Put, "c", vec![0; 10]).expect("c");
1401        assert_eq!(wal.data_bytes(), 30); // 20 + 10
1402        assert_eq!(wal.entry_count(), 2);
1403    }
1404
1405    // ------------------------------------------------------------------
1406    // 30. Checkpoint on empty WAL
1407    // ------------------------------------------------------------------
1408
1409    #[test]
1410    fn test_swal_checkpoint_on_empty() {
1411        let mut wal = swal_default();
1412        let cp = wal.create_checkpoint();
1413        assert_eq!(cp, 0); // next_sequence(1) - 1 = 0
1414        assert_eq!(wal.stats().checkpoint_count, 1);
1415    }
1416
1417    // ------------------------------------------------------------------
1418    // 31. entries_since with no matches
1419    // ------------------------------------------------------------------
1420
1421    #[test]
1422    fn test_swal_entries_since_none() {
1423        let mut wal = swal_default();
1424        wal.append(WalEntryKind::Put, "a", vec![]).expect("a");
1425        let since = wal.entries_since(100);
1426        assert!(since.is_empty());
1427    }
1428
1429    // ------------------------------------------------------------------
1430    // 32. Default config values
1431    // ------------------------------------------------------------------
1432
1433    #[test]
1434    fn test_swal_default_config() {
1435        let cfg = WalConfig::default();
1436        assert_eq!(cfg.max_entries, 10_000);
1437        assert_eq!(cfg.max_data_bytes, 100_000_000);
1438        assert_eq!(cfg.checkpoint_interval, 50);
1439    }
1440
1441    // ------------------------------------------------------------------
1442    // 33. Debug impl for StorageWriteAheadLog
1443    // ------------------------------------------------------------------
1444
1445    #[test]
1446    fn test_swal_debug_does_not_panic() {
1447        let mut wal = swal_default();
1448        wal.append(WalEntryKind::Put, "dbg", vec![1]).expect("a");
1449        let dbg = format!("{:?}", wal);
1450        assert!(dbg.contains("StorageWriteAheadLog"));
1451    }
1452
1453    // ------------------------------------------------------------------
1454    // 34. WalEntryKind equality
1455    // ------------------------------------------------------------------
1456
1457    #[test]
1458    fn test_swal_entry_kind_equality() {
1459        assert_eq!(WalEntryKind::Put, WalEntryKind::Put);
1460        assert_ne!(WalEntryKind::Put, WalEntryKind::Delete);
1461        assert_ne!(WalEntryKind::Delete, WalEntryKind::Update);
1462    }
1463
1464    // ------------------------------------------------------------------
1465    // 35. StorageWalEntry clone
1466    // ------------------------------------------------------------------
1467
1468    #[test]
1469    fn test_swal_entry_clone() {
1470        let entry = StorageWalEntry {
1471            sequence: 42,
1472            kind: WalEntryKind::Update,
1473            key: "cloned".to_string(),
1474            data: vec![7, 8, 9],
1475            timestamp_tick: 10,
1476        };
1477        let cloned = entry.clone();
1478        assert_eq!(cloned.sequence, 42);
1479        assert_eq!(cloned.kind, WalEntryKind::Update);
1480        assert_eq!(cloned.key, "cloned");
1481        assert_eq!(cloned.data, vec![7, 8, 9]);
1482    }
1483
1484    // ------------------------------------------------------------------
1485    // 36. StorageWalStats clone and debug
1486    // ------------------------------------------------------------------
1487
1488    #[test]
1489    fn test_swal_stats_clone_and_debug() {
1490        let mut wal = swal_default();
1491        wal.append(WalEntryKind::Put, "s", vec![1, 2, 3])
1492            .expect("s");
1493        let stats = wal.stats();
1494        let cloned = stats.clone();
1495        assert_eq!(cloned.entry_count, 1);
1496        let dbg = format!("{:?}", cloned);
1497        assert!(dbg.contains("entry_count"));
1498    }
1499
1500    // ------------------------------------------------------------------
1501    // 37. Large sequential append + replay
1502    // ------------------------------------------------------------------
1503
1504    #[test]
1505    fn test_swal_large_sequential_append_and_replay() {
1506        let cfg = WalConfig {
1507            max_entries: 500,
1508            max_data_bytes: 100_000,
1509            checkpoint_interval: 100,
1510        };
1511        let mut wal = StorageWriteAheadLog::new(cfg);
1512
1513        for i in 0..200 {
1514            wal.append(WalEntryKind::Put, &format!("key{i}"), vec![i as u8])
1515                .expect("append");
1516        }
1517        assert_eq!(wal.entry_count(), 200);
1518
1519        wal.create_checkpoint(); // at seq 200
1520        for i in 200..250 {
1521            wal.append(WalEntryKind::Put, &format!("key{i}"), vec![i as u8])
1522                .expect("append");
1523        }
1524
1525        let replayed = wal.replay_from_checkpoint(0).expect("replay");
1526        assert_eq!(replayed.len(), 50);
1527    }
1528
1529    // ------------------------------------------------------------------
1530    // 38. should_checkpoint at tick 0 with interval 0
1531    // ------------------------------------------------------------------
1532
1533    #[test]
1534    fn test_swal_should_checkpoint_interval_zero() {
1535        let cfg = WalConfig {
1536            max_entries: 100,
1537            max_data_bytes: 100_000,
1538            checkpoint_interval: 0,
1539        };
1540        let wal = StorageWriteAheadLog::new(cfg);
1541        // 0 - 0 >= 0 → true
1542        assert!(wal.should_checkpoint());
1543    }
1544
1545    // ------------------------------------------------------------------
1546    // 39. Delete entries have zero data bytes
1547    // ------------------------------------------------------------------
1548
1549    #[test]
1550    fn test_swal_delete_entries_zero_data() {
1551        let mut wal = swal_default();
1552        wal.append(WalEntryKind::Delete, "gone", vec![])
1553            .expect("del");
1554        assert_eq!(wal.data_bytes(), 0);
1555        assert_eq!(wal.entry_count(), 1);
1556    }
1557}