Skip to main content

idb/cli/
undo.rs

1//! CLI implementation for the `inno undo` subcommand.
2//!
3//! Analyzes undo tablespace files (`.ibu` and `.ibd`) by reading rollback
4//! segment arrays, rollback segment headers, undo segment headers, and
5//! undo log header chains to produce a comprehensive transaction history
6//! and segment state report.
7
8use std::io::Write;
9
10use crate::cli::{csv_escape, wprintln};
11use crate::innodb::undo;
12use crate::IdbError;
13
14/// Options for the `inno undo` subcommand.
15pub struct UndoOptions {
16    /// Path to the InnoDB undo tablespace file (.ibu or .ibd).
17    pub file: String,
18    /// Show a specific page only.
19    pub page: Option<u64>,
20    /// Show additional detail including undo records.
21    pub verbose: bool,
22    /// Output in JSON format.
23    pub json: bool,
24    /// Output as CSV.
25    pub csv: bool,
26    /// Override the auto-detected page size.
27    pub page_size: Option<u32>,
28    /// Path to MySQL keyring file for decrypting encrypted tablespaces.
29    pub keyring: Option<String>,
30    /// Use memory-mapped I/O for file access.
31    pub mmap: bool,
32}
33
34/// Analyze undo tablespace and display results.
35pub fn execute(opts: &UndoOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
36    let mut ts = crate::cli::open_tablespace(&opts.file, opts.page_size, opts.mmap)?;
37
38    if let Some(ref keyring_path) = opts.keyring {
39        crate::cli::setup_decryption(&mut ts, keyring_path)?;
40    }
41
42    // Single-page mode: just dump undo headers for one page
43    if let Some(page_no) = opts.page {
44        return execute_single_page(&mut ts, page_no, opts, writer);
45    }
46
47    let analysis = undo::analyze_undo_tablespace(&mut ts)?;
48
49    if opts.json {
50        let json =
51            serde_json::to_string_pretty(&analysis).map_err(|e| IdbError::Parse(e.to_string()))?;
52        wprintln!(writer, "{}", json)?;
53        return Ok(());
54    }
55
56    if opts.csv {
57        return write_csv(&analysis, writer);
58    }
59
60    write_text(&analysis, opts.verbose, writer)
61}
62
63/// Display undo headers for a single page.
64fn execute_single_page(
65    ts: &mut crate::innodb::tablespace::Tablespace,
66    page_no: u64,
67    opts: &UndoOptions,
68    writer: &mut dyn Write,
69) -> Result<(), IdbError> {
70    let page_data = ts.read_page(page_no)?;
71
72    let page_header = undo::UndoPageHeader::parse(&page_data)
73        .ok_or_else(|| IdbError::Parse(format!("Page {} is not an undo log page", page_no)))?;
74
75    let segment_header = undo::UndoSegmentHeader::parse(&page_data);
76
77    if opts.json {
78        #[derive(serde::Serialize)]
79        struct SinglePageOutput {
80            page_no: u64,
81            page_header: undo::UndoPageHeader,
82            #[serde(skip_serializing_if = "Option::is_none")]
83            segment_header: Option<undo::UndoSegmentHeader>,
84            log_headers: Vec<undo::UndoLogHeader>,
85            record_count: usize,
86        }
87
88        let log_headers = if let Some(ref seg) = segment_header {
89            if seg.last_log > 0 {
90                undo::walk_undo_log_headers(&page_data, seg.last_log)
91            } else {
92                Vec::new()
93            }
94        } else {
95            Vec::new()
96        };
97
98        let records =
99            undo::walk_undo_records(&page_data, page_header.start, page_header.free, 10000);
100
101        let output = SinglePageOutput {
102            page_no,
103            page_header,
104            segment_header,
105            log_headers,
106            record_count: records.len(),
107        };
108
109        let json =
110            serde_json::to_string_pretty(&output).map_err(|e| IdbError::Parse(e.to_string()))?;
111        wprintln!(writer, "{}", json)?;
112        return Ok(());
113    }
114
115    wprintln!(writer, "Undo Page {}", page_no)?;
116    wprintln!(writer, "  Type:          {}", page_header.page_type.name())?;
117    wprintln!(writer, "  Start offset:  {}", page_header.start)?;
118    wprintln!(writer, "  Free offset:   {}", page_header.free)?;
119
120    if let Some(ref seg) = segment_header {
121        wprintln!(writer, "  Segment state: {}", seg.state.name())?;
122        wprintln!(writer, "  Last log:      {}", seg.last_log)?;
123
124        if seg.last_log > 0 {
125            let log_headers = undo::walk_undo_log_headers(&page_data, seg.last_log);
126            wprintln!(writer)?;
127            wprintln!(writer, "  Undo Log Headers ({}):", log_headers.len())?;
128            for (i, hdr) in log_headers.iter().enumerate() {
129                wprintln!(
130                    writer,
131                    "    [{}] trx_id={} trx_no={} del_marks={} dict_trans={}",
132                    i,
133                    hdr.trx_id,
134                    hdr.trx_no,
135                    hdr.del_marks,
136                    hdr.dict_trans
137                )?;
138            }
139        }
140    }
141
142    if opts.verbose {
143        let records =
144            undo::walk_undo_records(&page_data, page_header.start, page_header.free, 10000);
145        wprintln!(writer)?;
146        wprintln!(writer, "  Undo Records ({}):", records.len())?;
147        for rec in &records {
148            wprintln!(
149                writer,
150                "    offset={} type={} info_bits={} data_len={}",
151                rec.offset,
152                rec.record_type,
153                rec.info_bits,
154                rec.data_len
155            )?;
156        }
157    }
158
159    Ok(())
160}
161
162/// Write full text output for undo analysis.
163fn write_text(
164    analysis: &undo::UndoAnalysis,
165    verbose: bool,
166    writer: &mut dyn Write,
167) -> Result<(), IdbError> {
168    // RSEG array overview
169    if !analysis.rseg_slots.is_empty() {
170        wprintln!(
171            writer,
172            "Rollback Segment Array ({} slots)",
173            analysis.rseg_slots.len()
174        )?;
175        wprintln!(
176            writer,
177            "{:<6} {:<12} {:<12} {:<12}",
178            "Slot",
179            "Page",
180            "History",
181            "Active Slots"
182        )?;
183        wprintln!(writer, "{}", "-".repeat(44))?;
184
185        for (i, rseg) in analysis.rseg_headers.iter().enumerate() {
186            wprintln!(
187                writer,
188                "{:<6} {:<12} {:<12} {:<12}",
189                i,
190                rseg.page_no,
191                rseg.history_size,
192                rseg.active_slot_count
193            )?;
194        }
195        wprintln!(writer)?;
196    }
197
198    // Segment summary
199    wprintln!(
200        writer,
201        "Undo Segments ({} total, {} active)",
202        analysis.segments.len(),
203        analysis.active_transactions
204    )?;
205    wprintln!(
206        writer,
207        "{:<8} {:<10} {:<8} {:<8} {:<8} {:<8}",
208        "Page",
209        "State",
210        "Type",
211        "Logs",
212        "Records",
213        "Free"
214    )?;
215    wprintln!(writer, "{}", "-".repeat(52))?;
216
217    for seg in &analysis.segments {
218        wprintln!(
219            writer,
220            "{:<8} {:<10} {:<8} {:<8} {:<8} {:<8}",
221            seg.page_no,
222            seg.segment_header.state.name(),
223            seg.page_header.page_type.name(),
224            seg.log_headers.len(),
225            seg.record_count,
226            seg.page_header.free
227        )?;
228    }
229
230    // Verbose: undo log header details
231    if verbose && !analysis.segments.is_empty() {
232        wprintln!(writer)?;
233        wprintln!(
234            writer,
235            "Undo Log Headers ({} total)",
236            analysis.total_transactions
237        )?;
238        wprintln!(
239            writer,
240            "{:<8} {:<16} {:<16} {:<10} {:<6} {:<6}",
241            "Page",
242            "TRX ID",
243            "TRX No",
244            "Del Marks",
245            "XID",
246            "DDL"
247        )?;
248        wprintln!(writer, "{}", "-".repeat(64))?;
249
250        for seg in &analysis.segments {
251            for hdr in &seg.log_headers {
252                wprintln!(
253                    writer,
254                    "{:<8} {:<16} {:<16} {:<10} {:<6} {:<6}",
255                    seg.page_no,
256                    hdr.trx_id,
257                    hdr.trx_no,
258                    if hdr.del_marks { "yes" } else { "no" },
259                    if hdr.xid_exists { "yes" } else { "no" },
260                    if hdr.dict_trans { "yes" } else { "no" }
261                )?;
262            }
263        }
264    }
265
266    // Summary line
267    wprintln!(writer)?;
268    wprintln!(
269        writer,
270        "Total: {} segments, {} transactions, {} active",
271        analysis.segments.len(),
272        analysis.total_transactions,
273        analysis.active_transactions
274    )?;
275
276    Ok(())
277}
278
279/// Write CSV output for undo analysis (transaction listing).
280fn write_csv(analysis: &undo::UndoAnalysis, writer: &mut dyn Write) -> Result<(), IdbError> {
281    wprintln!(
282        writer,
283        "page_no,state,type,trx_id,trx_no,del_marks,xid_exists,dict_trans,table_id"
284    )?;
285
286    for seg in &analysis.segments {
287        if seg.log_headers.is_empty() {
288            // Segment with no log headers — still report the segment
289            wprintln!(
290                writer,
291                "{},{},{},,,,,",
292                seg.page_no,
293                csv_escape(seg.segment_header.state.name()),
294                csv_escape(seg.page_header.page_type.name())
295            )?;
296        }
297        for hdr in &seg.log_headers {
298            wprintln!(
299                writer,
300                "{},{},{},{},{},{},{},{},{}",
301                seg.page_no,
302                csv_escape(seg.segment_header.state.name()),
303                csv_escape(seg.page_header.page_type.name()),
304                hdr.trx_id,
305                hdr.trx_no,
306                hdr.del_marks,
307                hdr.xid_exists,
308                hdr.dict_trans,
309                hdr.table_id
310            )?;
311        }
312    }
313
314    Ok(())
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use crate::innodb::constants::FIL_PAGE_DATA;
321    use byteorder::{BigEndian, ByteOrder};
322
323    /// Build a minimal synthetic undo page for testing.
324    fn build_undo_page() -> Vec<u8> {
325        let mut page = vec![0u8; 16384];
326        let base = FIL_PAGE_DATA;
327
328        // FIL header: page type = FIL_PAGE_UNDO_LOG (2)
329        BigEndian::write_u16(&mut page[24..], 2);
330
331        // Undo page header
332        BigEndian::write_u16(&mut page[base..], 2); // UPDATE type
333        BigEndian::write_u16(&mut page[base + 2..], 120); // start
334        BigEndian::write_u16(&mut page[base + 4..], 200); // free
335
336        // Undo segment header (at base + 18)
337        let seg_base = base + 18;
338        BigEndian::write_u16(&mut page[seg_base..], 1); // ACTIVE state
339        BigEndian::write_u16(&mut page[seg_base + 2..], 90); // last_log offset
340
341        // Undo log header at offset 90
342        let log_offset = 90;
343        BigEndian::write_u64(&mut page[log_offset..], 1001); // trx_id
344        BigEndian::write_u64(&mut page[log_offset + 8..], 500); // trx_no
345        BigEndian::write_u16(&mut page[log_offset + 16..], 1); // del_marks
346        BigEndian::write_u16(&mut page[log_offset + 18..], 120); // log_start
347        page[log_offset + 20] = 0; // xid_exists
348        page[log_offset + 21] = 0; // dict_trans
349        BigEndian::write_u64(&mut page[log_offset + 22..], 42); // table_id
350        BigEndian::write_u16(&mut page[log_offset + 30..], 0); // next_log
351        BigEndian::write_u16(&mut page[log_offset + 32..], 0); // prev_log
352
353        page
354    }
355
356    #[test]
357    fn test_execute_single_page_json() {
358        use crate::innodb::tablespace::Tablespace;
359
360        let page = build_undo_page();
361        let mut ts = Tablespace::from_bytes(page).unwrap();
362
363        let opts = UndoOptions {
364            file: "test.ibu".to_string(),
365            page: Some(0),
366            verbose: false,
367            json: true,
368            csv: false,
369            page_size: None,
370            keyring: None,
371            mmap: false,
372        };
373
374        let mut buf = Vec::new();
375        execute_single_page(&mut ts, 0, &opts, &mut buf).unwrap();
376        let output = String::from_utf8(buf).unwrap();
377        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
378        assert_eq!(parsed["page_no"], 0);
379        assert_eq!(parsed["page_header"]["page_type"], "Update");
380        assert_eq!(parsed["log_headers"][0]["trx_id"], 1001);
381    }
382
383    #[test]
384    fn test_execute_single_page_text() {
385        use crate::innodb::tablespace::Tablespace;
386
387        let page = build_undo_page();
388        let mut ts = Tablespace::from_bytes(page).unwrap();
389
390        let opts = UndoOptions {
391            file: "test.ibu".to_string(),
392            page: Some(0),
393            verbose: false,
394            json: false,
395            csv: false,
396            page_size: None,
397            keyring: None,
398            mmap: false,
399        };
400
401        let mut buf = Vec::new();
402        execute_single_page(&mut ts, 0, &opts, &mut buf).unwrap();
403        let output = String::from_utf8(buf).unwrap();
404        assert!(output.contains("Undo Page 0"));
405        assert!(output.contains("UPDATE"));
406        assert!(output.contains("ACTIVE"));
407        assert!(output.contains("trx_id=1001"));
408    }
409
410    #[test]
411    fn test_write_text_empty_analysis() {
412        let analysis = undo::UndoAnalysis {
413            rseg_slots: Vec::new(),
414            rseg_headers: Vec::new(),
415            segments: Vec::new(),
416            total_transactions: 0,
417            active_transactions: 0,
418        };
419
420        let mut buf = Vec::new();
421        write_text(&analysis, false, &mut buf).unwrap();
422        let output = String::from_utf8(buf).unwrap();
423        assert!(output.contains("0 total, 0 active"));
424    }
425
426    #[test]
427    fn test_write_csv_header() {
428        let analysis = undo::UndoAnalysis {
429            rseg_slots: Vec::new(),
430            rseg_headers: Vec::new(),
431            segments: Vec::new(),
432            total_transactions: 0,
433            active_transactions: 0,
434        };
435
436        let mut buf = Vec::new();
437        write_csv(&analysis, &mut buf).unwrap();
438        let output = String::from_utf8(buf).unwrap();
439        assert!(output.starts_with("page_no,state,type,trx_id,"));
440    }
441}