1use crate::schema::{ColumnDef, ColumnName, ColumnType, TableConstraint, TableName};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
7#[serde(rename_all = "snake_case")]
8pub struct MigrationPlan {
9 pub comment: Option<String>,
10 #[serde(default)]
11 pub created_at: Option<String>,
12 pub version: u32,
13 pub actions: Vec<MigrationAction>,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
17#[serde(tag = "type", rename_all = "snake_case")]
18pub enum MigrationAction {
19 CreateTable {
20 table: TableName,
21 columns: Vec<ColumnDef>,
22 constraints: Vec<TableConstraint>,
23 },
24 DeleteTable {
25 table: TableName,
26 },
27 AddColumn {
28 table: TableName,
29 column: Box<ColumnDef>,
30 fill_with: Option<String>,
32 },
33 RenameColumn {
34 table: TableName,
35 from: ColumnName,
36 to: ColumnName,
37 },
38 DeleteColumn {
39 table: TableName,
40 column: ColumnName,
41 },
42 ModifyColumnType {
43 table: TableName,
44 column: ColumnName,
45 new_type: ColumnType,
46 },
47 AddConstraint {
48 table: TableName,
49 constraint: TableConstraint,
50 },
51 RemoveConstraint {
52 table: TableName,
53 constraint: TableConstraint,
54 },
55 RenameTable {
56 from: TableName,
57 to: TableName,
58 },
59 RawSql {
60 sql: String,
61 },
62}
63
64impl fmt::Display for MigrationAction {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 match self {
67 MigrationAction::CreateTable { table, .. } => {
68 write!(f, "CreateTable: {}", table)
69 }
70 MigrationAction::DeleteTable { table } => {
71 write!(f, "DeleteTable: {}", table)
72 }
73 MigrationAction::AddColumn { table, column, .. } => {
74 write!(f, "AddColumn: {}.{}", table, column.name)
75 }
76 MigrationAction::RenameColumn { table, from, to } => {
77 write!(f, "RenameColumn: {}.{} -> {}", table, from, to)
78 }
79 MigrationAction::DeleteColumn { table, column } => {
80 write!(f, "DeleteColumn: {}.{}", table, column)
81 }
82 MigrationAction::ModifyColumnType { table, column, .. } => {
83 write!(f, "ModifyColumnType: {}.{}", table, column)
84 }
85 MigrationAction::AddConstraint { table, constraint } => {
86 let constraint_name = match constraint {
87 TableConstraint::PrimaryKey { .. } => "PRIMARY KEY",
88 TableConstraint::Unique { name, .. } => {
89 if let Some(n) = name {
90 return write!(f, "AddConstraint: {}.{} (UNIQUE)", table, n);
91 }
92 "UNIQUE"
93 }
94 TableConstraint::ForeignKey { name, .. } => {
95 if let Some(n) = name {
96 return write!(f, "AddConstraint: {}.{} (FOREIGN KEY)", table, n);
97 }
98 "FOREIGN KEY"
99 }
100 TableConstraint::Check { name, .. } => {
101 return write!(f, "AddConstraint: {}.{} (CHECK)", table, name);
102 }
103 TableConstraint::Index { name, .. } => {
104 if let Some(n) = name {
105 return write!(f, "AddConstraint: {}.{} (INDEX)", table, n);
106 }
107 "INDEX"
108 }
109 };
110 write!(f, "AddConstraint: {}.{}", table, constraint_name)
111 }
112 MigrationAction::RemoveConstraint { table, constraint } => {
113 let constraint_name = match constraint {
114 TableConstraint::PrimaryKey { .. } => "PRIMARY KEY",
115 TableConstraint::Unique { name, .. } => {
116 if let Some(n) = name {
117 return write!(f, "RemoveConstraint: {}.{} (UNIQUE)", table, n);
118 }
119 "UNIQUE"
120 }
121 TableConstraint::ForeignKey { name, .. } => {
122 if let Some(n) = name {
123 return write!(f, "RemoveConstraint: {}.{} (FOREIGN KEY)", table, n);
124 }
125 "FOREIGN KEY"
126 }
127 TableConstraint::Check { name, .. } => {
128 return write!(f, "RemoveConstraint: {}.{} (CHECK)", table, name);
129 }
130 TableConstraint::Index { name, .. } => {
131 if let Some(n) = name {
132 return write!(f, "RemoveConstraint: {}.{} (INDEX)", table, n);
133 }
134 "INDEX"
135 }
136 };
137 write!(f, "RemoveConstraint: {}.{}", table, constraint_name)
138 }
139 MigrationAction::RenameTable { from, to } => {
140 write!(f, "RenameTable: {} -> {}", from, to)
141 }
142 MigrationAction::RawSql { sql } => {
143 let display_sql = if sql.len() > 50 {
145 format!("{}...", &sql[..47])
146 } else {
147 sql.clone()
148 };
149 write!(f, "RawSql: {}", display_sql)
150 }
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::schema::{ReferenceAction, SimpleColumnType};
159 use rstest::rstest;
160
161 fn default_column() -> ColumnDef {
162 ColumnDef {
163 name: "email".into(),
164 r#type: ColumnType::Simple(SimpleColumnType::Text),
165 nullable: true,
166 default: None,
167 comment: None,
168 primary_key: None,
169 unique: None,
170 index: None,
171 foreign_key: None,
172 }
173 }
174
175 #[rstest]
176 #[case::create_table(
177 MigrationAction::CreateTable {
178 table: "users".into(),
179 columns: vec![],
180 constraints: vec![],
181 },
182 "CreateTable: users"
183 )]
184 #[case::delete_table(
185 MigrationAction::DeleteTable {
186 table: "users".into(),
187 },
188 "DeleteTable: users"
189 )]
190 #[case::add_column(
191 MigrationAction::AddColumn {
192 table: "users".into(),
193 column: Box::new(default_column()),
194 fill_with: None,
195 },
196 "AddColumn: users.email"
197 )]
198 #[case::rename_column(
199 MigrationAction::RenameColumn {
200 table: "users".into(),
201 from: "old_name".into(),
202 to: "new_name".into(),
203 },
204 "RenameColumn: users.old_name -> new_name"
205 )]
206 #[case::delete_column(
207 MigrationAction::DeleteColumn {
208 table: "users".into(),
209 column: "email".into(),
210 },
211 "DeleteColumn: users.email"
212 )]
213 #[case::modify_column_type(
214 MigrationAction::ModifyColumnType {
215 table: "users".into(),
216 column: "age".into(),
217 new_type: ColumnType::Simple(SimpleColumnType::Integer),
218 },
219 "ModifyColumnType: users.age"
220 )]
221 #[case::add_constraint_index_with_name(
222 MigrationAction::AddConstraint {
223 table: "users".into(),
224 constraint: TableConstraint::Index {
225 name: Some("ix_users__email".into()),
226 columns: vec!["email".into()],
227 },
228 },
229 "AddConstraint: users.ix_users__email (INDEX)"
230 )]
231 #[case::add_constraint_index_without_name(
232 MigrationAction::AddConstraint {
233 table: "users".into(),
234 constraint: TableConstraint::Index {
235 name: None,
236 columns: vec!["email".into()],
237 },
238 },
239 "AddConstraint: users.INDEX"
240 )]
241 #[case::remove_constraint_index_with_name(
242 MigrationAction::RemoveConstraint {
243 table: "users".into(),
244 constraint: TableConstraint::Index {
245 name: Some("ix_users__email".into()),
246 columns: vec!["email".into()],
247 },
248 },
249 "RemoveConstraint: users.ix_users__email (INDEX)"
250 )]
251 #[case::remove_constraint_index_without_name(
252 MigrationAction::RemoveConstraint {
253 table: "users".into(),
254 constraint: TableConstraint::Index {
255 name: None,
256 columns: vec!["email".into()],
257 },
258 },
259 "RemoveConstraint: users.INDEX"
260 )]
261 #[case::rename_table(
262 MigrationAction::RenameTable {
263 from: "old_table".into(),
264 to: "new_table".into(),
265 },
266 "RenameTable: old_table -> new_table"
267 )]
268 fn test_display_basic_actions(#[case] action: MigrationAction, #[case] expected: &str) {
269 assert_eq!(action.to_string(), expected);
270 }
271
272 #[rstest]
273 #[case::add_constraint_primary_key(
274 MigrationAction::AddConstraint {
275 table: "users".into(),
276 constraint: TableConstraint::PrimaryKey {
277 auto_increment: false,
278 columns: vec!["id".into()],
279 },
280 },
281 "AddConstraint: users.PRIMARY KEY"
282 )]
283 #[case::add_constraint_unique_with_name(
284 MigrationAction::AddConstraint {
285 table: "users".into(),
286 constraint: TableConstraint::Unique {
287 name: Some("uq_email".into()),
288 columns: vec!["email".into()],
289 },
290 },
291 "AddConstraint: users.uq_email (UNIQUE)"
292 )]
293 #[case::add_constraint_unique_without_name(
294 MigrationAction::AddConstraint {
295 table: "users".into(),
296 constraint: TableConstraint::Unique {
297 name: None,
298 columns: vec!["email".into()],
299 },
300 },
301 "AddConstraint: users.UNIQUE"
302 )]
303 #[case::add_constraint_foreign_key_with_name(
304 MigrationAction::AddConstraint {
305 table: "posts".into(),
306 constraint: TableConstraint::ForeignKey {
307 name: Some("fk_user".into()),
308 columns: vec!["user_id".into()],
309 ref_table: "users".into(),
310 ref_columns: vec!["id".into()],
311 on_delete: Some(ReferenceAction::Cascade),
312 on_update: None,
313 },
314 },
315 "AddConstraint: posts.fk_user (FOREIGN KEY)"
316 )]
317 #[case::add_constraint_foreign_key_without_name(
318 MigrationAction::AddConstraint {
319 table: "posts".into(),
320 constraint: TableConstraint::ForeignKey {
321 name: None,
322 columns: vec!["user_id".into()],
323 ref_table: "users".into(),
324 ref_columns: vec!["id".into()],
325 on_delete: None,
326 on_update: None,
327 },
328 },
329 "AddConstraint: posts.FOREIGN KEY"
330 )]
331 #[case::add_constraint_check(
332 MigrationAction::AddConstraint {
333 table: "users".into(),
334 constraint: TableConstraint::Check {
335 name: "chk_age".into(),
336 expr: "age > 0".into(),
337 },
338 },
339 "AddConstraint: users.chk_age (CHECK)"
340 )]
341 fn test_display_add_constraint(#[case] action: MigrationAction, #[case] expected: &str) {
342 assert_eq!(action.to_string(), expected);
343 }
344
345 #[rstest]
346 #[case::remove_constraint_primary_key(
347 MigrationAction::RemoveConstraint {
348 table: "users".into(),
349 constraint: TableConstraint::PrimaryKey {
350 auto_increment: false,
351 columns: vec!["id".into()],
352 },
353 },
354 "RemoveConstraint: users.PRIMARY KEY"
355 )]
356 #[case::remove_constraint_unique_with_name(
357 MigrationAction::RemoveConstraint {
358 table: "users".into(),
359 constraint: TableConstraint::Unique {
360 name: Some("uq_email".into()),
361 columns: vec!["email".into()],
362 },
363 },
364 "RemoveConstraint: users.uq_email (UNIQUE)"
365 )]
366 #[case::remove_constraint_unique_without_name(
367 MigrationAction::RemoveConstraint {
368 table: "users".into(),
369 constraint: TableConstraint::Unique {
370 name: None,
371 columns: vec!["email".into()],
372 },
373 },
374 "RemoveConstraint: users.UNIQUE"
375 )]
376 #[case::remove_constraint_foreign_key_with_name(
377 MigrationAction::RemoveConstraint {
378 table: "posts".into(),
379 constraint: TableConstraint::ForeignKey {
380 name: Some("fk_user".into()),
381 columns: vec!["user_id".into()],
382 ref_table: "users".into(),
383 ref_columns: vec!["id".into()],
384 on_delete: None,
385 on_update: None,
386 },
387 },
388 "RemoveConstraint: posts.fk_user (FOREIGN KEY)"
389 )]
390 #[case::remove_constraint_foreign_key_without_name(
391 MigrationAction::RemoveConstraint {
392 table: "posts".into(),
393 constraint: TableConstraint::ForeignKey {
394 name: None,
395 columns: vec!["user_id".into()],
396 ref_table: "users".into(),
397 ref_columns: vec!["id".into()],
398 on_delete: None,
399 on_update: None,
400 },
401 },
402 "RemoveConstraint: posts.FOREIGN KEY"
403 )]
404 #[case::remove_constraint_check(
405 MigrationAction::RemoveConstraint {
406 table: "users".into(),
407 constraint: TableConstraint::Check {
408 name: "chk_age".into(),
409 expr: "age > 0".into(),
410 },
411 },
412 "RemoveConstraint: users.chk_age (CHECK)"
413 )]
414 fn test_display_remove_constraint(#[case] action: MigrationAction, #[case] expected: &str) {
415 assert_eq!(action.to_string(), expected);
416 }
417
418 #[rstest]
419 #[case::raw_sql_short(
420 MigrationAction::RawSql {
421 sql: "SELECT 1".into(),
422 },
423 "RawSql: SELECT 1"
424 )]
425 fn test_display_raw_sql_short(#[case] action: MigrationAction, #[case] expected: &str) {
426 assert_eq!(action.to_string(), expected);
427 }
428
429 #[test]
430 fn test_display_raw_sql_long() {
431 let action = MigrationAction::RawSql {
432 sql:
433 "SELECT * FROM users WHERE id = 1 AND name = 'test' AND email = 'test@example.com'"
434 .into(),
435 };
436 let result = action.to_string();
437 assert!(result.starts_with("RawSql: "));
438 assert!(result.ends_with("..."));
439 assert!(result.len() > 10);
440 }
441}