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