1use crate::db_type::DbType;
6use crate::error::DbError;
7use std::path::PathBuf;
8
9pub struct Migration {
11 pub version: String,
13 pub name: String,
15 pub sql_up: String,
17 pub sql_down: String,
19 pub batch: i32,
21 pub executed_at: Option<chrono::DateTime<chrono::Utc>>,
23}
24
25impl Migration {
26 pub fn new(version: &str, name: &str, sql_up: &str, sql_down: &str) -> Self {
28 Self {
29 version: version.to_string(),
30 name: name.to_string(),
31 sql_up: sql_up.to_string(),
32 sql_down: sql_down.to_string(),
33 batch: 0,
34 executed_at: None,
35 }
36 }
37
38 pub fn with_batch(mut self, batch: i32) -> Self {
40 self.batch = batch;
41 self
42 }
43
44 pub fn with_executed_at(mut self, time: chrono::DateTime<chrono::Utc>) -> Self {
46 self.executed_at = Some(time);
47 self
48 }
49}
50
51impl std::fmt::Debug for Migration {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.debug_struct("Migration")
54 .field("version", &self.version)
55 .field("name", &self.name)
56 .field("batch", &self.batch)
57 .finish()
58 }
59}
60
61pub trait MigrationResolver: Send + Sync {
63 fn resolve(&self, db_type: DbType) -> Result<Vec<Migration>, DbError>;
65}
66
67pub struct FileMigrationResolver {
69 pub path: PathBuf,
71}
72
73impl FileMigrationResolver {
74 pub fn new(path: PathBuf) -> Self {
76 Self { path }
77 }
78}
79
80impl MigrationResolver for FileMigrationResolver {
81 fn resolve(&self, db_type: DbType) -> Result<Vec<Migration>, DbError> {
82 let mut migrations = Vec::new();
83
84 let entries = match std::fs::read_dir(&self.path) {
86 Ok(entries) => entries,
87 Err(e) => {
88 return Err(DbError::MigrationError(format!(
89 "Cannot read migration directory {}: {}",
90 self.path.display(),
91 e
92 )));
93 }
94 };
95
96 let _ = db_type; let mut sql_files: Vec<std::path::PathBuf> = Vec::new();
100 for entry in entries {
101 let entry = entry.map_err(|e| {
102 DbError::MigrationError(format!("Cannot read directory entry: {}", e))
103 })?;
104 let path = entry.path();
105 if path.extension().and_then(|s| s.to_str()) == Some("sql") {
106 sql_files.push(path);
107 }
108 }
109
110 sql_files.sort();
112
113 let mut version_map: std::collections::HashMap<
116 String,
117 (Option<String>, Option<String>, String),
118 > = std::collections::HashMap::new();
119
120 for path in sql_files {
121 let filename = match path.file_stem().and_then(|s| s.to_str()) {
122 Some(name) => name.to_string(),
123 None => continue,
124 };
125
126 let content = std::fs::read_to_string(&path).map_err(|e| {
127 DbError::MigrationError(format!(
128 "Cannot read migration file {}: {}",
129 path.display(),
130 e
131 ))
132 })?;
133
134 if filename.ends_with("_up") {
136 let base = &filename[..filename.len() - 3];
137 let (version, name) = parse_migration_filename(base);
138 let entry = version_map
139 .entry(version.clone())
140 .or_insert((None, None, name));
141 entry.0 = Some(content);
142 } else if filename.ends_with("_down") {
143 let base = &filename[..filename.len() - 5];
144 let (version, name) = parse_migration_filename(base);
145 let entry = version_map
146 .entry(version.clone())
147 .or_insert((None, None, name));
148 entry.1 = Some(content);
149 } else {
150 let (version, name) = parse_migration_filename(&filename);
152 let entry = version_map
153 .entry(version.clone())
154 .or_insert((None, None, name));
155 if entry.0.is_none() {
156 entry.0 = Some(content);
157 }
158 }
159 }
160
161 type VersionEntry = (Option<String>, Option<String>, String);
163 let mut sorted_versions: Vec<(String, VersionEntry)> = version_map.into_iter().collect();
164 sorted_versions.sort_by(|a, b| a.0.cmp(&b.0));
165
166 for (version, (sql_up, sql_down, name)) in sorted_versions {
167 let migration = Migration::new(
168 &version,
169 &name,
170 sql_up.unwrap_or_default().as_str(),
171 sql_down.unwrap_or_default().as_str(),
172 );
173 migrations.push(migration);
174 }
175
176 Ok(migrations)
177 }
178}
179
180fn parse_migration_filename(filename: &str) -> (String, String) {
182 if let Some(underscore_pos) = filename.find('_') {
183 let version = filename[..underscore_pos].to_string();
184 let name = filename[underscore_pos + 1..].to_string();
185 (version, name)
186 } else {
187 (filename.to_string(), filename.to_string())
189 }
190}
191
192pub struct MigrationContext {
194 pub table_name: String,
196 pub connection: Option<Box<dyn crate::pool::Connection>>,
198 pub db_type: Option<DbType>,
200}
201
202impl Default for MigrationContext {
203 fn default() -> Self {
204 Self {
205 table_name: "__migrations".to_string(),
206 connection: None,
207 db_type: None,
208 }
209 }
210}
211
212impl MigrationContext {
213 pub fn with_db_type(mut self, db_type: DbType) -> Self {
215 self.db_type = Some(db_type);
216 self
217 }
218}
219
220fn validate_migration_version(version: &str) -> Result<(), DbError> {
228 if version.is_empty() || version.len() > 255 {
229 return Err(DbError::InvalidInput(format!(
230 "invalid migration version: empty or too long (max 255 chars): {:?}",
231 version
232 )));
233 }
234 let valid = version
236 .chars()
237 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.');
238 if !valid {
239 return Err(DbError::InvalidInput(format!(
240 "invalid migration version: only ASCII alphanumeric, underscore, hyphen, dot allowed, got {:?}",
241 version
242 )));
243 }
244 if version.contains("--") {
246 return Err(DbError::InvalidInput(format!(
247 "invalid migration version: SQL comment sequence '--' not allowed: {:?}",
248 version
249 )));
250 }
251 Ok(())
252}
253
254fn supports_ddl_transactions(db_type: DbType) -> bool {
263 matches!(db_type, DbType::PostgreSQL | DbType::Sqlite)
264}
265
266#[derive(Debug, Clone, PartialEq)]
268pub enum MigrationDirection {
269 Up,
271 Down,
273}
274
275pub struct Migrator {
277 context: MigrationContext,
278 migrations: Vec<Migration>,
279}
280
281impl Migrator {
282 pub fn new(context: MigrationContext) -> Self {
284 Self {
285 context,
286 migrations: Vec::new(),
287 }
288 }
289
290 pub fn add_migration(mut self, migration: Migration) -> Self {
292 self.migrations.push(migration);
293 self
294 }
295
296 pub fn add_migrations(mut self, migrations: Vec<Migration>) -> Self {
298 self.migrations.extend(migrations);
299 self
300 }
301
302 pub fn get_migrations(&self) -> &Vec<Migration> {
304 &self.migrations
305 }
306
307 pub fn get_pending_migrations(&self) -> Vec<&Migration> {
309 self.migrations.iter().filter(|m| m.batch == 0).collect()
310 }
311
312 pub fn get_applied_migrations(&self) -> Vec<&Migration> {
314 self.migrations.iter().filter(|m| m.batch > 0).collect()
315 }
316
317 pub fn latest_version(&self) -> Option<&str> {
319 self.migrations.last().map(|m| m.version.as_str())
320 }
321
322 pub fn find_migration(&self, version: &str) -> Option<&Migration> {
324 self.migrations.iter().find(|m| m.version == version)
325 }
326
327 pub fn check_version_conflicts(&self) -> Result<(), DbError> {
332 let mut seen = std::collections::HashSet::new();
333 for m in &self.migrations {
334 if !seen.insert(&m.version) {
335 return Err(DbError::MigrationError(format!(
336 "迁移版本冲突:版本号 '{}' 重复定义",
337 m.version
338 )));
339 }
340 }
341 Ok(())
342 }
343
344 pub fn build_create_migrations_table_sql(&self) -> String {
363 let table = &self.context.table_name;
364 let timestamp_default = match self.context.db_type {
365 Some(DbType::SqlServer) => "DATETIME DEFAULT GETDATE()",
366 Some(DbType::Oracle) => "TIMESTAMP DEFAULT CURRENT_TIMESTAMP",
367 _ => "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP",
368 };
369 format!(
370 "CREATE TABLE IF NOT EXISTS {} (\
371 version VARCHAR(255) NOT NULL PRIMARY KEY, \
372 name VARCHAR(255), \
373 batch INTEGER NOT NULL, \
374 executed_at {ts}\
375 )",
376 table,
377 ts = timestamp_default
378 )
379 }
380
381 async fn ensure_migrations_table(&mut self) -> Result<(), DbError> {
388 let sql = self.build_create_migrations_table_sql();
389 if let Some(ref mut conn) = self.context.connection {
390 conn.execute(&sql).await?;
391 }
392 Ok(())
393 }
394
395 async fn load_applied_migrations(
402 &mut self,
403 ) -> Result<std::collections::HashMap<String, i32>, DbError> {
404 let mut applied = std::collections::HashMap::new();
405 if let Some(ref mut conn) = self.context.connection {
406 let sql = format!("SELECT version, batch FROM {}", self.context.table_name);
407 let rows = conn.query(&sql).await?;
408 for row in rows {
409 if let Some(crate::Value::String(version)) = row.get("version") {
410 let batch = match row.get("batch") {
411 Some(crate::Value::I32(b)) => *b,
412 Some(crate::Value::I64(b)) => *b as i32,
413 _ => 0,
414 };
415 applied.insert(version.clone(), batch);
416 }
417 }
418 }
419 Ok(applied)
420 }
421
422 async fn sync_state_from_db(&mut self) -> Result<(), DbError> {
426 let applied = self.load_applied_migrations().await?;
427 for migration in &mut self.migrations {
428 if let Some(batch) = applied.get(&migration.version) {
429 migration.batch = *batch;
430 migration.executed_at = Some(chrono::Utc::now());
431 } else {
432 migration.batch = 0;
433 migration.executed_at = None;
434 }
435 }
436 Ok(())
437 }
438
439 async fn record_migration(
443 &mut self,
444 version: &str,
445 name: &str,
446 batch: i32,
447 ) -> Result<(), DbError> {
448 if let Some(ref mut conn) = self.context.connection {
449 validate_migration_version(version)?;
452 if name.contains('\'') || name.contains(';') {
454 return Err(DbError::MigrationError(format!("非法迁移名称: {}", name)));
455 }
456 let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S");
457 let sql = format!(
458 "INSERT INTO {} (version, name, batch, executed_at) VALUES ('{}', '{}', {}, '{}')",
459 self.context.table_name, version, name, batch, now
460 );
461 conn.execute(&sql).await?;
462 }
463 Ok(())
464 }
465
466 async fn remove_migration(&mut self, version: &str) -> Result<(), DbError> {
470 if let Some(ref mut conn) = self.context.connection {
471 validate_migration_version(version)?;
472 let sql = format!(
473 "DELETE FROM {} WHERE version = '{}'",
474 self.context.table_name, version
475 );
476 conn.execute(&sql).await?;
477 }
478 Ok(())
479 }
480
481 pub async fn migrate(&mut self) -> Result<Vec<String>, DbError> {
490 self.check_version_conflicts()?;
492
493 self.ensure_migrations_table().await?;
495
496 self.sync_state_from_db().await?;
498
499 let mut applied = Vec::new();
500 let current_batch = self.migrations.iter().map(|m| m.batch).max().unwrap_or(0) + 1;
501
502 let pending_indices: Vec<usize> = self
504 .migrations
505 .iter()
506 .enumerate()
507 .filter(|(_, m)| m.batch == 0)
508 .map(|(idx, _)| idx)
509 .collect();
510
511 if pending_indices.is_empty() {
512 return Ok(applied);
513 }
514
515 let use_transaction = self
517 .context
518 .db_type
519 .map(supports_ddl_transactions)
520 .unwrap_or(false);
521
522 if use_transaction {
524 if let Some(ref mut conn) = self.context.connection {
525 conn.begin_transaction().await?;
526 }
527 }
528
529 for migration_idx in &pending_indices {
531 let sql_up = self.migrations[*migration_idx].sql_up.clone();
532 let version = self.migrations[*migration_idx].version.clone();
533 let name = self.migrations[*migration_idx].name.clone();
534
535 let exec_result = async {
536 if let Some(ref mut conn) = self.context.connection {
537 if !sql_up.is_empty() {
538 conn.execute(&sql_up).await?;
539 }
540 }
541 Ok::<(), DbError>(())
542 }
543 .await;
544
545 if let Err(e) = exec_result {
546 if use_transaction {
548 if let Some(ref mut conn) = self.context.connection {
549 let _ = conn.rollback().await;
550 }
551 }
552 return Err(e);
553 }
554
555 let now = chrono::Utc::now();
557 self.migrations[*migration_idx].batch = current_batch;
558 self.migrations[*migration_idx].executed_at = Some(now);
559
560 if let Err(e) = self.record_migration(&version, &name, current_batch).await {
562 if use_transaction {
564 if let Some(ref mut conn) = self.context.connection {
565 let _ = conn.rollback().await;
566 }
567 }
568 return Err(e);
569 }
570
571 applied.push(version);
572 }
573
574 if use_transaction {
576 if let Some(ref mut conn) = self.context.connection {
577 conn.commit().await?;
578 }
579 }
580
581 Ok(applied)
582 }
583
584 pub async fn rollback(&mut self, version: &str) -> Result<(), DbError> {
588 self.ensure_migrations_table().await?;
590 self.sync_state_from_db().await?;
591
592 let migration_idx = self
593 .migrations
594 .iter()
595 .position(|m| m.version == version)
596 .ok_or_else(|| DbError::MigrationError(format!("Migration {} not found", version)))?;
597
598 if self.migrations[migration_idx].batch == 0 {
599 return Err(DbError::MigrationError(format!(
600 "Migration {} not applied",
601 version
602 )));
603 }
604
605 let sql_down = self.migrations[migration_idx].sql_down.clone();
606
607 if let Some(ref mut conn) = self.context.connection {
608 if !sql_down.is_empty() {
609 conn.execute(&sql_down).await?;
610 }
611 }
612
613 self.migrations[migration_idx].batch = 0;
614 self.migrations[migration_idx].executed_at = None;
615
616 self.remove_migration(version).await?;
618
619 Ok(())
620 }
621
622 pub async fn up(&mut self, target_version: Option<&str>) -> Result<Vec<String>, DbError> {
627 self.check_version_conflicts()?;
629
630 self.ensure_migrations_table().await?;
632 self.sync_state_from_db().await?;
633
634 let mut applied = Vec::new();
635 let current_batch = self.migrations.iter().map(|m| m.batch).max().unwrap_or(0) + 1;
636
637 let pending: Vec<(usize, String, String)> = self
639 .migrations
640 .iter()
641 .enumerate()
642 .filter(|(_, m)| m.batch == 0)
643 .take_while(|(_, m)| {
644 if let Some(target) = target_version {
645 m.version.as_str() <= target
646 } else {
647 true
648 }
649 })
650 .map(|(idx, m)| (idx, m.version.clone(), m.name.clone()))
651 .collect();
652
653 for (idx, version, name) in pending {
654 let sql_up = self.migrations[idx].sql_up.clone();
655 if let Some(ref mut conn) = self.context.connection {
656 if !sql_up.is_empty() {
657 conn.execute(&sql_up).await?;
658 }
659 }
660
661 self.migrations[idx].batch = current_batch;
662 self.migrations[idx].executed_at = Some(chrono::Utc::now());
663
664 self.record_migration(&version, &name, current_batch)
666 .await?;
667
668 applied.push(version);
669 }
670
671 Ok(applied)
672 }
673
674 pub async fn down(&mut self, target_version: Option<&str>) -> Result<Vec<String>, DbError> {
678 self.check_version_conflicts()?;
680
681 self.ensure_migrations_table().await?;
683 self.sync_state_from_db().await?;
684
685 let mut rolled_back = Vec::new();
686
687 let mut indices: Vec<usize> = (0..self.migrations.len()).collect();
689 indices.reverse();
690
691 let pending_rollback: Vec<(usize, String)> = indices
692 .iter()
693 .filter(|&&idx| self.migrations[idx].batch > 0)
694 .take_while(|&&idx| {
695 if let Some(target) = target_version {
696 self.migrations[idx].version.as_str() > target
697 } else {
698 true
699 }
700 })
701 .map(|&idx| (idx, self.migrations[idx].version.clone()))
702 .collect();
703
704 for (idx, version) in pending_rollback {
705 let sql_down = self.migrations[idx].sql_down.clone();
706 if let Some(ref mut conn) = self.context.connection {
707 if !sql_down.is_empty() {
708 conn.execute(&sql_down).await?;
709 }
710 }
711
712 self.migrations[idx].batch = 0;
713 self.migrations[idx].executed_at = None;
714
715 self.remove_migration(&version).await?;
717
718 rolled_back.push(version);
719 }
720
721 Ok(rolled_back)
722 }
723
724 pub async fn reset(&mut self) -> Result<Vec<String>, DbError> {
726 self.down(None).await?;
728 self.migrate().await
730 }
731
732 pub async fn refresh(&mut self) -> Result<Vec<String>, DbError> {
734 self.reset().await
735 }
736
737 pub fn progress(&self) -> MigrationProgress {
739 let total = self.migrations.len();
740 let applied = self.migrations.iter().filter(|m| m.batch > 0).count();
741 MigrationProgress::new(total, applied)
742 }
743}
744
745#[derive(Debug, Clone)]
747pub struct MigrationProgress {
748 pub total: usize,
750 pub applied: usize,
752 pub pending: usize,
754 pub current_batch: i32,
756}
757
758impl MigrationProgress {
759 pub fn new(total: usize, applied: usize) -> Self {
761 Self {
762 total,
763 applied,
764 pending: total - applied,
765 current_batch: 0,
766 }
767 }
768
769 pub fn percent_complete(&self) -> f64 {
771 if self.total == 0 {
772 return 100.0;
773 }
774 (self.applied as f64 / self.total as f64) * 100.0
775 }
776}
777
778pub struct SchemaBuilder {
780 table_name: String,
781 columns: Vec<ColumnDef>,
782 indexes: Vec<IndexDef>,
783 foreign_keys: Vec<ForeignKeyDef>,
784 if_not_exists: bool,
785}
786
787impl SchemaBuilder {
788 pub fn new(table_name: &str) -> Self {
790 Self {
791 table_name: table_name.to_string(),
792 columns: Vec::new(),
793 indexes: Vec::new(),
794 foreign_keys: Vec::new(),
795 if_not_exists: true,
796 }
797 }
798
799 pub fn add_column(mut self, column: ColumnDef) -> Self {
801 self.columns.push(column);
802 self
803 }
804
805 pub fn add_index(mut self, index: IndexDef) -> Self {
807 self.indexes.push(index);
808 self
809 }
810
811 pub fn add_foreign_key(mut self, fk: ForeignKeyDef) -> Self {
813 self.foreign_keys.push(fk);
814 self
815 }
816
817 pub fn if_not_exists(mut self, value: bool) -> Self {
819 self.if_not_exists = value;
820 self
821 }
822
823 pub fn build(&self, db_type: DbType) -> Result<String, DbError> {
825 let mut sql = String::new();
826 sql.push_str("CREATE TABLE ");
827 if self.if_not_exists {
828 sql.push_str("IF NOT EXISTS ");
829 }
830 sql.push_str(&self.table_name);
831 sql.push_str(" (");
832
833 let col_defs: Vec<String> = self.columns.iter().map(|c| c.build(db_type)).collect();
834 sql.push_str(&col_defs.join(", "));
835
836 for index in &self.indexes {
837 sql.push_str(", ");
838 sql.push_str(&index.build(db_type));
839 }
840
841 for fk in &self.foreign_keys {
842 sql.push_str(", ");
843 sql.push_str(&fk.build(db_type)?);
844 }
845
846 sql.push(')');
847 Ok(sql)
848 }
849}
850
851#[derive(Debug, Clone)]
853pub struct ColumnDef {
854 pub name: String,
856 pub col_type: String,
858 pub length: Option<usize>,
860 pub precision: Option<(u32, u32)>,
862 pub nullable: bool,
864 pub default: Option<String>,
866 pub auto_increment: bool,
868 pub unique: bool,
870 pub comment: Option<String>,
872}
873
874impl ColumnDef {
875 pub fn new(name: &str, col_type: &str) -> Self {
877 Self {
878 name: name.to_string(),
879 col_type: col_type.to_string(),
880 length: None,
881 precision: None,
882 nullable: true,
883 default: None,
884 auto_increment: false,
885 unique: false,
886 comment: None,
887 }
888 }
889
890 pub fn not_null(mut self) -> Self {
892 self.nullable = false;
893 self
894 }
895
896 pub fn default(mut self, value: &str) -> Self {
898 self.default = Some(value.to_string());
899 self
900 }
901
902 pub fn auto_increment(mut self) -> Self {
904 self.auto_increment = true;
905 self
906 }
907
908 pub fn unique(mut self) -> Self {
910 self.unique = true;
911 self
912 }
913
914 pub fn comment(mut self, comment: &str) -> Self {
916 self.comment = Some(comment.to_string());
917 self
918 }
919
920 pub fn length(mut self, len: usize) -> Self {
922 self.length = Some(len);
923 self
924 }
925
926 fn build(&self, db_type: DbType) -> String {
927 let mut sql = format!("{} {}", self.name, self.col_type);
928 if let Some(len) = self.length {
929 if matches!(db_type, DbType::MySQL) {
930 sql.push_str(&format!("({})", len));
931 }
932 }
933 if self.auto_increment {
934 match db_type {
935 DbType::MySQL => sql.push_str(" AUTO_INCREMENT"),
936 DbType::PostgreSQL => sql.push_str(" GENERATED BY DEFAULT AS IDENTITY"),
937 DbType::Sqlite => sql.push_str(" AUTOINCREMENT"),
938 _ => {}
939 }
940 }
941 if !self.nullable {
942 sql.push_str(" NOT NULL");
943 }
944 if let Some(ref def) = self.default {
945 sql.push_str(&format!(" DEFAULT {}", def));
946 }
947 if self.unique {
948 sql.push_str(" UNIQUE");
949 }
950 sql
951 }
952}
953
954#[derive(Debug, Clone)]
956pub struct IndexDef {
957 pub name: String,
959 pub columns: Vec<String>,
961 pub unique: bool,
963 pub index_type: Option<String>,
965}
966
967impl IndexDef {
968 pub fn new(name: &str, columns: Vec<&str>) -> Self {
970 Self {
971 name: name.to_string(),
972 columns: columns.into_iter().map(|s| s.to_string()).collect(),
973 unique: false,
974 index_type: None,
975 }
976 }
977
978 pub fn unique(mut self) -> Self {
980 self.unique = true;
981 self
982 }
983
984 fn build(&self, _db_type: DbType) -> String {
985 let unique_str = if self.unique { "UNIQUE " } else { "" };
986 format!(
987 "{}KEY {} ({})",
988 unique_str,
989 self.name,
990 self.columns.join(", ")
991 )
992 }
993}
994
995#[derive(Debug, Clone)]
997pub struct ForeignKeyDef {
998 pub name: String,
1000 pub column: String,
1002 pub referenced_table: String,
1004 pub referenced_column: String,
1006 pub on_delete: Option<String>,
1008 pub on_update: Option<String>,
1010}
1011
1012impl ForeignKeyDef {
1013 pub fn new(name: &str, column: &str, referenced_table: &str, referenced_column: &str) -> Self {
1015 Self {
1016 name: name.to_string(),
1017 column: column.to_string(),
1018 referenced_table: referenced_table.to_string(),
1019 referenced_column: referenced_column.to_string(),
1020 on_delete: None,
1021 on_update: None,
1022 }
1023 }
1024
1025 pub fn on_delete(mut self, action: &str) -> Self {
1027 self.on_delete = Some(action.to_string());
1028 self
1029 }
1030
1031 pub fn on_update(mut self, action: &str) -> Self {
1033 self.on_update = Some(action.to_string());
1034 self
1035 }
1036
1037 fn build(&self, _db_type: DbType) -> Result<String, DbError> {
1038 crate::sql_safety::validate_identifier(&self.name, "foreign key constraint name")?;
1040 crate::sql_safety::validate_identifier(&self.column, "foreign key column")?;
1041 crate::sql_safety::validate_identifier(
1042 &self.referenced_table,
1043 "foreign key referenced table",
1044 )?;
1045 crate::sql_safety::validate_identifier(
1046 &self.referenced_column,
1047 "foreign key referenced column",
1048 )?;
1049 if let Some(ref on_delete) = self.on_delete {
1050 crate::sql_safety::validate_fk_action(on_delete)?;
1051 }
1052 if let Some(ref on_update) = self.on_update {
1053 crate::sql_safety::validate_fk_action(on_update)?;
1054 }
1055 let mut sql = format!(
1056 "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({})",
1057 self.name, self.column, self.referenced_table, self.referenced_column
1058 );
1059 if let Some(ref on_delete) = self.on_delete {
1060 sql.push_str(&format!(" ON DELETE {}", on_delete.trim().to_uppercase()));
1061 }
1062 if let Some(ref on_update) = self.on_update {
1063 sql.push_str(&format!(" ON UPDATE {}", on_update.trim().to_uppercase()));
1064 }
1065 Ok(sql)
1066 }
1067}
1068
1069#[cfg(test)]
1070mod tests {
1071 use super::*;
1072
1073 #[test]
1074 fn test_migration_new() {
1075 let m = Migration::new("001", "create_users", "CREATE TABLE...", "DROP TABLE...");
1076 assert_eq!(m.version, "001");
1077 assert_eq!(m.name, "create_users");
1078 }
1079
1080 #[test]
1081 fn test_migration_with_batch() {
1082 let m = Migration::new("001", "create_users", "UP", "DOWN").with_batch(1);
1083 assert_eq!(m.batch, 1);
1084 }
1085
1086 #[test]
1087 fn test_migrator_latest_version() {
1088 let ctx = MigrationContext::default();
1089 let migrator = Migrator::new(ctx)
1090 .add_migration(Migration::new("001", "v1", "UP", "DOWN"))
1091 .add_migration(Migration::new("002", "v2", "UP", "DOWN"));
1092
1093 assert_eq!(migrator.latest_version(), Some("002"));
1094 }
1095
1096 #[test]
1097 fn test_migrator_find_migration() {
1098 let ctx = MigrationContext::default();
1099 let migrator =
1100 Migrator::new(ctx).add_migration(Migration::new("001", "create_users", "UP", "DOWN"));
1101
1102 assert!(migrator.find_migration("001").is_some());
1103 assert!(migrator.find_migration("999").is_none());
1104 }
1105
1106 #[test]
1107 fn test_column_def() {
1108 let col = ColumnDef::new("id", "INT").not_null().auto_increment();
1109 assert_eq!(col.name, "id");
1110 assert!(!col.nullable);
1111 assert!(col.auto_increment);
1112 }
1113
1114 #[test]
1115 fn test_column_build_mysql() {
1116 let col = ColumnDef::new("id", "INT").not_null();
1117 let sql = col.build(DbType::MySQL);
1118 assert!(sql.contains("NOT NULL"));
1119 }
1120
1121 #[test]
1122 fn test_index_build() {
1123 let idx = IndexDef::new("idx_name", vec!["name"]).unique();
1124 let sql = idx.build(DbType::MySQL);
1125 assert!(sql.contains("UNIQUE KEY"));
1126 }
1127
1128 #[test]
1129 fn test_foreign_key_build() {
1130 let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id").on_delete("CASCADE");
1131 let sql = fk.build(DbType::MySQL).unwrap();
1132 assert!(sql.contains("FOREIGN KEY"));
1133 assert!(sql.contains("ON DELETE CASCADE"));
1134 }
1135
1136 #[test]
1137 fn test_foreign_key_build_normalizes_action_case() {
1138 let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id").on_delete("cascade");
1140 let sql = fk.build(DbType::MySQL).unwrap();
1141 assert!(sql.contains("ON DELETE CASCADE"));
1142 }
1143
1144 #[test]
1145 fn test_foreign_key_rejects_sql_injection_in_column() {
1146 let fk = ForeignKeyDef::new("fk_user", "user_id; DROP TABLE users", "users", "id");
1147 let result = fk.build(DbType::MySQL);
1148 assert!(result.is_err());
1149 }
1150
1151 #[test]
1152 fn test_foreign_key_rejects_sql_injection_in_ref_table() {
1153 let fk = ForeignKeyDef::new("fk_user", "user_id", "users; DROP TABLE users", "id");
1154 let result = fk.build(DbType::MySQL);
1155 assert!(result.is_err());
1156 }
1157
1158 #[test]
1159 fn test_foreign_key_rejects_sql_injection_in_on_delete() {
1160 let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id")
1161 .on_delete("CASCADE; DROP TABLE users");
1162 let result = fk.build(DbType::MySQL);
1163 assert!(result.is_err());
1164 }
1165
1166 #[test]
1167 fn test_foreign_key_rejects_invalid_on_update_action() {
1168 let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id").on_update("EVIL_ACTION");
1169 let result = fk.build(DbType::MySQL);
1170 assert!(result.is_err());
1171 }
1172
1173 #[test]
1174 fn test_schema_builder() {
1175 let schema = SchemaBuilder::new("users")
1176 .add_column(ColumnDef::new("id", "INT").not_null().auto_increment())
1177 .add_column(ColumnDef::new("name", "VARCHAR").length(255));
1178
1179 let sql = schema.build(DbType::MySQL).unwrap();
1180 assert!(sql.contains("CREATE TABLE"));
1181 assert!(sql.contains("users"));
1182 }
1183
1184 #[test]
1185 fn test_migration_progress() {
1186 let progress = MigrationProgress::new(10, 4);
1187 assert_eq!(progress.pending, 6);
1188 assert!((progress.percent_complete() - 40.0).abs() < 0.01);
1189 }
1190}