Skip to main content

idb/innodb/
undo.rs

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