Skip to main content

idb/innodb/
compat.rs

1//! MySQL version compatibility checking for InnoDB tablespaces.
2//!
3//! Analyzes an InnoDB tablespace file and checks whether it is compatible
4//! with a target MySQL version. Reports warnings and errors for features
5//! that are deprecated, removed, or unsupported in the target version.
6//!
7//! # Usage
8//!
9//! ```rust,ignore
10//! use idb::innodb::tablespace::Tablespace;
11//! use idb::innodb::compat::{extract_tablespace_info, build_compat_report, MysqlVersion};
12//!
13//! let mut ts = Tablespace::open("table.ibd").unwrap();
14//! let info = extract_tablespace_info(&mut ts).unwrap();
15//! let target = MysqlVersion::parse("8.4.0").unwrap();
16//! let report = build_compat_report(&info, &target, "table.ibd");
17//! println!("Compatible: {}", report.compatible);
18//! ```
19
20use byteorder::{BigEndian, ByteOrder};
21use serde::Serialize;
22
23use crate::innodb::constants::*;
24use crate::innodb::page::FilHeader;
25use crate::innodb::page_types::PageType;
26use crate::innodb::vendor::{InnoDbVendor, VendorInfo};
27use crate::IdbError;
28
29/// A parsed MySQL version (major.minor.patch).
30///
31/// # Examples
32///
33/// ```
34/// use idb::innodb::compat::MysqlVersion;
35///
36/// let v = MysqlVersion::parse("8.4.0").unwrap();
37/// assert_eq!(v.major, 8);
38/// assert_eq!(v.minor, 4);
39/// assert_eq!(v.patch, 0);
40/// assert_eq!(v.to_string(), "8.4.0");
41/// assert_eq!(v.to_id(), 80400);
42///
43/// let v2 = MysqlVersion::from_id(90001);
44/// assert_eq!(v2.major, 9);
45/// assert_eq!(v2.minor, 0);
46/// assert_eq!(v2.patch, 1);
47/// ```
48#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
49pub struct MysqlVersion {
50    pub major: u32,
51    pub minor: u32,
52    pub patch: u32,
53}
54
55impl MysqlVersion {
56    /// Parse from "8.0.32" format.
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use idb::innodb::compat::MysqlVersion;
62    ///
63    /// let v = MysqlVersion::parse("5.7.44").unwrap();
64    /// assert_eq!(v.major, 5);
65    /// assert_eq!(v.minor, 7);
66    /// assert_eq!(v.patch, 44);
67    ///
68    /// assert!(MysqlVersion::parse("8.0").is_err());
69    /// assert!(MysqlVersion::parse("abc").is_err());
70    /// ```
71    pub fn parse(s: &str) -> Result<Self, IdbError> {
72        let parts: Vec<&str> = s.split('.').collect();
73        if parts.len() != 3 {
74            return Err(IdbError::Argument(format!(
75                "Invalid MySQL version '{}': expected format X.Y.Z",
76                s
77            )));
78        }
79        let major = parts[0]
80            .parse::<u32>()
81            .map_err(|_| IdbError::Argument(format!("Invalid major version in '{}'", s)))?;
82        let minor = parts[1]
83            .parse::<u32>()
84            .map_err(|_| IdbError::Argument(format!("Invalid minor version in '{}'", s)))?;
85        let patch = parts[2]
86            .parse::<u32>()
87            .map_err(|_| IdbError::Argument(format!("Invalid patch version in '{}'", s)))?;
88        Ok(MysqlVersion {
89            major,
90            minor,
91            patch,
92        })
93    }
94
95    /// Create from MySQL version_id (e.g., 80032 -> 8.0.32).
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use idb::innodb::compat::MysqlVersion;
101    ///
102    /// let v = MysqlVersion::from_id(80032);
103    /// assert_eq!(v.to_string(), "8.0.32");
104    /// ```
105    pub fn from_id(version_id: u64) -> Self {
106        MysqlVersion {
107            major: (version_id / 10000) as u32,
108            minor: ((version_id % 10000) / 100) as u32,
109            patch: (version_id % 100) as u32,
110        }
111    }
112
113    /// Convert to MySQL version_id format.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use idb::innodb::compat::MysqlVersion;
119    ///
120    /// let v = MysqlVersion::parse("8.0.32").unwrap();
121    /// assert_eq!(v.to_id(), 80032);
122    /// ```
123    pub fn to_id(&self) -> u64 {
124        (self.major as u64) * 10000 + (self.minor as u64) * 100 + self.patch as u64
125    }
126
127    /// Check if this version is >= another.
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// use idb::innodb::compat::MysqlVersion;
133    ///
134    /// let v8 = MysqlVersion::parse("8.4.0").unwrap();
135    /// let v9 = MysqlVersion::parse("9.0.0").unwrap();
136    /// assert!(v9.is_at_least(&v8));
137    /// assert!(!v8.is_at_least(&v9));
138    /// assert!(v8.is_at_least(&v8));
139    /// ```
140    pub fn is_at_least(&self, other: &MysqlVersion) -> bool {
141        (self.major, self.minor, self.patch) >= (other.major, other.minor, other.patch)
142    }
143}
144
145impl std::fmt::Display for MysqlVersion {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
148    }
149}
150
151/// Severity of a compatibility finding.
152///
153/// # Examples
154///
155/// ```
156/// use idb::innodb::compat::Severity;
157///
158/// assert_eq!(format!("{}", Severity::Error), "error");
159/// assert_eq!(format!("{}", Severity::Warning), "warning");
160/// assert_eq!(format!("{}", Severity::Info), "info");
161/// ```
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
163pub enum Severity {
164    /// Informational: no action required.
165    Info,
166    /// Warning: feature is deprecated or may cause issues.
167    Warning,
168    /// Error: tablespace cannot be used with the target version.
169    Error,
170}
171
172impl std::fmt::Display for Severity {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        match self {
175            Severity::Info => write!(f, "info"),
176            Severity::Warning => write!(f, "warning"),
177            Severity::Error => write!(f, "error"),
178        }
179    }
180}
181
182/// A single compatibility check result.
183#[derive(Debug, Clone, Serialize)]
184pub struct CompatCheck {
185    /// Name of the check (e.g., "page_size", "row_format", "sdi").
186    pub check: String,
187    /// Human-readable description of the finding.
188    pub message: String,
189    /// Severity level of the finding.
190    pub severity: Severity,
191    /// Current value observed in the tablespace, if applicable.
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub current_value: Option<String>,
194    /// Expected or recommended value, if applicable.
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub expected: Option<String>,
197}
198
199/// Information extracted from a tablespace for compatibility analysis.
200#[derive(Debug, Clone, Serialize)]
201pub struct TablespaceInfo {
202    /// Detected page size in bytes.
203    pub page_size: u32,
204    /// Raw FSP flags from page 0.
205    pub fsp_flags: u32,
206    /// Space ID from the FSP header.
207    pub space_id: u32,
208    /// Row format name (e.g., "DYNAMIC", "COMPRESSED"), if available from SDI.
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub row_format: Option<String>,
211    /// Whether the tablespace contains SDI pages.
212    pub has_sdi: bool,
213    /// Whether the tablespace is encrypted.
214    pub is_encrypted: bool,
215    /// Detected vendor information.
216    pub vendor: VendorInfo,
217    /// MySQL version ID from SDI metadata, if available.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub mysql_version_id: Option<u64>,
220    /// Whether the tablespace contains compressed pages (FIL_PAGE_COMPRESSED).
221    pub has_compressed_pages: bool,
222    /// Whether the tablespace uses instant ADD COLUMN (detected from SDI).
223    pub has_instant_columns: bool,
224}
225
226/// Compatibility report for a tablespace.
227#[derive(Debug, Clone, Serialize)]
228pub struct CompatReport {
229    /// Path to the analyzed file.
230    pub file: String,
231    /// Target MySQL version string.
232    pub target_version: String,
233    /// Source MySQL version string (from SDI metadata), if available.
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub source_version: Option<String>,
236    /// Whether the tablespace is compatible with the target version (no errors).
237    pub compatible: bool,
238    /// Individual check results.
239    pub checks: Vec<CompatCheck>,
240    /// Summary counts.
241    pub summary: CompatSummary,
242}
243
244/// Summary counts for a compatibility report.
245#[derive(Debug, Clone, Serialize)]
246pub struct CompatSummary {
247    /// Total number of checks performed.
248    pub total_checks: usize,
249    /// Number of error-level findings.
250    pub errors: usize,
251    /// Number of warning-level findings.
252    pub warnings: usize,
253    /// Number of info-level findings.
254    pub info: usize,
255}
256
257/// Extract tablespace metadata for compatibility analysis.
258///
259/// Reads page 0 to get FSP flags, space ID, and vendor info.
260/// Optionally extracts SDI metadata if available.
261pub fn extract_tablespace_info(
262    ts: &mut crate::innodb::tablespace::Tablespace,
263) -> Result<TablespaceInfo, IdbError> {
264    let page_size = ts.page_size();
265    let page0 = ts.read_page(0)?;
266    let vendor = ts.vendor_info().clone();
267    let fsp_flags = if page0.len() >= (FIL_PAGE_DATA + FSP_SPACE_FLAGS + 4) {
268        BigEndian::read_u32(&page0[FIL_PAGE_DATA + FSP_SPACE_FLAGS..])
269    } else {
270        0
271    };
272    let space_id = ts
273        .fsp_header()
274        .map(|h| h.space_id)
275        .unwrap_or_else(|| FilHeader::parse(&page0).map(|h| h.space_id).unwrap_or(0));
276    let is_encrypted = ts.encryption_info().is_some();
277
278    // Check for SDI pages
279    let sdi_pages = crate::innodb::sdi::find_sdi_pages(ts).unwrap_or_default();
280    let has_sdi = !sdi_pages.is_empty();
281
282    // Try to extract SDI for version info and row format
283    let mut mysql_version_id = None;
284    let mut row_format = None;
285    let mut has_instant_columns = false;
286
287    if has_sdi {
288        if let Ok(records) = crate::innodb::sdi::extract_sdi_from_pages(ts, &sdi_pages) {
289            for rec in &records {
290                if rec.sdi_type == 1 {
291                    if let Ok(envelope) =
292                        serde_json::from_str::<crate::innodb::schema::SdiEnvelope>(&rec.data)
293                    {
294                        mysql_version_id = Some(envelope.mysqld_version_id);
295                        let rf_code = envelope.dd_object.row_format;
296                        row_format =
297                            Some(crate::innodb::schema::row_format_name(rf_code).to_string());
298                        has_instant_columns = crate::innodb::schema::has_instant_columns(
299                            envelope.dd_object.se_private_data.as_deref().unwrap_or(""),
300                        );
301                    }
302                }
303            }
304        }
305    }
306
307    // Check for compressed pages (FIL_PAGE_COMPRESSED type)
308    let has_compressed_pages = {
309        let page_count = ts.page_count();
310        let mut found = false;
311        // Check first 10 pages (or all if fewer) for compression indicator
312        let check_count = page_count.min(10);
313        for i in 0..check_count {
314            if let Ok(page) = ts.read_page(i) {
315                if let Some(hdr) = FilHeader::parse(&page) {
316                    if hdr.page_type == PageType::Compressed {
317                        found = true;
318                        break;
319                    }
320                }
321            }
322        }
323        found
324    };
325
326    Ok(TablespaceInfo {
327        page_size,
328        fsp_flags,
329        space_id,
330        row_format,
331        has_sdi,
332        is_encrypted,
333        vendor,
334        mysql_version_id,
335        has_compressed_pages,
336        has_instant_columns,
337    })
338}
339
340/// Run all compatibility checks against a target MySQL version.
341///
342/// Returns a list of findings with severity levels. Error-level findings
343/// indicate the tablespace cannot be used with the target version.
344pub fn check_compatibility(info: &TablespaceInfo, target: &MysqlVersion) -> Vec<CompatCheck> {
345    let mut checks = Vec::new();
346
347    check_page_size(info, target, &mut checks);
348    check_row_format(info, target, &mut checks);
349    check_sdi_presence(info, target, &mut checks);
350    check_encryption(info, target, &mut checks);
351    check_vendor_compatibility(info, target, &mut checks);
352    check_compression(info, target, &mut checks);
353
354    checks
355}
356
357/// Build a full compatibility report.
358///
359/// Runs all checks and produces a structured report with summary counts
360/// and an overall compatible/incompatible verdict.
361pub fn build_compat_report(
362    info: &TablespaceInfo,
363    target: &MysqlVersion,
364    file: &str,
365) -> CompatReport {
366    let checks = check_compatibility(info, target);
367
368    let errors = checks
369        .iter()
370        .filter(|c| c.severity == Severity::Error)
371        .count();
372    let warnings = checks
373        .iter()
374        .filter(|c| c.severity == Severity::Warning)
375        .count();
376    let info_count = checks
377        .iter()
378        .filter(|c| c.severity == Severity::Info)
379        .count();
380
381    let source_version = info.mysql_version_id.map(|id| {
382        let v = MysqlVersion::from_id(id);
383        v.to_string()
384    });
385
386    CompatReport {
387        file: file.to_string(),
388        target_version: target.to_string(),
389        source_version,
390        compatible: errors == 0,
391        checks,
392        summary: CompatSummary {
393            total_checks: errors + warnings + info_count,
394            errors,
395            warnings,
396            info: info_count,
397        },
398    }
399}
400
401// --- Private check functions ---
402
403fn check_page_size(info: &TablespaceInfo, target: &MysqlVersion, checks: &mut Vec<CompatCheck>) {
404    // 4K/8K/32K/64K page sizes added in MySQL 5.7.6
405    let non_default = info.page_size != SIZE_PAGE_DEFAULT;
406    if non_default
407        && !target.is_at_least(&MysqlVersion {
408            major: 5,
409            minor: 7,
410            patch: 6,
411        })
412    {
413        checks.push(CompatCheck {
414            check: "page_size".to_string(),
415            message: format!(
416                "Non-default page size {} requires MySQL 5.7.6+",
417                info.page_size
418            ),
419            severity: Severity::Error,
420            current_value: Some(info.page_size.to_string()),
421            expected: Some("16384".to_string()),
422        });
423    } else if non_default {
424        checks.push(CompatCheck {
425            check: "page_size".to_string(),
426            message: format!("Non-default page size {} is supported", info.page_size),
427            severity: Severity::Info,
428            current_value: Some(info.page_size.to_string()),
429            expected: None,
430        });
431    }
432}
433
434fn check_row_format(info: &TablespaceInfo, target: &MysqlVersion, checks: &mut Vec<CompatCheck>) {
435    if let Some(ref rf) = info.row_format {
436        let rf_upper = rf.to_uppercase();
437        // COMPRESSED deprecated in 8.4+
438        if rf_upper == "COMPRESSED"
439            && target.is_at_least(&MysqlVersion {
440                major: 8,
441                minor: 4,
442                patch: 0,
443            })
444        {
445            checks.push(CompatCheck {
446                check: "row_format".to_string(),
447                message: "ROW_FORMAT=COMPRESSED is deprecated in MySQL 8.4+".to_string(),
448                severity: Severity::Warning,
449                current_value: Some(rf.clone()),
450                expected: Some("DYNAMIC".to_string()),
451            });
452        }
453        // REDUNDANT deprecated in 9.0+
454        if rf_upper == "REDUNDANT"
455            && target.is_at_least(&MysqlVersion {
456                major: 9,
457                minor: 0,
458                patch: 0,
459            })
460        {
461            checks.push(CompatCheck {
462                check: "row_format".to_string(),
463                message: "ROW_FORMAT=REDUNDANT is deprecated in MySQL 9.0+".to_string(),
464                severity: Severity::Warning,
465                current_value: Some(rf.clone()),
466                expected: Some("DYNAMIC".to_string()),
467            });
468        }
469    }
470}
471
472fn check_sdi_presence(info: &TablespaceInfo, target: &MysqlVersion, checks: &mut Vec<CompatCheck>) {
473    // SDI required for MySQL 8.0+
474    if target.is_at_least(&MysqlVersion {
475        major: 8,
476        minor: 0,
477        patch: 0,
478    }) && !info.has_sdi
479    {
480        checks.push(CompatCheck {
481            check: "sdi".to_string(),
482            message: "Tablespace lacks SDI metadata required by MySQL 8.0+".to_string(),
483            severity: Severity::Error,
484            current_value: Some("absent".to_string()),
485            expected: Some("present".to_string()),
486        });
487    } else if info.has_sdi
488        && !target.is_at_least(&MysqlVersion {
489            major: 8,
490            minor: 0,
491            patch: 0,
492        })
493    {
494        checks.push(CompatCheck {
495            check: "sdi".to_string(),
496            message: "Tablespace has SDI metadata not recognized by MySQL < 8.0".to_string(),
497            severity: Severity::Warning,
498            current_value: Some("present".to_string()),
499            expected: Some("absent".to_string()),
500        });
501    }
502}
503
504fn check_encryption(info: &TablespaceInfo, target: &MysqlVersion, checks: &mut Vec<CompatCheck>) {
505    // Tablespace-level encryption added in MySQL 5.7.11
506    if info.is_encrypted
507        && !target.is_at_least(&MysqlVersion {
508            major: 5,
509            minor: 7,
510            patch: 11,
511        })
512    {
513        checks.push(CompatCheck {
514            check: "encryption".to_string(),
515            message: "Tablespace encryption requires MySQL 5.7.11+".to_string(),
516            severity: Severity::Error,
517            current_value: Some("encrypted".to_string()),
518            expected: Some("unencrypted".to_string()),
519        });
520    }
521}
522
523fn check_vendor_compatibility(
524    info: &TablespaceInfo,
525    target: &MysqlVersion,
526    checks: &mut Vec<CompatCheck>,
527) {
528    // MariaDB -> MySQL = error (divergent formats)
529    if info.vendor.vendor == InnoDbVendor::MariaDB {
530        checks.push(CompatCheck {
531            check: "vendor".to_string(),
532            message: "MariaDB tablespace is not compatible with MySQL".to_string(),
533            severity: Severity::Error,
534            current_value: Some(info.vendor.to_string()),
535            expected: Some("MySQL".to_string()),
536        });
537    }
538    // Percona -> MySQL is fine (binary compatible)
539    if info.vendor.vendor == InnoDbVendor::Percona {
540        checks.push(CompatCheck {
541            check: "vendor".to_string(),
542            message: "Percona XtraDB tablespace is binary-compatible with MySQL".to_string(),
543            severity: Severity::Info,
544            current_value: Some(info.vendor.to_string()),
545            expected: None,
546        });
547    }
548    // Suppress the unused variable warning
549    let _ = target;
550}
551
552fn check_compression(info: &TablespaceInfo, _target: &MysqlVersion, checks: &mut Vec<CompatCheck>) {
553    if info.has_compressed_pages {
554        checks.push(CompatCheck {
555            check: "compression".to_string(),
556            message: "Tablespace uses page compression".to_string(),
557            severity: Severity::Info,
558            current_value: Some("compressed".to_string()),
559            expected: None,
560        });
561    }
562}
563
564/// Per-file result for directory scan mode.
565#[derive(Debug, Clone, Serialize)]
566pub struct ScanFileResult {
567    /// Relative path within the data directory.
568    pub file: String,
569    /// Whether this file is compatible with the target version.
570    pub compatible: bool,
571    /// Error message if the file could not be analyzed.
572    #[serde(skip_serializing_if = "Option::is_none")]
573    pub error: Option<String>,
574    /// Individual check results (empty if error occurred).
575    #[serde(skip_serializing_if = "Vec::is_empty")]
576    pub checks: Vec<CompatCheck>,
577}
578
579/// Directory scan compatibility report.
580#[derive(Debug, Clone, Serialize)]
581pub struct ScanCompatReport {
582    /// Target MySQL version.
583    pub target_version: String,
584    /// Number of files scanned.
585    pub files_scanned: usize,
586    /// Number of compatible files.
587    pub files_compatible: usize,
588    /// Number of incompatible files.
589    pub files_incompatible: usize,
590    /// Number of files with errors.
591    pub files_error: usize,
592    /// Per-file results.
593    pub results: Vec<ScanFileResult>,
594}
595
596#[cfg(test)]
597mod tests {
598    use super::*;
599    use crate::innodb::vendor::MariaDbFormat;
600
601    #[test]
602    fn test_version_parse_valid() {
603        let v = MysqlVersion::parse("8.0.32").unwrap();
604        assert_eq!(v.major, 8);
605        assert_eq!(v.minor, 0);
606        assert_eq!(v.patch, 32);
607    }
608
609    #[test]
610    fn test_version_parse_invalid_format() {
611        assert!(MysqlVersion::parse("8.0").is_err());
612        assert!(MysqlVersion::parse("8").is_err());
613        assert!(MysqlVersion::parse("").is_err());
614        assert!(MysqlVersion::parse("8.0.x").is_err());
615    }
616
617    #[test]
618    fn test_version_from_id() {
619        let v = MysqlVersion::from_id(80032);
620        assert_eq!(v.major, 8);
621        assert_eq!(v.minor, 0);
622        assert_eq!(v.patch, 32);
623
624        let v = MysqlVersion::from_id(90001);
625        assert_eq!(v.major, 9);
626        assert_eq!(v.minor, 0);
627        assert_eq!(v.patch, 1);
628    }
629
630    #[test]
631    fn test_version_to_id() {
632        let v = MysqlVersion::parse("8.0.32").unwrap();
633        assert_eq!(v.to_id(), 80032);
634
635        let v = MysqlVersion::parse("9.0.1").unwrap();
636        assert_eq!(v.to_id(), 90001);
637    }
638
639    #[test]
640    fn test_version_display() {
641        let v = MysqlVersion::parse("8.4.0").unwrap();
642        assert_eq!(v.to_string(), "8.4.0");
643    }
644
645    #[test]
646    fn test_version_is_at_least() {
647        let v8 = MysqlVersion::parse("8.0.0").unwrap();
648        let v84 = MysqlVersion::parse("8.4.0").unwrap();
649        let v9 = MysqlVersion::parse("9.0.0").unwrap();
650
651        assert!(v9.is_at_least(&v84));
652        assert!(v9.is_at_least(&v8));
653        assert!(v84.is_at_least(&v8));
654        assert!(v8.is_at_least(&v8));
655        assert!(!v8.is_at_least(&v84));
656        assert!(!v84.is_at_least(&v9));
657    }
658
659    #[test]
660    fn test_severity_display() {
661        assert_eq!(Severity::Info.to_string(), "info");
662        assert_eq!(Severity::Warning.to_string(), "warning");
663        assert_eq!(Severity::Error.to_string(), "error");
664    }
665
666    #[test]
667    fn test_check_page_size_default() {
668        let info = TablespaceInfo {
669            page_size: 16384,
670            fsp_flags: 0,
671            space_id: 1,
672            row_format: None,
673            has_sdi: true,
674            is_encrypted: false,
675            vendor: VendorInfo::mysql(),
676            mysql_version_id: None,
677            has_compressed_pages: false,
678            has_instant_columns: false,
679        };
680        let target = MysqlVersion::parse("8.0.0").unwrap();
681        let mut checks = Vec::new();
682        check_page_size(&info, &target, &mut checks);
683        // Default page size should produce no checks
684        assert!(checks.is_empty());
685    }
686
687    #[test]
688    fn test_check_page_size_non_default_old_mysql() {
689        let info = TablespaceInfo {
690            page_size: 8192,
691            fsp_flags: 0,
692            space_id: 1,
693            row_format: None,
694            has_sdi: false,
695            is_encrypted: false,
696            vendor: VendorInfo::mysql(),
697            mysql_version_id: None,
698            has_compressed_pages: false,
699            has_instant_columns: false,
700        };
701        let target = MysqlVersion::parse("5.6.0").unwrap();
702        let mut checks = Vec::new();
703        check_page_size(&info, &target, &mut checks);
704        assert_eq!(checks.len(), 1);
705        assert_eq!(checks[0].severity, Severity::Error);
706    }
707
708    #[test]
709    fn test_check_sdi_missing_for_8_0() {
710        let info = TablespaceInfo {
711            page_size: 16384,
712            fsp_flags: 0,
713            space_id: 1,
714            row_format: None,
715            has_sdi: false,
716            is_encrypted: false,
717            vendor: VendorInfo::mysql(),
718            mysql_version_id: None,
719            has_compressed_pages: false,
720            has_instant_columns: false,
721        };
722        let target = MysqlVersion::parse("8.0.0").unwrap();
723        let mut checks = Vec::new();
724        check_sdi_presence(&info, &target, &mut checks);
725        assert_eq!(checks.len(), 1);
726        assert_eq!(checks[0].severity, Severity::Error);
727        assert!(checks[0].message.contains("lacks SDI"));
728    }
729
730    #[test]
731    fn test_check_sdi_present_for_pre_8() {
732        let info = TablespaceInfo {
733            page_size: 16384,
734            fsp_flags: 0,
735            space_id: 1,
736            row_format: None,
737            has_sdi: true,
738            is_encrypted: false,
739            vendor: VendorInfo::mysql(),
740            mysql_version_id: None,
741            has_compressed_pages: false,
742            has_instant_columns: false,
743        };
744        let target = MysqlVersion::parse("5.7.44").unwrap();
745        let mut checks = Vec::new();
746        check_sdi_presence(&info, &target, &mut checks);
747        assert_eq!(checks.len(), 1);
748        assert_eq!(checks[0].severity, Severity::Warning);
749    }
750
751    #[test]
752    fn test_check_vendor_mariadb() {
753        let info = TablespaceInfo {
754            page_size: 16384,
755            fsp_flags: 0,
756            space_id: 1,
757            row_format: None,
758            has_sdi: false,
759            is_encrypted: false,
760            vendor: VendorInfo::mariadb(MariaDbFormat::FullCrc32),
761            mysql_version_id: None,
762            has_compressed_pages: false,
763            has_instant_columns: false,
764        };
765        let target = MysqlVersion::parse("8.4.0").unwrap();
766        let mut checks = Vec::new();
767        check_vendor_compatibility(&info, &target, &mut checks);
768        assert_eq!(checks.len(), 1);
769        assert_eq!(checks[0].severity, Severity::Error);
770        assert!(checks[0].message.contains("MariaDB"));
771    }
772
773    #[test]
774    fn test_check_vendor_percona() {
775        let info = TablespaceInfo {
776            page_size: 16384,
777            fsp_flags: 0,
778            space_id: 1,
779            row_format: None,
780            has_sdi: true,
781            is_encrypted: false,
782            vendor: VendorInfo::percona(),
783            mysql_version_id: None,
784            has_compressed_pages: false,
785            has_instant_columns: false,
786        };
787        let target = MysqlVersion::parse("8.4.0").unwrap();
788        let mut checks = Vec::new();
789        check_vendor_compatibility(&info, &target, &mut checks);
790        assert_eq!(checks.len(), 1);
791        assert_eq!(checks[0].severity, Severity::Info);
792    }
793
794    #[test]
795    fn test_check_row_format_compressed_84() {
796        let info = TablespaceInfo {
797            page_size: 16384,
798            fsp_flags: 0,
799            space_id: 1,
800            row_format: Some("COMPRESSED".to_string()),
801            has_sdi: true,
802            is_encrypted: false,
803            vendor: VendorInfo::mysql(),
804            mysql_version_id: None,
805            has_compressed_pages: false,
806            has_instant_columns: false,
807        };
808        let target = MysqlVersion::parse("8.4.0").unwrap();
809        let mut checks = Vec::new();
810        check_row_format(&info, &target, &mut checks);
811        assert_eq!(checks.len(), 1);
812        assert_eq!(checks[0].severity, Severity::Warning);
813        assert!(checks[0].message.contains("COMPRESSED"));
814    }
815
816    #[test]
817    fn test_check_row_format_redundant_90() {
818        let info = TablespaceInfo {
819            page_size: 16384,
820            fsp_flags: 0,
821            space_id: 1,
822            row_format: Some("REDUNDANT".to_string()),
823            has_sdi: true,
824            is_encrypted: false,
825            vendor: VendorInfo::mysql(),
826            mysql_version_id: None,
827            has_compressed_pages: false,
828            has_instant_columns: false,
829        };
830        let target = MysqlVersion::parse("9.0.0").unwrap();
831        let mut checks = Vec::new();
832        check_row_format(&info, &target, &mut checks);
833        assert_eq!(checks.len(), 1);
834        assert_eq!(checks[0].severity, Severity::Warning);
835        assert!(checks[0].message.contains("REDUNDANT"));
836    }
837
838    #[test]
839    fn test_check_encryption_old_mysql() {
840        let info = TablespaceInfo {
841            page_size: 16384,
842            fsp_flags: 0,
843            space_id: 1,
844            row_format: None,
845            has_sdi: false,
846            is_encrypted: true,
847            vendor: VendorInfo::mysql(),
848            mysql_version_id: None,
849            has_compressed_pages: false,
850            has_instant_columns: false,
851        };
852        let target = MysqlVersion::parse("5.6.0").unwrap();
853        let mut checks = Vec::new();
854        check_encryption(&info, &target, &mut checks);
855        assert_eq!(checks.len(), 1);
856        assert_eq!(checks[0].severity, Severity::Error);
857    }
858
859    #[test]
860    fn test_build_compat_report_compatible() {
861        let info = TablespaceInfo {
862            page_size: 16384,
863            fsp_flags: 0,
864            space_id: 1,
865            row_format: Some("DYNAMIC".to_string()),
866            has_sdi: true,
867            is_encrypted: false,
868            vendor: VendorInfo::mysql(),
869            mysql_version_id: Some(80032),
870            has_compressed_pages: false,
871            has_instant_columns: false,
872        };
873        let target = MysqlVersion::parse("8.4.0").unwrap();
874        let report = build_compat_report(&info, &target, "test.ibd");
875        assert!(report.compatible);
876        assert_eq!(report.summary.errors, 0);
877        assert_eq!(report.source_version, Some("8.0.32".to_string()));
878    }
879
880    #[test]
881    fn test_build_compat_report_incompatible() {
882        let info = TablespaceInfo {
883            page_size: 16384,
884            fsp_flags: 0,
885            space_id: 1,
886            row_format: None,
887            has_sdi: false,
888            is_encrypted: false,
889            vendor: VendorInfo::mariadb(MariaDbFormat::FullCrc32),
890            mysql_version_id: None,
891            has_compressed_pages: false,
892            has_instant_columns: false,
893        };
894        let target = MysqlVersion::parse("8.4.0").unwrap();
895        let report = build_compat_report(&info, &target, "test.ibd");
896        assert!(!report.compatible);
897        assert!(report.summary.errors > 0);
898    }
899}