1use sea_query::{Alias, Query, Table};
2
3use vespertide_core::{ColumnDef, TableDef};
4
5use super::helpers::{
6 build_sea_column_def_with_table, build_sqlite_temp_table_create, normalize_enum_default,
7 quote_ident, recreate_indexes_after_rebuild,
8};
9use super::rename_table::build_rename_table;
10use super::types::{BuiltQuery, DatabaseBackend, RawSql};
11use crate::error::QueryError;
12
13#[expect(
23 clippy::too_many_lines,
24 reason = "three-backend dispatch (PG / MySQL / SQLite) plus optional backfill UPDATE; splitting per-backend helpers scatters the read flow"
25)]
26pub fn build_modify_column_default(
27 backend: DatabaseBackend,
28 table: &str,
29 column: &str,
30 new_default: Option<&str>,
31 backfill: Option<&str>,
32 current_schema: &[TableDef],
33 pending_constraints: &[vespertide_core::TableConstraint],
34) -> Result<Vec<BuiltQuery>, QueryError> {
35 let mut queries = Vec::new();
36
37 match backend {
38 DatabaseBackend::Postgres => {
39 let quoted_table = quote_ident(table, backend);
40 let quoted_column = quote_ident(column, backend);
41 let alter_sql = if let Some(default_value) = new_default {
42 let column_type = current_schema
44 .iter()
45 .find(|t| t.name == table)
46 .and_then(|t| t.columns.iter().find(|c| c.name == column))
47 .map(|c| &c.r#type);
48
49 let normalized_default = if let Some(col_type) = column_type {
50 normalize_enum_default(col_type, default_value)
51 } else {
52 default_value.to_string()
53 };
54
55 format!(
56 "ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} SET DEFAULT {normalized_default}"
57 )
58 } else {
59 format!("ALTER TABLE {quoted_table} ALTER COLUMN {quoted_column} DROP DEFAULT")
60 };
61 queries.push(BuiltQuery::Raw(RawSql::uniform(alter_sql)));
62 }
63 DatabaseBackend::MySql => {
64 let table_def = current_schema
66 .iter()
67 .find(|t| t.name == table)
68 .ok_or_else(|| {
69 QueryError::SchemaError(format!("Table '{table}' not found in current schema."))
70 })?;
71
72 let column_def = table_def
73 .columns
74 .iter()
75 .find(|c| c.name == column)
76 .ok_or_else(|| {
77 QueryError::SchemaError(format!(
78 "Column '{column}' not found in table '{table}'."
79 ))
80 })?;
81
82 let modified_col_def = ColumnDef {
84 default: new_default.map(std::convert::Into::into),
85 ..column_def.clone()
86 };
87
88 let sea_col = build_sea_column_def_with_table(backend, table, &modified_col_def);
89
90 let stmt = Table::alter()
91 .table(Alias::new(table))
92 .modify_column(sea_col)
93 .to_owned();
94 queries.push(BuiltQuery::AlterTable(Box::new(stmt)));
95 }
96 DatabaseBackend::Sqlite => {
97 let table_def = current_schema
100 .iter()
101 .find(|t| t.name == table)
102 .ok_or_else(|| {
103 QueryError::SchemaError(format!("Table '{table}' not found in current schema."))
104 })?;
105
106 let mut new_columns = table_def.columns.clone();
108 if let Some(col) = new_columns.iter_mut().find(|c| c.name == column) {
109 col.default = new_default.map(std::convert::Into::into);
110 }
111
112 let temp_table = format!("{table}_temp");
114
115 let create_query = build_sqlite_temp_table_create(
117 backend,
118 &temp_table,
119 table,
120 &new_columns,
121 &table_def.constraints,
122 );
123 queries.push(create_query);
124
125 let column_aliases: Vec<Alias> = table_def
127 .columns
128 .iter()
129 .map(|c| Alias::new(&c.name))
130 .collect();
131 let mut select_query = Query::select();
132 for col_alias in &column_aliases {
133 select_query.column(col_alias.clone());
134 }
135 select_query.from(Alias::new(table));
136
137 let insert_stmt = Query::insert()
138 .into_table(Alias::new(&temp_table))
139 .columns(column_aliases.clone())
140 .select_from(select_query)
141 .unwrap()
142 .to_owned();
143 queries.push(BuiltQuery::Insert(Box::new(insert_stmt)));
144
145 let drop_table = Table::drop().table(Alias::new(table)).to_owned();
147 queries.push(BuiltQuery::DropTable(Box::new(drop_table)));
148
149 queries.push(build_rename_table(&temp_table, table));
151
152 queries.extend(recreate_indexes_after_rebuild(
154 table,
155 &table_def.constraints,
156 pending_constraints,
157 ));
158 }
159 }
160
161 if let Some(value) = backfill {
167 let quoted_table = quote_ident(table, backend);
168 let quoted_column = quote_ident(column, backend);
169 let update_sql = format!("UPDATE {quoted_table} SET {quoted_column} = {value}");
170 queries.push(BuiltQuery::Raw(RawSql::uniform(update_sql)));
171 }
172
173 Ok(queries)
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use insta::{assert_snapshot, with_settings};
180 use rstest::rstest;
181 use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableConstraint};
182
183 fn col(name: &str, ty: ColumnType, nullable: bool) -> ColumnDef {
184 ColumnDef::new(name, ty, nullable)
185 }
186
187 fn table_def(
188 name: &str,
189 columns: Vec<ColumnDef>,
190 constraints: Vec<TableConstraint>,
191 ) -> TableDef {
192 TableDef {
193 name: name.into(),
194 description: None,
195 columns,
196 constraints,
197 }
198 }
199
200 #[rstest]
201 #[case::postgres_set_default(DatabaseBackend::Postgres, Some("'unknown'"))]
202 #[case::postgres_drop_default(DatabaseBackend::Postgres, None)]
203 #[case::mysql_set_default(DatabaseBackend::MySql, Some("'unknown'"))]
204 #[case::mysql_drop_default(DatabaseBackend::MySql, None)]
205 #[case::sqlite_set_default(DatabaseBackend::Sqlite, Some("'unknown'"))]
206 #[case::sqlite_drop_default(DatabaseBackend::Sqlite, None)]
207 fn test_build_modify_column_default(
208 #[case] backend: DatabaseBackend,
209 #[case] new_default: Option<&str>,
210 ) {
211 let schema = vec![table_def(
212 "users",
213 vec![
214 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
215 col("email", ColumnType::Simple(SimpleColumnType::Text), true),
216 ],
217 vec![],
218 )];
219
220 let result =
221 build_modify_column_default(backend, "users", "email", new_default, None, &schema, &[]);
222 assert!(result.is_ok());
223 let queries = result.unwrap();
224 let sql = queries
225 .iter()
226 .map(|q| q.build(backend))
227 .collect::<Vec<String>>()
228 .join("\n");
229
230 let suffix = format!(
231 "{}_{}_users",
232 match backend {
233 DatabaseBackend::Postgres => "postgres",
234 DatabaseBackend::MySql => "mysql",
235 DatabaseBackend::Sqlite => "sqlite",
236 },
237 if new_default.is_some() {
238 "set_default"
239 } else {
240 "drop_default"
241 }
242 );
243
244 with_settings!({ snapshot_suffix => suffix }, {
245 assert_snapshot!(sql);
246 });
247 }
248
249 #[rstest]
251 #[case::postgres_table_not_found(DatabaseBackend::Postgres)]
252 #[case::mysql_table_not_found(DatabaseBackend::MySql)]
253 #[case::sqlite_table_not_found(DatabaseBackend::Sqlite)]
254 fn test_table_not_found(#[case] backend: DatabaseBackend) {
255 if backend == DatabaseBackend::Postgres {
257 return;
258 }
259
260 let result = build_modify_column_default(
261 backend,
262 "users",
263 "email",
264 Some("'default'"),
265 None,
266 &[],
267 &[],
268 );
269 assert!(result.is_err());
270 let err_msg = result.unwrap_err().to_string();
271 assert!(err_msg.contains("Table 'users' not found"));
272 }
273
274 #[rstest]
276 #[case::postgres_column_not_found(DatabaseBackend::Postgres)]
277 #[case::mysql_column_not_found(DatabaseBackend::MySql)]
278 #[case::sqlite_column_not_found(DatabaseBackend::Sqlite)]
279 fn test_column_not_found(#[case] backend: DatabaseBackend) {
280 if backend == DatabaseBackend::Postgres || backend == DatabaseBackend::Sqlite {
283 return;
284 }
285
286 let schema = vec![table_def(
287 "users",
288 vec![col(
289 "id",
290 ColumnType::Simple(SimpleColumnType::Integer),
291 false,
292 )],
293 vec![],
294 )];
295
296 let result = build_modify_column_default(
297 backend,
298 "users",
299 "email",
300 Some("'default'"),
301 None,
302 &schema,
303 &[],
304 );
305 assert!(result.is_err());
306 let err_msg = result.unwrap_err().to_string();
307 assert!(err_msg.contains("Column 'email' not found"));
308 }
309
310 #[test]
313 fn test_postgres_column_not_in_schema_uses_default_as_is() {
314 let schema = vec![table_def(
315 "users",
316 vec![col(
317 "id",
318 ColumnType::Simple(SimpleColumnType::Integer),
319 false,
320 )],
321 vec![],
323 )];
324
325 let result = build_modify_column_default(
327 DatabaseBackend::Postgres,
328 "users",
329 "status", Some("'active'"),
331 None,
332 &schema,
333 &[],
334 );
335 assert!(result.is_ok());
336 let queries = result.unwrap();
337 let sql = queries
338 .iter()
339 .map(|q| q.build(DatabaseBackend::Postgres))
340 .collect::<Vec<String>>()
341 .join("\n");
342
343 assert!(sql.contains("ALTER TABLE \"users\" ALTER COLUMN \"status\" SET DEFAULT 'active'"));
345 }
346
347 #[rstest]
349 #[case::postgres_with_index(DatabaseBackend::Postgres)]
350 #[case::mysql_with_index(DatabaseBackend::MySql)]
351 #[case::sqlite_with_index(DatabaseBackend::Sqlite)]
352 fn test_modify_default_with_index(#[case] backend: DatabaseBackend) {
353 let schema = vec![table_def(
354 "users",
355 vec![
356 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
357 col("email", ColumnType::Simple(SimpleColumnType::Text), true),
358 ],
359 vec![TableConstraint::Index {
360 name: Some("idx_users_email".into()),
361 columns: vec!["email".into()],
362 }],
363 )];
364
365 let result = build_modify_column_default(
366 backend,
367 "users",
368 "email",
369 Some("'default@example.com'"),
370 None,
371 &schema,
372 &[],
373 );
374 assert!(result.is_ok());
375 let queries = result.unwrap();
376 let sql = queries
377 .iter()
378 .map(|q| q.build(backend))
379 .collect::<Vec<String>>()
380 .join("\n");
381
382 if backend == DatabaseBackend::Sqlite {
384 assert!(sql.contains("CREATE INDEX"));
385 assert!(sql.contains("idx_users_email"));
386 }
387
388 let suffix = format!(
389 "{}_with_index",
390 match backend {
391 DatabaseBackend::Postgres => "postgres",
392 DatabaseBackend::MySql => "mysql",
393 DatabaseBackend::Sqlite => "sqlite",
394 }
395 );
396
397 with_settings!({ snapshot_suffix => suffix }, {
398 assert_snapshot!(sql);
399 });
400 }
401
402 #[rstest]
404 #[case::postgres_change_default(DatabaseBackend::Postgres)]
405 #[case::mysql_change_default(DatabaseBackend::MySql)]
406 #[case::sqlite_change_default(DatabaseBackend::Sqlite)]
407 fn test_change_default_value(#[case] backend: DatabaseBackend) {
408 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text), true);
409 email_col.default = Some("'old@example.com'".into());
410
411 let schema = vec![table_def(
412 "users",
413 vec![
414 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
415 email_col,
416 ],
417 vec![],
418 )];
419
420 let result = build_modify_column_default(
421 backend,
422 "users",
423 "email",
424 Some("'new@example.com'"),
425 None,
426 &schema,
427 &[],
428 );
429 assert!(result.is_ok());
430 let queries = result.unwrap();
431 let sql = queries
432 .iter()
433 .map(|q| q.build(backend))
434 .collect::<Vec<String>>()
435 .join("\n");
436
437 let suffix = format!(
438 "{}_change_default",
439 match backend {
440 DatabaseBackend::Postgres => "postgres",
441 DatabaseBackend::MySql => "mysql",
442 DatabaseBackend::Sqlite => "sqlite",
443 }
444 );
445
446 with_settings!({ snapshot_suffix => suffix }, {
447 assert_snapshot!(sql);
448 });
449 }
450
451 #[rstest]
453 #[case::postgres_integer_default(DatabaseBackend::Postgres)]
454 #[case::mysql_integer_default(DatabaseBackend::MySql)]
455 #[case::sqlite_integer_default(DatabaseBackend::Sqlite)]
456 fn test_integer_default(#[case] backend: DatabaseBackend) {
457 let schema = vec![table_def(
458 "products",
459 vec![
460 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
461 col(
462 "quantity",
463 ColumnType::Simple(SimpleColumnType::Integer),
464 false,
465 ),
466 ],
467 vec![],
468 )];
469
470 let result = build_modify_column_default(
471 backend,
472 "products",
473 "quantity",
474 Some("0"),
475 None,
476 &schema,
477 &[],
478 );
479 assert!(result.is_ok());
480 let queries = result.unwrap();
481 let sql = queries
482 .iter()
483 .map(|q| q.build(backend))
484 .collect::<Vec<String>>()
485 .join("\n");
486
487 let suffix = format!(
488 "{}_integer_default",
489 match backend {
490 DatabaseBackend::Postgres => "postgres",
491 DatabaseBackend::MySql => "mysql",
492 DatabaseBackend::Sqlite => "sqlite",
493 }
494 );
495
496 with_settings!({ snapshot_suffix => suffix }, {
497 assert_snapshot!(sql);
498 });
499 }
500
501 #[rstest]
503 #[case::postgres_boolean_default(DatabaseBackend::Postgres)]
504 #[case::mysql_boolean_default(DatabaseBackend::MySql)]
505 #[case::sqlite_boolean_default(DatabaseBackend::Sqlite)]
506 fn test_boolean_default(#[case] backend: DatabaseBackend) {
507 let schema = vec![table_def(
508 "users",
509 vec![
510 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
511 col(
512 "is_active",
513 ColumnType::Simple(SimpleColumnType::Boolean),
514 false,
515 ),
516 ],
517 vec![],
518 )];
519
520 let result = build_modify_column_default(
521 backend,
522 "users",
523 "is_active",
524 Some("true"),
525 None,
526 &schema,
527 &[],
528 );
529 assert!(result.is_ok());
530 let queries = result.unwrap();
531 let sql = queries
532 .iter()
533 .map(|q| q.build(backend))
534 .collect::<Vec<String>>()
535 .join("\n");
536
537 let suffix = format!(
538 "{}_boolean_default",
539 match backend {
540 DatabaseBackend::Postgres => "postgres",
541 DatabaseBackend::MySql => "mysql",
542 DatabaseBackend::Sqlite => "sqlite",
543 }
544 );
545
546 with_settings!({ snapshot_suffix => suffix }, {
547 assert_snapshot!(sql);
548 });
549 }
550
551 #[rstest]
553 #[case::postgres_function_default(DatabaseBackend::Postgres)]
554 #[case::mysql_function_default(DatabaseBackend::MySql)]
555 #[case::sqlite_function_default(DatabaseBackend::Sqlite)]
556 fn test_function_default(#[case] backend: DatabaseBackend) {
557 let schema = vec![table_def(
558 "events",
559 vec![
560 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
561 col(
562 "created_at",
563 ColumnType::Simple(SimpleColumnType::Timestamp),
564 false,
565 ),
566 ],
567 vec![],
568 )];
569
570 let default_value = match backend {
571 DatabaseBackend::Postgres => "NOW()",
572 DatabaseBackend::MySql | DatabaseBackend::Sqlite => "CURRENT_TIMESTAMP",
573 };
574
575 let result = build_modify_column_default(
576 backend,
577 "events",
578 "created_at",
579 Some(default_value),
580 None,
581 &schema,
582 &[],
583 );
584 assert!(result.is_ok());
585 let queries = result.unwrap();
586 let sql = queries
587 .iter()
588 .map(|q| q.build(backend))
589 .collect::<Vec<String>>()
590 .join("\n");
591
592 let suffix = format!(
593 "{}_function_default",
594 match backend {
595 DatabaseBackend::Postgres => "postgres",
596 DatabaseBackend::MySql => "mysql",
597 DatabaseBackend::Sqlite => "sqlite",
598 }
599 );
600
601 with_settings!({ snapshot_suffix => suffix }, {
602 assert_snapshot!(sql);
603 });
604 }
605
606 #[rstest]
608 #[case::postgres_drop_existing_default(DatabaseBackend::Postgres)]
609 #[case::mysql_drop_existing_default(DatabaseBackend::MySql)]
610 #[case::sqlite_drop_existing_default(DatabaseBackend::Sqlite)]
611 fn test_drop_existing_default(#[case] backend: DatabaseBackend) {
612 let mut status_col = col("status", ColumnType::Simple(SimpleColumnType::Text), false);
613 status_col.default = Some("'pending'".into());
614
615 let schema = vec![table_def(
616 "orders",
617 vec![
618 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
619 status_col,
620 ],
621 vec![],
622 )];
623
624 let result = build_modify_column_default(
625 backend,
626 "orders",
627 "status",
628 None, None, &schema,
631 &[],
632 );
633 assert!(result.is_ok());
634 let queries = result.unwrap();
635 let sql = queries
636 .iter()
637 .map(|q| q.build(backend))
638 .collect::<Vec<String>>()
639 .join("\n");
640
641 let suffix = format!(
642 "{}_drop_existing_default",
643 match backend {
644 DatabaseBackend::Postgres => "postgres",
645 DatabaseBackend::MySql => "mysql",
646 DatabaseBackend::Sqlite => "sqlite",
647 }
648 );
649
650 with_settings!({ snapshot_suffix => suffix }, {
651 assert_snapshot!(sql);
652 });
653 }
654
655 #[rstest]
659 #[case::postgres(DatabaseBackend::Postgres)]
660 #[case::mysql(DatabaseBackend::MySql)]
661 #[case::sqlite(DatabaseBackend::Sqlite)]
662 fn build_modify_column_default_with_backfill_emits_update_statement(
663 #[case] backend: DatabaseBackend,
664 ) {
665 let schema = vec![table_def(
666 "users",
667 vec![
668 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
669 col("status", ColumnType::Simple(SimpleColumnType::Text), false),
670 ],
671 vec![],
672 )];
673
674 let queries = build_modify_column_default(
675 backend,
676 "users",
677 "status",
678 Some("'active'"),
679 Some("'active'"),
680 &schema,
681 &[],
682 )
683 .expect("backfill path should succeed");
684 let sql = queries
685 .iter()
686 .map(|q| q.build(backend))
687 .collect::<Vec<_>>()
688 .join("\n");
689
690 let update_count = sql.matches("UPDATE").count();
692 assert!(update_count >= 1, "expected backfill UPDATE in: {sql}");
693 assert!(sql.contains("SET"));
694 assert!(sql.contains("status"));
695 assert!(sql.contains("'active'"));
696 }
697}