Skip to main content

rorm_sql/
alter_table.rs

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/**
10Representation of operations to execute in the context of an ALTER TABLE statement.
11 */
12#[derive(Debug)]
13pub enum AlterTableOperation<'until_build, 'post_build> {
14    /// Use this operation to rename a table
15    RenameTo {
16        /// New name of the table
17        name: String,
18    },
19    /// Use this operation to rename a column within a table
20    RenameColumnTo {
21        /// Current column name
22        column_name: String,
23        /// New column name
24        new_column_name: String,
25    },
26    /// Use this operation to add a column to an existing table.
27    AddColumn {
28        /// Operation to use for adding the column
29        operation: CreateColumnImpl<'until_build, 'post_build>,
30    },
31    /// Use this operation to drop an existing column.
32    DropColumn {
33        /// Name of the column to drop
34        name: String,
35    },
36    /**
37    Use this operation to change an existing column in place, preserving its data.
38
39    Which change to make is decided by the caller: this operation renders the
40    single statement it is given and nothing else.
41    */
42    AlterColumn {
43        /// Name of the column to alter
44        name: &'until_build str,
45
46        /// The change to apply to it
47        operation: AlterColumnOperation,
48    },
49}
50
51/**
52Representation of a single change to an existing column.
53
54Each variant renders into exactly one `ALTER TABLE` statement, or into none at
55all in sqlite: it has no `ALTER COLUMN`, and none of these changes are
56observable in a `STRICT` table - [`DbType::VarChar`] and [`DbType::Text`] are
57both `TEXT`, every integer is `INTEGER`, both floats are `REAL`, and a maximum
58length is never enforced.
59 */
60#[derive(Copy, Clone, Debug)]
61pub enum AlterColumnOperation {
62    /// Set the column's type
63    ///
64    /// The type has to be one which is fully described by itself. A
65    /// [`DbType::VarChar`] carries its maximum length and a [`DbType::Choices`]
66    /// its enum, so neither can be rendered from the type alone.
67    SetType {
68        /// The type to change the column to
69        data_type: DbType,
70    },
71
72    /// Add the check constraint enforcing the column's maximum length
73    ///
74    /// A column which already has one has to [drop](AlterColumnOperation::DropMaxLength)
75    /// it first: a constraint can't be redefined, only replaced.
76    SetMaxLength {
77        /// The maximum number of characters the column may hold
78        max_length: i32,
79    },
80
81    /// Drop the check constraint enforcing the column's maximum length
82    DropMaxLength,
83}
84
85/**
86The trait representing an alter table builder
87*/
88pub trait AlterTable<'post_build> {
89    /**
90    This method is used to build the alter table statement.
91     */
92    fn build(self) -> Result<Vec<(String, Vec<Value<'post_build>>)>, Error>;
93}
94
95/**
96Representation of the data of an ALTER TABLE statement.
97 */
98#[derive(Debug)]
99pub struct AlterTableData<'until_build, 'post_build> {
100    /// Name of the table to operate on
101    pub(crate) name: &'until_build str,
102    /// Operation to execute
103    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/**
109Implementation of the [AlterTable] trait for the different database dialects.
110
111Should only be constructed via [crate::DBImpl::alter_table].
112 */
113#[derive(Debug)]
114pub enum AlterTableImpl<'until_build, 'post_build> {
115    /**
116    SQLite representation of the ALTER TABLE operation.
117     */
118    #[cfg(feature = "sqlite")]
119    SQLite(AlterTableData<'until_build, 'post_build>),
120    /**
121    Postgres representation of the ALTER TABLE operation.
122     */
123    #[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                // The actions to execute, each as its own ALTER TABLE statement.
133                //
134                // It stays empty if the operation is a no-op for this dialect,
135                // in which case no statement is produced at all.
136                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                    // Deliberately a no-op, see `AlterColumnOperation`.
163                    // Sqlite has no `ALTER COLUMN` and needs none for these
164                    // changes: not one of them is observable in a `STRICT` table.
165                    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                // The actions to execute, each as its own ALTER TABLE statement
173                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                                // A `character varying` carries its maximum
205                                // length and a `Choices` its enum, so for
206                                // neither does the type describe the column.
207                                #[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/// Wraps every action into its own `ALTER TABLE` statement
252/// and appends the statements the operation produced on the side.
253#[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    // Only `AddColumn` can bind values, and it produces a single action
266    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    /// Collapses insignificant whitespace in `sql`
283    ///
284    /// Sql ignores whitespace, so the builders are free to leave a separator
285    /// behind an annotation which rendered nothing, and the assertions here
286    /// shouldn't have to spell those out. (Which also means they must not
287    /// contain string literals.)
288    fn normalize(sql: &str) -> String {
289        sql.split_whitespace()
290            .collect::<Vec<_>>()
291            .join(" ")
292            .replace(" ;", ";")
293            .replace(" ,", ",")
294    }
295
296    /// The statements `db` produces for `operation` on the `user` table
297    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    /// Both dialects share these, so they are asserted for whichever is built
320    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        /// Sqlite has no `ALTER COLUMN` and needs none: not one of these
375        /// changes is observable in a `STRICT` table.
376        #[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        /// Not even the types postgres refuses may produce anything
398        #[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        /// Every operation renders into exactly one statement,
440        /// just like the four which existed before.
441        #[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        /// The caller decides whether a constraint exists, so the drop is
471        /// unqualified: a `DROP CONSTRAINT IF EXISTS` would paper over a
472        /// migration which got its own delta wrong.
473        #[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        /// Widening an integer, the other alterable type change
483        #[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        /// `serial` is not a type but a column with its own sequence, so an
497        /// `auto_increment` column must never be given one. That is a migration
498        /// decision though, so rorm-cli makes it - here the base type is
499        /// rendered, which is the only correct thing for an `ALTER COLUMN`.
500        #[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        /// A `character varying` carries its maximum length in its type,
520        /// so the type alone doesn't describe the column.
521        #[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        /// An enum's type is only known to the column which creates it
534        #[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}