1use vespertide_core::TableDef;
2
3use super::helpers::{
4 build_mysql_modify_column_with, build_pg_alter_column_sql, build_sqlite_modify_column_with,
5 find_column_in_schema, normalize_enum_default, quote_ident,
6};
7use super::types::{BuiltQuery, DatabaseBackend, RawSql};
8use crate::error::QueryError;
9
10pub fn build_modify_column_default(
20 backend: DatabaseBackend,
21 table: &str,
22 column: &str,
23 new_default: Option<&str>,
24 backfill: Option<&str>,
25 current_schema: &[TableDef],
26 pending_constraints: &[vespertide_core::TableConstraint],
27) -> Result<Vec<BuiltQuery>, QueryError> {
28 let mut queries = Vec::new();
29
30 match backend {
31 DatabaseBackend::Postgres => {
32 let alter_sql = if let Some(default_value) = new_default {
33 let column_type =
35 find_column_in_schema(current_schema, table, column).map(|c| &c.r#type);
36
37 let normalized_default = if let Some(col_type) = column_type {
38 normalize_enum_default(col_type, default_value)
39 } else {
40 default_value.to_string()
41 };
42
43 build_pg_alter_column_sql(
44 table,
45 column,
46 &format!("SET DEFAULT {normalized_default}"),
47 )
48 } else {
49 build_pg_alter_column_sql(table, column, "DROP DEFAULT")
50 };
51 queries.push(BuiltQuery::Raw(RawSql::uniform(alter_sql)));
52 }
53 DatabaseBackend::MySql => {
54 queries.push(build_mysql_modify_column_with(
56 table,
57 column,
58 current_schema,
59 "MySQL requires current schema information to modify column defaults",
60 |c| c.default = new_default.map(std::convert::Into::into),
61 )?);
62 }
63 DatabaseBackend::Sqlite => {
64 queries.extend(build_sqlite_modify_column_with(
67 table,
68 column,
69 current_schema,
70 pending_constraints,
71 "SQLite requires current schema information to modify column defaults",
72 |c| c.default = new_default.map(std::convert::Into::into),
73 )?);
74 }
75 }
76
77 if let Some(value) = backfill {
83 let quoted_table = quote_ident(table, backend);
84 let quoted_column = quote_ident(column, backend);
85 let update_sql = format!("UPDATE {quoted_table} SET {quoted_column} = {value}");
86 queries.push(BuiltQuery::Raw(RawSql::uniform(update_sql)));
87 }
88
89 Ok(queries)
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::test_support::{backend_tag, col_n as col, joined_sql, table_def};
96 use insta::{assert_snapshot, with_settings};
97 use rstest::rstest;
98 use vespertide_core::{ColumnType, SimpleColumnType, TableConstraint};
99
100 #[rstest]
101 #[case::postgres_set_default(DatabaseBackend::Postgres, Some("'unknown'"))]
102 #[case::postgres_drop_default(DatabaseBackend::Postgres, None)]
103 #[case::mysql_set_default(DatabaseBackend::MySql, Some("'unknown'"))]
104 #[case::mysql_drop_default(DatabaseBackend::MySql, None)]
105 #[case::sqlite_set_default(DatabaseBackend::Sqlite, Some("'unknown'"))]
106 #[case::sqlite_drop_default(DatabaseBackend::Sqlite, None)]
107 fn test_build_modify_column_default(
108 #[case] backend: DatabaseBackend,
109 #[case] new_default: Option<&str>,
110 ) {
111 let schema = vec![table_def(
112 "users",
113 vec![
114 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
115 col("email", ColumnType::Simple(SimpleColumnType::Text), true),
116 ],
117 vec![],
118 )];
119
120 let result =
121 build_modify_column_default(backend, "users", "email", new_default, None, &schema, &[]);
122 assert!(result.is_ok());
123 let queries = result.unwrap();
124 let sql = joined_sql(backend, &queries);
125
126 let suffix = format!(
127 "{}_{}_users",
128 backend_tag(backend),
129 if new_default.is_some() {
130 "set_default"
131 } else {
132 "drop_default"
133 }
134 );
135
136 with_settings!({ snapshot_suffix => suffix }, {
137 assert_snapshot!(sql);
138 });
139 }
140
141 #[rstest]
143 #[case::postgres_table_not_found(DatabaseBackend::Postgres)]
144 #[case::mysql_table_not_found(DatabaseBackend::MySql)]
145 #[case::sqlite_table_not_found(DatabaseBackend::Sqlite)]
146 fn test_table_not_found(#[case] backend: DatabaseBackend) {
147 if backend == DatabaseBackend::Postgres {
149 return;
150 }
151
152 let result = build_modify_column_default(
153 backend,
154 "users",
155 "email",
156 Some("'default'"),
157 None,
158 &[],
159 &[],
160 );
161 assert!(result.is_err());
162 let err_msg = result.unwrap_err().to_string();
163 assert!(err_msg.contains("Table 'users' not found"));
164 }
165
166 #[rstest]
168 #[case::postgres_column_not_found(DatabaseBackend::Postgres)]
169 #[case::mysql_column_not_found(DatabaseBackend::MySql)]
170 #[case::sqlite_column_not_found(DatabaseBackend::Sqlite)]
171 fn test_column_not_found(#[case] backend: DatabaseBackend) {
172 if backend == DatabaseBackend::Postgres || backend == DatabaseBackend::Sqlite {
175 return;
176 }
177
178 let schema = vec![table_def(
179 "users",
180 vec![col(
181 "id",
182 ColumnType::Simple(SimpleColumnType::Integer),
183 false,
184 )],
185 vec![],
186 )];
187
188 let result = build_modify_column_default(
189 backend,
190 "users",
191 "email",
192 Some("'default'"),
193 None,
194 &schema,
195 &[],
196 );
197 assert!(result.is_err());
198 let err_msg = result.unwrap_err().to_string();
199 assert!(err_msg.contains("Column 'email' not found"));
200 }
201
202 #[test]
205 fn test_postgres_column_not_in_schema_uses_default_as_is() {
206 let schema = vec![table_def(
207 "users",
208 vec![col(
209 "id",
210 ColumnType::Simple(SimpleColumnType::Integer),
211 false,
212 )],
213 vec![],
215 )];
216
217 let result = build_modify_column_default(
219 DatabaseBackend::Postgres,
220 "users",
221 "status", Some("'active'"),
223 None,
224 &schema,
225 &[],
226 );
227 assert!(result.is_ok());
228 let queries = result.unwrap();
229 let sql = joined_sql(DatabaseBackend::Postgres, &queries);
230
231 assert!(sql.contains("ALTER TABLE \"users\" ALTER COLUMN \"status\" SET DEFAULT 'active'"));
233 }
234
235 #[rstest]
237 #[case::postgres_with_index(DatabaseBackend::Postgres)]
238 #[case::mysql_with_index(DatabaseBackend::MySql)]
239 #[case::sqlite_with_index(DatabaseBackend::Sqlite)]
240 fn test_modify_default_with_index(#[case] backend: DatabaseBackend) {
241 let schema = vec![table_def(
242 "users",
243 vec![
244 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
245 col("email", ColumnType::Simple(SimpleColumnType::Text), true),
246 ],
247 vec![TableConstraint::Index {
248 name: Some("idx_users_email".into()),
249 columns: vec!["email".into()],
250 }],
251 )];
252
253 let result = build_modify_column_default(
254 backend,
255 "users",
256 "email",
257 Some("'default@example.com'"),
258 None,
259 &schema,
260 &[],
261 );
262 assert!(result.is_ok());
263 let queries = result.unwrap();
264 let sql = joined_sql(backend, &queries);
265
266 if backend == DatabaseBackend::Sqlite {
268 assert!(sql.contains("CREATE INDEX"));
269 assert!(sql.contains("idx_users_email"));
270 }
271
272 let suffix = format!("{}_with_index", backend_tag(backend));
273
274 with_settings!({ snapshot_suffix => suffix }, {
275 assert_snapshot!(sql);
276 });
277 }
278
279 #[rstest]
281 #[case::postgres_change_default(DatabaseBackend::Postgres)]
282 #[case::mysql_change_default(DatabaseBackend::MySql)]
283 #[case::sqlite_change_default(DatabaseBackend::Sqlite)]
284 fn test_change_default_value(#[case] backend: DatabaseBackend) {
285 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text), true);
286 email_col.default = Some("'old@example.com'".into());
287
288 let schema = vec![table_def(
289 "users",
290 vec![
291 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
292 email_col,
293 ],
294 vec![],
295 )];
296
297 let result = build_modify_column_default(
298 backend,
299 "users",
300 "email",
301 Some("'new@example.com'"),
302 None,
303 &schema,
304 &[],
305 );
306 assert!(result.is_ok());
307 let queries = result.unwrap();
308 let sql = joined_sql(backend, &queries);
309
310 let suffix = format!("{}_change_default", backend_tag(backend));
311
312 with_settings!({ snapshot_suffix => suffix }, {
313 assert_snapshot!(sql);
314 });
315 }
316
317 #[rstest]
319 #[case::postgres_integer_default(DatabaseBackend::Postgres)]
320 #[case::mysql_integer_default(DatabaseBackend::MySql)]
321 #[case::sqlite_integer_default(DatabaseBackend::Sqlite)]
322 fn test_integer_default(#[case] backend: DatabaseBackend) {
323 let schema = vec![table_def(
324 "products",
325 vec![
326 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
327 col(
328 "quantity",
329 ColumnType::Simple(SimpleColumnType::Integer),
330 false,
331 ),
332 ],
333 vec![],
334 )];
335
336 let result = build_modify_column_default(
337 backend,
338 "products",
339 "quantity",
340 Some("0"),
341 None,
342 &schema,
343 &[],
344 );
345 assert!(result.is_ok());
346 let queries = result.unwrap();
347 let sql = joined_sql(backend, &queries);
348
349 let suffix = format!("{}_integer_default", backend_tag(backend));
350
351 with_settings!({ snapshot_suffix => suffix }, {
352 assert_snapshot!(sql);
353 });
354 }
355
356 #[rstest]
358 #[case::postgres_boolean_default(DatabaseBackend::Postgres)]
359 #[case::mysql_boolean_default(DatabaseBackend::MySql)]
360 #[case::sqlite_boolean_default(DatabaseBackend::Sqlite)]
361 fn test_boolean_default(#[case] backend: DatabaseBackend) {
362 let schema = vec![table_def(
363 "users",
364 vec![
365 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
366 col(
367 "is_active",
368 ColumnType::Simple(SimpleColumnType::Boolean),
369 false,
370 ),
371 ],
372 vec![],
373 )];
374
375 let result = build_modify_column_default(
376 backend,
377 "users",
378 "is_active",
379 Some("true"),
380 None,
381 &schema,
382 &[],
383 );
384 assert!(result.is_ok());
385 let queries = result.unwrap();
386 let sql = joined_sql(backend, &queries);
387
388 let suffix = format!("{}_boolean_default", backend_tag(backend));
389
390 with_settings!({ snapshot_suffix => suffix }, {
391 assert_snapshot!(sql);
392 });
393 }
394
395 #[rstest]
397 #[case::postgres_function_default(DatabaseBackend::Postgres)]
398 #[case::mysql_function_default(DatabaseBackend::MySql)]
399 #[case::sqlite_function_default(DatabaseBackend::Sqlite)]
400 fn test_function_default(#[case] backend: DatabaseBackend) {
401 let schema = vec![table_def(
402 "events",
403 vec![
404 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
405 col(
406 "created_at",
407 ColumnType::Simple(SimpleColumnType::Timestamp),
408 false,
409 ),
410 ],
411 vec![],
412 )];
413
414 let default_value = match backend {
415 DatabaseBackend::Postgres => "NOW()",
416 DatabaseBackend::MySql | DatabaseBackend::Sqlite => "CURRENT_TIMESTAMP",
417 };
418
419 let result = build_modify_column_default(
420 backend,
421 "events",
422 "created_at",
423 Some(default_value),
424 None,
425 &schema,
426 &[],
427 );
428 assert!(result.is_ok());
429 let queries = result.unwrap();
430 let sql = joined_sql(backend, &queries);
431
432 let suffix = format!("{}_function_default", backend_tag(backend));
433
434 with_settings!({ snapshot_suffix => suffix }, {
435 assert_snapshot!(sql);
436 });
437 }
438
439 #[rstest]
441 #[case::postgres_drop_existing_default(DatabaseBackend::Postgres)]
442 #[case::mysql_drop_existing_default(DatabaseBackend::MySql)]
443 #[case::sqlite_drop_existing_default(DatabaseBackend::Sqlite)]
444 fn test_drop_existing_default(#[case] backend: DatabaseBackend) {
445 let mut status_col = col("status", ColumnType::Simple(SimpleColumnType::Text), false);
446 status_col.default = Some("'pending'".into());
447
448 let schema = vec![table_def(
449 "orders",
450 vec![
451 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
452 status_col,
453 ],
454 vec![],
455 )];
456
457 let result = build_modify_column_default(
458 backend,
459 "orders",
460 "status",
461 None, None, &schema,
464 &[],
465 );
466 assert!(result.is_ok());
467 let queries = result.unwrap();
468 let sql = joined_sql(backend, &queries);
469
470 let suffix = format!("{}_drop_existing_default", backend_tag(backend));
471
472 with_settings!({ snapshot_suffix => suffix }, {
473 assert_snapshot!(sql);
474 });
475 }
476
477 #[rstest]
481 #[case::postgres(DatabaseBackend::Postgres)]
482 #[case::mysql(DatabaseBackend::MySql)]
483 #[case::sqlite(DatabaseBackend::Sqlite)]
484 fn build_modify_column_default_with_backfill_emits_update_statement(
485 #[case] backend: DatabaseBackend,
486 ) {
487 let schema = vec![table_def(
488 "users",
489 vec![
490 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
491 col("status", ColumnType::Simple(SimpleColumnType::Text), false),
492 ],
493 vec![],
494 )];
495
496 let queries = build_modify_column_default(
497 backend,
498 "users",
499 "status",
500 Some("'active'"),
501 Some("'active'"),
502 &schema,
503 &[],
504 )
505 .expect("backfill path should succeed");
506 let sql = joined_sql(backend, &queries);
507
508 let update_count = sql.matches("UPDATE").count();
510 assert!(update_count >= 1, "expected backfill UPDATE in: {sql}");
511 assert!(sql.contains("SET"));
512 assert!(sql.contains("status"));
513 assert!(sql.contains("'active'"));
514 }
515
516 #[rstest]
520 #[case::postgres(DatabaseBackend::Postgres)]
521 #[case::mysql(DatabaseBackend::MySql)]
522 #[case::sqlite(DatabaseBackend::Sqlite)]
523 fn build_modify_column_default_backfill_expression_is_verbatim(
524 #[case] backend: DatabaseBackend,
525 ) {
526 const BACKFILL: &str = "(CASE WHEN plan_key::text = 'API' THEN 'MONTHLY_QUOTA' ELSE 'SEAT' END)::billing_metric";
527
528 let schema = vec![table_def(
529 "users",
530 vec![
531 col("id", ColumnType::Simple(SimpleColumnType::Integer), false),
532 col(
533 "plan_key",
534 ColumnType::Simple(SimpleColumnType::Text),
535 false,
536 ),
537 col("metric", ColumnType::Simple(SimpleColumnType::Text), false),
538 ],
539 vec![],
540 )];
541
542 let queries = build_modify_column_default(
543 backend,
544 "users",
545 "metric",
546 None,
547 Some(BACKFILL),
548 &schema,
549 &[],
550 )
551 .expect("backfill path should succeed");
552 let sql = joined_sql(backend, &queries);
553
554 assert!(
555 sql.contains(BACKFILL),
556 "backfill must survive byte-for-byte, got: {sql}"
557 );
558 }
559}