1use std::path::PathBuf;
6use sz_orm_model::DbError;
7use sz_orm_model::DbType;
8
9pub struct Migration {
10 pub version: String,
11 pub name: String,
12 pub sql_up: String,
13 pub sql_down: String,
14 pub batch: i32,
15 pub executed_at: Option<chrono::DateTime<chrono::Utc>>,
16}
17
18impl Migration {
19 pub fn new(version: &str, name: &str, sql_up: &str, sql_down: &str) -> Self {
20 Self {
21 version: version.to_string(),
22 name: name.to_string(),
23 sql_up: sql_up.to_string(),
24 sql_down: sql_down.to_string(),
25 batch: 0,
26 executed_at: None,
27 }
28 }
29
30 pub fn with_batch(mut self, batch: i32) -> Self {
31 self.batch = batch;
32 self
33 }
34
35 pub fn with_executed_at(mut self, time: chrono::DateTime<chrono::Utc>) -> Self {
36 self.executed_at = Some(time);
37 self
38 }
39}
40
41impl std::fmt::Debug for Migration {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.debug_struct("Migration")
44 .field("version", &self.version)
45 .field("name", &self.name)
46 .field("batch", &self.batch)
47 .finish()
48 }
49}
50
51pub trait MigrationResolver: Send + Sync {
52 fn resolve(&self, db_type: DbType) -> Result<Vec<Migration>, DbError>;
53}
54
55pub struct FileMigrationResolver {
56 pub path: PathBuf,
57}
58
59impl FileMigrationResolver {
60 pub fn new(path: PathBuf) -> Self {
61 Self { path }
62 }
63}
64
65impl MigrationResolver for FileMigrationResolver {
66 fn resolve(&self, db_type: DbType) -> Result<Vec<Migration>, DbError> {
67 let mut migrations = Vec::new();
68
69 let entries = match std::fs::read_dir(&self.path) {
71 Ok(entries) => entries,
72 Err(e) => {
73 return Err(DbError::MigrationError(format!(
74 "Cannot read migration directory {}: {}",
75 self.path.display(),
76 e
77 )));
78 }
79 };
80
81 let _ = db_type; let mut sql_files: Vec<std::path::PathBuf> = Vec::new();
85 for entry in entries {
86 let entry = entry.map_err(|e| {
87 DbError::MigrationError(format!("Cannot read directory entry: {}", e))
88 })?;
89 let path = entry.path();
90 if path.extension().and_then(|s| s.to_str()) == Some("sql") {
91 sql_files.push(path);
92 }
93 }
94
95 sql_files.sort();
97
98 let mut version_map: std::collections::HashMap<
101 String,
102 (Option<String>, Option<String>, String),
103 > = std::collections::HashMap::new();
104
105 for path in sql_files {
106 let filename = match path.file_stem().and_then(|s| s.to_str()) {
107 Some(name) => name.to_string(),
108 None => continue,
109 };
110
111 let content = std::fs::read_to_string(&path).map_err(|e| {
112 DbError::MigrationError(format!(
113 "Cannot read migration file {}: {}",
114 path.display(),
115 e
116 ))
117 })?;
118
119 if filename.ends_with("_up") {
121 let base = &filename[..filename.len() - 3];
122 let (version, name) = parse_migration_filename(base);
123 let entry = version_map
124 .entry(version.clone())
125 .or_insert((None, None, name));
126 entry.0 = Some(content);
127 } else if filename.ends_with("_down") {
128 let base = &filename[..filename.len() - 5];
129 let (version, name) = parse_migration_filename(base);
130 let entry = version_map
131 .entry(version.clone())
132 .or_insert((None, None, name));
133 entry.1 = Some(content);
134 } else {
135 let (version, name) = parse_migration_filename(&filename);
137 let entry = version_map
138 .entry(version.clone())
139 .or_insert((None, None, name));
140 if entry.0.is_none() {
141 entry.0 = Some(content);
142 }
143 }
144 }
145
146 type VersionEntry = (Option<String>, Option<String>, String);
148 let mut sorted_versions: Vec<(String, VersionEntry)> = version_map.into_iter().collect();
149 sorted_versions.sort_by(|a, b| a.0.cmp(&b.0));
150
151 for (version, (sql_up, sql_down, name)) in sorted_versions {
152 let migration = Migration::new(
153 &version,
154 &name,
155 sql_up.unwrap_or_default().as_str(),
156 sql_down.unwrap_or_default().as_str(),
157 );
158 migrations.push(migration);
159 }
160
161 Ok(migrations)
162 }
163}
164
165fn parse_migration_filename(filename: &str) -> (String, String) {
167 if let Some(underscore_pos) = filename.find('_') {
168 let version = filename[..underscore_pos].to_string();
169 let name = filename[underscore_pos + 1..].to_string();
170 (version, name)
171 } else {
172 (filename.to_string(), filename.to_string())
174 }
175}
176
177pub struct MigrationContext {
178 pub table_name: String,
179 pub connection: Option<Box<dyn sz_orm_pool::Connection>>,
180 pub db_type: Option<DbType>,
182}
183
184impl Default for MigrationContext {
185 fn default() -> Self {
186 Self {
187 table_name: "__migrations".to_string(),
188 connection: None,
189 db_type: None,
190 }
191 }
192}
193
194impl MigrationContext {
195 pub fn with_db_type(mut self, db_type: DbType) -> Self {
197 self.db_type = Some(db_type);
198 self
199 }
200}
201
202fn validate_migration_version(version: &str) -> Result<(), DbError> {
210 if version.is_empty() || version.len() > 255 {
211 return Err(DbError::InvalidInput(format!(
212 "invalid migration version: empty or too long (max 255 chars): {:?}",
213 version
214 )));
215 }
216 let valid = version
218 .chars()
219 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.');
220 if !valid {
221 return Err(DbError::InvalidInput(format!(
222 "invalid migration version: only ASCII alphanumeric, underscore, hyphen, dot allowed, got {:?}",
223 version
224 )));
225 }
226 if version.contains("--") {
228 return Err(DbError::InvalidInput(format!(
229 "invalid migration version: SQL comment sequence '--' not allowed: {:?}",
230 version
231 )));
232 }
233 Ok(())
234}
235
236fn supports_ddl_transactions(db_type: DbType) -> bool {
245 matches!(db_type, DbType::PostgreSQL | DbType::Sqlite)
246}
247
248#[derive(Debug, Clone, PartialEq)]
249pub enum MigrationDirection {
250 Up,
251 Down,
252}
253
254pub struct Migrator {
255 context: MigrationContext,
256 migrations: Vec<Migration>,
257}
258
259impl Migrator {
260 pub fn new(context: MigrationContext) -> Self {
261 Self {
262 context,
263 migrations: Vec::new(),
264 }
265 }
266
267 pub fn add_migration(mut self, migration: Migration) -> Self {
268 self.migrations.push(migration);
269 self
270 }
271
272 pub fn add_migrations(mut self, migrations: Vec<Migration>) -> Self {
273 self.migrations.extend(migrations);
274 self
275 }
276
277 pub fn get_migrations(&self) -> &Vec<Migration> {
278 &self.migrations
279 }
280
281 pub fn get_pending_migrations(&self) -> Vec<&Migration> {
282 self.migrations.iter().filter(|m| m.batch == 0).collect()
283 }
284
285 pub fn get_applied_migrations(&self) -> Vec<&Migration> {
286 self.migrations.iter().filter(|m| m.batch > 0).collect()
287 }
288
289 pub fn latest_version(&self) -> Option<&str> {
290 self.migrations.last().map(|m| m.version.as_str())
291 }
292
293 pub fn find_migration(&self, version: &str) -> Option<&Migration> {
294 self.migrations.iter().find(|m| m.version == version)
295 }
296
297 pub fn check_version_conflicts(&self) -> Result<(), DbError> {
302 let mut seen = std::collections::HashSet::new();
303 for m in &self.migrations {
304 if !seen.insert(&m.version) {
305 return Err(DbError::MigrationError(format!(
306 "迁移版本冲突:版本号 '{}' 重复定义",
307 m.version
308 )));
309 }
310 }
311 Ok(())
312 }
313
314 pub fn build_create_migrations_table_sql(&self) -> String {
333 let table = &self.context.table_name;
334 let timestamp_default = match self.context.db_type {
335 Some(DbType::SqlServer) => "DATETIME DEFAULT GETDATE()",
336 Some(DbType::Oracle) => "TIMESTAMP DEFAULT CURRENT_TIMESTAMP",
337 _ => "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP",
338 };
339 format!(
340 "CREATE TABLE IF NOT EXISTS {} (\
341 version VARCHAR(255) NOT NULL PRIMARY KEY, \
342 name VARCHAR(255), \
343 batch INTEGER NOT NULL, \
344 executed_at {ts}\
345 )",
346 table,
347 ts = timestamp_default
348 )
349 }
350
351 async fn ensure_migrations_table(&mut self) -> Result<(), DbError> {
358 let sql = self.build_create_migrations_table_sql();
359 if let Some(ref mut conn) = self.context.connection {
360 conn.execute(&sql).await?;
361 }
362 Ok(())
363 }
364
365 async fn load_applied_migrations(
372 &mut self,
373 ) -> Result<std::collections::HashMap<String, i32>, DbError> {
374 let mut applied = std::collections::HashMap::new();
375 if let Some(ref mut conn) = self.context.connection {
376 let sql = format!("SELECT version, batch FROM {}", self.context.table_name);
377 let rows = conn.query(&sql).await?;
378 for row in rows {
379 if let Some(sz_orm_model::Value::String(version)) = row.get("version") {
380 let batch = match row.get("batch") {
381 Some(sz_orm_model::Value::I32(b)) => *b,
382 Some(sz_orm_model::Value::I64(b)) => *b as i32,
383 _ => 0,
384 };
385 applied.insert(version.clone(), batch);
386 }
387 }
388 }
389 Ok(applied)
390 }
391
392 async fn sync_state_from_db(&mut self) -> Result<(), DbError> {
396 let applied = self.load_applied_migrations().await?;
397 for migration in &mut self.migrations {
398 if let Some(batch) = applied.get(&migration.version) {
399 migration.batch = *batch;
400 migration.executed_at = Some(chrono::Utc::now());
401 } else {
402 migration.batch = 0;
403 migration.executed_at = None;
404 }
405 }
406 Ok(())
407 }
408
409 async fn record_migration(
413 &mut self,
414 version: &str,
415 name: &str,
416 batch: i32,
417 ) -> Result<(), DbError> {
418 if let Some(ref mut conn) = self.context.connection {
419 validate_migration_version(version)?;
422 if name.contains('\'') || name.contains(';') {
424 return Err(DbError::MigrationError(format!("非法迁移名称: {}", name)));
425 }
426 let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S");
427 let sql = format!(
428 "INSERT INTO {} (version, name, batch, executed_at) VALUES ('{}', '{}', {}, '{}')",
429 self.context.table_name, version, name, batch, now
430 );
431 conn.execute(&sql).await?;
432 }
433 Ok(())
434 }
435
436 async fn remove_migration(&mut self, version: &str) -> Result<(), DbError> {
440 if let Some(ref mut conn) = self.context.connection {
441 validate_migration_version(version)?;
442 let sql = format!(
443 "DELETE FROM {} WHERE version = '{}'",
444 self.context.table_name, version
445 );
446 conn.execute(&sql).await?;
447 }
448 Ok(())
449 }
450
451 pub async fn migrate(&mut self) -> Result<Vec<String>, DbError> {
460 self.check_version_conflicts()?;
462
463 self.ensure_migrations_table().await?;
465
466 self.sync_state_from_db().await?;
468
469 let mut applied = Vec::new();
470 let current_batch = self.migrations.iter().map(|m| m.batch).max().unwrap_or(0) + 1;
471
472 let pending_indices: Vec<usize> = self
474 .migrations
475 .iter()
476 .enumerate()
477 .filter(|(_, m)| m.batch == 0)
478 .map(|(idx, _)| idx)
479 .collect();
480
481 if pending_indices.is_empty() {
482 return Ok(applied);
483 }
484
485 let use_transaction = self
487 .context
488 .db_type
489 .map(supports_ddl_transactions)
490 .unwrap_or(false);
491
492 if use_transaction {
494 if let Some(ref mut conn) = self.context.connection {
495 conn.begin_transaction().await?;
496 }
497 }
498
499 for migration_idx in &pending_indices {
501 let sql_up = self.migrations[*migration_idx].sql_up.clone();
502 let version = self.migrations[*migration_idx].version.clone();
503 let name = self.migrations[*migration_idx].name.clone();
504
505 let exec_result = async {
506 if let Some(ref mut conn) = self.context.connection {
507 if !sql_up.is_empty() {
508 conn.execute(&sql_up).await?;
509 }
510 }
511 Ok::<(), DbError>(())
512 }
513 .await;
514
515 if let Err(e) = exec_result {
516 if use_transaction {
518 if let Some(ref mut conn) = self.context.connection {
519 let _ = conn.rollback().await;
520 }
521 }
522 return Err(e);
523 }
524
525 let now = chrono::Utc::now();
527 self.migrations[*migration_idx].batch = current_batch;
528 self.migrations[*migration_idx].executed_at = Some(now);
529
530 if let Err(e) = self.record_migration(&version, &name, current_batch).await {
532 if use_transaction {
534 if let Some(ref mut conn) = self.context.connection {
535 let _ = conn.rollback().await;
536 }
537 }
538 return Err(e);
539 }
540
541 applied.push(version);
542 }
543
544 if use_transaction {
546 if let Some(ref mut conn) = self.context.connection {
547 conn.commit().await?;
548 }
549 }
550
551 Ok(applied)
552 }
553
554 pub async fn rollback(&mut self, version: &str) -> Result<(), DbError> {
558 self.ensure_migrations_table().await?;
560 self.sync_state_from_db().await?;
561
562 let migration_idx = self
563 .migrations
564 .iter()
565 .position(|m| m.version == version)
566 .ok_or_else(|| DbError::MigrationError(format!("Migration {} not found", version)))?;
567
568 if self.migrations[migration_idx].batch == 0 {
569 return Err(DbError::MigrationError(format!(
570 "Migration {} not applied",
571 version
572 )));
573 }
574
575 let sql_down = self.migrations[migration_idx].sql_down.clone();
576
577 if let Some(ref mut conn) = self.context.connection {
578 if !sql_down.is_empty() {
579 conn.execute(&sql_down).await?;
580 }
581 }
582
583 self.migrations[migration_idx].batch = 0;
584 self.migrations[migration_idx].executed_at = None;
585
586 self.remove_migration(version).await?;
588
589 Ok(())
590 }
591
592 pub async fn up(&mut self, target_version: Option<&str>) -> Result<Vec<String>, DbError> {
597 self.check_version_conflicts()?;
599
600 self.ensure_migrations_table().await?;
602 self.sync_state_from_db().await?;
603
604 let mut applied = Vec::new();
605 let current_batch = self.migrations.iter().map(|m| m.batch).max().unwrap_or(0) + 1;
606
607 let pending: Vec<(usize, String, String)> = self
609 .migrations
610 .iter()
611 .enumerate()
612 .filter(|(_, m)| m.batch == 0)
613 .take_while(|(_, m)| {
614 if let Some(target) = target_version {
615 m.version.as_str() <= target
616 } else {
617 true
618 }
619 })
620 .map(|(idx, m)| (idx, m.version.clone(), m.name.clone()))
621 .collect();
622
623 for (idx, version, name) in pending {
624 let sql_up = self.migrations[idx].sql_up.clone();
625 if let Some(ref mut conn) = self.context.connection {
626 if !sql_up.is_empty() {
627 conn.execute(&sql_up).await?;
628 }
629 }
630
631 self.migrations[idx].batch = current_batch;
632 self.migrations[idx].executed_at = Some(chrono::Utc::now());
633
634 self.record_migration(&version, &name, current_batch)
636 .await?;
637
638 applied.push(version);
639 }
640
641 Ok(applied)
642 }
643
644 pub async fn down(&mut self, target_version: Option<&str>) -> Result<Vec<String>, DbError> {
648 self.check_version_conflicts()?;
650
651 self.ensure_migrations_table().await?;
653 self.sync_state_from_db().await?;
654
655 let mut rolled_back = Vec::new();
656
657 let mut indices: Vec<usize> = (0..self.migrations.len()).collect();
659 indices.reverse();
660
661 let pending_rollback: Vec<(usize, String)> = indices
662 .iter()
663 .filter(|&&idx| self.migrations[idx].batch > 0)
664 .take_while(|&&idx| {
665 if let Some(target) = target_version {
666 self.migrations[idx].version.as_str() > target
667 } else {
668 true
669 }
670 })
671 .map(|&idx| (idx, self.migrations[idx].version.clone()))
672 .collect();
673
674 for (idx, version) in pending_rollback {
675 let sql_down = self.migrations[idx].sql_down.clone();
676 if let Some(ref mut conn) = self.context.connection {
677 if !sql_down.is_empty() {
678 conn.execute(&sql_down).await?;
679 }
680 }
681
682 self.migrations[idx].batch = 0;
683 self.migrations[idx].executed_at = None;
684
685 self.remove_migration(&version).await?;
687
688 rolled_back.push(version);
689 }
690
691 Ok(rolled_back)
692 }
693
694 pub async fn reset(&mut self) -> Result<Vec<String>, DbError> {
696 self.down(None).await?;
698 self.migrate().await
700 }
701
702 pub async fn refresh(&mut self) -> Result<Vec<String>, DbError> {
704 self.reset().await
705 }
706
707 pub fn progress(&self) -> MigrationProgress {
709 let total = self.migrations.len();
710 let applied = self.migrations.iter().filter(|m| m.batch > 0).count();
711 MigrationProgress::new(total, applied)
712 }
713}
714
715#[derive(Debug, Clone)]
716pub struct MigrationProgress {
717 pub total: usize,
718 pub applied: usize,
719 pub pending: usize,
720 pub current_batch: i32,
721}
722
723impl MigrationProgress {
724 pub fn new(total: usize, applied: usize) -> Self {
725 Self {
726 total,
727 applied,
728 pending: total - applied,
729 current_batch: 0,
730 }
731 }
732
733 pub fn percent_complete(&self) -> f64 {
734 if self.total == 0 {
735 return 100.0;
736 }
737 (self.applied as f64 / self.total as f64) * 100.0
738 }
739}
740
741pub struct SchemaBuilder {
742 table_name: String,
743 columns: Vec<ColumnDef>,
744 indexes: Vec<IndexDef>,
745 foreign_keys: Vec<ForeignKeyDef>,
746 if_not_exists: bool,
747}
748
749impl SchemaBuilder {
750 pub fn new(table_name: &str) -> Self {
751 Self {
752 table_name: table_name.to_string(),
753 columns: Vec::new(),
754 indexes: Vec::new(),
755 foreign_keys: Vec::new(),
756 if_not_exists: true,
757 }
758 }
759
760 pub fn add_column(mut self, column: ColumnDef) -> Self {
761 self.columns.push(column);
762 self
763 }
764
765 pub fn add_index(mut self, index: IndexDef) -> Self {
766 self.indexes.push(index);
767 self
768 }
769
770 pub fn add_foreign_key(mut self, fk: ForeignKeyDef) -> Self {
771 self.foreign_keys.push(fk);
772 self
773 }
774
775 pub fn if_not_exists(mut self, value: bool) -> Self {
776 self.if_not_exists = value;
777 self
778 }
779
780 pub fn build(&self, db_type: DbType) -> Result<String, DbError> {
781 let mut sql = String::new();
782 sql.push_str("CREATE TABLE ");
783 if self.if_not_exists {
784 sql.push_str("IF NOT EXISTS ");
785 }
786 sql.push_str(&self.table_name);
787 sql.push_str(" (");
788
789 let col_defs: Vec<String> = self.columns.iter().map(|c| c.build(db_type)).collect();
790 sql.push_str(&col_defs.join(", "));
791
792 for index in &self.indexes {
793 sql.push_str(", ");
794 sql.push_str(&index.build(db_type));
795 }
796
797 for fk in &self.foreign_keys {
798 sql.push_str(", ");
799 sql.push_str(&fk.build(db_type)?);
800 }
801
802 sql.push(')');
803 Ok(sql)
804 }
805}
806
807#[derive(Debug, Clone)]
808pub struct ColumnDef {
809 pub name: String,
810 pub col_type: String,
811 pub length: Option<usize>,
812 pub precision: Option<(u32, u32)>,
813 pub nullable: bool,
814 pub default: Option<String>,
815 pub auto_increment: bool,
816 pub unique: bool,
817 pub comment: Option<String>,
818}
819
820impl ColumnDef {
821 pub fn new(name: &str, col_type: &str) -> Self {
822 Self {
823 name: name.to_string(),
824 col_type: col_type.to_string(),
825 length: None,
826 precision: None,
827 nullable: true,
828 default: None,
829 auto_increment: false,
830 unique: false,
831 comment: None,
832 }
833 }
834
835 pub fn not_null(mut self) -> Self {
836 self.nullable = false;
837 self
838 }
839
840 pub fn default(mut self, value: &str) -> Self {
841 self.default = Some(value.to_string());
842 self
843 }
844
845 pub fn auto_increment(mut self) -> Self {
846 self.auto_increment = true;
847 self
848 }
849
850 pub fn unique(mut self) -> Self {
851 self.unique = true;
852 self
853 }
854
855 pub fn comment(mut self, comment: &str) -> Self {
856 self.comment = Some(comment.to_string());
857 self
858 }
859
860 pub fn length(mut self, len: usize) -> Self {
861 self.length = Some(len);
862 self
863 }
864
865 fn build(&self, db_type: DbType) -> String {
866 let mut sql = format!("{} {}", self.name, self.col_type);
867 if let Some(len) = self.length {
868 if matches!(db_type, DbType::MySQL) {
869 sql.push_str(&format!("({})", len));
870 }
871 }
872 if self.auto_increment {
873 match db_type {
874 DbType::MySQL => sql.push_str(" AUTO_INCREMENT"),
875 DbType::PostgreSQL => sql.push_str(" GENERATED BY DEFAULT AS IDENTITY"),
876 DbType::Sqlite => sql.push_str(" AUTOINCREMENT"),
877 _ => {}
878 }
879 }
880 if !self.nullable {
881 sql.push_str(" NOT NULL");
882 }
883 if let Some(ref def) = self.default {
884 sql.push_str(&format!(" DEFAULT {}", def));
885 }
886 if self.unique {
887 sql.push_str(" UNIQUE");
888 }
889 sql
890 }
891}
892
893#[derive(Debug, Clone)]
894pub struct IndexDef {
895 pub name: String,
896 pub columns: Vec<String>,
897 pub unique: bool,
898 pub index_type: Option<String>,
899}
900
901impl IndexDef {
902 pub fn new(name: &str, columns: Vec<&str>) -> Self {
903 Self {
904 name: name.to_string(),
905 columns: columns.into_iter().map(|s| s.to_string()).collect(),
906 unique: false,
907 index_type: None,
908 }
909 }
910
911 pub fn unique(mut self) -> Self {
912 self.unique = true;
913 self
914 }
915
916 fn build(&self, _db_type: DbType) -> String {
917 let unique_str = if self.unique { "UNIQUE " } else { "" };
918 format!(
919 "{}KEY {} ({})",
920 unique_str,
921 self.name,
922 self.columns.join(", ")
923 )
924 }
925}
926
927#[derive(Debug, Clone)]
928pub struct ForeignKeyDef {
929 pub name: String,
930 pub column: String,
931 pub referenced_table: String,
932 pub referenced_column: String,
933 pub on_delete: Option<String>,
934 pub on_update: Option<String>,
935}
936
937impl ForeignKeyDef {
938 pub fn new(name: &str, column: &str, referenced_table: &str, referenced_column: &str) -> Self {
939 Self {
940 name: name.to_string(),
941 column: column.to_string(),
942 referenced_table: referenced_table.to_string(),
943 referenced_column: referenced_column.to_string(),
944 on_delete: None,
945 on_update: None,
946 }
947 }
948
949 pub fn on_delete(mut self, action: &str) -> Self {
950 self.on_delete = Some(action.to_string());
951 self
952 }
953
954 pub fn on_update(mut self, action: &str) -> Self {
955 self.on_update = Some(action.to_string());
956 self
957 }
958
959 fn build(&self, _db_type: DbType) -> Result<String, DbError> {
960 sz_orm_model::sql_safety::validate_identifier(&self.name, "foreign key constraint name")?;
962 sz_orm_model::sql_safety::validate_identifier(&self.column, "foreign key column")?;
963 sz_orm_model::sql_safety::validate_identifier(
964 &self.referenced_table,
965 "foreign key referenced table",
966 )?;
967 sz_orm_model::sql_safety::validate_identifier(
968 &self.referenced_column,
969 "foreign key referenced column",
970 )?;
971 if let Some(ref on_delete) = self.on_delete {
972 sz_orm_model::sql_safety::validate_fk_action(on_delete)?;
973 }
974 if let Some(ref on_update) = self.on_update {
975 sz_orm_model::sql_safety::validate_fk_action(on_update)?;
976 }
977 let mut sql = format!(
978 "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({})",
979 self.name, self.column, self.referenced_table, self.referenced_column
980 );
981 if let Some(ref on_delete) = self.on_delete {
982 sql.push_str(&format!(" ON DELETE {}", on_delete.trim().to_uppercase()));
983 }
984 if let Some(ref on_update) = self.on_update {
985 sql.push_str(&format!(" ON UPDATE {}", on_update.trim().to_uppercase()));
986 }
987 Ok(sql)
988 }
989}
990
991#[cfg(test)]
992mod tests {
993 use super::*;
994
995 #[test]
996 fn test_migration_new() {
997 let m = Migration::new("001", "create_users", "CREATE TABLE...", "DROP TABLE...");
998 assert_eq!(m.version, "001");
999 assert_eq!(m.name, "create_users");
1000 }
1001
1002 #[test]
1003 fn test_migration_with_batch() {
1004 let m = Migration::new("001", "create_users", "UP", "DOWN").with_batch(1);
1005 assert_eq!(m.batch, 1);
1006 }
1007
1008 #[test]
1009 fn test_migrator_latest_version() {
1010 let ctx = MigrationContext::default();
1011 let migrator = Migrator::new(ctx)
1012 .add_migration(Migration::new("001", "v1", "UP", "DOWN"))
1013 .add_migration(Migration::new("002", "v2", "UP", "DOWN"));
1014
1015 assert_eq!(migrator.latest_version(), Some("002"));
1016 }
1017
1018 #[test]
1019 fn test_migrator_find_migration() {
1020 let ctx = MigrationContext::default();
1021 let migrator =
1022 Migrator::new(ctx).add_migration(Migration::new("001", "create_users", "UP", "DOWN"));
1023
1024 assert!(migrator.find_migration("001").is_some());
1025 assert!(migrator.find_migration("999").is_none());
1026 }
1027
1028 #[test]
1029 fn test_column_def() {
1030 let col = ColumnDef::new("id", "INT").not_null().auto_increment();
1031 assert_eq!(col.name, "id");
1032 assert!(!col.nullable);
1033 assert!(col.auto_increment);
1034 }
1035
1036 #[test]
1037 fn test_column_build_mysql() {
1038 let col = ColumnDef::new("id", "INT").not_null();
1039 let sql = col.build(DbType::MySQL);
1040 assert!(sql.contains("NOT NULL"));
1041 }
1042
1043 #[test]
1044 fn test_index_build() {
1045 let idx = IndexDef::new("idx_name", vec!["name"]).unique();
1046 let sql = idx.build(DbType::MySQL);
1047 assert!(sql.contains("UNIQUE KEY"));
1048 }
1049
1050 #[test]
1051 fn test_foreign_key_build() {
1052 let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id").on_delete("CASCADE");
1053 let sql = fk.build(DbType::MySQL).unwrap();
1054 assert!(sql.contains("FOREIGN KEY"));
1055 assert!(sql.contains("ON DELETE CASCADE"));
1056 }
1057
1058 #[test]
1059 fn test_foreign_key_build_normalizes_action_case() {
1060 let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id").on_delete("cascade");
1062 let sql = fk.build(DbType::MySQL).unwrap();
1063 assert!(sql.contains("ON DELETE CASCADE"));
1064 }
1065
1066 #[test]
1067 fn test_foreign_key_rejects_sql_injection_in_column() {
1068 let fk = ForeignKeyDef::new("fk_user", "user_id; DROP TABLE users", "users", "id");
1069 let result = fk.build(DbType::MySQL);
1070 assert!(result.is_err());
1071 }
1072
1073 #[test]
1074 fn test_foreign_key_rejects_sql_injection_in_ref_table() {
1075 let fk = ForeignKeyDef::new("fk_user", "user_id", "users; DROP TABLE users", "id");
1076 let result = fk.build(DbType::MySQL);
1077 assert!(result.is_err());
1078 }
1079
1080 #[test]
1081 fn test_foreign_key_rejects_sql_injection_in_on_delete() {
1082 let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id")
1083 .on_delete("CASCADE; DROP TABLE users");
1084 let result = fk.build(DbType::MySQL);
1085 assert!(result.is_err());
1086 }
1087
1088 #[test]
1089 fn test_foreign_key_rejects_invalid_on_update_action() {
1090 let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id").on_update("EVIL_ACTION");
1091 let result = fk.build(DbType::MySQL);
1092 assert!(result.is_err());
1093 }
1094
1095 #[test]
1096 fn test_schema_builder() {
1097 let schema = SchemaBuilder::new("users")
1098 .add_column(ColumnDef::new("id", "INT").not_null().auto_increment())
1099 .add_column(ColumnDef::new("name", "VARCHAR").length(255));
1100
1101 let sql = schema.build(DbType::MySQL).unwrap();
1102 assert!(sql.contains("CREATE TABLE"));
1103 assert!(sql.contains("users"));
1104 }
1105
1106 #[test]
1107 fn test_migration_progress() {
1108 let progress = MigrationProgress::new(10, 4);
1109 assert_eq!(progress.pending, 6);
1110 assert!((progress.percent_complete() - 40.0).abs() < 0.01);
1111 }
1112}