Skip to main content

rust_ef/migration/
engine_sql.rs

1//! `MigrationEngine` SQL generation — Up/Down DDL from `SchemaChange` lists.
2
3use super::dialect::MigrationDialect;
4use super::diff::index_name;
5use super::engine::MigrationEngine;
6use super::history::{MIGRATION_HISTORY_TABLE, PRODUCT_VERSION};
7use super::schema_change::SchemaChange;
8use super::types::{ModelSnapshot, SnapshotColumn};
9
10impl MigrationEngine {
11    pub(crate) fn initial_create_with_fks(&self, current: &ModelSnapshot) -> Vec<SchemaChange> {
12        let mut changes = self.initial_create(current);
13        for et in &current.entity_types {
14            Self::append_create_table_fks(&mut changes, &et.table_name, &et.columns);
15            Self::append_create_table_indexes(&mut changes, &et.table_name, &et.columns);
16        }
17        changes
18    }
19
20    // -----------------------------------------------------------------------
21    // SQL generation
22    // -----------------------------------------------------------------------
23
24    /// Standard foreign-key constraint name used by Add/DropForeignKey.
25    pub fn foreign_key_name(table: &str, column: &str, referenced_table: &str) -> String {
26        format!("fk_{}_{}_{}", table, column, referenced_table)
27    }
28
29    /// Standard index name used by CreateIndex/DropIndex.
30    pub fn index_name(table: &str, column: &str) -> String {
31        format!("ix_{}_{}", table, column)
32    }
33
34    pub fn generate_alter_column_sql(
35        &self,
36        table: &str,
37        column_name: &str,
38        new: &SnapshotColumn,
39    ) -> String {
40        let q = |s: &str| self.dialect.quote(s);
41        let col_type = self.dialect.map_column_type(new);
42        match self.dialect {
43            MigrationDialect::Postgres => {
44                let nullable = if new.is_required {
45                    "SET NOT NULL"
46                } else {
47                    "DROP NOT NULL"
48                };
49                format!(
50                    "ALTER TABLE {} ALTER COLUMN {} TYPE {};\nALTER TABLE {} ALTER COLUMN {} {};\n",
51                    q(table),
52                    q(column_name),
53                    col_type,
54                    q(table),
55                    q(column_name),
56                    nullable
57                )
58            }
59            MigrationDialect::MySql => {
60                let nullable = if new.is_required { "NOT NULL" } else { "NULL" };
61                format!(
62                    "ALTER TABLE {} MODIFY COLUMN {} {} {};\n",
63                    q(table),
64                    q(column_name),
65                    col_type,
66                    nullable
67                )
68            }
69            MigrationDialect::Sqlite => format!(
70                "-- SQLite does not support ALTER COLUMN type changes; \
71                 rebuild table manually for {}.{}\n",
72                q(table),
73                q(column_name)
74            ),
75        }
76    }
77
78    pub(crate) fn generate_up_sql(&self, changes: &[SchemaChange]) -> String {
79        self.generate_up_sql_inner(changes, true)
80    }
81
82    pub(crate) fn generate_ddl_sql(&self, changes: &[SchemaChange]) -> String {
83        self.generate_up_sql_inner(changes, false)
84    }
85
86    fn generate_up_sql_inner(&self, changes: &[SchemaChange], record_history: bool) -> String {
87        let mut sql = String::from("-- Up Migration (auto-generated by rust-ef)\n\n");
88        let q = |s: &str| self.dialect.quote(s);
89        let create_kw = if record_history {
90            "CREATE TABLE"
91        } else {
92            "CREATE TABLE IF NOT EXISTS"
93        };
94
95        for change in changes {
96            match change {
97                SchemaChange::CreateTable { table, columns } => {
98                    // PostgreSQL: create sequences before the table
99                    if self.dialect == MigrationDialect::Postgres {
100                        for c in columns {
101                            if c.is_sequence {
102                                if let Some(seq_name) = &c.sequence_name {
103                                    sql.push_str(&format!(
104                                        "CREATE SEQUENCE IF NOT EXISTS {};\n",
105                                        q(seq_name)
106                                    ));
107                                }
108                            }
109                        }
110                    }
111
112                    sql.push_str(&format!("{} {} (\n", create_kw, q(table)));
113
114                    // Separate primary key columns from regular columns
115                    let pk_columns: Vec<&str> = columns
116                        .iter()
117                        .filter(|c| c.is_primary_key)
118                        .map(|c| c.column_name.as_str())
119                        .collect();
120
121                    let col_defs: Vec<String> = columns
122                        .iter()
123                        .map(|c| {
124                            let nullable = if c.is_required { "NOT NULL" } else { "NULL" };
125                            let col_type = self.dialect.map_column_type(c);
126                            // PostgreSQL sequence column: add DEFAULT nextval('seq_name')
127                            if c.is_sequence && self.dialect == MigrationDialect::Postgres {
128                                if let Some(seq_name) = &c.sequence_name {
129                                    return format!(
130                                        "{} {} DEFAULT nextval('{}') {}",
131                                        q(&c.column_name),
132                                        col_type,
133                                        seq_name,
134                                        nullable
135                                    );
136                                }
137                            }
138                            // Don't put PRIMARY KEY on individual columns; handle separately
139                            [q(&c.column_name), col_type, nullable.to_string()]
140                                .into_iter()
141                                .filter(|s| !s.is_empty())
142                                .collect::<Vec<_>>()
143                                .join(" ")
144                        })
145                        .collect();
146                    sql.push_str(&format!("    {}\n", col_defs.join(",\n    ")));
147
148                    // Composite primary key constraint
149                    if pk_columns.len() == 1 {
150                        sql.push_str(&format!("    ,PRIMARY KEY ({})\n", q(pk_columns[0])));
151                    } else if pk_columns.len() > 1 {
152                        let pk_list: Vec<String> = pk_columns.iter().map(|c| q(c)).collect();
153                        sql.push_str(&format!("    ,PRIMARY KEY ({})\n", pk_list.join(", ")));
154                    }
155
156                    sql.push_str(");\n\n");
157                }
158                SchemaChange::DropTable { table } => {
159                    sql.push_str(&format!("DROP TABLE IF EXISTS {};\n", q(table)));
160                }
161                SchemaChange::AddColumn { table, column } => {
162                    let col_type = self.dialect.map_column_type(column);
163                    let nullable = if column.is_required {
164                        "NOT NULL"
165                    } else {
166                        "NULL"
167                    };
168                    sql.push_str(&format!(
169                        "ALTER TABLE {} ADD COLUMN {} {} {};\n",
170                        q(table),
171                        q(&column.column_name),
172                        col_type,
173                        nullable
174                    ));
175                }
176                SchemaChange::DropColumn { table, column_name } => {
177                    sql.push_str(&format!(
178                        "ALTER TABLE {} DROP COLUMN IF EXISTS {};\n",
179                        q(table),
180                        q(column_name)
181                    ));
182                }
183                SchemaChange::AlterColumn {
184                    table,
185                    column_name,
186                    old: _,
187                    new,
188                } => {
189                    sql.push_str(&self.generate_alter_column_sql(table, column_name, new));
190                }
191                SchemaChange::AddForeignKey {
192                    table,
193                    column,
194                    referenced_table,
195                    referenced_column,
196                    on_delete,
197                } => {
198                    let fk_name = Self::foreign_key_name(table, column, referenced_table);
199                    let on_delete_clause = on_delete
200                        .as_deref()
201                        .map(|c| format!(" ON DELETE {}", c))
202                        .unwrap_or_default();
203                    sql.push_str(&format!(
204                        "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}){};\n",
205                        q(table),
206                        q(&fk_name),
207                        q(column),
208                        q(referenced_table),
209                        q(referenced_column),
210                        on_delete_clause
211                    ));
212                }
213                SchemaChange::DropForeignKey {
214                    table,
215                    column,
216                    referenced_table,
217                } => {
218                    let fk_name = Self::foreign_key_name(table, column, referenced_table);
219                    match self.dialect {
220                        MigrationDialect::MySql => sql.push_str(&format!(
221                            "ALTER TABLE {} DROP FOREIGN KEY {};\n",
222                            q(table),
223                            q(&fk_name)
224                        )),
225                        _ => sql.push_str(&format!(
226                            "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};\n",
227                            q(table),
228                            q(&fk_name)
229                        )),
230                    }
231                }
232                SchemaChange::CreateIndex {
233                    table,
234                    column,
235                    is_unique,
236                } => {
237                    let unique_kw = if *is_unique { "UNIQUE " } else { "" };
238                    let idx_name = index_name(table, column);
239                    sql.push_str(&format!(
240                        "CREATE {unique_kw}INDEX {} ON {} ({});\n",
241                        q(&idx_name),
242                        q(table),
243                        q(column)
244                    ));
245                }
246                SchemaChange::DropIndex {
247                    table,
248                    column,
249                    is_unique: _,
250                } => {
251                    let idx_name = index_name(table, column);
252                    match self.dialect {
253                        MigrationDialect::MySql => {
254                            sql.push_str(&format!("DROP INDEX {} ON {};\n", q(&idx_name), q(table)))
255                        }
256                        _ => sql.push_str(&format!("DROP INDEX IF EXISTS {};\n", q(&idx_name))),
257                    }
258                }
259            }
260        }
261
262        if record_history {
263            sql.push_str(&format!(
264                "INSERT INTO {}(migration_id, product_version) VALUES ('{{migration_id}}', '{}');\n",
265                q(MIGRATION_HISTORY_TABLE),
266                PRODUCT_VERSION
267            ));
268        }
269
270        sql
271    }
272
273    pub(super) fn generate_down_sql(&self, changes: &[SchemaChange]) -> String {
274        let mut sql = String::from("-- Down Migration (auto-generated by rust-ef)\n\n");
275        let q = |s: &str| self.dialect.quote(s);
276
277        // Reverse the changes for the down migration
278        for change in changes.iter().rev() {
279            match change {
280                SchemaChange::CreateTable { table, .. } => {
281                    sql.push_str(&format!("DROP TABLE IF EXISTS {};\n", q(table)));
282                }
283                SchemaChange::DropTable { table } => {
284                    sql.push_str(&format!(
285                        "-- WARNING: Cannot restore table {} (original schema unknown)\n",
286                        q(table)
287                    ));
288                }
289                SchemaChange::AddColumn { table, column } => {
290                    sql.push_str(&format!(
291                        "ALTER TABLE {} DROP COLUMN IF EXISTS {};\n",
292                        q(table),
293                        q(&column.column_name)
294                    ));
295                }
296                SchemaChange::DropColumn { table, column_name } => {
297                    sql.push_str(&format!(
298                        "-- WARNING: Cannot restore column {} on {} (original type unknown)\n",
299                        q(column_name),
300                        q(table)
301                    ));
302                }
303                SchemaChange::AlterColumn {
304                    table,
305                    column_name,
306                    old,
307                    new: _,
308                } => {
309                    sql.push_str(&self.generate_alter_column_sql(table, column_name, old));
310                }
311                SchemaChange::AddForeignKey {
312                    table,
313                    column,
314                    referenced_table,
315                    ..
316                } => {
317                    let fk_name = Self::foreign_key_name(table, column, referenced_table);
318                    match self.dialect {
319                        MigrationDialect::MySql => sql.push_str(&format!(
320                            "ALTER TABLE {} DROP FOREIGN KEY {};\n",
321                            q(table),
322                            q(&fk_name)
323                        )),
324                        _ => sql.push_str(&format!(
325                            "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};\n",
326                            q(table),
327                            q(&fk_name)
328                        )),
329                    }
330                }
331                SchemaChange::DropForeignKey {
332                    table,
333                    column,
334                    referenced_table,
335                    ..
336                } => {
337                    sql.push_str(&format!(
338                        "-- WARNING: Cannot restore foreign key constraint {} on {}.{}\n",
339                        q(&Self::foreign_key_name(table, column, referenced_table)),
340                        q(table),
341                        q(column)
342                    ));
343                }
344                SchemaChange::CreateIndex {
345                    table,
346                    column,
347                    is_unique: _,
348                } => {
349                    let idx_name = index_name(table, column);
350                    match self.dialect {
351                        MigrationDialect::MySql => {
352                            sql.push_str(&format!("DROP INDEX {} ON {};\n", q(&idx_name), q(table)))
353                        }
354                        _ => sql.push_str(&format!("DROP INDEX IF EXISTS {};\n", q(&idx_name))),
355                    }
356                }
357                SchemaChange::DropIndex {
358                    table,
359                    column,
360                    is_unique,
361                } => {
362                    let unique_kw = if *is_unique { "UNIQUE " } else { "" };
363                    let idx_name = index_name(table, column);
364                    sql.push_str(&format!(
365                        "CREATE {unique_kw}INDEX {} ON {} ({});\n",
366                        q(&idx_name),
367                        q(table),
368                        q(column)
369                    ));
370                }
371            }
372        }
373
374        sql.push_str(&format!(
375            "DELETE FROM {} WHERE migration_id = '{{migration_id}}';\n",
376            q(MIGRATION_HISTORY_TABLE)
377        ));
378
379        sql
380    }
381}