1use std::io::Write;
7
8use crate::cli::{csv_escape, wprintln};
9use crate::IdbError;
10
11pub struct BinlogOptions {
13 pub file: String,
15 pub limit: Option<usize>,
17 pub filter_type: Option<String>,
19 pub verbose: bool,
21 pub json: bool,
23 pub csv: bool,
25}
26
27pub 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
49fn write_text(
51 analysis: &crate::binlog::BinlogAnalysis,
52 opts: &BinlogOptions,
53 writer: &mut dyn Write,
54) -> Result<(), IdbError> {
55 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 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 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 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
144fn 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
171fn 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}