Skip to main content

akar_storage/
local_wal.rs

1//! LocalWAL — per-transaction in-memory WAL buffer.
2//!
3//! Each write transaction has its own `LocalWAL` that buffers WAL records
4//! in-memory during the transaction. On commit, the entire buffer is
5//! bulk-copied into the global `WAL` via `WAL::log_committed_wal()`.
6//!
7//! This avoids contention on the global WAL mutex during writes — only
8//! the commit path needs to serialize.
9
10use crate::wal::WALRecord;
11use std::io::Write;
12
13/// A serialized WAL buffer backed by an in-memory byte vector.
14///
15/// Records are written in the same binary format as the global `WAL`
16/// (see `WAL::flush_to_disk()`), so the buffer can be bulk-copied
17/// directly into the global WAL file on commit.
18#[derive(Debug, Default)]
19pub struct LocalWAL {
20    /// Serialized WAL records in memory.
21    buffer: Vec<u8>,
22    /// Number of records buffered.
23    count: usize,
24    /// Estimated total size in bytes.
25    size: usize,
26}
27
28impl LocalWAL {
29    /// Create a new empty LocalWAL buffer.
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Log a pre-built record (drain path for typed SQL-path records, P60.2).
35    pub fn log_record(&mut self, record: &WALRecord) {
36        self.write_record(record);
37    }
38
39    /// Serialize a WAL record into the in-memory buffer.
40    fn write_record(&mut self, record: &WALRecord) {
41        use akar_common::serialization::Serialize;
42        match record {
43            WALRecord::Insert { table_id, data } => {
44                self.buffer.write_all(b"I").unwrap();
45                table_id.serialize(&mut self.buffer).unwrap();
46                (data.len() as u32).serialize(&mut self.buffer).unwrap();
47                self.buffer.write_all(data).unwrap();
48                self.size += 1 + 8 + 4 + data.len();
49            }
50            WALRecord::Delete { table_id, row_id } => {
51                self.buffer.write_all(b"D").unwrap();
52                table_id.serialize(&mut self.buffer).unwrap();
53                row_id.serialize(&mut self.buffer).unwrap();
54                self.size += 1 + 8 + 8;
55            }
56            WALRecord::Update {
57                table_id,
58                row_id,
59                column,
60                data,
61            } => {
62                self.buffer.write_all(b"U").unwrap();
63                table_id.serialize(&mut self.buffer).unwrap();
64                row_id.serialize(&mut self.buffer).unwrap();
65                column.serialize(&mut self.buffer).unwrap();
66                (data.len() as u32).serialize(&mut self.buffer).unwrap();
67                self.buffer.write_all(data).unwrap();
68                self.size += 1 + 8 + 8 + 4 + 4 + data.len();
69            }
70            WALRecord::UpdateFsm { page_idx, is_free } => {
71                self.buffer.write_all(b"F").unwrap();
72                page_idx.serialize(&mut self.buffer).unwrap();
73                let is_free_u8: u8 = if *is_free { 1 } else { 0 };
74                is_free_u8.serialize(&mut self.buffer).unwrap();
75                self.size += 1 + 8 + 1;
76            }
77            WALRecord::ColumnWrite {
78                table_id,
79                col_id,
80                page_id,
81                data,
82            } => {
83                self.buffer.write_all(b"W").unwrap();
84                table_id.serialize(&mut self.buffer).unwrap();
85                col_id.serialize(&mut self.buffer).unwrap();
86                page_id.serialize(&mut self.buffer).unwrap();
87                (data.len() as u32).serialize(&mut self.buffer).unwrap();
88                self.buffer.write_all(data).unwrap();
89                self.size += 1 + 8 + 4 + 8 + 4 + data.len();
90            }
91            WALRecord::Commit { transaction_id } => {
92                self.buffer.write_all(b"C").unwrap();
93                transaction_id.serialize(&mut self.buffer).unwrap();
94                self.size += 1 + 8;
95            }
96            WALRecord::Rollback { transaction_id } => {
97                self.buffer.write_all(b"R").unwrap();
98                transaction_id.serialize(&mut self.buffer).unwrap();
99                self.size += 1 + 8;
100            }
101            WALRecord::Checkpoint => {
102                self.buffer.write_all(b"K").unwrap();
103                self.size += 1;
104            }
105            // LocalWALData is the raw buffer from a committed LocalWAL.
106            // It is never written by a LocalWAL itself (only by the global WAL
107            // when merging). We log it as raw bytes with the 'L' tag.
108            WALRecord::LocalWALData { data } => {
109                self.buffer.write_all(b"L").unwrap();
110                (data.len() as u32).serialize(&mut self.buffer).unwrap();
111                self.buffer.write_all(data).unwrap();
112                self.size += 1 + 4 + data.len();
113            }
114            // DDL variants — each writes a tag + u64 table_id
115            WALRecord::CreateTable { table_id }
116            | WALRecord::DropTable { table_id }
117            | WALRecord::AlterTable { table_id }
118            | WALRecord::CreateIndex { table_id }
119            | WALRecord::DropIndex { table_id }
120            | WALRecord::CreateSequence { table_id } => {
121                let tag: u8 = match record {
122                    WALRecord::CreateTable { .. } => b'T',
123                    WALRecord::DropTable { .. } => b'A',
124                    WALRecord::AlterTable { .. } => b'M',
125                    WALRecord::CreateIndex { .. } => b'N',
126                    WALRecord::DropIndex { .. } => b'X',
127                    WALRecord::CreateSequence { .. } => b'Q',
128                    _ => unreachable!(),
129                };
130                self.buffer.write_all(&[tag]).unwrap();
131                table_id.serialize(&mut self.buffer).unwrap();
132                self.size += 1 + 8;
133            }
134        }
135        self.count += 1;
136    }
137
138    /// Log a table row insertion.
139    pub fn log_insert(&mut self, table_id: u64, data: Vec<u8>) {
140        self.write_record(&WALRecord::Insert { table_id, data });
141    }
142
143    /// Log a table row deletion.
144    pub fn log_delete(&mut self, table_id: u64, row_id: u64) {
145        self.write_record(&WALRecord::Delete { table_id, row_id });
146    }
147
148    /// Log a table row update.
149    pub fn log_update(&mut self, table_id: u64, row_id: u64, column: u32, data: Vec<u8>) {
150        self.write_record(&WALRecord::Update {
151            table_id,
152            row_id,
153            column,
154            data,
155        });
156    }
157
158    /// Log a column page write.
159    pub fn log_column_write(&mut self, table_id: u64, col_id: u32, page_id: u64, data: Vec<u8>) {
160        self.write_record(&WALRecord::ColumnWrite {
161            table_id,
162            col_id,
163            page_id,
164            data,
165        });
166    }
167
168    /// Log the beginning of a write transaction.
169    pub fn log_begin_transaction(&mut self) {
170        // The begin transaction marker is implicit — no separate record.
171        // The first log_*/logCommit records define the transaction boundary.
172    }
173
174    /// Log a commit (will be flushed to global WAL on commit).
175    pub fn log_commit(&mut self, transaction_id: u64) {
176        self.write_record(&WALRecord::Commit { transaction_id });
177    }
178
179    /// Log a rollback.
180    pub fn log_rollback(&mut self, transaction_id: u64) {
181        self.write_record(&WALRecord::Rollback { transaction_id });
182    }
183
184    /// Log a checkpoint marker.
185    pub fn log_checkpoint(&mut self) {
186        self.write_record(&WALRecord::Checkpoint);
187    }
188
189    /// Return a reference to the serialized buffer.
190    pub fn buffer(&self) -> &[u8] {
191        &self.buffer
192    }
193
194    /// Consume and return the serialized buffer (for bulk-copy to global WAL).
195    pub fn into_buffer(mut self) -> Vec<u8> {
196        std::mem::take(&mut self.buffer)
197    }
198
199    /// Number of records buffered.
200    pub fn count(&self) -> usize {
201        self.count
202    }
203
204    /// Total size of buffered data in bytes.
205    pub fn size(&self) -> usize {
206        self.size
207    }
208
209    /// Whether the buffer is empty.
210    pub fn is_empty(&self) -> bool {
211        self.buffer.is_empty()
212    }
213
214    /// Clear all buffered records.
215    pub fn clear(&mut self) {
216        self.buffer.clear();
217        self.count = 0;
218        self.size = 0;
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_local_wal_empty() {
228        let lwal = LocalWAL::new();
229        assert!(lwal.is_empty());
230        assert_eq!(lwal.count(), 0);
231        assert_eq!(lwal.size(), 0);
232    }
233
234    #[test]
235    fn test_local_wal_insert_record() {
236        let mut lwal = LocalWAL::new();
237        lwal.log_insert(1, vec![0x01, 0x02, 0x03]);
238        assert!(!lwal.is_empty());
239        assert_eq!(lwal.count(), 1);
240        assert!(lwal.size() > 0);
241    }
242
243    #[test]
244    fn test_local_wal_multiple_records() {
245        let mut lwal = LocalWAL::new();
246        lwal.log_insert(1, vec![0x01]);
247        lwal.log_delete(1, 42);
248        lwal.log_update(1, 42, 0, vec![0x05]);
249        lwal.log_commit(100);
250        assert_eq!(lwal.count(), 4);
251        assert!(lwal.size() > 0);
252    }
253
254    #[test]
255    fn test_local_wal_clear() {
256        let mut lwal = LocalWAL::new();
257        lwal.log_insert(1, vec![0x01]);
258        lwal.clear();
259        assert!(lwal.is_empty());
260        assert_eq!(lwal.count(), 0);
261    }
262
263    #[test]
264    fn test_local_wal_into_buffer() {
265        let mut lwal = LocalWAL::new();
266        lwal.log_insert(1, vec![0x01, 0x02]);
267        let buf = lwal.into_buffer();
268        assert!(!buf.is_empty());
269    }
270
271    #[test]
272    fn test_local_wal_buffer_content() {
273        let mut lwal = LocalWAL::new();
274        lwal.log_insert(42, vec![0xAB, 0xCD]);
275        let buf = lwal.buffer();
276        // Format: b"I" + u64 table_id(42) + u32 data_len(2) + data
277        assert_eq!(buf[0], b'I', "First byte should be 'I' for Insert");
278    }
279}