1use rorm_declaration::imr::DbType;
2
3use crate::create_column::{self, CreateColumn, CreateColumnImpl, PostgresType};
4#[cfg(feature = "postgres")]
5use crate::db_specific::postgres;
6use crate::error::Error;
7use crate::Value;
8
9#[derive(Debug)]
13pub enum AlterTableOperation<'until_build, 'post_build> {
14 RenameTo {
16 name: String,
18 },
19 RenameColumnTo {
21 column_name: String,
23 new_column_name: String,
25 },
26 AddColumn {
28 operation: CreateColumnImpl<'until_build, 'post_build>,
30 },
31 DropColumn {
33 name: String,
35 },
36 AlterColumn {
43 name: &'until_build str,
45
46 operation: AlterColumnOperation,
48 },
49}
50
51#[derive(Copy, Clone, Debug)]
61pub enum AlterColumnOperation {
62 SetType {
68 data_type: DbType,
70 },
71
72 SetMaxLength {
77 max_length: i32,
79 },
80
81 DropMaxLength,
83}
84
85pub trait AlterTable<'post_build> {
89 fn build(self) -> Result<Vec<(String, Vec<Value<'post_build>>)>, Error>;
93}
94
95#[derive(Debug)]
99pub struct AlterTableData<'until_build, 'post_build> {
100 pub(crate) name: &'until_build str,
102 pub(crate) operation: AlterTableOperation<'until_build, 'post_build>,
104 pub(crate) lookup: Vec<Value<'post_build>>,
105 pub(crate) statements: Vec<(String, Vec<Value<'post_build>>)>,
106}
107
108#[derive(Debug)]
114pub enum AlterTableImpl<'until_build, 'post_build> {
115 #[cfg(feature = "sqlite")]
119 SQLite(AlterTableData<'until_build, 'post_build>),
120 #[cfg(feature = "postgres")]
124 Postgres(AlterTableData<'until_build, 'post_build>),
125}
126
127impl<'post_build> AlterTable<'post_build> for AlterTableImpl<'_, 'post_build> {
128 fn build(self) -> Result<Vec<(String, Vec<Value<'post_build>>)>, Error> {
129 match self {
130 #[cfg(feature = "sqlite")]
131 AlterTableImpl::SQLite(mut d) => {
132 let mut actions: Vec<String> = Vec::new();
137
138 match d.operation {
139 AlterTableOperation::RenameTo { name } => {
140 actions.push(format!("RENAME TO \"{name}\""));
141 }
142 AlterTableOperation::RenameColumnTo {
143 column_name,
144 new_column_name,
145 } => actions.push(format!(
146 "RENAME COLUMN \"{column_name}\" TO \"{new_column_name}\""
147 )),
148 AlterTableOperation::AddColumn { mut operation } => {
149 let mut action = String::from("ADD COLUMN ");
150
151 if let CreateColumnImpl::SQLite(ccd) = &mut operation {
152 ccd.statements = Some(&mut d.statements);
153 ccd.lookup = Some(&mut d.lookup);
154 }
155
156 operation.build(&mut action)?;
157 actions.push(action);
158 }
159 AlterTableOperation::DropColumn { name } => {
160 actions.push(format!("DROP COLUMN \"{name}\""))
161 }
162 AlterTableOperation::AlterColumn { .. } => {}
166 };
167
168 Ok(finish(d.name, actions, d.lookup, d.statements))
169 }
170 #[cfg(feature = "postgres")]
171 AlterTableImpl::Postgres(mut d) => {
172 let mut actions: Vec<String> = Vec::new();
174
175 match d.operation {
176 AlterTableOperation::RenameTo { name } => {
177 actions.push(format!("RENAME TO \"{name}\""));
178 }
179 AlterTableOperation::RenameColumnTo {
180 column_name,
181 new_column_name,
182 } => {
183 actions.push(format!(
184 "RENAME COLUMN \"{column_name}\" TO \"{new_column_name}\""
185 ));
186 }
187 AlterTableOperation::AddColumn { mut operation } => {
188 let mut action = String::from("ADD COLUMN ");
189
190 #[allow(irrefutable_let_patterns)]
191 if let CreateColumnImpl::Postgres(ccd) = &mut operation {
192 ccd.statements = Some(&mut d.statements);
193 }
194
195 operation.build(&mut action)?;
196 actions.push(action);
197 }
198 AlterTableOperation::DropColumn { name } => {
199 actions.push(format!("DROP COLUMN \"{name}\""))
200 }
201 AlterTableOperation::AlterColumn { name, operation } => {
202 actions.push(match operation {
203 AlterColumnOperation::SetType { data_type } => {
204 #[allow(deprecated)]
208 let unrenderable = match data_type {
209 DbType::VarChar => {
210 Some("its maximum length is part of its type")
211 }
212 DbType::Choices => {
213 Some("its enum type belongs to the column creating it")
214 }
215 _ => None,
216 };
217 if let Some(reason) = unrenderable {
218 return Err(Error::SQLBuildError(format!(
219 "Column \"{name}\" can't be given the type \
220 {data_type:?}: {reason}"
221 )));
222 }
223
224 let data_type = match create_column::postgres_type(data_type, [])? {
225 PostgresType::Normal(x) => x,
226 PostgresType::Choices(_) => {
227 unreachable!("Choices is rejected above")
228 }
229 };
230
231 format!("ALTER COLUMN \"{name}\" TYPE {data_type}")
232 }
233 AlterColumnOperation::SetMaxLength { max_length } => format!(
234 "ADD CONSTRAINT \"{}\" CHECK (length(\"{name}\") <= {max_length})",
235 postgres::max_length_check_name(d.name, name),
236 ),
237 AlterColumnOperation::DropMaxLength => format!(
238 "DROP CONSTRAINT \"{}\"",
239 postgres::max_length_check_name(d.name, name),
240 ),
241 });
242 }
243 };
244
245 Ok(finish(d.name, actions, d.lookup, d.statements))
246 }
247 }
248 }
249}
250
251#[cfg(any(feature = "sqlite", feature = "postgres"))]
254fn finish<'post_build>(
255 table: &str,
256 actions: Vec<String>,
257 lookup: Vec<Value<'post_build>>,
258 side_statements: Vec<(String, Vec<Value<'post_build>>)>,
259) -> Vec<(String, Vec<Value<'post_build>>)> {
260 let mut statements: Vec<(String, Vec<Value<'post_build>>)> = actions
261 .into_iter()
262 .map(|action| (format!("ALTER TABLE \"{table}\" {action};"), Vec::new()))
263 .collect();
264
265 if let Some((_, first)) = statements.first_mut() {
267 *first = lookup;
268 }
269
270 statements.extend(side_statements);
271 statements
272}
273
274#[cfg(test)]
275mod test {
276 use rorm_declaration::imr::{Annotation, DbType};
277
278 use crate::alter_table::{AlterColumnOperation, AlterTable, AlterTableOperation};
279 use crate::error::Error;
280 use crate::DBImpl;
281
282 fn normalize(sql: &str) -> String {
289 sql.split_whitespace()
290 .collect::<Vec<_>>()
291 .join(" ")
292 .replace(" ;", ";")
293 .replace(" ,", ",")
294 }
295
296 fn alter(db: DBImpl, operation: AlterTableOperation) -> Vec<String> {
298 db.alter_table("user", operation)
299 .build()
300 .expect("The operation builds")
301 .into_iter()
302 .map(|(statement, _)| normalize(&statement))
303 .collect()
304 }
305
306 fn alter_err(db: DBImpl, operation: AlterTableOperation) -> Error {
307 db.alter_table("user", operation)
308 .build()
309 .expect_err("The operation doesn't build")
310 }
311
312 fn alter_column(operation: AlterColumnOperation) -> AlterTableOperation<'static, 'static> {
313 AlterTableOperation::AlterColumn {
314 name: "login",
315 operation,
316 }
317 }
318
319 fn assert_common(db: DBImpl) {
321 assert_eq!(
322 alter(
323 db,
324 AlterTableOperation::RenameTo {
325 name: "person".to_string()
326 }
327 ),
328 [r#"ALTER TABLE "user" RENAME TO "person";"#]
329 );
330 assert_eq!(
331 alter(
332 db,
333 AlterTableOperation::RenameColumnTo {
334 column_name: "login".to_string(),
335 new_column_name: "username".to_string(),
336 }
337 ),
338 [r#"ALTER TABLE "user" RENAME COLUMN "login" TO "username";"#]
339 );
340 assert_eq!(
341 alter(
342 db,
343 AlterTableOperation::DropColumn {
344 name: "login".to_string()
345 }
346 ),
347 [r#"ALTER TABLE "user" DROP COLUMN "login";"#]
348 );
349 }
350
351 #[cfg(feature = "sqlite")]
352 mod sqlite {
353 use super::*;
354
355 #[test]
356 fn the_existing_operations_are_unchanged() {
357 assert_common(DBImpl::SQLite);
358 assert_eq!(
359 alter(
360 DBImpl::SQLite,
361 AlterTableOperation::AddColumn {
362 operation: DBImpl::SQLite.create_column(
363 "user",
364 "login",
365 DbType::Text,
366 &[Annotation::MaxLength(255), Annotation::NotNull],
367 ),
368 }
369 ),
370 [r#"ALTER TABLE "user" ADD COLUMN "login" TEXT NOT NULL;"#]
371 );
372 }
373
374 #[test]
377 fn every_alter_column_operation_is_a_noop() {
378 let cases = [
379 AlterColumnOperation::SetType {
380 data_type: DbType::Text,
381 },
382 AlterColumnOperation::SetType {
383 data_type: DbType::Int64,
384 },
385 AlterColumnOperation::SetMaxLength { max_length: 255 },
386 AlterColumnOperation::DropMaxLength,
387 ];
388 for operation in cases {
389 assert_eq!(
390 alter(DBImpl::SQLite, alter_column(operation)),
391 Vec::<String>::new(),
392 "{operation:?}"
393 );
394 }
395 }
396
397 #[test]
399 fn an_unrenderable_type_is_a_noop_too() {
400 #[allow(deprecated)]
401 for data_type in [DbType::VarChar, DbType::Choices] {
402 assert_eq!(
403 alter(
404 DBImpl::SQLite,
405 alter_column(AlterColumnOperation::SetType { data_type })
406 ),
407 Vec::<String>::new(),
408 "{data_type:?}"
409 );
410 }
411 }
412 }
413
414 #[cfg(feature = "postgres")]
415 mod postgres {
416 use super::*;
417
418 #[test]
419 fn the_existing_operations_are_unchanged() {
420 assert_common(DBImpl::Postgres);
421 assert_eq!(
422 alter(
423 DBImpl::Postgres,
424 AlterTableOperation::AddColumn {
425 operation: DBImpl::Postgres.create_column(
426 "user",
427 "login",
428 DbType::Text,
429 &[Annotation::MaxLength(255), Annotation::NotNull],
430 ),
431 }
432 ),
433 [
434 r#"ALTER TABLE "user" ADD COLUMN "login" text CONSTRAINT "user_login_max_length" CHECK (length("login") <= 255) NOT NULL;"#
435 ]
436 );
437 }
438
439 #[test]
442 fn every_operation_is_a_single_statement() {
443 assert_eq!(
444 alter(
445 DBImpl::Postgres,
446 alter_column(AlterColumnOperation::SetType {
447 data_type: DbType::Text
448 })
449 ),
450 [r#"ALTER TABLE "user" ALTER COLUMN "login" TYPE text;"#]
451 );
452 assert_eq!(
453 alter(
454 DBImpl::Postgres,
455 alter_column(AlterColumnOperation::SetMaxLength { max_length: 255 })
456 ),
457 [
458 r#"ALTER TABLE "user" ADD CONSTRAINT "user_login_max_length" CHECK (length("login") <= 255);"#
459 ]
460 );
461 assert_eq!(
462 alter(
463 DBImpl::Postgres,
464 alter_column(AlterColumnOperation::DropMaxLength)
465 ),
466 [r#"ALTER TABLE "user" DROP CONSTRAINT "user_login_max_length";"#]
467 );
468 }
469
470 #[test]
474 fn dropping_a_max_length_is_not_conditional() {
475 let statements = alter(
476 DBImpl::Postgres,
477 alter_column(AlterColumnOperation::DropMaxLength),
478 );
479 assert!(!statements[0].contains("IF EXISTS"), "{statements:?}");
480 }
481
482 #[test]
484 fn widening_an_integer() {
485 assert_eq!(
486 alter(
487 DBImpl::Postgres,
488 alter_column(AlterColumnOperation::SetType {
489 data_type: DbType::Int64
490 })
491 ),
492 [r#"ALTER TABLE "user" ALTER COLUMN "login" TYPE bigint;"#]
493 );
494 }
495
496 #[test]
501 fn a_type_is_never_rendered_as_serial() {
502 for (data_type, expected) in [
503 (DbType::Int16, "smallint"),
504 (DbType::Int32, "integer"),
505 (DbType::Int64, "bigint"),
506 ] {
507 assert_eq!(
508 alter(
509 DBImpl::Postgres,
510 alter_column(AlterColumnOperation::SetType { data_type })
511 ),
512 [format!(
513 r#"ALTER TABLE "user" ALTER COLUMN "login" TYPE {expected};"#
514 )]
515 );
516 }
517 }
518
519 #[test]
522 fn setting_a_varchar_type_is_an_error() {
523 #[allow(deprecated)]
524 let operation = AlterColumnOperation::SetType {
525 data_type: DbType::VarChar,
526 };
527 assert!(matches!(
528 alter_err(DBImpl::Postgres, alter_column(operation)),
529 Error::SQLBuildError(msg) if msg.contains("maximum length")
530 ));
531 }
532
533 #[test]
535 fn setting_an_enum_type_is_an_error() {
536 assert!(matches!(
537 alter_err(
538 DBImpl::Postgres,
539 alter_column(AlterColumnOperation::SetType {
540 data_type: DbType::Choices
541 })
542 ),
543 Error::SQLBuildError(msg) if msg.contains("enum type")
544 ));
545 }
546 }
547}