Skip to main content

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}