Skip to main content

vespertide_query/sql/
mod.rs

1pub mod add_column;
2pub mod add_constraint;
3pub mod create_table;
4pub mod delete_column;
5pub mod delete_table;
6pub mod helpers;
7pub mod modify_column_comment;
8pub mod modify_column_default;
9pub mod modify_column_nullable;
10pub mod modify_column_type;
11pub mod raw_sql;
12pub mod remap_enum_values;
13pub mod remove_constraint;
14pub mod rename_column;
15pub mod rename_table;
16pub mod replace_constraint;
17pub mod types;
18
19pub use helpers::*;
20pub use types::{BuiltQuery, DatabaseBackend, RawSql};
21
22use crate::error::QueryError;
23use vespertide_core::{MigrationAction, TableConstraint, TableDef};
24
25use self::{
26    add_column::build_add_column, add_constraint::build_add_constraint,
27    create_table::build_create_table, delete_column::build_delete_column,
28    delete_table::build_delete_table, modify_column_comment::build_modify_column_comment,
29    modify_column_default::build_modify_column_default,
30    modify_column_nullable::build_modify_column_nullable,
31    remap_enum_values::build_remap_enum_values, remove_constraint::build_remove_constraint,
32    rename_column::build_rename_column, rename_table::build_rename_table,
33    replace_constraint::build_replace_constraint,
34};
35
36/// Build SQL for a single migration action against a known schema.
37///
38/// For multi-action plans use [`crate::build_plan_queries`] which handles
39/// schema evolution between actions.
40pub fn build_action_queries(
41    backend: DatabaseBackend,
42    action: &MigrationAction,
43    current_schema: &[TableDef],
44) -> Result<Vec<BuiltQuery>, QueryError> {
45    build_action_queries_with_pending(backend, action, current_schema, &[])
46}
47
48/// Build SQL queries for a migration action, with awareness of pending constraints.
49///
50/// `pending_constraints` are constraints that exist in the logical schema but haven't been
51/// physically created as database indexes yet. This is used by `SQLite` temp table rebuilds
52/// to avoid recreating indexes that will be created by future `AddConstraint` actions.
53#[expect(
54    clippy::too_many_lines,
55    reason = "flat 14-variant MigrationAction dispatcher kept inline so the variant→builder mapping stays auditable; extracting individual arms scatters the routing logic"
56)]
57pub fn build_action_queries_with_pending(
58    backend: DatabaseBackend,
59    action: &MigrationAction,
60    current_schema: &[TableDef],
61    pending_constraints: &[TableConstraint],
62) -> Result<Vec<BuiltQuery>, QueryError> {
63    match action {
64        MigrationAction::CreateTable {
65            table,
66            columns,
67            constraints,
68        } => build_create_table(backend, table, columns, constraints),
69
70        MigrationAction::DeleteTable { table } => Ok(vec![build_delete_table(table)]),
71
72        MigrationAction::AddColumn {
73            table,
74            column,
75            fill_with,
76        } => build_add_column(
77            backend,
78            table,
79            column,
80            fill_with.as_deref(),
81            current_schema,
82            pending_constraints,
83        ),
84
85        MigrationAction::RenameColumn { table, from, to } => {
86            Ok(vec![build_rename_column(table, from, to)])
87        }
88
89        MigrationAction::DeleteColumn { table, column } => {
90            // Find the column type from current schema for enum DROP TYPE support
91            let column_type = current_schema
92                .iter()
93                .find(|t| t.name == *table)
94                .and_then(|t| t.columns.iter().find(|c| c.name == *column))
95                .map(|c| &c.r#type);
96            Ok(build_delete_column(
97                backend,
98                table,
99                column,
100                column_type,
101                current_schema,
102                pending_constraints,
103            ))
104        }
105
106        MigrationAction::ModifyColumnType {
107            table,
108            column,
109            new_type,
110            fill_with,
111            narrowing_strategy,
112            timezone,
113        } => modify_column_type::build_with_narrowing_preprocess(
114            backend,
115            table.as_str(),
116            column.as_str(),
117            new_type,
118            fill_with.as_ref(),
119            narrowing_strategy.as_ref(),
120            timezone.as_deref(),
121            current_schema,
122            pending_constraints,
123        ),
124
125        MigrationAction::ModifyColumnNullable {
126            table,
127            column,
128            nullable,
129            fill_with,
130            delete_null_rows,
131        } => build_modify_column_nullable(
132            backend,
133            table,
134            column,
135            *nullable,
136            fill_with.as_deref(),
137            delete_null_rows.unwrap_or(false),
138            current_schema,
139            pending_constraints,
140        ),
141
142        MigrationAction::ModifyColumnDefault {
143            table,
144            column,
145            new_default,
146            backfill,
147        } => build_modify_column_default(
148            backend,
149            table,
150            column,
151            new_default.as_deref(),
152            backfill.as_deref(),
153            current_schema,
154            pending_constraints,
155        ),
156
157        MigrationAction::ModifyColumnComment {
158            table,
159            column,
160            new_comment,
161        } => build_comment_action_queries(
162            backend,
163            table,
164            column,
165            new_comment.as_ref(),
166            current_schema,
167        ),
168
169        MigrationAction::RenameTable { from, to } => Ok(vec![build_rename_table(from, to)]),
170
171        MigrationAction::RawSql { sql } => Ok(vec![BuiltQuery::Raw(RawSql::uniform(sql.clone()))]),
172
173        MigrationAction::AddConstraint { .. }
174        | MigrationAction::RemoveConstraint { .. }
175        | MigrationAction::ReplaceConstraint { .. } => {
176            build_constraint_action_queries(backend, action, current_schema, pending_constraints)
177        }
178
179        MigrationAction::RemapEnumValues {
180            table,
181            column,
182            mapping,
183        } => build_remap_enum_values(backend, table.as_str(), column.as_str(), mapping),
184
185        _ => unreachable!("MigrationAction is #[non_exhaustive]; all variants are matched above"),
186    }
187}
188
189fn build_comment_action_queries(
190    backend: DatabaseBackend,
191    table: &str,
192    column: &str,
193    new_comment: Option<&String>,
194    current_schema: &[TableDef],
195) -> Result<Vec<BuiltQuery>, QueryError> {
196    build_modify_column_comment(
197        backend,
198        table,
199        column,
200        new_comment.map(String::as_str),
201        current_schema,
202    )
203}
204
205fn build_constraint_action_queries(
206    backend: DatabaseBackend,
207    action: &MigrationAction,
208    current_schema: &[TableDef],
209    pending_constraints: &[TableConstraint],
210) -> Result<Vec<BuiltQuery>, QueryError> {
211    match action {
212        MigrationAction::AddConstraint { table, constraint } => build_add_constraint(
213            backend,
214            table,
215            constraint,
216            current_schema,
217            pending_constraints,
218        ),
219        MigrationAction::RemoveConstraint { table, constraint } => build_remove_constraint(
220            backend,
221            table,
222            constraint,
223            current_schema,
224            pending_constraints,
225        ),
226        MigrationAction::ReplaceConstraint { table, from, to } => build_replace_constraint(
227            backend,
228            table,
229            from,
230            to,
231            current_schema,
232            pending_constraints,
233        ),
234        _ => unreachable!("only constraint actions are dispatched here"),
235    }
236}
237
238#[cfg(test)]
239mod tests;