Skip to main content

idb/cli/
compat.rs

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
15/// Options for the `inno compat` subcommand.
16pub struct CompatOptions {
17    /// Path to a single InnoDB data file (.ibd).
18    pub file: Option<String>,
19    /// Path to a data directory to scan.
20    pub scan: Option<String>,
21    /// Target MySQL version (e.g., "8.4.0", "9.0.0").
22    pub target: String,
23    /// Show detailed check information.
24    pub verbose: bool,
25    /// Output in JSON format.
26    pub json: bool,
27    /// Override page size (default: auto-detect).
28    pub page_size: Option<u32>,
29    /// Path to MySQL keyring file for decrypting encrypted tablespaces.
30    pub keyring: Option<String>,
31    /// Use memory-mapped I/O.
32    pub mmap: bool,
33    /// Maximum directory recursion depth (None = default 2, Some(0) = unlimited).
34    pub depth: Option<u32>,
35}
36
37/// Execute the compat subcommand.
38pub 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    // Sort by file path for deterministic output
247    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}