1use sea_query::{Alias, ForeignKey};
2
3use vespertide_core::{TableConstraint, TableDef};
4
5use super::helpers::{build_sqlite_table_rebuild, require_table_in_schema, to_sea_fk_action};
6use super::types::{BuiltQuery, DatabaseBackend};
7use crate::error::QueryError;
8
9pub fn build_replace_constraint(
17 backend: DatabaseBackend,
18 table: &str,
19 from: &TableConstraint,
20 to: &TableConstraint,
21 current_schema: &[TableDef],
22 pending_constraints: &[TableConstraint],
23) -> Result<Vec<BuiltQuery>, QueryError> {
24 match (from, to) {
25 (
26 TableConstraint::ForeignKey {
27 name: old_name,
28 columns: old_columns,
29 ..
30 },
31 TableConstraint::ForeignKey {
32 name: new_name,
33 columns: new_columns,
34 ref_table,
35 ref_columns,
36 on_delete,
37 on_update,
38 ..
39 },
40 ) => {
41 if backend == DatabaseBackend::Sqlite {
42 build_sqlite_constraint_replace(
43 backend,
44 table,
45 from,
46 to,
47 current_schema,
48 pending_constraints,
49 )
50 } else {
51 Ok(build_direct_foreign_key_replace(
52 table,
53 old_name.as_deref(),
54 old_columns,
55 new_name.as_deref(),
56 new_columns,
57 ref_table,
58 ref_columns,
59 on_delete.as_ref(),
60 on_update.as_ref(),
61 ))
62 }
63 }
64 _ => {
66 if backend == DatabaseBackend::Sqlite {
67 build_sqlite_constraint_replace(
68 backend,
69 table,
70 from,
71 to,
72 current_schema,
73 pending_constraints,
74 )
75 } else {
76 let mut queries = super::remove_constraint::build_remove_constraint(
77 backend,
78 table,
79 from,
80 current_schema,
81 pending_constraints,
82 )?;
83
84 let modified_schema: Vec<TableDef> = current_schema
86 .iter()
87 .map(|t| {
88 if t.name == table {
89 let mut modified = t.clone();
90 modified.constraints.retain(|c| c != from);
91 modified.constraints.push(to.clone());
92 modified
93 } else {
94 t.clone()
95 }
96 })
97 .collect();
98
99 queries.extend(super::add_constraint::build_add_constraint(
100 backend,
101 table,
102 to,
103 &modified_schema,
104 pending_constraints,
105 )?);
106 Ok(queries)
107 }
108 }
109 }
110}
111
112#[expect(
113 clippy::too_many_arguments,
114 reason = "mirrors foreign key action fields"
115)]
116fn build_direct_foreign_key_replace<T: AsRef<str>, U: AsRef<str>, V: AsRef<str>>(
117 table: &str,
118 old_name: Option<&str>,
119 old_columns: &[T],
120 new_name: Option<&str>,
121 new_columns: &[U],
122 ref_table: &str,
123 ref_columns: &[V],
124 on_delete: Option<&vespertide_core::ReferenceAction>,
125 on_update: Option<&vespertide_core::ReferenceAction>,
126) -> Vec<BuiltQuery> {
127 let old_fk_name = vespertide_naming::build_foreign_key_name(table, old_columns, old_name);
128 let fk_drop = ForeignKey::drop()
129 .name(&old_fk_name)
130 .table(Alias::new(table))
131 .to_owned();
132 let fk_create = build_replacement_foreign_key(
133 table,
134 new_name,
135 new_columns,
136 ref_table,
137 ref_columns,
138 on_delete,
139 on_update,
140 );
141
142 vec![
143 BuiltQuery::DropForeignKey(Box::new(fk_drop)),
144 BuiltQuery::CreateForeignKey(Box::new(fk_create)),
145 ]
146}
147
148fn build_replacement_foreign_key<T: AsRef<str>, U: AsRef<str>>(
149 table: &str,
150 new_name: Option<&str>,
151 new_columns: &[T],
152 ref_table: &str,
153 ref_columns: &[U],
154 on_delete: Option<&vespertide_core::ReferenceAction>,
155 on_update: Option<&vespertide_core::ReferenceAction>,
156) -> sea_query::ForeignKeyCreateStatement {
157 let new_fk_name = vespertide_naming::build_foreign_key_name(table, new_columns, new_name);
158 let mut fk_create = ForeignKey::create();
159 fk_create.name(&new_fk_name);
160 fk_create.from_tbl(Alias::new(table));
161 for col in new_columns {
162 fk_create.from_col(Alias::new(col.as_ref()));
163 }
164 fk_create.to_tbl(Alias::new(ref_table));
165 for col in ref_columns {
166 fk_create.to_col(Alias::new(col.as_ref()));
167 }
168 if let Some(action) = on_delete {
169 fk_create.on_delete(to_sea_fk_action(action));
170 }
171 if let Some(action) = on_update {
172 fk_create.on_update(to_sea_fk_action(action));
173 }
174 fk_create
175}
176
177fn build_sqlite_constraint_replace(
180 backend: DatabaseBackend,
181 table: &str,
182 from: &TableConstraint,
183 to: &TableConstraint,
184 current_schema: &[TableDef],
185 pending_constraints: &[TableConstraint],
186) -> Result<Vec<BuiltQuery>, QueryError> {
187 let table_def = require_table_in_schema(
188 current_schema,
189 table,
190 "SQLite requires current schema information to replace constraints",
191 )?;
192
193 let new_constraints: Vec<TableConstraint> = table_def
195 .constraints
196 .iter()
197 .map(|c| if c == from { to.clone() } else { c.clone() })
198 .collect();
199
200 Ok(build_sqlite_table_rebuild(
201 backend,
202 table,
203 &table_def.columns,
204 &new_constraints,
205 &table_def.columns,
206 &table_def.constraints,
207 pending_constraints,
208 ))
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use insta::{assert_snapshot, with_settings};
215 use rstest::rstest;
216 use vespertide_core::{
217 ColumnDef, ColumnType, ReferenceAction, SimpleColumnType, TableConstraint, TableDef,
218 };
219
220 fn test_schema() -> Vec<TableDef> {
221 vec![
222 TableDef {
223 name: "users".into(),
224 columns: vec![ColumnDef {
225 name: "id".into(),
226 r#type: ColumnType::Simple(SimpleColumnType::Integer),
227 nullable: false,
228 default: None,
229 comment: None,
230 primary_key: None,
231 unique: None,
232 index: None,
233 foreign_key: None,
234 }],
235 constraints: vec![TableConstraint::PrimaryKey {
236 auto_increment: false,
237 columns: vec!["id".into()],
238 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
239 }],
240 description: None,
241 },
242 TableDef {
243 name: "posts".into(),
244 columns: vec![
245 ColumnDef {
246 name: "id".into(),
247 r#type: ColumnType::Simple(SimpleColumnType::Integer),
248 nullable: false,
249 default: None,
250 comment: None,
251 primary_key: None,
252 unique: None,
253 index: None,
254 foreign_key: None,
255 },
256 ColumnDef {
257 name: "user_id".into(),
258 r#type: ColumnType::Simple(SimpleColumnType::Integer),
259 nullable: false,
260 default: None,
261 comment: None,
262 primary_key: None,
263 unique: None,
264 index: None,
265 foreign_key: None,
266 },
267 ],
268 constraints: vec![
269 TableConstraint::PrimaryKey {
270 auto_increment: false,
271 columns: vec!["id".into()],
272 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
273 },
274 TableConstraint::ForeignKey {
275 name: Some("fk_user".into()),
276 columns: vec!["user_id".into()],
277 ref_table: "users".into(),
278 ref_columns: vec!["id".into()],
279 on_delete: None,
280 on_update: None,
281 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
282 },
283 ],
284 description: None,
285 },
286 ]
287 }
288
289 #[rstest]
290 #[case::postgres(DatabaseBackend::Postgres)]
291 #[case::mysql(DatabaseBackend::MySql)]
292 #[case::sqlite(DatabaseBackend::Sqlite)]
293 fn replace_fk_on_delete(#[case] backend: DatabaseBackend) {
294 let schema = test_schema();
295 let from = TableConstraint::ForeignKey {
296 name: Some("fk_user".into()),
297 columns: vec!["user_id".into()],
298 ref_table: "users".into(),
299 ref_columns: vec!["id".into()],
300 on_delete: None,
301 on_update: None,
302 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
303 };
304 let to = TableConstraint::ForeignKey {
305 name: Some("fk_user".into()),
306 columns: vec!["user_id".into()],
307 ref_table: "users".into(),
308 ref_columns: vec!["id".into()],
309 on_delete: Some(ReferenceAction::Cascade),
310 on_update: None,
311 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
312 };
313
314 let queries = build_replace_constraint(backend, "posts", &from, &to, &schema, &[])
315 .expect("should succeed");
316
317 let sql: Vec<String> = queries.iter().map(|q| q.build(backend)).collect();
318 let combined = sql.join(";\n");
319
320 with_settings!({
321 description => format!("replace FK on_delete for {:?}", backend),
322 omit_expression => true,
323 snapshot_suffix => format!("replace_fk_on_delete_{:?}", backend),
324 }, {
325 assert_snapshot!(combined);
326 });
327 }
328
329 #[rstest]
330 #[case::postgres(DatabaseBackend::Postgres)]
331 #[case::mysql(DatabaseBackend::MySql)]
332 #[case::sqlite(DatabaseBackend::Sqlite)]
333 fn replace_fk_on_update(#[case] backend: DatabaseBackend) {
334 let schema = test_schema();
335 let from = TableConstraint::ForeignKey {
336 name: Some("fk_user".into()),
337 columns: vec!["user_id".into()],
338 ref_table: "users".into(),
339 ref_columns: vec!["id".into()],
340 on_delete: None,
341 on_update: None,
342 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
343 };
344 let to = TableConstraint::ForeignKey {
345 name: Some("fk_user".into()),
346 columns: vec!["user_id".into()],
347 ref_table: "users".into(),
348 ref_columns: vec!["id".into()],
349 on_delete: None,
350 on_update: Some(ReferenceAction::Cascade),
351 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
352 };
353
354 let queries = build_replace_constraint(backend, "posts", &from, &to, &schema, &[])
355 .expect("should succeed");
356 let sql: Vec<String> = queries.iter().map(|q| q.build(backend)).collect();
357 let combined = sql.join(";\n");
358
359 with_settings!({
360 description => format!("replace FK on_update for {:?}", backend),
361 omit_expression => true,
362 snapshot_suffix => format!("replace_fk_on_update_{:?}", backend),
363 }, {
364 assert_snapshot!(combined);
365 });
366 }
367
368 #[rstest]
369 #[case::postgres(DatabaseBackend::Postgres)]
370 #[case::mysql(DatabaseBackend::MySql)]
371 #[case::sqlite(DatabaseBackend::Sqlite)]
372 fn replace_unique_constraint(#[case] backend: DatabaseBackend) {
373 let schema = vec![
376 TableDef {
377 name: "other".into(),
378 description: None,
379 columns: vec![ColumnDef {
380 name: "id".into(),
381 r#type: ColumnType::Simple(SimpleColumnType::Integer),
382 nullable: false,
383 default: None,
384 comment: None,
385 primary_key: None,
386 unique: None,
387 index: None,
388 foreign_key: None,
389 }],
390 constraints: vec![],
391 },
392 TableDef {
393 name: "users".into(),
394 description: None,
395 columns: vec![
396 ColumnDef {
397 name: "id".into(),
398 r#type: ColumnType::Simple(SimpleColumnType::Integer),
399 nullable: false,
400 default: None,
401 comment: None,
402 primary_key: None,
403 unique: None,
404 index: None,
405 foreign_key: None,
406 },
407 ColumnDef {
408 name: "email".into(),
409 r#type: ColumnType::Simple(SimpleColumnType::Text),
410 nullable: false,
411 default: None,
412 comment: None,
413 primary_key: None,
414 unique: None,
415 index: None,
416 foreign_key: None,
417 },
418 ],
419 constraints: vec![
420 TableConstraint::PrimaryKey {
421 auto_increment: false,
422 columns: vec!["id".into()],
423 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
424 },
425 TableConstraint::Unique {
426 name: Some("uq_email".into()),
427 columns: vec!["email".into()],
428 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
429 keep: vespertide_core::KeepPolicy::First,
430 },
431 },
432 ],
433 },
434 ];
435 let from = TableConstraint::Unique {
436 name: Some("uq_email".into()),
437 columns: vec!["email".into()],
438 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
439 keep: vespertide_core::KeepPolicy::First,
440 },
441 };
442 let to = TableConstraint::Unique {
443 name: Some("uq_email_new".into()),
444 columns: vec!["email".into()],
445 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
446 keep: vespertide_core::KeepPolicy::First,
447 },
448 };
449
450 let queries = build_replace_constraint(backend, "users", &from, &to, &schema, &[])
451 .expect("should succeed");
452 let sql: Vec<String> = queries.iter().map(|q| q.build(backend)).collect();
453 let combined = sql.join(";\n");
454
455 with_settings!({
456 description => format!("replace unique constraint for {:?}", backend),
457 omit_expression => true,
458 snapshot_suffix => format!("replace_unique_{:?}", backend),
459 }, {
460 assert_snapshot!(combined);
461 });
462 }
463
464 #[test]
465 fn replace_constraint_table_not_found_sqlite() {
466 let from = TableConstraint::Unique {
467 name: Some("uq_old".into()),
468 columns: vec!["col".into()],
469 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
470 keep: vespertide_core::KeepPolicy::First,
471 },
472 };
473 let to = TableConstraint::Unique {
474 name: Some("uq_new".into()),
475 columns: vec!["col".into()],
476 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
477 keep: vespertide_core::KeepPolicy::First,
478 },
479 };
480 let err =
481 build_replace_constraint(DatabaseBackend::Sqlite, "missing", &from, &to, &[], &[])
482 .unwrap_err();
483 assert!(format!("{err}").contains("missing"));
484 }
485}