Skip to main content

idb/cli/
binlog.rs

1//! CLI implementation for the `inno binlog` subcommand.
2//!
3//! Parses MySQL binary log files and displays event summaries, format
4//! description info, table map details, and row-based event statistics.
5
6use std::io::Write;
7
8use crate::cli::{csv_escape, wprintln};
9use crate::IdbError;
10
11/// Options for the `inno binlog` subcommand.
12pub struct BinlogOptions {
13    /// Path to the MySQL binary log file.
14    pub file: String,
15    /// Maximum number of events to display.
16    pub limit: Option<usize>,
17    /// Filter events by type name (e.g. "TABLE_MAP", "WRITE_ROWS").
18    pub filter_type: Option<String>,
19    /// Show additional detail (column types for TABLE_MAP events).
20    pub verbose: bool,
21    /// Output in JSON format.
22    pub json: bool,
23    /// Output as CSV.
24    pub csv: bool,
25}
26
27/// Analyze a binary log file and display results.
28pub fn execute(opts: &BinlogOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
29    let file = std::fs::File::open(&opts.file)
30        .map_err(|e| IdbError::Io(format!("{}: {}", opts.file, e)))?;
31
32    let reader = std::io::BufReader::new(file);
33    let analysis = crate::binlog::analyze_binlog(reader)?;
34
35    if opts.json {
36        let json =
37            serde_json::to_string_pretty(&analysis).map_err(|e| IdbError::Parse(e.to_string()))?;
38        wprintln!(writer, "{}", json)?;
39        return Ok(());
40    }
41
42    if opts.csv {
43        return write_csv(&analysis, opts, writer);
44    }
45
46    write_text(&analysis, opts, writer)
47}
48
49/// Write text output for binlog analysis.
50fn write_text(
51    analysis: &crate::binlog::BinlogAnalysis,
52    opts: &BinlogOptions,
53    writer: &mut dyn Write,
54) -> Result<(), IdbError> {
55    // Format description header
56    wprintln!(writer, "Binary Log: {}", opts.file)?;
57    wprintln!(
58        writer,
59        "  Server Version: {}",
60        analysis.format_description.server_version
61    )?;
62    wprintln!(
63        writer,
64        "  Binlog Version: {}",
65        analysis.format_description.binlog_version
66    )?;
67    wprintln!(
68        writer,
69        "  Checksum Algorithm: {}",
70        analysis.format_description.checksum_alg
71    )?;
72    wprintln!(writer)?;
73
74    // Event type summary
75    wprintln!(
76        writer,
77        "Event Type Summary ({} total):",
78        analysis.event_count
79    )?;
80    let mut type_counts: Vec<_> = analysis.event_type_counts.iter().collect();
81    type_counts.sort_by(|a, b| b.1.cmp(a.1));
82    for (name, count) in &type_counts {
83        wprintln!(writer, "  {:<30} {:>6}", name, count)?;
84    }
85    wprintln!(writer)?;
86
87    // Table maps
88    if !analysis.table_maps.is_empty() {
89        wprintln!(writer, "Table Maps ({}):", analysis.table_maps.len())?;
90        for tm in &analysis.table_maps {
91            wprintln!(
92                writer,
93                "  table_id={} {}.{} ({} columns)",
94                tm.table_id,
95                tm.database_name,
96                tm.table_name,
97                tm.column_count
98            )?;
99            if opts.verbose {
100                wprintln!(writer, "    Column types: {:?}", &tm.column_types)?;
101            }
102        }
103        wprintln!(writer)?;
104    }
105
106    // Event listing
107    let events = filter_events(&analysis.events, opts);
108    let limit = opts.limit.unwrap_or(events.len());
109    let display_events = &events[..limit.min(events.len())];
110
111    if !display_events.is_empty() {
112        wprintln!(
113            writer,
114            "{:<12} {:<30} {:<10} {:<12}",
115            "Position",
116            "Type",
117            "Size",
118            "Timestamp"
119        )?;
120        wprintln!(writer, "{}", "-".repeat(66))?;
121        for evt in display_events {
122            wprintln!(
123                writer,
124                "{:<12} {:<30} {:<10} {:<12}",
125                evt.offset,
126                evt.event_type,
127                evt.event_length,
128                evt.timestamp
129            )?;
130        }
131    }
132
133    if events.len() > limit {
134        wprintln!(
135            writer,
136            "\n... {} more events (use --limit to show more)",
137            events.len() - limit
138        )?;
139    }
140
141    Ok(())
142}
143
144/// Write CSV output for binlog events.
145fn write_csv(
146    analysis: &crate::binlog::BinlogAnalysis,
147    opts: &BinlogOptions,
148    writer: &mut dyn Write,
149) -> Result<(), IdbError> {
150    wprintln!(writer, "position,type,size,timestamp,server_id")?;
151
152    let events = filter_events(&analysis.events, opts);
153    let limit = opts.limit.unwrap_or(events.len());
154    let display_events = &events[..limit.min(events.len())];
155
156    for evt in display_events {
157        wprintln!(
158            writer,
159            "{},{},{},{},{}",
160            evt.offset,
161            csv_escape(&evt.event_type),
162            evt.event_length,
163            evt.timestamp,
164            evt.server_id
165        )?;
166    }
167
168    Ok(())
169}
170
171/// Filter events by type name if a filter is provided.
172fn filter_events<'a>(
173    events: &'a [crate::binlog::BinlogEventSummary],
174    opts: &BinlogOptions,
175) -> Vec<&'a crate::binlog::BinlogEventSummary> {
176    match &opts.filter_type {
177        Some(filter) => {
178            let filter_upper = filter.to_uppercase();
179            events
180                .iter()
181                .filter(|e| e.event_type.to_uppercase().contains(&filter_upper))
182                .collect()
183        }
184        None => events.iter().collect(),
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use crate::binlog::{BinlogAnalysis, BinlogEventSummary, FormatDescriptionEvent};
192    use std::collections::HashMap;
193
194    fn sample_analysis() -> BinlogAnalysis {
195        let mut event_type_counts = HashMap::new();
196        event_type_counts.insert("QUERY_EVENT".to_string(), 5);
197        event_type_counts.insert("TABLE_MAP_EVENT".to_string(), 2);
198
199        BinlogAnalysis {
200            format_description: FormatDescriptionEvent {
201                binlog_version: 4,
202                server_version: "8.0.35".to_string(),
203                create_timestamp: 0,
204                header_length: 19,
205                checksum_alg: 1,
206            },
207            event_count: 7,
208            event_type_counts,
209            table_maps: Vec::new(),
210            events: vec![
211                BinlogEventSummary {
212                    offset: 4,
213                    event_type: "FORMAT_DESCRIPTION_EVENT".to_string(),
214                    type_code: 15,
215                    event_length: 119,
216                    timestamp: 1700000000,
217                    server_id: 1,
218                },
219                BinlogEventSummary {
220                    offset: 123,
221                    event_type: "QUERY_EVENT".to_string(),
222                    type_code: 2,
223                    event_length: 50,
224                    timestamp: 1700000001,
225                    server_id: 1,
226                },
227            ],
228        }
229    }
230
231    #[test]
232    fn test_write_text_output() {
233        let analysis = sample_analysis();
234        let opts = BinlogOptions {
235            file: "test-bin.000001".to_string(),
236            limit: None,
237            filter_type: None,
238            verbose: false,
239            json: false,
240            csv: false,
241        };
242
243        let mut buf = Vec::new();
244        write_text(&analysis, &opts, &mut buf).unwrap();
245        let output = String::from_utf8(buf).unwrap();
246        assert!(output.contains("Binary Log: test-bin.000001"));
247        assert!(output.contains("Server Version: 8.0.35"));
248        assert!(output.contains("7 total"));
249        assert!(output.contains("FORMAT_DESCRIPTION_EVENT"));
250    }
251
252    #[test]
253    fn test_write_csv_output() {
254        let analysis = sample_analysis();
255        let opts = BinlogOptions {
256            file: "test-bin.000001".to_string(),
257            limit: None,
258            filter_type: None,
259            verbose: false,
260            json: false,
261            csv: true,
262        };
263
264        let mut buf = Vec::new();
265        write_csv(&analysis, &opts, &mut buf).unwrap();
266        let output = String::from_utf8(buf).unwrap();
267        assert!(output.starts_with("position,type,size,timestamp,server_id"));
268        assert!(output.contains("FORMAT_DESCRIPTION_EVENT"));
269    }
270
271    #[test]
272    fn test_filter_events_by_type() {
273        let analysis = sample_analysis();
274        let opts = BinlogOptions {
275            file: "test".to_string(),
276            limit: None,
277            filter_type: Some("query".to_string()),
278            verbose: false,
279            json: false,
280            csv: false,
281        };
282
283        let filtered = filter_events(&analysis.events, &opts);
284        assert_eq!(filtered.len(), 1);
285        assert_eq!(filtered[0].event_type, "QUERY_EVENT");
286    }
287
288    #[test]
289    fn test_json_output() {
290        let analysis = sample_analysis();
291        let json = serde_json::to_string_pretty(&analysis).unwrap();
292        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
293        assert_eq!(parsed["event_count"], 7);
294    }
295}