aster_forge_db_migration/
index.rs1use sea_orm_migration::sea_orm::sea_query::{Alias, Index, IndexDropStatement};
2use sea_orm_migration::sea_orm::{ConnectionTrait, DatabaseBackend, DbErr, Statement};
3
4pub async fn drop_index_if_exists<C>(
13 db: &C,
14 table_name: &str,
15 index_name: &str,
16) -> Result<(), DbErr>
17where
18 C: ConnectionTrait,
19{
20 let backend = db.get_database_backend();
21 if backend == DatabaseBackend::MySql && !mysql_index_exists(db, table_name, index_name).await? {
22 return Ok(());
23 }
24
25 db.execute(&drop_index_for_backend(backend, table_name, index_name))
26 .await?;
27 Ok(())
28}
29
30pub async fn rename_mysql_index_if_exists<C>(
39 db: &C,
40 table_name: &str,
41 old_index_name: &str,
42 new_index_name: &str,
43) -> Result<(), DbErr>
44where
45 C: ConnectionTrait,
46{
47 validate_mysql_identifier(table_name)?;
48 validate_mysql_identifier(old_index_name)?;
49 validate_mysql_identifier(new_index_name)?;
50
51 if db.get_database_backend() != DatabaseBackend::MySql {
52 return Err(DbErr::Custom(
53 "rename_mysql_index_if_exists requires a MySQL connection".to_string(),
54 ));
55 }
56
57 if !mysql_index_exists(db, table_name, old_index_name).await?
58 || mysql_index_exists(db, table_name, new_index_name).await?
59 {
60 return Ok(());
61 }
62
63 db.execute_unprepared(&format!(
64 "ALTER TABLE `{table_name}` RENAME INDEX `{old_index_name}` TO `{new_index_name}`"
65 ))
66 .await?;
67 Ok(())
68}
69
70fn drop_index_for_backend(
71 backend: DatabaseBackend,
72 table_name: &str,
73 index_name: &str,
74) -> IndexDropStatement {
75 let mut statement = Index::drop();
76 statement
77 .name(index_name.to_owned())
78 .table(Alias::new(table_name));
79 if backend != DatabaseBackend::MySql {
80 statement.if_exists();
81 }
82 statement.clone()
83}
84
85async fn mysql_index_exists<C>(db: &C, table_name: &str, index_name: &str) -> Result<bool, DbErr>
86where
87 C: ConnectionTrait,
88{
89 let row = db
90 .query_one_raw(Statement::from_sql_and_values(
91 DatabaseBackend::MySql,
92 "SELECT 1 FROM information_schema.statistics \
93 WHERE table_schema = DATABASE() AND table_name = ? AND index_name = ? LIMIT 1",
94 [table_name.into(), index_name.into()],
95 ))
96 .await?;
97 Ok(row.is_some())
98}
99
100fn validate_mysql_identifier(identifier: &str) -> Result<(), DbErr> {
101 if !identifier.is_empty()
102 && identifier
103 .bytes()
104 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
105 {
106 return Ok(());
107 }
108
109 Err(DbErr::Custom(format!(
110 "invalid MySQL migration identifier: {identifier:?}"
111 )))
112}
113
114#[cfg(test)]
115mod tests {
116 use sea_orm_migration::sea_orm::sea_query::{
117 MysqlQueryBuilder, PostgresQueryBuilder, SqliteQueryBuilder,
118 };
119 use sea_orm_migration::sea_orm::{ConnectionTrait, Database, DatabaseBackend};
120
121 use super::{
122 drop_index_for_backend, drop_index_if_exists, rename_mysql_index_if_exists,
123 validate_mysql_identifier,
124 };
125
126 #[test]
127 fn drop_index_sql_respects_backend_capabilities() {
128 let mysql = drop_index_for_backend(DatabaseBackend::MySql, "example_table", "idx_example")
129 .to_string(MysqlQueryBuilder);
130 assert_eq!(mysql, "DROP INDEX `idx_example` ON `example_table`");
131
132 let postgres =
133 drop_index_for_backend(DatabaseBackend::Postgres, "example_table", "idx_example")
134 .to_string(PostgresQueryBuilder);
135 assert_eq!(postgres, "DROP INDEX IF EXISTS \"idx_example\"");
136
137 let sqlite =
138 drop_index_for_backend(DatabaseBackend::Sqlite, "example_table", "idx_example")
139 .to_string(SqliteQueryBuilder);
140 assert_eq!(sqlite, "DROP INDEX IF EXISTS \"idx_example\"");
141 }
142
143 #[test]
144 fn mysql_identifier_validation_rejects_raw_sql_fragments() {
145 assert!(validate_mysql_identifier("idx_example_2026").is_ok());
146 assert!(validate_mysql_identifier("").is_err());
147 assert!(validate_mysql_identifier("idx-example").is_err());
148 assert!(validate_mysql_identifier("idx` DROP TABLE users").is_err());
149 }
150
151 #[tokio::test]
152 async fn drop_index_if_exists_is_idempotent_on_sqlite() {
153 let db = Database::connect("sqlite::memory:").await.unwrap();
154 db.execute_unprepared("CREATE TABLE example_table (id INTEGER PRIMARY KEY)")
155 .await
156 .unwrap();
157 db.execute_unprepared("CREATE INDEX idx_example ON example_table (id)")
158 .await
159 .unwrap();
160
161 drop_index_if_exists(&db, "example_table", "idx_example")
162 .await
163 .unwrap();
164 drop_index_if_exists(&db, "example_table", "idx_example")
165 .await
166 .unwrap();
167 }
168
169 #[tokio::test]
170 async fn mysql_index_rename_rejects_non_mysql_connections() {
171 let db = Database::connect("sqlite::memory:").await.unwrap();
172 let error = rename_mysql_index_if_exists(
173 &db,
174 "example_table",
175 "idx_example_old",
176 "idx_example_new",
177 )
178 .await
179 .unwrap_err();
180
181 assert!(error.to_string().contains("requires a MySQL connection"));
182 }
183}