idb/innodb/undo.rs
1//! UNDO log page parsing.
2//!
3//! UNDO log pages (page type 2 / `FIL_PAGE_UNDO_LOG`) store previous versions
4//! of modified records for MVCC and rollback. Each undo page has an
5//! [`UndoPageHeader`] at `FIL_PAGE_DATA` (byte 38) describing the undo type
6//! and free space pointers, followed by an [`UndoSegmentHeader`] with the
7//! segment state and transaction metadata.
8
9use byteorder::{BigEndian, ByteOrder};
10use serde::Serialize;
11
12use crate::innodb::constants::FIL_PAGE_DATA;
13
14/// Undo log page header offsets (relative to FIL_PAGE_DATA).
15///
16/// From trx0undo.h in MySQL source.
17const TRX_UNDO_PAGE_TYPE: usize = 0; // 2 bytes
18const TRX_UNDO_PAGE_START: usize = 2; // 2 bytes
19const TRX_UNDO_PAGE_FREE: usize = 4; // 2 bytes
20#[allow(dead_code)]
21const TRX_UNDO_PAGE_NODE: usize = 6; // 12 bytes (FLST_NODE)
22const TRX_UNDO_PAGE_HDR_SIZE: usize = 18;
23
24/// Undo segment header offsets (relative to FIL_PAGE_DATA + TRX_UNDO_PAGE_HDR_SIZE).
25const TRX_UNDO_STATE: usize = 0; // 2 bytes
26const TRX_UNDO_LAST_LOG: usize = 2; // 2 bytes
27#[allow(dead_code)]
28const TRX_UNDO_FSEG_HEADER: usize = 4; // 10 bytes (FSEG_HEADER)
29#[allow(dead_code)]
30const TRX_UNDO_PAGE_LIST: usize = 14; // 16 bytes (FLST_BASE_NODE)
31const TRX_UNDO_SEG_HDR_SIZE: usize = 30;
32
33/// Undo log header offsets (at the start of the undo log within the page).
34const TRX_UNDO_TRX_ID: usize = 0; // 8 bytes
35const TRX_UNDO_TRX_NO: usize = 8; // 8 bytes
36const TRX_UNDO_DEL_MARKS: usize = 16; // 2 bytes
37const TRX_UNDO_LOG_START: usize = 18; // 2 bytes
38const TRX_UNDO_XID_EXISTS: usize = 20; // 1 byte
39const TRX_UNDO_DICT_TRANS: usize = 21; // 1 byte
40const TRX_UNDO_TABLE_ID: usize = 22; // 8 bytes
41const TRX_UNDO_NEXT_LOG: usize = 30; // 2 bytes
42const TRX_UNDO_PREV_LOG: usize = 32; // 2 bytes
43
44/// Undo page types.
45///
46/// # Examples
47///
48/// ```
49/// use idb::innodb::undo::UndoPageType;
50///
51/// let insert = UndoPageType::from_u16(1);
52/// assert_eq!(insert, UndoPageType::Insert);
53/// assert_eq!(insert.name(), "INSERT");
54///
55/// let update = UndoPageType::from_u16(2);
56/// assert_eq!(update, UndoPageType::Update);
57/// assert_eq!(update.name(), "UPDATE");
58///
59/// let unknown = UndoPageType::from_u16(99);
60/// assert_eq!(unknown, UndoPageType::Unknown(99));
61/// assert_eq!(unknown.name(), "UNKNOWN");
62/// ```
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
64pub enum UndoPageType {
65 /// Insert undo log (INSERT operations only)
66 Insert,
67 /// Update undo log (UPDATE and DELETE operations)
68 Update,
69 /// Unknown type
70 Unknown(u16),
71}
72
73impl UndoPageType {
74 /// Convert a raw u16 value from the undo page header to an `UndoPageType`.
75 pub fn from_u16(value: u16) -> Self {
76 match value {
77 1 => UndoPageType::Insert,
78 2 => UndoPageType::Update,
79 v => UndoPageType::Unknown(v),
80 }
81 }
82
83 /// Returns the MySQL source-style name for this undo page type.
84 pub fn name(&self) -> &'static str {
85 match self {
86 UndoPageType::Insert => "INSERT",
87 UndoPageType::Update => "UPDATE",
88 UndoPageType::Unknown(_) => "UNKNOWN",
89 }
90 }
91}
92
93/// Undo segment states.
94///
95/// # Examples
96///
97/// ```
98/// use idb::innodb::undo::UndoState;
99///
100/// assert_eq!(UndoState::from_u16(1), UndoState::Active);
101/// assert_eq!(UndoState::from_u16(2), UndoState::Cached);
102/// assert_eq!(UndoState::from_u16(3), UndoState::ToFree);
103/// assert_eq!(UndoState::from_u16(4), UndoState::ToPurge);
104/// assert_eq!(UndoState::from_u16(5), UndoState::Prepared);
105///
106/// assert_eq!(UndoState::Active.name(), "ACTIVE");
107/// assert_eq!(UndoState::ToPurge.name(), "TO_PURGE");
108///
109/// let unknown = UndoState::from_u16(0);
110/// assert_eq!(unknown, UndoState::Unknown(0));
111/// assert_eq!(unknown.name(), "UNKNOWN");
112/// ```
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
114pub enum UndoState {
115 /// Active transaction is using this segment
116 Active,
117 /// Cached for reuse
118 Cached,
119 /// Insert undo segment can be freed
120 ToFree,
121 /// Update undo segment will not be freed (has delete marks)
122 ToPurge,
123 /// Prepared transaction undo
124 Prepared,
125 /// Unknown state
126 Unknown(u16),
127}
128
129impl UndoState {
130 /// Convert a raw u16 value from the undo segment header to an `UndoState`.
131 pub fn from_u16(value: u16) -> Self {
132 match value {
133 1 => UndoState::Active,
134 2 => UndoState::Cached,
135 3 => UndoState::ToFree,
136 4 => UndoState::ToPurge,
137 5 => UndoState::Prepared,
138 v => UndoState::Unknown(v),
139 }
140 }
141
142 /// Returns the MySQL source-style name for this undo state.
143 pub fn name(&self) -> &'static str {
144 match self {
145 UndoState::Active => "ACTIVE",
146 UndoState::Cached => "CACHED",
147 UndoState::ToFree => "TO_FREE",
148 UndoState::ToPurge => "TO_PURGE",
149 UndoState::Prepared => "PREPARED",
150 UndoState::Unknown(_) => "UNKNOWN",
151 }
152 }
153}
154
155/// Parsed undo log page header.
156#[derive(Debug, Clone, Serialize)]
157pub struct UndoPageHeader {
158 /// Type of undo log (INSERT or UPDATE).
159 pub page_type: UndoPageType,
160 /// Offset of the start of undo log records on this page.
161 pub start: u16,
162 /// Offset of the first free byte on this page.
163 pub free: u16,
164}
165
166/// Parsed undo segment header (only on first page of undo segment).
167#[derive(Debug, Clone, Serialize)]
168pub struct UndoSegmentHeader {
169 /// State of the undo segment.
170 pub state: UndoState,
171 /// Offset of the last undo log header on the segment.
172 pub last_log: u16,
173}
174
175impl UndoPageHeader {
176 /// Parse an undo page header from a full page buffer.
177 ///
178 /// The undo page header starts at FIL_PAGE_DATA (byte 38).
179 ///
180 /// # Examples
181 ///
182 /// ```
183 /// use idb::innodb::undo::{UndoPageHeader, UndoPageType};
184 /// use byteorder::{BigEndian, ByteOrder};
185 ///
186 /// // Build a minimal page buffer (at least 38 + 18 = 56 bytes).
187 /// let mut page = vec![0u8; 64];
188 /// let base = 38; // FIL_PAGE_DATA
189 ///
190 /// // Undo page type = UPDATE (2) at offset base+0
191 /// BigEndian::write_u16(&mut page[base..], 2);
192 /// // Start offset at base+2
193 /// BigEndian::write_u16(&mut page[base + 2..], 80);
194 /// // Free offset at base+4
195 /// BigEndian::write_u16(&mut page[base + 4..], 160);
196 ///
197 /// let hdr = UndoPageHeader::parse(&page).unwrap();
198 /// assert_eq!(hdr.page_type, UndoPageType::Update);
199 /// assert_eq!(hdr.start, 80);
200 /// assert_eq!(hdr.free, 160);
201 /// ```
202 pub fn parse(page_data: &[u8]) -> Option<Self> {
203 let base = FIL_PAGE_DATA;
204 if page_data.len() < base + TRX_UNDO_PAGE_HDR_SIZE {
205 return None;
206 }
207
208 let d = &page_data[base..];
209 Some(UndoPageHeader {
210 page_type: UndoPageType::from_u16(BigEndian::read_u16(&d[TRX_UNDO_PAGE_TYPE..])),
211 start: BigEndian::read_u16(&d[TRX_UNDO_PAGE_START..]),
212 free: BigEndian::read_u16(&d[TRX_UNDO_PAGE_FREE..]),
213 })
214 }
215}
216
217impl UndoSegmentHeader {
218 /// Parse an undo segment header from a full page buffer.
219 ///
220 /// The segment header follows the page header at FIL_PAGE_DATA + TRX_UNDO_PAGE_HDR_SIZE.
221 ///
222 /// # Examples
223 ///
224 /// ```
225 /// use idb::innodb::undo::{UndoSegmentHeader, UndoState};
226 /// use byteorder::{BigEndian, ByteOrder};
227 ///
228 /// // Need at least 38 (FIL header) + 18 (page header) + 30 (seg header) = 86 bytes.
229 /// let mut page = vec![0u8; 96];
230 /// let base = 38 + 18; // FIL_PAGE_DATA + TRX_UNDO_PAGE_HDR_SIZE
231 ///
232 /// // State = CACHED (2) at base+0
233 /// BigEndian::write_u16(&mut page[base..], 2);
234 /// // Last log offset at base+2
235 /// BigEndian::write_u16(&mut page[base + 2..], 200);
236 ///
237 /// let hdr = UndoSegmentHeader::parse(&page).unwrap();
238 /// assert_eq!(hdr.state, UndoState::Cached);
239 /// assert_eq!(hdr.last_log, 200);
240 /// ```
241 pub fn parse(page_data: &[u8]) -> Option<Self> {
242 let base = FIL_PAGE_DATA + TRX_UNDO_PAGE_HDR_SIZE;
243 if page_data.len() < base + TRX_UNDO_SEG_HDR_SIZE {
244 return None;
245 }
246
247 let d = &page_data[base..];
248 Some(UndoSegmentHeader {
249 state: UndoState::from_u16(BigEndian::read_u16(&d[TRX_UNDO_STATE..])),
250 last_log: BigEndian::read_u16(&d[TRX_UNDO_LAST_LOG..]),
251 })
252 }
253}
254
255/// Parsed undo log record header (at the start of an undo log within the page).
256#[derive(Debug, Clone, Serialize)]
257pub struct UndoLogHeader {
258 /// Transaction ID that created this undo log.
259 pub trx_id: u64,
260 /// Transaction serial number.
261 pub trx_no: u64,
262 /// Whether delete marks exist in this undo log.
263 pub del_marks: bool,
264 /// Offset of the first undo log record.
265 pub log_start: u16,
266 /// Whether XID info exists (distributed transactions).
267 pub xid_exists: bool,
268 /// Whether this is a DDL transaction.
269 pub dict_trans: bool,
270 /// Table ID (for insert undo logs).
271 pub table_id: u64,
272 /// Offset of the next undo log header (0 if last).
273 pub next_log: u16,
274 /// Offset of the previous undo log header (0 if first).
275 pub prev_log: u16,
276}
277
278impl UndoLogHeader {
279 /// Parse an undo log header from a page at the given offset.
280 ///
281 /// The `log_offset` is typically obtained from UndoSegmentHeader::last_log
282 /// or UndoPageHeader::start.
283 ///
284 /// # Examples
285 ///
286 /// ```
287 /// use idb::innodb::undo::UndoLogHeader;
288 /// use byteorder::{BigEndian, ByteOrder};
289 ///
290 /// // The undo log header is 34 bytes starting at log_offset.
291 /// let log_offset = 100;
292 /// let mut page = vec![0u8; log_offset + 34];
293 ///
294 /// // trx_id (8 bytes) at offset 0
295 /// BigEndian::write_u64(&mut page[log_offset..], 1001);
296 /// // trx_no (8 bytes) at offset 8
297 /// BigEndian::write_u64(&mut page[log_offset + 8..], 500);
298 /// // del_marks (2 bytes) at offset 16
299 /// BigEndian::write_u16(&mut page[log_offset + 16..], 1);
300 /// // log_start (2 bytes) at offset 18
301 /// BigEndian::write_u16(&mut page[log_offset + 18..], 120);
302 /// // xid_exists (1 byte) at offset 20
303 /// page[log_offset + 20] = 1;
304 /// // dict_trans (1 byte) at offset 21
305 /// page[log_offset + 21] = 0;
306 /// // table_id (8 bytes) at offset 22
307 /// BigEndian::write_u64(&mut page[log_offset + 22..], 42);
308 /// // next_log (2 bytes) at offset 30
309 /// BigEndian::write_u16(&mut page[log_offset + 30..], 0);
310 /// // prev_log (2 bytes) at offset 32
311 /// BigEndian::write_u16(&mut page[log_offset + 32..], 0);
312 ///
313 /// let hdr = UndoLogHeader::parse(&page, log_offset).unwrap();
314 /// assert_eq!(hdr.trx_id, 1001);
315 /// assert_eq!(hdr.trx_no, 500);
316 /// assert!(hdr.del_marks);
317 /// assert_eq!(hdr.log_start, 120);
318 /// assert!(hdr.xid_exists);
319 /// assert!(!hdr.dict_trans);
320 /// assert_eq!(hdr.table_id, 42);
321 /// assert_eq!(hdr.next_log, 0);
322 /// assert_eq!(hdr.prev_log, 0);
323 /// ```
324 pub fn parse(page_data: &[u8], log_offset: usize) -> Option<Self> {
325 if page_data.len() < log_offset + 34 {
326 return None;
327 }
328
329 let d = &page_data[log_offset..];
330 Some(UndoLogHeader {
331 trx_id: BigEndian::read_u64(&d[TRX_UNDO_TRX_ID..]),
332 trx_no: BigEndian::read_u64(&d[TRX_UNDO_TRX_NO..]),
333 del_marks: BigEndian::read_u16(&d[TRX_UNDO_DEL_MARKS..]) != 0,
334 log_start: BigEndian::read_u16(&d[TRX_UNDO_LOG_START..]),
335 xid_exists: d[TRX_UNDO_XID_EXISTS] != 0,
336 dict_trans: d[TRX_UNDO_DICT_TRANS] != 0,
337 table_id: BigEndian::read_u64(&d[TRX_UNDO_TABLE_ID..]),
338 next_log: BigEndian::read_u16(&d[TRX_UNDO_NEXT_LOG..]),
339 prev_log: BigEndian::read_u16(&d[TRX_UNDO_PREV_LOG..]),
340 })
341 }
342}
343
344/// Rollback segment array page header (page type FIL_PAGE_RSEG_ARRAY, MySQL 8.0+).
345///
346/// This page is the first page of an undo tablespace (.ibu) and contains
347/// an array of rollback segment page numbers.
348#[derive(Debug, Clone, Serialize)]
349pub struct RsegArrayHeader {
350 /// Number of rollback segment slots.
351 pub size: u32,
352}
353
354impl RsegArrayHeader {
355 /// Parse a rollback segment array header from a full page buffer.
356 ///
357 /// RSEG array header starts at FIL_PAGE_DATA.
358 pub fn parse(page_data: &[u8]) -> Option<Self> {
359 let base = FIL_PAGE_DATA;
360 if page_data.len() < base + 4 {
361 return None;
362 }
363
364 Some(RsegArrayHeader {
365 size: BigEndian::read_u32(&page_data[base..]),
366 })
367 }
368
369 /// Read rollback segment page numbers from the array.
370 ///
371 /// Each slot is a 4-byte page number. Returns up to `max_slots` entries.
372 pub fn read_slots(page_data: &[u8], max_slots: usize) -> Vec<u32> {
373 let base = FIL_PAGE_DATA + 4; // After the size field
374 let mut slots = Vec::new();
375
376 for i in 0..max_slots {
377 let offset = base + i * 4;
378 if offset + 4 > page_data.len() {
379 break;
380 }
381 let page_no = BigEndian::read_u32(&page_data[offset..]);
382 if page_no != 0 && page_no != crate::innodb::constants::FIL_NULL {
383 slots.push(page_no);
384 }
385 }
386
387 slots
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
396 fn test_undo_page_type() {
397 assert_eq!(UndoPageType::from_u16(1), UndoPageType::Insert);
398 assert_eq!(UndoPageType::from_u16(2), UndoPageType::Update);
399 assert_eq!(UndoPageType::from_u16(1).name(), "INSERT");
400 assert_eq!(UndoPageType::from_u16(2).name(), "UPDATE");
401 }
402
403 #[test]
404 fn test_undo_state() {
405 assert_eq!(UndoState::from_u16(1), UndoState::Active);
406 assert_eq!(UndoState::from_u16(2), UndoState::Cached);
407 assert_eq!(UndoState::from_u16(3), UndoState::ToFree);
408 assert_eq!(UndoState::from_u16(4), UndoState::ToPurge);
409 assert_eq!(UndoState::from_u16(5), UndoState::Prepared);
410 assert_eq!(UndoState::from_u16(1).name(), "ACTIVE");
411 }
412
413 #[test]
414 fn test_undo_page_header_parse() {
415 let mut page = vec![0u8; 256];
416 let base = FIL_PAGE_DATA;
417
418 // Set page type = INSERT (1)
419 BigEndian::write_u16(&mut page[base + TRX_UNDO_PAGE_TYPE..], 1);
420 // Set start offset
421 BigEndian::write_u16(&mut page[base + TRX_UNDO_PAGE_START..], 100);
422 // Set free offset
423 BigEndian::write_u16(&mut page[base + TRX_UNDO_PAGE_FREE..], 200);
424
425 let hdr = UndoPageHeader::parse(&page).unwrap();
426 assert_eq!(hdr.page_type, UndoPageType::Insert);
427 assert_eq!(hdr.start, 100);
428 assert_eq!(hdr.free, 200);
429 }
430
431 #[test]
432 fn test_undo_segment_header_parse() {
433 let mut page = vec![0u8; 256];
434 let base = FIL_PAGE_DATA + TRX_UNDO_PAGE_HDR_SIZE;
435
436 // Set state = ACTIVE (1)
437 BigEndian::write_u16(&mut page[base + TRX_UNDO_STATE..], 1);
438 // Set last log offset
439 BigEndian::write_u16(&mut page[base + TRX_UNDO_LAST_LOG..], 150);
440
441 let hdr = UndoSegmentHeader::parse(&page).unwrap();
442 assert_eq!(hdr.state, UndoState::Active);
443 assert_eq!(hdr.last_log, 150);
444 }
445}