1use std::io::Write;
9
10use crate::cli::{csv_escape, wprintln};
11use crate::innodb::undo;
12use crate::IdbError;
13
14pub struct UndoOptions {
16 pub file: String,
18 pub page: Option<u64>,
20 pub verbose: bool,
22 pub json: bool,
24 pub csv: bool,
26 pub page_size: Option<u32>,
28 pub keyring: Option<String>,
30 pub mmap: bool,
32}
33
34pub 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 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
63fn 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
162fn write_text(
164 analysis: &undo::UndoAnalysis,
165 verbose: bool,
166 writer: &mut dyn Write,
167) -> Result<(), IdbError> {
168 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 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 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 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
279fn 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 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 fn build_undo_page() -> Vec<u8> {
325 let mut page = vec![0u8; 16384];
326 let base = FIL_PAGE_DATA;
327
328 BigEndian::write_u16(&mut page[24..], 2);
330
331 BigEndian::write_u16(&mut page[base..], 2); BigEndian::write_u16(&mut page[base + 2..], 120); BigEndian::write_u16(&mut page[base + 4..], 200); let seg_base = base + 18;
338 BigEndian::write_u16(&mut page[seg_base..], 1); BigEndian::write_u16(&mut page[seg_base + 2..], 90); let log_offset = 90;
343 BigEndian::write_u64(&mut page[log_offset..], 1001); BigEndian::write_u64(&mut page[log_offset + 8..], 500); BigEndian::write_u16(&mut page[log_offset + 16..], 1); BigEndian::write_u16(&mut page[log_offset + 18..], 120); page[log_offset + 20] = 0; page[log_offset + 21] = 0; BigEndian::write_u64(&mut page[log_offset + 22..], 42); BigEndian::write_u16(&mut page[log_offset + 30..], 0); BigEndian::write_u16(&mut page[log_offset + 32..], 0); 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}