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 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                        // Note: reliable instant ADD COLUMN detection requires
299                        // se_private_data from the DD, which is not exposed in SDI.
300                    }
301                }
302            }
303        }
304    }
305
306    // Check for compressed pages (FIL_PAGE_COMPRESSED type)
307    let has_compressed_pages = {
308        let page_count = ts.page_count();
309        let mut found = false;
310        // Check first 10 pages (or all if fewer) for compression indicator
311        let check_count = page_count.min(10);
312        for i in 0..check_count {
313            if let Ok(page) = ts.read_page(i) {
314                if let Some(hdr) = FilHeader::parse(&page) {
315                    if hdr.page_type == PageType::Compressed {
316                        found = true;
317                        break;
318                    }
319                }
320            }
321        }
322        found
323    };
324
325    Ok(TablespaceInfo {
326        page_size,
327        fsp_flags,
328        space_id,
329        row_format,
330        has_sdi,
331        is_encrypted,
332        vendor,
333        mysql_version_id,
334        has_compressed_pages,
335        has_instant_columns,
336    })
337}
338
339/// Run all compatibility checks against a target MySQL version.
340///
341/// Returns a list of findings with severity levels. Error-level findings
342/// indicate the tablespace cannot be used with the target version.
343pub fn check_compatibility(info: &TablespaceInfo, target: &MysqlVersion) -> Vec<CompatCheck> {
344    let mut checks = Vec::new();
345
346    check_page_size(info, target, &mut checks);
347    check_row_format(info, target, &mut checks);
348    check_sdi_presence(info, target, &mut checks);
349    check_encryption(info, target, &mut checks);
350    check_vendor_compatibility(info, target, &mut checks);
351    check_compression(info, target, &mut checks);
352
353    checks
354}
355
356/// Build a full compatibility report.
357///
358/// Runs all checks and produces a structured report with summary counts
359/// and an overall compatible/incompatible verdict.
360pub fn build_compat_report(
361    info: &TablespaceInfo,
362    target: &MysqlVersion,
363    file: &str,
364) -> CompatReport {
365    let checks = check_compatibility(info, target);
366
367    let errors = checks
368        .iter()
369        .filter(|c| c.severity == Severity::Error)
370        .count();
371    let warnings = checks
372        .iter()
373        .filter(|c| c.severity == Severity::Warning)
374        .count();
375    let info_count = checks
376        .iter()
377        .filter(|c| c.severity == Severity::Info)
378        .count();
379
380    let source_version = info.mysql_version_id.map(|id| {
381        let v = MysqlVersion::from_id(id);
382        v.to_string()
383    });
384
385    CompatReport {
386        file: file.to_string(),
387        target_version: target.to_string(),
388        source_version,
389        compatible: errors == 0,
390        checks,
391        summary: CompatSummary {
392            total_checks: errors + warnings + info_count,
393            errors,
394            warnings,
395            info: info_count,
396        },
397    }
398}
399
400// --- Private check functions ---
401
402fn check_page_size(info: &TablespaceInfo, target: &MysqlVersion, checks: &mut Vec<CompatCheck>) {
403    // 4K/8K/32K/64K page sizes added in MySQL 5.7.6
404    let non_default = info.page_size != SIZE_PAGE_DEFAULT;
405    if non_default
406        && !target.is_at_least(&MysqlVersion {
407            major: 5,
408            minor: 7,
409            patch: 6,
410        })
411    {
412        checks.push(CompatCheck {
413            check: "page_size".to_string(),
414            message: format!(
415                "Non-default page size {} requires MySQL 5.7.6+",
416                info.page_size
417            ),
418            severity: Severity::Error,
419            current_value: Some(info.page_size.to_string()),
420            expected: Some("16384".to_string()),
421        });
422    } else if non_default {
423        checks.push(CompatCheck {
424            check: "page_size".to_string(),
425            message: format!("Non-default page size {} is supported", info.page_size),
426            severity: Severity::Info,
427            current_value: Some(info.page_size.to_string()),
428            expected: None,
429        });
430    }
431}
432
433fn check_row_format(info: &TablespaceInfo, target: &MysqlVersion, checks: &mut Vec<CompatCheck>) {
434    if let Some(ref rf) = info.row_format {
435        let rf_upper = rf.to_uppercase();
436        // COMPRESSED deprecated in 8.4+
437        if rf_upper == "COMPRESSED"
438            && target.is_at_least(&MysqlVersion {
439                major: 8,
440                minor: 4,
441                patch: 0,
442            })
443        {
444            checks.push(CompatCheck {
445                check: "row_format".to_string(),
446                message: "ROW_FORMAT=COMPRESSED is deprecated in MySQL 8.4+".to_string(),
447                severity: Severity::Warning,
448                current_value: Some(rf.clone()),
449                expected: Some("DYNAMIC".to_string()),
450            });
451        }
452        // REDUNDANT deprecated in 9.0+
453        if rf_upper == "REDUNDANT"
454            && target.is_at_least(&MysqlVersion {
455                major: 9,
456                minor: 0,
457                patch: 0,
458            })
459        {
460            checks.push(CompatCheck {
461                check: "row_format".to_string(),
462                message: "ROW_FORMAT=REDUNDANT is deprecated in MySQL 9.0+".to_string(),
463                severity: Severity::Warning,
464                current_value: Some(rf.clone()),
465                expected: Some("DYNAMIC".to_string()),
466            });
467        }
468    }
469}
470
471fn check_sdi_presence(info: &TablespaceInfo, target: &MysqlVersion, checks: &mut Vec<CompatCheck>) {
472    // SDI required for MySQL 8.0+
473    if target.is_at_least(&MysqlVersion {
474        major: 8,
475        minor: 0,
476        patch: 0,
477    }) && !info.has_sdi
478    {
479        checks.push(CompatCheck {
480            check: "sdi".to_string(),
481            message: "Tablespace lacks SDI metadata required by MySQL 8.0+".to_string(),
482            severity: Severity::Error,
483            current_value: Some("absent".to_string()),
484            expected: Some("present".to_string()),
485        });
486    } else if info.has_sdi
487        && !target.is_at_least(&MysqlVersion {
488            major: 8,
489            minor: 0,
490            patch: 0,
491        })
492    {
493        checks.push(CompatCheck {
494            check: "sdi".to_string(),
495            message: "Tablespace has SDI metadata not recognized by MySQL < 8.0".to_string(),
496            severity: Severity::Warning,
497            current_value: Some("present".to_string()),
498            expected: Some("absent".to_string()),
499        });
500    }
501}
502
503fn check_encryption(info: &TablespaceInfo, target: &MysqlVersion, checks: &mut Vec<CompatCheck>) {
504    // Tablespace-level encryption added in MySQL 5.7.11
505    if info.is_encrypted
506        && !target.is_at_least(&MysqlVersion {
507            major: 5,
508            minor: 7,
509            patch: 11,
510        })
511    {
512        checks.push(CompatCheck {
513            check: "encryption".to_string(),
514            message: "Tablespace encryption requires MySQL 5.7.11+".to_string(),
515            severity: Severity::Error,
516            current_value: Some("encrypted".to_string()),
517            expected: Some("unencrypted".to_string()),
518        });
519    }
520}
521
522fn check_vendor_compatibility(
523    info: &TablespaceInfo,
524    target: &MysqlVersion,
525    checks: &mut Vec<CompatCheck>,
526) {
527    // MariaDB -> MySQL = error (divergent formats)
528    if info.vendor.vendor == InnoDbVendor::MariaDB {
529        checks.push(CompatCheck {
530            check: "vendor".to_string(),
531            message: "MariaDB tablespace is not compatible with MySQL".to_string(),
532            severity: Severity::Error,
533            current_value: Some(info.vendor.to_string()),
534            expected: Some("MySQL".to_string()),
535        });
536    }
537    // Percona -> MySQL is fine (binary compatible)
538    if info.vendor.vendor == InnoDbVendor::Percona {
539        checks.push(CompatCheck {
540            check: "vendor".to_string(),
541            message: "Percona XtraDB tablespace is binary-compatible with MySQL".to_string(),
542            severity: Severity::Info,
543            current_value: Some(info.vendor.to_string()),
544            expected: None,
545        });
546    }
547    // Suppress the unused variable warning
548    let _ = target;
549}
550
551fn check_compression(info: &TablespaceInfo, _target: &MysqlVersion, checks: &mut Vec<CompatCheck>) {
552    if info.has_compressed_pages {
553        checks.push(CompatCheck {
554            check: "compression".to_string(),
555            message: "Tablespace uses page compression".to_string(),
556            severity: Severity::Info,
557            current_value: Some("compressed".to_string()),
558            expected: None,
559        });
560    }
561}
562
563/// Per-file result for directory scan mode.
564#[derive(Debug, Clone, Serialize)]
565pub struct ScanFileResult {
566    /// Relative path within the data directory.
567    pub file: String,
568    /// Whether this file is compatible with the target version.
569    pub compatible: bool,
570    /// Error message if the file could not be analyzed.
571    #[serde(skip_serializing_if = "Option::is_none")]
572    pub error: Option<String>,
573    /// Individual check results (empty if error occurred).
574    #[serde(skip_serializing_if = "Vec::is_empty")]
575    pub checks: Vec<CompatCheck>,
576}
577
578/// Directory scan compatibility report.
579#[derive(Debug, Clone, Serialize)]
580pub struct ScanCompatReport {
581    /// Target MySQL version.
582    pub target_version: String,
583    /// Number of files scanned.
584    pub files_scanned: usize,
585    /// Number of compatible files.
586    pub files_compatible: usize,
587    /// Number of incompatible files.
588    pub files_incompatible: usize,
589    /// Number of files with errors.
590    pub files_error: usize,
591    /// Per-file results.
592    pub results: Vec<ScanFileResult>,
593}
594
595#[cfg(test)]
596mod tests {
597    use super::*;
598    use crate::innodb::vendor::MariaDbFormat;
599
600    #[test]
601    fn test_version_parse_valid() {
602        let v = MysqlVersion::parse("8.0.32").unwrap();
603        assert_eq!(v.major, 8);
604        assert_eq!(v.minor, 0);
605        assert_eq!(v.patch, 32);
606    }
607
608    #[test]
609    fn test_version_parse_invalid_format() {
610        assert!(MysqlVersion::parse("8.0").is_err());
611        assert!(MysqlVersion::parse("8").is_err());
612        assert!(MysqlVersion::parse("").is_err());
613        assert!(MysqlVersion::parse("8.0.x").is_err());
614    }
615
616    #[test]
617    fn test_version_from_id() {
618        let v = MysqlVersion::from_id(80032);
619        assert_eq!(v.major, 8);
620        assert_eq!(v.minor, 0);
621        assert_eq!(v.patch, 32);
622
623        let v = MysqlVersion::from_id(90001);
624        assert_eq!(v.major, 9);
625        assert_eq!(v.minor, 0);
626        assert_eq!(v.patch, 1);
627    }
628
629    #[test]
630    fn test_version_to_id() {
631        let v = MysqlVersion::parse("8.0.32").unwrap();
632        assert_eq!(v.to_id(), 80032);
633
634        let v = MysqlVersion::parse("9.0.1").unwrap();
635        assert_eq!(v.to_id(), 90001);
636    }
637
638    #[test]
639    fn test_version_display() {
640        let v = MysqlVersion::parse("8.4.0").unwrap();
641        assert_eq!(v.to_string(), "8.4.0");
642    }
643
644    #[test]
645    fn test_version_is_at_least() {
646        let v8 = MysqlVersion::parse("8.0.0").unwrap();
647        let v84 = MysqlVersion::parse("8.4.0").unwrap();
648        let v9 = MysqlVersion::parse("9.0.0").unwrap();
649
650        assert!(v9.is_at_least(&v84));
651        assert!(v9.is_at_least(&v8));
652        assert!(v84.is_at_least(&v8));
653        assert!(v8.is_at_least(&v8));
654        assert!(!v8.is_at_least(&v84));
655        assert!(!v84.is_at_least(&v9));
656    }
657
658    #[test]
659    fn test_severity_display() {
660        assert_eq!(Severity::Info.to_string(), "info");
661        assert_eq!(Severity::Warning.to_string(), "warning");
662        assert_eq!(Severity::Error.to_string(), "error");
663    }
664
665    #[test]
666    fn test_check_page_size_default() {
667        let info = TablespaceInfo {
668            page_size: 16384,
669            fsp_flags: 0,
670            space_id: 1,
671            row_format: None,
672            has_sdi: true,
673            is_encrypted: false,
674            vendor: VendorInfo::mysql(),
675            mysql_version_id: None,
676            has_compressed_pages: false,
677            has_instant_columns: false,
678        };
679        let target = MysqlVersion::parse("8.0.0").unwrap();
680        let mut checks = Vec::new();
681        check_page_size(&info, &target, &mut checks);
682        // Default page size should produce no checks
683        assert!(checks.is_empty());
684    }
685
686    #[test]
687    fn test_check_page_size_non_default_old_mysql() {
688        let info = TablespaceInfo {
689            page_size: 8192,
690            fsp_flags: 0,
691            space_id: 1,
692            row_format: None,
693            has_sdi: false,
694            is_encrypted: false,
695            vendor: VendorInfo::mysql(),
696            mysql_version_id: None,
697            has_compressed_pages: false,
698            has_instant_columns: false,
699        };
700        let target = MysqlVersion::parse("5.6.0").unwrap();
701        let mut checks = Vec::new();
702        check_page_size(&info, &target, &mut checks);
703        assert_eq!(checks.len(), 1);
704        assert_eq!(checks[0].severity, Severity::Error);
705    }
706
707    #[test]
708    fn test_check_sdi_missing_for_8_0() {
709        let info = TablespaceInfo {
710            page_size: 16384,
711            fsp_flags: 0,
712            space_id: 1,
713            row_format: None,
714            has_sdi: false,
715            is_encrypted: false,
716            vendor: VendorInfo::mysql(),
717            mysql_version_id: None,
718            has_compressed_pages: false,
719            has_instant_columns: false,
720        };
721        let target = MysqlVersion::parse("8.0.0").unwrap();
722        let mut checks = Vec::new();
723        check_sdi_presence(&info, &target, &mut checks);
724        assert_eq!(checks.len(), 1);
725        assert_eq!(checks[0].severity, Severity::Error);
726        assert!(checks[0].message.contains("lacks SDI"));
727    }
728
729    #[test]
730    fn test_check_sdi_present_for_pre_8() {
731        let info = TablespaceInfo {
732            page_size: 16384,
733            fsp_flags: 0,
734            space_id: 1,
735            row_format: None,
736            has_sdi: true,
737            is_encrypted: false,
738            vendor: VendorInfo::mysql(),
739            mysql_version_id: None,
740            has_compressed_pages: false,
741            has_instant_columns: false,
742        };
743        let target = MysqlVersion::parse("5.7.44").unwrap();
744        let mut checks = Vec::new();
745        check_sdi_presence(&info, &target, &mut checks);
746        assert_eq!(checks.len(), 1);
747        assert_eq!(checks[0].severity, Severity::Warning);
748    }
749
750    #[test]
751    fn test_check_vendor_mariadb() {
752        let info = TablespaceInfo {
753            page_size: 16384,
754            fsp_flags: 0,
755            space_id: 1,
756            row_format: None,
757            has_sdi: false,
758            is_encrypted: false,
759            vendor: VendorInfo::mariadb(MariaDbFormat::FullCrc32),
760            mysql_version_id: None,
761            has_compressed_pages: false,
762            has_instant_columns: false,
763        };
764        let target = MysqlVersion::parse("8.4.0").unwrap();
765        let mut checks = Vec::new();
766        check_vendor_compatibility(&info, &target, &mut checks);
767        assert_eq!(checks.len(), 1);
768        assert_eq!(checks[0].severity, Severity::Error);
769        assert!(checks[0].message.contains("MariaDB"));
770    }
771
772    #[test]
773    fn test_check_vendor_percona() {
774        let info = TablespaceInfo {
775            page_size: 16384,
776            fsp_flags: 0,
777            space_id: 1,
778            row_format: None,
779            has_sdi: true,
780            is_encrypted: false,
781            vendor: VendorInfo::percona(),
782            mysql_version_id: None,
783            has_compressed_pages: false,
784            has_instant_columns: false,
785        };
786        let target = MysqlVersion::parse("8.4.0").unwrap();
787        let mut checks = Vec::new();
788        check_vendor_compatibility(&info, &target, &mut checks);
789        assert_eq!(checks.len(), 1);
790        assert_eq!(checks[0].severity, Severity::Info);
791    }
792
793    #[test]
794    fn test_check_row_format_compressed_84() {
795        let info = TablespaceInfo {
796            page_size: 16384,
797            fsp_flags: 0,
798            space_id: 1,
799            row_format: Some("COMPRESSED".to_string()),
800            has_sdi: true,
801            is_encrypted: false,
802            vendor: VendorInfo::mysql(),
803            mysql_version_id: None,
804            has_compressed_pages: false,
805            has_instant_columns: false,
806        };
807        let target = MysqlVersion::parse("8.4.0").unwrap();
808        let mut checks = Vec::new();
809        check_row_format(&info, &target, &mut checks);
810        assert_eq!(checks.len(), 1);
811        assert_eq!(checks[0].severity, Severity::Warning);
812        assert!(checks[0].message.contains("COMPRESSED"));
813    }
814
815    #[test]
816    fn test_check_row_format_redundant_90() {
817        let info = TablespaceInfo {
818            page_size: 16384,
819            fsp_flags: 0,
820            space_id: 1,
821            row_format: Some("REDUNDANT".to_string()),
822            has_sdi: true,
823            is_encrypted: false,
824            vendor: VendorInfo::mysql(),
825            mysql_version_id: None,
826            has_compressed_pages: false,
827            has_instant_columns: false,
828        };
829        let target = MysqlVersion::parse("9.0.0").unwrap();
830        let mut checks = Vec::new();
831        check_row_format(&info, &target, &mut checks);
832        assert_eq!(checks.len(), 1);
833        assert_eq!(checks[0].severity, Severity::Warning);
834        assert!(checks[0].message.contains("REDUNDANT"));
835    }
836
837    #[test]
838    fn test_check_encryption_old_mysql() {
839        let info = TablespaceInfo {
840            page_size: 16384,
841            fsp_flags: 0,
842            space_id: 1,
843            row_format: None,
844            has_sdi: false,
845            is_encrypted: true,
846            vendor: VendorInfo::mysql(),
847            mysql_version_id: None,
848            has_compressed_pages: false,
849            has_instant_columns: false,
850        };
851        let target = MysqlVersion::parse("5.6.0").unwrap();
852        let mut checks = Vec::new();
853        check_encryption(&info, &target, &mut checks);
854        assert_eq!(checks.len(), 1);
855        assert_eq!(checks[0].severity, Severity::Error);
856    }
857
858    #[test]
859    fn test_build_compat_report_compatible() {
860        let info = TablespaceInfo {
861            page_size: 16384,
862            fsp_flags: 0,
863            space_id: 1,
864            row_format: Some("DYNAMIC".to_string()),
865            has_sdi: true,
866            is_encrypted: false,
867            vendor: VendorInfo::mysql(),
868            mysql_version_id: Some(80032),
869            has_compressed_pages: false,
870            has_instant_columns: false,
871        };
872        let target = MysqlVersion::parse("8.4.0").unwrap();
873        let report = build_compat_report(&info, &target, "test.ibd");
874        assert!(report.compatible);
875        assert_eq!(report.summary.errors, 0);
876        assert_eq!(report.source_version, Some("8.0.32".to_string()));
877    }
878
879    #[test]
880    fn test_build_compat_report_incompatible() {
881        let info = TablespaceInfo {
882            page_size: 16384,
883            fsp_flags: 0,
884            space_id: 1,
885            row_format: None,
886            has_sdi: false,
887            is_encrypted: false,
888            vendor: VendorInfo::mariadb(MariaDbFormat::FullCrc32),
889            mysql_version_id: None,
890            has_compressed_pages: false,
891            has_instant_columns: false,
892        };
893        let target = MysqlVersion::parse("8.4.0").unwrap();
894        let report = build_compat_report(&info, &target, "test.ibd");
895        assert!(!report.compatible);
896        assert!(report.summary.errors > 0);
897    }
898}