Skip to main content

vespertide_core/action/
prefix.rs

1use super::{MigrationAction, MigrationPlan};
2use crate::schema::TableName;
3
4impl MigrationPlan {
5    /// Apply a prefix to all table names in the migration plan.
6    /// This modifies all table references in all actions.
7    pub fn with_prefix(self, prefix: &str) -> Self {
8        if prefix.is_empty() {
9            return self;
10        }
11        Self {
12            actions: self
13                .actions
14                .into_iter()
15                .map(|action| action.with_prefix(prefix))
16                .collect(),
17            ..self
18        }
19    }
20}
21
22impl MigrationAction {
23    /// Apply a prefix to all table names in this action.
24    pub fn with_prefix(self, prefix: &str) -> Self {
25        if prefix.is_empty() {
26            return self;
27        }
28
29        prefix_migration_action(self, prefix)
30    }
31}
32
33fn prefix_migration_action(action: MigrationAction, prefix: &str) -> MigrationAction {
34    match action {
35        MigrationAction::CreateTable {
36            table,
37            columns,
38            constraints,
39        } => MigrationAction::CreateTable {
40            table: add_prefix(table, prefix),
41            columns,
42            constraints: constraints
43                .into_iter()
44                .map(|c| c.with_prefix(prefix))
45                .collect(),
46        },
47        MigrationAction::DeleteTable { table } => MigrationAction::DeleteTable {
48            table: add_prefix(table, prefix),
49        },
50        MigrationAction::RenameTable { from, to } => MigrationAction::RenameTable {
51            from: add_prefix(from, prefix),
52            to: add_prefix(to, prefix),
53        },
54        MigrationAction::RawSql { sql } => MigrationAction::RawSql { sql },
55        action => prefix_column_or_constraint_action(action, prefix),
56    }
57}
58
59fn prefix_column_or_constraint_action(action: MigrationAction, prefix: &str) -> MigrationAction {
60    match action {
61        MigrationAction::AddColumn {
62            table,
63            column,
64            fill_with,
65        } => MigrationAction::AddColumn {
66            table: add_prefix(table, prefix),
67            column,
68            fill_with,
69        },
70        MigrationAction::RenameColumn { table, from, to } => MigrationAction::RenameColumn {
71            table: add_prefix(table, prefix),
72            from,
73            to,
74        },
75        MigrationAction::DeleteColumn { table, column } => MigrationAction::DeleteColumn {
76            table: add_prefix(table, prefix),
77            column,
78        },
79        MigrationAction::ModifyColumnType {
80            table,
81            column,
82            new_type,
83            fill_with,
84            narrowing_strategy,
85            timezone,
86        } => MigrationAction::ModifyColumnType {
87            table: add_prefix(table, prefix),
88            column,
89            new_type,
90            fill_with,
91            narrowing_strategy,
92            timezone,
93        },
94        MigrationAction::ModifyColumnNullable {
95            table,
96            column,
97            nullable,
98            fill_with,
99            delete_null_rows,
100        } => MigrationAction::ModifyColumnNullable {
101            table: add_prefix(table, prefix),
102            column,
103            nullable,
104            fill_with,
105            delete_null_rows,
106        },
107        action => prefix_remaining_action(action, prefix),
108    }
109}
110
111fn prefix_remaining_action(action: MigrationAction, prefix: &str) -> MigrationAction {
112    match action {
113        MigrationAction::ModifyColumnDefault {
114            table,
115            column,
116            new_default,
117            backfill,
118        } => MigrationAction::ModifyColumnDefault {
119            table: add_prefix(table, prefix),
120            column,
121            new_default,
122            backfill,
123        },
124        MigrationAction::ModifyColumnComment {
125            table,
126            column,
127            new_comment,
128        } => MigrationAction::ModifyColumnComment {
129            table: add_prefix(table, prefix),
130            column,
131            new_comment,
132        },
133        MigrationAction::AddConstraint { table, constraint } => MigrationAction::AddConstraint {
134            table: format!("{prefix}{table}").into(),
135            constraint: constraint.with_prefix(prefix),
136        },
137        MigrationAction::RemoveConstraint { table, constraint } => {
138            MigrationAction::RemoveConstraint {
139                table: add_prefix(table, prefix),
140                constraint: constraint.with_prefix(prefix),
141            }
142        }
143        MigrationAction::ReplaceConstraint { table, from, to } => {
144            MigrationAction::ReplaceConstraint {
145                table: add_prefix(table, prefix),
146                from: from.with_prefix(prefix),
147                to: to.with_prefix(prefix),
148            }
149        }
150        other => other,
151    }
152}
153
154fn add_prefix(table: TableName, prefix: &str) -> TableName {
155    let mut table = table.into_inner();
156    table.insert_str(0, prefix);
157    table.into()
158}
159
160#[cfg(test)]
161mod tests {
162    //! Coverage-closure tests for the `RawSql` arm of `prefix_migration_action`.
163    //! Targets `uncovered-detail.json` line 54.
164    use super::*;
165
166    #[test]
167    fn raw_sql_with_prefix_is_a_noop_on_sql_body() {
168        // Hits line 54 — the RawSql arm of prefix_migration_action.
169        let action = MigrationAction::RawSql {
170            sql: "SELECT 1".to_string(),
171        };
172        let prefixed = action.with_prefix("p_");
173        match prefixed {
174            MigrationAction::RawSql { sql } => assert_eq!(sql, "SELECT 1"),
175            other => panic!("expected RawSql, got {other:?}"),
176        }
177    }
178
179    #[test]
180    fn raw_sql_within_plan_with_prefix_preserves_sql() {
181        // Drives the same RawSql arm via MigrationPlan::with_prefix.
182        let plan = MigrationPlan {
183            id: String::new(),
184            comment: None,
185            created_at: None,
186            version: 1,
187            actions: vec![MigrationAction::RawSql {
188                sql: "UPDATE x SET y = 1".to_string(),
189            }],
190        };
191        let prefixed = plan.with_prefix("tenant_");
192        match prefixed.actions.into_iter().next() {
193            Some(MigrationAction::RawSql { sql }) => assert_eq!(sql, "UPDATE x SET y = 1"),
194            other => panic!("expected RawSql, got {other:?}"),
195        }
196    }
197
198    #[test]
199    fn remap_enum_values_with_prefix_passes_through_catch_all() {
200        // Drives line 54 — the `other => other` catch-all of
201        // `prefix_remaining_action`. `RemapEnumValues` is not explicitly
202        // handled by any of the three prefix dispatchers, so it must fall
203        // through unchanged.
204        let action = MigrationAction::RemapEnumValues {
205            table: "user".into(),
206            column: "status".into(),
207            mapping: vec![(1_i64, 2_i64)].into_iter().collect(),
208        };
209        let prefixed = action.with_prefix("t_");
210        match prefixed {
211            MigrationAction::RemapEnumValues {
212                table,
213                column,
214                mapping,
215            } => {
216                // Catch-all arm does NOT rewrite the table name (by
217                // design — the action is the prefix-agnostic enum
218                // value-remap, table identifiers are left to whichever
219                // sibling action carries the rename).
220                assert_eq!(table.as_str(), "user");
221                assert_eq!(column.as_str(), "status");
222                assert_eq!(mapping, vec![(1_i64, 2_i64)].into_iter().collect());
223            }
224            other => panic!("expected RemapEnumValues, got {other:?}"),
225        }
226    }
227}