Skip to main content

ipfrs_storage/
write_ahead_log.rs

1//! Production-grade Write-Ahead Log (WAL) for crash recovery and durability.
2//!
3//! Entries are encoded in a binary format and stored in an in-memory `Vec<u8>`
4//! segment buffer. The encoding is self-describing and checksum-protected so
5//! that any corruption is detected deterministically during recovery.
6//!
7//! # Entry layout
8//!
9//! ```text
10//! [magic(4)] [seq_num(8)] [tx_id(8)] [op_type(1)] [key_len(4)] [key(n)]
11//! [value_len(4)] [value(m)] [timestamp(8)] [checksum(8)]
12//! ```
13//!
14//! The checksum is the FNV-1a 64-bit hash of all preceding bytes in the entry.
15
16use std::collections::HashMap;
17use thiserror::Error;
18
19// ─────────────────────────────────────────────────────────────────────────────
20// FNV-1a 64-bit
21// ─────────────────────────────────────────────────────────────────────────────
22
23/// Computes the FNV-1a 64-bit hash of `data`.
24#[inline]
25pub fn fnv1a_64(data: &[u8]) -> u64 {
26    let mut h: u64 = 14_695_981_039_346_656_037;
27    for &b in data {
28        h ^= b as u64;
29        h = h.wrapping_mul(1_099_511_628_211);
30    }
31    h
32}
33
34/// WAL entry magic bytes — detect non-WAL or truncated data immediately.
35pub const WAL_MAGIC: [u8; 4] = *b"WALX";
36
37/// Minimum byte size of an encoded WAL entry (no key, no value).
38/// magic(4) + seq(8) + tx_id(8) + op_type(1) + key_len(4) + value_len(4)
39/// + timestamp(8) + checksum(8) = 45
40const MIN_ENTRY_BYTES: usize = 45;
41
42// ─────────────────────────────────────────────────────────────────────────────
43// Error
44// ─────────────────────────────────────────────────────────────────────────────
45
46/// Errors returned by [`WalWriteAheadLog`] operations.
47#[derive(Debug, Error, PartialEq, Eq, Clone)]
48pub enum WalError {
49    /// A decoded entry's checksum does not match the computed value.
50    #[error("checksum mismatch for entry {entry_seq}: expected {expected:#018x}, got {got:#018x}")]
51    ChecksumMismatch {
52        entry_seq: u64,
53        expected: u64,
54        got: u64,
55    },
56
57    /// The entry's byte representation is malformed.
58    #[error("corrupted entry at seq {0}")]
59    CorruptedEntry(u64),
60
61    /// No transaction with the given id is tracked.
62    #[error("transaction {0} not found")]
63    TransactionNotFound(u64),
64
65    /// The encoded entry exceeds the configured maximum.
66    #[error("entry too large: {0} bytes")]
67    EntryTooLarge(usize),
68
69    /// The segment buffer is full.
70    #[error("segment full")]
71    SegmentFull,
72
73    /// The four-byte magic header is invalid.
74    #[error("invalid WAL magic bytes")]
75    InvalidMagic,
76}
77
78// ─────────────────────────────────────────────────────────────────────────────
79// WalOpType
80// ─────────────────────────────────────────────────────────────────────────────
81
82/// Discriminant for WAL entry operations.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
84#[repr(u8)]
85pub enum WalOpType {
86    /// Regular key-value put.
87    Put = 1,
88    /// Key deletion.
89    Delete = 2,
90    /// Transaction begin marker.
91    Begin = 3,
92    /// Transaction commit marker.
93    Commit = 4,
94    /// Transaction rollback marker.
95    Rollback = 5,
96    /// Checkpoint — all prior committed entries may be discarded.
97    Checkpoint = 6,
98}
99
100impl WalOpType {
101    fn from_u8(v: u8) -> Option<Self> {
102        match v {
103            1 => Some(Self::Put),
104            2 => Some(Self::Delete),
105            3 => Some(Self::Begin),
106            4 => Some(Self::Commit),
107            5 => Some(Self::Rollback),
108            6 => Some(Self::Checkpoint),
109            _ => None,
110        }
111    }
112}
113
114// ─────────────────────────────────────────────────────────────────────────────
115// WalEntry
116// ─────────────────────────────────────────────────────────────────────────────
117
118/// A single decoded entry in the write-ahead log.
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct WalEntry {
121    /// Monotonically increasing sequence number assigned at write time.
122    pub seq_num: u64,
123    /// Transaction identifier (0 = no transaction / standalone write).
124    pub tx_id: u64,
125    /// Operation type.
126    pub op_type: WalOpType,
127    /// Key bytes (may be empty for Begin/Commit/Rollback/Checkpoint).
128    pub key: Vec<u8>,
129    /// Value bytes (may be empty for Delete/Begin/Commit/Rollback/Checkpoint).
130    pub value: Vec<u8>,
131    /// Unix timestamp in nanoseconds captured at write time.
132    pub timestamp: u64,
133    /// FNV-1a 64-bit checksum of all preceding bytes in the encoded entry.
134    pub checksum: u64,
135}
136
137impl WalEntry {
138    /// Encodes this entry to its wire format.
139    ///
140    /// Layout:
141    /// `[magic(4)][seq_num(8)][tx_id(8)][op_type(1)][key_len(4)][key(n)]`
142    /// `[value_len(4)][value(m)][timestamp(8)][checksum(8)]`
143    pub fn encode(&self) -> Vec<u8> {
144        let total = 4 + 8 + 8 + 1 + 4 + self.key.len() + 4 + self.value.len() + 8 + 8;
145        let mut buf = Vec::with_capacity(total);
146
147        buf.extend_from_slice(&WAL_MAGIC);
148        buf.extend_from_slice(&self.seq_num.to_le_bytes());
149        buf.extend_from_slice(&self.tx_id.to_le_bytes());
150        buf.push(self.op_type as u8);
151        buf.extend_from_slice(&(self.key.len() as u32).to_le_bytes());
152        buf.extend_from_slice(&self.key);
153        buf.extend_from_slice(&(self.value.len() as u32).to_le_bytes());
154        buf.extend_from_slice(&self.value);
155        buf.extend_from_slice(&self.timestamp.to_le_bytes());
156
157        // Checksum covers everything written so far.
158        let checksum = fnv1a_64(&buf);
159        buf.extend_from_slice(&checksum.to_le_bytes());
160
161        buf
162    }
163
164    /// Attempts to decode one entry starting at `data[offset..]`.
165    ///
166    /// Returns `(entry, bytes_consumed)` on success.
167    ///
168    /// # Errors
169    /// - [`WalError::InvalidMagic`] if the four-byte header does not match.
170    /// - [`WalError::CorruptedEntry`] if the buffer is too short or an unknown
171    ///   op-type is encountered.
172    /// - [`WalError::ChecksumMismatch`] if the stored checksum disagrees.
173    pub fn decode(data: &[u8], offset: usize) -> Result<(WalEntry, usize), WalError> {
174        let buf = &data[offset..];
175
176        if buf.len() < MIN_ENTRY_BYTES {
177            return Err(WalError::CorruptedEntry(0));
178        }
179
180        // Magic
181        if buf[..4] != WAL_MAGIC {
182            return Err(WalError::InvalidMagic);
183        }
184
185        let seq_num = u64::from_le_bytes(
186            buf[4..12]
187                .try_into()
188                .map_err(|_| WalError::CorruptedEntry(0))?,
189        );
190        let tx_id = u64::from_le_bytes(
191            buf[12..20]
192                .try_into()
193                .map_err(|_| WalError::CorruptedEntry(seq_num))?,
194        );
195        let op_u8 = buf[20];
196        let op_type = WalOpType::from_u8(op_u8).ok_or(WalError::CorruptedEntry(seq_num))?;
197
198        let key_len = u32::from_le_bytes(
199            buf[21..25]
200                .try_into()
201                .map_err(|_| WalError::CorruptedEntry(seq_num))?,
202        ) as usize;
203
204        let key_end = 25 + key_len;
205        if buf.len() < key_end + 4 {
206            return Err(WalError::CorruptedEntry(seq_num));
207        }
208        let key = buf[25..key_end].to_vec();
209
210        let value_len = u32::from_le_bytes(
211            buf[key_end..key_end + 4]
212                .try_into()
213                .map_err(|_| WalError::CorruptedEntry(seq_num))?,
214        ) as usize;
215
216        let value_start = key_end + 4;
217        let value_end = value_start + value_len;
218        let ts_end = value_end + 8;
219        let cs_end = ts_end + 8;
220
221        if buf.len() < cs_end {
222            return Err(WalError::CorruptedEntry(seq_num));
223        }
224
225        let value = buf[value_start..value_end].to_vec();
226        let timestamp = u64::from_le_bytes(
227            buf[value_end..ts_end]
228                .try_into()
229                .map_err(|_| WalError::CorruptedEntry(seq_num))?,
230        );
231        let stored_checksum = u64::from_le_bytes(
232            buf[ts_end..cs_end]
233                .try_into()
234                .map_err(|_| WalError::CorruptedEntry(seq_num))?,
235        );
236
237        // Verify checksum — covers the bytes *before* the 8-byte checksum field.
238        let computed = fnv1a_64(&buf[..ts_end]);
239        if computed != stored_checksum {
240            return Err(WalError::ChecksumMismatch {
241                entry_seq: seq_num,
242                expected: stored_checksum,
243                got: computed,
244            });
245        }
246
247        let entry = WalEntry {
248            seq_num,
249            tx_id,
250            op_type,
251            key,
252            value,
253            timestamp,
254            checksum: stored_checksum,
255        };
256
257        Ok((entry, cs_end))
258    }
259}
260
261// ─────────────────────────────────────────────────────────────────────────────
262// Transaction state
263// ─────────────────────────────────────────────────────────────────────────────
264
265/// Lifecycle state of an in-flight or completed transaction.
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum TxState {
268    /// Transaction is open and accepting writes.
269    Active,
270    /// Transaction has been durably committed.
271    Committed,
272    /// Transaction was explicitly rolled back.
273    RolledBack,
274}
275
276/// An in-memory transaction record tracking accumulated entries.
277#[derive(Debug, Clone)]
278pub struct Transaction {
279    /// Unique transaction identifier.
280    pub tx_id: u64,
281    /// Entries buffered within this transaction.
282    pub entries: Vec<WalEntry>,
283    /// Current lifecycle state.
284    pub state: TxState,
285}
286
287impl Transaction {
288    fn new(tx_id: u64) -> Self {
289        Self {
290            tx_id,
291            entries: Vec::new(),
292            state: TxState::Active,
293        }
294    }
295}
296
297// ─────────────────────────────────────────────────────────────────────────────
298// Configuration
299// ─────────────────────────────────────────────────────────────────────────────
300
301/// Configuration knobs for [`WalWriteAheadLog`].
302#[derive(Debug, Clone)]
303pub struct WalConfig {
304    /// Maximum byte size of a single segment buffer before it is considered
305    /// full.  `0` means unlimited.
306    pub max_segment_size: usize,
307
308    /// Whether each write should trigger an immediate sync (no-op for
309    /// in-memory implementation, included for API completeness).
310    pub sync_on_write: bool,
311
312    /// Maximum number of entries per segment before a new one is started.
313    /// `0` means unlimited.
314    pub max_entries_per_segment: usize,
315
316    /// How many entries to accumulate between automatic checkpoints.
317    /// `0` disables auto-checkpointing.
318    pub checkpoint_interval: usize,
319}
320
321impl Default for WalConfig {
322    fn default() -> Self {
323        Self {
324            max_segment_size: 64 * 1024 * 1024, // 64 MiB
325            sync_on_write: false,
326            max_entries_per_segment: 100_000,
327            checkpoint_interval: 10_000,
328        }
329    }
330}
331
332// ─────────────────────────────────────────────────────────────────────────────
333// RecoveryResult
334// ─────────────────────────────────────────────────────────────────────────────
335
336/// Summary of a recovery scan over a WAL segment buffer.
337#[derive(Debug, Clone, Default, PartialEq, Eq)]
338pub struct RecoveryResult {
339    /// Total entries successfully decoded from the byte buffer.
340    pub entries_recovered: usize,
341    /// Number of distinct transaction ids encountered.
342    pub transactions_recovered: usize,
343    /// Number of entries that were replayed (committed Put/Delete only).
344    pub entries_replayed: usize,
345    /// Number of entries discarded because they belonged to uncommitted txns.
346    pub partial_tx_discarded: usize,
347    /// Highest sequence number seen during recovery.
348    pub last_seq_num: u64,
349}
350
351// ─────────────────────────────────────────────────────────────────────────────
352// WalStats
353// ─────────────────────────────────────────────────────────────────────────────
354
355/// Live statistics snapshot of a [`WalWriteAheadLog`].
356#[derive(Debug, Clone, Default, PartialEq, Eq)]
357pub struct WalStats {
358    /// Total entries ever written (including control entries).
359    pub total_entries: u64,
360    /// Total encoded bytes ever appended to segment buffers.
361    pub total_bytes: u64,
362    /// Number of currently active (uncommitted) transactions.
363    pub active_transactions: usize,
364    /// Number of segment buffers managed (always 1 in this implementation).
365    pub segments_count: usize,
366    /// Sequence number of the most recent checkpoint entry.
367    pub last_checkpoint_seq: u64,
368}
369
370// ─────────────────────────────────────────────────────────────────────────────
371// WriteAheadLog
372// ─────────────────────────────────────────────────────────────────────────────
373
374/// In-memory, crash-recovery Write-Ahead Log.
375///
376/// All entries are serialised into a `Vec<u8>` segment buffer using a
377/// length-framed binary format with FNV-1a-64 checksums.  The buffer can be
378/// exported, persisted externally, and then fed back into [`Self::recover`] on
379/// restart.
380///
381/// # Example
382///
383/// ```rust
384/// use ipfrs_storage::write_ahead_log::{WalWriteAheadLog, WalConfig, WalOpType};
385///
386/// let mut wal = WalWriteAheadLog::new(WalConfig::default());
387/// let seq = wal.write(b"hello".to_vec(), b"world".to_vec(), WalOpType::Put).unwrap();
388/// assert!(seq > 0);
389/// let data = wal.export_segment();
390/// assert!(!data.is_empty());
391/// ```
392pub struct WalWriteAheadLog {
393    /// Encoded entry bytes for the current segment.
394    segment: Vec<u8>,
395    /// All decoded entries in insertion order (mirrors the segment buffer).
396    entries: Vec<WalEntry>,
397    /// Active and recently-ended transaction records.
398    transactions: HashMap<u64, Transaction>,
399    /// Monotonically increasing sequence counter.
400    next_seq: u64,
401    /// Monotonically increasing transaction id counter.
402    next_tx_id: u64,
403    /// Configuration.
404    config: WalConfig,
405    /// Cumulative statistics.
406    stats: WalStats,
407    /// Sequence number of the most recent committed checkpoint entry.
408    last_checkpoint_seq: u64,
409    /// Write count since last auto-checkpoint.
410    writes_since_checkpoint: usize,
411}
412
413impl WalWriteAheadLog {
414    /// Creates a new WAL with the given configuration.
415    pub fn new(config: WalConfig) -> Self {
416        Self {
417            segment: Vec::new(),
418            entries: Vec::new(),
419            transactions: HashMap::new(),
420            next_seq: 1,
421            next_tx_id: 1,
422            config,
423            stats: WalStats {
424                segments_count: 1,
425                ..WalStats::default()
426            },
427            last_checkpoint_seq: 0,
428            writes_since_checkpoint: 0,
429        }
430    }
431
432    /// Returns the current wall-clock time in nanoseconds.
433    fn now_ns() -> u64 {
434        use std::time::{SystemTime, UNIX_EPOCH};
435        SystemTime::now()
436            .duration_since(UNIX_EPOCH)
437            .unwrap_or_default()
438            .as_nanos() as u64
439    }
440
441    /// Allocates the next sequence number.
442    fn alloc_seq(&mut self) -> u64 {
443        let s = self.next_seq;
444        self.next_seq += 1;
445        s
446    }
447
448    /// Allocates the next transaction id.
449    fn alloc_tx_id(&mut self) -> u64 {
450        let id = self.next_tx_id;
451        self.next_tx_id += 1;
452        id
453    }
454
455    /// Appends a pre-constructed [`WalEntry`] to the segment and the entry list.
456    fn append_entry(&mut self, entry: WalEntry) -> Result<u64, WalError> {
457        let encoded = entry.encode();
458        let encoded_len = encoded.len();
459
460        // Guard: segment size limit
461        if self.config.max_segment_size > 0
462            && self.segment.len() + encoded_len > self.config.max_segment_size
463        {
464            return Err(WalError::SegmentFull);
465        }
466
467        // Guard: max entries per segment
468        if self.config.max_entries_per_segment > 0
469            && self.entries.len() >= self.config.max_entries_per_segment
470        {
471            return Err(WalError::SegmentFull);
472        }
473
474        let seq = entry.seq_num;
475        self.segment.extend_from_slice(&encoded);
476        self.stats.total_entries += 1;
477        self.stats.total_bytes += encoded_len as u64;
478        self.entries.push(entry);
479
480        Ok(seq)
481    }
482
483    // ─────────────────────────────────────────────────────────────────────────
484    // Public API
485    // ─────────────────────────────────────────────────────────────────────────
486
487    /// Writes a standalone (non-transactional) entry and returns its sequence
488    /// number.  Auto-checkpoints when the configured interval is reached.
489    pub fn write(&mut self, key: Vec<u8>, value: Vec<u8>, op: WalOpType) -> Result<u64, WalError> {
490        let seq = self.alloc_seq();
491        let entry = WalEntry {
492            seq_num: seq,
493            tx_id: 0,
494            op_type: op,
495            key,
496            value,
497            timestamp: Self::now_ns(),
498            checksum: 0, // filled by encode()
499        };
500        let seq = self.append_entry(entry)?;
501
502        self.writes_since_checkpoint += 1;
503        if self.config.checkpoint_interval > 0
504            && self.writes_since_checkpoint >= self.config.checkpoint_interval
505        {
506            self.checkpoint()?;
507        }
508
509        Ok(seq)
510    }
511
512    /// Begins a new transaction.  Returns the transaction id.
513    pub fn begin_transaction(&mut self) -> u64 {
514        let tx_id = self.alloc_tx_id();
515        let seq = self.alloc_seq();
516        let entry = WalEntry {
517            seq_num: seq,
518            tx_id,
519            op_type: WalOpType::Begin,
520            key: Vec::new(),
521            value: Vec::new(),
522            timestamp: Self::now_ns(),
523            checksum: 0,
524        };
525        // Best-effort: silently ignore segment-full when starting a transaction
526        // so the caller can still inspect the error on the first real write.
527        let _ = self.append_entry(entry);
528
529        let tx = Transaction::new(tx_id);
530        self.transactions.insert(tx_id, tx);
531        self.stats.active_transactions = self
532            .transactions
533            .values()
534            .filter(|t| t.state == TxState::Active)
535            .count();
536
537        tx_id
538    }
539
540    /// Writes an entry within a transaction.  Returns the entry sequence number.
541    pub fn write_tx(
542        &mut self,
543        tx_id: u64,
544        key: Vec<u8>,
545        value: Vec<u8>,
546        op: WalOpType,
547    ) -> Result<u64, WalError> {
548        // Verify transaction is active.
549        {
550            let tx = self
551                .transactions
552                .get(&tx_id)
553                .ok_or(WalError::TransactionNotFound(tx_id))?;
554            if tx.state != TxState::Active {
555                return Err(WalError::TransactionNotFound(tx_id));
556            }
557        }
558
559        let seq = self.alloc_seq();
560        let entry = WalEntry {
561            seq_num: seq,
562            tx_id,
563            op_type: op,
564            key,
565            value,
566            timestamp: Self::now_ns(),
567            checksum: 0,
568        };
569
570        let seq = self.append_entry(entry.clone())?;
571
572        // Buffer entry in the transaction record.
573        if let Some(tx) = self.transactions.get_mut(&tx_id) {
574            tx.entries.push(entry);
575        }
576
577        Ok(seq)
578    }
579
580    /// Commits a transaction: writes the Commit marker and marks it committed.
581    pub fn commit_transaction(&mut self, tx_id: u64) -> Result<(), WalError> {
582        {
583            let tx = self
584                .transactions
585                .get(&tx_id)
586                .ok_or(WalError::TransactionNotFound(tx_id))?;
587            if tx.state != TxState::Active {
588                return Err(WalError::TransactionNotFound(tx_id));
589            }
590        }
591
592        let seq = self.alloc_seq();
593        let entry = WalEntry {
594            seq_num: seq,
595            tx_id,
596            op_type: WalOpType::Commit,
597            key: Vec::new(),
598            value: Vec::new(),
599            timestamp: Self::now_ns(),
600            checksum: 0,
601        };
602        self.append_entry(entry)?;
603
604        if let Some(tx) = self.transactions.get_mut(&tx_id) {
605            tx.state = TxState::Committed;
606        }
607
608        self.stats.active_transactions = self
609            .transactions
610            .values()
611            .filter(|t| t.state == TxState::Active)
612            .count();
613
614        Ok(())
615    }
616
617    /// Rolls back a transaction: writes the Rollback marker and marks it rolled back.
618    pub fn rollback_transaction(&mut self, tx_id: u64) -> Result<(), WalError> {
619        {
620            let tx = self
621                .transactions
622                .get(&tx_id)
623                .ok_or(WalError::TransactionNotFound(tx_id))?;
624            if tx.state != TxState::Active {
625                return Err(WalError::TransactionNotFound(tx_id));
626            }
627        }
628
629        let seq = self.alloc_seq();
630        let entry = WalEntry {
631            seq_num: seq,
632            tx_id,
633            op_type: WalOpType::Rollback,
634            key: Vec::new(),
635            value: Vec::new(),
636            timestamp: Self::now_ns(),
637            checksum: 0,
638        };
639        self.append_entry(entry)?;
640
641        if let Some(tx) = self.transactions.get_mut(&tx_id) {
642            tx.state = TxState::RolledBack;
643        }
644
645        self.stats.active_transactions = self
646            .transactions
647            .values()
648            .filter(|t| t.state == TxState::Active)
649            .count();
650
651        Ok(())
652    }
653
654    /// Creates a checkpoint entry and returns its sequence number.
655    ///
656    /// All entries whose `seq_num` is less than or equal to the previous
657    /// checkpoint sequence, **and** which do not belong to an active
658    /// transaction, are removed from the in-memory entry list and the segment
659    /// is rebuilt.
660    pub fn checkpoint(&mut self) -> Result<u64, WalError> {
661        let seq = self.alloc_seq();
662        let entry = WalEntry {
663            seq_num: seq,
664            tx_id: 0,
665            op_type: WalOpType::Checkpoint,
666            key: Vec::new(),
667            value: Vec::new(),
668            timestamp: Self::now_ns(),
669            checksum: 0,
670        };
671        self.append_entry(entry)?;
672
673        // Collect tx_ids of currently active transactions so we don't discard
674        // entries they might still need.
675        let active_tx_ids: std::collections::HashSet<u64> = self
676            .transactions
677            .values()
678            .filter(|t| t.state == TxState::Active)
679            .map(|t| t.tx_id)
680            .collect();
681
682        let truncate_before = self.last_checkpoint_seq;
683
684        // Retain entries that are either:
685        // - From an active transaction (cannot discard yet), OR
686        // - At or after the previous checkpoint sequence.
687        self.entries
688            .retain(|e| e.seq_num > truncate_before || active_tx_ids.contains(&e.tx_id));
689
690        // Rebuild the segment buffer from the retained entries.
691        let mut rebuilt = Vec::with_capacity(self.segment.len());
692        for e in &self.entries {
693            rebuilt.extend_from_slice(&e.encode());
694        }
695        self.segment = rebuilt;
696
697        self.last_checkpoint_seq = seq;
698        self.stats.last_checkpoint_seq = seq;
699        self.writes_since_checkpoint = 0;
700
701        Ok(seq)
702    }
703
704    /// Scans a raw byte buffer, decodes all valid entries, validates checksums,
705    /// and returns a [`RecoveryResult`].
706    ///
707    /// Entries belonging to transactions that were never committed (i.e. no
708    /// matching Commit entry appears) are discarded from the result.
709    pub fn recover(data: &[u8]) -> Result<RecoveryResult, WalError> {
710        let mut all_entries: Vec<WalEntry> = Vec::new();
711        let mut pos = 0usize;
712
713        while pos < data.len() {
714            // Skip any padding / garbage between entries gracefully if the
715            // magic bytes don't match — attempt to find the next magic marker.
716            if pos + 4 <= data.len() && data[pos..pos + 4] != WAL_MAGIC {
717                pos += 1;
718                continue;
719            }
720
721            match WalEntry::decode(data, pos) {
722                Ok((entry, consumed)) => {
723                    pos += consumed;
724                    all_entries.push(entry);
725                }
726                Err(WalError::ChecksumMismatch { .. }) => {
727                    // Stop on first checksum failure to avoid replaying garbage.
728                    break;
729                }
730                Err(_) => {
731                    // Truncated entry — stop scanning.
732                    break;
733                }
734            }
735        }
736
737        // Determine which transactions are fully committed.
738        let mut committed_tx: std::collections::HashSet<u64> = std::collections::HashSet::new();
739        let mut rolled_back_tx: std::collections::HashSet<u64> = std::collections::HashSet::new();
740        let mut seen_tx: std::collections::HashSet<u64> = std::collections::HashSet::new();
741
742        for e in &all_entries {
743            if e.tx_id != 0 {
744                seen_tx.insert(e.tx_id);
745                match e.op_type {
746                    WalOpType::Commit => {
747                        committed_tx.insert(e.tx_id);
748                    }
749                    WalOpType::Rollback => {
750                        rolled_back_tx.insert(e.tx_id);
751                    }
752                    _ => {}
753                }
754            }
755        }
756
757        let last_seq_num = all_entries.iter().map(|e| e.seq_num).max().unwrap_or(0);
758
759        let entries_recovered = all_entries.len();
760        let transactions_recovered = seen_tx.len();
761
762        // Count discarded entries: entries from transactions that were started
763        // but never committed (and not rolled back) are considered partial.
764        let partial_tx_ids: std::collections::HashSet<u64> = seen_tx
765            .iter()
766            .filter(|id| !committed_tx.contains(id) && !rolled_back_tx.contains(id))
767            .copied()
768            .collect();
769
770        let partial_tx_discarded: usize = all_entries
771            .iter()
772            .filter(|e| e.tx_id != 0 && partial_tx_ids.contains(&e.tx_id))
773            .count();
774
775        // Count replayable entries: standalone committed Put/Delete, or
776        // transactional Put/Delete from committed transactions.
777        let entries_replayed: usize = all_entries
778            .iter()
779            .filter(|e| {
780                let is_data_op = matches!(e.op_type, WalOpType::Put | WalOpType::Delete);
781                if !is_data_op {
782                    return false;
783                }
784                if e.tx_id == 0 {
785                    // Standalone entry — always replay.
786                    return true;
787                }
788                // Only replay if the transaction committed.
789                committed_tx.contains(&e.tx_id)
790            })
791            .count();
792
793        Ok(RecoveryResult {
794            entries_recovered,
795            transactions_recovered,
796            entries_replayed,
797            partial_tx_discarded,
798            last_seq_num,
799        })
800    }
801
802    /// Iterates over the provided entries and calls `handler` for each committed
803    /// Put or Delete entry.  Returns the number of entries passed to the handler.
804    ///
805    /// Standalone (tx_id == 0) Put/Delete entries are always replayed.
806    /// Transactional entries are only replayed when their transaction's Commit
807    /// marker is present in `entries`.
808    pub fn replay(entries: &[WalEntry], handler: &mut dyn FnMut(&WalEntry)) -> usize {
809        // First pass: determine which transactions committed.
810        let committed_tx: std::collections::HashSet<u64> = entries
811            .iter()
812            .filter(|e| e.op_type == WalOpType::Commit && e.tx_id != 0)
813            .map(|e| e.tx_id)
814            .collect();
815
816        // Second pass: invoke handler for each replayable entry in order.
817        let mut count = 0usize;
818        for entry in entries {
819            let is_data = matches!(entry.op_type, WalOpType::Put | WalOpType::Delete);
820            if !is_data {
821                continue;
822            }
823            let replayable = entry.tx_id == 0 || committed_tx.contains(&entry.tx_id);
824            if replayable {
825                handler(entry);
826                count += 1;
827            }
828        }
829        count
830    }
831
832    /// Returns a clone of the current segment buffer.
833    pub fn export_segment(&self) -> Vec<u8> {
834        self.segment.clone()
835    }
836
837    /// Returns a snapshot of current statistics.
838    pub fn stats(&self) -> WalStats {
839        WalStats {
840            total_entries: self.stats.total_entries,
841            total_bytes: self.stats.total_bytes,
842            active_transactions: self.stats.active_transactions,
843            segments_count: 1,
844            last_checkpoint_seq: self.last_checkpoint_seq,
845        }
846    }
847
848    /// Removes all entries with `seq_num < seq_num` from the in-memory list and
849    /// rebuilds the segment buffer.  Returns the number of entries removed.
850    pub fn truncate_before(&mut self, seq_num: u64) -> usize {
851        let before = self.entries.len();
852        self.entries.retain(|e| e.seq_num >= seq_num);
853        let removed = before - self.entries.len();
854
855        if removed > 0 {
856            let mut rebuilt = Vec::with_capacity(self.segment.len());
857            for e in &self.entries {
858                rebuilt.extend_from_slice(&e.encode());
859            }
860            self.segment = rebuilt;
861        }
862
863        removed
864    }
865
866    /// Returns the total number of in-memory entries.
867    pub fn entry_count(&self) -> usize {
868        self.entries.len()
869    }
870}
871
872// ─────────────────────────────────────────────────────────────────────────────
873// Tests
874// ─────────────────────────────────────────────────────────────────────────────
875
876#[cfg(test)]
877mod tests {
878    use super::*;
879
880    // ── Inline xorshift64 PRNG (no rand crate) ────────────────────────────────
881
882    struct Xorshift64 {
883        state: u64,
884    }
885
886    impl Xorshift64 {
887        fn new(seed: u64) -> Self {
888            Self {
889                state: if seed == 0 {
890                    0xDEAD_BEEF_CAFE_1234
891                } else {
892                    seed
893                },
894            }
895        }
896
897        fn next(&mut self) -> u64 {
898            self.state ^= self.state << 13;
899            self.state ^= self.state >> 7;
900            self.state ^= self.state << 17;
901            self.state
902        }
903
904        fn next_bytes(&mut self, len: usize) -> Vec<u8> {
905            let mut out = Vec::with_capacity(len);
906            while out.len() < len {
907                let v = self.next();
908                out.extend_from_slice(&v.to_le_bytes());
909            }
910            out.truncate(len);
911            out
912        }
913    }
914
915    fn default_wal() -> WalWriteAheadLog {
916        WalWriteAheadLog::new(WalConfig::default())
917    }
918
919    // ── 1. FNV-1a hash ────────────────────────────────────────────────────────
920
921    #[test]
922    fn test_fnv1a_64_empty() {
923        assert_eq!(fnv1a_64(&[]), 14_695_981_039_346_656_037u64);
924    }
925
926    #[test]
927    fn test_fnv1a_64_known_value() {
928        // "foobar" → well-known FNV-1a-64 value
929        let h = fnv1a_64(b"foobar");
930        assert_ne!(h, 0);
931        // Deterministic: same input same output
932        assert_eq!(h, fnv1a_64(b"foobar"));
933    }
934
935    #[test]
936    fn test_fnv1a_64_differs_by_one_byte() {
937        let a = fnv1a_64(b"hello");
938        let b = fnv1a_64(b"hEllo");
939        assert_ne!(a, b);
940    }
941
942    // ── 2. WalOpType round-trip ───────────────────────────────────────────────
943
944    #[test]
945    fn test_wal_op_type_from_u8_all_valid() {
946        for (byte, expected) in &[
947            (1u8, WalOpType::Put),
948            (2, WalOpType::Delete),
949            (3, WalOpType::Begin),
950            (4, WalOpType::Commit),
951            (5, WalOpType::Rollback),
952            (6, WalOpType::Checkpoint),
953        ] {
954            assert_eq!(WalOpType::from_u8(*byte), Some(*expected));
955        }
956    }
957
958    #[test]
959    fn test_wal_op_type_from_u8_invalid() {
960        assert_eq!(WalOpType::from_u8(0), None);
961        assert_eq!(WalOpType::from_u8(7), None);
962        assert_eq!(WalOpType::from_u8(255), None);
963    }
964
965    // ── 3. WalEntry encode / decode round-trips ───────────────────────────────
966
967    #[test]
968    fn test_encode_decode_put_entry() {
969        let key = b"my-key".to_vec();
970        let value = b"my-value-data".to_vec();
971        let entry = WalEntry {
972            seq_num: 42,
973            tx_id: 0,
974            op_type: WalOpType::Put,
975            key: key.clone(),
976            value: value.clone(),
977            timestamp: 123_456_789,
978            checksum: 0,
979        };
980        let encoded = entry.encode();
981        // Checksum is computed by encode, so retrieve it
982        let cs = u64::from_le_bytes(encoded[encoded.len() - 8..].try_into().unwrap());
983
984        let (decoded, consumed) = WalEntry::decode(&encoded, 0).unwrap();
985        assert_eq!(consumed, encoded.len());
986        assert_eq!(decoded.seq_num, 42);
987        assert_eq!(decoded.tx_id, 0);
988        assert_eq!(decoded.op_type, WalOpType::Put);
989        assert_eq!(decoded.key, key);
990        assert_eq!(decoded.value, value);
991        assert_eq!(decoded.timestamp, 123_456_789);
992        assert_eq!(decoded.checksum, cs);
993    }
994
995    #[test]
996    fn test_encode_decode_delete_entry() {
997        let entry = WalEntry {
998            seq_num: 1,
999            tx_id: 5,
1000            op_type: WalOpType::Delete,
1001            key: b"del-key".to_vec(),
1002            value: Vec::new(),
1003            timestamp: 0,
1004            checksum: 0,
1005        };
1006        let enc = entry.encode();
1007        let (dec, _) = WalEntry::decode(&enc, 0).unwrap();
1008        assert_eq!(dec.op_type, WalOpType::Delete);
1009        assert_eq!(dec.key, b"del-key");
1010        assert!(dec.value.is_empty());
1011    }
1012
1013    #[test]
1014    fn test_encode_decode_empty_key_value() {
1015        let entry = WalEntry {
1016            seq_num: 99,
1017            tx_id: 0,
1018            op_type: WalOpType::Checkpoint,
1019            key: Vec::new(),
1020            value: Vec::new(),
1021            timestamp: 0,
1022            checksum: 0,
1023        };
1024        let enc = entry.encode();
1025        let (dec, consumed) = WalEntry::decode(&enc, 0).unwrap();
1026        assert_eq!(consumed, enc.len());
1027        assert_eq!(dec.op_type, WalOpType::Checkpoint);
1028        assert!(dec.key.is_empty());
1029        assert!(dec.value.is_empty());
1030    }
1031
1032    #[test]
1033    fn test_encode_decode_with_offset() {
1034        let padding = vec![0u8; 16];
1035        let entry = WalEntry {
1036            seq_num: 7,
1037            tx_id: 3,
1038            op_type: WalOpType::Put,
1039            key: b"k".to_vec(),
1040            value: b"v".to_vec(),
1041            timestamp: 1,
1042            checksum: 0,
1043        };
1044        let mut buf = padding.clone();
1045        buf.extend_from_slice(&entry.encode());
1046
1047        let (dec, _) = WalEntry::decode(&buf, 16).unwrap();
1048        assert_eq!(dec.seq_num, 7);
1049    }
1050
1051    #[test]
1052    fn test_encode_decode_large_payload() {
1053        let mut rng = Xorshift64::new(0xABCDEF01);
1054        let key = rng.next_bytes(512);
1055        let value = rng.next_bytes(4096);
1056        let entry = WalEntry {
1057            seq_num: 1000,
1058            tx_id: 0,
1059            op_type: WalOpType::Put,
1060            key: key.clone(),
1061            value: value.clone(),
1062            timestamp: 999,
1063            checksum: 0,
1064        };
1065        let enc = entry.encode();
1066        let (dec, _) = WalEntry::decode(&enc, 0).unwrap();
1067        assert_eq!(dec.key, key);
1068        assert_eq!(dec.value, value);
1069    }
1070
1071    // ── 4. Checksum validation ────────────────────────────────────────────────
1072
1073    #[test]
1074    fn test_checksum_mismatch_detected() {
1075        let entry = WalEntry {
1076            seq_num: 1,
1077            tx_id: 0,
1078            op_type: WalOpType::Put,
1079            key: b"k".to_vec(),
1080            value: b"v".to_vec(),
1081            timestamp: 0,
1082            checksum: 0,
1083        };
1084        let mut enc = entry.encode();
1085        // Flip a byte in the value region (byte 30 is within value for short entries)
1086        let flip_pos = enc.len() - 10; // well before checksum
1087        enc[flip_pos] ^= 0xFF;
1088
1089        let result = WalEntry::decode(&enc, 0);
1090        assert!(matches!(result, Err(WalError::ChecksumMismatch { .. })));
1091    }
1092
1093    #[test]
1094    fn test_invalid_magic_detected() {
1095        let entry = WalEntry {
1096            seq_num: 2,
1097            tx_id: 0,
1098            op_type: WalOpType::Put,
1099            key: b"k".to_vec(),
1100            value: b"v".to_vec(),
1101            timestamp: 0,
1102            checksum: 0,
1103        };
1104        let mut enc = entry.encode();
1105        enc[0] = b'Z'; // corrupt magic
1106
1107        let result = WalEntry::decode(&enc, 0);
1108        assert!(matches!(result, Err(WalError::InvalidMagic)));
1109    }
1110
1111    #[test]
1112    fn test_corrupted_entry_too_short() {
1113        let data = vec![b'W', b'A', b'L', b'X', 0x01]; // too short
1114        let result = WalEntry::decode(&data, 0);
1115        assert!(matches!(result, Err(WalError::CorruptedEntry(_))));
1116    }
1117
1118    #[test]
1119    fn test_corrupted_entry_truncated_key() {
1120        let entry = WalEntry {
1121            seq_num: 5,
1122            tx_id: 0,
1123            op_type: WalOpType::Put,
1124            key: b"longkey12345678".to_vec(),
1125            value: b"val".to_vec(),
1126            timestamp: 0,
1127            checksum: 0,
1128        };
1129        let mut enc = entry.encode();
1130        enc.truncate(enc.len() / 2); // chop in half
1131        let result = WalEntry::decode(&enc, 0);
1132        assert!(result.is_err());
1133    }
1134
1135    // ── 5. WriteAheadLog::write ───────────────────────────────────────────────
1136
1137    #[test]
1138    fn test_write_returns_sequential_seq_nums() {
1139        let mut wal = default_wal();
1140        let s1 = wal
1141            .write(b"k1".to_vec(), b"v1".to_vec(), WalOpType::Put)
1142            .unwrap();
1143        let s2 = wal
1144            .write(b"k2".to_vec(), b"v2".to_vec(), WalOpType::Put)
1145            .unwrap();
1146        let s3 = wal
1147            .write(b"k3".to_vec(), b"v3".to_vec(), WalOpType::Delete)
1148            .unwrap();
1149        assert!(s1 < s2);
1150        assert!(s2 < s3);
1151    }
1152
1153    #[test]
1154    fn test_write_increases_entry_count() {
1155        let mut wal = default_wal();
1156        assert_eq!(wal.entry_count(), 0);
1157        wal.write(b"a".to_vec(), b"b".to_vec(), WalOpType::Put)
1158            .unwrap();
1159        assert_eq!(wal.entry_count(), 1);
1160        wal.write(b"c".to_vec(), b"d".to_vec(), WalOpType::Put)
1161            .unwrap();
1162        assert_eq!(wal.entry_count(), 2);
1163    }
1164
1165    #[test]
1166    fn test_write_segment_not_empty() {
1167        let mut wal = default_wal();
1168        wal.write(b"key".to_vec(), b"val".to_vec(), WalOpType::Put)
1169            .unwrap();
1170        assert!(!wal.export_segment().is_empty());
1171    }
1172
1173    #[test]
1174    fn test_stats_tracks_entries_and_bytes() {
1175        let mut wal = default_wal();
1176        wal.write(b"x".to_vec(), b"y".to_vec(), WalOpType::Put)
1177            .unwrap();
1178        wal.write(b"x".to_vec(), Vec::new(), WalOpType::Delete)
1179            .unwrap();
1180        let s = wal.stats();
1181        assert_eq!(s.total_entries, 2);
1182        assert!(s.total_bytes > 0);
1183    }
1184
1185    // ── 6. begin / commit / rollback ─────────────────────────────────────────
1186
1187    #[test]
1188    fn test_begin_transaction_returns_unique_ids() {
1189        let mut wal = default_wal();
1190        let t1 = wal.begin_transaction();
1191        let t2 = wal.begin_transaction();
1192        let t3 = wal.begin_transaction();
1193        assert_ne!(t1, t2);
1194        assert_ne!(t2, t3);
1195    }
1196
1197    #[test]
1198    fn test_write_tx_valid_transaction() {
1199        let mut wal = default_wal();
1200        let tx = wal.begin_transaction();
1201        let seq = wal
1202            .write_tx(tx, b"key".to_vec(), b"value".to_vec(), WalOpType::Put)
1203            .unwrap();
1204        assert!(seq > 0);
1205    }
1206
1207    #[test]
1208    fn test_write_tx_unknown_transaction_errors() {
1209        let mut wal = default_wal();
1210        let result = wal.write_tx(9999, b"k".to_vec(), b"v".to_vec(), WalOpType::Put);
1211        assert!(matches!(result, Err(WalError::TransactionNotFound(9999))));
1212    }
1213
1214    #[test]
1215    fn test_commit_transaction_succeeds() {
1216        let mut wal = default_wal();
1217        let tx = wal.begin_transaction();
1218        wal.write_tx(tx, b"k".to_vec(), b"v".to_vec(), WalOpType::Put)
1219            .unwrap();
1220        wal.commit_transaction(tx).unwrap();
1221    }
1222
1223    #[test]
1224    fn test_commit_unknown_transaction_errors() {
1225        let mut wal = default_wal();
1226        let result = wal.commit_transaction(42);
1227        assert!(matches!(result, Err(WalError::TransactionNotFound(42))));
1228    }
1229
1230    #[test]
1231    fn test_rollback_transaction_succeeds() {
1232        let mut wal = default_wal();
1233        let tx = wal.begin_transaction();
1234        wal.write_tx(tx, b"k".to_vec(), b"v".to_vec(), WalOpType::Put)
1235            .unwrap();
1236        wal.rollback_transaction(tx).unwrap();
1237    }
1238
1239    #[test]
1240    fn test_rollback_unknown_transaction_errors() {
1241        let mut wal = default_wal();
1242        let result = wal.rollback_transaction(77);
1243        assert!(matches!(result, Err(WalError::TransactionNotFound(77))));
1244    }
1245
1246    #[test]
1247    fn test_double_commit_errors() {
1248        let mut wal = default_wal();
1249        let tx = wal.begin_transaction();
1250        wal.commit_transaction(tx).unwrap();
1251        // Second commit should fail — tx is no longer active.
1252        let result = wal.commit_transaction(tx);
1253        assert!(result.is_err());
1254    }
1255
1256    #[test]
1257    fn test_double_rollback_errors() {
1258        let mut wal = default_wal();
1259        let tx = wal.begin_transaction();
1260        wal.rollback_transaction(tx).unwrap();
1261        let result = wal.rollback_transaction(tx);
1262        assert!(result.is_err());
1263    }
1264
1265    #[test]
1266    fn test_active_transactions_count_decreases_after_commit() {
1267        let mut wal = default_wal();
1268        let t1 = wal.begin_transaction();
1269        let t2 = wal.begin_transaction();
1270        assert_eq!(wal.stats().active_transactions, 2);
1271        wal.commit_transaction(t1).unwrap();
1272        assert_eq!(wal.stats().active_transactions, 1);
1273        wal.rollback_transaction(t2).unwrap();
1274        assert_eq!(wal.stats().active_transactions, 0);
1275    }
1276
1277    // ── 7. Checkpoint ─────────────────────────────────────────────────────────
1278
1279    #[test]
1280    fn test_checkpoint_returns_seq_num() {
1281        let mut wal = default_wal();
1282        wal.write(b"a".to_vec(), b"b".to_vec(), WalOpType::Put)
1283            .unwrap();
1284        let cp_seq = wal.checkpoint().unwrap();
1285        assert!(cp_seq > 0);
1286        assert_eq!(wal.stats().last_checkpoint_seq, cp_seq);
1287    }
1288
1289    #[test]
1290    fn test_checkpoint_truncates_old_entries() {
1291        let mut wal = default_wal();
1292        for i in 0u8..50 {
1293            wal.write(vec![i], vec![i], WalOpType::Put).unwrap();
1294        }
1295        let before = wal.entry_count();
1296        wal.checkpoint().unwrap();
1297        // A second checkpoint should truncate the entries before the first.
1298        for i in 50u8..100 {
1299            wal.write(vec![i], vec![i], WalOpType::Put).unwrap();
1300        }
1301        wal.checkpoint().unwrap();
1302        let after = wal.entry_count();
1303        // After two checkpoints the oldest batch should be gone.
1304        assert!(after < before + 51); // strictly fewer than before + one checkpoint entry
1305    }
1306
1307    #[test]
1308    fn test_checkpoint_preserves_active_tx_entries() {
1309        let mut wal = default_wal();
1310        let tx = wal.begin_transaction();
1311        wal.write_tx(tx, b"k".to_vec(), b"v".to_vec(), WalOpType::Put)
1312            .unwrap();
1313        wal.checkpoint().unwrap();
1314        // Active tx entry must survive the checkpoint.
1315        assert!(wal.entries.iter().any(|e| e.tx_id == tx));
1316    }
1317
1318    #[test]
1319    fn test_auto_checkpoint_triggers() {
1320        let config = WalConfig {
1321            checkpoint_interval: 3,
1322            ..WalConfig::default()
1323        };
1324        let mut wal = WalWriteAheadLog::new(config);
1325        for i in 0u8..3 {
1326            wal.write(vec![i], vec![i], WalOpType::Put).unwrap();
1327        }
1328        // After 3 writes the auto-checkpoint should have fired.
1329        assert_eq!(wal.writes_since_checkpoint, 0);
1330        assert!(wal.last_checkpoint_seq > 0);
1331    }
1332
1333    // ── 8. Segment export / rebuild ───────────────────────────────────────────
1334
1335    #[test]
1336    fn test_export_segment_contains_all_entries() {
1337        let mut wal = default_wal();
1338        wal.write(b"k1".to_vec(), b"v1".to_vec(), WalOpType::Put)
1339            .unwrap();
1340        wal.write(b"k2".to_vec(), b"v2".to_vec(), WalOpType::Delete)
1341            .unwrap();
1342        let seg = wal.export_segment();
1343        // Parse back: should find exactly 2 entries.
1344        let mut pos = 0usize;
1345        let mut count = 0usize;
1346        while pos < seg.len() {
1347            let (_, consumed) = WalEntry::decode(&seg, pos).unwrap();
1348            pos += consumed;
1349            count += 1;
1350        }
1351        assert_eq!(count, 2);
1352    }
1353
1354    #[test]
1355    fn test_export_segment_is_deterministic() {
1356        let mut wal = default_wal();
1357        wal.write(b"key".to_vec(), b"val".to_vec(), WalOpType::Put)
1358            .unwrap();
1359        let a = wal.export_segment();
1360        let b = wal.export_segment();
1361        assert_eq!(a, b);
1362    }
1363
1364    // ── 9. Recovery ───────────────────────────────────────────────────────────
1365
1366    #[test]
1367    fn test_recover_clean_log() {
1368        let mut wal = default_wal();
1369        wal.write(b"a".to_vec(), b"1".to_vec(), WalOpType::Put)
1370            .unwrap();
1371        wal.write(b"b".to_vec(), b"2".to_vec(), WalOpType::Put)
1372            .unwrap();
1373        wal.write(b"c".to_vec(), Vec::new(), WalOpType::Delete)
1374            .unwrap();
1375        let seg = wal.export_segment();
1376
1377        let rr = WalWriteAheadLog::recover(&seg).unwrap();
1378        assert_eq!(rr.entries_recovered, 3);
1379        assert_eq!(rr.entries_replayed, 3); // all standalone
1380        assert_eq!(rr.partial_tx_discarded, 0);
1381        assert_eq!(rr.transactions_recovered, 0);
1382        assert!(rr.last_seq_num >= 3);
1383    }
1384
1385    #[test]
1386    fn test_recover_committed_transaction() {
1387        let mut wal = default_wal();
1388        let tx = wal.begin_transaction();
1389        wal.write_tx(tx, b"k".to_vec(), b"v".to_vec(), WalOpType::Put)
1390            .unwrap();
1391        wal.commit_transaction(tx).unwrap();
1392        let seg = wal.export_segment();
1393
1394        let rr = WalWriteAheadLog::recover(&seg).unwrap();
1395        assert_eq!(rr.entries_replayed, 1); // the Put inside the committed tx
1396        assert_eq!(rr.partial_tx_discarded, 0);
1397        assert_eq!(rr.transactions_recovered, 1);
1398    }
1399
1400    #[test]
1401    fn test_recover_partial_tx_discarded() {
1402        let mut wal = default_wal();
1403        let tx = wal.begin_transaction();
1404        wal.write_tx(tx, b"k".to_vec(), b"v".to_vec(), WalOpType::Put)
1405            .unwrap();
1406        // Deliberately do NOT commit — simulate crash.
1407        let seg = wal.export_segment();
1408
1409        let rr = WalWriteAheadLog::recover(&seg).unwrap();
1410        assert_eq!(rr.entries_replayed, 0); // tx never committed
1411        assert!(rr.partial_tx_discarded > 0);
1412    }
1413
1414    #[test]
1415    fn test_recover_rolled_back_tx_not_replayed() {
1416        let mut wal = default_wal();
1417        let tx = wal.begin_transaction();
1418        wal.write_tx(tx, b"k".to_vec(), b"v".to_vec(), WalOpType::Put)
1419            .unwrap();
1420        wal.rollback_transaction(tx).unwrap();
1421        let seg = wal.export_segment();
1422
1423        let rr = WalWriteAheadLog::recover(&seg).unwrap();
1424        assert_eq!(rr.entries_replayed, 0);
1425        // Rolled back is not "partial" — it has an explicit end marker.
1426        assert_eq!(rr.partial_tx_discarded, 0);
1427    }
1428
1429    #[test]
1430    fn test_recover_mixed_committed_and_partial() {
1431        let mut wal = default_wal();
1432        // Committed tx
1433        let t1 = wal.begin_transaction();
1434        wal.write_tx(t1, b"k1".to_vec(), b"v1".to_vec(), WalOpType::Put)
1435            .unwrap();
1436        wal.commit_transaction(t1).unwrap();
1437        // Partial tx (no commit)
1438        let t2 = wal.begin_transaction();
1439        wal.write_tx(t2, b"k2".to_vec(), b"v2".to_vec(), WalOpType::Put)
1440            .unwrap();
1441        wal.write_tx(t2, b"k3".to_vec(), b"v3".to_vec(), WalOpType::Put)
1442            .unwrap();
1443        let seg = wal.export_segment();
1444
1445        let rr = WalWriteAheadLog::recover(&seg).unwrap();
1446        assert_eq!(rr.entries_replayed, 1); // only t1's Put
1447        assert!(rr.partial_tx_discarded >= 2); // t2's 2 Puts + Begin
1448        assert_eq!(rr.transactions_recovered, 2);
1449    }
1450
1451    #[test]
1452    fn test_recover_empty_buffer() {
1453        let rr = WalWriteAheadLog::recover(&[]).unwrap();
1454        assert_eq!(rr.entries_recovered, 0);
1455        assert_eq!(rr.last_seq_num, 0);
1456    }
1457
1458    #[test]
1459    fn test_recover_garbage_prefix_skipped() {
1460        let mut wal = default_wal();
1461        wal.write(b"k".to_vec(), b"v".to_vec(), WalOpType::Put)
1462            .unwrap();
1463        let mut seg = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00];
1464        seg.extend_from_slice(&wal.export_segment());
1465
1466        let rr = WalWriteAheadLog::recover(&seg).unwrap();
1467        assert_eq!(rr.entries_recovered, 1);
1468    }
1469
1470    #[test]
1471    fn test_recover_last_seq_num_correct() {
1472        let mut wal = default_wal();
1473        for i in 0u8..10 {
1474            wal.write(vec![i], vec![i], WalOpType::Put).unwrap();
1475        }
1476        let seg = wal.export_segment();
1477        let rr = WalWriteAheadLog::recover(&seg).unwrap();
1478        assert_eq!(rr.last_seq_num, 10);
1479    }
1480
1481    // ── 10. Replay ────────────────────────────────────────────────────────────
1482
1483    #[test]
1484    fn test_replay_standalone_entries() {
1485        let mut wal = default_wal();
1486        wal.write(b"k1".to_vec(), b"v1".to_vec(), WalOpType::Put)
1487            .unwrap();
1488        wal.write(b"k2".to_vec(), b"v2".to_vec(), WalOpType::Put)
1489            .unwrap();
1490        wal.write(b"k3".to_vec(), Vec::new(), WalOpType::Delete)
1491            .unwrap();
1492
1493        let entries: Vec<WalEntry> = wal.entries.clone();
1494        let mut replayed = Vec::new();
1495        let count = WalWriteAheadLog::replay(&entries, &mut |e| {
1496            replayed.push(e.clone());
1497        });
1498        assert_eq!(count, 3);
1499        assert_eq!(replayed.len(), 3);
1500    }
1501
1502    #[test]
1503    fn test_replay_skips_control_entries() {
1504        let mut wal = default_wal();
1505        wal.write(b"k".to_vec(), b"v".to_vec(), WalOpType::Put)
1506            .unwrap();
1507        wal.checkpoint().unwrap(); // adds a Checkpoint entry
1508
1509        let entries: Vec<WalEntry> = wal.entries.clone();
1510        let mut count_called = 0usize;
1511        WalWriteAheadLog::replay(&entries, &mut |_| {
1512            count_called += 1;
1513        });
1514        // Only the Put entry should be replayed, not the Checkpoint.
1515        assert_eq!(count_called, 1);
1516    }
1517
1518    #[test]
1519    fn test_replay_skips_uncommitted_tx() {
1520        let mut wal = default_wal();
1521        let tx = wal.begin_transaction();
1522        wal.write_tx(tx, b"k".to_vec(), b"v".to_vec(), WalOpType::Put)
1523            .unwrap();
1524        // No commit
1525
1526        let entries: Vec<WalEntry> = wal.entries.clone();
1527        let mut count_called = 0usize;
1528        WalWriteAheadLog::replay(&entries, &mut |_| {
1529            count_called += 1;
1530        });
1531        assert_eq!(count_called, 0);
1532    }
1533
1534    #[test]
1535    fn test_replay_includes_committed_tx() {
1536        let mut wal = default_wal();
1537        let tx = wal.begin_transaction();
1538        wal.write_tx(tx, b"k".to_vec(), b"v".to_vec(), WalOpType::Put)
1539            .unwrap();
1540        wal.write_tx(tx, b"k2".to_vec(), Vec::new(), WalOpType::Delete)
1541            .unwrap();
1542        wal.commit_transaction(tx).unwrap();
1543
1544        let entries: Vec<WalEntry> = wal.entries.clone();
1545        let mut seen_keys: Vec<Vec<u8>> = Vec::new();
1546        WalWriteAheadLog::replay(&entries, &mut |e| {
1547            seen_keys.push(e.key.clone());
1548        });
1549        assert_eq!(seen_keys.len(), 2);
1550        assert!(seen_keys.contains(&b"k".to_vec()));
1551        assert!(seen_keys.contains(&b"k2".to_vec()));
1552    }
1553
1554    #[test]
1555    fn test_replay_order_preserved() {
1556        let mut wal = default_wal();
1557        let keys: &[&[u8]] = &[b"alpha", b"beta_", b"gamma", b"delta"];
1558        for k in keys {
1559            wal.write(k.to_vec(), b"v".to_vec(), WalOpType::Put)
1560                .unwrap();
1561        }
1562        let entries: Vec<WalEntry> = wal.entries.clone();
1563        let mut order: Vec<Vec<u8>> = Vec::new();
1564        WalWriteAheadLog::replay(&entries, &mut |e| {
1565            order.push(e.key.clone());
1566        });
1567        for (i, k) in keys.iter().enumerate() {
1568            assert_eq!(order[i], k.to_vec());
1569        }
1570    }
1571
1572    #[test]
1573    fn test_replay_empty_entries() {
1574        let count = WalWriteAheadLog::replay(&[], &mut |_| {});
1575        assert_eq!(count, 0);
1576    }
1577
1578    // ── 11. Truncate ──────────────────────────────────────────────────────────
1579
1580    #[test]
1581    fn test_truncate_before_removes_entries() {
1582        let mut wal = default_wal();
1583        for i in 1u8..=10 {
1584            wal.write(vec![i], vec![i], WalOpType::Put).unwrap();
1585        }
1586        assert_eq!(wal.entry_count(), 10);
1587        let removed = wal.truncate_before(6);
1588        assert_eq!(removed, 5); // seq 1-5 removed
1589        assert_eq!(wal.entry_count(), 5);
1590    }
1591
1592    #[test]
1593    fn test_truncate_before_zero_removes_nothing() {
1594        let mut wal = default_wal();
1595        wal.write(b"k".to_vec(), b"v".to_vec(), WalOpType::Put)
1596            .unwrap();
1597        let removed = wal.truncate_before(0);
1598        assert_eq!(removed, 0);
1599        assert_eq!(wal.entry_count(), 1);
1600    }
1601
1602    #[test]
1603    fn test_truncate_before_large_seq_removes_all() {
1604        let mut wal = default_wal();
1605        for i in 0u8..5 {
1606            wal.write(vec![i], vec![i], WalOpType::Put).unwrap();
1607        }
1608        let removed = wal.truncate_before(u64::MAX);
1609        assert_eq!(removed, 5);
1610        assert_eq!(wal.entry_count(), 0);
1611        assert!(wal.export_segment().is_empty());
1612    }
1613
1614    #[test]
1615    fn test_truncate_rebuilds_segment() {
1616        let mut wal = default_wal();
1617        for i in 1u8..=6 {
1618            wal.write(vec![i], vec![i], WalOpType::Put).unwrap();
1619        }
1620        let seg_before = wal.export_segment().len();
1621        wal.truncate_before(4);
1622        let seg_after = wal.export_segment().len();
1623        assert!(seg_after < seg_before);
1624    }
1625
1626    // ── 12. Stats ─────────────────────────────────────────────────────────────
1627
1628    #[test]
1629    fn test_stats_segments_count() {
1630        let wal = default_wal();
1631        assert_eq!(wal.stats().segments_count, 1);
1632    }
1633
1634    #[test]
1635    fn test_stats_last_checkpoint_seq_initial_zero() {
1636        let wal = default_wal();
1637        assert_eq!(wal.stats().last_checkpoint_seq, 0);
1638    }
1639
1640    #[test]
1641    fn test_stats_after_checkpoint() {
1642        let mut wal = default_wal();
1643        wal.write(b"k".to_vec(), b"v".to_vec(), WalOpType::Put)
1644            .unwrap();
1645        let cp = wal.checkpoint().unwrap();
1646        assert_eq!(wal.stats().last_checkpoint_seq, cp);
1647    }
1648
1649    #[test]
1650    fn test_stats_total_bytes_grows() {
1651        let mut wal = default_wal();
1652        let b0 = wal.stats().total_bytes;
1653        wal.write(b"key-large".to_vec(), vec![0u8; 256], WalOpType::Put)
1654            .unwrap();
1655        let b1 = wal.stats().total_bytes;
1656        assert!(b1 > b0);
1657    }
1658
1659    // ── 13. Segment full / size limits ────────────────────────────────────────
1660
1661    #[test]
1662    fn test_segment_full_on_size_limit() {
1663        let config = WalConfig {
1664            max_segment_size: 100, // very small
1665            ..WalConfig::default()
1666        };
1667        let mut wal = WalWriteAheadLog::new(config);
1668        // First write might fit; eventually we should hit the limit.
1669        let mut hit_full = false;
1670        for i in 0u8..100 {
1671            let result = wal.write(vec![i], vec![i; 10], WalOpType::Put);
1672            if matches!(result, Err(WalError::SegmentFull)) {
1673                hit_full = true;
1674                break;
1675            }
1676        }
1677        assert!(hit_full, "Expected SegmentFull error to be triggered");
1678    }
1679
1680    #[test]
1681    fn test_segment_full_on_entry_count_limit() {
1682        let config = WalConfig {
1683            max_entries_per_segment: 3,
1684            max_segment_size: 0, // unlimited size
1685            checkpoint_interval: 0,
1686            ..WalConfig::default()
1687        };
1688        let mut wal = WalWriteAheadLog::new(config);
1689        wal.write(b"a".to_vec(), b"1".to_vec(), WalOpType::Put)
1690            .unwrap();
1691        wal.write(b"b".to_vec(), b"2".to_vec(), WalOpType::Put)
1692            .unwrap();
1693        wal.write(b"c".to_vec(), b"3".to_vec(), WalOpType::Put)
1694            .unwrap();
1695        let result = wal.write(b"d".to_vec(), b"4".to_vec(), WalOpType::Put);
1696        assert!(matches!(result, Err(WalError::SegmentFull)));
1697    }
1698
1699    // ── 14. Multiple transactions interleaved ─────────────────────────────────
1700
1701    #[test]
1702    fn test_multiple_concurrent_transactions() {
1703        let mut wal = default_wal();
1704        let t1 = wal.begin_transaction();
1705        let t2 = wal.begin_transaction();
1706        wal.write_tx(t1, b"t1k1".to_vec(), b"v1".to_vec(), WalOpType::Put)
1707            .unwrap();
1708        wal.write_tx(t2, b"t2k1".to_vec(), b"v2".to_vec(), WalOpType::Put)
1709            .unwrap();
1710        wal.write_tx(t1, b"t1k2".to_vec(), b"v3".to_vec(), WalOpType::Put)
1711            .unwrap();
1712        wal.commit_transaction(t1).unwrap();
1713        wal.rollback_transaction(t2).unwrap();
1714
1715        let entries: Vec<WalEntry> = wal.entries.clone();
1716        let mut replayed_keys: Vec<Vec<u8>> = Vec::new();
1717        WalWriteAheadLog::replay(&entries, &mut |e| {
1718            replayed_keys.push(e.key.clone());
1719        });
1720        // Only t1 keys should appear
1721        assert!(replayed_keys.contains(&b"t1k1".to_vec()));
1722        assert!(replayed_keys.contains(&b"t1k2".to_vec()));
1723        assert!(!replayed_keys.contains(&b"t2k1".to_vec()));
1724    }
1725
1726    // ── 15. Encode / decode multiple entries sequentially ────────────────────
1727
1728    #[test]
1729    fn test_sequential_encode_decode_in_buffer() {
1730        let mut buf = Vec::new();
1731        let mut entries = Vec::new();
1732        for i in 0u64..20 {
1733            let e = WalEntry {
1734                seq_num: i + 1,
1735                tx_id: 0,
1736                op_type: WalOpType::Put,
1737                key: format!("key-{i}").into_bytes(),
1738                value: format!("val-{i}").into_bytes(),
1739                timestamp: i * 1000,
1740                checksum: 0,
1741            };
1742            buf.extend_from_slice(&e.encode());
1743            entries.push(e);
1744        }
1745
1746        let mut pos = 0usize;
1747        let mut decoded = Vec::new();
1748        while pos < buf.len() {
1749            let (e, consumed) = WalEntry::decode(&buf, pos).unwrap();
1750            decoded.push(e);
1751            pos += consumed;
1752        }
1753        assert_eq!(decoded.len(), 20);
1754        for (orig, dec) in entries.iter().zip(decoded.iter()) {
1755            assert_eq!(orig.seq_num, dec.seq_num);
1756            assert_eq!(orig.key, dec.key);
1757            assert_eq!(orig.value, dec.value);
1758        }
1759    }
1760
1761    // ── 16. WalConfig defaults ────────────────────────────────────────────────
1762
1763    #[test]
1764    fn test_wal_config_defaults_reasonable() {
1765        let cfg = WalConfig::default();
1766        assert!(cfg.max_segment_size > 0);
1767        assert!(cfg.max_entries_per_segment > 0);
1768        assert!(cfg.checkpoint_interval > 0);
1769    }
1770
1771    // ── 17. Stress / randomised round-trip ───────────────────────────────────
1772
1773    #[test]
1774    fn test_stress_random_writes_and_recovery() {
1775        let mut rng = Xorshift64::new(42);
1776        let mut wal = default_wal();
1777
1778        for _ in 0..200 {
1779            let key_len = (rng.next() % 64 + 1) as usize;
1780            let val_len = (rng.next() % 256) as usize;
1781            let key = rng.next_bytes(key_len);
1782            let val = rng.next_bytes(val_len);
1783            wal.write(key, val, WalOpType::Put).unwrap();
1784        }
1785
1786        let seg = wal.export_segment();
1787        let rr = WalWriteAheadLog::recover(&seg).unwrap();
1788        assert_eq!(rr.entries_recovered, 200);
1789        assert_eq!(rr.entries_replayed, 200);
1790    }
1791
1792    #[test]
1793    fn test_stress_mixed_transactions_and_standalone() {
1794        let mut rng = Xorshift64::new(7777);
1795        let mut wal = default_wal();
1796        let mut committed_put_count = 0usize;
1797        let mut standalone_count = 0usize;
1798
1799        for _ in 0..10 {
1800            // 5 standalone writes
1801            for j in 0u8..5 {
1802                wal.write(vec![j], rng.next_bytes(8), WalOpType::Put)
1803                    .unwrap();
1804                standalone_count += 1;
1805            }
1806            // A committed transaction with 3 puts
1807            let tx = wal.begin_transaction();
1808            for j in 0u8..3 {
1809                wal.write_tx(tx, vec![j], rng.next_bytes(8), WalOpType::Put)
1810                    .unwrap();
1811                committed_put_count += 1;
1812            }
1813            wal.commit_transaction(tx).unwrap();
1814            // An uncommitted transaction (simulated crash)
1815            let tx2 = wal.begin_transaction();
1816            wal.write_tx(tx2, b"partial".to_vec(), b"v".to_vec(), WalOpType::Put)
1817                .unwrap();
1818            // No commit
1819        }
1820
1821        let seg = wal.export_segment();
1822        let rr = WalWriteAheadLog::recover(&seg).unwrap();
1823        assert_eq!(rr.entries_replayed, standalone_count + committed_put_count);
1824        assert!(rr.partial_tx_discarded > 0);
1825    }
1826}