1use std::io::Write;
2use std::path::Path;
3
4use colored::Colorize;
5use rayon::prelude::*;
6
7use crate::cli::{create_progress_bar, wprintln};
8use crate::innodb::compat::{
9 build_compat_report, check_compatibility, extract_tablespace_info, MysqlVersion,
10 ScanCompatReport, ScanFileResult, Severity,
11};
12use crate::util::fs::find_tablespace_files;
13use crate::IdbError;
14
15pub struct CompatOptions {
17 pub file: Option<String>,
19 pub scan: Option<String>,
21 pub target: String,
23 pub verbose: bool,
25 pub json: bool,
27 pub page_size: Option<u32>,
29 pub keyring: Option<String>,
31 pub mmap: bool,
33 pub depth: Option<u32>,
35}
36
37pub fn execute(opts: &CompatOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
39 if opts.file.is_some() && opts.scan.is_some() {
40 return Err(IdbError::Argument(
41 "--file and --scan are mutually exclusive".to_string(),
42 ));
43 }
44
45 if opts.file.is_none() && opts.scan.is_none() {
46 return Err(IdbError::Argument(
47 "Either --file or --scan must be provided".to_string(),
48 ));
49 }
50
51 let target = MysqlVersion::parse(&opts.target)?;
52
53 if opts.scan.is_some() {
54 execute_scan(opts, &target, writer)
55 } else {
56 execute_single(opts, &target, writer)
57 }
58}
59
60fn execute_single(
61 opts: &CompatOptions,
62 target: &MysqlVersion,
63 writer: &mut dyn Write,
64) -> Result<(), IdbError> {
65 let file = opts.file.as_ref().unwrap();
66
67 let mut ts = crate::cli::open_tablespace(file, opts.page_size, opts.mmap)?;
68
69 if let Some(ref keyring_path) = opts.keyring {
70 crate::cli::setup_decryption(&mut ts, keyring_path)?;
71 }
72
73 let info = extract_tablespace_info(&mut ts)?;
74 let report = build_compat_report(&info, target, file);
75
76 if opts.json {
77 let json = serde_json::to_string_pretty(&report)
78 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
79 wprintln!(writer, "{}", json)?;
80 } else {
81 wprintln!(writer, "Compatibility Check: {}", file)?;
82 wprintln!(writer, " Target version: MySQL {}", report.target_version)?;
83 if let Some(ref sv) = report.source_version {
84 wprintln!(writer, " Source version: MySQL {}", sv)?;
85 }
86 wprintln!(writer)?;
87
88 if report.checks.is_empty() {
89 wprintln!(writer, " No compatibility issues found.")?;
90 } else {
91 for check in &report.checks {
92 let severity_str = match check.severity {
93 Severity::Error => "ERROR".red().to_string(),
94 Severity::Warning => "WARN".yellow().to_string(),
95 Severity::Info => "INFO".blue().to_string(),
96 };
97 wprintln!(
98 writer,
99 " [{}] {}: {}",
100 severity_str,
101 check.check,
102 check.message
103 )?;
104 if opts.verbose {
105 if let Some(ref cv) = check.current_value {
106 wprintln!(writer, " Current: {}", cv)?;
107 }
108 if let Some(ref exp) = check.expected {
109 wprintln!(writer, " Expected: {}", exp)?;
110 }
111 }
112 }
113 }
114
115 wprintln!(writer)?;
116 let overall = if report.compatible {
117 "COMPATIBLE".green().to_string()
118 } else {
119 "INCOMPATIBLE".red().to_string()
120 };
121 wprintln!(
122 writer,
123 " Result: {} ({} errors, {} warnings, {} info)",
124 overall,
125 report.summary.errors,
126 report.summary.warnings,
127 report.summary.info
128 )?;
129 }
130
131 Ok(())
132}
133
134fn check_file_compat(
135 path: &Path,
136 datadir: &Path,
137 target: &MysqlVersion,
138 page_size_override: Option<u32>,
139 keyring: &Option<String>,
140 use_mmap: bool,
141) -> ScanFileResult {
142 let display = path.strip_prefix(datadir).unwrap_or(path);
143 let display_str = display.display().to_string();
144 let path_str = path.to_string_lossy();
145
146 let mut ts = match crate::cli::open_tablespace(&path_str, page_size_override, use_mmap) {
147 Ok(t) => t,
148 Err(e) => {
149 return ScanFileResult {
150 file: display_str,
151 compatible: false,
152 error: Some(e.to_string()),
153 checks: Vec::new(),
154 };
155 }
156 };
157
158 if let Some(ref kp) = keyring {
159 let _ = crate::cli::setup_decryption(&mut ts, kp);
160 }
161
162 let info = match extract_tablespace_info(&mut ts) {
163 Ok(i) => i,
164 Err(e) => {
165 return ScanFileResult {
166 file: display_str,
167 compatible: false,
168 error: Some(e.to_string()),
169 checks: Vec::new(),
170 };
171 }
172 };
173
174 let checks = check_compatibility(&info, target);
175 let compatible = !checks.iter().any(|c| c.severity == Severity::Error);
176
177 ScanFileResult {
178 file: display_str,
179 compatible,
180 error: None,
181 checks,
182 }
183}
184
185fn execute_scan(
186 opts: &CompatOptions,
187 target: &MysqlVersion,
188 writer: &mut dyn Write,
189) -> Result<(), IdbError> {
190 let scan_dir = opts.scan.as_ref().unwrap();
191 let datadir = Path::new(scan_dir);
192
193 if !datadir.is_dir() {
194 return Err(IdbError::Argument(format!(
195 "Data directory does not exist: {}",
196 scan_dir
197 )));
198 }
199
200 let ibd_files = find_tablespace_files(datadir, &["ibd"], opts.depth)?;
201
202 if ibd_files.is_empty() {
203 if opts.json {
204 let report = ScanCompatReport {
205 target_version: target.to_string(),
206 files_scanned: 0,
207 files_compatible: 0,
208 files_incompatible: 0,
209 files_error: 0,
210 results: Vec::new(),
211 };
212 let json = serde_json::to_string_pretty(&report)
213 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
214 wprintln!(writer, "{}", json)?;
215 } else {
216 wprintln!(writer, "No .ibd files found in {}", scan_dir)?;
217 }
218 return Ok(());
219 }
220
221 let pb = if !opts.json {
222 Some(create_progress_bar(ibd_files.len() as u64, "files"))
223 } else {
224 None
225 };
226
227 let page_size = opts.page_size;
228 let keyring = opts.keyring.clone();
229 let use_mmap = opts.mmap;
230
231 let mut results: Vec<ScanFileResult> = ibd_files
232 .par_iter()
233 .map(|path| {
234 let r = check_file_compat(path, datadir, target, page_size, &keyring, use_mmap);
235 if let Some(ref pb) = pb {
236 pb.inc(1);
237 }
238 r
239 })
240 .collect();
241
242 if let Some(ref pb) = pb {
243 pb.finish_and_clear();
244 }
245
246 results.sort_by(|a, b| a.file.cmp(&b.file));
248
249 let files_scanned = results.len();
250 let files_compatible = results
251 .iter()
252 .filter(|r| r.compatible && r.error.is_none())
253 .count();
254 let files_incompatible = results
255 .iter()
256 .filter(|r| !r.compatible && r.error.is_none())
257 .count();
258 let files_error = results.iter().filter(|r| r.error.is_some()).count();
259
260 if opts.json {
261 let report = ScanCompatReport {
262 target_version: target.to_string(),
263 files_scanned,
264 files_compatible,
265 files_incompatible,
266 files_error,
267 results,
268 };
269 let json = serde_json::to_string_pretty(&report)
270 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
271 wprintln!(writer, "{}", json)?;
272 } else {
273 wprintln!(
274 writer,
275 "Scanning {} for MySQL {} compatibility ({} files)...\n",
276 scan_dir,
277 target,
278 files_scanned
279 )?;
280
281 for r in &results {
282 if let Some(ref err) = r.error {
283 wprintln!(writer, " {:<50} {} {}", r.file, "ERROR".yellow(), err)?;
284 } else if r.compatible {
285 wprintln!(writer, " {:<50} {}", r.file, "OK".green())?;
286 } else {
287 wprintln!(writer, " {:<50} {}", r.file, "INCOMPATIBLE".red())?;
288 }
289
290 if opts.verbose {
291 for check in &r.checks {
292 if check.severity != Severity::Info {
293 let severity_str = match check.severity {
294 Severity::Info => "INFO".green().to_string(),
295 Severity::Warning => "WARN".yellow().to_string(),
296 Severity::Error => "ERROR".red().to_string(),
297 };
298 wprintln!(
299 writer,
300 " [{}] {}: {}",
301 severity_str,
302 check.check,
303 check.message
304 )?;
305 }
306 }
307 }
308 }
309
310 wprintln!(writer)?;
311 wprintln!(writer, "Summary:")?;
312 wprintln!(
313 writer,
314 " Files: {} ({} compatible, {} incompatible{})",
315 files_scanned,
316 files_compatible,
317 files_incompatible,
318 if files_error > 0 {
319 format!(", {} error", files_error)
320 } else {
321 String::new()
322 }
323 )?;
324 }
325
326 Ok(())
327}