1use 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#[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 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 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 pub fn to_id(&self) -> u64 {
124 (self.major as u64) * 10000 + (self.minor as u64) * 100 + self.patch as u64
125 }
126
127 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
163pub enum Severity {
164 Info,
166 Warning,
168 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#[derive(Debug, Clone, Serialize)]
184pub struct CompatCheck {
185 pub check: String,
187 pub message: String,
189 pub severity: Severity,
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub current_value: Option<String>,
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub expected: Option<String>,
197}
198
199#[derive(Debug, Clone, Serialize)]
201pub struct TablespaceInfo {
202 pub page_size: u32,
204 pub fsp_flags: u32,
206 pub space_id: u32,
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub row_format: Option<String>,
211 pub has_sdi: bool,
213 pub is_encrypted: bool,
215 pub vendor: VendorInfo,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub mysql_version_id: Option<u64>,
220 pub has_compressed_pages: bool,
222 pub has_instant_columns: bool,
224}
225
226#[derive(Debug, Clone, Serialize)]
228pub struct CompatReport {
229 pub file: String,
231 pub target_version: String,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub source_version: Option<String>,
236 pub compatible: bool,
238 pub checks: Vec<CompatCheck>,
240 pub summary: CompatSummary,
242}
243
244#[derive(Debug, Clone, Serialize)]
246pub struct CompatSummary {
247 pub total_checks: usize,
249 pub errors: usize,
251 pub warnings: usize,
253 pub info: usize,
255}
256
257pub 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 let sdi_pages = crate::innodb::sdi::find_sdi_pages(ts).unwrap_or_default();
280 let has_sdi = !sdi_pages.is_empty();
281
282 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 let has_compressed_pages = {
309 let page_count = ts.page_count();
310 let mut found = false;
311 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
340pub 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
357pub 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
401fn check_page_size(info: &TablespaceInfo, target: &MysqlVersion, checks: &mut Vec<CompatCheck>) {
404 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Serialize)]
566pub struct ScanFileResult {
567 pub file: String,
569 pub compatible: bool,
571 #[serde(skip_serializing_if = "Option::is_none")]
573 pub error: Option<String>,
574 #[serde(skip_serializing_if = "Vec::is_empty")]
576 pub checks: Vec<CompatCheck>,
577}
578
579#[derive(Debug, Clone, Serialize)]
581pub struct ScanCompatReport {
582 pub target_version: String,
584 pub files_scanned: usize,
586 pub files_compatible: usize,
588 pub files_incompatible: usize,
590 pub files_error: usize,
592 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 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}