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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
46pub enum UndoPageType {
47    /// Insert undo log (INSERT operations only)
48    Insert,
49    /// Update undo log (UPDATE and DELETE operations)
50    Update,
51    /// Unknown type
52    Unknown(u16),
53}
54
55impl UndoPageType {
56    /// Convert a raw u16 value from the undo page header to an `UndoPageType`.
57    pub fn from_u16(value: u16) -> Self {
58        match value {
59            1 => UndoPageType::Insert,
60            2 => UndoPageType::Update,
61            v => UndoPageType::Unknown(v),
62        }
63    }
64
65    /// Returns the MySQL source-style name for this undo page type.
66    pub fn name(&self) -> &'static str {
67        match self {
68            UndoPageType::Insert => "INSERT",
69            UndoPageType::Update => "UPDATE",
70            UndoPageType::Unknown(_) => "UNKNOWN",
71        }
72    }
73}
74
75/// Undo segment states.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
77pub enum UndoState {
78    /// Active transaction is using this segment
79    Active,
80    /// Cached for reuse
81    Cached,
82    /// Insert undo segment can be freed
83    ToFree,
84    /// Update undo segment will not be freed (has delete marks)
85    ToPurge,
86    /// Prepared transaction undo
87    Prepared,
88    /// Unknown state
89    Unknown(u16),
90}
91
92impl UndoState {
93    /// Convert a raw u16 value from the undo segment header to an `UndoState`.
94    pub fn from_u16(value: u16) -> Self {
95        match value {
96            1 => UndoState::Active,
97            2 => UndoState::Cached,
98            3 => UndoState::ToFree,
99            4 => UndoState::ToPurge,
100            5 => UndoState::Prepared,
101            v => UndoState::Unknown(v),
102        }
103    }
104
105    /// Returns the MySQL source-style name for this undo state.
106    pub fn name(&self) -> &'static str {
107        match self {
108            UndoState::Active => "ACTIVE",
109            UndoState::Cached => "CACHED",
110            UndoState::ToFree => "TO_FREE",
111            UndoState::ToPurge => "TO_PURGE",
112            UndoState::Prepared => "PREPARED",
113            UndoState::Unknown(_) => "UNKNOWN",
114        }
115    }
116}
117
118/// Parsed undo log page header.
119#[derive(Debug, Clone, Serialize)]
120pub struct UndoPageHeader {
121    /// Type of undo log (INSERT or UPDATE).
122    pub page_type: UndoPageType,
123    /// Offset of the start of undo log records on this page.
124    pub start: u16,
125    /// Offset of the first free byte on this page.
126    pub free: u16,
127}
128
129/// Parsed undo segment header (only on first page of undo segment).
130#[derive(Debug, Clone, Serialize)]
131pub struct UndoSegmentHeader {
132    /// State of the undo segment.
133    pub state: UndoState,
134    /// Offset of the last undo log header on the segment.
135    pub last_log: u16,
136}
137
138impl UndoPageHeader {
139    /// Parse an undo page header from a full page buffer.
140    ///
141    /// The undo page header starts at FIL_PAGE_DATA (byte 38).
142    pub fn parse(page_data: &[u8]) -> Option<Self> {
143        let base = FIL_PAGE_DATA;
144        if page_data.len() < base + TRX_UNDO_PAGE_HDR_SIZE {
145            return None;
146        }
147
148        let d = &page_data[base..];
149        Some(UndoPageHeader {
150            page_type: UndoPageType::from_u16(BigEndian::read_u16(&d[TRX_UNDO_PAGE_TYPE..])),
151            start: BigEndian::read_u16(&d[TRX_UNDO_PAGE_START..]),
152            free: BigEndian::read_u16(&d[TRX_UNDO_PAGE_FREE..]),
153        })
154    }
155}
156
157impl UndoSegmentHeader {
158    /// Parse an undo segment header from a full page buffer.
159    ///
160    /// The segment header follows the page header at FIL_PAGE_DATA + TRX_UNDO_PAGE_HDR_SIZE.
161    pub fn parse(page_data: &[u8]) -> Option<Self> {
162        let base = FIL_PAGE_DATA + TRX_UNDO_PAGE_HDR_SIZE;
163        if page_data.len() < base + TRX_UNDO_SEG_HDR_SIZE {
164            return None;
165        }
166
167        let d = &page_data[base..];
168        Some(UndoSegmentHeader {
169            state: UndoState::from_u16(BigEndian::read_u16(&d[TRX_UNDO_STATE..])),
170            last_log: BigEndian::read_u16(&d[TRX_UNDO_LAST_LOG..]),
171        })
172    }
173}
174
175/// Parsed undo log record header (at the start of an undo log within the page).
176#[derive(Debug, Clone, Serialize)]
177pub struct UndoLogHeader {
178    /// Transaction ID that created this undo log.
179    pub trx_id: u64,
180    /// Transaction serial number.
181    pub trx_no: u64,
182    /// Whether delete marks exist in this undo log.
183    pub del_marks: bool,
184    /// Offset of the first undo log record.
185    pub log_start: u16,
186    /// Whether XID info exists (distributed transactions).
187    pub xid_exists: bool,
188    /// Whether this is a DDL transaction.
189    pub dict_trans: bool,
190    /// Table ID (for insert undo logs).
191    pub table_id: u64,
192    /// Offset of the next undo log header (0 if last).
193    pub next_log: u16,
194    /// Offset of the previous undo log header (0 if first).
195    pub prev_log: u16,
196}
197
198impl UndoLogHeader {
199    /// Parse an undo log header from a page at the given offset.
200    ///
201    /// The `log_offset` is typically obtained from UndoSegmentHeader::last_log
202    /// or UndoPageHeader::start.
203    pub fn parse(page_data: &[u8], log_offset: usize) -> Option<Self> {
204        if page_data.len() < log_offset + 34 {
205            return None;
206        }
207
208        let d = &page_data[log_offset..];
209        Some(UndoLogHeader {
210            trx_id: BigEndian::read_u64(&d[TRX_UNDO_TRX_ID..]),
211            trx_no: BigEndian::read_u64(&d[TRX_UNDO_TRX_NO..]),
212            del_marks: BigEndian::read_u16(&d[TRX_UNDO_DEL_MARKS..]) != 0,
213            log_start: BigEndian::read_u16(&d[TRX_UNDO_LOG_START..]),
214            xid_exists: d[TRX_UNDO_XID_EXISTS] != 0,
215            dict_trans: d[TRX_UNDO_DICT_TRANS] != 0,
216            table_id: BigEndian::read_u64(&d[TRX_UNDO_TABLE_ID..]),
217            next_log: BigEndian::read_u16(&d[TRX_UNDO_NEXT_LOG..]),
218            prev_log: BigEndian::read_u16(&d[TRX_UNDO_PREV_LOG..]),
219        })
220    }
221}
222
223/// Rollback segment array page header (page type FIL_PAGE_RSEG_ARRAY, MySQL 8.0+).
224///
225/// This page is the first page of an undo tablespace (.ibu) and contains
226/// an array of rollback segment page numbers.
227#[derive(Debug, Clone, Serialize)]
228pub struct RsegArrayHeader {
229    /// Number of rollback segment slots.
230    pub size: u32,
231}
232
233impl RsegArrayHeader {
234    /// Parse a rollback segment array header from a full page buffer.
235    ///
236    /// RSEG array header starts at FIL_PAGE_DATA.
237    pub fn parse(page_data: &[u8]) -> Option<Self> {
238        let base = FIL_PAGE_DATA;
239        if page_data.len() < base + 4 {
240            return None;
241        }
242
243        Some(RsegArrayHeader {
244            size: BigEndian::read_u32(&page_data[base..]),
245        })
246    }
247
248    /// Read rollback segment page numbers from the array.
249    ///
250    /// Each slot is a 4-byte page number. Returns up to `max_slots` entries.
251    pub fn read_slots(page_data: &[u8], max_slots: usize) -> Vec<u32> {
252        let base = FIL_PAGE_DATA + 4; // After the size field
253        let mut slots = Vec::new();
254
255        for i in 0..max_slots {
256            let offset = base + i * 4;
257            if offset + 4 > page_data.len() {
258                break;
259            }
260            let page_no = BigEndian::read_u32(&page_data[offset..]);
261            if page_no != 0 && page_no != crate::innodb::constants::FIL_NULL {
262                slots.push(page_no);
263            }
264        }
265
266        slots
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn test_undo_page_type() {
276        assert_eq!(UndoPageType::from_u16(1), UndoPageType::Insert);
277        assert_eq!(UndoPageType::from_u16(2), UndoPageType::Update);
278        assert_eq!(UndoPageType::from_u16(1).name(), "INSERT");
279        assert_eq!(UndoPageType::from_u16(2).name(), "UPDATE");
280    }
281
282    #[test]
283    fn test_undo_state() {
284        assert_eq!(UndoState::from_u16(1), UndoState::Active);
285        assert_eq!(UndoState::from_u16(2), UndoState::Cached);
286        assert_eq!(UndoState::from_u16(3), UndoState::ToFree);
287        assert_eq!(UndoState::from_u16(4), UndoState::ToPurge);
288        assert_eq!(UndoState::from_u16(5), UndoState::Prepared);
289        assert_eq!(UndoState::from_u16(1).name(), "ACTIVE");
290    }
291
292    #[test]
293    fn test_undo_page_header_parse() {
294        let mut page = vec![0u8; 256];
295        let base = FIL_PAGE_DATA;
296
297        // Set page type = INSERT (1)
298        BigEndian::write_u16(&mut page[base + TRX_UNDO_PAGE_TYPE..], 1);
299        // Set start offset
300        BigEndian::write_u16(&mut page[base + TRX_UNDO_PAGE_START..], 100);
301        // Set free offset
302        BigEndian::write_u16(&mut page[base + TRX_UNDO_PAGE_FREE..], 200);
303
304        let hdr = UndoPageHeader::parse(&page).unwrap();
305        assert_eq!(hdr.page_type, UndoPageType::Insert);
306        assert_eq!(hdr.start, 100);
307        assert_eq!(hdr.free, 200);
308    }
309
310    #[test]
311    fn test_undo_segment_header_parse() {
312        let mut page = vec![0u8; 256];
313        let base = FIL_PAGE_DATA + TRX_UNDO_PAGE_HDR_SIZE;
314
315        // Set state = ACTIVE (1)
316        BigEndian::write_u16(&mut page[base + TRX_UNDO_STATE..], 1);
317        // Set last log offset
318        BigEndian::write_u16(&mut page[base + TRX_UNDO_LAST_LOG..], 150);
319
320        let hdr = UndoSegmentHeader::parse(&page).unwrap();
321        assert_eq!(hdr.state, UndoState::Active);
322        assert_eq!(hdr.last_log, 150);
323    }
324}