1use crate::error::MigrationError;
37use crate::operation::Operation;
38use crate::state::{
39 ConstraintState, DbDefault, FieldState, ForeignKeyState, IndexState, ModelState, OnDelete,
40 ProjectState, SqlType, auto_index_name,
41};
42use std::collections::BTreeSet;
43
44pub fn quote_ident(ident: &str) -> String {
46 let mut out = String::with_capacity(ident.len() + 2);
47 out.push('`');
48 for ch in ident.chars() {
49 if ch == '`' {
50 out.push('`');
51 }
52 out.push(ch);
53 }
54 out.push('`');
55 out
56}
57
58pub fn quote_string(value: &str) -> String {
60 let mut out = String::with_capacity(value.len() + 2);
61 out.push('\'');
62 for ch in value.chars() {
63 match ch {
64 '\'' => out.push_str("''"),
65 '\\' => out.push_str("\\\\"),
66 '\0' => out.push_str("\\0"),
67 other => out.push(other),
68 }
69 }
70 out.push('\'');
71 out
72}
73
74pub(super) fn render(op: &Operation, state: &ProjectState) -> Result<Vec<String>, MigrationError> {
81 match op {
82 Operation::CreateModel { model } => create_model_sql(model),
83 Operation::DeleteModel { name } => {
84 let model = state.require(name)?;
85 Ok(vec![format!("DROP TABLE {}", quote_ident(&model.table))])
86 }
87 Operation::RenameModel {
88 old_name, table, ..
89 } => {
90 let model = state.require(old_name)?;
91 if model.table == *table {
92 Ok(Vec::new())
93 } else {
94 Ok(vec![format!(
95 "ALTER TABLE {} RENAME TO {}",
96 quote_ident(&model.table),
97 quote_ident(table)
98 )])
99 }
100 }
101 Operation::AddField { model, field } => add_field_sql(state.require(model)?, field),
102 Operation::RemoveField { model, name } => remove_field_sql(state.require(model)?, name),
103 Operation::AlterField { model, name, field } => {
104 alter_field_sql(state.require(model)?, name, field)
105 }
106 Operation::RenameField {
107 model,
108 old_name,
109 new_name,
110 } => rename_field_sql(state.require(model)?, old_name, new_name),
111 Operation::CreateIndex { model, index } => {
112 let m = state.require(model)?;
113 Ok(vec![create_index_sql(m, index)?])
114 }
115 Operation::DeleteIndex { model, name } => {
116 let m = state.require(model)?;
117 Ok(vec![drop_index_sql(&m.table, name)])
118 }
119 Operation::AddConstraint { model, constraint } => {
120 add_constraint_sql(state.require(model)?, constraint)
121 }
122 Operation::DeleteConstraint { model, name } => {
123 delete_constraint_sql(state.require(model)?, name)
124 }
125 Operation::RunSQL { sql, .. } => Ok(vec![sql.clone()]),
126 Operation::RunRust { .. } => Ok(Vec::new()),
127 }
128}
129
130fn is_unkeyable(field: &FieldState) -> bool {
132 match field.sql_type {
133 SqlType::Text => field.max_length.is_none(),
134 SqlType::Binary | SqlType::Json => true,
135 _ => false,
136 }
137}
138
139fn keyed_error(table: &str, column: &str, why: &str) -> MigrationError {
140 MigrationError::state(format!(
141 "MySQL cannot use column `{column}` of `{table}` as {why} without a length: \
142 give the field a `max_length` so it becomes a VARCHAR"
143 ))
144}
145
146fn ensure_keyable(model: &ModelState, column: &str, why: &str) -> Result<(), MigrationError> {
148 match model.column(column) {
149 Some(field) if is_unkeyable(field) => Err(keyed_error(&model.table, column, why)),
150 _ => Ok(()),
151 }
152}
153
154fn fk_name(table: &str, column: &str) -> String {
155 format!("{table}_{column}_fk")
156}
157
158fn create_model_sql(model: &ModelState) -> Result<Vec<String>, MigrationError> {
159 check_field_keys(model)?;
160 let mut parts = Vec::new();
161 for field in &model.fields {
162 parts.push(column_def(field, ColumnDef::Create)?);
163 }
164 for constraint in &model.constraints {
165 if let ConstraintState::Unique { columns, .. } = constraint {
166 for column in columns {
167 ensure_keyable(model, column, "part of a UNIQUE constraint")?;
168 }
169 }
170 parts.push(table_constraint_sql(constraint));
171 }
172 for field in &model.fields {
173 if let Some(fk) = &field.fk {
174 parts.push(foreign_key_clause(&model.table, &field.column, fk)?);
175 }
176 }
177 let mut stmts = vec![format!(
178 "CREATE TABLE {} (\n {}\n)",
179 quote_ident(&model.table),
180 parts.join(",\n ")
181 )];
182 stmts.extend(index_sqls(model)?);
183 Ok(stmts)
184}
185
186fn check_field_keys(model: &ModelState) -> Result<(), MigrationError> {
188 for field in &model.fields {
189 check_field_key(&model.table, field)?;
190 }
191 Ok(())
192}
193
194fn check_field_key(table: &str, field: &FieldState) -> Result<(), MigrationError> {
195 if !is_unkeyable(field) {
196 return Ok(());
197 }
198 let why = if field.primary_key {
199 "a primary key"
200 } else if field.unique {
201 "UNIQUE"
202 } else if field.index {
203 "indexed"
204 } else {
205 return Ok(());
206 };
207 Err(keyed_error(table, &field.column, why))
208}
209
210fn table_constraint_sql(constraint: &ConstraintState) -> String {
211 match constraint {
212 ConstraintState::Unique { name, columns } => {
213 format!(
214 "CONSTRAINT {} UNIQUE ({})",
215 quote_ident(name),
216 column_list(columns)
217 )
218 }
219 ConstraintState::Check { name, sql } => {
220 format!("CONSTRAINT {} CHECK ({sql})", quote_ident(name))
221 }
222 }
223}
224
225fn column_list(columns: &[String]) -> String {
226 columns
227 .iter()
228 .map(|c| quote_ident(c))
229 .collect::<Vec<_>>()
230 .join(", ")
231}
232
233fn foreign_key_clause(
234 table: &str,
235 column: &str,
236 fk: &ForeignKeyState,
237) -> Result<String, MigrationError> {
238 Ok(format!(
239 "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}) ON DELETE {}",
240 quote_ident(&fk_name(table, column)),
241 quote_ident(column),
242 quote_ident(&fk.target_table),
243 quote_ident(&fk.target_column),
244 on_delete_sql(fk.on_delete, table, column)?
245 ))
246}
247
248fn on_delete_sql(
249 policy: OnDelete,
250 table: &str,
251 column: &str,
252) -> Result<&'static str, MigrationError> {
253 match policy {
254 OnDelete::Cascade => Ok("CASCADE"),
255 OnDelete::Protect => Ok("RESTRICT"),
256 OnDelete::SetNull => Ok("SET NULL"),
257 OnDelete::DoNothing => Ok("NO ACTION"),
258 OnDelete::SetDefault => Err(MigrationError::state(format!(
259 "MySQL (InnoDB) does not support ON DELETE SET DEFAULT (column `{column}` of `{table}`)"
260 ))),
261 }
262}
263
264#[derive(Clone, Copy, PartialEq, Eq)]
266enum ColumnDef {
267 Create,
269 Modify,
271}
272
273fn column_def(field: &FieldState, mode: ColumnDef) -> Result<String, MigrationError> {
274 let mut s = format!("{} {}", quote_ident(&field.column), type_sql(field));
275 if field.auto && field.primary_key {
276 s.push_str(" NOT NULL AUTO_INCREMENT");
277 if mode == ColumnDef::Create {
278 s.push_str(" PRIMARY KEY");
279 }
280 return Ok(s);
281 }
282 if !field.nullable {
283 s.push_str(" NOT NULL");
284 }
285 if let Some(default) = &field.default {
286 s.push_str(" DEFAULT ");
287 s.push_str(&default_sql(field, default));
288 }
289 if mode == ColumnDef::Create {
290 if field.unique && !field.primary_key {
291 s.push_str(" UNIQUE");
292 }
293 if field.primary_key {
294 s.push_str(" PRIMARY KEY");
295 }
296 }
297 Ok(s)
298}
299
300fn type_sql(field: &FieldState) -> String {
301 match field.sql_type {
302 SqlType::SmallInt => "SMALLINT".to_owned(),
303 SqlType::Integer => "INT".to_owned(),
304 SqlType::BigInt | SqlType::Duration => "BIGINT".to_owned(),
305 SqlType::Real => "FLOAT".to_owned(),
306 SqlType::Double => "DOUBLE".to_owned(),
307 SqlType::Decimal => match (field.max_digits, field.decimal_places) {
308 (Some(p), Some(s)) => format!("DECIMAL({p},{s})"),
309 _ => "DECIMAL(38,10)".to_owned(),
310 },
311 SqlType::Bool => "TINYINT(1)".to_owned(),
312 SqlType::Text => match field.max_length {
313 Some(n) => format!("VARCHAR({n})"),
314 None => "TEXT".to_owned(),
315 },
316 SqlType::Binary => "BLOB".to_owned(),
317 SqlType::Date => "DATE".to_owned(),
318 SqlType::Time => "TIME(6)".to_owned(),
319 SqlType::Timestamp => "DATETIME(6)".to_owned(),
320 SqlType::Uuid => "CHAR(36)".to_owned(),
321 SqlType::Json => "JSON".to_owned(),
322 SqlType::IpAddr => "VARCHAR(45)".to_owned(),
323 }
324}
325
326fn default_sql(field: &FieldState, default: &DbDefault) -> String {
327 let literal = match default {
328 DbDefault::Now => return "CURRENT_TIMESTAMP(6)".to_owned(),
329 DbDefault::Int(i) => return i.to_string(),
330 DbDefault::Bool(true) => return "TRUE".to_owned(),
331 DbDefault::Bool(false) => return "FALSE".to_owned(),
332 DbDefault::Text(s) => quote_string(s),
333 };
334 if is_unkeyable(field) {
336 format!("({literal})")
337 } else {
338 literal
339 }
340}
341
342fn index_sqls(model: &ModelState) -> Result<Vec<String>, MigrationError> {
343 let mut stmts = Vec::new();
344 let mut seen = BTreeSet::new();
345 for field in &model.fields {
346 if field.index && !field.unique && !field.primary_key {
347 let name = auto_index_name(&model.table, &field.column);
348 if seen.insert(name.clone()) {
349 let index = IndexState {
350 name,
351 columns: vec![field.column.clone()],
352 unique: false,
353 };
354 stmts.push(create_index_sql(model, &index)?);
355 }
356 }
357 }
358 for index in &model.indexes {
359 if seen.insert(index.name.clone()) {
360 stmts.push(create_index_sql(model, index)?);
361 }
362 }
363 Ok(stmts)
364}
365
366fn create_index_sql(model: &ModelState, index: &IndexState) -> Result<String, MigrationError> {
367 for column in &index.columns {
368 ensure_keyable(model, column, "part of an index")?;
369 }
370 let unique = if index.unique { "UNIQUE " } else { "" };
371 Ok(format!(
372 "CREATE {unique}INDEX {} ON {} ({})",
373 quote_ident(&index.name),
374 quote_ident(&model.table),
375 column_list(&index.columns)
376 ))
377}
378
379fn drop_index_sql(table: &str, name: &str) -> String {
380 format!("DROP INDEX {} ON {}", quote_ident(name), quote_ident(table))
381}
382
383fn add_field_sql(model: &ModelState, field: &FieldState) -> Result<Vec<String>, MigrationError> {
384 check_field_key(&model.table, field)?;
385 let table = quote_ident(&model.table);
386 let mut stmts = vec![format!(
387 "ALTER TABLE {table} ADD COLUMN {}",
388 column_def(field, ColumnDef::Create)?
389 )];
390 if field.index && !field.unique && !field.primary_key {
391 let index = IndexState {
392 name: auto_index_name(&model.table, &field.column),
393 columns: vec![field.column.clone()],
394 unique: false,
395 };
396 stmts.push(create_index_sql(model, &index)?);
397 }
398 if let Some(fk) = &field.fk {
399 stmts.push(format!(
400 "ALTER TABLE {table} ADD {}",
401 foreign_key_clause(&model.table, &field.column, fk)?
402 ));
403 }
404 Ok(stmts)
405}
406
407fn require_field<'a>(model: &'a ModelState, name: &str) -> Result<&'a FieldState, MigrationError> {
408 model.field(name).ok_or_else(|| {
409 MigrationError::state(format!("field `{name}` not on model `{}`", model.name))
410 })
411}
412
413fn remove_field_sql(model: &ModelState, name: &str) -> Result<Vec<String>, MigrationError> {
414 let field = require_field(model, name)?;
415 let table = quote_ident(&model.table);
416 let mut stmts = Vec::new();
417 if field.fk.is_some() {
418 stmts.push(format!(
420 "ALTER TABLE {table} DROP FOREIGN KEY {}",
421 quote_ident(&fk_name(&model.table, &field.column))
422 ));
423 }
424 stmts.push(format!(
425 "ALTER TABLE {table} DROP COLUMN {}",
426 quote_ident(&field.column)
427 ));
428 Ok(stmts)
429}
430
431fn alter_field_sql(
434 model: &ModelState,
435 name: &str,
436 field: &FieldState,
437) -> Result<Vec<String>, MigrationError> {
438 let old = require_field(model, name)?;
439 let table = quote_ident(&model.table);
440 let mut stmts = Vec::new();
441 if old.column != field.column {
442 stmts.push(format!(
443 "ALTER TABLE {table} RENAME COLUMN {} TO {}",
444 quote_ident(&old.column),
445 quote_ident(&field.column)
446 ));
447 }
448 let definition_changed = old.sql_type != field.sql_type
449 || old.max_length != field.max_length
450 || old.max_digits != field.max_digits
451 || old.decimal_places != field.decimal_places
452 || old.nullable != field.nullable
453 || old.default != field.default;
454 if definition_changed {
455 check_field_key(&model.table, field)?;
456 stmts.push(format!(
457 "ALTER TABLE {table} MODIFY COLUMN {}",
458 column_def(field, ColumnDef::Modify)?
459 ));
460 }
461 Ok(stmts)
462}
463
464fn rename_field_sql(
465 model: &ModelState,
466 old_name: &str,
467 new_name: &str,
468) -> Result<Vec<String>, MigrationError> {
469 let field = require_field(model, old_name)?;
470 if field.column == old_name && old_name != new_name {
471 return Ok(vec![format!(
472 "ALTER TABLE {} RENAME COLUMN {} TO {}",
473 quote_ident(&model.table),
474 quote_ident(&field.column),
475 quote_ident(new_name)
476 )]);
477 }
478 Ok(Vec::new())
479}
480
481fn add_constraint_sql(
482 model: &ModelState,
483 constraint: &ConstraintState,
484) -> Result<Vec<String>, MigrationError> {
485 if let ConstraintState::Unique { columns, .. } = constraint {
486 for column in columns {
487 ensure_keyable(model, column, "part of a UNIQUE constraint")?;
488 }
489 }
490 Ok(vec![format!(
491 "ALTER TABLE {} ADD {}",
492 quote_ident(&model.table),
493 table_constraint_sql(constraint)
494 )])
495}
496
497fn delete_constraint_sql(model: &ModelState, name: &str) -> Result<Vec<String>, MigrationError> {
498 let constraint = model.constraint(name).ok_or_else(|| {
499 MigrationError::state(format!("constraint `{name}` not on model `{}`", model.name))
500 })?;
501 Ok(vec![match constraint {
502 ConstraintState::Unique { .. } => drop_index_sql(&model.table, name),
504 ConstraintState::Check { .. } => format!(
505 "ALTER TABLE {} DROP CHECK {}",
506 quote_ident(&model.table),
507 quote_ident(name)
508 ),
509 }])
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 #[test]
517 fn identifiers_use_backticks() {
518 assert_eq!(quote_ident("users"), "`users`");
519 assert_eq!(quote_ident("we`ird"), "`we``ird`");
520 }
521
522 #[test]
523 fn strings_escape_quotes_and_backslashes() {
524 assert_eq!(quote_string("it's"), "'it''s'");
525 assert_eq!(quote_string(r"a\b"), r"'a\\b'");
526 assert_eq!(quote_string("a\0b"), "'a\\0b'");
527 }
528}