1use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30
31use crate::IdbError;
32
33#[derive(Debug, Deserialize)]
39pub struct SdiEnvelope {
40 #[serde(default)]
42 pub mysqld_version_id: u64,
43 #[serde(default)]
45 pub dd_object_type: String,
46 #[serde(default)]
48 pub dd_object: DdTable,
49}
50
51#[derive(Debug, Default, Deserialize)]
53pub struct DdTable {
54 #[serde(default)]
56 pub name: String,
57 #[serde(default)]
59 pub schema_ref: String,
60 #[serde(default)]
62 pub engine: String,
63 #[serde(default)]
65 pub collation_id: u64,
66 #[serde(default)]
68 pub row_format: u64,
69 #[serde(default)]
71 pub comment: String,
72 #[serde(default)]
74 pub columns: Vec<DdColumn>,
75 #[serde(default)]
77 pub indexes: Vec<DdIndex>,
78 #[serde(default)]
80 pub foreign_keys: Vec<DdForeignKey>,
81 #[serde(default)]
83 pub mysql_version_id: u64,
84}
85
86#[derive(Debug, Default, Deserialize)]
88pub struct DdColumn {
89 #[serde(default)]
91 pub name: String,
92 #[serde(rename = "type", default)]
94 pub dd_type: u64,
95 #[serde(default)]
97 pub column_type_utf8: String,
98 #[serde(default)]
100 pub ordinal_position: u64,
101 #[serde(default)]
104 pub hidden: u64,
105 #[serde(default)]
107 pub is_nullable: bool,
108 #[serde(default)]
110 pub is_unsigned: bool,
111 #[serde(default)]
113 pub is_auto_increment: bool,
114 #[serde(default)]
116 pub is_virtual: bool,
117 #[serde(default)]
119 pub char_length: u64,
120 #[serde(default)]
122 pub numeric_precision: u64,
123 #[serde(default)]
125 pub numeric_scale: u64,
126 #[serde(default)]
128 pub datetime_precision: u64,
129 #[serde(default)]
131 pub collation_id: u64,
132 #[serde(default)]
134 pub default_value_utf8: String,
135 #[serde(default)]
137 pub default_value_utf8_null: bool,
138 #[serde(default)]
140 pub has_no_default: bool,
141 #[serde(default)]
143 pub default_option: String,
144 #[serde(default)]
146 pub update_option: String,
147 #[serde(default)]
149 pub generation_expression: String,
150 #[serde(default)]
152 pub generation_expression_utf8: String,
153 #[serde(default)]
155 pub elements: Vec<DdColumnElement>,
156 #[serde(default)]
158 pub comment: String,
159 #[serde(default)]
161 pub is_zerofill: bool,
162}
163
164#[derive(Debug, Default, Deserialize)]
166pub struct DdColumnElement {
167 #[serde(default)]
169 pub name: String,
170}
171
172#[derive(Debug, Default, Deserialize)]
174pub struct DdIndex {
175 #[serde(default)]
177 pub name: String,
178 #[serde(rename = "type", default)]
180 pub index_type: u64,
181 #[serde(default)]
183 pub algorithm: u64,
184 #[serde(default)]
186 pub hidden: bool,
187 #[serde(default)]
189 pub elements: Vec<DdIndexElement>,
190 #[serde(default)]
192 pub comment: String,
193 #[serde(default)]
195 pub is_visible: bool,
196}
197
198#[derive(Debug, Default, Deserialize)]
200pub struct DdIndexElement {
201 #[serde(default)]
203 pub column_opx: u64,
204 #[serde(default)]
206 pub length: u64,
207 #[serde(default)]
209 pub order: u64,
210 #[serde(default)]
212 pub hidden: bool,
213}
214
215#[derive(Debug, Default, Deserialize)]
217pub struct DdForeignKey {
218 #[serde(default)]
220 pub name: String,
221 #[serde(default)]
223 pub referenced_table_schema_name: String,
224 #[serde(default)]
226 pub referenced_table_name: String,
227 #[serde(default)]
229 pub update_rule: u64,
230 #[serde(default)]
232 pub delete_rule: u64,
233 #[serde(default)]
235 pub elements: Vec<DdForeignKeyElement>,
236}
237
238#[derive(Debug, Default, Deserialize)]
240pub struct DdForeignKeyElement {
241 #[serde(default)]
243 pub column_opx: u64,
244 #[serde(default)]
246 pub referenced_column_name: String,
247}
248
249#[derive(Debug, Clone, Serialize)]
255pub struct TableSchema {
256 #[serde(skip_serializing_if = "Option::is_none")]
258 pub schema_name: Option<String>,
259 pub table_name: String,
261 pub engine: String,
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub row_format: Option<String>,
266 #[serde(skip_serializing_if = "Option::is_none")]
268 pub collation: Option<String>,
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub charset: Option<String>,
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub comment: Option<String>,
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub mysql_version: Option<String>,
278 pub source: String,
280 pub columns: Vec<ColumnDef>,
282 pub indexes: Vec<IndexDef>,
284 #[serde(skip_serializing_if = "Vec::is_empty")]
286 pub foreign_keys: Vec<ForeignKeyDef>,
287 pub ddl: String,
289}
290
291#[derive(Debug, Clone, Serialize)]
293pub struct ColumnDef {
294 pub name: String,
296 pub column_type: String,
298 pub is_nullable: bool,
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub default_value: Option<String>,
303 #[serde(skip_serializing_if = "is_false")]
305 pub is_auto_increment: bool,
306 #[serde(skip_serializing_if = "Option::is_none")]
308 pub generation_expression: Option<String>,
309 #[serde(skip_serializing_if = "Option::is_none")]
311 pub is_virtual: Option<bool>,
312 #[serde(skip_serializing_if = "is_false")]
314 pub is_invisible: bool,
315 #[serde(skip_serializing_if = "Option::is_none")]
317 pub comment: Option<String>,
318}
319
320fn is_false(v: &bool) -> bool {
321 !v
322}
323
324#[derive(Debug, Clone, Serialize)]
326pub struct IndexDef {
327 pub name: String,
329 pub index_type: String,
331 pub columns: Vec<IndexColumnDef>,
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub comment: Option<String>,
336 #[serde(skip_serializing_if = "is_true")]
338 pub is_visible: bool,
339}
340
341fn is_true(v: &bool) -> bool {
342 *v
343}
344
345#[derive(Debug, Clone, Serialize)]
347pub struct IndexColumnDef {
348 pub name: String,
350 #[serde(skip_serializing_if = "Option::is_none")]
352 pub prefix_length: Option<u64>,
353 #[serde(skip_serializing_if = "Option::is_none")]
355 pub order: Option<String>,
356}
357
358#[derive(Debug, Clone, Serialize)]
360pub struct ForeignKeyDef {
361 pub name: String,
363 pub columns: Vec<String>,
365 pub referenced_table: String,
367 pub referenced_columns: Vec<String>,
369 pub on_update: String,
371 pub on_delete: String,
373}
374
375#[derive(Debug, Clone, Serialize)]
377pub struct InferredSchema {
378 pub source: String,
380 pub record_format: String,
382 pub indexes: Vec<InferredIndex>,
384}
385
386#[derive(Debug, Clone, Serialize)]
388pub struct InferredIndex {
389 pub index_id: u64,
391 pub leaf_pages: u64,
393 pub max_level: u16,
395}
396
397pub fn collation_name(id: u64) -> Option<&'static str> {
416 match id {
417 2 => Some("latin2_czech_cs"),
418 8 => Some("latin1_swedish_ci"),
419 11 => Some("ascii_general_ci"),
420 33 => Some("utf8mb3_general_ci"),
421 45 => Some("utf8mb4_general_ci"),
422 46 => Some("utf8mb4_bin"),
423 47 => Some("latin1_bin"),
424 48 => Some("latin1_general_ci"),
425 63 => Some("binary"),
426 83 => Some("utf8mb3_bin"),
427 224 => Some("utf8mb4_unicode_ci"),
428 255 => Some("utf8mb4_0900_ai_ci"),
429 _ => None,
430 }
431}
432
433pub fn charset_from_collation(id: u64) -> Option<&'static str> {
446 match id {
447 2 => Some("latin2"),
448 8 | 47 | 48 => Some("latin1"),
449 11 => Some("ascii"),
450 33 | 83 => Some("utf8mb3"),
451 45 | 46 | 224 | 255 => Some("utf8mb4"),
452 63 => Some("binary"),
453 _ => None,
454 }
455}
456
457pub fn row_format_name(id: u64) -> &'static str {
469 match id {
470 1 => "FIXED",
471 2 => "DYNAMIC",
472 3 => "COMPRESSED",
473 4 => "REDUNDANT",
474 5 => "COMPACT",
475 _ => "UNKNOWN",
476 }
477}
478
479pub fn fk_rule_name(rule: u64) -> &'static str {
490 match rule {
491 0 => "NO ACTION",
492 1 => "RESTRICT",
493 2 => "CASCADE",
494 3 => "SET NULL",
495 4 => "SET DEFAULT",
496 _ => "NO ACTION",
497 }
498}
499
500pub fn dd_type_to_sql(col: &DdColumn) -> String {
516 match col.dd_type {
517 1 => "tinyint".to_string(),
518 2 => "smallint".to_string(),
519 3 => "mediumint".to_string(),
520 4 => "int".to_string(),
521 5 => "bigint".to_string(),
522 6 => format_decimal(col),
523 7 => "float".to_string(),
524 8 => "double".to_string(),
525 9 | 10 => "binary".to_string(), 11 => "year".to_string(),
527 12 => "date".to_string(),
528 13 => "time".to_string(),
529 14 => "datetime".to_string(),
530 15 => "timestamp".to_string(),
531 16 => format_varchar(col),
532 17 => format_char(col),
533 18 => "bit".to_string(),
534 19 => "enum".to_string(),
535 20 => "set".to_string(),
536 23 => "tinyblob".to_string(),
537 24 => "mediumblob".to_string(),
538 25 => "longblob".to_string(),
539 26 => "blob".to_string(),
540 27 => format_text(col),
541 28 => "varbinary".to_string(),
542 29 => "binary".to_string(),
543 30 => "geometry".to_string(),
544 31 => "json".to_string(),
545 _ => format!("unknown_type({})", col.dd_type),
546 }
547}
548
549fn format_decimal(col: &DdColumn) -> String {
550 if col.numeric_precision > 0 {
551 if col.numeric_scale > 0 {
552 format!("decimal({},{})", col.numeric_precision, col.numeric_scale)
553 } else {
554 format!("decimal({})", col.numeric_precision)
555 }
556 } else {
557 "decimal".to_string()
558 }
559}
560
561fn format_varchar(col: &DdColumn) -> String {
562 let max_bytes_per_char = charset_max_bytes(col.collation_id);
564 let char_len = if max_bytes_per_char > 0 {
565 col.char_length / max_bytes_per_char
566 } else {
567 col.char_length
568 };
569 format!("varchar({})", char_len)
570}
571
572fn format_char(col: &DdColumn) -> String {
573 let max_bytes_per_char = charset_max_bytes(col.collation_id);
574 let char_len = if max_bytes_per_char > 0 {
575 col.char_length / max_bytes_per_char
576 } else {
577 col.char_length
578 };
579 format!("char({})", char_len.max(1))
580}
581
582fn format_text(col: &DdColumn) -> String {
583 match col.char_length {
584 0..=255 => "tinytext".to_string(),
585 256..=65535 => "text".to_string(),
586 65536..=16777215 => "mediumtext".to_string(),
587 _ => "longtext".to_string(),
588 }
589}
590
591fn charset_max_bytes(collation_id: u64) -> u64 {
593 match collation_id {
594 2 | 8 | 11 | 47 | 48 => 1, 33 | 83 => 3, 45 | 46 | 224 | 255 => 4, 63 => 1, _ => 4, }
600}
601
602fn format_mysql_version(version_id: u64) -> String {
604 if version_id == 0 {
605 return "unknown".to_string();
606 }
607 let major = version_id / 10000;
608 let minor = (version_id % 10000) / 100;
609 let patch = version_id % 100;
610 format!("{}.{}.{}", major, minor, patch)
611}
612
613pub fn extract_schema_from_sdi(sdi_json: &str) -> Result<TableSchema, IdbError> {
680 let envelope: SdiEnvelope = serde_json::from_str(sdi_json)
681 .map_err(|e| IdbError::Parse(format!("Failed to parse SDI JSON: {}", e)))?;
682
683 let dd = &envelope.dd_object;
684
685 let all_columns: Vec<&DdColumn> = {
687 let mut cols: Vec<&DdColumn> = dd.columns.iter().collect();
688 cols.sort_by_key(|c| c.ordinal_position);
689 cols
690 };
691
692 let column_by_index: HashMap<u64, &DdColumn> = dd
695 .columns
696 .iter()
697 .enumerate()
698 .map(|(i, c)| (i as u64, c))
699 .collect();
700
701 let visible_columns: Vec<&DdColumn> = {
707 let mut cols: Vec<&DdColumn> = all_columns
708 .iter()
709 .copied()
710 .filter(|c| c.hidden == 1 || c.hidden == 4)
711 .collect();
712 cols.sort_by_key(|c| c.ordinal_position);
713 cols
714 };
715
716 let columns: Vec<ColumnDef> = visible_columns
718 .iter()
719 .map(|c| build_column_def(c))
720 .collect();
721
722 let indexes: Vec<IndexDef> = dd
724 .indexes
725 .iter()
726 .filter(|idx| !idx.hidden)
727 .map(|idx| build_index_def(idx, &column_by_index))
728 .collect();
729
730 let foreign_keys: Vec<ForeignKeyDef> = dd
732 .foreign_keys
733 .iter()
734 .map(|fk| build_fk_def(fk, &column_by_index))
735 .collect();
736
737 let row_fmt = row_format_name(dd.row_format);
738 let coll = collation_name(dd.collation_id);
739 let cs = charset_from_collation(dd.collation_id);
740 let mysql_ver = format_mysql_version(envelope.mysqld_version_id);
741
742 let schema_name = if dd.schema_ref.is_empty() {
743 None
744 } else {
745 Some(dd.schema_ref.clone())
746 };
747
748 let comment = if dd.comment.is_empty() {
749 None
750 } else {
751 Some(dd.comment.clone())
752 };
753
754 let mut schema = TableSchema {
755 schema_name,
756 table_name: dd.name.clone(),
757 engine: dd.engine.clone(),
758 row_format: Some(row_fmt.to_string()),
759 collation: coll.map(|s| s.to_string()),
760 charset: cs.map(|s| s.to_string()),
761 comment,
762 mysql_version: Some(mysql_ver),
763 source: "sdi".to_string(),
764 columns,
765 indexes,
766 foreign_keys,
767 ddl: String::new(),
768 };
769
770 schema.ddl = generate_ddl(&schema);
771 Ok(schema)
772}
773
774fn build_column_def(col: &DdColumn) -> ColumnDef {
775 let column_type = if !col.column_type_utf8.is_empty() {
776 col.column_type_utf8.clone()
777 } else {
778 dd_type_to_sql(col)
779 };
780
781 let default_value = if !col.default_option.is_empty() {
782 Some(col.default_option.clone())
784 } else if !col.has_no_default && !col.default_value_utf8_null {
785 let is_numeric = matches!(col.dd_type, 1..=8);
787 if is_numeric {
788 Some(col.default_value_utf8.clone())
789 } else {
790 Some(format!("'{}'", col.default_value_utf8.replace('\'', "''")))
791 }
792 } else if !col.has_no_default && col.is_nullable && col.default_value_utf8_null {
793 Some("NULL".to_string())
794 } else {
795 None
796 };
797
798 let generation_expression = if !col.generation_expression_utf8.is_empty() {
799 Some(col.generation_expression_utf8.clone())
800 } else {
801 None
802 };
803
804 let is_virtual = if generation_expression.is_some() {
805 Some(col.is_virtual)
806 } else {
807 None
808 };
809
810 let comment = if col.comment.is_empty() {
811 None
812 } else {
813 Some(col.comment.clone())
814 };
815
816 ColumnDef {
817 name: col.name.clone(),
818 column_type,
819 is_nullable: col.is_nullable,
820 default_value,
821 is_auto_increment: col.is_auto_increment,
822 generation_expression,
823 is_invisible: col.hidden == 4,
824 is_virtual,
825 comment,
826 }
827}
828
829fn build_index_def(idx: &DdIndex, columns: &HashMap<u64, &DdColumn>) -> IndexDef {
830 let index_type = match idx.index_type {
831 1 => "PRIMARY KEY",
832 2 => "UNIQUE KEY",
833 3 => "KEY",
834 4 => "FULLTEXT KEY",
835 5 => "SPATIAL KEY",
836 _ => "KEY",
837 };
838
839 let idx_columns: Vec<IndexColumnDef> = idx
840 .elements
841 .iter()
842 .filter(|e| !e.hidden)
843 .map(|e| {
844 let col_name = columns
845 .get(&e.column_opx)
846 .map(|c| c.name.clone())
847 .unwrap_or_else(|| format!("col_{}", e.column_opx));
848
849 let prefix_length = if e.length < 4294967295 {
850 let col = columns.get(&e.column_opx);
854 let full_len = col.map(|c| c.char_length).unwrap_or(0);
855 let max_bytes = col.map(|c| charset_max_bytes(c.collation_id)).unwrap_or(4);
856 let full_char_len = if max_bytes > 0 {
857 full_len / max_bytes
858 } else {
859 full_len
860 };
861 if e.length < full_char_len {
862 let prefix_chars = if max_bytes > 0 {
864 e.length / max_bytes
865 } else {
866 e.length
867 };
868 if prefix_chars > 0 {
869 Some(prefix_chars)
870 } else {
871 Some(e.length)
872 }
873 } else {
874 None
875 }
876 } else {
877 None
878 };
879
880 let order = if e.order == 1 {
881 Some("DESC".to_string())
882 } else {
883 None
884 };
885
886 IndexColumnDef {
887 name: col_name,
888 prefix_length,
889 order,
890 }
891 })
892 .collect();
893
894 let comment = if idx.comment.is_empty() {
895 None
896 } else {
897 Some(idx.comment.clone())
898 };
899
900 IndexDef {
901 name: idx.name.clone(),
902 index_type: index_type.to_string(),
903 columns: idx_columns,
904 comment,
905 is_visible: idx.is_visible,
906 }
907}
908
909fn build_fk_def(fk: &DdForeignKey, columns: &HashMap<u64, &DdColumn>) -> ForeignKeyDef {
910 let fk_columns: Vec<String> = fk
911 .elements
912 .iter()
913 .map(|e| {
914 columns
915 .get(&e.column_opx)
916 .map(|c| c.name.clone())
917 .unwrap_or_else(|| format!("col_{}", e.column_opx))
918 })
919 .collect();
920
921 let ref_columns: Vec<String> = fk
922 .elements
923 .iter()
924 .map(|e| e.referenced_column_name.clone())
925 .collect();
926
927 let ref_table = if fk.referenced_table_schema_name.is_empty() {
928 format!("`{}`", fk.referenced_table_name)
929 } else {
930 format!(
931 "`{}`.`{}`",
932 fk.referenced_table_schema_name, fk.referenced_table_name
933 )
934 };
935
936 ForeignKeyDef {
937 name: fk.name.clone(),
938 columns: fk_columns,
939 referenced_table: ref_table,
940 referenced_columns: ref_columns,
941 on_update: fk_rule_name(fk.update_rule).to_string(),
942 on_delete: fk_rule_name(fk.delete_rule).to_string(),
943 }
944}
945
946pub fn generate_ddl(schema: &TableSchema) -> String {
955 let mut ddl = format!("CREATE TABLE `{}` (\n", schema.table_name);
956 let mut parts: Vec<String> = Vec::new();
957
958 for col in &schema.columns {
960 parts.push(format_column_ddl(col));
961 }
962
963 for idx in &schema.indexes {
965 parts.push(format_index_ddl(idx));
966 }
967
968 for fk in &schema.foreign_keys {
970 parts.push(format_fk_ddl(fk));
971 }
972
973 ddl.push_str(&parts.join(",\n"));
974 ddl.push_str("\n)");
975
976 let mut options = Vec::new();
978 options.push(format!("ENGINE={}", schema.engine));
979 if let Some(ref cs) = schema.charset {
980 options.push(format!("DEFAULT CHARSET={}", cs));
981 }
982 if let Some(ref coll) = schema.collation {
983 options.push(format!("COLLATE={}", coll));
984 }
985 if let Some(ref fmt) = schema.row_format {
986 if fmt != "DYNAMIC" {
987 options.push(format!("ROW_FORMAT={}", fmt));
989 }
990 }
991 if let Some(ref comment) = schema.comment {
992 options.push(format!("COMMENT='{}'", comment.replace('\'', "''")));
993 }
994
995 if !options.is_empty() {
996 ddl.push(' ');
997 ddl.push_str(&options.join(" "));
998 }
999 ddl.push(';');
1000
1001 ddl
1002}
1003
1004fn format_column_ddl(col: &ColumnDef) -> String {
1005 let mut parts = vec![format!(" `{}` {}", col.name, col.column_type)];
1006
1007 if !col.is_nullable {
1008 parts.push("NOT NULL".to_string());
1009 }
1010
1011 if let Some(ref default) = col.default_value {
1012 parts.push(format!("DEFAULT {}", default));
1013 }
1014
1015 if col.is_auto_increment {
1016 parts.push("AUTO_INCREMENT".to_string());
1017 }
1018
1019 if let Some(ref expr) = col.generation_expression {
1020 let stored_or_virtual = if col.is_virtual == Some(true) {
1021 "VIRTUAL"
1022 } else {
1023 "STORED"
1024 };
1025 parts.push(format!(
1026 "GENERATED ALWAYS AS ({}) {}",
1027 expr, stored_or_virtual
1028 ));
1029 }
1030
1031 if col.is_invisible {
1032 parts.push("/*!80023 INVISIBLE */".to_string());
1033 }
1034
1035 if let Some(ref comment) = col.comment {
1036 parts.push(format!("COMMENT '{}'", comment.replace('\'', "''")));
1037 }
1038
1039 parts.join(" ")
1040}
1041
1042fn format_index_ddl(idx: &IndexDef) -> String {
1043 let cols = format_index_columns(&idx.columns);
1044
1045 let visibility = if !idx.is_visible {
1046 " /*!80000 INVISIBLE */"
1047 } else {
1048 ""
1049 };
1050
1051 let comment = if let Some(ref c) = idx.comment {
1052 format!(" COMMENT '{}'", c.replace('\'', "''"))
1053 } else {
1054 String::new()
1055 };
1056
1057 match idx.index_type.as_str() {
1058 "PRIMARY KEY" => format!(" PRIMARY KEY ({}){}{}", cols, comment, visibility),
1059 _ => format!(
1060 " {} `{}` ({}){}{}",
1061 idx.index_type, idx.name, cols, comment, visibility
1062 ),
1063 }
1064}
1065
1066fn format_index_columns(columns: &[IndexColumnDef]) -> String {
1067 columns
1068 .iter()
1069 .map(|c| {
1070 let mut s = format!("`{}`", c.name);
1071 if let Some(len) = c.prefix_length {
1072 s.push_str(&format!("({})", len));
1073 }
1074 if let Some(ref ord) = c.order {
1075 s.push(' ');
1076 s.push_str(ord);
1077 }
1078 s
1079 })
1080 .collect::<Vec<_>>()
1081 .join(", ")
1082}
1083
1084fn format_fk_ddl(fk: &ForeignKeyDef) -> String {
1085 let cols = fk
1086 .columns
1087 .iter()
1088 .map(|c| format!("`{}`", c))
1089 .collect::<Vec<_>>()
1090 .join(", ");
1091 let ref_cols = fk
1092 .referenced_columns
1093 .iter()
1094 .map(|c| format!("`{}`", c))
1095 .collect::<Vec<_>>()
1096 .join(", ");
1097
1098 let mut s = format!(
1099 " CONSTRAINT `{}` FOREIGN KEY ({}) REFERENCES {} ({})",
1100 fk.name, cols, fk.referenced_table, ref_cols
1101 );
1102
1103 if fk.on_delete != "NO ACTION" {
1104 s.push_str(&format!(" ON DELETE {}", fk.on_delete));
1105 }
1106 if fk.on_update != "NO ACTION" {
1107 s.push_str(&format!(" ON UPDATE {}", fk.on_update));
1108 }
1109
1110 s
1111}
1112
1113pub fn infer_schema_from_pages(
1122 ts: &mut crate::innodb::tablespace::Tablespace,
1123) -> Result<InferredSchema, IdbError> {
1124 use crate::innodb::index::IndexHeader;
1125 use crate::innodb::page::FilHeader;
1126 use crate::innodb::page_types::PageType;
1127 use std::collections::BTreeMap;
1128
1129 let page_count = ts.page_count();
1130 let mut is_compact = true;
1131 let mut index_stats: BTreeMap<u64, (u64, u16)> = BTreeMap::new(); for page_num in 0..page_count {
1134 let page_data = match ts.read_page(page_num) {
1135 Ok(d) => d,
1136 Err(_) => continue,
1137 };
1138
1139 let header = match FilHeader::parse(&page_data) {
1140 Some(h) => h,
1141 None => continue,
1142 };
1143
1144 if header.page_type != PageType::Index {
1145 continue;
1146 }
1147
1148 let idx = match IndexHeader::parse(&page_data) {
1149 Some(h) => h,
1150 None => continue,
1151 };
1152
1153 if !idx.is_compact() {
1154 is_compact = false;
1155 }
1156
1157 let entry = index_stats.entry(idx.index_id).or_insert((0, 0));
1158 if idx.is_leaf() {
1159 entry.0 += 1;
1160 }
1161 if idx.level > entry.1 {
1162 entry.1 = idx.level;
1163 }
1164 }
1165
1166 let indexes = index_stats
1167 .into_iter()
1168 .map(|(index_id, (leaf_pages, max_level))| InferredIndex {
1169 index_id,
1170 leaf_pages,
1171 max_level,
1172 })
1173 .collect();
1174
1175 Ok(InferredSchema {
1176 source: "Inferred (no SDI metadata available)".to_string(),
1177 record_format: if is_compact { "COMPACT" } else { "REDUNDANT" }.to_string(),
1178 indexes,
1179 })
1180}
1181
1182#[cfg(test)]
1187mod tests {
1188 use super::*;
1189
1190 #[test]
1191 fn test_collation_name() {
1192 assert_eq!(collation_name(255), Some("utf8mb4_0900_ai_ci"));
1193 assert_eq!(collation_name(63), Some("binary"));
1194 assert_eq!(collation_name(45), Some("utf8mb4_general_ci"));
1195 assert_eq!(collation_name(46), Some("utf8mb4_bin"));
1196 assert_eq!(collation_name(33), Some("utf8mb3_general_ci"));
1197 assert_eq!(collation_name(0), None);
1198 }
1199
1200 #[test]
1201 fn test_charset_from_collation() {
1202 assert_eq!(charset_from_collation(255), Some("utf8mb4"));
1203 assert_eq!(charset_from_collation(63), Some("binary"));
1204 assert_eq!(charset_from_collation(8), Some("latin1"));
1205 assert_eq!(charset_from_collation(33), Some("utf8mb3"));
1206 assert_eq!(charset_from_collation(0), None);
1207 }
1208
1209 #[test]
1210 fn test_row_format_name() {
1211 assert_eq!(row_format_name(1), "FIXED");
1212 assert_eq!(row_format_name(2), "DYNAMIC");
1213 assert_eq!(row_format_name(3), "COMPRESSED");
1214 assert_eq!(row_format_name(99), "UNKNOWN");
1215 }
1216
1217 #[test]
1218 fn test_fk_rule_name() {
1219 assert_eq!(fk_rule_name(0), "NO ACTION");
1220 assert_eq!(fk_rule_name(1), "RESTRICT");
1221 assert_eq!(fk_rule_name(2), "CASCADE");
1222 assert_eq!(fk_rule_name(3), "SET NULL");
1223 assert_eq!(fk_rule_name(4), "SET DEFAULT");
1224 }
1225
1226 #[test]
1227 fn test_dd_type_to_sql_int() {
1228 let col = DdColumn {
1229 dd_type: 4,
1230 numeric_precision: 10,
1231 ..Default::default()
1232 };
1233 assert_eq!(dd_type_to_sql(&col), "int");
1234 }
1235
1236 #[test]
1237 fn test_dd_type_to_sql_varchar() {
1238 let col = DdColumn {
1239 dd_type: 16,
1240 char_length: 400,
1241 collation_id: 255, ..Default::default()
1243 };
1244 assert_eq!(dd_type_to_sql(&col), "varchar(100)");
1245 }
1246
1247 #[test]
1248 fn test_dd_type_to_sql_decimal() {
1249 let col = DdColumn {
1250 dd_type: 6,
1251 numeric_precision: 10,
1252 numeric_scale: 2,
1253 ..Default::default()
1254 };
1255 assert_eq!(dd_type_to_sql(&col), "decimal(10,2)");
1256 }
1257
1258 #[test]
1259 fn test_dd_type_to_sql_text() {
1260 let col = DdColumn {
1261 dd_type: 27,
1262 char_length: 65535,
1263 ..Default::default()
1264 };
1265 assert_eq!(dd_type_to_sql(&col), "text");
1266
1267 let col = DdColumn {
1268 dd_type: 27,
1269 char_length: 16777215,
1270 ..Default::default()
1271 };
1272 assert_eq!(dd_type_to_sql(&col), "mediumtext");
1273 }
1274
1275 #[test]
1276 fn test_format_mysql_version() {
1277 assert_eq!(format_mysql_version(90001), "9.0.1");
1278 assert_eq!(format_mysql_version(80040), "8.0.40");
1279 assert_eq!(format_mysql_version(0), "unknown");
1280 }
1281
1282 #[test]
1283 fn test_extract_schema_from_sdi_minimal() {
1284 let json = r#"{
1285 "mysqld_version_id": 90001,
1286 "dd_object_type": "Table",
1287 "dd_object": {
1288 "name": "users",
1289 "schema_ref": "myapp",
1290 "engine": "InnoDB",
1291 "collation_id": 255,
1292 "row_format": 2,
1293 "columns": [
1294 {
1295 "name": "id",
1296 "type": 4,
1297 "column_type_utf8": "int unsigned",
1298 "ordinal_position": 1,
1299 "hidden": 1,
1300 "is_nullable": false,
1301 "is_auto_increment": true,
1302 "has_no_default": true
1303 },
1304 {
1305 "name": "email",
1306 "type": 16,
1307 "column_type_utf8": "varchar(255)",
1308 "ordinal_position": 2,
1309 "hidden": 1,
1310 "is_nullable": false,
1311 "has_no_default": true
1312 },
1313 {
1314 "name": "DB_TRX_ID",
1315 "type": 10,
1316 "ordinal_position": 3,
1317 "hidden": 2
1318 },
1319 {
1320 "name": "DB_ROLL_PTR",
1321 "type": 9,
1322 "ordinal_position": 4,
1323 "hidden": 2
1324 }
1325 ],
1326 "indexes": [
1327 {
1328 "name": "PRIMARY",
1329 "type": 1,
1330 "hidden": false,
1331 "is_visible": true,
1332 "elements": [
1333 { "column_opx": 0, "hidden": false, "length": 4, "order": 2 },
1334 { "column_opx": 2, "hidden": true, "length": 4294967295, "order": 2 },
1335 { "column_opx": 3, "hidden": true, "length": 4294967295, "order": 2 }
1336 ]
1337 },
1338 {
1339 "name": "idx_email",
1340 "type": 2,
1341 "hidden": false,
1342 "is_visible": true,
1343 "elements": [
1344 { "column_opx": 1, "hidden": false, "length": 4294967295, "order": 2 },
1345 { "column_opx": 0, "hidden": true, "length": 4294967295, "order": 2 }
1346 ]
1347 }
1348 ],
1349 "foreign_keys": []
1350 }
1351 }"#;
1352
1353 let schema = extract_schema_from_sdi(json).unwrap();
1354 assert_eq!(schema.table_name, "users");
1355 assert_eq!(schema.schema_name, Some("myapp".to_string()));
1356 assert_eq!(schema.engine, "InnoDB");
1357 assert_eq!(schema.source, "sdi");
1358 assert_eq!(schema.columns.len(), 2);
1359 assert_eq!(schema.columns[0].name, "id");
1360 assert_eq!(schema.columns[0].column_type, "int unsigned");
1361 assert!(schema.columns[0].is_auto_increment);
1362 assert_eq!(schema.columns[1].name, "email");
1363 assert_eq!(schema.columns[1].column_type, "varchar(255)");
1364 assert_eq!(schema.indexes.len(), 2);
1365 assert_eq!(schema.indexes[0].index_type, "PRIMARY KEY");
1366 assert_eq!(schema.indexes[0].columns.len(), 1); assert_eq!(schema.indexes[1].index_type, "UNIQUE KEY");
1368 assert_eq!(schema.indexes[1].name, "idx_email");
1369 assert!(schema.ddl.contains("CREATE TABLE `users`"));
1370 assert!(schema
1371 .ddl
1372 .contains("`id` int unsigned NOT NULL AUTO_INCREMENT"));
1373 assert!(schema.ddl.contains("PRIMARY KEY (`id`)"));
1374 assert!(schema.ddl.contains("UNIQUE KEY `idx_email` (`email`)"));
1375 }
1376
1377 #[test]
1378 fn test_extract_schema_with_fk() {
1379 let json = r#"{
1380 "mysqld_version_id": 80040,
1381 "dd_object_type": "Table",
1382 "dd_object": {
1383 "name": "orders",
1384 "schema_ref": "shop",
1385 "engine": "InnoDB",
1386 "collation_id": 255,
1387 "row_format": 2,
1388 "columns": [
1389 {
1390 "name": "id",
1391 "type": 4,
1392 "column_type_utf8": "int",
1393 "ordinal_position": 1,
1394 "hidden": 1,
1395 "is_nullable": false,
1396 "is_auto_increment": true
1397 },
1398 {
1399 "name": "user_id",
1400 "type": 4,
1401 "column_type_utf8": "int",
1402 "ordinal_position": 2,
1403 "hidden": 1,
1404 "is_nullable": false
1405 }
1406 ],
1407 "indexes": [
1408 {
1409 "name": "PRIMARY",
1410 "type": 1,
1411 "hidden": false,
1412 "is_visible": true,
1413 "elements": [
1414 { "column_opx": 0, "hidden": false, "length": 4, "order": 2 }
1415 ]
1416 }
1417 ],
1418 "foreign_keys": [
1419 {
1420 "name": "fk_orders_user",
1421 "referenced_table_schema_name": "shop",
1422 "referenced_table_name": "users",
1423 "update_rule": 0,
1424 "delete_rule": 2,
1425 "elements": [
1426 { "column_opx": 1, "referenced_column_name": "id" }
1427 ]
1428 }
1429 ]
1430 }
1431 }"#;
1432
1433 let schema = extract_schema_from_sdi(json).unwrap();
1434 assert_eq!(schema.foreign_keys.len(), 1);
1435 let fk = &schema.foreign_keys[0];
1436 assert_eq!(fk.name, "fk_orders_user");
1437 assert_eq!(fk.columns, vec!["user_id"]);
1438 assert_eq!(fk.referenced_table, "`shop`.`users`");
1439 assert_eq!(fk.referenced_columns, vec!["id"]);
1440 assert_eq!(fk.on_delete, "CASCADE");
1441 assert_eq!(fk.on_update, "NO ACTION");
1442 assert!(schema.ddl.contains("CONSTRAINT `fk_orders_user` FOREIGN KEY (`user_id`) REFERENCES `shop`.`users` (`id`) ON DELETE CASCADE"));
1443 }
1444
1445 #[test]
1446 fn test_extract_schema_with_generated_column() {
1447 let json = r#"{
1448 "mysqld_version_id": 80040,
1449 "dd_object_type": "Table",
1450 "dd_object": {
1451 "name": "products",
1452 "schema_ref": "shop",
1453 "engine": "InnoDB",
1454 "collation_id": 255,
1455 "row_format": 2,
1456 "columns": [
1457 {
1458 "name": "price",
1459 "type": 6,
1460 "column_type_utf8": "decimal(10,2)",
1461 "ordinal_position": 1,
1462 "hidden": 1,
1463 "is_nullable": false
1464 },
1465 {
1466 "name": "tax",
1467 "type": 6,
1468 "column_type_utf8": "decimal(10,2)",
1469 "ordinal_position": 2,
1470 "hidden": 1,
1471 "is_nullable": true,
1472 "is_virtual": true,
1473 "generation_expression_utf8": "`price` * 0.1"
1474 }
1475 ],
1476 "indexes": [],
1477 "foreign_keys": []
1478 }
1479 }"#;
1480
1481 let schema = extract_schema_from_sdi(json).unwrap();
1482 assert_eq!(schema.columns.len(), 2);
1483 let tax = &schema.columns[1];
1484 assert_eq!(tax.generation_expression, Some("`price` * 0.1".to_string()));
1485 assert_eq!(tax.is_virtual, Some(true));
1486 assert!(schema
1487 .ddl
1488 .contains("GENERATED ALWAYS AS (`price` * 0.1) VIRTUAL"));
1489 }
1490
1491 #[test]
1492 fn test_ddl_generation_table_options() {
1493 let schema = TableSchema {
1494 schema_name: Some("mydb".to_string()),
1495 table_name: "test".to_string(),
1496 engine: "InnoDB".to_string(),
1497 row_format: Some("COMPRESSED".to_string()),
1498 collation: Some("utf8mb4_0900_ai_ci".to_string()),
1499 charset: Some("utf8mb4".to_string()),
1500 comment: None,
1501 mysql_version: Some("8.0.40".to_string()),
1502 source: "sdi".to_string(),
1503 columns: vec![ColumnDef {
1504 name: "id".to_string(),
1505 column_type: "int".to_string(),
1506 is_nullable: false,
1507 default_value: None,
1508 is_auto_increment: true,
1509 generation_expression: None,
1510 is_invisible: false,
1511 is_virtual: None,
1512 comment: None,
1513 }],
1514 indexes: vec![IndexDef {
1515 name: "PRIMARY".to_string(),
1516 index_type: "PRIMARY KEY".to_string(),
1517 columns: vec![IndexColumnDef {
1518 name: "id".to_string(),
1519 prefix_length: None,
1520 order: None,
1521 }],
1522 comment: None,
1523 is_visible: true,
1524 }],
1525 foreign_keys: vec![],
1526 ddl: String::new(),
1527 };
1528
1529 let ddl = generate_ddl(&schema);
1530 assert!(ddl.contains("ENGINE=InnoDB"));
1531 assert!(ddl.contains("DEFAULT CHARSET=utf8mb4"));
1532 assert!(ddl.contains("COLLATE=utf8mb4_0900_ai_ci"));
1533 assert!(ddl.contains("ROW_FORMAT=COMPRESSED"));
1534 }
1535
1536 #[test]
1537 fn test_hidden_column_filtering() {
1538 let json = r#"{
1539 "mysqld_version_id": 90001,
1540 "dd_object_type": "Table",
1541 "dd_object": {
1542 "name": "t",
1543 "engine": "InnoDB",
1544 "collation_id": 255,
1545 "row_format": 2,
1546 "columns": [
1547 { "name": "a", "type": 4, "column_type_utf8": "int", "ordinal_position": 1, "hidden": 1 },
1548 { "name": "b", "type": 4, "column_type_utf8": "int", "ordinal_position": 2, "hidden": 1 },
1549 { "name": "DB_TRX_ID", "type": 10, "ordinal_position": 3, "hidden": 2 },
1550 { "name": "DB_ROLL_PTR", "type": 9, "ordinal_position": 4, "hidden": 2 },
1551 { "name": "DB_ROW_ID", "type": 10, "ordinal_position": 5, "hidden": 2 }
1552 ],
1553 "indexes": [],
1554 "foreign_keys": []
1555 }
1556 }"#;
1557
1558 let schema = extract_schema_from_sdi(json).unwrap();
1559 assert_eq!(schema.columns.len(), 2);
1560 assert_eq!(schema.columns[0].name, "a");
1561 assert_eq!(schema.columns[1].name, "b");
1562 }
1563
1564 #[test]
1565 fn test_nullable_column_default_null() {
1566 let json = r#"{
1567 "mysqld_version_id": 90001,
1568 "dd_object_type": "Table",
1569 "dd_object": {
1570 "name": "t",
1571 "engine": "InnoDB",
1572 "collation_id": 255,
1573 "row_format": 2,
1574 "columns": [
1575 {
1576 "name": "notes",
1577 "type": 16,
1578 "column_type_utf8": "varchar(255)",
1579 "ordinal_position": 1,
1580 "hidden": 1,
1581 "is_nullable": true,
1582 "has_no_default": false,
1583 "default_value_utf8": "",
1584 "default_value_utf8_null": true
1585 }
1586 ],
1587 "indexes": [],
1588 "foreign_keys": []
1589 }
1590 }"#;
1591
1592 let schema = extract_schema_from_sdi(json).unwrap();
1593 assert_eq!(schema.columns[0].default_value, Some("NULL".to_string()));
1594 assert!(schema.ddl.contains("DEFAULT NULL"));
1595 }
1596
1597 #[test]
1598 fn test_empty_string_default() {
1599 let json = r#"{
1600 "mysqld_version_id": 90001,
1601 "dd_object_type": "Table",
1602 "dd_object": {
1603 "name": "t",
1604 "engine": "InnoDB",
1605 "collation_id": 255,
1606 "row_format": 2,
1607 "columns": [
1608 {
1609 "name": "tag",
1610 "type": 16,
1611 "column_type_utf8": "varchar(50)",
1612 "ordinal_position": 1,
1613 "hidden": 1,
1614 "is_nullable": false,
1615 "has_no_default": false,
1616 "default_value_utf8": "",
1617 "default_value_utf8_null": false
1618 }
1619 ],
1620 "indexes": [],
1621 "foreign_keys": []
1622 }
1623 }"#;
1624
1625 let schema = extract_schema_from_sdi(json).unwrap();
1626 assert_eq!(schema.columns[0].default_value, Some("''".to_string()));
1627 assert!(schema.ddl.contains("DEFAULT ''"));
1628 }
1629
1630 #[test]
1631 fn test_invisible_column() {
1632 let json = r#"{
1633 "mysqld_version_id": 80040,
1634 "dd_object_type": "Table",
1635 "dd_object": {
1636 "name": "t",
1637 "engine": "InnoDB",
1638 "collation_id": 255,
1639 "row_format": 2,
1640 "columns": [
1641 {
1642 "name": "id",
1643 "type": 4,
1644 "column_type_utf8": "int",
1645 "ordinal_position": 1,
1646 "hidden": 1,
1647 "is_nullable": false,
1648 "is_auto_increment": true,
1649 "has_no_default": true
1650 },
1651 {
1652 "name": "secret",
1653 "type": 16,
1654 "column_type_utf8": "varchar(100)",
1655 "ordinal_position": 2,
1656 "hidden": 4,
1657 "is_nullable": true,
1658 "has_no_default": false,
1659 "default_value_utf8_null": true
1660 },
1661 {
1662 "name": "DB_TRX_ID",
1663 "type": 10,
1664 "column_type_utf8": "",
1665 "ordinal_position": 3,
1666 "hidden": 2
1667 }
1668 ],
1669 "indexes": [],
1670 "foreign_keys": []
1671 }
1672 }"#;
1673
1674 let schema = extract_schema_from_sdi(json).unwrap();
1675 assert_eq!(schema.columns.len(), 2);
1677 assert_eq!(schema.columns[0].name, "id");
1678 assert!(!schema.columns[0].is_invisible);
1679 assert_eq!(schema.columns[1].name, "secret");
1680 assert!(schema.columns[1].is_invisible);
1681 assert!(schema.ddl.contains("/*!80023 INVISIBLE */"));
1683 assert!(!schema.ddl.contains("DB_TRX_ID"));
1685 }
1686
1687 #[test]
1688 fn test_index_desc_order() {
1689 let json = r#"{
1690 "mysqld_version_id": 80040,
1691 "dd_object_type": "Table",
1692 "dd_object": {
1693 "name": "t",
1694 "engine": "InnoDB",
1695 "collation_id": 255,
1696 "row_format": 2,
1697 "columns": [
1698 { "name": "a", "type": 4, "column_type_utf8": "int", "ordinal_position": 1, "hidden": 1 },
1699 { "name": "b", "type": 4, "column_type_utf8": "int", "ordinal_position": 2, "hidden": 1 }
1700 ],
1701 "indexes": [
1702 {
1703 "name": "idx_b_desc",
1704 "type": 3,
1705 "hidden": false,
1706 "is_visible": true,
1707 "elements": [
1708 { "column_opx": 1, "hidden": false, "length": 4294967295, "order": 1 }
1709 ]
1710 }
1711 ],
1712 "foreign_keys": []
1713 }
1714 }"#;
1715
1716 let schema = extract_schema_from_sdi(json).unwrap();
1717 assert_eq!(schema.indexes[0].columns[0].order, Some("DESC".to_string()));
1718 assert!(schema.ddl.contains("`b` DESC"));
1719 }
1720}