Skip to main content

reddb_server/storage/wal/
record.rs

1use reddb_file::{
2    decode_main_wal_record_frame_with_authority, encode_main_wal_record_frame_into,
3    encode_main_wal_record_frame_with_authority_into, MainWalRecordFrame, MainWalRecordFrameRef,
4    WAL_FILE_VERSION,
5};
6use std::io::{self, Read};
7
8pub const WAL_DEFAULT_TERM: u64 = crate::replication::DEFAULT_REPLICATION_TERM;
9
10pub use reddb_file::MainWalRecordAuthority as WalRecordAuthority;
11pub use reddb_file::MainWalRecordType as RecordType;
12
13/// A single entry in the write-ahead log
14#[derive(Debug, Clone, PartialEq)]
15pub enum WalRecord {
16    /// Durable page-ownership intent. It must be resolved before page reuse.
17    PageOwnership {
18        record: reddb_file::page_ownership_journal::PageOwnershipRecord,
19    },
20    /// Start of a transaction
21    Begin { tx_id: u64 },
22    /// Commit of a transaction
23    Commit { tx_id: u64 },
24    /// Rollback of a transaction
25    Rollback { tx_id: u64 },
26    /// Write of a page — always carries uncompressed data (transparent to
27    /// callers: `read()` decompresses on-the-fly).
28    PageWrite {
29        tx_id: u64,
30        page_id: u32,
31        data: Vec<u8>,
32    },
33    /// Atomic logical commit batch. Recovery applies all actions in
34    /// order iff this complete record and checksum are present.
35    TxCommitBatch { tx_id: u64, actions: Vec<Vec<u8>> },
36    /// Full-page image — pristine page bytes captured before the first
37    /// modification per checkpoint cycle. Recovery applies these before
38    /// redo so torn writes are healed without the `-dwb` sidecar.
39    FullPageImage {
40        tx_id: u64,
41        page_id: u32,
42        ckpt_epoch: u64,
43        data: Vec<u8>,
44    },
45    /// Logical vector insert payload. Recovery can replay FP32 into the
46    /// in-memory vector-turbo index without requiring snapshot files.
47    VectorInsert {
48        collection: String,
49        entity_id: u64,
50        vector: Vec<f32>,
51    },
52    /// Logical probabilistic-structure mutation delta. Checkpoints carry
53    /// full snapshots through the regular store state; crash recovery
54    /// replays these deltas after loading the latest snapshot.
55    ProbabilisticDelta {
56        kind: u8,
57        operation: u8,
58        name: String,
59        operands: Vec<Vec<u8>>,
60    },
61    /// Checkpoint marker (indicates up to which LSN pages are flushed)
62    Checkpoint { lsn: u64 },
63}
64
65impl WalRecord {
66    /// Serialize record to bytes (including checksum).
67    ///
68    /// `PageWrite` compression and physical framing are owned by `reddb-file`.
69    pub fn encode(&self) -> Vec<u8> {
70        self.encode_with_term(WAL_DEFAULT_TERM)
71    }
72
73    /// Serialize record to bytes with the replication term stamped into
74    /// the physical record envelope.
75    pub fn encode_with_term(&self, term: u64) -> Vec<u8> {
76        let mut buf = Vec::new();
77        self.encode_with_term_into(&mut buf, term);
78        buf
79    }
80
81    /// Serialize record into a caller-owned scratch buffer, appending the
82    /// encoded bytes (including checksum) to `out`.
83    ///
84    /// This is the allocation-light entry point for the lock-free append path:
85    /// concurrent appenders each encode into their own per-call `out` buffer
86    /// *before* taking the WAL lock, so the scratch is never shared across
87    /// threads and needs no `thread_local!`. Reusing one `out` across many
88    /// records (the commit blob) avoids the fresh `Vec` + copy that
89    /// [`encode`](Self::encode) allocates per record. The bytes appended are
90    /// byte-identical to `encode()`.
91    pub fn encode_into(&self, out: &mut Vec<u8>) {
92        self.encode_with_term_into(out, WAL_DEFAULT_TERM)
93    }
94
95    /// Serialize record into a caller-owned scratch buffer with the replication
96    /// term stamped into the physical envelope. See [`encode_into`](Self::encode_into).
97    ///
98    /// The checksum is computed over only the bytes this call appends (the slice
99    /// starting at the buffer's prior length), so appending after existing
100    /// records leaves them untouched and keeps each record's CRC self-contained.
101    ///
102    /// The payload is written straight from this record's own buffers — the
103    /// append path never clones a page image or a commit batch into an owned
104    /// file frame first.
105    pub fn encode_with_term_into(&self, out: &mut Vec<u8>, term: u64) {
106        encode_main_wal_record_frame_into(self.to_file_frame_ref(), term, out)
107            .expect("main WAL record cannot be encoded");
108    }
109
110    pub fn encode_with_authority_into(&self, out: &mut Vec<u8>, authority: WalRecordAuthority) {
111        encode_main_wal_record_frame_with_authority_into(self.to_file_frame_ref(), authority, out)
112            .expect("main WAL record cannot be encoded");
113    }
114
115    /// Read a record from a reader.
116    ///
117    /// Handles both v1 (`PageWrite`) and v2 (`PageWriteCompressed`) record
118    /// formats transparently — callers always receive uncompressed data.
119    pub fn read<R: Read>(reader: &mut R) -> io::Result<Option<WalRecord>> {
120        Ok(Self::read_with_term(reader)?.map(|(_, record)| record))
121    }
122
123    /// Read a record and return the term stamped into its physical envelope.
124    pub fn read_with_term<R: Read>(reader: &mut R) -> io::Result<Option<(u64, WalRecord)>> {
125        Self::read_with_format_version(reader, WAL_FILE_VERSION)
126    }
127
128    /// Read a record and return the term + ownership epoch stamped into
129    /// its physical envelope.
130    pub fn read_with_authority<R: Read>(
131        reader: &mut R,
132    ) -> io::Result<Option<(WalRecordAuthority, WalRecord)>> {
133        Self::read_with_authority_format_version(reader, WAL_FILE_VERSION)
134    }
135
136    pub(crate) fn read_with_format_version<R: Read>(
137        reader: &mut R,
138        format_version: u8,
139    ) -> io::Result<Option<(u64, WalRecord)>> {
140        Ok(
141            Self::read_with_authority_format_version(reader, format_version)?
142                .map(|(authority, record)| (authority.term, record)),
143        )
144    }
145
146    pub(crate) fn read_with_authority_format_version<R: Read>(
147        reader: &mut R,
148        format_version: u8,
149    ) -> io::Result<Option<(WalRecordAuthority, WalRecord)>> {
150        Ok(decode_main_wal_record_frame_with_authority(
151            reader,
152            format_version,
153            WalRecordAuthority {
154                term: WAL_DEFAULT_TERM,
155                ownership_epoch: None,
156            },
157        )?
158        .map(|(authority, frame)| (authority, WalRecord::from_file_frame(frame))))
159    }
160}
161
162impl WalRecord {
163    /// Borrow this record as a file frame. Every payload is passed through by
164    /// reference, so encoding a `PageWrite`, `FullPageImage` or `TxCommitBatch`
165    /// costs no copy of the payload.
166    fn to_file_frame_ref(&self) -> MainWalRecordFrameRef<'_> {
167        match self {
168            WalRecord::PageOwnership { record } => {
169                MainWalRecordFrameRef::PageOwnership { record: *record }
170            }
171            WalRecord::Begin { tx_id } => MainWalRecordFrameRef::Begin { tx_id: *tx_id },
172            WalRecord::Commit { tx_id } => MainWalRecordFrameRef::Commit { tx_id: *tx_id },
173            WalRecord::Rollback { tx_id } => MainWalRecordFrameRef::Rollback { tx_id: *tx_id },
174            WalRecord::PageWrite {
175                tx_id,
176                page_id,
177                data,
178            } => MainWalRecordFrameRef::PageWrite {
179                tx_id: *tx_id,
180                page_id: *page_id,
181                data,
182            },
183            WalRecord::TxCommitBatch { tx_id, actions } => MainWalRecordFrameRef::TxCommitBatch {
184                tx_id: *tx_id,
185                actions,
186            },
187            WalRecord::FullPageImage {
188                tx_id,
189                page_id,
190                ckpt_epoch,
191                data,
192            } => MainWalRecordFrameRef::FullPageImage {
193                tx_id: *tx_id,
194                page_id: *page_id,
195                ckpt_epoch: *ckpt_epoch,
196                data,
197            },
198            WalRecord::VectorInsert {
199                collection,
200                entity_id,
201                vector,
202            } => MainWalRecordFrameRef::VectorInsert {
203                collection,
204                entity_id: *entity_id,
205                vector,
206            },
207            WalRecord::ProbabilisticDelta {
208                kind,
209                operation,
210                name,
211                operands,
212            } => MainWalRecordFrameRef::ProbabilisticDelta {
213                kind: *kind,
214                operation: *operation,
215                name,
216                operands,
217            },
218            WalRecord::Checkpoint { lsn } => MainWalRecordFrameRef::Checkpoint { lsn: *lsn },
219        }
220    }
221
222    /// The pre-#2013 owning conversion, kept as the baseline the borrowed
223    /// encode path is proven byte-identical against.
224    #[cfg(test)]
225    fn to_file_frame(&self) -> MainWalRecordFrame {
226        match self {
227            WalRecord::PageOwnership { record } => {
228                MainWalRecordFrame::PageOwnership { record: *record }
229            }
230            WalRecord::Begin { tx_id } => MainWalRecordFrame::Begin { tx_id: *tx_id },
231            WalRecord::Commit { tx_id } => MainWalRecordFrame::Commit { tx_id: *tx_id },
232            WalRecord::Rollback { tx_id } => MainWalRecordFrame::Rollback { tx_id: *tx_id },
233            WalRecord::PageWrite {
234                tx_id,
235                page_id,
236                data,
237            } => MainWalRecordFrame::PageWrite {
238                tx_id: *tx_id,
239                page_id: *page_id,
240                data: data.clone(),
241            },
242            WalRecord::TxCommitBatch { tx_id, actions } => MainWalRecordFrame::TxCommitBatch {
243                tx_id: *tx_id,
244                actions: actions.clone(),
245            },
246            WalRecord::FullPageImage {
247                tx_id,
248                page_id,
249                ckpt_epoch,
250                data,
251            } => MainWalRecordFrame::FullPageImage {
252                tx_id: *tx_id,
253                page_id: *page_id,
254                ckpt_epoch: *ckpt_epoch,
255                data: data.clone(),
256            },
257            WalRecord::VectorInsert {
258                collection,
259                entity_id,
260                vector,
261            } => MainWalRecordFrame::VectorInsert {
262                collection: collection.clone(),
263                entity_id: *entity_id,
264                vector: vector.clone(),
265            },
266            WalRecord::ProbabilisticDelta {
267                kind,
268                operation,
269                name,
270                operands,
271            } => MainWalRecordFrame::ProbabilisticDelta {
272                kind: *kind,
273                operation: *operation,
274                name: name.clone(),
275                operands: operands.clone(),
276            },
277            WalRecord::Checkpoint { lsn } => MainWalRecordFrame::Checkpoint { lsn: *lsn },
278        }
279    }
280
281    fn from_file_frame(frame: MainWalRecordFrame) -> Self {
282        match frame {
283            MainWalRecordFrame::PageOwnership { record } => WalRecord::PageOwnership { record },
284            MainWalRecordFrame::Begin { tx_id } => WalRecord::Begin { tx_id },
285            MainWalRecordFrame::Commit { tx_id } => WalRecord::Commit { tx_id },
286            MainWalRecordFrame::Rollback { tx_id } => WalRecord::Rollback { tx_id },
287            MainWalRecordFrame::PageWrite {
288                tx_id,
289                page_id,
290                data,
291            } => WalRecord::PageWrite {
292                tx_id,
293                page_id,
294                data,
295            },
296            MainWalRecordFrame::TxCommitBatch { tx_id, actions } => {
297                WalRecord::TxCommitBatch { tx_id, actions }
298            }
299            MainWalRecordFrame::FullPageImage {
300                tx_id,
301                page_id,
302                ckpt_epoch,
303                data,
304            } => WalRecord::FullPageImage {
305                tx_id,
306                page_id,
307                ckpt_epoch,
308                data,
309            },
310            MainWalRecordFrame::VectorInsert {
311                collection,
312                entity_id,
313                vector,
314            } => WalRecord::VectorInsert {
315                collection,
316                entity_id,
317                vector,
318            },
319            MainWalRecordFrame::ProbabilisticDelta {
320                kind,
321                operation,
322                name,
323                operands,
324            } => WalRecord::ProbabilisticDelta {
325                kind,
326                operation,
327                name,
328                operands,
329            },
330            MainWalRecordFrame::Checkpoint { lsn } => WalRecord::Checkpoint { lsn },
331        }
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use std::io::Cursor;
339
340    #[test]
341    fn page_ownership_intent_round_trips_through_native_wal() {
342        let record = WalRecord::PageOwnership {
343            record: reddb_file::page_ownership_journal::PageOwnershipRecord::Reserve {
344                epoch: 7,
345                page_id: 67,
346            },
347        };
348        let bytes = record.encode();
349        let decoded = WalRecord::read(&mut Cursor::new(bytes))
350            .expect("decode WAL record")
351            .expect("record is present");
352        assert_eq!(decoded, record);
353    }
354
355    // ==================== WalRecord::encode Tests ====================
356
357    #[test]
358    fn test_encode_begin() {
359        let record = WalRecord::Begin { tx_id: 12345 };
360        let encoded = record.encode();
361        assert!(!encoded.is_empty());
362    }
363
364    #[test]
365    fn test_encode_commit() {
366        let record = WalRecord::Commit { tx_id: 99999 };
367        let encoded = record.encode();
368        assert!(!encoded.is_empty());
369    }
370
371    #[test]
372    fn test_encode_rollback() {
373        let record = WalRecord::Rollback { tx_id: 54321 };
374        let encoded = record.encode();
375        assert!(!encoded.is_empty());
376    }
377
378    #[test]
379    fn test_encode_checkpoint() {
380        let record = WalRecord::Checkpoint { lsn: 1000000 };
381        let encoded = record.encode();
382        assert!(!encoded.is_empty());
383    }
384
385    #[test]
386    fn test_encode_page_write_small() {
387        // Small data (< COMPRESS_THRESHOLD) stays uncompressed.
388        let data = vec![1, 2, 3, 4, 5];
389        let record = WalRecord::PageWrite {
390            tx_id: 100,
391            page_id: 42,
392            data: data.clone(),
393        };
394        let encoded = record.encode();
395        assert!(!encoded.is_empty());
396    }
397
398    #[test]
399    fn test_encode_page_write_empty_data() {
400        let record = WalRecord::PageWrite {
401            tx_id: 1,
402            page_id: 0,
403            data: vec![],
404        };
405        let encoded = record.encode();
406        assert!(!encoded.is_empty());
407    }
408
409    #[test]
410    fn test_encode_tx_commit_batch() {
411        let record = WalRecord::TxCommitBatch {
412            tx_id: 7,
413            actions: vec![b"insert".to_vec(), b"update".to_vec()],
414        };
415        let encoded = record.encode();
416        assert!(!encoded.is_empty());
417    }
418
419    // ==================== WalRecord::read Tests ====================
420
421    #[test]
422    fn test_read_begin_roundtrip() {
423        let original = WalRecord::Begin { tx_id: 42 };
424        let encoded = original.encode();
425
426        let mut cursor = Cursor::new(encoded);
427        let decoded = WalRecord::read(&mut cursor).unwrap().unwrap();
428
429        assert_eq!(decoded, original);
430    }
431
432    #[test]
433    fn test_read_begin_roundtrip_preserves_term() {
434        let original = WalRecord::Begin { tx_id: 42 };
435        let encoded = original.encode_with_term(9);
436
437        let mut cursor = Cursor::new(encoded);
438        let (term, decoded) = WalRecord::read_with_term(&mut cursor).unwrap().unwrap();
439
440        assert_eq!(term, 9);
441        assert_eq!(decoded, original);
442    }
443
444    #[test]
445    fn test_read_begin_roundtrip_preserves_authority_epoch() {
446        let original = WalRecord::Begin { tx_id: 42 };
447        let authority = WalRecordAuthority {
448            term: 9,
449            ownership_epoch: Some(12),
450        };
451        let mut encoded = Vec::new();
452        original.encode_with_authority_into(&mut encoded, authority);
453
454        let mut cursor = Cursor::new(encoded);
455        let (decoded_authority, decoded) = WalRecord::read_with_authority(&mut cursor)
456            .unwrap()
457            .unwrap();
458
459        assert_eq!(decoded_authority, authority);
460        assert_eq!(decoded, original);
461    }
462
463    #[test]
464    fn test_read_commit_roundtrip() {
465        let original = WalRecord::Commit { tx_id: 999 };
466        let encoded = original.encode();
467
468        let mut cursor = Cursor::new(encoded);
469        let decoded = WalRecord::read(&mut cursor).unwrap().unwrap();
470
471        assert_eq!(decoded, original);
472    }
473
474    #[test]
475    fn test_read_rollback_roundtrip() {
476        let original = WalRecord::Rollback { tx_id: 777 };
477        let encoded = original.encode();
478
479        let mut cursor = Cursor::new(encoded);
480        let decoded = WalRecord::read(&mut cursor).unwrap().unwrap();
481
482        assert_eq!(decoded, original);
483    }
484
485    #[test]
486    fn test_read_checkpoint_roundtrip() {
487        let original = WalRecord::Checkpoint { lsn: 123456789 };
488        let encoded = original.encode();
489
490        let mut cursor = Cursor::new(encoded);
491        let decoded = WalRecord::read(&mut cursor).unwrap().unwrap();
492
493        assert_eq!(decoded, original);
494    }
495
496    #[test]
497    fn test_read_page_write_roundtrip() {
498        let original = WalRecord::PageWrite {
499            tx_id: 50,
500            page_id: 100,
501            data: vec![10, 20, 30, 40, 50, 60, 70, 80],
502        };
503        let encoded = original.encode();
504
505        let mut cursor = Cursor::new(encoded);
506        let decoded = WalRecord::read(&mut cursor).unwrap().unwrap();
507
508        assert_eq!(decoded, original);
509    }
510
511    #[test]
512    fn test_read_tx_commit_batch_roundtrip() {
513        let original = WalRecord::TxCommitBatch {
514            tx_id: 42,
515            actions: vec![b"old-version".to_vec(), b"new-version".to_vec()],
516        };
517        let encoded = original.encode();
518
519        let mut cursor = Cursor::new(encoded);
520        let decoded = WalRecord::read(&mut cursor).unwrap().unwrap();
521
522        assert_eq!(decoded, original);
523    }
524
525    #[test]
526    fn test_vector_insert_roundtrip() {
527        let original = WalRecord::VectorInsert {
528            collection: "turbo".to_string(),
529            entity_id: 42,
530            vector: vec![1.0, -0.5, 0.25],
531        };
532        let encoded = original.encode();
533
534        let mut cursor = Cursor::new(encoded);
535        let decoded = WalRecord::read(&mut cursor).unwrap().unwrap();
536
537        assert_eq!(decoded, original);
538    }
539
540    #[test]
541    fn test_read_page_write_large_data() {
542        // Large enough to trigger compression.
543        let data: Vec<u8> = (0..4096).map(|i| (i % 256) as u8).collect();
544        let original = WalRecord::PageWrite {
545            tx_id: 1,
546            page_id: 0,
547            data,
548        };
549        let encoded = original.encode();
550
551        let mut cursor = Cursor::new(encoded);
552        let decoded = WalRecord::read(&mut cursor).unwrap().unwrap();
553
554        // Round-trip: decoded data matches original (even if encoded differently).
555        assert_eq!(decoded, original);
556    }
557
558    #[test]
559    fn page_write_compressed_roundtrip() {
560        // Highly compressible payload: 1 KiB of repeated bytes.
561        let data = vec![0xABu8; 1024];
562        let record = WalRecord::PageWrite {
563            tx_id: 7,
564            page_id: 3,
565            data: data.clone(),
566        };
567        let encoded = record.encode();
568
569        let mut cursor = Cursor::new(encoded);
570        let decoded = WalRecord::read(&mut cursor).unwrap().unwrap();
571        assert_eq!(
572            decoded,
573            WalRecord::PageWrite {
574                tx_id: 7,
575                page_id: 3,
576                data
577            }
578        );
579    }
580
581    #[test]
582    fn full_page_image_roundtrip() {
583        let data: Vec<u8> = (0..4096).map(|i| (i % 251) as u8).collect();
584        let original = WalRecord::FullPageImage {
585            tx_id: 11,
586            page_id: 9,
587            ckpt_epoch: 42,
588            data: data.clone(),
589        };
590        let encoded = original.encode();
591
592        let mut cursor = Cursor::new(encoded);
593        let decoded = WalRecord::read(&mut cursor).unwrap().unwrap();
594        assert_eq!(decoded, original);
595    }
596
597    #[test]
598    fn full_page_image_checksum_mismatch_detected() {
599        let original = WalRecord::FullPageImage {
600            tx_id: 1,
601            page_id: 2,
602            ckpt_epoch: 3,
603            data: vec![0xAA; 32],
604        };
605        let mut encoded = original.encode();
606        let mid = encoded.len() / 2;
607        encoded[mid] ^= 0xFF;
608        let mut cursor = Cursor::new(encoded);
609        assert!(WalRecord::read(&mut cursor).is_err());
610    }
611
612    #[test]
613    fn test_read_eof() {
614        let mut cursor = Cursor::new(Vec::<u8>::new());
615        let result = WalRecord::read(&mut cursor).unwrap();
616        assert!(result.is_none());
617    }
618
619    #[test]
620    fn test_read_invalid_record_type() {
621        let buf = vec![99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // Invalid type 99
622        let mut cursor = Cursor::new(buf);
623        let result = WalRecord::read(&mut cursor);
624        assert!(result.is_err());
625    }
626
627    #[test]
628    fn test_read_checksum_mismatch() {
629        let record = WalRecord::Begin { tx_id: 42 };
630        let mut encoded = record.encode();
631
632        // Corrupt the last byte (checksum)
633        let len = encoded.len();
634        encoded[len - 1] ^= 0xFF;
635
636        let mut cursor = Cursor::new(encoded);
637        let result = WalRecord::read(&mut cursor);
638        assert!(result.is_err());
639    }
640
641    #[test]
642    fn test_read_data_corruption() {
643        let record = WalRecord::PageWrite {
644            tx_id: 1,
645            page_id: 2,
646            data: vec![1, 2, 3, 4],
647        };
648        let mut encoded = record.encode();
649
650        // Corrupt a data byte
651        encoded[15] ^= 0xFF;
652
653        let mut cursor = Cursor::new(encoded);
654        let result = WalRecord::read(&mut cursor);
655        assert!(result.is_err()); // Checksum will fail
656    }
657
658    // ==================== Multiple Records Tests ====================
659
660    #[test]
661    fn test_multiple_records_sequential() {
662        let records = vec![
663            WalRecord::Begin { tx_id: 1 },
664            WalRecord::PageWrite {
665                tx_id: 1,
666                page_id: 10,
667                data: vec![1, 2, 3],
668            },
669            WalRecord::PageWrite {
670                tx_id: 1,
671                page_id: 20,
672                data: vec![4, 5, 6],
673            },
674            WalRecord::Commit { tx_id: 1 },
675            WalRecord::Checkpoint { lsn: 100 },
676        ];
677
678        // Encode all
679        let mut buf = Vec::new();
680        for r in &records {
681            buf.extend_from_slice(&r.encode());
682        }
683
684        // Read them back
685        let mut cursor = Cursor::new(buf);
686        for expected in &records {
687            let decoded = WalRecord::read(&mut cursor).unwrap().unwrap();
688            assert_eq!(&decoded, expected);
689        }
690
691        // Next read should return None (EOF)
692        assert!(WalRecord::read(&mut cursor).unwrap().is_none());
693    }
694
695    // ==================== Constants Tests ====================
696
697    // ==================== encode_into scratch-buffer Tests ====================
698
699    /// `encode_into` appended to a fresh scratch must be byte-identical to the
700    /// per-allocation `encode()` baseline, for every record variant.
701    #[test]
702    fn test_encode_into_matches_encode_for_all_variants() {
703        let records = vec![
704            WalRecord::Begin { tx_id: 12345 },
705            WalRecord::Commit { tx_id: 99999 },
706            WalRecord::Rollback { tx_id: 54321 },
707            WalRecord::Checkpoint { lsn: 1_000_000 },
708            WalRecord::PageWrite {
709                tx_id: 100,
710                page_id: 42,
711                data: vec![1, 2, 3, 4, 5],
712            },
713            // Large, highly compressible payload → exercises the
714            // PageWriteCompressed early-return branch.
715            WalRecord::PageWrite {
716                tx_id: 7,
717                page_id: 3,
718                data: vec![0xABu8; 1024],
719            },
720            WalRecord::TxCommitBatch {
721                tx_id: 7,
722                actions: vec![b"insert".to_vec(), b"update".to_vec()],
723            },
724            WalRecord::FullPageImage {
725                tx_id: 11,
726                page_id: 9,
727                ckpt_epoch: 42,
728                data: (0..4096).map(|i| (i % 251) as u8).collect(),
729            },
730            WalRecord::VectorInsert {
731                collection: "turbo".to_string(),
732                entity_id: 42,
733                vector: vec![1.0, -0.5, 0.25],
734            },
735        ];
736
737        for record in &records {
738            let baseline = record.encode();
739            let mut scratch = Vec::new();
740            record.encode_into(&mut scratch);
741            assert_eq!(scratch, baseline, "encode_into mismatch for {record:?}");
742        }
743    }
744
745    /// Reusing one scratch buffer across several records yields exactly the
746    /// concatenation of the per-record `encode()` baselines — proving the
747    /// checksum is computed over each record's own slice, not the whole buffer.
748    #[test]
749    fn test_encode_into_reuses_scratch_across_records() {
750        let records = vec![
751            WalRecord::Begin { tx_id: 1 },
752            WalRecord::PageWrite {
753                tx_id: 1,
754                page_id: 10,
755                data: vec![1, 2, 3],
756            },
757            WalRecord::Commit { tx_id: 1 },
758        ];
759
760        let mut expected = Vec::new();
761        for r in &records {
762            expected.extend_from_slice(&r.encode());
763        }
764
765        // One scratch, reused for every record — no per-record allocation.
766        let mut scratch = Vec::new();
767        for r in &records {
768            r.encode_into(&mut scratch);
769        }
770
771        assert_eq!(scratch, expected);
772
773        // And the concatenation round-trips back to the original records.
774        let mut cursor = Cursor::new(scratch);
775        for expected in &records {
776            let decoded = WalRecord::read(&mut cursor).unwrap().unwrap();
777            assert_eq!(&decoded, expected);
778        }
779        assert!(WalRecord::read(&mut cursor).unwrap().is_none());
780    }
781
782    /// Every record variant, including empty/large/compressible payloads and
783    /// the edge cases each variant can carry.
784    fn every_record_variant() -> Vec<WalRecord> {
785        vec![
786            WalRecord::Begin { tx_id: 12345 },
787            WalRecord::Commit { tx_id: 99999 },
788            WalRecord::Rollback { tx_id: 54321 },
789            WalRecord::Checkpoint { lsn: 1_000_000 },
790            // Below the compression threshold → literal PageWrite frame.
791            WalRecord::PageWrite {
792                tx_id: 100,
793                page_id: 42,
794                data: vec![1, 2, 3, 4, 5],
795            },
796            WalRecord::PageWrite {
797                tx_id: 1,
798                page_id: 0,
799                data: Vec::new(),
800            },
801            // Compressible and above the threshold → PageWriteCompressed frame.
802            WalRecord::PageWrite {
803                tx_id: 7,
804                page_id: 3,
805                data: vec![0xABu8; 1024],
806            },
807            // Above the threshold but incompressible → stays a literal PageWrite.
808            WalRecord::PageWrite {
809                tx_id: 8,
810                page_id: 4,
811                data: (0..300u32)
812                    .map(|i| i.wrapping_mul(2_654_435_761) as u8)
813                    .collect(),
814            },
815            WalRecord::TxCommitBatch {
816                tx_id: 7,
817                actions: vec![b"insert".to_vec(), Vec::new(), b"update".to_vec()],
818            },
819            WalRecord::TxCommitBatch {
820                tx_id: 8,
821                actions: Vec::new(),
822            },
823            WalRecord::FullPageImage {
824                tx_id: 11,
825                page_id: 9,
826                ckpt_epoch: 42,
827                data: (0..4096).map(|i| (i % 251) as u8).collect(),
828            },
829            WalRecord::VectorInsert {
830                collection: "turbo".to_string(),
831                entity_id: 42,
832                vector: vec![1.0, -0.5, 0.25],
833            },
834            WalRecord::ProbabilisticDelta {
835                kind: 1,
836                operation: 2,
837                name: "visitors".to_string(),
838                operands: vec![b"alice".to_vec(), b"bob".to_vec()],
839            },
840        ]
841    }
842
843    /// The borrowed encode path (#2013) must persist *exactly* the bytes the
844    /// pre-#2013 owning path persisted, for every record variant and for both
845    /// the term-only and the full-authority envelope. The on-disk format is
846    /// frozen; only the in-memory representation changed.
847    #[test]
848    fn borrowed_encode_is_byte_identical_to_the_owning_frame_path() {
849        for record in every_record_variant() {
850            // Old path: clone the payload into an owned frame, then encode.
851            let mut owned_bytes = Vec::new();
852            encode_main_wal_record_frame_into(&record.to_file_frame(), 17, &mut owned_bytes)
853                .expect("owning encode");
854
855            // New path: encode straight from the borrowed record.
856            let mut borrowed_bytes = Vec::new();
857            record.encode_with_term_into(&mut borrowed_bytes, 17);
858
859            assert_eq!(
860                borrowed_bytes, owned_bytes,
861                "borrowed encode diverged from the owning baseline for {record:?}"
862            );
863
864            // Same again through the authority envelope (with an ownership epoch).
865            let authority = WalRecordAuthority {
866                term: 17,
867                ownership_epoch: Some(5),
868            };
869            let mut owned_auth = Vec::new();
870            encode_main_wal_record_frame_with_authority_into(
871                &record.to_file_frame(),
872                authority,
873                &mut owned_auth,
874            )
875            .expect("owning authority encode");
876
877            let mut borrowed_auth = Vec::new();
878            record.encode_with_authority_into(&mut borrowed_auth, authority);
879
880            assert_eq!(
881                borrowed_auth, owned_auth,
882                "borrowed authority encode diverged from the owning baseline for {record:?}"
883            );
884
885            // And the persisted bytes still decode back to the original record.
886            let mut cursor = Cursor::new(borrowed_bytes);
887            let (term, decoded) = WalRecord::read_with_term(&mut cursor).unwrap().unwrap();
888            assert_eq!(term, 17);
889            assert_eq!(decoded, record);
890        }
891    }
892
893    /// `encode_with_term_into` honours the term and matches the allocating
894    /// `encode_with_term` baseline even when appended after existing bytes.
895    #[test]
896    fn test_encode_with_term_into_matches_and_preserves_prefix() {
897        let prefix = b"PREFIX-BYTES".to_vec();
898        let record = WalRecord::Begin { tx_id: 42 };
899
900        let mut scratch = prefix.clone();
901        record.encode_with_term_into(&mut scratch, 9);
902
903        // Prefix untouched; suffix equals the allocating baseline.
904        assert_eq!(&scratch[..prefix.len()], &prefix[..]);
905        assert_eq!(&scratch[prefix.len()..], &record.encode_with_term(9)[..]);
906    }
907}