1use std::fmt;
8use std::io::{Read, Write};
9use std::path::PathBuf;
10
11const WAL_MAGIC: &[u8; 4] = b"AKAR";
13const WAL_VERSION: u16 = 2;
15
16#[derive(Debug, Clone, PartialEq)]
18pub enum WALRecord {
19 Insert {
20 table_id: u64,
21
22 data: Vec<u8>,
23 },
24 Delete {
25 table_id: u64,
26 row_id: u64,
27 },
28 Update {
29 table_id: u64,
30 row_id: u64,
31 column: u32,
32 data: Vec<u8>,
33 },
34 UpdateFsm {
36 page_idx: u64,
37 is_free: bool,
38 },
39 ColumnWrite {
41 table_id: u64,
42 col_id: u32,
43 page_id: u64,
44 data: Vec<u8>,
45 },
46 LocalWALData {
49 data: Vec<u8>,
50 },
51 Commit {
52 transaction_id: u64,
53 },
54 Rollback {
55 transaction_id: u64,
56 },
57 Checkpoint,
58 CreateTable {
61 table_id: u64,
62 },
63 DropTable {
65 table_id: u64,
66 },
67 AlterTable {
69 table_id: u64,
70 },
71 CreateIndex {
73 table_id: u64,
74 },
75 DropIndex {
77 table_id: u64,
78 },
79 CreateSequence {
81 table_id: u64,
82 },
83}
84
85pub type WalSink = std::sync::Arc<std::sync::Mutex<Vec<WALRecord>>>;
90
91pub fn log_insert_record(sink: &Option<WalSink>, table_id: u64, row: &[akar_common::types::Value]) {
95 if let Some(sink) = sink
96 && let Ok(mut buf) = sink.lock()
97 {
98 buf.push(WALRecord::Insert {
99 table_id,
100 data: crate::serialize_values_to_bytes(row),
101 });
102 }
103}
104
105pub fn log_rel_insert_record(
110 sink: &Option<WalSink>,
111 table_id: u64,
112 src: u64,
113 dst: u64,
114 props: &[akar_common::types::Value],
115) {
116 use akar_common::types::Value;
117 let mut row = Vec::with_capacity(props.len() + 2);
118 row.push(Value::UInt64(src));
119 row.push(Value::UInt64(dst));
120 row.extend_from_slice(props);
121 log_insert_record(sink, table_id, &row);
122}
123
124pub fn log_delete_record(sink: &Option<WalSink>, table_id: u64, row_id: u64) {
126 if let Some(sink) = sink
127 && let Ok(mut buf) = sink.lock()
128 {
129 buf.push(WALRecord::Delete { table_id, row_id });
130 }
131}
132
133pub fn log_update_record(
136 sink: &Option<WalSink>,
137 table_id: u64,
138 row_id: u64,
139 column: u32,
140 value: &akar_common::types::Value,
141) {
142 if let Some(sink) = sink
143 && let Ok(mut buf) = sink.lock()
144 {
145 buf.push(WALRecord::Update {
146 table_id,
147 row_id,
148 column,
149 data: crate::serialize_values_to_bytes(std::slice::from_ref(value)),
150 });
151 }
152}
153
154pub fn decode_wal_buffer(buffer: &[u8]) -> std::io::Result<Vec<WALRecord>> {
161 use akar_common::serialization::Deserialize;
162 let mut cursor = std::io::Cursor::new(buffer);
163 let mut records = Vec::new();
164 while (cursor.position() as usize) < buffer.len() {
165 let mut tag_buf = [0u8; 1];
166 if cursor.read_exact(&mut tag_buf).is_err() {
167 break;
168 }
169 let record = match tag_buf[0] {
170 b'I' => {
171 let table_id = u64::deserialize(&mut cursor)?;
172 let data_len = u32::deserialize(&mut cursor)? as usize;
173 let mut data = vec![0u8; data_len];
174 cursor.read_exact(&mut data)?;
175 WALRecord::Insert { table_id, data }
176 }
177 b'D' => {
178 let table_id = u64::deserialize(&mut cursor)?;
179 let row_id = u64::deserialize(&mut cursor)?;
180 WALRecord::Delete { table_id, row_id }
181 }
182 b'U' => {
183 let table_id = u64::deserialize(&mut cursor)?;
184 let row_id = u64::deserialize(&mut cursor)?;
185 let column = u32::deserialize(&mut cursor)?;
186 let data_len = u32::deserialize(&mut cursor)? as usize;
187 let mut data = vec![0u8; data_len];
188 cursor.read_exact(&mut data)?;
189 WALRecord::Update {
190 table_id,
191 row_id,
192 column,
193 data,
194 }
195 }
196 b'C' => WALRecord::Commit {
197 transaction_id: u64::deserialize(&mut cursor)?,
198 },
199 _ => {
204 tracing::warn!(
205 "LocalWAL blob: unexpected tag byte 0x{:02x} at offset {}; keeping {} decoded record(s)",
206 tag_buf[0],
207 cursor.position(),
208 records.len()
209 );
210 break;
211 }
212 };
213 records.push(record);
214 }
215 Ok(records)
216}
217
218impl fmt::Display for WALRecord {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 match self {
221 WALRecord::Insert { table_id, data } => {
222 write!(f, "INSERT table={} data_len={}", table_id, data.len())
223 }
224 WALRecord::Delete { table_id, row_id } => {
225 write!(f, "DELETE table={} row={}", table_id, row_id)
226 }
227 WALRecord::Update {
228 table_id,
229 row_id,
230 column,
231 data,
232 } => {
233 write!(
234 f,
235 "UPDATE table={} row={} col={} data_len={}",
236 table_id,
237 row_id,
238 column,
239 data.len()
240 )
241 }
242 WALRecord::UpdateFsm { page_idx, is_free } => {
243 write!(f, "FSM page={} {}", page_idx, if *is_free { "FREE" } else { "ALLOC" })
244 }
245 WALRecord::ColumnWrite {
246 table_id,
247 col_id,
248 page_id,
249 data,
250 } => {
251 write!(
252 f,
253 "COLUMN_WRITE table={} col={} page={} data_len={}",
254 table_id,
255 col_id,
256 page_id,
257 data.len()
258 )
259 }
260 WALRecord::LocalWALData { data } => {
261 write!(f, "LOCAL_WAL data_len={}", data.len())
262 }
263 WALRecord::Commit { transaction_id } => {
264 write!(f, "COMMIT txn={}", transaction_id)
265 }
266 WALRecord::Rollback { transaction_id } => {
267 write!(f, "ROLLBACK txn={}", transaction_id)
268 }
269 WALRecord::Checkpoint => write!(f, "CHECKPOINT"),
270 WALRecord::CreateTable { table_id } => {
271 write!(f, "CREATE_TABLE id={}", table_id)
272 }
273 WALRecord::DropTable { table_id } => {
274 write!(f, "DROP_TABLE id={}", table_id)
275 }
276 WALRecord::AlterTable { table_id } => {
277 write!(f, "ALTER_TABLE id={}", table_id)
278 }
279 WALRecord::CreateIndex { table_id } => {
280 write!(f, "CREATE_INDEX table_id={}", table_id)
281 }
282 WALRecord::DropIndex { table_id } => {
283 write!(f, "DROP_INDEX table_id={}", table_id)
284 }
285 WALRecord::CreateSequence { table_id } => {
286 write!(f, "CREATE_SEQUENCE id={}", table_id)
287 }
288 }
289 }
290}
291
292pub struct WAL {
298 path: PathBuf,
299 records: Vec<WALRecord>,
300 flushed_count: usize,
302 needs_header: bool,
304 total_size: usize,
305 is_dirty: bool,
306}
307
308impl WAL {
309 pub fn new(path: PathBuf) -> Self {
310 Self {
311 path,
312 records: Vec::new(),
313 flushed_count: 0,
314 needs_header: true,
315 total_size: 0,
316 is_dirty: false,
317 }
318 }
319
320 pub fn path(&self) -> &PathBuf {
321 &self.path
322 }
323
324 pub fn append(&mut self, record: WALRecord) {
325 let size = match &record {
326 WALRecord::Insert { data, .. } => data.len(),
327 WALRecord::Update { data, .. } => data.len(),
328 WALRecord::UpdateFsm { .. } => 8 + 1, WALRecord::ColumnWrite { data, .. } => data.len(),
330 WALRecord::LocalWALData { data } => data.len(),
331 WALRecord::CreateTable { .. }
333 | WALRecord::DropTable { .. }
334 | WALRecord::AlterTable { .. }
335 | WALRecord::CreateIndex { .. }
336 | WALRecord::DropIndex { .. }
337 | WALRecord::CreateSequence { .. } => 8,
338 _ => 8,
339 };
340 self.total_size += size;
341 self.is_dirty = true;
342 self.records.push(record);
343 }
344
345 pub fn log_column_write(&mut self, table_id: u64, col_id: u32, page_id: u64, data: &[u8]) {
347 self.append(WALRecord::ColumnWrite {
348 table_id,
349 col_id,
350 page_id,
351 data: data.to_vec(),
352 });
353 }
354
355 pub fn write_raw_buffer(&mut self, local_wal_buffer: &[u8]) {
364 if local_wal_buffer.is_empty() {
365 return;
366 }
367 self.total_size += local_wal_buffer.len();
368 self.is_dirty = true;
369 self.records.push(WALRecord::LocalWALData {
370 data: local_wal_buffer.to_vec(),
371 });
372 }
373
374 pub fn records(&self) -> &[WALRecord] {
375 &self.records
376 }
377 pub fn clear(&mut self) -> std::io::Result<()> {
382 self.records.clear();
383 self.flushed_count = 0;
384 self.total_size = 0;
385 self.is_dirty = false;
386 self.needs_header = true;
387 std::fs::OpenOptions::new()
389 .create(true)
390 .write(true)
391 .truncate(true)
392 .open(&self.path)?
393 .sync_data()
394 }
395 pub fn len(&self) -> usize {
396 self.records.len()
397 }
398 pub fn is_empty(&self) -> bool {
399 self.records.is_empty()
400 }
401 pub fn total_size(&self) -> usize {
402 self.total_size
403 }
404 pub fn is_dirty(&self) -> bool {
405 self.is_dirty
406 }
407
408 pub fn replay<F>(&self, mut apply: F) -> std::io::Result<()>
410 where
411 F: FnMut(&WALRecord) -> std::io::Result<()>,
412 {
413 for record in &self.records {
414 apply(record)?;
415 }
416 Ok(())
417 }
418
419 pub fn flush_to_disk(&mut self) -> std::io::Result<()> {
442 if self.flushed_count >= self.records.len() {
443 return Ok(()); }
445
446 let mut file = std::fs::OpenOptions::new().create(true).append(true).open(&self.path)?;
447
448 if self.needs_header {
450 file.write_all(WAL_MAGIC)?;
451 file.write_all(&WAL_VERSION.to_le_bytes())?;
452 self.needs_header = false;
453 }
454
455 let new_records = &self.records[self.flushed_count..];
457 Self::append_records_to_file(&mut file, new_records)?;
458
459 self.flushed_count = self.records.len();
460 file.sync_data()
461 }
462
463 fn append_records_to_file(file: &mut std::fs::File, records: &[WALRecord]) -> std::io::Result<()> {
465 use akar_common::serialization::Serialize;
466 let mut crc_buf = [0u8; 4];
467 for record in records {
468 let mut payload = Vec::new();
469 match record {
470 WALRecord::Insert { table_id, data } => {
471 payload.write_all(b"I")?;
472 table_id.serialize(&mut payload)?;
473 (data.len() as u32).serialize(&mut payload)?;
474 payload.write_all(data)?;
475 }
476 WALRecord::Delete { table_id, row_id } => {
477 payload.write_all(b"D")?;
478 table_id.serialize(&mut payload)?;
479 row_id.serialize(&mut payload)?;
480 }
481 WALRecord::Update {
482 table_id,
483 row_id,
484 column,
485 data,
486 } => {
487 payload.write_all(b"U")?;
488 table_id.serialize(&mut payload)?;
489 row_id.serialize(&mut payload)?;
490 column.serialize(&mut payload)?;
491 (data.len() as u32).serialize(&mut payload)?;
492 payload.write_all(data)?;
493 }
494 WALRecord::UpdateFsm { page_idx, is_free } => {
495 payload.write_all(b"F")?;
496 page_idx.serialize(&mut payload)?;
497 let is_free_u8: u8 = if *is_free { 1 } else { 0 };
498 is_free_u8.serialize(&mut payload)?;
499 }
500 WALRecord::ColumnWrite {
501 table_id,
502 col_id,
503 page_id,
504 data,
505 } => {
506 payload.write_all(b"W")?;
507 table_id.serialize(&mut payload)?;
508 col_id.serialize(&mut payload)?;
509 page_id.serialize(&mut payload)?;
510 (data.len() as u32).serialize(&mut payload)?;
511 payload.write_all(data)?;
512 }
513 WALRecord::LocalWALData { data } => {
514 payload.write_all(b"L")?;
515 (data.len() as u32).serialize(&mut payload)?;
516 payload.write_all(data)?;
517 }
518 WALRecord::Commit { transaction_id } => {
519 payload.write_all(b"C")?;
520 transaction_id.serialize(&mut payload)?;
521 }
522 WALRecord::Rollback { transaction_id } => {
523 payload.write_all(b"R")?;
524 transaction_id.serialize(&mut payload)?;
525 }
526 WALRecord::Checkpoint => {
527 payload.write_all(b"K")?;
528 }
529 WALRecord::CreateTable { table_id } => {
531 payload.write_all(b"T")?;
532 table_id.serialize(&mut payload)?;
533 }
534 WALRecord::DropTable { table_id } => {
535 payload.write_all(b"A")?;
536 table_id.serialize(&mut payload)?;
537 }
538 WALRecord::AlterTable { table_id } => {
539 payload.write_all(b"M")?;
540 table_id.serialize(&mut payload)?;
541 }
542 WALRecord::CreateIndex { table_id } => {
543 payload.write_all(b"N")?;
544 table_id.serialize(&mut payload)?;
545 }
546 WALRecord::DropIndex { table_id } => {
547 payload.write_all(b"X")?;
548 table_id.serialize(&mut payload)?;
549 }
550 WALRecord::CreateSequence { table_id } => {
551 payload.write_all(b"Q")?;
552 table_id.serialize(&mut payload)?;
553 }
554 }
555 let checksum = crc32fast::hash(&payload);
556 crc_buf.copy_from_slice(&checksum.to_le_bytes());
557 file.write_all(&crc_buf)?;
558 file.write_all(&payload)?;
559 }
560 Ok(())
561 }
562
563 pub fn load_from_disk(&mut self) -> std::io::Result<()> {
568 use std::io::{BufReader, Read};
569
570 if !self.path.exists() {
571 return Ok(()); }
573
574 let file = std::fs::File::open(&self.path)?;
575 let mut reader = BufReader::new(file);
576
577 let mut buffer = Vec::new();
579 reader.read_to_end(&mut buffer)?;
580
581 if buffer.is_empty() {
582 return Ok(());
583 }
584
585 let (is_v2, cursor_pos) = if buffer.len() >= 6 && &buffer[..4] == WAL_MAGIC {
587 let version = u16::from_le_bytes([buffer[4], buffer[5]]);
588 if version >= 2 { (true, 6usize) } else { (false, 0usize) }
589 } else {
590 (false, 0usize)
591 };
592
593 let mut cursor = std::io::Cursor::new(&buffer[cursor_pos..]);
594 let mut skipped = 0u32;
595
596 if is_v2 {
597 while cursor.position() < (buffer.len() - cursor_pos) as u64 {
599 let mut crc_bytes = [0u8; 4];
601 if cursor.read_exact(&mut crc_bytes).is_err() {
602 break;
603 }
604 let expected_crc = u32::from_le_bytes(crc_bytes);
605
606 let record_start = cursor.position() as usize;
608 let record_data = &buffer[(cursor_pos + record_start)..];
609 if record_data.is_empty() {
610 break;
611 }
612
613 let tag = record_data[0];
616 let payload_len = match tag {
617 b'I' => {
618 if record_data.len() < 13 {
620 skipped += 1;
621 break;
622 }
623 let data_len = u32::from_le_bytes(record_data[9..13].try_into().unwrap()) as usize;
624 1 + 8 + 4 + data_len }
626 b'D' => 1 + 8 + 8, b'U' => {
628 if record_data.len() < 25 {
630 skipped += 1;
631 break;
632 }
633 let data_len = u32::from_le_bytes(record_data[21..25].try_into().unwrap()) as usize;
634 1 + 8 + 8 + 4 + 4 + data_len
635 }
636 b'F' => 1 + 8 + 1, b'W' => {
638 if record_data.len() < 25 {
640 skipped += 1;
641 break;
642 }
643 let data_len = u32::from_le_bytes(record_data[21..25].try_into().unwrap()) as usize;
644 1 + 8 + 4 + 8 + 4 + data_len
645 }
646 b'L' => {
647 if record_data.len() < 5 {
648 skipped += 1;
649 break;
650 }
651 let data_len = u32::from_le_bytes(record_data[1..5].try_into().unwrap()) as usize;
652 1 + 4 + data_len
653 }
654 b'C' => 1 + 8, b'R' => 1 + 8,
656 b'K' => 1, b'T' | b'A' | b'M' | b'N' | b'X' | b'Q' => 1 + 8,
659 _ => {
660 skipped += 1;
662 break;
663 }
664 };
665
666 if payload_len > record_data.len() {
667 skipped += 1;
668 break;
669 }
670
671 let computed_crc = crc32fast::hash(&record_data[..payload_len]);
673
674 if computed_crc != expected_crc {
675 skipped += 1;
677 cursor.set_position(cursor.position() + payload_len as u64);
678 continue;
679 }
680
681 cursor.set_position(cursor.position() + payload_len as u64);
683 let mut inner = std::io::Cursor::new(&record_data[..payload_len]);
684 self.parse_record(&mut inner)?;
685 }
686 } else {
687 self.parse_v1_records(&mut cursor)?;
689 }
690
691 if skipped > 0 {
692 tracing::warn!(
693 "WAL: skipped {} corrupted record(s) during recovery (v{})",
694 skipped,
695 if is_v2 { 2 } else { 1 }
696 );
697 }
698
699 self.flushed_count = self.records.len();
701 self.needs_header = false;
702 self.total_size = buffer.len();
703 self.is_dirty = !self.records.is_empty();
704 Ok(())
705 }
706
707 fn parse_record(&mut self, cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result<()> {
709 use akar_common::serialization::Deserialize;
710 let mut tag_buf = [0u8; 1];
711 if cursor.read_exact(&mut tag_buf).is_err() {
712 return Ok(());
713 }
714 let tag = tag_buf[0];
715 match tag {
716 b'I' => {
717 let table_id = u64::deserialize(cursor)?;
718 let data_len = u32::deserialize(cursor)? as usize;
719 let mut data = vec![0u8; data_len];
720 cursor.read_exact(&mut data)?;
721 self.records.push(WALRecord::Insert { table_id, data });
722 }
723 b'D' => {
724 let table_id = u64::deserialize(cursor)?;
725 let row_id = u64::deserialize(cursor)?;
726 self.records.push(WALRecord::Delete { table_id, row_id });
727 }
728 b'U' => {
729 let table_id = u64::deserialize(cursor)?;
730 let row_id = u64::deserialize(cursor)?;
731 let column = u32::deserialize(cursor)?;
732 let data_len = u32::deserialize(cursor)? as usize;
733 let mut data = vec![0u8; data_len];
734 cursor.read_exact(&mut data)?;
735 self.records.push(WALRecord::Update {
736 table_id,
737 row_id,
738 column,
739 data,
740 });
741 }
742 b'F' => {
743 let page_idx = u64::deserialize(cursor)?;
744 let is_free_u8 = u8::deserialize(cursor)?;
745 self.records.push(WALRecord::UpdateFsm {
746 page_idx,
747 is_free: is_free_u8 != 0,
748 });
749 }
750 b'W' => {
751 let table_id = u64::deserialize(cursor)?;
752 let col_id = u32::deserialize(cursor)?;
753 let page_id = u64::deserialize(cursor)?;
754 let data_len = u32::deserialize(cursor)? as usize;
755 let mut data = vec![0u8; data_len];
756 cursor.read_exact(&mut data)?;
757 self.records.push(WALRecord::ColumnWrite {
758 table_id,
759 col_id,
760 page_id,
761 data,
762 });
763 }
764 b'C' => {
765 let transaction_id = u64::deserialize(cursor)?;
766 self.records.push(WALRecord::Commit { transaction_id });
767 }
768 b'R' => {
769 let transaction_id = u64::deserialize(cursor)?;
770 self.records.push(WALRecord::Rollback { transaction_id });
771 }
772 b'L' => {
773 let data_len = u32::deserialize(cursor)? as usize;
774 let mut data = vec![0u8; data_len];
775 cursor.read_exact(&mut data)?;
776 self.records.push(WALRecord::LocalWALData { data });
777 }
778 b'K' => {
779 self.records.push(WALRecord::Checkpoint);
780 }
781 b'T' => {
783 let table_id = u64::deserialize(cursor)?;
784 self.records.push(WALRecord::CreateTable { table_id });
785 }
786 b'A' => {
787 let table_id = u64::deserialize(cursor)?;
788 self.records.push(WALRecord::DropTable { table_id });
789 }
790 b'M' => {
791 let table_id = u64::deserialize(cursor)?;
792 self.records.push(WALRecord::AlterTable { table_id });
793 }
794 b'N' => {
795 let table_id = u64::deserialize(cursor)?;
796 self.records.push(WALRecord::CreateIndex { table_id });
797 }
798 b'X' => {
799 let table_id = u64::deserialize(cursor)?;
800 self.records.push(WALRecord::DropIndex { table_id });
801 }
802 b'Q' => {
803 let table_id = u64::deserialize(cursor)?;
804 self.records.push(WALRecord::CreateSequence { table_id });
805 }
806 _ => {
807 return Err(std::io::Error::new(
808 std::io::ErrorKind::InvalidData,
809 format!("WAL: unknown record tag byte: 0x{:02x}", tag),
810 ));
811 }
812 }
813 Ok(())
814 }
815
816 fn parse_v1_records(&mut self, cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result<()> {
818 while cursor.position() < cursor.get_ref().len() as u64 {
819 self.parse_record(cursor)?;
820 }
821 Ok(())
822 }
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828
829 #[test]
830 fn test_decode_wal_buffer_roundtrip() {
831 use crate::local_wal::LocalWAL;
832 use akar_common::types::Value;
833
834 let mut lwal = LocalWAL::new();
837 lwal.log_insert(
838 7,
839 crate::serialize_values_to_bytes(&[Value::Int64(42), Value::String("hi".into())]),
840 );
841 lwal.log_delete(7, 3);
842 lwal.log_update(7, 5, 1, crate::serialize_values_to_bytes(&[Value::Double(2.5)]));
843
844 let decoded = decode_wal_buffer(lwal.buffer()).unwrap();
845 assert_eq!(decoded.len(), 3);
846 match &decoded[0] {
847 WALRecord::Insert { table_id, data } => {
848 assert_eq!(*table_id, 7);
849 let values = crate::deserialize_values_from_bytes(data, 2);
850 assert_eq!(values[0], Value::Int64(42));
851 assert_eq!(values[1], Value::String("hi".into()));
852 }
853 other => panic!("expected Insert, got {other:?}"),
854 }
855 assert_eq!(decoded[1], WALRecord::Delete { table_id: 7, row_id: 3 });
856 match &decoded[2] {
857 WALRecord::Update {
858 table_id,
859 row_id,
860 column,
861 data,
862 } => {
863 assert_eq!(*table_id, 7);
864 assert_eq!(*row_id, 5);
865 assert_eq!(*column, 1);
866 assert_eq!(crate::deserialize_values_from_bytes(data, 1), vec![Value::Double(2.5)]);
867 }
868 other => panic!("expected Update, got {other:?}"),
869 }
870 }
871
872 #[test]
873 fn test_decode_wal_buffer_empty_and_unknown_tag() {
874 assert!(decode_wal_buffer(&[]).unwrap().is_empty());
876
877 let mut buf = Vec::new();
880 let mut lwal = crate::local_wal::LocalWAL::new();
881 lwal.log_insert(1, vec![9]);
882 buf.extend_from_slice(lwal.buffer());
883 buf.push(b'Z'); buf.push(0xFF); let decoded = decode_wal_buffer(&buf).unwrap();
887 assert_eq!(decoded.len(), 1);
888 assert_eq!(
889 decoded[0],
890 WALRecord::Insert {
891 table_id: 1,
892 data: vec![9]
893 }
894 );
895 }
896
897 #[test]
898 fn test_serialize_values_to_bytes_roundtrip() {
899 use akar_common::types::Value;
900 let row = vec![
901 Value::Null,
902 Value::Bool(true),
903 Value::Int64(-17),
904 Value::Double(1.25),
905 Value::String("akar".into()),
906 ];
907 let bytes = crate::serialize_values_to_bytes(&row);
908 assert_eq!(
909 crate::deserialize_values_from_bytes(&bytes, row.len()),
910 row,
911 "serialize_values_to_bytes must roundtrip through the recovery deserializer"
912 );
913 }
914
915 #[test]
916 fn test_log_helpers_noop_without_sink() {
917 use akar_common::types::Value;
918 let none: Option<WalSink> = None;
919 log_insert_record(&none, 1, &[Value::Int64(1)]);
921 log_delete_record(&none, 1, 0);
922 log_update_record(&none, 1, 0, 0, &Value::Null);
923 log_rel_insert_record(&none, 1, 0, 1, &[]);
924 }
925
926 #[test]
927 fn test_wal_append_clear() {
928 let dir = tempfile::tempdir().unwrap();
929 let mut wal = WAL::new(dir.path().join("wal.log"));
930 assert!(wal.is_empty());
931 wal.append(WALRecord::Insert {
932 table_id: 1,
933 data: vec![1, 2, 3],
934 });
935 assert_eq!(wal.len(), 1);
936 assert!(wal.is_dirty());
937 wal.append(WALRecord::Commit { transaction_id: 42 });
938 assert_eq!(wal.len(), 2);
939 wal.clear().unwrap();
940 assert!(wal.is_empty());
941 assert!(!wal.is_dirty());
942 }
943
944 #[test]
945 fn test_wal_replay() {
946 let dir = tempfile::tempdir().unwrap();
947 let mut wal = WAL::new(dir.path().join("wal.log"));
948 wal.append(WALRecord::Insert {
949 table_id: 1,
950 data: vec![10, 20],
951 });
952 wal.append(WALRecord::Commit { transaction_id: 1 });
953 let mut count = 0;
954 wal.replay(|record| {
955 count += 1;
956 if let WALRecord::Insert { table_id, data } = record {
957 assert_eq!(*table_id, 1);
958 assert_eq!(data, &[10, 20]);
959 }
960 Ok(())
961 })
962 .unwrap();
963 assert_eq!(count, 2);
964 }
965
966 #[test]
967 fn test_wal_flush_to_disk() {
968 let dir = tempfile::tempdir().unwrap();
969 let wal_path = dir.path().join("wal.log");
970 let mut wal = WAL::new(wal_path.clone());
971 wal.append(WALRecord::Insert {
972 table_id: 1,
973 data: vec![1, 2, 3],
974 });
975 wal.flush_to_disk().unwrap();
976 assert!(wal_path.exists());
977 assert!(std::fs::metadata(&wal_path).unwrap().len() > 0);
978 }
979
980 #[test]
981 fn test_wal_checksum_roundtrip() {
982 let dir = tempfile::tempdir().unwrap();
983 let wal_path = dir.path().join("wal.log");
984 let mut wal = WAL::new(wal_path.clone());
985 wal.append(WALRecord::Insert {
986 table_id: 42,
987 data: vec![10, 20, 30, 40, 50],
988 });
989 wal.append(WALRecord::Delete {
990 table_id: 7,
991 row_id: 99,
992 });
993 wal.append(WALRecord::Commit { transaction_id: 1 });
994 wal.flush_to_disk().unwrap();
995
996 let bytes = std::fs::read(&wal_path).unwrap();
998 assert_eq!(&bytes[..4], b"AKAR");
999 assert_eq!(u16::from_le_bytes([bytes[4], bytes[5]]), 2);
1000
1001 let mut wal2 = WAL::new(wal_path);
1003 wal2.load_from_disk().unwrap();
1004 assert_eq!(wal2.len(), 3);
1005 assert!(matches!(
1006 &wal2.records()[0],
1007 WALRecord::Insert { table_id: 42, data } if data == &vec![10, 20, 30, 40, 50]
1008 ));
1009 assert!(matches!(
1010 &wal2.records()[1],
1011 WALRecord::Delete {
1012 table_id: 7,
1013 row_id: 99
1014 }
1015 ));
1016 assert!(matches!(&wal2.records()[2], WALRecord::Commit { transaction_id: 1 }));
1017 }
1018
1019 #[test]
1020 fn test_wal_corrupted_record_skipped() {
1021 let dir = tempfile::tempdir().unwrap();
1022 let wal_path = dir.path().join("wal.log");
1023
1024 let mut wal = WAL::new(wal_path.clone());
1026 wal.append(WALRecord::Insert {
1027 table_id: 1,
1028 data: vec![100, 200],
1029 });
1030 wal.append(WALRecord::Commit { transaction_id: 5 });
1031 wal.flush_to_disk().unwrap();
1032
1033 let mut bytes = std::fs::read(&wal_path).unwrap();
1035 let corrupt_offset = 6 + 4 + 1 + 8 + 4 + 1; if corrupt_offset < bytes.len() {
1039 bytes[corrupt_offset] ^= 0xFF;
1040 }
1041 std::fs::write(&wal_path, &bytes).unwrap();
1042
1043 let mut wal2 = WAL::new(wal_path);
1045 let _result = wal2.load_from_disk();
1046 }
1050
1051 #[test]
1052 fn test_wal_v1_backward_compat() {
1053 use akar_common::serialization::Serialize;
1055 let dir = tempfile::tempdir().unwrap();
1056 let wal_path = dir.path().join("wal.log");
1057 let mut file = std::fs::File::create(&wal_path).unwrap();
1058
1059 file.write_all(b"I").unwrap();
1061 42u64.serialize(&mut file).unwrap();
1062 (3u32).serialize(&mut file).unwrap();
1063 file.write_all(&[1, 2, 3]).unwrap();
1064 file.write_all(b"C").unwrap();
1066 1u64.serialize(&mut file).unwrap();
1067 drop(file);
1068
1069 let mut wal = WAL::new(wal_path);
1071 wal.load_from_disk().unwrap();
1072 assert_eq!(wal.len(), 2);
1073 assert!(matches!(&wal.records()[0], WALRecord::Insert { table_id: 42, .. }));
1074 assert!(matches!(&wal.records()[1], WALRecord::Commit { transaction_id: 1 }));
1075 }
1076
1077 #[test]
1078 fn test_wal_all_record_types_checksum() {
1079 let dir = tempfile::tempdir().unwrap();
1080 let wal_path = dir.path().join("wal.log");
1081 let mut wal = WAL::new(wal_path.clone());
1082
1083 wal.append(WALRecord::Insert {
1084 table_id: 1,
1085 data: vec![1],
1086 });
1087 wal.append(WALRecord::Delete { table_id: 2, row_id: 3 });
1088 wal.append(WALRecord::Update {
1089 table_id: 4,
1090 row_id: 5,
1091 column: 6,
1092 data: vec![7, 8],
1093 });
1094 wal.append(WALRecord::UpdateFsm {
1095 page_idx: 100,
1096 is_free: true,
1097 });
1098 wal.append(WALRecord::ColumnWrite {
1099 table_id: 10,
1100 col_id: 11,
1101 page_id: 12,
1102 data: vec![99],
1103 });
1104 wal.append(WALRecord::LocalWALData { data: vec![10, 11, 12] });
1105 wal.append(WALRecord::Commit { transaction_id: 50 });
1106 wal.append(WALRecord::Rollback { transaction_id: 51 });
1107 wal.append(WALRecord::Checkpoint);
1108 wal.append(WALRecord::CreateTable { table_id: 20 });
1109 wal.append(WALRecord::DropTable { table_id: 21 });
1110 wal.append(WALRecord::AlterTable { table_id: 22 });
1111 wal.append(WALRecord::CreateIndex { table_id: 23 });
1112 wal.append(WALRecord::DropIndex { table_id: 24 });
1113 wal.append(WALRecord::CreateSequence { table_id: 25 });
1114
1115 wal.flush_to_disk().unwrap();
1116
1117 let mut wal2 = WAL::new(wal_path);
1118 wal2.load_from_disk().unwrap();
1119 assert_eq!(wal2.len(), 15);
1120 }
1121}