Skip to main content

vespertide_core/action/
prefix.rs

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