1use std::io::Write;
4
5use crate::cli::{csv_escape, wprintln};
6use crate::innodb::timeline::{
7 extract_binlog_timeline, extract_redo_timeline, extract_undo_timeline, merge_timeline,
8 TimelineAction, TimelineReport,
9};
10use crate::IdbError;
11
12pub struct TimelineOptions {
14 pub redo_log: Option<String>,
15 pub undo_file: Option<String>,
16 pub binlog: Option<String>,
17 pub file: Option<String>,
18 pub datadir: Option<String>,
19 pub space_id: Option<u32>,
20 pub page: Option<u64>,
21 pub table: Option<String>,
22 pub limit: Option<usize>,
23 pub verbose: bool,
24 pub json: bool,
25 pub page_size: Option<u32>,
26 pub keyring: Option<String>,
27}
28
29pub fn execute(opts: &TimelineOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
30 if opts.redo_log.is_none() && opts.undo_file.is_none() && opts.binlog.is_none() {
31 return Err(IdbError::Argument(
32 "At least one of --redo-log, --undo-file, or --binlog is required".to_string(),
33 ));
34 }
35
36 let redo_entries = if let Some(ref path) = opts.redo_log {
38 let mut log = crate::innodb::log::LogFile::open(path)?;
39 extract_redo_timeline(&mut log)?
40 } else {
41 Vec::new()
42 };
43
44 let undo_entries = if let Some(ref path) = opts.undo_file {
45 let mut ts = crate::cli::open_tablespace(path, opts.page_size, false)?;
46 if let Some(ref keyring_path) = opts.keyring {
47 crate::cli::setup_decryption(&mut ts, keyring_path)?;
48 }
49 extract_undo_timeline(&mut ts)?
50 } else {
51 Vec::new()
52 };
53
54 let binlog_entries = if let Some(ref path) = opts.binlog {
55 if opts.file.is_some() {
56 let file = std::fs::File::open(path)
58 .map_err(|e| IdbError::Io(format!("Cannot open {}: {}", path, e)))?;
59 let reader = std::io::BufReader::new(file);
60 let mut result = crate::innodb::timeline::extract_binlog_timeline_enriched(reader)?;
61
62 if let Some(ref ibd_path) = opts.file {
64 let mut ts = crate::cli::open_tablespace(ibd_path, opts.page_size, false)?;
65 if let Some(ref keyring_path) = opts.keyring {
66 crate::cli::setup_decryption(&mut ts, keyring_path)?;
67 }
68 let _correlated = crate::innodb::timeline::correlate_binlog_pages(
69 &mut result.entries,
70 &mut ts,
71 &result.table_maps,
72 &result.row_data,
73 )?;
74 }
75 result.entries
76 } else {
77 let file = std::fs::File::open(path)
78 .map_err(|e| IdbError::Io(format!("Cannot open {}: {}", path, e)))?;
79 let reader = std::io::BufReader::new(file);
80 extract_binlog_timeline(reader)?
81 }
82 } else {
83 Vec::new()
84 };
85
86 let mut report = merge_timeline(redo_entries, undo_entries, binlog_entries);
87
88 if let Some(ref datadir) = opts.datadir {
90 if let Ok(space_map) = crate::innodb::timeline::build_space_table_map(datadir) {
91 let reverse: std::collections::HashMap<String, u32> = space_map
93 .into_iter()
94 .map(|(sid, name)| (name, sid))
95 .collect();
96 for entry in &mut report.entries {
97 if let crate::innodb::timeline::TimelineAction::Binlog {
98 database, table, ..
99 } = &entry.action
100 {
101 if let (Some(db), Some(tbl)) = (database, table) {
102 let full = format!("{}.{}", db, tbl);
103 if let Some(&sid) = reverse.get(&full) {
104 entry.space_id = Some(sid);
105 }
106 }
107 }
108 }
109 }
110 }
111
112 apply_filters(&mut report, opts);
114
115 if opts.json {
116 let json = serde_json::to_string_pretty(&report)
117 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
118 wprintln!(writer, "{}", json)?;
119 } else {
120 write_text(&report, opts, writer)?;
121 }
122
123 Ok(())
124}
125
126fn apply_filters(report: &mut TimelineReport, opts: &TimelineOptions) {
127 if let Some(sid) = opts.space_id {
128 report.entries.retain(|e| e.space_id == Some(sid));
129 }
130 if let Some(pno) = opts.page {
131 report.entries.retain(|e| e.page_no == Some(pno as u32));
132 }
133 if let Some(ref table) = opts.table {
134 let lower = table.to_lowercase();
135 report.entries.retain(|e| match &e.action {
136 TimelineAction::Binlog {
137 database, table, ..
138 } => {
139 let db = database.as_deref().unwrap_or("");
140 let tbl = table.as_deref().unwrap_or("");
141 db.to_lowercase().contains(&lower) || tbl.to_lowercase().contains(&lower)
142 }
143 _ => true, });
145 }
146 if let Some(limit) = opts.limit {
147 report.entries.truncate(limit);
148 }
149}
150
151fn write_text(
152 report: &TimelineReport,
153 opts: &TimelineOptions,
154 writer: &mut dyn Write,
155) -> Result<(), IdbError> {
156 wprintln!(writer, "Transaction Timeline")?;
157
158 let mut sources = Vec::new();
160 if let Some(ref p) = opts.redo_log {
161 sources.push(format!("redo log ({})", short_name(p)));
162 }
163 if let Some(ref p) = opts.undo_file {
164 sources.push(format!("undo ({})", short_name(p)));
165 }
166 if let Some(ref p) = opts.binlog {
167 sources.push(format!("binlog ({})", short_name(p)));
168 }
169 wprintln!(writer, " Sources: {}", sources.join(", "))?;
170 wprintln!(
171 writer,
172 " Redo entries: {} | Undo entries: {} | Binlog entries: {} | Correlated: {}",
173 report.redo_count,
174 report.undo_count,
175 report.binlog_count,
176 report.correlated_count
177 )?;
178 wprintln!(writer)?;
179
180 wprintln!(
182 writer,
183 " {:<6} {:<18} {:<8} {:<12} {}",
184 "SEQ",
185 "LSN",
186 "SOURCE",
187 "SPACE:PAGE",
188 "ACTION"
189 )?;
190
191 for entry in &report.entries {
192 let lsn_str = entry
193 .lsn
194 .map(|l| l.to_string())
195 .unwrap_or_else(|| "-".to_string());
196 let page_str = match (entry.space_id, entry.page_no) {
197 (Some(s), Some(p)) => format!("{}:{}", s, p),
198 (None, Some(p)) => format!("-:{}", p),
199 _ => "-".to_string(),
200 };
201 let action_str = format_action(&entry.action, opts.verbose);
202
203 wprintln!(
204 writer,
205 " {:<6} {:<18} {:<8} {:<12} {}",
206 entry.seq,
207 lsn_str,
208 entry.source,
209 page_str,
210 action_str
211 )?;
212 }
213
214 if !report.page_summaries.is_empty() {
216 wprintln!(writer)?;
217 wprintln!(writer, "Page Summary:")?;
218 wprintln!(
219 writer,
220 " {:<12} {:<6} {:<6} {:<8} {:<18} {}",
221 "SPACE:PAGE",
222 "REDO",
223 "UNDO",
224 "BINLOG",
225 "FIRST_LSN",
226 "LAST_LSN"
227 )?;
228 for ps in &report.page_summaries {
229 let first = ps
230 .first_lsn
231 .map(|l| l.to_string())
232 .unwrap_or_else(|| "-".to_string());
233 let last = ps
234 .last_lsn
235 .map(|l| l.to_string())
236 .unwrap_or_else(|| "-".to_string());
237 wprintln!(
238 writer,
239 " {:<12} {:<6} {:<6} {:<8} {:<18} {}",
240 format!("{}:{}", ps.space_id, ps.page_no),
241 ps.redo_entries,
242 ps.undo_entries,
243 ps.binlog_entries,
244 first,
245 last
246 )?;
247 }
248 }
249
250 Ok(())
251}
252
253pub fn write_csv(report: &TimelineReport, writer: &mut dyn Write) -> Result<(), IdbError> {
254 wprintln!(
255 writer,
256 "seq,lsn,timestamp,source,space_id,page_no,action_type,details"
257 )?;
258 for entry in &report.entries {
259 let lsn = entry.lsn.map(|l| l.to_string()).unwrap_or_default();
260 let ts = entry.timestamp.map(|t| t.to_string()).unwrap_or_default();
261 let sid = entry.space_id.map(|s| s.to_string()).unwrap_or_default();
262 let pno = entry.page_no.map(|p| p.to_string()).unwrap_or_default();
263 let (action_type, details) = csv_action(&entry.action);
264
265 wprintln!(
266 writer,
267 "{},{},{},{},{},{},{},{}",
268 entry.seq,
269 lsn,
270 ts,
271 entry.source,
272 sid,
273 pno,
274 csv_escape(&action_type),
275 csv_escape(&details)
276 )?;
277 }
278 Ok(())
279}
280
281fn format_action(action: &TimelineAction, verbose: bool) -> String {
282 match action {
283 TimelineAction::Redo {
284 mlog_type,
285 single_rec,
286 } => {
287 if verbose {
288 format!("{} (single_rec={})", mlog_type, single_rec)
289 } else {
290 mlog_type.clone()
291 }
292 }
293 TimelineAction::Undo {
294 record_type,
295 trx_id,
296 table_id,
297 ..
298 } => {
299 if verbose {
300 format!("trx={} {} table_id={}", trx_id, record_type, table_id)
301 } else {
302 format!("trx={} {}", trx_id, record_type)
303 }
304 }
305 TimelineAction::Binlog {
306 event_type,
307 database,
308 table,
309 xid,
310 pk_values,
311 } => {
312 let mut s = event_type.clone();
313 if let Some(db) = database {
314 if let Some(tbl) = table {
315 s.push_str(&format!(" {}.{}", db, tbl));
316 }
317 }
318 if let Some(x) = xid {
319 s.push_str(&format!(" (xid={})", x));
320 }
321 if let Some(pks) = pk_values {
322 s.push_str(&format!(" PK=({})", pks.join(", ")));
323 }
324 s
325 }
326 }
327}
328
329fn csv_action(action: &TimelineAction) -> (String, String) {
330 match action {
331 TimelineAction::Redo {
332 mlog_type,
333 single_rec,
334 } => (mlog_type.clone(), format!("single_rec={}", single_rec)),
335 TimelineAction::Undo {
336 record_type,
337 trx_id,
338 undo_no,
339 table_id,
340 } => (
341 record_type.clone(),
342 format!(
343 "trx_id={} undo_no={} table_id={}",
344 trx_id, undo_no, table_id
345 ),
346 ),
347 TimelineAction::Binlog {
348 event_type,
349 database,
350 table,
351 xid,
352 ..
353 } => {
354 let mut detail = String::new();
355 if let Some(db) = database {
356 detail.push_str(&format!("db={}", db));
357 }
358 if let Some(tbl) = table {
359 if !detail.is_empty() {
360 detail.push(' ');
361 }
362 detail.push_str(&format!("table={}", tbl));
363 }
364 if let Some(x) = xid {
365 if !detail.is_empty() {
366 detail.push(' ');
367 }
368 detail.push_str(&format!("xid={}", x));
369 }
370 (event_type.clone(), detail)
371 }
372 }
373}
374
375fn short_name(path: &str) -> &str {
376 std::path::Path::new(path)
377 .file_name()
378 .and_then(|s| s.to_str())
379 .unwrap_or(path)
380}