1use std::io::{Read, Write};
9
10use crate::error::{HoronError, HoronResult};
11use crate::format::*;
12use crate::quant::SemLayout;
13use crate::snapshot::NodeEntry;
14
15#[derive(Debug, Clone)]
17pub struct WalEntry {
18 pub seq: u32,
20 pub op: u8,
22 pub key: String,
24 pub payload: WalPayload,
26}
27
28#[derive(Debug, Clone)]
30pub enum WalPayload {
31 Insert(NodeEntry),
33 Update {
35 data: Vec<u8>,
37 metadata: Vec<(String, String)>,
39 },
40 Delete,
42 SetMeta {
44 meta_key: String,
46 meta_value: String,
48 },
49 SetSemantic {
51 coords: Vec<u8>,
53 },
54 Epoch {
57 epoch_id: u64,
59 flags: u8,
61 },
62}
63
64impl WalEntry {
65 fn write_body<W: Write>(&self, w: &mut W, layout: &SemLayout) -> HoronResult<()> {
70 w.write_all(&self.seq.to_le_bytes())?;
72 w.write_all(&[self.op])?;
74 let key_bytes = self.key.as_bytes();
76 w.write_all(&(key_bytes.len() as u16).to_le_bytes())?;
77 w.write_all(key_bytes)?;
78
79 match &self.payload {
80 WalPayload::Insert(entry) => {
81 w.write_all(&(entry.data.len() as u32).to_le_bytes())?;
83 w.write_all(&entry.data)?;
84 w.write_all(&(entry.metadata.len() as u16).to_le_bytes())?;
86 for (mk, mv) in &entry.metadata {
87 let mk_b = mk.as_bytes();
88 let mv_b = mv.as_bytes();
89 w.write_all(&(mk_b.len() as u16).to_le_bytes())?;
90 w.write_all(mk_b)?;
91 w.write_all(&(mv_b.len() as u16).to_le_bytes())?;
92 w.write_all(mv_b)?;
93 }
94 if layout.quantized {
96 if layout.disk_bytes() > 0 {
97 w.write_all(&layout.encode_tail(&entry.semantic_coords)?)?;
98 }
99 } else if !entry.semantic_coords.is_empty() {
100 w.write_all(&entry.semantic_coords)?;
101 }
102 }
103 WalPayload::Update { data, metadata } => {
104 w.write_all(&(data.len() as u32).to_le_bytes())?;
105 w.write_all(data)?;
106 w.write_all(&(metadata.len() as u16).to_le_bytes())?;
107 for (mk, mv) in metadata {
108 let mk_b = mk.as_bytes();
109 let mv_b = mv.as_bytes();
110 w.write_all(&(mk_b.len() as u16).to_le_bytes())?;
111 w.write_all(mk_b)?;
112 w.write_all(&(mv_b.len() as u16).to_le_bytes())?;
113 w.write_all(mv_b)?;
114 }
115 }
116 WalPayload::Delete => {
117 }
119 WalPayload::SetMeta { meta_key, meta_value } => {
120 let mk_b = meta_key.as_bytes();
121 let mv_b = meta_value.as_bytes();
122 w.write_all(&(mk_b.len() as u16).to_le_bytes())?;
123 w.write_all(mk_b)?;
124 w.write_all(&(mv_b.len() as u16).to_le_bytes())?;
125 w.write_all(mv_b)?;
126 }
127 WalPayload::SetSemantic { coords } => {
128 if layout.quantized {
129 w.write_all(&layout.encode_tail(coords)?)?;
130 } else {
131 w.write_all(coords)?;
132 }
133 }
134 WalPayload::Epoch { epoch_id, flags } => {
135 w.write_all(&epoch_id.to_le_bytes())?;
136 w.write_all(&[*flags])?;
137 }
138 }
139
140 Ok(())
141 }
142
143 pub fn write_to<W: Write>(&self, w: &mut W, layout: &SemLayout) -> HoronResult<()> {
145 let mut body = Vec::new();
146 self.write_body(&mut body, layout)?;
147
148 let crc = crc32fast::hash(&body);
149 w.write_all(&body)?;
150 w.write_all(&crc.to_le_bytes())?;
151
152 Ok(())
153 }
154
155 pub fn read_from<R: Read>(
160 r: &mut R,
161 layout: &SemLayout,
162 ) -> HoronResult<Option<Self>> {
163 let mut buf2 = [0u8; 2];
164 let mut buf4 = [0u8; 4];
165
166 if r.read_exact(&mut buf4).is_err() {
169 return Ok(None); }
171 let mut body = Vec::new();
172 body.extend_from_slice(&buf4);
173 let seq = u32::from_le_bytes(buf4);
174
175 let mut op_buf = [0u8; 1];
177 r.read_exact(&mut op_buf)?;
178 body.push(op_buf[0]);
179 let op = op_buf[0];
180
181 r.read_exact(&mut buf2)?;
183 body.extend_from_slice(&buf2);
184 let key_len = u16::from_le_bytes(buf2) as usize;
185 let mut key_buf = vec![0u8; key_len];
186 r.read_exact(&mut key_buf)?;
187 body.extend_from_slice(&key_buf);
188 let key = String::from_utf8(key_buf)
189 .map_err(|e| HoronError::InvalidFormat(format!("invalid UTF-8 key: {}", e)))?;
190
191 let payload = match op {
193 OP_INSERT => {
194 let entry = read_insert_body(r, &mut body, layout)?;
195 WalPayload::Insert(entry)
196 }
197 OP_UPDATE => {
198 let (data, metadata) = read_update_body(r, &mut body)?;
199 WalPayload::Update { data, metadata }
200 }
201 OP_DELETE => {
202 WalPayload::Delete
203 }
204 OP_SET_META => {
205 let (mk, mv) = read_meta_body(r, &mut body)?;
206 WalPayload::SetMeta { meta_key: mk, meta_value: mv }
207 }
208 OP_SET_SEMANTIC => {
209 let mut disk = vec![0u8; layout.disk_bytes()];
212 r.read_exact(&mut disk)?;
213 body.extend_from_slice(&disk);
214 let coords = if layout.quantized {
215 layout.decode_tail(&disk)?
216 } else {
217 disk
218 };
219 WalPayload::SetSemantic { coords }
220 }
221 OP_EPOCH => {
222 let mut buf8 = [0u8; 8];
223 r.read_exact(&mut buf8)?;
224 body.extend_from_slice(&buf8);
225 let epoch_id = u64::from_le_bytes(buf8);
226 let mut flag_buf = [0u8; 1];
227 r.read_exact(&mut flag_buf)?;
228 body.push(flag_buf[0]);
229 WalPayload::Epoch { epoch_id, flags: flag_buf[0] }
230 }
231 _ => {
232 return Err(HoronError::InvalidFormat(
233 format!("unknown WAL op code: 0x{:02X}", op)
234 ));
235 }
236 };
237
238 r.read_exact(&mut buf4)?;
240 let stored_crc = u32::from_le_bytes(buf4);
241 let computed_crc = crc32fast::hash(&body);
242
243 if stored_crc != computed_crc {
244 return Ok(None);
246 }
247
248 Ok(Some(WalEntry { seq, op, key, payload }))
249 }
250}
251
252fn read_insert_body<R: Read>(
254 r: &mut R,
255 body: &mut Vec<u8>,
256 layout: &SemLayout,
257) -> HoronResult<NodeEntry> {
258 let mut buf2 = [0u8; 2];
259 let mut buf4 = [0u8; 4];
260
261 r.read_exact(&mut buf4)?;
263 body.extend_from_slice(&buf4);
264 let data_len = u32::from_le_bytes(buf4) as usize;
265 if data_len > MAX_ENTRY_DATA {
266 return Err(HoronError::InvalidFormat(format!(
267 "WAL entry data length {} exceeds maximum {} — corrupt length field",
268 data_len, MAX_ENTRY_DATA
269 )));
270 }
271 let data = crate::format::read_bounded_vec(r, data_len, "WAL entry data")?;
272 body.extend_from_slice(&data);
273
274 r.read_exact(&mut buf2)?;
276 body.extend_from_slice(&buf2);
277 let meta_count = u16::from_le_bytes(buf2) as usize;
278 let mut metadata = Vec::with_capacity(meta_count);
279 for _ in 0..meta_count {
280 r.read_exact(&mut buf2)?;
281 body.extend_from_slice(&buf2);
282 let mk_len = u16::from_le_bytes(buf2) as usize;
283 let mut mk_buf = vec![0u8; mk_len];
284 r.read_exact(&mut mk_buf)?;
285 body.extend_from_slice(&mk_buf);
286
287 r.read_exact(&mut buf2)?;
288 body.extend_from_slice(&buf2);
289 let mv_len = u16::from_le_bytes(buf2) as usize;
290 let mut mv_buf = vec![0u8; mv_len];
291 r.read_exact(&mut mv_buf)?;
292 body.extend_from_slice(&mv_buf);
293
294 let mk = String::from_utf8(mk_buf)
295 .map_err(|e| HoronError::InvalidFormat(format!("invalid UTF-8: {}", e)))?;
296 let mv = String::from_utf8(mv_buf)
297 .map_err(|e| HoronError::InvalidFormat(format!("invalid UTF-8: {}", e)))?;
298 metadata.push((mk, mv));
299 }
300
301 let disk_bytes = layout.disk_bytes();
303 let mut semantic_coords = vec![0u8; disk_bytes];
304 if disk_bytes > 0 {
305 r.read_exact(&mut semantic_coords)?;
306 body.extend_from_slice(&semantic_coords);
307 if layout.quantized {
308 semantic_coords = layout.decode_tail(&semantic_coords)?;
309 }
310 }
311
312 Ok(NodeEntry {
313 key: String::new(), data,
315 metadata,
316 semantic_coords,
317 })
318}
319
320fn read_update_body<R: Read>(
322 r: &mut R,
323 body: &mut Vec<u8>,
324) -> HoronResult<(Vec<u8>, Vec<(String, String)>)> {
325 let mut buf2 = [0u8; 2];
326 let mut buf4 = [0u8; 4];
327
328 r.read_exact(&mut buf4)?;
330 body.extend_from_slice(&buf4);
331 let data_len = u32::from_le_bytes(buf4) as usize;
332 if data_len > MAX_ENTRY_DATA {
333 return Err(HoronError::InvalidFormat(format!(
334 "WAL entry data length {} exceeds maximum {} — corrupt length field",
335 data_len, MAX_ENTRY_DATA
336 )));
337 }
338 let data = crate::format::read_bounded_vec(r, data_len, "WAL entry data")?;
339 body.extend_from_slice(&data);
340
341 r.read_exact(&mut buf2)?;
343 body.extend_from_slice(&buf2);
344 let meta_count = u16::from_le_bytes(buf2) as usize;
345 let mut metadata = Vec::with_capacity(meta_count);
346 for _ in 0..meta_count {
347 r.read_exact(&mut buf2)?;
348 body.extend_from_slice(&buf2);
349 let mk_len = u16::from_le_bytes(buf2) as usize;
350 let mut mk_buf = vec![0u8; mk_len];
351 r.read_exact(&mut mk_buf)?;
352 body.extend_from_slice(&mk_buf);
353
354 r.read_exact(&mut buf2)?;
355 body.extend_from_slice(&buf2);
356 let mv_len = u16::from_le_bytes(buf2) as usize;
357 let mut mv_buf = vec![0u8; mv_len];
358 r.read_exact(&mut mv_buf)?;
359 body.extend_from_slice(&mv_buf);
360
361 let mk = String::from_utf8(mk_buf)
362 .map_err(|e| HoronError::InvalidFormat(format!("invalid UTF-8: {}", e)))?;
363 let mv = String::from_utf8(mv_buf)
364 .map_err(|e| HoronError::InvalidFormat(format!("invalid UTF-8: {}", e)))?;
365 metadata.push((mk, mv));
366 }
367
368 Ok((data, metadata))
369}
370
371fn read_meta_body<R: Read>(
373 r: &mut R,
374 body: &mut Vec<u8>,
375) -> HoronResult<(String, String)> {
376 let mut buf2 = [0u8; 2];
377
378 r.read_exact(&mut buf2)?;
379 body.extend_from_slice(&buf2);
380 let mk_len = u16::from_le_bytes(buf2) as usize;
381 let mut mk_buf = vec![0u8; mk_len];
382 r.read_exact(&mut mk_buf)?;
383 body.extend_from_slice(&mk_buf);
384
385 r.read_exact(&mut buf2)?;
386 body.extend_from_slice(&buf2);
387 let mv_len = u16::from_le_bytes(buf2) as usize;
388 let mut mv_buf = vec![0u8; mv_len];
389 r.read_exact(&mut mv_buf)?;
390 body.extend_from_slice(&mv_buf);
391
392 let mk = String::from_utf8(mk_buf)
393 .map_err(|e| HoronError::InvalidFormat(format!("invalid UTF-8: {}", e)))?;
394 let mv = String::from_utf8(mv_buf)
395 .map_err(|e| HoronError::InvalidFormat(format!("invalid UTF-8: {}", e)))?;
396
397 Ok((mk, mv))
398}
399
400const MAX_BLOCK_DECOMPRESSED: usize = 256 * 1024 * 1024;
402
403pub fn write_wal_block<W: Write>(
407 w: &mut W,
408 entries_data: &[u8],
409 entry_count: u16,
410 algo: u8,
411) -> HoronResult<()> {
412 let compressed = crate::compression::compress(entries_data, algo)?;
413 w.write_all(&entry_count.to_le_bytes())?;
414 w.write_all(&(compressed.len() as u32).to_le_bytes())?;
415 w.write_all(&compressed)?;
416 Ok(())
417}
418
419pub fn read_wal_block<R: Read>(
422 r: &mut R,
423 algo: u8,
424) -> HoronResult<Option<(Vec<u8>, u16)>> {
425 let mut buf2 = [0u8; 2];
427 if r.read_exact(&mut buf2).is_err() {
428 return Ok(None); }
430 let entry_count = u16::from_le_bytes(buf2);
431
432 if entry_count == 0 || entry_count > WAL_BLOCK_SIZE as u16 {
433 return Ok(None); }
435
436 let mut buf4 = [0u8; 4];
438 if r.read_exact(&mut buf4).is_err() {
439 return Ok(None); }
441 let compressed_len = u32::from_le_bytes(buf4) as usize;
442
443 if compressed_len == 0 || compressed_len > MAX_BLOCK_DECOMPRESSED {
444 return Ok(None); }
446
447 let compressed = match crate::format::read_bounded_vec(r, compressed_len, "WAL block") {
449 Ok(c) => c,
450 Err(_) => return Ok(None), };
452
453 let decompressed = crate::compression::decompress(&compressed, algo, MAX_BLOCK_DECOMPRESSED)?;
455 Ok(Some((decompressed, entry_count)))
456}
457
458pub fn write_wal_header<W: Write>(w: &mut W, entry_count: u32, base_seq: u32) -> HoronResult<()> {
460 w.write_all(&entry_count.to_le_bytes())?;
461 w.write_all(&base_seq.to_le_bytes())?;
462 Ok(())
463}
464
465pub fn read_wal_header<R: Read>(r: &mut R) -> HoronResult<(u32, u32)> {
467 let mut buf4 = [0u8; 4];
468 r.read_exact(&mut buf4)?;
469 let entry_count = u32::from_le_bytes(buf4);
470 r.read_exact(&mut buf4)?;
471 let base_seq = u32::from_le_bytes(buf4);
472 Ok((entry_count, base_seq))
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use std::io::Cursor;
479
480 fn l0() -> SemLayout {
482 SemLayout::plain(0)
483 }
484
485 fn make_insert_entry(seq: u32, key: &str, data: &[u8]) -> WalEntry {
486 WalEntry {
487 seq,
488 op: OP_INSERT,
489 key: key.to_string(),
490 payload: WalPayload::Insert(NodeEntry {
491 key: key.to_string(),
492 data: data.to_vec(),
493 metadata: vec![],
494 semantic_coords: vec![],
495 }),
496 }
497 }
498
499 #[test]
500 fn test_wal_entry_insert_roundtrip() {
501 let entry = make_insert_entry(1, "/test", b"hello");
502
503 let mut buf = Vec::new();
504 entry.write_to(&mut buf, &l0()).unwrap();
505
506 let mut cursor = Cursor::new(&buf);
507 let parsed = WalEntry::read_from(&mut cursor, &l0()).unwrap().unwrap();
508
509 assert_eq!(parsed.seq, 1);
510 assert_eq!(parsed.op, OP_INSERT);
511 assert_eq!(parsed.key, "/test");
512 match parsed.payload {
513 WalPayload::Insert(e) => assert_eq!(e.data, b"hello"),
514 _ => panic!("expected Insert"),
515 }
516 }
517
518 #[test]
519 fn test_wal_entry_delete_roundtrip() {
520 let entry = WalEntry {
521 seq: 5,
522 op: OP_DELETE,
523 key: "/gone".to_string(),
524 payload: WalPayload::Delete,
525 };
526
527 let mut buf = Vec::new();
528 entry.write_to(&mut buf, &l0()).unwrap();
529
530 let mut cursor = Cursor::new(&buf);
531 let parsed = WalEntry::read_from(&mut cursor, &l0()).unwrap().unwrap();
532
533 assert_eq!(parsed.seq, 5);
534 assert_eq!(parsed.op, OP_DELETE);
535 assert!(matches!(parsed.payload, WalPayload::Delete));
536 }
537
538 #[test]
539 fn test_wal_entry_set_meta_roundtrip() {
540 let entry = WalEntry {
541 seq: 10,
542 op: OP_SET_META,
543 key: "/doc".to_string(),
544 payload: WalPayload::SetMeta {
545 meta_key: "author".to_string(),
546 meta_value: "alice".to_string(),
547 },
548 };
549
550 let mut buf = Vec::new();
551 entry.write_to(&mut buf, &l0()).unwrap();
552
553 let mut cursor = Cursor::new(&buf);
554 let parsed = WalEntry::read_from(&mut cursor, &l0()).unwrap().unwrap();
555
556 assert_eq!(parsed.seq, 10);
557 match parsed.payload {
558 WalPayload::SetMeta { meta_key, meta_value } => {
559 assert_eq!(meta_key, "author");
560 assert_eq!(meta_value, "alice");
561 }
562 _ => panic!("expected SetMeta"),
563 }
564 }
565
566 #[test]
567 fn test_wal_corrupted_crc_returns_none() {
568 let entry = make_insert_entry(1, "/test", b"data");
569 let mut buf = Vec::new();
570 entry.write_to(&mut buf, &l0()).unwrap();
571
572 let len = buf.len();
574 buf[len - 1] ^= 0xFF;
575
576 let mut cursor = Cursor::new(&buf);
577 let result = WalEntry::read_from(&mut cursor, &l0()).unwrap();
578 assert!(result.is_none(), "corrupted CRC should return None");
579 }
580
581 #[test]
582 fn test_wal_multiple_entries() {
583 let entries = vec![
584 make_insert_entry(1, "/a", b"aaa"),
585 make_insert_entry(2, "/b", b"bbb"),
586 WalEntry {
587 seq: 3,
588 op: OP_DELETE,
589 key: "/a".to_string(),
590 payload: WalPayload::Delete,
591 },
592 ];
593
594 let mut buf = Vec::new();
595 for e in &entries {
596 e.write_to(&mut buf, &l0()).unwrap();
597 }
598
599 let mut cursor = Cursor::new(&buf);
600 let e1 = WalEntry::read_from(&mut cursor, &l0()).unwrap().unwrap();
601 let e2 = WalEntry::read_from(&mut cursor, &l0()).unwrap().unwrap();
602 let e3 = WalEntry::read_from(&mut cursor, &l0()).unwrap().unwrap();
603
604 assert_eq!(e1.seq, 1);
605 assert_eq!(e2.seq, 2);
606 assert_eq!(e3.seq, 3);
607 assert_eq!(e3.op, OP_DELETE);
608
609 let e4 = WalEntry::read_from(&mut cursor, &l0()).unwrap();
611 assert!(e4.is_none());
612 }
613
614 #[test]
615 fn test_wal_header_roundtrip() {
616 let mut buf = Vec::new();
617 write_wal_header(&mut buf, 42, 100).unwrap();
618
619 let mut cursor = Cursor::new(&buf);
620 let (count, base) = read_wal_header(&mut cursor).unwrap();
621 assert_eq!(count, 42);
622 assert_eq!(base, 100);
623 }
624
625 #[test]
626 fn test_wal_block_roundtrip() {
627 let entries: Vec<WalEntry> = (1..=10)
628 .map(|i| make_insert_entry(i, &format!("/n{}", i), b"data"))
629 .collect();
630
631 let mut raw = Vec::new();
633 for e in &entries {
634 e.write_to(&mut raw, &l0()).unwrap();
635 }
636
637 let mut block_buf = Vec::new();
639 write_wal_block(&mut block_buf, &raw, entries.len() as u16, ALGO_ZSTD).unwrap();
640
641 let mut cursor = Cursor::new(&block_buf);
643 let (decompressed, count) = read_wal_block(&mut cursor, ALGO_ZSTD).unwrap().unwrap();
644 assert_eq!(count, 10);
645 assert_eq!(decompressed, raw);
646
647 let mut inner = Cursor::new(&decompressed);
649 for i in 1..=10u32 {
650 let parsed = WalEntry::read_from(&mut inner, &l0()).unwrap().unwrap();
651 assert_eq!(parsed.seq, i);
652 }
653 assert!(WalEntry::read_from(&mut inner, &l0()).unwrap().is_none());
654 }
655
656 #[test]
657 fn test_wal_block_single_entry() {
658 let entry = make_insert_entry(1, "/single", b"one");
659 let mut raw = Vec::new();
660 entry.write_to(&mut raw, &l0()).unwrap();
661
662 let mut block_buf = Vec::new();
663 write_wal_block(&mut block_buf, &raw, 1, ALGO_ZSTD).unwrap();
664
665 let mut cursor = Cursor::new(&block_buf);
666 let (decompressed, count) = read_wal_block(&mut cursor, ALGO_ZSTD).unwrap().unwrap();
667 assert_eq!(count, 1);
668 assert_eq!(decompressed, raw);
669 }
670
671 #[test]
672 fn test_wal_block_full_64() {
673 let entries: Vec<WalEntry> = (1..=64)
674 .map(|i| make_insert_entry(i, &format!("/node_{}", i), b"payload"))
675 .collect();
676
677 let mut raw = Vec::new();
678 for e in &entries {
679 e.write_to(&mut raw, &l0()).unwrap();
680 }
681
682 let mut block_buf = Vec::new();
683 write_wal_block(&mut block_buf, &raw, 64, ALGO_ZSTD).unwrap();
684
685 let mut cursor = Cursor::new(&block_buf);
686 let (decompressed, count) = read_wal_block(&mut cursor, ALGO_ZSTD).unwrap().unwrap();
687 assert_eq!(count, 64);
688 assert_eq!(decompressed, raw);
689 }
690
691 #[test]
692 fn test_wal_block_eof_returns_none() {
693 let cursor = Cursor::new(Vec::<u8>::new());
694 let result = read_wal_block(&mut cursor.clone(), ALGO_ZSTD).unwrap();
695 assert!(result.is_none());
696 }
697
698 #[test]
699 fn test_wal_block_truncated_header() {
700 let mut cursor = Cursor::new(vec![0x01]);
702 let result = read_wal_block(&mut cursor, ALGO_ZSTD).unwrap();
703 assert!(result.is_none());
704 }
705
706 #[test]
707 fn test_wal_block_multiple_blocks() {
708 let mut all_blocks = Vec::new();
709 let mut expected_entries = Vec::new();
710
711 for block_idx in 0..3u32 {
713 let count = if block_idx < 2 { 64 } else { 12 };
714 let entries: Vec<WalEntry> = (0..count)
715 .map(|i| {
716 let seq = block_idx * 64 + i + 1;
717 make_insert_entry(seq, &format!("/b{}/n{}", block_idx, i), b"x")
718 })
719 .collect();
720
721 let mut raw = Vec::new();
722 for e in &entries {
723 e.write_to(&mut raw, &l0()).unwrap();
724 }
725
726 write_wal_block(&mut all_blocks, &raw, count as u16, ALGO_ZSTD).unwrap();
727 expected_entries.extend(entries);
728 }
729
730 let mut cursor = Cursor::new(&all_blocks);
732 let mut read_count = 0u32;
733 for block_idx in 0..3 {
734 let (decompressed, count) = read_wal_block(&mut cursor, ALGO_ZSTD).unwrap().unwrap();
735 let expected_count: u16 = if block_idx < 2 { 64 } else { 12 };
736 assert_eq!(count, expected_count);
737
738 let mut inner = Cursor::new(&decompressed);
739 for _ in 0..count {
740 let parsed = WalEntry::read_from(&mut inner, &l0()).unwrap().unwrap();
741 assert_eq!(parsed.seq, read_count + 1);
742 read_count += 1;
743 }
744 }
745 assert_eq!(read_count, 140);
746
747 assert!(read_wal_block(&mut cursor, ALGO_ZSTD).unwrap().is_none());
749 }
750}