1pub mod fields;
42pub mod models;
43pub mod postgres;
44pub mod special;
45mod to_tokens;
46
47pub use fields::{AddField, AlterField, RemoveField, RenameField};
49pub use models::{CreateModel, DeleteModel, FieldDefinition, MoveModel, RenameModel};
50pub use postgres::{CreateCollation, CreateExtension, DropExtension};
51pub use special::{RunCode, RunSQL, StateOperation};
52
53use super::{FieldState, FieldType, ModelState, ProjectState};
56use pg_escape::{quote_identifier, quote_literal};
57use reinhardt_query::prelude::{
58 Alias, AlterTableStatement, ColumnDef, CreateIndexStatement, CreateTableStatement,
59 DropIndexStatement, DropTableStatement, Query, SimpleExpr, Value,
60};
61use serde::{Deserialize, Serialize};
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
83#[serde(rename_all = "lowercase")]
84pub enum IndexType {
85 #[default]
90 BTree,
91
92 Hash,
97
98 Gin,
103
104 Gist,
109
110 Brin,
115
116 Fulltext,
121
122 Spatial,
127}
128
129impl std::fmt::Display for IndexType {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 match self {
132 IndexType::BTree => write!(f, "btree"),
133 IndexType::Hash => write!(f, "hash"),
134 IndexType::Gin => write!(f, "gin"),
135 IndexType::Gist => write!(f, "gist"),
136 IndexType::Brin => write!(f, "brin"),
137 IndexType::Fulltext => write!(f, "fulltext"),
138 IndexType::Spatial => write!(f, "spatial"),
139 }
140 }
141}
142
143pub(crate) fn generated_index_name(
144 table: &str,
145 columns: &[String],
146 expressions: Option<&[String]>,
147) -> String {
148 let suffix = if expressions.is_some_and(|expressions| !expressions.is_empty()) {
149 "expr".to_string()
150 } else {
151 columns.join("_")
152 };
153 format!("idx_{table}_{suffix}")
154}
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
161#[serde(rename_all = "UPPERCASE")]
162pub enum MySqlAlgorithm {
163 Instant,
165 Inplace,
167 Copy,
169 #[default]
170 Default,
172}
173
174impl std::fmt::Display for MySqlAlgorithm {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 match self {
177 MySqlAlgorithm::Instant => write!(f, "INSTANT"),
178 MySqlAlgorithm::Inplace => write!(f, "INPLACE"),
179 MySqlAlgorithm::Copy => write!(f, "COPY"),
180 MySqlAlgorithm::Default => write!(f, "DEFAULT"),
181 }
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
187#[serde(rename_all = "UPPERCASE")]
188pub enum MySqlLock {
189 None,
191 Shared,
193 Exclusive,
195 #[default]
196 Default,
198}
199
200impl std::fmt::Display for MySqlLock {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 match self {
203 MySqlLock::None => write!(f, "NONE"),
204 MySqlLock::Shared => write!(f, "SHARED"),
205 MySqlLock::Exclusive => write!(f, "EXCLUSIVE"),
206 MySqlLock::Default => write!(f, "DEFAULT"),
207 }
208 }
209}
210
211#[non_exhaustive]
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
214pub struct AlterTableOptions {
215 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub algorithm: Option<MySqlAlgorithm>,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub lock: Option<MySqlLock>,
221}
222
223impl AlterTableOptions {
224 pub fn new() -> Self {
226 Self::default()
227 }
228 pub fn with_algorithm(mut self, algorithm: MySqlAlgorithm) -> Self {
230 self.algorithm = Some(algorithm);
231 self
232 }
233 pub fn with_lock(mut self, lock: MySqlLock) -> Self {
235 self.lock = Some(lock);
236 self
237 }
238 pub fn is_empty(&self) -> bool {
240 self.algorithm.is_none() && self.lock.is_none()
241 }
242 pub fn to_sql_suffix(&self) -> String {
244 let mut parts = Vec::new();
245 if let Some(algo) = &self.algorithm
246 && *algo != MySqlAlgorithm::Default
247 {
248 parts.push(format!("ALGORITHM={}", algo));
249 }
250 if let Some(lock) = &self.lock
251 && *lock != MySqlLock::Default
252 {
253 parts.push(format!("LOCK={}", lock));
254 }
255 if parts.is_empty() {
256 String::new()
257 } else {
258 format!(", {}", parts.join(", "))
259 }
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
269#[serde(rename_all = "UPPERCASE")]
270pub enum PartitionType {
271 Range,
273 List,
275 Hash,
277 Key,
279}
280
281impl std::fmt::Display for PartitionType {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 match self {
284 PartitionType::Range => write!(f, "RANGE"),
285 PartitionType::List => write!(f, "LIST"),
286 PartitionType::Hash => write!(f, "HASH"),
287 PartitionType::Key => write!(f, "KEY"),
288 }
289 }
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
294#[serde(tag = "type")]
295pub enum PartitionValues {
296 LessThan(String),
298 In(Vec<String>),
300 ModuloCount(u32),
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
306pub struct PartitionDef {
307 pub name: String,
309 pub values: PartitionValues,
311}
312
313impl PartitionDef {
314 pub fn new(name: impl Into<String>, values: PartitionValues) -> Self {
316 Self {
317 name: name.into(),
318 values,
319 }
320 }
321 pub fn less_than(name: impl Into<String>, value: impl Into<String>) -> Self {
323 Self::new(name, PartitionValues::LessThan(value.into()))
324 }
325 pub fn maxvalue(name: impl Into<String>) -> Self {
327 Self::new(name, PartitionValues::LessThan("MAXVALUE".to_string()))
328 }
329 pub fn list_in(name: impl Into<String>, values: Vec<String>) -> Self {
331 Self::new(name, PartitionValues::In(values))
332 }
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
342pub struct InterleaveSpec {
343 pub parent_table: String,
345 pub parent_columns: Vec<String>,
347}
348
349#[non_exhaustive]
351#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
352pub struct PartitionOptions {
353 pub partition_type: PartitionType,
355 pub column: String,
357 pub partitions: Vec<PartitionDef>,
359}
360
361impl PartitionOptions {
362 pub fn new(
364 partition_type: PartitionType,
365 column: impl Into<String>,
366 partitions: Vec<PartitionDef>,
367 ) -> Self {
368 Self {
369 partition_type,
370 column: column.into(),
371 partitions,
372 }
373 }
374 pub fn range(column: impl Into<String>, partitions: Vec<PartitionDef>) -> Self {
376 Self::new(PartitionType::Range, column, partitions)
377 }
378 pub fn list(column: impl Into<String>, partitions: Vec<PartitionDef>) -> Self {
380 Self::new(PartitionType::List, column, partitions)
381 }
382 pub fn hash(column: impl Into<String>, num_partitions: u32) -> Self {
384 Self::new(
385 PartitionType::Hash,
386 column,
387 vec![PartitionDef::new(
388 "",
389 PartitionValues::ModuloCount(num_partitions),
390 )],
391 )
392 }
393 pub fn key(column: impl Into<String>, num_partitions: u32) -> Self {
395 Self::new(
396 PartitionType::Key,
397 column,
398 vec![PartitionDef::new(
399 "",
400 PartitionValues::ModuloCount(num_partitions),
401 )],
402 )
403 }
404 pub fn to_sql(&self) -> String {
406 let mut sql = format!("PARTITION BY {}({})", self.partition_type, self.column);
407 match self.partition_type {
408 PartitionType::Hash | PartitionType::Key => {
409 if let Some(p) = self.partitions.first()
410 && let PartitionValues::ModuloCount(n) = &p.values
411 {
412 sql.push_str(&format!(" PARTITIONS {}", n));
413 }
414 }
415 PartitionType::Range | PartitionType::List => {
416 sql.push_str(" (");
417 let defs: Vec<String> = self
418 .partitions
419 .iter()
420 .map(|p| {
421 let vals = match &p.values {
422 PartitionValues::LessThan(v) => {
423 if v == "MAXVALUE" {
424 "VALUES LESS THAN MAXVALUE".to_string()
425 } else {
426 format!("VALUES LESS THAN ('{}')", v)
427 }
428 }
429 PartitionValues::In(v) => format!(
430 "VALUES IN ({})",
431 v.iter()
432 .map(|x| format!("'{}'", x))
433 .collect::<Vec<_>>()
434 .join(", ")
435 ),
436 PartitionValues::ModuloCount(_) => String::new(),
437 };
438 format!("PARTITION {} {}", p.name, vals)
439 })
440 .collect();
441 sql.push_str(&defs.join(", "));
442 sql.push(')');
443 }
444 }
445 sql
446 }
447}
448
449#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
453#[serde(rename_all = "lowercase")]
454pub enum DeferrableOption {
455 Immediate,
457 Deferred,
459}
460
461impl std::fmt::Display for DeferrableOption {
462 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
463 match self {
464 DeferrableOption::Immediate => write!(f, "DEFERRABLE INITIALLY IMMEDIATE"),
465 DeferrableOption::Deferred => write!(f, "DEFERRABLE INITIALLY DEFERRED"),
466 }
467 }
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
472#[serde(tag = "type")]
473pub enum Constraint {
474 PrimaryKey {
479 name: String,
481 columns: Vec<String>,
483 },
484 ForeignKey {
486 name: String,
488 columns: Vec<String>,
490 referenced_table: String,
492 referenced_columns: Vec<String>,
494 on_delete: super::ForeignKeyAction,
496 on_update: super::ForeignKeyAction,
498 #[serde(default, skip_serializing_if = "Option::is_none")]
500 deferrable: Option<DeferrableOption>,
501 },
502 Unique {
504 name: String,
506 columns: Vec<String>,
508 },
509 Check {
511 name: String,
513 expression: String,
515 },
516 OneToOne {
518 name: String,
520 column: String,
522 referenced_table: String,
524 referenced_column: String,
526 on_delete: super::ForeignKeyAction,
528 on_update: super::ForeignKeyAction,
530 #[serde(default, skip_serializing_if = "Option::is_none")]
532 deferrable: Option<DeferrableOption>,
533 },
534 ManyToMany {
536 name: String,
538 through_table: String,
540 source_column: String,
542 target_column: String,
544 target_table: String,
546 },
547 Exclude {
549 name: String,
551 elements: Vec<(String, String)>,
553 #[serde(default, skip_serializing_if = "Option::is_none")]
554 using: Option<String>,
556 #[serde(default, skip_serializing_if = "Option::is_none")]
557 where_clause: Option<String>,
559 },
560}
561
562impl std::fmt::Display for Constraint {
563 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
564 match self {
565 Constraint::PrimaryKey { name, columns } => {
566 write!(
567 f,
568 "CONSTRAINT {} PRIMARY KEY ({})",
569 name,
570 columns.join(", ")
571 )
572 }
573 Constraint::ForeignKey {
574 name,
575 columns,
576 referenced_table,
577 referenced_columns,
578 on_delete,
579 on_update,
580 deferrable,
581 } => {
582 write!(
583 f,
584 "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {}({}) ON DELETE {} ON UPDATE {}",
585 name,
586 columns.join(", "),
587 referenced_table,
588 referenced_columns.join(", "),
589 on_delete.to_sql_keyword(),
590 on_update.to_sql_keyword()
591 )?;
592 if let Some(defer_opt) = deferrable {
593 write!(f, " {}", defer_opt)?;
594 }
595 Ok(())
596 }
597 Constraint::Unique { name, columns } => {
598 let columns = columns
599 .iter()
600 .map(|column| quote_identifier(column))
601 .collect::<Vec<_>>()
602 .join(", ");
603 write!(f, "CONSTRAINT {} UNIQUE ({})", name, columns)
604 }
605 Constraint::Check { name, expression } => {
606 write!(f, "CONSTRAINT {} CHECK ({})", name, expression)
607 }
608 Constraint::OneToOne {
609 name,
610 column,
611 referenced_table,
612 referenced_column,
613 on_delete,
614 on_update,
615 deferrable,
616 } => {
617 write!(
618 f,
619 "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {}({}) ON DELETE {} ON UPDATE {}",
620 name,
621 column,
622 referenced_table,
623 referenced_column,
624 on_delete.to_sql_keyword(),
625 on_update.to_sql_keyword()
626 )?;
627 if let Some(defer_opt) = deferrable {
628 write!(f, " {}", defer_opt)?;
629 }
630 write!(
631 f,
632 ", CONSTRAINT {}_unique UNIQUE ({})",
633 name,
634 quote_identifier(column)
635 )
636 }
637 Constraint::ManyToMany { through_table, .. } => {
638 write!(f, "-- ManyToMany via {}", through_table)
639 }
640 Constraint::Exclude {
641 name,
642 elements,
643 using,
644 where_clause,
645 } => {
646 let elements_str: Vec<String> = elements
647 .iter()
648 .map(|(col, op)| format!("{} WITH {}", col, op))
649 .collect();
650 let using_str = using.as_deref().unwrap_or("gist");
651 if let Some(where_cl) = where_clause {
652 write!(
653 f,
654 "CONSTRAINT {} EXCLUDE USING {} ({}) WHERE ({})",
655 name,
656 using_str,
657 elements_str.join(", "),
658 where_cl
659 )
660 } else {
661 write!(
662 f,
663 "CONSTRAINT {} EXCLUDE USING {} ({})",
664 name,
665 using_str,
666 elements_str.join(", ")
667 )
668 }
669 }
670 }
671 }
672}
673
674#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
678#[serde(tag = "type", content = "value")]
679pub enum BulkLoadSource {
680 File(String),
682 Stdin,
684 Program(String),
686}
687
688#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
692#[serde(rename_all = "lowercase")]
693pub enum BulkLoadFormat {
694 #[default]
696 Text,
697 Csv,
699 Binary,
701}
702
703impl std::fmt::Display for BulkLoadFormat {
704 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
705 match self {
706 BulkLoadFormat::Text => write!(f, "TEXT"),
707 BulkLoadFormat::Csv => write!(f, "CSV"),
708 BulkLoadFormat::Binary => write!(f, "BINARY"),
709 }
710 }
711}
712
713#[non_exhaustive]
717#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
718pub struct BulkLoadOptions {
719 #[serde(default, skip_serializing_if = "Option::is_none")]
721 pub delimiter: Option<char>,
722 #[serde(default, skip_serializing_if = "Option::is_none")]
724 pub null_string: Option<String>,
725 #[serde(default)]
727 pub header: bool,
728 #[serde(default, skip_serializing_if = "Option::is_none")]
730 pub columns: Option<Vec<String>>,
731 #[serde(default)]
733 pub local: bool,
734 #[serde(default, skip_serializing_if = "Option::is_none")]
736 pub quote: Option<char>,
737 #[serde(default, skip_serializing_if = "Option::is_none")]
739 pub escape: Option<char>,
740 #[serde(default, skip_serializing_if = "Option::is_none")]
742 pub line_terminator: Option<String>,
743 #[serde(default, skip_serializing_if = "Option::is_none")]
745 pub encoding: Option<String>,
746}
747
748impl BulkLoadOptions {
749 pub fn new() -> Self {
751 Self::default()
752 }
753
754 pub fn with_delimiter(mut self, delimiter: char) -> Self {
756 self.delimiter = Some(delimiter);
757 self
758 }
759
760 pub fn with_null_string(mut self, null_string: impl Into<String>) -> Self {
762 self.null_string = Some(null_string.into());
763 self
764 }
765
766 pub fn with_header(mut self, header: bool) -> Self {
768 self.header = header;
769 self
770 }
771
772 pub fn with_columns(mut self, columns: Vec<String>) -> Self {
774 self.columns = Some(columns);
775 self
776 }
777
778 pub fn with_local(mut self, local: bool) -> Self {
780 self.local = local;
781 self
782 }
783
784 pub fn with_quote(mut self, quote: char) -> Self {
786 self.quote = Some(quote);
787 self
788 }
789
790 pub fn with_escape(mut self, escape: char) -> Self {
792 self.escape = Some(escape);
793 self
794 }
795
796 pub fn with_line_terminator(mut self, terminator: impl Into<String>) -> Self {
798 self.line_terminator = Some(terminator.into());
799 self
800 }
801
802 pub fn with_encoding(mut self, encoding: impl Into<String>) -> Self {
804 self.encoding = Some(encoding.into());
805 self
806 }
807}
808
809#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
815#[serde(tag = "type")]
816pub enum Operation {
817 CreateTable {
819 name: String,
821 columns: Vec<ColumnDefinition>,
823 #[serde(default)]
824 constraints: Vec<Constraint>,
826 #[serde(default, skip_serializing_if = "Option::is_none")]
827 without_rowid: Option<bool>,
829 #[serde(default, skip_serializing_if = "Option::is_none")]
830 interleave_in_parent: Option<InterleaveSpec>,
832 #[serde(default, skip_serializing_if = "Option::is_none")]
833 partition: Option<PartitionOptions>,
835 },
836 DropTable {
838 name: String,
840 },
841 AddColumn {
843 table: String,
845 column: ColumnDefinition,
847 #[serde(default, skip_serializing_if = "Option::is_none")]
848 mysql_options: Option<AlterTableOptions>,
850 },
851 DropColumn {
853 table: String,
855 column: String,
857 },
858 AlterColumn {
860 table: String,
862 column: String,
864 #[serde(default, skip_serializing_if = "Option::is_none")]
868 old_definition: Option<ColumnDefinition>,
869 new_definition: ColumnDefinition,
871 #[serde(default, skip_serializing_if = "Option::is_none")]
872 mysql_options: Option<AlterTableOptions>,
874 },
875 RenameTable {
877 old_name: String,
879 new_name: String,
881 },
882 RenameColumn {
884 table: String,
886 old_name: String,
888 new_name: String,
890 },
891 AddConstraint {
893 table: String,
895 constraint_sql: String,
897 },
898 DropConstraint {
900 table: String,
902 constraint_name: String,
904 },
905 CreateIndex {
907 table: String,
909 columns: Vec<String>,
911 unique: bool,
913 #[serde(default, skip_serializing_if = "Option::is_none")]
917 index_type: Option<IndexType>,
918 #[serde(default, skip_serializing_if = "Option::is_none")]
923 where_clause: Option<String>,
924 #[serde(default)]
929 concurrently: bool,
930 #[serde(default, skip_serializing_if = "Option::is_none")]
944 expressions: Option<Vec<String>>,
945 #[serde(default, skip_serializing_if = "Option::is_none")]
947 mysql_options: Option<AlterTableOptions>,
948 #[serde(default, skip_serializing_if = "Option::is_none")]
967 operator_class: Option<String>,
968 },
969 CreateIndexRepair {
971 table: String,
973 #[serde(default, skip_serializing_if = "Option::is_none")]
975 name: Option<String>,
976 columns: Vec<String>,
978 unique: bool,
980 #[serde(default, skip_serializing_if = "Option::is_none")]
982 index_type: Option<IndexType>,
983 #[serde(default, skip_serializing_if = "Option::is_none")]
985 where_clause: Option<String>,
986 #[serde(default)]
988 concurrently: bool,
989 #[serde(default, skip_serializing_if = "Option::is_none")]
991 expressions: Option<Vec<String>>,
992 #[serde(default, skip_serializing_if = "Option::is_none")]
994 mysql_options: Option<AlterTableOptions>,
995 #[serde(default, skip_serializing_if = "Option::is_none")]
997 operator_class: Option<String>,
998 },
999 DropIndex {
1001 table: String,
1003 columns: Vec<String>,
1005 },
1006 DropNamedIndex {
1008 table: String,
1010 name: String,
1012 #[serde(default)]
1014 columns: Vec<String>,
1015 #[serde(default)]
1017 unique: bool,
1018 #[serde(default, skip_serializing_if = "Option::is_none")]
1020 index_type: Option<IndexType>,
1021 #[serde(default, skip_serializing_if = "Option::is_none")]
1023 where_clause: Option<String>,
1024 #[serde(default)]
1026 concurrently: bool,
1027 #[serde(default, skip_serializing_if = "Option::is_none")]
1029 expressions: Option<Vec<String>>,
1030 #[serde(default, skip_serializing_if = "Option::is_none")]
1032 mysql_options: Option<AlterTableOptions>,
1033 #[serde(default, skip_serializing_if = "Option::is_none")]
1035 operator_class: Option<String>,
1036 },
1037 RunSQL {
1039 sql: String,
1041 reverse_sql: Option<String>,
1043 },
1044 RunRust {
1046 code: String,
1048 reverse_code: Option<String>,
1050 },
1051 AlterTableComment {
1053 table: String,
1055 comment: Option<String>,
1057 },
1058 AlterUniqueTogether {
1060 table: String,
1062 unique_together: Vec<Vec<String>>,
1064 },
1065 AlterModelOptions {
1067 table: String,
1069 options: std::collections::HashMap<String, String>,
1071 },
1072 CreateInheritedTable {
1074 name: String,
1076 columns: Vec<ColumnDefinition>,
1078 base_table: String,
1080 join_column: String,
1082 },
1083 AddDiscriminatorColumn {
1085 table: String,
1087 column_name: String,
1089 default_value: String,
1091 },
1092 MoveModel {
1101 model_name: String,
1103 from_app: String,
1105 to_app: String,
1107 rename_table: bool,
1109 old_table_name: Option<String>,
1111 new_table_name: Option<String>,
1113 },
1114 CreateSchema {
1118 name: String,
1120 #[serde(default)]
1122 if_not_exists: bool,
1123 },
1124 DropSchema {
1128 name: String,
1130 #[serde(default)]
1132 cascade: bool,
1133 #[serde(default = "default_true")]
1135 if_exists: bool,
1136 },
1137 CreateExtension {
1142 name: String,
1144 #[serde(default = "default_true")]
1146 if_not_exists: bool,
1147 #[serde(default)]
1149 schema: Option<String>,
1150 },
1151 BulkLoad {
1189 table: String,
1191 source: BulkLoadSource,
1193 #[serde(default)]
1195 format: BulkLoadFormat,
1196 #[serde(default)]
1198 options: BulkLoadOptions,
1199 },
1200 SetAutoIncrementValue {
1217 table: String,
1219 column: String,
1222 value: i64,
1224 },
1225 CreateCompositePrimaryKey {
1236 table: String,
1238 columns: Vec<String>,
1240 #[serde(default, skip_serializing_if = "Option::is_none")]
1243 constraint_name: Option<String>,
1244 },
1245}
1246
1247fn mysql_quote_identifier(identifier: &str) -> String {
1248 format!("`{}`", identifier.replace('`', "``"))
1249}
1250
1251fn unquote_sql_identifier(identifier: &str) -> String {
1252 let trimmed = identifier.trim();
1253 let Some(quote) = trimmed.chars().next() else {
1254 return String::new();
1255 };
1256 let stripped = match quote {
1257 '"' => trimmed
1258 .strip_prefix('"')
1259 .and_then(|value| value.strip_suffix('"')),
1260 '`' => trimmed
1261 .strip_prefix('`')
1262 .and_then(|value| value.strip_suffix('`')),
1263 '\'' => trimmed
1264 .strip_prefix('\'')
1265 .and_then(|value| value.strip_suffix('\'')),
1266 _ => None,
1267 };
1268 stripped.map_or_else(
1269 || trimmed.to_string(),
1270 |value| value.replace(&format!("{quote}{quote}"), "e.to_string()),
1271 )
1272}
1273
1274fn split_sql_identifier_list(identifier_list: &str) -> Option<Vec<String>> {
1275 let mut identifiers = Vec::new();
1276 let mut current = String::new();
1277 let mut quote = None;
1278 let mut chars = identifier_list.chars().peekable();
1279
1280 while let Some(character) = chars.next() {
1281 if let Some(quote_char) = quote {
1282 current.push(character);
1283 if character == quote_char {
1284 if chars.peek() == Some("e_char) {
1285 current.push(chars.next().expect("peeked quote must exist"));
1286 } else {
1287 quote = None;
1288 }
1289 }
1290 continue;
1291 }
1292
1293 match character {
1294 '"' | '`' | '\'' => {
1295 quote = Some(character);
1296 current.push(character);
1297 }
1298 ',' => {
1299 let identifier = unquote_sql_identifier(¤t);
1300 if identifier.is_empty() {
1301 return None;
1302 }
1303 identifiers.push(identifier);
1304 current.clear();
1305 }
1306 _ => current.push(character),
1307 }
1308 }
1309
1310 if quote.is_some() {
1311 return None;
1312 }
1313 let identifier = unquote_sql_identifier(¤t);
1314 if identifier.is_empty() {
1315 return None;
1316 }
1317 identifiers.push(identifier);
1318 Some(identifiers)
1319}
1320
1321fn mysql_quote_unique_constraint_columns(constraint_sql: &str) -> String {
1322 let Some(unique_start) = constraint_sql.find(" UNIQUE") else {
1323 return constraint_sql.to_string();
1324 };
1325 let Some(open_offset) = constraint_sql[unique_start..].find('(') else {
1326 return constraint_sql.to_string();
1327 };
1328 let open = unique_start + open_offset;
1329 let mut depth = 0usize;
1330 let mut quote = None;
1331 let mut close = None;
1332 let mut chars = constraint_sql[open..].char_indices().peekable();
1333
1334 while let Some((offset, character)) = chars.next() {
1335 if let Some(quote_char) = quote {
1336 if character == quote_char {
1337 if chars.peek().is_some_and(|(_, next)| *next == quote_char) {
1338 chars.next();
1339 } else {
1340 quote = None;
1341 }
1342 }
1343 continue;
1344 }
1345
1346 match character {
1347 '"' | '`' | '\'' => quote = Some(character),
1348 '(' => depth += 1,
1349 ')' => {
1350 depth = depth.saturating_sub(1);
1351 if depth == 0 {
1352 close = Some(open + offset);
1353 break;
1354 }
1355 }
1356 _ => {}
1357 }
1358 }
1359
1360 let Some(close) = close else {
1361 return constraint_sql.to_string();
1362 };
1363 let Some(columns) = split_sql_identifier_list(&constraint_sql[open + 1..close]) else {
1364 return constraint_sql.to_string();
1365 };
1366 let quoted_columns = columns
1367 .iter()
1368 .map(|column| mysql_quote_identifier(column))
1369 .collect::<Vec<_>>()
1370 .join(", ");
1371 format!(
1372 "{}{}{}",
1373 &constraint_sql[..open + 1],
1374 quoted_columns,
1375 &constraint_sql[close..]
1376 )
1377}
1378
1379const fn default_true() -> bool {
1381 true
1382}
1383
1384impl Operation {
1385 pub fn state_forwards(&self, app_label: &str, state: &mut ProjectState) {
1387 match self {
1388 Operation::CreateTable { name, columns, .. } => {
1389 let mut model = ModelState::new(app_label, name.clone());
1390 for column in columns {
1391 let field = FieldState::new(
1392 column.name.to_string(),
1393 column.type_definition.clone(),
1394 false,
1395 );
1396 model.add_field(field);
1397 }
1398 state.add_model(model);
1399 }
1400 Operation::DropTable { name } => {
1401 state.remove_model(app_label, name);
1402 }
1403 Operation::AddColumn { table, column, .. } => {
1404 if let Some(model) = state.get_model_mut(app_label, table) {
1405 let field = FieldState::new(
1406 column.name.to_string(),
1407 column.type_definition.clone(),
1408 false,
1409 );
1410 model.add_field(field);
1411 }
1412 }
1413 Operation::DropColumn { table, column } => {
1414 if let Some(model) = state.get_model_mut(app_label, table) {
1415 model.remove_field(column);
1416 }
1417 }
1418 Operation::AlterColumn {
1419 table,
1420 column,
1421 new_definition,
1422 ..
1423 } => {
1424 if let Some(model) = state.get_model_mut(app_label, table) {
1425 let field = FieldState::new(
1426 column.to_string(),
1427 new_definition.type_definition.clone(),
1428 false,
1429 );
1430 model.alter_field(column, field);
1431 }
1432 }
1433 Operation::RenameTable { old_name, new_name } => {
1434 state.rename_model(app_label, old_name, new_name.to_string());
1435 }
1436 Operation::RenameColumn {
1437 table,
1438 old_name,
1439 new_name,
1440 } => {
1441 if let Some(model) = state.get_model_mut(app_label, table) {
1442 model.rename_field(old_name, new_name.to_string());
1443 }
1444 }
1445 Operation::CreateInheritedTable {
1446 name,
1447 columns,
1448 base_table,
1449 join_column,
1450 } => {
1451 let mut model = ModelState::new(app_label, name.clone());
1452 model.base_model = Some(base_table.to_string());
1453 model.inheritance_type = Some("joined_table".to_string());
1454
1455 let join_field = FieldState::new(
1456 join_column.to_string(),
1457 FieldType::Custom(format!("INTEGER REFERENCES {}(id)", base_table)),
1458 false,
1459 );
1460 model.add_field(join_field);
1461
1462 for column in columns {
1463 let field = FieldState::new(
1464 column.name.to_string(),
1465 column.type_definition.clone(),
1466 false,
1467 );
1468 model.add_field(field);
1469 }
1470 state.add_model(model);
1471 }
1472 Operation::AddDiscriminatorColumn {
1473 table,
1474 column_name,
1475 default_value,
1476 } => {
1477 if let Some(model) = state.get_model_mut(app_label, table) {
1478 model.discriminator_column = Some(column_name.to_string());
1479 model.inheritance_type = Some("single_table".to_string());
1480 let field = FieldState::new(
1481 column_name.to_string(),
1482 FieldType::Custom(format!("VARCHAR(50) DEFAULT '{}'", default_value)),
1483 false,
1484 );
1485 model.add_field(field);
1486 }
1487 }
1488 Operation::AddConstraint { .. }
1489 | Operation::DropConstraint { .. }
1490 | Operation::CreateIndex { .. }
1491 | Operation::CreateIndexRepair { .. }
1492 | Operation::DropIndex { .. }
1493 | Operation::DropNamedIndex { .. }
1494 | Operation::RunSQL { .. }
1495 | Operation::RunRust { .. }
1496 | Operation::AlterTableComment { .. }
1497 | Operation::AlterUniqueTogether { .. }
1498 | Operation::AlterModelOptions { .. }
1499 | Operation::SetAutoIncrementValue { .. }
1500 | Operation::CreateCompositePrimaryKey { .. } => {
1501 }
1504 Operation::MoveModel {
1505 model_name,
1506 from_app,
1507 to_app,
1508 rename_table,
1509 old_table_name,
1510 new_table_name,
1511 } => {
1512 if let Some(model) = state.get_model(from_app, model_name).cloned() {
1515 state.remove_model(from_app, model_name);
1516
1517 let mut new_model = model;
1519 new_model.app_label = to_app.to_string();
1520
1521 if *rename_table
1523 && let (Some(_old_name), Some(new_name)) = (old_table_name, new_table_name)
1524 {
1525 new_model.table_name = new_name.to_string();
1526 }
1527
1528 state.add_model(new_model);
1529 }
1530 }
1531 Operation::CreateSchema { .. }
1533 | Operation::DropSchema { .. }
1534 | Operation::CreateExtension { .. } => {
1535 }
1537 Operation::BulkLoad { .. } => {
1539 }
1541 }
1542 }
1543
1544 fn column_to_sql_without_pk(col: &ColumnDefinition, dialect: &SqlDialect) -> String {
1549 let mut parts = Vec::new();
1550
1551 parts.push(quote_identifier(&col.name));
1553
1554 if col.auto_increment {
1556 match dialect {
1557 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
1558 match &col.type_definition {
1560 FieldType::BigInteger => {
1561 parts
1562 .push("BIGINT GENERATED BY DEFAULT AS IDENTITY".to_string().into());
1563 }
1564 FieldType::Integer => {
1565 parts.push(
1566 "INTEGER GENERATED BY DEFAULT AS IDENTITY"
1567 .to_string()
1568 .into(),
1569 );
1570 }
1571 FieldType::SmallInteger => {
1572 parts.push(
1573 "SMALLINT GENERATED BY DEFAULT AS IDENTITY"
1574 .to_string()
1575 .into(),
1576 );
1577 }
1578 _ => {
1579 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1581 }
1582 }
1583 }
1584 SqlDialect::Mysql => {
1585 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1586 parts.push("AUTO_INCREMENT".to_string().into());
1587 }
1588 SqlDialect::Sqlite => {
1589 match &col.type_definition {
1593 FieldType::BigInteger | FieldType::Integer | FieldType::SmallInteger => {
1594 parts.push("INTEGER".to_string().into());
1595 }
1596 _ => {
1597 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1601 }
1602 }
1603 }
1606 }
1607 } else {
1608 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1609 }
1610
1611 if col.not_null {
1613 parts.push("NOT NULL".to_string().into());
1614 }
1615
1616 if col.unique {
1618 parts.push("UNIQUE".to_string().into());
1619 }
1620
1621 if let Some(default) = &col.default {
1623 parts.push(format!("DEFAULT {}", default).into());
1624 }
1625
1626 parts.join(" ")
1627 }
1628
1629 fn column_to_sql(col: &ColumnDefinition, dialect: &SqlDialect) -> String {
1631 let mut parts = Vec::new();
1632
1633 parts.push(quote_identifier(&col.name));
1635
1636 if col.auto_increment {
1638 match dialect {
1639 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
1640 match &col.type_definition {
1642 FieldType::BigInteger => {
1643 parts
1644 .push("BIGINT GENERATED BY DEFAULT AS IDENTITY".to_string().into());
1645 }
1646 FieldType::Integer => {
1647 parts.push(
1648 "INTEGER GENERATED BY DEFAULT AS IDENTITY"
1649 .to_string()
1650 .into(),
1651 );
1652 }
1653 FieldType::SmallInteger => {
1654 parts.push(
1655 "SMALLINT GENERATED BY DEFAULT AS IDENTITY"
1656 .to_string()
1657 .into(),
1658 );
1659 }
1660 _ => {
1661 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1663 }
1664 }
1665 }
1666 SqlDialect::Mysql => {
1667 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1668 parts.push("AUTO_INCREMENT".to_string().into());
1669 }
1670 SqlDialect::Sqlite => {
1671 let widened_to_integer = matches!(
1682 &col.type_definition,
1683 FieldType::BigInteger | FieldType::Integer | FieldType::SmallInteger
1684 );
1685 if widened_to_integer {
1686 parts.push("INTEGER".to_string().into());
1687 } else {
1688 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1689 }
1690 if col.primary_key {
1695 if widened_to_integer {
1696 parts.push("PRIMARY KEY AUTOINCREMENT".to_string().into());
1697 } else {
1698 parts.push("PRIMARY KEY".to_string().into());
1699 }
1700 if col.unique {
1702 parts.push("UNIQUE".to_string().into());
1703 }
1704 if let Some(default) = &col.default {
1705 parts.push(format!("DEFAULT {}", default).into());
1706 }
1707 return parts.join(" ");
1708 }
1709 }
1710 }
1711 } else {
1712 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1713 }
1714
1715 if col.not_null {
1717 parts.push("NOT NULL".to_string().into());
1718 }
1719
1720 if col.primary_key {
1722 parts.push("PRIMARY KEY".to_string().into());
1723 }
1724
1725 if col.unique {
1727 parts.push("UNIQUE".to_string().into());
1728 }
1729
1730 if let Some(default) = &col.default {
1732 parts.push(format!("DEFAULT {}", default).into());
1733 }
1734
1735 parts.join(" ")
1736 }
1737
1738 pub fn to_sql(&self, dialect: &SqlDialect) -> String {
1740 match self {
1741 Operation::CreateTable {
1742 name,
1743 columns,
1744 constraints,
1745 without_rowid,
1746 interleave_in_parent,
1747 partition,
1748 } => {
1749 let pk_columns: Vec<&String> = columns
1751 .iter()
1752 .filter(|col| col.primary_key)
1753 .map(|col| &col.name)
1754 .collect();
1755 let has_composite_pk = pk_columns.len() > 1;
1756
1757 let mut parts = Vec::new();
1758 for col in columns {
1759 if has_composite_pk {
1761 parts.push(format!(
1762 " {}",
1763 Self::column_to_sql_without_pk(col, dialect)
1764 ));
1765 } else {
1766 parts.push(format!(" {}", Self::column_to_sql(col, dialect)));
1767 }
1768 }
1769
1770 if has_composite_pk {
1772 let pk_constraint_name = format!("{}_pkey", name);
1773 let quoted_pk_columns = pk_columns
1774 .iter()
1775 .map(|s| quote_identifier(s))
1776 .collect::<Vec<_>>()
1777 .join(", ");
1778 let pk_constraint = format!(
1779 " CONSTRAINT {} PRIMARY KEY ({})",
1780 quote_identifier(&pk_constraint_name),
1781 quoted_pk_columns
1782 );
1783 parts.push(pk_constraint);
1784 }
1785
1786 for constraint in constraints {
1787 parts.push(format!(" {}", constraint));
1788 }
1789 let mut sql = format!(
1790 "CREATE TABLE {} (\n{}\n)",
1791 quote_identifier(name),
1792 parts.join(",\n")
1793 );
1794
1795 if matches!(dialect, SqlDialect::Sqlite)
1797 && let Some(true) = without_rowid
1798 {
1799 sql.push_str(" WITHOUT ROWID");
1800 }
1801
1802 if matches!(dialect, SqlDialect::Mysql)
1804 && let Some(partition_opts) = partition
1805 {
1806 sql.push(' ');
1807 sql.push_str(&partition_opts.to_sql());
1808 }
1809
1810 if matches!(dialect, SqlDialect::Cockroachdb)
1812 && let Some(interleave) = interleave_in_parent
1813 {
1814 let quoted_columns = interleave
1815 .parent_columns
1816 .iter()
1817 .map(|col| quote_identifier(col))
1818 .collect::<Vec<_>>()
1819 .join(", ");
1820 sql.push_str(&format!(
1821 " INTERLEAVE IN PARENT {} ({})",
1822 quote_identifier(&interleave.parent_table),
1823 quoted_columns
1824 ));
1825 }
1826
1827 sql.push(';');
1828 sql
1829 }
1830 Operation::DropTable { name } => format!("DROP TABLE {};", quote_identifier(name)),
1831 Operation::AddColumn {
1832 table,
1833 column,
1834 mysql_options,
1835 } => {
1836 let base_sql = format!(
1837 "ALTER TABLE {} ADD COLUMN {}",
1838 quote_identifier(table),
1839 Self::column_to_sql(column, dialect)
1840 );
1841
1842 if matches!(dialect, SqlDialect::Mysql)
1844 && let Some(opts) = mysql_options
1845 {
1846 let suffix = opts.to_sql_suffix();
1847 if !suffix.is_empty() {
1848 return format!("{}{};", base_sql, suffix);
1849 }
1850 }
1851
1852 format!("{};", base_sql)
1853 }
1854 Operation::DropColumn { table, column } => {
1855 format!(
1856 "ALTER TABLE {} DROP COLUMN {};",
1857 quote_identifier(table),
1858 quote_identifier(column)
1859 )
1860 }
1861 Operation::AlterColumn {
1862 table,
1863 column,
1864 old_definition,
1865 new_definition,
1866 mysql_options,
1867 ..
1868 } => {
1869 let sql_type = new_definition.type_definition.to_sql_for_dialect(dialect);
1870 match dialect {
1871 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
1872 let mut statements = Vec::new();
1873 if old_definition
1874 .as_ref()
1875 .is_some_and(|old_definition| old_definition.default.is_some())
1876 {
1877 statements.push(format!(
1878 "ALTER TABLE {} ALTER COLUMN {} DROP DEFAULT;",
1879 quote_identifier(table),
1880 quote_identifier(column)
1881 ));
1882 }
1883 statements.push(format!(
1884 "ALTER TABLE {} ALTER COLUMN {} TYPE {};",
1885 quote_identifier(table),
1886 quote_identifier(column),
1887 sql_type
1888 ));
1889 if let Some(default) = &new_definition.default {
1890 statements.push(format!(
1891 "ALTER TABLE {} ALTER COLUMN {} SET DEFAULT {};",
1892 quote_identifier(table),
1893 quote_identifier(column),
1894 default
1895 ));
1896 }
1897 statements.join(" ")
1898 }
1899 SqlDialect::Mysql => {
1900 let base_sql = format!(
1901 "ALTER TABLE {} MODIFY COLUMN {}",
1902 quote_identifier(table),
1903 Self::column_to_sql(new_definition, dialect)
1904 );
1905
1906 if let Some(opts) = mysql_options {
1908 let suffix = opts.to_sql_suffix();
1909 if !suffix.is_empty() {
1910 return format!("{}{};", base_sql, suffix);
1911 }
1912 }
1913
1914 format!("{};", base_sql)
1915 }
1916 SqlDialect::Sqlite => {
1917 format!(
1918 "-- SQLite does not support ALTER COLUMN, table recreation required for {}",
1919 quote_identifier(table)
1920 )
1921 }
1922 }
1923 }
1924 Operation::RenameColumn {
1925 table,
1926 old_name,
1927 new_name,
1928 } => {
1929 format!(
1930 "ALTER TABLE {} RENAME COLUMN {} TO {};",
1931 quote_identifier(table),
1932 quote_identifier(old_name),
1933 quote_identifier(new_name)
1934 )
1935 }
1936 Operation::RenameTable { old_name, new_name } => {
1937 format!(
1938 "ALTER TABLE {} RENAME TO {};",
1939 quote_identifier(old_name),
1940 quote_identifier(new_name)
1941 )
1942 }
1943 Operation::AddConstraint {
1944 table,
1945 constraint_sql,
1946 } => {
1947 let constraint_sql = if matches!(dialect, SqlDialect::Mysql) {
1948 mysql_quote_unique_constraint_columns(constraint_sql)
1949 } else {
1950 constraint_sql.clone()
1951 };
1952 format!(
1953 "ALTER TABLE {} ADD {};",
1954 quote_identifier(table),
1955 constraint_sql
1956 )
1957 }
1958 Operation::DropConstraint {
1959 table,
1960 constraint_name,
1961 } => {
1962 format!(
1963 "ALTER TABLE {} DROP CONSTRAINT {};",
1964 quote_identifier(table),
1965 quote_identifier(constraint_name)
1966 )
1967 }
1968 Operation::CreateIndex {
1969 table,
1970 columns,
1971 unique,
1972 index_type,
1973 where_clause,
1974 concurrently,
1975 expressions,
1976 mysql_options,
1977 operator_class,
1978 } => {
1979 let unique_str = if *unique { "UNIQUE " } else { "" };
1980
1981 let concurrent_str = if *concurrently && matches!(dialect, SqlDialect::Postgres) {
1983 "CONCURRENTLY "
1984 } else {
1985 ""
1986 };
1987
1988 let (mysql_prefix, effective_unique) = match (index_type, dialect) {
1990 (Some(IndexType::Fulltext), SqlDialect::Mysql) => ("FULLTEXT ", ""),
1991 (Some(IndexType::Spatial), SqlDialect::Mysql) => ("SPATIAL ", ""),
1992 _ => ("", unique_str),
1993 };
1994
1995 let (index_content, name_suffix) =
1997 if let Some(exprs) = expressions.as_ref().filter(|e| !e.is_empty()) {
1998 let content = exprs.join(", ");
2001 let suffix = "expr";
2002 (content, suffix.to_string())
2003 } else {
2004 let content = if let Some(op_class) = operator_class {
2006 if matches!(dialect, SqlDialect::Postgres) {
2008 columns
2009 .iter()
2010 .map(|c| format!("{} {}", quote_identifier(c), op_class))
2011 .collect::<Vec<_>>()
2012 .join(", ")
2013 } else {
2014 columns
2016 .iter()
2017 .map(|c| quote_identifier(c).to_string())
2018 .collect::<Vec<_>>()
2019 .join(", ")
2020 }
2021 } else {
2022 columns
2024 .iter()
2025 .map(|c| quote_identifier(c).to_string())
2026 .collect::<Vec<_>>()
2027 .join(", ")
2028 };
2029 (content, columns.join("_"))
2030 };
2031
2032 let idx_name = if name_suffix == "expr" {
2033 format!("idx_{table}_expr")
2034 } else {
2035 generated_index_name(table, columns, None)
2036 };
2037
2038 let using_clause = match (index_type, dialect) {
2040 (Some(IndexType::BTree), _) => String::new(), (Some(idx_type), SqlDialect::Postgres | SqlDialect::Cockroachdb) => {
2042 format!(" USING {}", idx_type)
2043 }
2044 (Some(IndexType::Fulltext | IndexType::Spatial), SqlDialect::Mysql) => {
2046 String::new()
2047 }
2048 _ => String::new(),
2049 };
2050
2051 let mut sql = match dialect {
2056 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
2057 format!(
2059 "CREATE {}INDEX {}{}",
2060 effective_unique,
2061 concurrent_str,
2062 quote_identifier(&idx_name)
2063 )
2064 }
2065 SqlDialect::Mysql => {
2066 format!(
2068 "CREATE {}{}INDEX {}",
2069 mysql_prefix,
2070 effective_unique,
2071 quote_identifier(&idx_name)
2072 )
2073 }
2074 SqlDialect::Sqlite => {
2075 format!(
2077 "CREATE {}INDEX {}",
2078 effective_unique,
2079 quote_identifier(&idx_name)
2080 )
2081 }
2082 };
2083 sql.push_str(&format!(
2087 " ON {}{} ({})",
2088 quote_identifier(table),
2089 using_clause,
2090 index_content
2091 ));
2092
2093 if let Some(where_cond) = where_clause
2095 && !matches!(dialect, SqlDialect::Mysql)
2096 {
2097 sql.push_str(&format!(" WHERE {}", where_cond));
2098 }
2099
2100 if matches!(dialect, SqlDialect::Mysql)
2102 && let Some(opts) = mysql_options
2103 {
2104 let suffix = opts.to_sql_suffix();
2105 if !suffix.is_empty() {
2106 sql.push_str(&suffix);
2107 }
2108 }
2109
2110 sql.push(';');
2111 sql
2112 }
2113 Operation::CreateIndexRepair {
2114 table,
2115 name,
2116 columns,
2117 unique,
2118 index_type,
2119 where_clause,
2120 concurrently,
2121 expressions,
2122 mysql_options,
2123 operator_class,
2124 } => {
2125 let create = Operation::CreateIndex {
2126 table: table.clone(),
2127 columns: columns.clone(),
2128 unique: *unique,
2129 index_type: *index_type,
2130 where_clause: where_clause.clone(),
2131 concurrently: *concurrently,
2132 expressions: expressions.clone(),
2133 mysql_options: *mysql_options,
2134 operator_class: operator_class.clone(),
2135 };
2136 let sql = create.to_sql(dialect);
2137 name.as_ref().map_or(sql.clone(), |name| {
2138 let generated_name =
2139 generated_index_name(table, columns, expressions.as_deref());
2140 let generated_name = quote_identifier(&generated_name);
2141 let name = quote_identifier(name);
2142 sql.replacen(generated_name.as_ref(), name.as_ref(), 1)
2143 })
2144 }
2145 Operation::DropIndex { table, columns } => {
2146 let idx_name = generated_index_name(table, columns, None);
2147 match dialect {
2148 SqlDialect::Mysql => {
2149 format!(
2150 "DROP INDEX {} ON {};",
2151 quote_identifier(&idx_name),
2152 quote_identifier(table)
2153 )
2154 }
2155 SqlDialect::Postgres | SqlDialect::Sqlite | SqlDialect::Cockroachdb => {
2156 format!("DROP INDEX {};", quote_identifier(&idx_name))
2157 }
2158 }
2159 }
2160 Operation::DropNamedIndex { table, name, .. } => match dialect {
2161 SqlDialect::Mysql => format!(
2162 "DROP INDEX {} ON {};",
2163 quote_identifier(name),
2164 quote_identifier(table)
2165 ),
2166 SqlDialect::Postgres | SqlDialect::Sqlite | SqlDialect::Cockroachdb => {
2167 format!("DROP INDEX {};", quote_identifier(name))
2168 }
2169 },
2170 Operation::RunSQL { sql, .. } => sql.to_string(),
2171 Operation::RunRust { code, .. } => {
2172 format!("-- RunRust: {}", code.lines().next().unwrap_or(""))
2174 }
2175 Operation::AlterTableComment { table, comment } => match dialect {
2176 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
2177 if let Some(comment_text) = comment {
2178 format!(
2179 "COMMENT ON TABLE {} IS '{}';",
2180 quote_identifier(table),
2181 comment_text
2182 )
2183 } else {
2184 format!("COMMENT ON TABLE {} IS NULL;", quote_identifier(table))
2185 }
2186 }
2187 SqlDialect::Mysql => {
2188 if let Some(comment_text) = comment {
2189 format!(
2190 "ALTER TABLE {} COMMENT='{}';",
2191 quote_identifier(table),
2192 comment_text
2193 )
2194 } else {
2195 format!("ALTER TABLE {} COMMENT='';", quote_identifier(table))
2196 }
2197 }
2198 SqlDialect::Sqlite => String::new(),
2199 },
2200 Operation::AlterUniqueTogether {
2201 table,
2202 unique_together,
2203 } => {
2204 let mut sql = Vec::new();
2205 for (idx, fields) in unique_together.iter().enumerate() {
2206 let constraint_name = format!("{}_{}_uniq", table, idx);
2207 let fields_str = fields
2208 .iter()
2209 .map(|f| quote_identifier(f))
2210 .collect::<Vec<_>>()
2211 .join(", ");
2212 sql.push(format!(
2213 "ALTER TABLE {} ADD CONSTRAINT {} UNIQUE ({});",
2214 quote_identifier(table),
2215 quote_identifier(&constraint_name),
2216 fields_str
2217 ));
2218 }
2219 sql.join("\n")
2220 }
2221 Operation::AlterModelOptions { .. } => String::new(),
2222 Operation::CreateInheritedTable {
2223 name,
2224 columns,
2225 base_table,
2226 join_column,
2227 } => {
2228 let mut parts = Vec::new();
2229 parts.push(format!(
2230 " {} INTEGER REFERENCES {}(id)",
2231 quote_identifier(join_column),
2232 quote_identifier(base_table)
2233 ));
2234 for col in columns {
2235 parts.push(format!(" {}", Self::column_to_sql(col, dialect)));
2236 }
2237 format!(
2238 "CREATE TABLE {} (\n{}\n);",
2239 quote_identifier(name),
2240 parts.join(",\n")
2241 )
2242 }
2243 Operation::AddDiscriminatorColumn {
2244 table,
2245 column_name,
2246 default_value,
2247 } => {
2248 format!(
2249 "ALTER TABLE {} ADD COLUMN {} VARCHAR(50) DEFAULT '{}';",
2250 quote_identifier(table),
2251 quote_identifier(column_name),
2252 default_value
2253 )
2254 }
2255 Operation::MoveModel {
2256 rename_table,
2257 old_table_name,
2258 new_table_name,
2259 ..
2260 } => {
2261 if *rename_table {
2264 if let (Some(old_name), Some(new_name)) = (old_table_name, new_table_name) {
2265 match dialect {
2266 SqlDialect::Postgres | SqlDialect::Sqlite | SqlDialect::Cockroachdb => {
2267 format!(
2268 "ALTER TABLE {} RENAME TO {};",
2269 quote_identifier(old_name),
2270 quote_identifier(new_name)
2271 )
2272 }
2273 SqlDialect::Mysql => {
2274 format!(
2275 "RENAME TABLE {} TO {};",
2276 quote_identifier(old_name),
2277 quote_identifier(new_name)
2278 )
2279 }
2280 }
2281 } else {
2282 "-- MoveModel: No table rename specified".to_string()
2283 }
2284 } else {
2285 "-- MoveModel: State-only operation (no table rename)".to_string()
2287 }
2288 }
2289 Operation::CreateSchema {
2290 name,
2291 if_not_exists,
2292 } => {
2293 let if_not_exists_clause = if *if_not_exists { " IF NOT EXISTS" } else { "" };
2294 format!(
2295 "CREATE SCHEMA{} {};",
2296 if_not_exists_clause,
2297 quote_identifier(name)
2298 )
2299 }
2300 Operation::DropSchema {
2301 name,
2302 cascade,
2303 if_exists,
2304 } => {
2305 let if_exists_clause = if *if_exists { " IF EXISTS" } else { "" };
2306 let cascade_clause = if *cascade { " CASCADE" } else { "" };
2307 format!(
2308 "DROP SCHEMA{} {}{};",
2309 if_exists_clause,
2310 quote_identifier(name),
2311 cascade_clause
2312 )
2313 }
2314 Operation::CreateExtension {
2315 name,
2316 if_not_exists,
2317 schema,
2318 } => {
2319 let if_not_exists_clause = if *if_not_exists { " IF NOT EXISTS" } else { "" };
2321 let schema_clause = if let Some(s) = schema {
2322 format!(" SCHEMA {}", quote_identifier(s))
2323 } else {
2324 String::new()
2325 };
2326 format!(
2327 "CREATE EXTENSION{} {}{};",
2328 if_not_exists_clause,
2329 quote_identifier(name),
2330 schema_clause
2331 )
2332 }
2333 Operation::BulkLoad {
2334 table,
2335 source,
2336 format,
2337 options,
2338 } => Self::bulk_load_to_sql(table, source, format, options, dialect),
2339 Operation::SetAutoIncrementValue {
2340 table,
2341 column,
2342 value,
2343 } => Self::set_auto_increment_to_sql(table, column, *value, dialect),
2344 Operation::CreateCompositePrimaryKey {
2345 table,
2346 columns,
2347 constraint_name,
2348 } => Self::create_composite_pk_to_sql(table, columns, constraint_name.as_deref()),
2349 }
2350 }
2351
2352 fn set_auto_increment_to_sql(
2359 table: &str,
2360 column: &str,
2361 value: i64,
2362 dialect: &SqlDialect,
2363 ) -> String {
2364 match dialect {
2365 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
2366 format!(
2371 "SELECT setval(pg_get_serial_sequence({}, {}), {}, false);",
2372 quote_literal(table),
2373 quote_literal(column),
2374 value
2375 )
2376 }
2377 SqlDialect::Mysql => {
2378 format!(
2379 "ALTER TABLE {} AUTO_INCREMENT = {};",
2380 quote_identifier(table),
2381 value
2382 )
2383 }
2384 SqlDialect::Sqlite => {
2385 format!(
2390 "INSERT OR REPLACE INTO sqlite_sequence(name, seq) VALUES ({}, {});",
2391 quote_literal(table),
2392 value
2393 )
2394 }
2395 }
2396 }
2397
2398 fn create_composite_pk_to_sql(
2435 table: &str,
2436 columns: &[String],
2437 constraint_name: Option<&str>,
2438 ) -> String {
2439 if columns.is_empty() {
2440 return format!(
2447 "SYNTAX_ERROR_create_composite_pk_on_{}_requires_at_least_one_column;",
2448 table.replace(|c: char| !c.is_ascii_alphanumeric(), "_")
2449 );
2450 }
2451
2452 let default_name;
2453 let name: &str = match constraint_name {
2454 Some(n) => n,
2455 None => {
2456 default_name = format!("{}_pkey", table);
2457 &default_name
2458 }
2459 };
2460
2461 let quoted_columns = columns
2462 .iter()
2463 .map(|c| quote_identifier(c).to_string())
2464 .collect::<Vec<_>>()
2465 .join(", ");
2466
2467 format!(
2468 "ALTER TABLE {} ADD CONSTRAINT {} PRIMARY KEY ({});",
2469 quote_identifier(table),
2470 quote_identifier(name),
2471 quoted_columns
2472 )
2473 }
2474
2475 fn bulk_load_to_sql(
2477 table: &str,
2478 source: &BulkLoadSource,
2479 format: &BulkLoadFormat,
2480 options: &BulkLoadOptions,
2481 dialect: &SqlDialect,
2482 ) -> String {
2483 match dialect {
2484 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
2485 Self::postgres_copy_from_sql(table, source, format, options)
2486 }
2487 SqlDialect::Mysql => Self::mysql_load_data_sql(table, source, format, options),
2488 SqlDialect::Sqlite => {
2489 format!(
2491 "-- SQLite does not support bulk loading. Use INSERT statements instead for table {}",
2492 quote_identifier(table)
2493 )
2494 }
2495 }
2496 }
2497
2498 fn postgres_copy_from_sql(
2500 table: &str,
2501 source: &BulkLoadSource,
2502 format: &BulkLoadFormat,
2503 options: &BulkLoadOptions,
2504 ) -> String {
2505 let source_clause = match source {
2506 BulkLoadSource::File(path) => format!("'{}'", path),
2507 BulkLoadSource::Stdin => "STDIN".to_string(),
2508 BulkLoadSource::Program(cmd) => format!("PROGRAM '{}'", cmd),
2509 };
2510
2511 let columns_clause = if let Some(cols) = &options.columns {
2512 let quoted_cols = cols
2513 .iter()
2514 .map(|c| quote_identifier(c))
2515 .collect::<Vec<_>>()
2516 .join(", ");
2517 format!(" ({})", quoted_cols)
2518 } else {
2519 String::new()
2520 };
2521
2522 let mut with_options = Vec::new();
2523
2524 with_options.push(format!("FORMAT {}", format));
2526
2527 if let Some(delim) = options.delimiter {
2529 with_options.push(format!("DELIMITER '{}'", delim));
2530 }
2531
2532 if let Some(null_str) = &options.null_string {
2534 with_options.push(format!("NULL '{}'", null_str));
2535 }
2536
2537 if options.header {
2539 with_options.push("HEADER true".to_string());
2540 }
2541
2542 if let Some(quote) = options.quote {
2544 with_options.push(format!("QUOTE '{}'", quote));
2545 }
2546
2547 if let Some(escape) = options.escape {
2549 with_options.push(format!("ESCAPE '{}'", escape));
2550 }
2551
2552 format!(
2553 "COPY {}{} FROM {} WITH ({});",
2554 quote_identifier(table),
2555 columns_clause,
2556 source_clause,
2557 with_options.join(", ")
2558 )
2559 }
2560
2561 fn mysql_load_data_sql(
2563 table: &str,
2564 source: &BulkLoadSource,
2565 format: &BulkLoadFormat,
2566 options: &BulkLoadOptions,
2567 ) -> String {
2568 let local_clause = if options.local { " LOCAL" } else { "" };
2569
2570 let file_path = match source {
2571 BulkLoadSource::File(path) => path.clone(),
2572 BulkLoadSource::Stdin => {
2573 return format!(
2574 "-- MySQL does not support LOAD DATA from STDIN directly for table {}",
2575 quote_identifier(table)
2576 );
2577 }
2578 BulkLoadSource::Program(_) => {
2579 return format!(
2580 "-- MySQL does not support LOAD DATA from PROGRAM directly for table {}",
2581 quote_identifier(table)
2582 );
2583 }
2584 };
2585
2586 let columns_clause = if let Some(cols) = &options.columns {
2587 let quoted_cols = cols
2588 .iter()
2589 .map(|c| quote_identifier(c))
2590 .collect::<Vec<_>>()
2591 .join(", ");
2592 format!(" ({})", quoted_cols)
2593 } else {
2594 String::new()
2595 };
2596
2597 let delimiter = options.delimiter.unwrap_or(match format {
2599 BulkLoadFormat::Csv => ',',
2600 BulkLoadFormat::Text | BulkLoadFormat::Binary => '\t',
2601 });
2602
2603 let mut field_options = Vec::new();
2604 field_options.push(format!("TERMINATED BY '{}'", delimiter));
2605
2606 if *format == BulkLoadFormat::Csv {
2608 let quote = options.quote.unwrap_or('"');
2609 field_options.push(format!("ENCLOSED BY '{}'", quote));
2610 }
2611
2612 if let Some(escape) = options.escape {
2614 field_options.push(format!("ESCAPED BY '{}'", escape));
2615 }
2616
2617 let line_terminator = options
2619 .line_terminator
2620 .clone()
2621 .unwrap_or_else(|| "\\n".to_string());
2622
2623 let encoding_clause = if let Some(enc) = &options.encoding {
2625 format!(" CHARACTER SET {}", enc)
2626 } else {
2627 String::new()
2628 };
2629
2630 let ignore_clause = if options.header {
2632 " IGNORE 1 LINES"
2633 } else {
2634 ""
2635 };
2636
2637 format!(
2638 "LOAD DATA{} INFILE '{}'{} INTO TABLE {} FIELDS {} LINES TERMINATED BY '{}'{}{};",
2639 local_clause,
2640 file_path,
2641 encoding_clause,
2642 quote_identifier(table),
2643 field_options.join(" "),
2644 line_terminator,
2645 ignore_clause,
2646 columns_clause
2647 )
2648 }
2649
2650 pub fn to_reverse_sql(
2682 &self,
2683 dialect: &SqlDialect,
2684 project_state: &ProjectState,
2685 ) -> super::Result<Option<Vec<String>>> {
2686 match self {
2687 Operation::CreateTable { name, .. } => Ok(Some(vec![format!(
2688 "DROP TABLE {};",
2689 quote_identifier(name)
2690 )])),
2691 Operation::AddColumn { table, column, .. } => Ok(Some(vec![format!(
2692 "ALTER TABLE {} DROP COLUMN {};",
2693 quote_identifier(table),
2694 quote_identifier(&column.name)
2695 )])),
2696 Operation::RunSQL { reverse_sql, .. } => {
2697 Ok(reverse_sql.as_ref().map(|s| vec![s.to_string()]))
2698 }
2699 Operation::RunRust { reverse_code, .. } => Ok(reverse_code.as_ref().map(|code| {
2700 vec![format!(
2701 "-- RunRust (reverse): {}",
2702 code.lines().next().unwrap_or("")
2703 )]
2704 })),
2705 Operation::RenameTable { old_name, new_name } => Ok(Some(vec![format!(
2707 "ALTER TABLE {} RENAME TO {};",
2708 quote_identifier(new_name),
2709 quote_identifier(old_name)
2710 )])),
2711 Operation::RenameColumn {
2712 table,
2713 old_name,
2714 new_name,
2715 } => Ok(Some(vec![format!(
2716 "ALTER TABLE {} RENAME COLUMN {} TO {};",
2717 quote_identifier(table),
2718 quote_identifier(new_name),
2719 quote_identifier(old_name)
2720 )])),
2721 Operation::CreateIndex {
2722 table,
2723 columns,
2724 expressions,
2725 ..
2726 } => {
2727 let index_name = generated_index_name(table, columns, expressions.as_deref());
2730 let sql = match dialect {
2734 SqlDialect::Mysql => format!(
2735 "DROP INDEX {} ON {};",
2736 quote_identifier(&index_name),
2737 quote_identifier(table)
2738 ),
2739 SqlDialect::Postgres | SqlDialect::Sqlite | SqlDialect::Cockroachdb => {
2740 format!("DROP INDEX {};", quote_identifier(&index_name))
2741 }
2742 };
2743 Ok(Some(vec![sql]))
2744 }
2745 Operation::CreateIndexRepair {
2746 table,
2747 name,
2748 columns,
2749 expressions,
2750 ..
2751 } => {
2752 let index_name = name.clone().unwrap_or_else(|| {
2753 generated_index_name(table, columns, expressions.as_deref())
2754 });
2755 let sql = match dialect {
2756 SqlDialect::Mysql => format!(
2757 "DROP INDEX {} ON {};",
2758 quote_identifier(&index_name),
2759 quote_identifier(table)
2760 ),
2761 SqlDialect::Postgres | SqlDialect::Sqlite | SqlDialect::Cockroachdb => {
2762 format!("DROP INDEX {};", quote_identifier(&index_name))
2763 }
2764 };
2765 Ok(Some(vec![sql]))
2766 }
2767 Operation::AddConstraint {
2768 table,
2769 constraint_sql,
2770 } => {
2771 let constraint_name =
2774 Self::extract_constraint_name(constraint_sql).ok_or_else(|| {
2775 super::MigrationError::InvalidMigration(format!(
2776 "Cannot extract constraint name from: {}",
2777 constraint_sql
2778 ))
2779 })?;
2780 Ok(Some(vec![format!(
2781 "ALTER TABLE {} DROP CONSTRAINT {};",
2782 quote_identifier(table),
2783 quote_identifier(&constraint_name)
2784 )]))
2785 }
2786 Operation::DropColumn { table, column } => {
2788 if let Some(model) = project_state.find_model_by_table(table)
2790 && let Some(field) = model.get_field(column)
2791 {
2792 let col_def = ColumnDefinition::from_field_state(column.clone(), field);
2793 let col_sql = Self::column_to_sql(&col_def, dialect);
2794 return Ok(Some(vec![format!(
2795 "ALTER TABLE {} ADD COLUMN {};",
2796 quote_identifier(table),
2797 col_sql
2798 )]));
2799 }
2800 Ok(None)
2802 }
2803 Operation::AlterColumn {
2804 table,
2805 column,
2806 old_definition,
2807 new_definition: _,
2808 ..
2809 } => {
2810 let resolved_old_def = old_definition.clone().or_else(|| {
2813 project_state
2814 .find_model_by_table(table)
2815 .and_then(|model| model.get_field(column))
2816 .map(|field| ColumnDefinition::from_field_state(column.clone(), field))
2817 });
2818
2819 let Some(old_def) = resolved_old_def else {
2820 return Ok(None);
2822 };
2823
2824 let type_sql = old_def.type_definition.to_sql_for_dialect(dialect);
2825 let null_clause = if old_def.not_null { " NOT NULL" } else { "" };
2826
2827 let stmts = match dialect {
2834 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
2835 let nullability_clause = if old_def.not_null {
2850 "SET NOT NULL"
2851 } else {
2852 "DROP NOT NULL"
2853 };
2854 vec![
2855 format!(
2856 "ALTER TABLE {table} ALTER COLUMN {column} TYPE {type_sql};",
2857 table = quote_identifier(table),
2858 column = quote_identifier(column),
2859 type_sql = type_sql,
2860 ),
2861 format!(
2862 "ALTER TABLE {table} ALTER COLUMN {column} {nullability_clause};",
2863 table = quote_identifier(table),
2864 column = quote_identifier(column),
2865 nullability_clause = nullability_clause,
2866 ),
2867 ]
2868 }
2869 SqlDialect::Mysql => vec![format!(
2870 "ALTER TABLE {} MODIFY COLUMN {} {}{};",
2871 quote_identifier(table),
2872 quote_identifier(column),
2873 type_sql,
2874 null_clause
2875 )],
2876 SqlDialect::Sqlite => vec![format!(
2877 "-- SQLite does not support ALTER COLUMN, table recreation required for {}",
2878 quote_identifier(table)
2879 )],
2880 };
2881 Ok(Some(stmts))
2882 }
2883 Operation::DropIndex { table, columns } => {
2884 let index_name = generated_index_name(table, columns, None);
2888 let columns_list = columns
2889 .iter()
2890 .map(|c| quote_identifier(c).to_string())
2891 .collect::<Vec<_>>()
2892 .join(", ");
2893 Ok(Some(vec![format!(
2894 "CREATE INDEX {} ON {} ({});",
2895 quote_identifier(&index_name),
2896 quote_identifier(table),
2897 columns_list
2898 )]))
2899 }
2900 Operation::DropNamedIndex {
2901 table,
2902 name,
2903 columns,
2904 unique,
2905 index_type,
2906 where_clause,
2907 concurrently,
2908 expressions,
2909 mysql_options,
2910 operator_class,
2911 ..
2912 } => {
2913 let create = Operation::CreateIndexRepair {
2914 table: table.clone(),
2915 name: Some(name.clone()),
2916 columns: columns.clone(),
2917 unique: *unique,
2918 index_type: *index_type,
2919 where_clause: where_clause.clone(),
2920 concurrently: *concurrently,
2921 expressions: expressions.clone(),
2922 mysql_options: *mysql_options,
2923 operator_class: operator_class.clone(),
2924 };
2925 Ok(Some(vec![create.to_sql(dialect)]))
2926 }
2927 Operation::DropConstraint {
2928 table,
2929 constraint_name,
2930 } => {
2931 if let Some(model) = project_state.find_model_by_table(table)
2933 && let Some(constraint_def) = model
2934 .constraints
2935 .iter()
2936 .find(|c| c.name == *constraint_name)
2937 {
2938 let constraint = constraint_def.to_constraint();
2939 return Ok(Some(vec![format!(
2940 "ALTER TABLE {} ADD {};",
2941 quote_identifier(table),
2942 constraint
2943 )]));
2944 }
2945 Ok(None)
2947 }
2948 Operation::DropTable { name } => {
2949 if let Some(model) = project_state.find_model_by_table(name) {
2951 let mut parts = Vec::new();
2952
2953 for (field_name, field) in &model.fields {
2955 let col_def = ColumnDefinition::from_field_state(field_name.clone(), field);
2956 parts.push(format!(" {}", Self::column_to_sql(&col_def, dialect)));
2957 }
2958
2959 for constraint_def in &model.constraints {
2961 let constraint = constraint_def.to_constraint();
2962 parts.push(format!(" {}", constraint));
2963 }
2964
2965 return Ok(Some(vec![format!(
2966 "CREATE TABLE {} (\n{}\n);",
2967 quote_identifier(name),
2968 parts.join(",\n")
2969 )]));
2970 }
2971 Ok(None)
2973 }
2974 Operation::BulkLoad { table, .. } => {
2975 Ok(Some(vec![format!(
2978 "TRUNCATE TABLE {};",
2979 quote_identifier(table)
2980 )]))
2981 }
2982 _ => Ok(None),
2983 }
2984 }
2985
2986 pub fn state_backwards(&self, app_label: &str, state: &mut ProjectState) {
3006 match self {
3007 Operation::CreateTable { name, .. } => {
3008 state
3010 .models
3011 .remove(&(app_label.to_string(), name.to_string()));
3012 }
3013 Operation::DropTable { name: _ } => {
3014 }
3017 Operation::RenameTable { old_name, new_name } => {
3018 if let Some(mut model) = state
3020 .models
3021 .remove(&(app_label.to_string(), new_name.to_string()))
3022 {
3023 model.table_name = old_name.to_string();
3024 state
3025 .models
3026 .insert((app_label.to_string(), old_name.to_string()), model);
3027 }
3028 }
3029 Operation::AddColumn { table, column, .. } => {
3030 if let Some(model) = state.find_model_by_table_mut(table) {
3032 model.remove_field(&column.name);
3033 }
3034 }
3035 Operation::DropColumn {
3036 table: _,
3037 column: _,
3038 } => {
3039 }
3042 Operation::AlterColumn {
3043 table: _,
3044 column: _,
3045 ..
3046 } => {
3047 }
3050 Operation::RenameColumn {
3051 table,
3052 old_name,
3053 new_name,
3054 } => {
3055 if let Some(model) = state.find_model_by_table_mut(table) {
3057 model.rename_field(new_name, old_name.to_string());
3058 }
3059 }
3060 Operation::AddConstraint { table, .. } => {
3061 if let Some(model) = state.find_model_by_table_mut(table) {
3064 let _ = model;
3067 }
3068 }
3069 Operation::DropConstraint {
3070 table: _,
3071 constraint_name: _,
3072 } => {
3073 }
3076 _ => {
3077 }
3079 }
3080 }
3081
3082 fn extract_constraint_name(constraint_sql: &str) -> Option<String> {
3088 let sql = constraint_sql.trim();
3089
3090 if sql.starts_with("CONSTRAINT ") || sql.contains(" CONSTRAINT ") {
3092 let parts: Vec<&str> = sql.split_whitespace().collect();
3093 if let Some(pos) = parts.iter().position(|&s| s == "CONSTRAINT")
3094 && pos + 1 < parts.len()
3095 {
3096 return Some(parts[pos + 1].to_string());
3097 }
3098 }
3099
3100 None
3101 }
3102}
3103
3104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3106pub struct ColumnDefinition {
3107 pub name: String,
3109 pub type_definition: FieldType,
3111 #[serde(default)]
3112 pub not_null: bool,
3114 #[serde(default)]
3115 pub unique: bool,
3117 #[serde(default)]
3118 pub primary_key: bool,
3120 #[serde(default)]
3121 pub auto_increment: bool,
3123 #[serde(default)]
3124 pub default: Option<String>,
3126}
3127
3128impl ColumnDefinition {
3129 pub fn new(name: impl Into<String>, type_def: FieldType) -> Self {
3131 Self {
3132 name: name.into(),
3133 type_definition: type_def,
3134 not_null: false,
3135 unique: false,
3136 primary_key: false,
3137 auto_increment: false,
3138 default: None,
3139 }
3140 }
3141
3142 pub fn from_field_state(name: impl Into<String>, field_state: &FieldState) -> Self {
3161 let name_str = name.into();
3162 let params = &field_state.params;
3163
3164 let primary_key = params
3166 .get("primary_key")
3167 .and_then(|v| v.parse::<bool>().ok())
3168 .unwrap_or(false);
3169
3170 let not_null = !field_state.nullable || primary_key;
3187
3188 let unique = params
3189 .get("unique")
3190 .and_then(|v| v.parse::<bool>().ok())
3191 .unwrap_or(false);
3192
3193 let auto_increment = params
3194 .get("auto_increment")
3195 .and_then(|v| v.parse::<bool>().ok())
3196 .unwrap_or(false);
3197
3198 let default = params.get("default").cloned();
3199
3200 let type_definition = resolve_foreign_key_column_type(field_state)
3213 .unwrap_or_else(|| field_state.field_type.clone());
3214
3215 Self {
3216 name: name_str,
3217 type_definition,
3218 not_null,
3219 unique,
3220 primary_key,
3221 auto_increment,
3222 default,
3223 }
3224 }
3225}
3226
3227fn resolve_foreign_key_column_type(field_state: &FieldState) -> Option<FieldType> {
3269 resolve_foreign_key_column_type_with(field_state, super::model_registry::global_registry())
3270}
3271
3272fn resolve_foreign_key_column_type_with(
3280 field_state: &FieldState,
3281 registry: &super::model_registry::ModelRegistry,
3282) -> Option<FieldType> {
3283 let target_model = field_state.params.get("fk_target")?;
3284 let target = match field_state.params.get("fk_target_app") {
3290 Some(app) => registry
3291 .find_model_qualified(app, target_model)
3292 .or_else(|| registry.find_model_by_name(target_model)),
3293 None => registry.find_model_by_name(target_model),
3294 };
3295 let target = match target {
3296 Some(t) => t,
3297 None => {
3298 if registry.count_models_by_name(target_model) > 1 {
3303 tracing::warn!(
3304 model_name = %target_model,
3305 fk_target_app = ?field_state.params.get("fk_target_app"),
3306 "FK target name is ambiguous across apps and the qualified \
3307 lookup did not resolve a unique target. Refusing to resolve \
3308 to avoid silent wrong-target resolution. Ensure the FK \
3309 target type is registered and that its `Model::app_label()` \
3310 matches one of the registered apps.",
3311 );
3312 }
3313 return None;
3314 }
3315 };
3316 let pk_field = target
3318 .fields
3319 .values()
3320 .find(|f| f.params.get("primary_key").map(String::as_str) == Some("true"))?;
3321 Some(pk_field.field_type.clone())
3322}
3323
3324pub fn field_type_string_to_field_type(
3352 field_type: &str,
3353 attributes: &std::collections::HashMap<String, String>,
3354) -> Result<FieldType, String> {
3355 let type_name = field_type.split('.').next_back().unwrap_or(field_type);
3357
3358 match type_name {
3359 "IntegerField"
3361 | "PositiveIntegerField"
3362 | "SmallIntegerField"
3363 | "PositiveSmallIntegerField" => Ok(FieldType::Integer),
3364 "BigIntegerField" | "PositiveBigIntegerField" => Ok(FieldType::BigInteger),
3365 "AutoField" => Ok(FieldType::Integer),
3366 "BigAutoField" => Ok(FieldType::BigInteger),
3367 "SmallAutoField" => Ok(FieldType::SmallInteger),
3368
3369 "CharField" => {
3371 let max_length = attributes
3372 .get("max_length")
3373 .and_then(|v| v.parse::<u32>().ok())
3374 .ok_or_else(|| "CharField requires max_length attribute".to_string())?;
3375 Ok(FieldType::VarChar(max_length))
3376 }
3377 "TextField" => Ok(FieldType::Text),
3378 "SlugField" => {
3379 let max_length = attributes
3380 .get("max_length")
3381 .and_then(|v| v.parse::<u32>().ok())
3382 .unwrap_or(50);
3383 Ok(FieldType::VarChar(max_length))
3384 }
3385 "EmailField" => {
3386 let max_length = attributes
3387 .get("max_length")
3388 .and_then(|v| v.parse::<u32>().ok())
3389 .unwrap_or(254);
3390 Ok(FieldType::VarChar(max_length))
3391 }
3392 "URLField" => {
3393 let max_length = attributes
3394 .get("max_length")
3395 .and_then(|v| v.parse::<u32>().ok())
3396 .unwrap_or(200);
3397 Ok(FieldType::VarChar(max_length))
3398 }
3399
3400 "BooleanField" => Ok(FieldType::Boolean),
3402 "NullBooleanField" => Ok(FieldType::Boolean),
3403
3404 "DateField" => Ok(FieldType::Date),
3406 "TimeField" => Ok(FieldType::Time),
3407 "DateTimeField" => Ok(FieldType::DateTime),
3408 "DurationField" => Ok(FieldType::BigInteger), "FloatField" => Ok(FieldType::Float),
3412 "DecimalField" => {
3413 let precision = attributes
3414 .get("max_digits")
3415 .and_then(|v| v.parse::<u32>().ok())
3416 .unwrap_or(10);
3417 let scale = attributes
3418 .get("decimal_places")
3419 .and_then(|v| v.parse::<u32>().ok())
3420 .unwrap_or(2);
3421 Ok(FieldType::Decimal { precision, scale })
3422 }
3423
3424 "BinaryField" => Ok(FieldType::Binary),
3426
3427 "UUIDField" => Ok(FieldType::Uuid),
3429
3430 "JSONField" => Ok(FieldType::Json),
3432
3433 "FileField" | "ImageField" => {
3435 let max_length = attributes
3436 .get("max_length")
3437 .and_then(|v| v.parse::<u32>().ok())
3438 .unwrap_or(100);
3439 Ok(FieldType::VarChar(max_length))
3440 }
3441
3442 "GenericIPAddressField" | "IPAddressField" => {
3444 Ok(FieldType::VarChar(39)) }
3447
3448 "ForeignKey" => {
3450 Ok(FieldType::BigInteger)
3452 }
3453 "OneToOneField" => Ok(FieldType::BigInteger),
3454
3455 other => Err(format!("Unsupported field type: {}", other)),
3457 }
3458}
3459
3460#[derive(Debug, Clone, Copy)]
3462pub enum SqlDialect {
3463 Sqlite,
3465 Postgres,
3467 Mysql,
3469 Cockroachdb,
3471}
3472
3473#[derive(Debug, Clone)]
3492pub struct SqliteTableRecreation {
3493 pub table_name: String,
3495 pub new_columns: Vec<ColumnDefinition>,
3497 pub columns_to_copy: Vec<String>,
3499 pub constraints: Vec<Constraint>,
3501 pub raw_constraint_sqls: Vec<String>,
3503 pub without_rowid: bool,
3505}
3506
3507impl SqliteTableRecreation {
3508 pub fn for_drop_column(
3510 table_name: impl Into<String>,
3511 current_columns: Vec<ColumnDefinition>,
3512 column_to_drop: &str,
3513 current_constraints: Vec<Constraint>,
3514 ) -> Self {
3515 let table_name = table_name.into();
3516 let new_columns: Vec<_> = current_columns
3517 .into_iter()
3518 .filter(|c| c.name != column_to_drop)
3519 .collect();
3520 let columns_to_copy: Vec<_> = new_columns.iter().map(|c| c.name.to_string()).collect();
3521
3522 let constraints: Vec<_> = current_constraints
3524 .into_iter()
3525 .filter(|c| !Self::constraint_references_column(c, column_to_drop))
3526 .collect();
3527
3528 Self {
3529 table_name,
3530 new_columns,
3531 columns_to_copy,
3532 constraints,
3533 raw_constraint_sqls: Vec::new(),
3534 without_rowid: false,
3535 }
3536 }
3537
3538 pub fn for_alter_column(
3540 table_name: impl Into<String>,
3541 current_columns: Vec<ColumnDefinition>,
3542 column_name: &str,
3543 new_definition: ColumnDefinition,
3544 current_constraints: Vec<Constraint>,
3545 ) -> Self {
3546 let table_name = table_name.into();
3547 let new_columns: Vec<_> = current_columns
3548 .into_iter()
3549 .map(|c| {
3550 if c.name == column_name {
3551 new_definition.clone()
3552 } else {
3553 c
3554 }
3555 })
3556 .collect();
3557 let columns_to_copy: Vec<_> = new_columns.iter().map(|c| c.name.to_string()).collect();
3558
3559 Self {
3560 table_name,
3561 new_columns,
3562 columns_to_copy,
3563 constraints: current_constraints,
3564 raw_constraint_sqls: Vec::new(),
3565 without_rowid: false,
3566 }
3567 }
3568
3569 pub fn for_add_constraint(
3574 table_name: impl Into<String>,
3575 current_columns: Vec<ColumnDefinition>,
3576 current_constraints: Vec<Constraint>,
3577 constraint_sql: String,
3578 ) -> Self {
3579 let table_name = table_name.into();
3580 let columns_to_copy: Vec<_> = current_columns.iter().map(|c| c.name.to_string()).collect();
3581
3582 Self {
3583 table_name,
3584 new_columns: current_columns,
3585 columns_to_copy,
3586 constraints: current_constraints,
3587 raw_constraint_sqls: vec![constraint_sql],
3588 without_rowid: false,
3589 }
3590 }
3591
3592 pub fn for_drop_constraint(
3597 table_name: impl Into<String>,
3598 current_columns: Vec<ColumnDefinition>,
3599 current_constraints: Vec<Constraint>,
3600 constraint_name: &str,
3601 ) -> Self {
3602 let table_name = table_name.into();
3603 let columns_to_copy: Vec<_> = current_columns.iter().map(|c| c.name.to_string()).collect();
3604
3605 let constraints: Vec<_> = current_constraints
3607 .into_iter()
3608 .filter(|c| !Self::constraint_has_name(c, constraint_name))
3609 .collect();
3610
3611 Self {
3612 table_name,
3613 new_columns: current_columns,
3614 columns_to_copy,
3615 constraints,
3616 raw_constraint_sqls: Vec::new(),
3617 without_rowid: false,
3618 }
3619 }
3620
3621 pub fn to_sql_statements(&self) -> Vec<String> {
3623 let temp_table = format!("{}_new", self.table_name);
3624
3625 let column_defs: Vec<String> = self
3627 .new_columns
3628 .iter()
3629 .map(|c| Operation::column_to_sql(c, &SqlDialect::Sqlite))
3630 .collect();
3631
3632 let constraint_defs: Vec<String> = self.constraints.iter().map(|c| c.to_string()).collect();
3633
3634 let mut create_parts = column_defs;
3635 create_parts.extend(constraint_defs);
3636 create_parts.extend(self.raw_constraint_sqls.clone());
3638
3639 let mut create_sql = format!(
3640 "CREATE TABLE \"{}\" (\n {}\n)",
3641 temp_table,
3642 create_parts.join(",\n ")
3643 );
3644 if self.without_rowid {
3645 create_sql.push_str(" WITHOUT ROWID");
3646 }
3647 create_sql.push(';');
3648
3649 let columns_list = self
3651 .columns_to_copy
3652 .iter()
3653 .map(|c| format!("\"{}\"", c))
3654 .collect::<Vec<_>>()
3655 .join(", ");
3656 let insert_sql = format!(
3657 "INSERT INTO \"{}\" SELECT {} FROM \"{}\";",
3658 temp_table, columns_list, self.table_name
3659 );
3660
3661 let drop_sql = format!("DROP TABLE \"{}\";", self.table_name);
3663
3664 let rename_sql = format!(
3666 "ALTER TABLE \"{}\" RENAME TO \"{}\";",
3667 temp_table, self.table_name
3668 );
3669
3670 vec![create_sql, insert_sql, drop_sql, rename_sql]
3671 }
3672
3673 fn constraint_references_column(constraint: &Constraint, column_name: &str) -> bool {
3675 match constraint {
3676 Constraint::PrimaryKey { columns, .. } => columns.iter().any(|c| c == column_name),
3677 Constraint::ForeignKey { columns, .. } => columns.iter().any(|c| c == column_name),
3678 Constraint::Unique { columns, .. } => columns.iter().any(|c| c == column_name),
3679 Constraint::Check { expression, .. } => expression.contains(column_name),
3680 Constraint::OneToOne { column, .. } => column == column_name,
3681 Constraint::ManyToMany { source_column, .. } => source_column == column_name,
3682 Constraint::Exclude { elements, .. } => {
3683 elements.iter().any(|(col, _)| col == column_name)
3684 }
3685 }
3686 }
3687
3688 fn constraint_has_name(constraint: &Constraint, constraint_name: &str) -> bool {
3690 match constraint {
3691 Constraint::PrimaryKey { name, .. } => name == constraint_name,
3692 Constraint::ForeignKey { name, .. } => name == constraint_name,
3693 Constraint::Unique { name, .. } => name == constraint_name,
3694 Constraint::Check { name, .. } => name == constraint_name,
3695 Constraint::OneToOne { name, .. } => name == constraint_name,
3696 Constraint::ManyToMany { name, .. } => name == constraint_name,
3697 Constraint::Exclude { name, .. } => name == constraint_name,
3698 }
3699 }
3700}
3701
3702impl Operation {
3703 pub fn requires_sqlite_recreation(&self) -> bool {
3705 matches!(
3706 self,
3707 Operation::DropColumn { .. }
3708 | Operation::AlterColumn { .. }
3709 | Operation::AddConstraint { .. }
3710 | Operation::DropConstraint { .. }
3711 )
3712 }
3713
3714 pub fn reverse_requires_sqlite_recreation(&self) -> bool {
3726 matches!(
3727 self,
3728 Operation::AddColumn { .. }
3730 | Operation::AlterColumn { .. }
3732 | Operation::AddConstraint { .. }
3734 | Operation::DropConstraint { .. }
3736 )
3737 }
3738
3739 pub fn to_reverse_operation(
3755 &self,
3756 project_state: &ProjectState,
3757 ) -> super::Result<Option<Operation>> {
3758 match self {
3759 Operation::CreateTable { name, .. } => {
3760 Ok(Some(Operation::DropTable { name: name.clone() }))
3761 }
3762 Operation::DropTable { name } => {
3763 if let Some(model) = project_state.find_model_by_table(name) {
3765 let columns: Vec<ColumnDefinition> = model
3766 .fields
3767 .iter()
3768 .map(|(field_name, field)| {
3769 ColumnDefinition::from_field_state(field_name.clone(), field)
3770 })
3771 .collect();
3772 let constraints: Vec<Constraint> = model
3773 .constraints
3774 .iter()
3775 .map(|c| c.to_constraint())
3776 .collect();
3777 return Ok(Some(Operation::CreateTable {
3778 name: name.clone(),
3779 columns,
3780 constraints,
3781 without_rowid: None,
3782 interleave_in_parent: None,
3783 partition: None,
3784 }));
3785 }
3786 Ok(None)
3787 }
3788 Operation::AddColumn { table, column, .. } => Ok(Some(Operation::DropColumn {
3789 table: table.clone(),
3790 column: column.name.clone(),
3791 })),
3792 Operation::DropColumn { table, column } => {
3793 if let Some(model) = project_state.find_model_by_table(table)
3795 && let Some(field) = model.get_field(column)
3796 {
3797 let col_def = ColumnDefinition::from_field_state(column.clone(), field);
3798 return Ok(Some(Operation::AddColumn {
3799 table: table.clone(),
3800 column: col_def,
3801 mysql_options: None,
3802 }));
3803 }
3804 Ok(None)
3805 }
3806 Operation::AlterColumn {
3807 table,
3808 column,
3809 old_definition,
3810 new_definition: _,
3811 ..
3812 } => {
3813 let resolved_old_def = old_definition.clone().or_else(|| {
3817 project_state
3818 .find_model_by_table(table)
3819 .and_then(|model| model.get_field(column))
3820 .map(|field| ColumnDefinition::from_field_state(column.clone(), field))
3821 });
3822
3823 if let Some(col_def) = resolved_old_def {
3824 return Ok(Some(Operation::AlterColumn {
3825 table: table.clone(),
3826 column: column.clone(),
3827 old_definition: None,
3828 new_definition: col_def,
3829 mysql_options: None,
3830 }));
3831 }
3832 Ok(None)
3833 }
3834 Operation::AddConstraint {
3835 table,
3836 constraint_sql,
3837 } => {
3838 if let Some(constraint_name) = Self::extract_constraint_name(constraint_sql) {
3840 return Ok(Some(Operation::DropConstraint {
3841 table: table.clone(),
3842 constraint_name,
3843 }));
3844 }
3845 Err(super::MigrationError::InvalidMigration(format!(
3846 "Cannot extract constraint name from: {}",
3847 constraint_sql
3848 )))
3849 }
3850 Operation::DropConstraint {
3851 table,
3852 constraint_name,
3853 } => {
3854 if let Some(model) = project_state.find_model_by_table(table)
3856 && let Some(constraint_def) = model
3857 .constraints
3858 .iter()
3859 .find(|c| c.name == *constraint_name)
3860 {
3861 let constraint = constraint_def.to_constraint();
3862 return Ok(Some(Operation::AddConstraint {
3863 table: table.clone(),
3864 constraint_sql: format!("{}", constraint),
3865 }));
3866 }
3867 Ok(None)
3868 }
3869 Operation::RenameTable { old_name, new_name } => Ok(Some(Operation::RenameTable {
3870 old_name: new_name.clone(),
3871 new_name: old_name.clone(),
3872 })),
3873 Operation::RenameColumn {
3874 table,
3875 old_name,
3876 new_name,
3877 } => Ok(Some(Operation::RenameColumn {
3878 table: table.clone(),
3879 old_name: new_name.clone(),
3880 new_name: old_name.clone(),
3881 })),
3882 Operation::CreateIndex {
3883 table,
3884 columns,
3885 unique,
3886 index_type,
3887 where_clause,
3888 concurrently,
3889 expressions,
3890 mysql_options,
3891 operator_class,
3892 } => Ok(Some(Operation::DropNamedIndex {
3893 table: table.clone(),
3894 name: generated_index_name(table, columns, expressions.as_deref()),
3895 columns: columns.clone(),
3896 unique: *unique,
3897 index_type: *index_type,
3898 where_clause: where_clause.clone(),
3899 concurrently: *concurrently,
3900 expressions: expressions.clone(),
3901 mysql_options: *mysql_options,
3902 operator_class: operator_class.clone(),
3903 })),
3904 Operation::CreateIndexRepair {
3905 table,
3906 name,
3907 columns,
3908 unique,
3909 index_type,
3910 where_clause,
3911 concurrently,
3912 expressions,
3913 mysql_options,
3914 operator_class,
3915 } => Ok(Some(Operation::DropNamedIndex {
3916 table: table.clone(),
3917 name: name.clone().unwrap_or_else(|| {
3918 generated_index_name(table, columns, expressions.as_deref())
3919 }),
3920 columns: columns.clone(),
3921 unique: *unique,
3922 index_type: *index_type,
3923 where_clause: where_clause.clone(),
3924 concurrently: *concurrently,
3925 expressions: expressions.clone(),
3926 mysql_options: *mysql_options,
3927 operator_class: operator_class.clone(),
3928 })),
3929 Operation::DropIndex { table, columns } => {
3930 Ok(Some(Operation::CreateIndex {
3933 table: table.clone(),
3934 columns: columns.clone(),
3935 unique: false,
3936 index_type: None,
3937 where_clause: None,
3938 concurrently: false,
3939 expressions: None,
3940 mysql_options: None,
3941 operator_class: None,
3942 }))
3943 }
3944 Operation::DropNamedIndex {
3945 table,
3946 name,
3947 columns,
3948 unique,
3949 index_type,
3950 where_clause,
3951 concurrently,
3952 expressions,
3953 mysql_options,
3954 operator_class,
3955 ..
3956 } => Ok(Some(Operation::CreateIndexRepair {
3957 table: table.clone(),
3958 name: Some(name.clone()),
3959 columns: columns.clone(),
3960 unique: *unique,
3961 index_type: *index_type,
3962 where_clause: where_clause.clone(),
3963 concurrently: *concurrently,
3964 expressions: expressions.clone(),
3965 mysql_options: *mysql_options,
3966 operator_class: operator_class.clone(),
3967 })),
3968 Operation::RunSQL { .. } | Operation::RunRust { .. } | Operation::BulkLoad { .. } => {
3970 Ok(None)
3971 }
3972 _ => Ok(None),
3974 }
3975 }
3976}
3977
3978pub use Operation::{AddColumn, AlterColumn, CreateTable, DropColumn};
3980
3981pub enum OperationStatement {
3983 TableCreate(CreateTableStatement),
3985 TableDrop(DropTableStatement),
3987 TableAlter(AlterTableStatement),
3989 TableRename(AlterTableStatement),
3991 IndexCreate(CreateIndexStatement),
3993 IndexDrop(DropIndexStatement),
3995 RawSql(String),
3997}
3998
3999impl OperationStatement {
4000 pub async fn execute<'c, E>(&self, executor: E) -> Result<(), sqlx::Error>
4002 where
4003 E: sqlx::Executor<'c, Database = sqlx::Postgres>,
4004 {
4005 use crate::backends::sql_build_helpers;
4006 use crate::backends::types::DatabaseType;
4007 let db_type = DatabaseType::Postgres;
4008 match self {
4009 OperationStatement::TableCreate(stmt) => {
4010 let sql = sql_build_helpers::build_create_table_sql(db_type, stmt);
4011 sqlx::query(&sql).execute(executor).await?;
4012 }
4013 OperationStatement::TableDrop(stmt) => {
4014 let sql = sql_build_helpers::build_drop_table_sql(db_type, stmt);
4015 sqlx::query(&sql).execute(executor).await?;
4016 }
4017 OperationStatement::TableAlter(stmt) => {
4018 let sql = sql_build_helpers::build_alter_table_sql(db_type, stmt);
4019 sqlx::query(&sql).execute(executor).await?;
4020 }
4021 OperationStatement::TableRename(stmt) => {
4022 let sql = sql_build_helpers::build_alter_table_sql(db_type, stmt);
4023 sqlx::query(&sql).execute(executor).await?;
4024 }
4025 OperationStatement::IndexCreate(stmt) => {
4026 let sql = sql_build_helpers::build_create_index_sql(db_type, stmt);
4027 sqlx::query(&sql).execute(executor).await?;
4028 }
4029 OperationStatement::IndexDrop(stmt) => {
4030 let sql = sql_build_helpers::build_drop_index_sql(db_type, stmt);
4031 sqlx::query(&sql).execute(executor).await?;
4032 }
4033 OperationStatement::RawSql(sql) => {
4034 sqlx::query(sql).execute(executor).await?;
4036 }
4037 }
4038 Ok(())
4039 }
4040
4041 pub fn to_sql_string(&self, db_type: crate::backends::types::DatabaseType) -> String {
4047 use crate::backends::sql_build_helpers;
4048
4049 match self {
4050 OperationStatement::TableCreate(stmt) => {
4051 sql_build_helpers::build_create_table_sql(db_type, stmt)
4052 }
4053 OperationStatement::TableDrop(stmt) => {
4054 sql_build_helpers::build_drop_table_sql(db_type, stmt)
4055 }
4056 OperationStatement::TableAlter(stmt) => {
4057 sql_build_helpers::build_alter_table_sql(db_type, stmt)
4058 }
4059 OperationStatement::TableRename(stmt) => {
4060 sql_build_helpers::build_alter_table_sql(db_type, stmt)
4061 }
4062 OperationStatement::IndexCreate(stmt) => {
4063 sql_build_helpers::build_create_index_sql(db_type, stmt)
4064 }
4065 OperationStatement::IndexDrop(stmt) => {
4066 sql_build_helpers::build_drop_index_sql(db_type, stmt)
4067 }
4068 OperationStatement::RawSql(sql) => sql.clone(),
4069 }
4070 }
4071}
4072
4073impl Operation {
4074 pub fn to_statement(&self) -> OperationStatement {
4076 match self {
4077 Operation::CreateTable {
4078 name,
4079 columns,
4080 constraints,
4081 ..
4082 } => {
4083 OperationStatement::TableCreate(self.build_create_table(name, columns, constraints))
4084 }
4085 Operation::DropTable { name } => {
4086 OperationStatement::TableDrop(self.build_drop_table(name))
4087 }
4088 Operation::AddColumn { table, column, .. } => {
4089 OperationStatement::TableAlter(self.build_add_column(table, column))
4090 }
4091 Operation::DropColumn { table, column } => {
4092 OperationStatement::TableAlter(self.build_drop_column(table, column))
4093 }
4094 Operation::AlterColumn {
4095 table,
4096 column,
4097 new_definition,
4098 ..
4099 } => OperationStatement::TableAlter(self.build_alter_column(
4100 table,
4101 column,
4102 new_definition,
4103 )),
4104 Operation::RenameTable { old_name, new_name } => {
4105 OperationStatement::TableRename(self.build_rename_table(old_name, new_name))
4106 }
4107 Operation::RenameColumn {
4109 table,
4110 old_name,
4111 new_name,
4112 } => OperationStatement::RawSql(format!(
4113 "ALTER TABLE {} RENAME COLUMN {} TO {}",
4114 quote_identifier(table),
4115 quote_identifier(old_name),
4116 quote_identifier(new_name)
4117 )),
4118 Operation::AddConstraint {
4119 table,
4120 constraint_sql,
4121 } => {
4122 OperationStatement::RawSql(format!(
4124 "ALTER TABLE {} ADD {}",
4125 quote_identifier(table),
4126 constraint_sql
4127 ))
4128 }
4129 Operation::DropConstraint {
4130 table,
4131 constraint_name,
4132 } => OperationStatement::RawSql(format!(
4133 "ALTER TABLE {} DROP CONSTRAINT {}",
4134 quote_identifier(table),
4135 quote_identifier(constraint_name)
4136 )),
4137 Operation::CreateIndex {
4138 table,
4139 columns,
4140 unique,
4141 ..
4142 } => {
4143 let idx_name = format!("idx_{}_{}", table, columns.join("_"));
4144 OperationStatement::IndexCreate(
4145 self.build_create_index(&idx_name, table, columns, *unique),
4146 )
4147 }
4148 Operation::CreateIndexRepair {
4149 table,
4150 name,
4151 columns,
4152 unique,
4153 expressions,
4154 ..
4155 } => {
4156 let generated_name;
4157 let idx_name = if let Some(name) = name.as_deref() {
4158 name
4159 } else {
4160 generated_name = generated_index_name(table, columns, expressions.as_deref());
4161 &generated_name
4162 };
4163 OperationStatement::IndexCreate(
4164 self.build_create_index(idx_name, table, columns, *unique),
4165 )
4166 }
4167 Operation::DropIndex { table, columns } => {
4168 let idx_name = format!("idx_{}_{}", table, columns.join("_"));
4169 OperationStatement::IndexDrop(self.build_drop_index(&idx_name))
4170 }
4171 Operation::DropNamedIndex { name, .. } => {
4172 OperationStatement::IndexDrop(self.build_drop_index(name))
4173 }
4174 Operation::RunSQL { sql, .. } => OperationStatement::RawSql(sql.to_string()),
4175 Operation::RunRust { code, .. } => {
4176 OperationStatement::RawSql(format!(
4178 "-- RunRust: {}",
4179 code.lines().next().unwrap_or("")
4180 ))
4181 }
4182 Operation::AlterTableComment { table, comment } => {
4183 OperationStatement::RawSql(if let Some(comment_text) = comment {
4185 format!(
4186 "COMMENT ON TABLE {} IS '{}'",
4187 quote_identifier(table),
4188 comment_text.replace('\'', "''") )
4190 } else {
4191 format!("COMMENT ON TABLE {} IS NULL", quote_identifier(table))
4192 })
4193 }
4194 Operation::AlterUniqueTogether {
4195 table,
4196 unique_together,
4197 } => {
4198 let mut sqls = Vec::new();
4199 for (idx, fields) in unique_together.iter().enumerate() {
4200 let constraint_name = format!("{}_{}_uniq", table, idx);
4201 let fields_str: Vec<String> = fields
4202 .iter()
4203 .map(|f| quote_identifier(f).to_string())
4204 .collect();
4205 sqls.push(format!(
4206 "ALTER TABLE {} ADD CONSTRAINT {} UNIQUE ({})",
4207 quote_identifier(table),
4208 quote_identifier(&constraint_name),
4209 fields_str.join(", ")
4210 ));
4211 }
4212 OperationStatement::RawSql(sqls.join(";\n"))
4213 }
4214 Operation::AlterModelOptions { .. } => OperationStatement::RawSql(String::new()),
4215 Operation::CreateInheritedTable {
4216 name,
4217 columns,
4218 base_table,
4219 join_column,
4220 } => {
4221 let mut stmt = Query::create_table();
4222 stmt.table(Alias::new(name.as_str())).if_not_exists();
4223
4224 let join_col = ColumnDef::new(Alias::new(join_column.as_str()));
4226 let join_col = join_col.integer();
4227 stmt.col(join_col);
4228
4229 for col in columns {
4231 let mut column = ColumnDef::new(Alias::new(col.name.as_str()));
4232 column = self.apply_column_type(column, &col.type_definition);
4233 stmt.col(column);
4234 }
4235
4236 let mut fk = reinhardt_query::prelude::ForeignKey::create();
4238 fk.from_tbl(Alias::new(name.as_str()))
4239 .from_col(Alias::new(join_column.as_str()))
4240 .to_tbl(Alias::new(base_table.as_str()))
4241 .to_col(Alias::new("id"));
4242 stmt.foreign_key_from_builder(&mut fk);
4243
4244 OperationStatement::TableCreate(stmt.to_owned())
4245 }
4246 Operation::AddDiscriminatorColumn {
4247 table,
4248 column_name,
4249 default_value,
4250 } => {
4251 let mut stmt = Query::alter_table();
4252 stmt.table(Alias::new(table.as_str()));
4253
4254 let mut col = ColumnDef::new(Alias::new(column_name.as_str()));
4255 col = col
4256 .string_len(50)
4257 .default(SimpleExpr::from(default_value.to_string()));
4258 stmt.add_column(col);
4259
4260 OperationStatement::TableAlter(stmt.to_owned())
4261 }
4262 Operation::MoveModel {
4263 rename_table,
4264 old_table_name,
4265 new_table_name,
4266 ..
4267 } => {
4268 if *rename_table {
4270 if let (Some(old_name), Some(new_name)) = (old_table_name, new_table_name) {
4271 OperationStatement::TableRename(self.build_rename_table(old_name, new_name))
4272 } else {
4273 OperationStatement::RawSql("-- MoveModel: State-only operation".to_string())
4275 }
4276 } else {
4277 OperationStatement::RawSql("-- MoveModel: State-only operation".to_string())
4279 }
4280 }
4281 Operation::CreateSchema {
4282 name,
4283 if_not_exists,
4284 } => {
4285 let sql = if *if_not_exists {
4287 format!("CREATE SCHEMA IF NOT EXISTS {}", quote_identifier(name))
4288 } else {
4289 format!("CREATE SCHEMA {}", quote_identifier(name))
4290 };
4291 OperationStatement::RawSql(sql)
4292 }
4293 Operation::DropSchema {
4294 name,
4295 cascade,
4296 if_exists,
4297 } => {
4298 let if_exists_clause = if *if_exists { " IF EXISTS" } else { "" };
4300 let cascade_clause = if *cascade { " CASCADE" } else { "" };
4301 let sql = format!(
4302 "DROP SCHEMA{} {}{}",
4303 if_exists_clause,
4304 quote_identifier(name),
4305 cascade_clause
4306 );
4307 OperationStatement::RawSql(sql)
4308 }
4309 Operation::CreateExtension {
4310 name,
4311 if_not_exists,
4312 schema,
4313 } => {
4314 let if_not_exists_clause = if *if_not_exists { " IF NOT EXISTS" } else { "" };
4316 let schema_clause = if let Some(s) = schema {
4317 format!(" SCHEMA {}", quote_identifier(s))
4318 } else {
4319 String::new()
4320 };
4321 let sql = format!(
4322 "CREATE EXTENSION{} {}{}",
4323 if_not_exists_clause,
4324 quote_identifier(name),
4325 schema_clause
4326 );
4327 OperationStatement::RawSql(sql)
4328 }
4329 Operation::BulkLoad {
4330 table,
4331 source,
4332 format,
4333 options,
4334 } => {
4335 OperationStatement::RawSql(Self::postgres_copy_from_sql(
4338 table, source, format, options,
4339 ))
4340 }
4341 Operation::SetAutoIncrementValue { table, .. } => {
4342 OperationStatement::RawSql(format!(
4354 "SELECT 1/0 AS \"SetAutoIncrementValue on {} requires dialect-aware rendering; call Operation::to_sql(&dialect) instead of to_statement()\";",
4355 table.replace('"', "\"\"")
4356 ))
4357 }
4358 Operation::CreateCompositePrimaryKey {
4359 table,
4360 columns,
4361 constraint_name,
4362 } => OperationStatement::RawSql(Self::create_composite_pk_to_sql(
4363 table,
4364 columns,
4365 constraint_name.as_deref(),
4366 )),
4367 }
4368 }
4369
4370 fn build_create_table(
4372 &self,
4373 name: &str,
4374 columns: &[ColumnDefinition],
4375 constraints: &[Constraint],
4376 ) -> CreateTableStatement {
4377 let mut stmt = Query::create_table();
4378 stmt.table(Alias::new(name)).if_not_exists();
4379
4380 for col in columns {
4381 let mut column = ColumnDef::new(Alias::new(col.name.as_str()));
4382 column = self.apply_column_type(column, &col.type_definition);
4383
4384 if col.not_null {
4385 column = column.not_null(true);
4386 }
4387 if col.unique {
4388 column = column.unique(true);
4389 }
4390 if col.primary_key {
4391 column = column.primary_key(true);
4392 }
4393 if col.auto_increment {
4394 column = column.auto_increment(true);
4395 }
4396 if let Some(default) = &col.default {
4397 column = column.default(SimpleExpr::from(self.convert_default_value(default)));
4398 }
4399
4400 stmt.col(column);
4401 }
4402
4403 for constraint in constraints {
4405 match constraint {
4406 Constraint::PrimaryKey { columns, .. } => {
4407 let col_idens: Vec<Alias> =
4408 columns.iter().map(|c| Alias::new(c.as_str())).collect();
4409 stmt.primary_key(col_idens);
4410 }
4411 Constraint::ForeignKey {
4412 name,
4413 columns,
4414 referenced_table,
4415 referenced_columns,
4416 on_delete,
4417 on_update,
4418 ..
4419 } => {
4420 let mut fk = reinhardt_query::prelude::ForeignKey::create();
4421 fk.name(Alias::new(name.as_str()))
4422 .from_tbl(Alias::new(name.as_str()))
4423 .to_tbl(Alias::new(referenced_table.as_str()));
4424
4425 for col in columns {
4426 fk.from_col(Alias::new(col.as_str()));
4427 }
4428 for col in referenced_columns {
4429 fk.to_col(Alias::new(col.as_str()));
4430 }
4431
4432 fk.on_delete((*on_delete).into());
4433 fk.on_update((*on_update).into());
4434
4435 stmt.foreign_key_from_builder(&mut fk);
4436 }
4437 Constraint::Unique { columns, .. } => {
4438 let col_idens: Vec<Alias> =
4439 columns.iter().map(|c| Alias::new(c.as_str())).collect();
4440 stmt.unique(col_idens);
4441 }
4442 Constraint::Check { name, expression } => {
4443 let _ = (name, expression); }
4447 Constraint::OneToOne {
4448 name,
4449 column,
4450 referenced_table,
4451 referenced_column,
4452 on_delete,
4453 on_update,
4454 ..
4455 } => {
4456 let mut fk = reinhardt_query::prelude::ForeignKey::create();
4458 fk.name(Alias::new(name.as_str()))
4459 .from_tbl(Alias::new(name.as_str()))
4460 .to_tbl(Alias::new(referenced_table.as_str()))
4461 .from_col(Alias::new(column.as_str()))
4462 .to_col(Alias::new(referenced_column.as_str()))
4463 .on_delete((*on_delete).into())
4464 .on_update((*on_update).into());
4465
4466 stmt.foreign_key_from_builder(&mut fk);
4467
4468 }
4471 Constraint::ManyToMany { .. } => {
4472 }
4475 Constraint::Exclude { .. } => {
4476 }
4479 }
4480 }
4481
4482 stmt.to_owned()
4483 }
4484
4485 fn build_drop_table(&self, name: &str) -> DropTableStatement {
4487 Query::drop_table()
4488 .table(Alias::new(name))
4489 .if_exists()
4490 .cascade()
4491 .to_owned()
4492 }
4493
4494 fn build_add_column(&self, table: &str, column: &ColumnDefinition) -> AlterTableStatement {
4496 let mut stmt = Query::alter_table();
4497 stmt.table(Alias::new(table));
4498
4499 let mut col_def = ColumnDef::new(Alias::new(column.name.as_str()));
4500 col_def = self.apply_column_type(col_def, &column.type_definition);
4501
4502 if column.not_null {
4503 col_def = col_def.not_null(true);
4504 }
4505 if let Some(default) = &column.default {
4506 col_def = col_def.default(SimpleExpr::from(self.convert_default_value(default)));
4507 }
4508
4509 stmt.add_column(col_def);
4510 stmt.to_owned()
4511 }
4512
4513 fn build_drop_column(&self, table: &str, column: &str) -> AlterTableStatement {
4515 Query::alter_table()
4516 .table(Alias::new(table))
4517 .drop_column(Alias::new(column))
4518 .to_owned()
4519 }
4520
4521 fn build_alter_column(
4523 &self,
4524 table: &str,
4525 column: &str,
4526 new_definition: &ColumnDefinition,
4527 ) -> AlterTableStatement {
4528 let mut stmt = Query::alter_table();
4529 stmt.table(Alias::new(table));
4530
4531 let mut col_def = ColumnDef::new(Alias::new(column));
4532 col_def = self.apply_column_type(col_def, &new_definition.type_definition);
4533
4534 if new_definition.not_null {
4535 col_def = col_def.not_null(true);
4536 }
4537
4538 stmt.modify_column(col_def);
4539 stmt.to_owned()
4540 }
4541
4542 fn build_rename_table(&self, old_name: &str, new_name: &str) -> AlterTableStatement {
4544 Query::alter_table()
4545 .table(Alias::new(old_name))
4546 .rename_table(Alias::new(new_name))
4547 .to_owned()
4548 }
4549
4550 fn build_create_index(
4552 &self,
4553 name: &str,
4554 table: &str,
4555 columns: &[String],
4556 unique: bool,
4557 ) -> CreateIndexStatement {
4558 let mut stmt = Query::create_index();
4559 stmt.name(Alias::new(name)).table(Alias::new(table));
4560
4561 for col in columns {
4562 stmt.col(Alias::new(col));
4563 }
4564
4565 if unique {
4566 stmt.unique();
4567 }
4568
4569 stmt.to_owned()
4570 }
4571
4572 fn build_drop_index(&self, name: &str) -> DropIndexStatement {
4574 Query::drop_index().name(Alias::new(name)).to_owned()
4575 }
4576
4577 fn apply_column_type(&self, col_def: ColumnDef, field_type: &FieldType) -> ColumnDef {
4579 use FieldType;
4580 match field_type {
4581 FieldType::Integer => col_def.integer(),
4582 FieldType::BigInteger => col_def.big_integer(),
4583 FieldType::SmallInteger => col_def.small_integer(),
4584 FieldType::TinyInt => col_def.tiny_integer(),
4585 FieldType::VarChar(max_length) => col_def.string_len(*max_length),
4586 FieldType::Char(max_length) => col_def.char_len(*max_length),
4587 FieldType::Text | FieldType::TinyText | FieldType::MediumText | FieldType::LongText => {
4588 col_def.text()
4589 }
4590 FieldType::Boolean => col_def.custom(Alias::new("BOOLEAN")),
4596 FieldType::DateTime => col_def.timestamp(),
4597 FieldType::TimestampTz => col_def.timestamp_with_time_zone(),
4598 FieldType::Date => col_def.date(),
4599 FieldType::Time => col_def.time(),
4600 FieldType::Decimal { precision, scale } => col_def.decimal(*precision, *scale),
4601 FieldType::Float => col_def.float(),
4602 FieldType::Double | FieldType::Real => col_def.double(),
4603 FieldType::Json => col_def.json(),
4604 FieldType::JsonBinary => col_def.json_binary(),
4605 FieldType::Uuid => col_def.uuid(),
4606 FieldType::Binary | FieldType::Bytea => col_def.binary(0),
4607 FieldType::Blob | FieldType::TinyBlob | FieldType::MediumBlob | FieldType::LongBlob => {
4608 col_def.binary(0)
4609 }
4610 FieldType::MediumInt => col_def.integer(),
4611 FieldType::Year => col_def.small_integer(),
4612 FieldType::Enum { values } => {
4613 col_def.custom(Alias::new(format!("ENUM({})", values.join(","))))
4614 }
4615 FieldType::Set { values } => {
4616 col_def.custom(Alias::new(format!("SET({})", values.join(","))))
4617 }
4618 FieldType::ForeignKey { .. } => {
4619 col_def.integer()
4621 }
4622 FieldType::OneToOne { .. } => {
4623 col_def.big_integer()
4626 }
4627 FieldType::ManyToMany { .. } => {
4628 col_def.big_integer()
4631 }
4632 FieldType::Array(inner) => {
4634 let inner_sql = inner.to_sql_string();
4636 col_def.custom(Alias::new(format!("{}[]", inner_sql)))
4637 }
4638 FieldType::HStore => col_def.custom(Alias::new("HSTORE")),
4639 FieldType::CIText => col_def.custom(Alias::new("CITEXT")),
4640 FieldType::Int4Range => col_def.custom(Alias::new("INT4RANGE")),
4641 FieldType::Int8Range => col_def.custom(Alias::new("INT8RANGE")),
4642 FieldType::NumRange => col_def.custom(Alias::new("NUMRANGE")),
4643 FieldType::DateRange => col_def.custom(Alias::new("DATERANGE")),
4644 FieldType::TsRange => col_def.custom(Alias::new("TSRANGE")),
4645 FieldType::TsTzRange => col_def.custom(Alias::new("TSTZRANGE")),
4646 FieldType::TsVector => col_def.custom(Alias::new("TSVECTOR")),
4647 FieldType::TsQuery => col_def.custom(Alias::new("TSQUERY")),
4648 FieldType::Custom(custom_type) => col_def.custom(Alias::new(custom_type)),
4649 }
4650 }
4651
4652 fn convert_default_value(&self, default: &str) -> Value {
4654 let trimmed = default.trim();
4655
4656 if trimmed.eq_ignore_ascii_case("null") {
4658 return Value::String(None);
4659 }
4660
4661 if trimmed.eq_ignore_ascii_case("true") {
4663 return Value::Bool(Some(true));
4664 }
4665 if trimmed.eq_ignore_ascii_case("false") {
4666 return Value::Bool(Some(false));
4667 }
4668
4669 if let Ok(i) = trimmed.parse::<i64>() {
4671 return Value::BigInt(Some(i));
4672 }
4673
4674 if let Ok(f) = trimmed.parse::<f64>() {
4676 return Value::Double(Some(f));
4677 }
4678
4679 if (trimmed.starts_with('"') && trimmed.ends_with('"'))
4681 || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
4682 {
4683 let unquoted = &trimmed[1..trimmed.len() - 1];
4684 return Value::String(Some(Box::new(unquoted.to_string())));
4685 }
4686
4687 if ((trimmed.starts_with('[') && trimmed.ends_with(']'))
4689 || (trimmed.starts_with('{') && trimmed.ends_with('}')))
4690 && let Ok(json) = serde_json::from_str::<serde_json::Value>(trimmed)
4691 {
4692 return json_to_sea_value(&json);
4693 }
4694
4695 const SQL_CONSTANTS: &[&str] = &[
4697 "CURRENT_TIMESTAMP",
4698 "CURRENT_DATE",
4699 "CURRENT_TIME",
4700 "CURRENT_USER",
4701 "SESSION_USER",
4702 "LOCALTIME",
4703 "LOCALTIMESTAMP",
4704 ];
4705
4706 if trimmed.ends_with("()") || trimmed.contains('(') {
4708 return Value::String(Some(Box::new(trimmed.to_string())));
4709 }
4710
4711 if SQL_CONSTANTS
4713 .iter()
4714 .any(|c| trimmed.eq_ignore_ascii_case(c))
4715 {
4716 return Value::String(Some(Box::new(trimmed.to_string())));
4717 }
4718
4719 Value::String(Some(Box::new(format!("'{}'", trimmed.replace('\'', "''")))))
4721 }
4722}
4723
4724fn json_to_sea_value(json: &serde_json::Value) -> Value {
4726 match json {
4727 serde_json::Value::Null => Value::String(None),
4728 serde_json::Value::Bool(b) => Value::Bool(Some(*b)),
4729 serde_json::Value::Number(n) => {
4730 if let Some(i) = n.as_i64() {
4731 Value::BigInt(Some(i))
4732 } else if let Some(f) = n.as_f64() {
4733 Value::Double(Some(f))
4734 } else {
4735 Value::String(Some(Box::new(n.to_string())))
4736 }
4737 }
4738 serde_json::Value::String(s) => Value::String(Some(Box::new(s.clone()))),
4739 serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
4740 Value::String(Some(Box::new(json.to_string())))
4742 }
4743 }
4744}
4745
4746use super::operation_trait::MigrationOperation;
4748
4749impl MigrationOperation for Operation {
4750 fn migration_name_fragment(&self) -> Option<String> {
4751 match self {
4752 Operation::CreateTable { name, .. } => Some(name.to_lowercase()),
4753 Operation::DropTable { name } => Some(format!("delete_{}", name.to_lowercase())),
4754 Operation::AddColumn { table, column, .. } => Some(format!(
4755 "{}_{}",
4756 table.to_lowercase(),
4757 column.name.to_lowercase()
4758 )),
4759 Operation::DropColumn { table, column } => Some(format!(
4760 "remove_{}_{}",
4761 table.to_lowercase(),
4762 column.to_lowercase()
4763 )),
4764 Operation::AlterColumn { table, column, .. } => Some(format!(
4765 "alter_{}_{}",
4766 table.to_lowercase(),
4767 column.to_lowercase()
4768 )),
4769 Operation::RenameTable { old_name, new_name } => Some(format!(
4770 "rename_{}_to_{}",
4771 old_name.to_lowercase(),
4772 new_name.to_lowercase()
4773 )),
4774 Operation::RenameColumn {
4775 table, new_name, ..
4776 } => Some(format!(
4777 "rename_{}_{}",
4778 table.to_lowercase(),
4779 new_name.to_lowercase()
4780 )),
4781 Operation::AddConstraint { table, .. } => {
4782 Some(format!("add_constraint_{}", table.to_lowercase()))
4783 }
4784 Operation::DropConstraint {
4785 table: _,
4786 constraint_name,
4787 } => Some(format!(
4788 "drop_constraint_{}",
4789 constraint_name.to_lowercase()
4790 )),
4791 Operation::CreateIndex { table, unique, .. } => {
4792 if *unique {
4793 Some(format!("create_unique_index_{}", table.to_lowercase()))
4794 } else {
4795 Some(format!("create_index_{}", table.to_lowercase()))
4796 }
4797 }
4798 Operation::CreateIndexRepair { table, unique, .. } => {
4799 if *unique {
4800 Some(format!("create_unique_index_{}", table.to_lowercase()))
4801 } else {
4802 Some(format!("create_index_{}", table.to_lowercase()))
4803 }
4804 }
4805 Operation::DropIndex { table, .. } => {
4806 Some(format!("drop_index_{}", table.to_lowercase()))
4807 }
4808 Operation::DropNamedIndex { table, .. } => {
4809 Some(format!("drop_index_{}", table.to_lowercase()))
4810 }
4811 Operation::RunSQL { .. } => None, Operation::RunRust { .. } => None, Operation::AlterTableComment { table, .. } => {
4814 Some(format!("alter_comment_{}", table.to_lowercase()))
4815 }
4816 Operation::AlterUniqueTogether { table, .. } => {
4817 Some(format!("alter_unique_{}", table.to_lowercase()))
4818 }
4819 Operation::AlterModelOptions { table, .. } => {
4820 Some(format!("alter_options_{}", table.to_lowercase()))
4821 }
4822 Operation::CreateInheritedTable { name, .. } => {
4823 Some(format!("create_inherited_{}", name.to_lowercase()))
4824 }
4825 Operation::AddDiscriminatorColumn { table, .. } => {
4826 Some(format!("add_discriminator_{}", table.to_lowercase()))
4827 }
4828 Operation::MoveModel {
4829 model_name,
4830 from_app,
4831 to_app,
4832 ..
4833 } => Some(format!(
4834 "move_{}_{}_{}_{}",
4835 from_app.to_lowercase(),
4836 model_name.to_lowercase(),
4837 to_app.to_lowercase(),
4838 model_name.to_lowercase()
4839 )),
4840 Operation::CreateSchema { name, .. } => {
4841 Some(format!("create_schema_{}", name.to_lowercase()))
4842 }
4843 Operation::DropSchema { name, .. } => {
4844 Some(format!("drop_schema_{}", name.to_lowercase()))
4845 }
4846 Operation::CreateExtension { name, .. } => {
4847 Some(format!("create_extension_{}", name.to_lowercase()))
4848 }
4849 Operation::BulkLoad { table, .. } => {
4850 Some(format!("bulk_load_{}", table.to_lowercase()))
4851 }
4852 Operation::SetAutoIncrementValue { table, column, .. } => Some(format!(
4853 "set_auto_increment_{}_{}",
4854 table.to_lowercase(),
4855 column.to_lowercase()
4856 )),
4857 Operation::CreateCompositePrimaryKey { table, .. } => {
4858 Some(format!("composite_pk_{}", table.to_lowercase()))
4859 }
4860 }
4861 }
4862
4863 fn describe(&self) -> String {
4864 match self {
4865 Operation::CreateTable { name, .. } => format!("Create table {}", name),
4866 Operation::DropTable { name } => format!("Drop table {}", name),
4867 Operation::AddColumn { table, column, .. } => {
4868 format!("Add column {} to {}", column.name, table)
4869 }
4870 Operation::DropColumn { table, column } => {
4871 format!("Drop column {} from {}", column, table)
4872 }
4873 Operation::AlterColumn { table, column, .. } => {
4874 format!("Alter column {} on {}", column, table)
4875 }
4876 Operation::RenameTable { old_name, new_name } => {
4877 format!("Rename table {} to {}", old_name, new_name)
4878 }
4879 Operation::RenameColumn {
4880 table,
4881 old_name,
4882 new_name,
4883 } => format!("Rename column {} to {} on {}", old_name, new_name, table),
4884 Operation::AddConstraint { table, .. } => format!("Add constraint on {}", table),
4885 Operation::DropConstraint {
4886 table,
4887 constraint_name,
4888 } => format!("Drop constraint {} from {}", constraint_name, table),
4889 Operation::CreateIndex { table, unique, .. } => {
4890 if *unique {
4891 format!("Create unique index on {}", table)
4892 } else {
4893 format!("Create index on {}", table)
4894 }
4895 }
4896 Operation::CreateIndexRepair { table, unique, .. } => {
4897 if *unique {
4898 format!("Create unique index on {}", table)
4899 } else {
4900 format!("Create index on {}", table)
4901 }
4902 }
4903 Operation::DropIndex { table, .. } => format!("Drop index on {}", table),
4904 Operation::DropNamedIndex { table, .. } => format!("Drop index on {}", table),
4905 Operation::RunSQL { sql, .. } => {
4906 let preview = if sql.len() > 50 {
4907 format!("{}...", &sql[..50])
4908 } else {
4909 (*sql).to_string()
4910 };
4911 format!("RunSQL: {}", preview)
4912 }
4913 Operation::RunRust { code, .. } => {
4914 let preview = if code.len() > 50 {
4915 format!("{}...", &code[..50])
4916 } else {
4917 (*code).to_string()
4918 };
4919 format!("RunRust: {}", preview)
4920 }
4921 Operation::AlterTableComment { table, comment } => match comment {
4922 Some(c) => format!("Set comment on {} to '{}'", table, c),
4923 None => format!("Remove comment from {}", table),
4924 },
4925 Operation::AlterUniqueTogether { table, .. } => {
4926 format!("Alter unique_together on {}", table)
4927 }
4928 Operation::AlterModelOptions { table, .. } => {
4929 format!("Alter model options on {}", table)
4930 }
4931 Operation::CreateInheritedTable {
4932 name, base_table, ..
4933 } => {
4934 format!("Create inherited table {} from {}", name, base_table)
4935 }
4936 Operation::AddDiscriminatorColumn {
4937 table, column_name, ..
4938 } => format!("Add discriminator column {} to {}", column_name, table),
4939 Operation::MoveModel {
4940 model_name,
4941 from_app,
4942 to_app,
4943 ..
4944 } => format!("Move model {} from {} to {}", model_name, from_app, to_app),
4945 Operation::CreateSchema { name, .. } => format!("Create schema {}", name),
4946 Operation::DropSchema { name, .. } => format!("Drop schema {}", name),
4947 Operation::CreateExtension { name, .. } => format!("Create extension {}", name),
4948 Operation::BulkLoad { table, source, .. } => {
4949 let source_desc = match source {
4950 BulkLoadSource::File(path) => format!("file '{}'", path),
4951 BulkLoadSource::Stdin => "STDIN".to_string(),
4952 BulkLoadSource::Program(cmd) => format!("program '{}'", cmd),
4953 };
4954 format!("Bulk load data into {} from {}", table, source_desc)
4955 }
4956 Operation::SetAutoIncrementValue {
4957 table,
4958 column,
4959 value,
4960 } => format!("Set auto-increment of {}.{} to {}", table, column, value),
4961 Operation::CreateCompositePrimaryKey { table, columns, .. } => format!(
4962 "Create composite primary key on {} ({})",
4963 table,
4964 columns.join(", ")
4965 ),
4966 }
4967 }
4968
4969 fn normalize(&self) -> Self
4974 where
4975 Self: Sized + Clone,
4976 {
4977 match self {
4978 Operation::CreateTable {
4980 name,
4981 columns,
4982 constraints,
4983 without_rowid,
4984 interleave_in_parent,
4985 partition,
4986 } => {
4987 let mut sorted_columns = columns.clone();
4988 sorted_columns.sort_by(|a, b| a.name.cmp(&b.name));
4989
4990 let mut sorted_constraints = constraints.clone();
4991 sorted_constraints.sort();
4992
4993 Operation::CreateTable {
4994 name: name.clone(),
4995 columns: sorted_columns,
4996 constraints: sorted_constraints,
4997 without_rowid: *without_rowid,
4998 interleave_in_parent: interleave_in_parent.clone(),
4999 partition: partition.clone(),
5000 }
5001 }
5002 Operation::CreateIndex {
5004 table,
5005 columns,
5006 unique,
5007 index_type,
5008 where_clause,
5009 concurrently,
5010 expressions,
5011 mysql_options,
5012 operator_class,
5013 } => {
5014 let mut sorted_columns = columns.clone();
5015 sorted_columns.sort();
5016
5017 Operation::CreateIndex {
5018 table: table.clone(),
5019 columns: sorted_columns,
5020 unique: *unique,
5021 index_type: *index_type,
5022 where_clause: where_clause.clone(),
5023 concurrently: *concurrently,
5024 expressions: expressions.clone(),
5025 mysql_options: *mysql_options,
5026 operator_class: operator_class.clone(),
5027 }
5028 }
5029 Operation::CreateIndexRepair {
5030 table,
5031 name,
5032 columns,
5033 unique,
5034 index_type,
5035 where_clause,
5036 concurrently,
5037 expressions,
5038 mysql_options,
5039 operator_class,
5040 } => {
5041 let mut sorted_columns = columns.clone();
5042 sorted_columns.sort();
5043
5044 Operation::CreateIndexRepair {
5045 table: table.clone(),
5046 name: name.clone(),
5047 columns: sorted_columns,
5048 unique: *unique,
5049 index_type: *index_type,
5050 where_clause: where_clause.clone(),
5051 concurrently: *concurrently,
5052 expressions: expressions.clone(),
5053 mysql_options: *mysql_options,
5054 operator_class: operator_class.clone(),
5055 }
5056 }
5057 Operation::DropIndex { table, columns } => {
5059 let mut sorted_columns = columns.clone();
5060 sorted_columns.sort();
5061
5062 Operation::DropIndex {
5063 table: table.clone(),
5064 columns: sorted_columns,
5065 }
5066 }
5067 Operation::DropNamedIndex {
5068 table,
5069 name,
5070 columns,
5071 unique,
5072 index_type,
5073 where_clause,
5074 concurrently,
5075 expressions,
5076 mysql_options,
5077 operator_class,
5078 } => {
5079 let mut sorted_columns = columns.clone();
5080 sorted_columns.sort();
5081 Operation::DropNamedIndex {
5082 table: table.clone(),
5083 name: name.clone(),
5084 columns: sorted_columns,
5085 unique: *unique,
5086 index_type: *index_type,
5087 where_clause: where_clause.clone(),
5088 concurrently: *concurrently,
5089 expressions: expressions.clone(),
5090 mysql_options: *mysql_options,
5091 operator_class: operator_class.clone(),
5092 }
5093 }
5094 Operation::AlterUniqueTogether {
5096 table,
5097 unique_together,
5098 } => {
5099 let mut sorted_unique_together: Vec<Vec<String>> = unique_together
5100 .iter()
5101 .map(|field_list| {
5102 let mut sorted = field_list.clone();
5103 sorted.sort();
5104 sorted
5105 })
5106 .collect();
5107 sorted_unique_together.sort();
5108
5109 Operation::AlterUniqueTogether {
5110 table: table.clone(),
5111 unique_together: sorted_unique_together,
5112 }
5113 }
5114 Operation::AlterModelOptions { table, options } => Operation::AlterModelOptions {
5119 table: table.clone(),
5120 options: options.clone(),
5121 },
5122 _ => self.clone(),
5124 }
5125 }
5126}
5127
5128#[cfg(test)]
5129mod tests {
5130 use super::*;
5131 use FieldType;
5132 use rstest::rstest;
5133
5134 #[test]
5135 fn test_create_table_to_statement() {
5136 let op = Operation::CreateTable {
5137 name: "users".to_string(),
5138 columns: vec![
5139 ColumnDefinition {
5140 name: "id".to_string(),
5141 type_definition: FieldType::Integer,
5142 not_null: false,
5143 unique: false,
5144 primary_key: true,
5145 auto_increment: true,
5146 default: None,
5147 },
5148 ColumnDefinition {
5149 name: "name".to_string(),
5150 type_definition: FieldType::VarChar(100),
5151 not_null: true,
5152 unique: false,
5153 primary_key: false,
5154 auto_increment: false,
5155 default: None,
5156 },
5157 ],
5158 constraints: vec![],
5159 without_rowid: None,
5160 partition: None,
5161 interleave_in_parent: None,
5162 };
5163
5164 let stmt = op.to_statement();
5165 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5166 assert!(
5167 sql.contains("CREATE TABLE"),
5168 "SQL should contain CREATE TABLE keyword, got: {}",
5169 sql
5170 );
5171 assert!(
5172 sql.contains("users"),
5173 "SQL should reference 'users' table, got: {}",
5174 sql
5175 );
5176 assert!(
5177 sql.contains("id") && sql.contains("name"),
5178 "SQL should contain both 'id' and 'name' columns, got: {}",
5179 sql
5180 );
5181 }
5182
5183 #[test]
5184 fn test_drop_table_to_statement() {
5185 let op = Operation::DropTable {
5186 name: "users".to_string(),
5187 };
5188
5189 let stmt = op.to_statement();
5190 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5191 assert!(
5192 sql.contains("DROP TABLE"),
5193 "SQL should contain DROP TABLE keyword, got: {}",
5194 sql
5195 );
5196 assert!(
5197 sql.contains("users"),
5198 "SQL should reference 'users' table, got: {}",
5199 sql
5200 );
5201 assert!(
5202 sql.contains("CASCADE"),
5203 "SQL should include CASCADE option, got: {}",
5204 sql
5205 );
5206 }
5207
5208 #[test]
5209 fn test_add_column_to_statement() {
5210 let op = Operation::AddColumn {
5211 table: "users".to_string(),
5212 column: ColumnDefinition {
5213 name: "email".to_string(),
5214 type_definition: FieldType::VarChar(255),
5215 not_null: true,
5216 unique: false,
5217 primary_key: false,
5218 auto_increment: false,
5219 default: Some("''".to_string()),
5220 },
5221 mysql_options: None,
5222 };
5223
5224 let stmt = op.to_statement();
5225 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5226 assert!(
5227 sql.contains("ALTER TABLE"),
5228 "SQL should contain ALTER TABLE keyword, got: {}",
5229 sql
5230 );
5231 assert!(
5232 sql.contains("users"),
5233 "SQL should reference 'users' table, got: {}",
5234 sql
5235 );
5236 assert!(
5237 sql.contains("ADD COLUMN"),
5238 "SQL should contain ADD COLUMN clause, got: {}",
5239 sql
5240 );
5241 assert!(
5242 sql.contains("email"),
5243 "SQL should reference 'email' column, got: {}",
5244 sql
5245 );
5246 }
5247
5248 #[test]
5249 fn test_drop_column_to_statement() {
5250 let op = Operation::DropColumn {
5251 table: "users".to_string(),
5252 column: "email".to_string(),
5253 };
5254
5255 let stmt = op.to_statement();
5256 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5257 assert!(
5258 sql.contains("ALTER TABLE"),
5259 "SQL should contain ALTER TABLE keyword, got: {}",
5260 sql
5261 );
5262 assert!(
5263 sql.contains("users"),
5264 "SQL should reference 'users' table, got: {}",
5265 sql
5266 );
5267 assert!(
5268 sql.contains("DROP COLUMN"),
5269 "SQL should contain DROP COLUMN clause, got: {}",
5270 sql
5271 );
5272 assert!(
5273 sql.contains("email"),
5274 "SQL should reference 'email' column, got: {}",
5275 sql
5276 );
5277 }
5278
5279 #[test]
5280 fn test_alter_column_to_statement() {
5281 let op = Operation::AlterColumn {
5282 table: "users".to_string(),
5283 column: "age".to_string(),
5284 old_definition: None,
5285 new_definition: ColumnDefinition {
5286 name: "age".to_string(),
5287 type_definition: FieldType::BigInteger,
5288 not_null: true,
5289 unique: false,
5290 primary_key: false,
5291 auto_increment: false,
5292 default: None,
5293 },
5294 mysql_options: None,
5295 };
5296
5297 let stmt = op.to_statement();
5298 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5299 assert!(
5300 sql.contains("ALTER TABLE"),
5301 "SQL should contain ALTER TABLE keyword, got: {}",
5302 sql
5303 );
5304 assert!(
5305 sql.contains("users"),
5306 "SQL should reference 'users' table, got: {}",
5307 sql
5308 );
5309 assert!(
5310 sql.contains("age"),
5311 "SQL should reference 'age' column, got: {}",
5312 sql
5313 );
5314 }
5315
5316 #[test]
5317 fn test_rename_table_to_statement() {
5318 let op = Operation::RenameTable {
5319 old_name: "users".to_string(),
5320 new_name: "accounts".to_string(),
5321 };
5322
5323 let stmt = op.to_statement();
5324 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5325 assert!(
5326 sql.contains("users"),
5327 "SQL should reference old table name 'users', got: {}",
5328 sql
5329 );
5330 assert!(
5331 sql.contains("accounts"),
5332 "SQL should reference new table name 'accounts', got: {}",
5333 sql
5334 );
5335 }
5336
5337 #[test]
5338 fn test_rename_column_to_statement() {
5339 let op = Operation::RenameColumn {
5340 table: "users".to_string(),
5341 old_name: "name".to_string(),
5342 new_name: "full_name".to_string(),
5343 };
5344
5345 let stmt = op.to_statement();
5346 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5347 assert!(
5348 sql.contains("ALTER TABLE"),
5349 "SQL should contain ALTER TABLE keyword, got: {}",
5350 sql
5351 );
5352 assert!(
5353 sql.contains("users"),
5354 "SQL should reference 'users' table, got: {}",
5355 sql
5356 );
5357 assert!(
5358 sql.contains("RENAME COLUMN"),
5359 "SQL should contain RENAME COLUMN clause, got: {}",
5360 sql
5361 );
5362 assert!(
5363 sql.contains("name"),
5364 "SQL should reference old column name 'name', got: {}",
5365 sql
5366 );
5367 assert!(
5368 sql.contains("full_name"),
5369 "SQL should reference new column name 'full_name', got: {}",
5370 sql
5371 );
5372 }
5373
5374 #[test]
5375 fn test_add_constraint_to_statement() {
5376 let op = Operation::AddConstraint {
5377 table: "users".to_string(),
5378 constraint_sql: "CONSTRAINT age_check CHECK (age >= 0)".to_string(),
5379 };
5380
5381 let stmt = op.to_statement();
5382 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5383 assert!(
5384 sql.contains("ALTER TABLE"),
5385 "SQL should contain ALTER TABLE keyword, got: {}",
5386 sql
5387 );
5388 assert!(
5389 sql.contains("users"),
5390 "SQL should reference 'users' table, got: {}",
5391 sql
5392 );
5393 assert!(
5394 sql.contains("ADD"),
5395 "SQL should contain ADD keyword, got: {}",
5396 sql
5397 );
5398 assert!(
5399 sql.contains("age_check"),
5400 "SQL should contain constraint name 'age_check', got: {}",
5401 sql
5402 );
5403 }
5404
5405 #[test]
5406 fn test_add_unique_constraint_to_sql_uses_mysql_identifier_quotes() {
5407 let op = Operation::AddConstraint {
5409 table: "users".to_string(),
5410 constraint_sql: "CONSTRAINT users_group_uniq UNIQUE (\"group\")".to_string(),
5411 };
5412
5413 let mysql_sql = op.to_sql(&SqlDialect::Mysql);
5415 let postgres_sql = op.to_sql(&SqlDialect::Postgres);
5416
5417 assert_eq!(
5419 mysql_sql,
5420 "ALTER TABLE users ADD CONSTRAINT users_group_uniq UNIQUE (`group`);"
5421 );
5422 assert_eq!(
5423 postgres_sql,
5424 "ALTER TABLE users ADD CONSTRAINT users_group_uniq UNIQUE (\"group\");"
5425 );
5426 }
5427
5428 #[test]
5429 fn test_drop_constraint_to_statement() {
5430 let op = Operation::DropConstraint {
5431 table: "users".to_string(),
5432 constraint_name: "age_check".to_string(),
5433 };
5434
5435 let stmt = op.to_statement();
5436 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5437 assert!(
5438 sql.contains("ALTER TABLE"),
5439 "SQL should contain ALTER TABLE keyword, got: {}",
5440 sql
5441 );
5442 assert!(
5443 sql.contains("users"),
5444 "SQL should reference 'users' table, got: {}",
5445 sql
5446 );
5447 assert!(
5448 sql.contains("DROP CONSTRAINT"),
5449 "SQL should contain DROP CONSTRAINT clause, got: {}",
5450 sql
5451 );
5452 assert!(
5453 sql.contains("age_check"),
5454 "SQL should reference constraint 'age_check', got: {}",
5455 sql
5456 );
5457 }
5458
5459 #[test]
5460 fn test_create_index_to_statement() {
5461 let op = Operation::CreateIndex {
5462 table: "users".to_string(),
5463 columns: vec!["email".to_string()],
5464 unique: false,
5465 index_type: None,
5466 where_clause: None,
5467 concurrently: false,
5468 expressions: None,
5469 mysql_options: None,
5470 operator_class: None,
5471 };
5472
5473 let stmt = op.to_statement();
5474 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5475 assert!(
5476 sql.contains("CREATE INDEX"),
5477 "SQL should contain CREATE INDEX keywords, got: {}",
5478 sql
5479 );
5480 assert!(
5481 sql.contains("users"),
5482 "SQL should reference 'users' table, got: {}",
5483 sql
5484 );
5485 assert!(
5486 sql.contains("email"),
5487 "SQL should reference 'email' column, got: {}",
5488 sql
5489 );
5490 }
5491
5492 #[test]
5493 fn test_create_unique_index_to_statement() {
5494 let op = Operation::CreateIndex {
5495 table: "users".to_string(),
5496 columns: vec!["email".to_string()],
5497 unique: true,
5498 index_type: None,
5499 where_clause: None,
5500 concurrently: false,
5501 expressions: None,
5502 mysql_options: None,
5503 operator_class: None,
5504 };
5505
5506 let stmt = op.to_statement();
5507 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5508 assert!(
5509 sql.contains("CREATE UNIQUE INDEX"),
5510 "SQL should contain CREATE UNIQUE INDEX keywords, got: {}",
5511 sql
5512 );
5513 assert!(
5514 sql.contains("users"),
5515 "SQL should reference 'users' table, got: {}",
5516 sql
5517 );
5518 assert!(
5519 sql.contains("email"),
5520 "SQL should reference 'email' column, got: {}",
5521 sql
5522 );
5523 }
5524
5525 #[test]
5526 fn test_drop_index_to_statement() {
5527 let op = Operation::DropIndex {
5528 table: "users".to_string(),
5529 columns: vec!["email".to_string()],
5530 };
5531
5532 let stmt = op.to_statement();
5533 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5534 assert!(
5535 sql.contains("DROP INDEX"),
5536 "SQL should contain DROP INDEX keywords, got: {}",
5537 sql
5538 );
5539 assert!(
5540 sql.contains("idx_users_email"),
5541 "SQL should contain generated index name 'idx_users_email', got: {}",
5542 sql
5543 );
5544 }
5545
5546 #[test]
5547 fn test_run_sql_to_statement() {
5548 let op = Operation::RunSQL {
5549 sql: "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"".to_string(),
5550 reverse_sql: Some("DROP EXTENSION \"uuid-ossp\"".to_string()),
5551 };
5552
5553 let stmt = op.to_statement();
5554 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5555 assert!(
5556 sql.contains("CREATE EXTENSION"),
5557 "SQL should contain CREATE EXTENSION keywords, got: {}",
5558 sql
5559 );
5560 assert!(
5561 sql.contains("uuid-ossp"),
5562 "SQL should reference 'uuid-ossp' extension, got: {}",
5563 sql
5564 );
5565 }
5566
5567 #[test]
5568 fn test_alter_table_comment_to_statement() {
5569 let op = Operation::AlterTableComment {
5570 table: "users".to_string(),
5571 comment: Some("User accounts table".to_string()),
5572 };
5573
5574 let stmt = op.to_statement();
5575 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5576 assert!(
5577 sql.contains("COMMENT ON TABLE"),
5578 "SQL should contain COMMENT ON TABLE keywords, got: {}",
5579 sql
5580 );
5581 assert!(
5582 sql.contains("users"),
5583 "SQL should reference 'users' table, got: {}",
5584 sql
5585 );
5586 assert!(
5587 sql.contains("User accounts table"),
5588 "SQL should include comment text 'User accounts table', got: {}",
5589 sql
5590 );
5591 }
5592
5593 #[test]
5594 fn test_alter_table_comment_null_to_statement() {
5595 let op = Operation::AlterTableComment {
5596 table: "users".to_string(),
5597 comment: None,
5598 };
5599
5600 let stmt = op.to_statement();
5601 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5602 assert!(
5603 sql.contains("COMMENT ON TABLE"),
5604 "SQL should contain COMMENT ON TABLE keywords, got: {}",
5605 sql
5606 );
5607 assert!(
5608 sql.contains("users"),
5609 "SQL should reference 'users' table, got: {}",
5610 sql
5611 );
5612 assert!(
5613 sql.contains("NULL"),
5614 "SQL should include NULL for null comment, got: {}",
5615 sql
5616 );
5617 }
5618
5619 #[test]
5620 fn test_alter_unique_together_to_statement() {
5621 let op = Operation::AlterUniqueTogether {
5622 table: "users".to_string(),
5623 unique_together: vec![vec!["email".to_string(), "username".to_string()]],
5624 };
5625
5626 let stmt = op.to_statement();
5627 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5628 assert!(
5629 sql.contains("ALTER TABLE"),
5630 "SQL should contain ALTER TABLE keyword, got: {}",
5631 sql
5632 );
5633 assert!(
5634 sql.contains("users"),
5635 "SQL should reference 'users' table, got: {}",
5636 sql
5637 );
5638 assert!(
5639 sql.contains("ADD CONSTRAINT"),
5640 "SQL should contain ADD CONSTRAINT clause, got: {}",
5641 sql
5642 );
5643 assert!(
5644 sql.contains("UNIQUE"),
5645 "SQL should contain UNIQUE keyword, got: {}",
5646 sql
5647 );
5648 assert!(
5649 sql.contains("email") && sql.contains("username"),
5650 "SQL should reference both 'email' and 'username' columns, got: {}",
5651 sql
5652 );
5653 }
5654
5655 #[test]
5656 fn test_alter_unique_together_empty() {
5657 let op = Operation::AlterUniqueTogether {
5658 table: "users".to_string(),
5659 unique_together: vec![],
5660 };
5661
5662 let stmt = op.to_statement();
5663 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5664 assert_eq!(
5665 sql, "",
5666 "SQL should be empty for empty unique_together constraint"
5667 );
5668 }
5669
5670 #[test]
5671 fn test_alter_model_options_to_statement() {
5672 let mut options = std::collections::HashMap::new();
5673 options.insert("db_table".to_string(), "custom_users".to_string());
5674
5675 let op = Operation::AlterModelOptions {
5676 table: "users".to_string(),
5677 options,
5678 };
5679
5680 let stmt = op.to_statement();
5681 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5682 assert_eq!(sql, "", "SQL should be empty for model options operation");
5683 }
5684
5685 #[test]
5686 fn test_create_inherited_table_to_statement() {
5687 let op = Operation::CreateInheritedTable {
5688 name: "admin_users".to_string(),
5689 columns: vec![ColumnDefinition {
5690 name: "admin_level".to_string(),
5691 type_definition: FieldType::Integer,
5692 not_null: true,
5693 unique: false,
5694 primary_key: false,
5695 auto_increment: false,
5696 default: Some("1".to_string()),
5697 }],
5698 base_table: "users".to_string(),
5699 join_column: "user_id".to_string(),
5700 };
5701
5702 let stmt = op.to_statement();
5703 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5704 assert!(
5705 sql.contains("CREATE TABLE"),
5706 "SQL should contain CREATE TABLE keywords, got: {}",
5707 sql
5708 );
5709 assert!(
5710 sql.contains("admin_users"),
5711 "SQL should reference 'admin_users' table, got: {}",
5712 sql
5713 );
5714 assert!(
5715 sql.contains("user_id"),
5716 "SQL should include join column 'user_id', got: {}",
5717 sql
5718 );
5719 }
5720
5721 #[test]
5722 fn test_add_discriminator_column_to_statement() {
5723 let op = Operation::AddDiscriminatorColumn {
5724 table: "users".to_string(),
5725 column_name: "user_type".to_string(),
5726 default_value: "regular".to_string(),
5727 };
5728
5729 let stmt = op.to_statement();
5730 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5731 assert!(
5732 sql.contains("ALTER TABLE"),
5733 "SQL should contain ALTER TABLE keyword, got: {}",
5734 sql
5735 );
5736 assert!(
5737 sql.contains("users"),
5738 "SQL should reference 'users' table, got: {}",
5739 sql
5740 );
5741 assert!(
5742 sql.contains("ADD COLUMN"),
5743 "SQL should contain ADD COLUMN clause, got: {}",
5744 sql
5745 );
5746 assert!(
5747 sql.contains("user_type"),
5748 "SQL should reference 'user_type' column, got: {}",
5749 sql
5750 );
5751 }
5752
5753 #[test]
5754 fn test_state_forwards_create_table() {
5755 let mut state = ProjectState::new();
5756 let op = Operation::CreateTable {
5757 name: "users".to_string(),
5758 columns: vec![
5759 ColumnDefinition {
5760 name: "id".to_string(),
5761 type_definition: FieldType::Integer,
5762 not_null: false,
5763 unique: false,
5764 primary_key: true,
5765 auto_increment: true,
5766 default: None,
5767 },
5768 ColumnDefinition {
5769 name: "name".to_string(),
5770 type_definition: FieldType::VarChar(100),
5771 not_null: true,
5772 unique: false,
5773 primary_key: false,
5774 auto_increment: false,
5775 default: None,
5776 },
5777 ],
5778 constraints: vec![],
5779 without_rowid: None,
5780 partition: None,
5781 interleave_in_parent: None,
5782 };
5783
5784 op.state_forwards("myapp", &mut state);
5785 let model = state.get_model("myapp", "users");
5786 assert!(model.is_some(), "Model 'users' should exist in state");
5787 let model = model.unwrap();
5788 assert_eq!(
5789 model.fields.len(),
5790 2,
5791 "Model should have exactly 2 fields, got: {}",
5792 model.fields.len()
5793 );
5794 assert!(
5795 model.fields.contains_key("id"),
5796 "Model should contain 'id' field"
5797 );
5798 assert!(
5799 model.fields.contains_key("name"),
5800 "Model should contain 'name' field"
5801 );
5802 }
5803
5804 #[test]
5805 fn test_state_forwards_drop_table() {
5806 let mut state = ProjectState::new();
5807 let mut model = ModelState::new("myapp", "users");
5808 model.add_field(FieldState::new("id".to_string(), FieldType::Integer, false));
5809 state.add_model(model);
5810
5811 let op = Operation::DropTable {
5812 name: "users".to_string(),
5813 };
5814
5815 op.state_forwards("myapp", &mut state);
5816 assert!(
5817 state.get_model("myapp", "users").is_none(),
5818 "Model 'users' should be removed from state after drop"
5819 );
5820 }
5821
5822 #[test]
5823 fn test_state_forwards_add_column() {
5824 let mut state = ProjectState::new();
5825 let mut model = ModelState::new("myapp", "users");
5826 model.add_field(FieldState::new("id".to_string(), FieldType::Integer, false));
5827 state.add_model(model);
5828
5829 let op = Operation::AddColumn {
5830 table: "users".to_string(),
5831 column: ColumnDefinition {
5832 name: "email".to_string(),
5833 type_definition: FieldType::VarChar(255),
5834 not_null: true,
5835 unique: false,
5836 primary_key: false,
5837 auto_increment: false,
5838 default: None,
5839 },
5840 mysql_options: None,
5841 };
5842
5843 op.state_forwards("myapp", &mut state);
5844 let model = state.get_model("myapp", "users").unwrap();
5845 assert_eq!(
5846 model.fields.len(),
5847 2,
5848 "Model should have 2 fields after adding 'email', got: {}",
5849 model.fields.len()
5850 );
5851 assert!(
5852 model.fields.contains_key("email"),
5853 "Model should contain newly added 'email' field"
5854 );
5855 }
5856
5857 #[test]
5858 fn test_state_forwards_drop_column() {
5859 let mut state = ProjectState::new();
5860 let mut model = ModelState::new("myapp", "users");
5861 model.add_field(FieldState::new("id".to_string(), FieldType::Integer, false));
5862 model.add_field(FieldState::new(
5863 "email".to_string(),
5864 FieldType::VarChar(255),
5865 false,
5866 ));
5867 state.add_model(model);
5868
5869 let op = Operation::DropColumn {
5870 table: "users".to_string(),
5871 column: "email".to_string(),
5872 };
5873
5874 op.state_forwards("myapp", &mut state);
5875 let model = state.get_model("myapp", "users").unwrap();
5876 assert_eq!(
5877 model.fields.len(),
5878 1,
5879 "Model should have 1 field after dropping 'email', got: {}",
5880 model.fields.len()
5881 );
5882 assert!(
5883 !model.fields.contains_key("email"),
5884 "Model should not contain dropped 'email' field"
5885 );
5886 }
5887
5888 #[test]
5889 fn test_state_forwards_rename_table() {
5890 let mut state = ProjectState::new();
5891 let mut model = ModelState::new("myapp", "users");
5892 model.add_field(FieldState::new("id".to_string(), FieldType::Integer, false));
5893 state.add_model(model);
5894
5895 let op = Operation::RenameTable {
5896 old_name: "users".to_string(),
5897 new_name: "accounts".to_string(),
5898 };
5899
5900 op.state_forwards("myapp", &mut state);
5901 assert!(
5902 state.get_model("myapp", "users").is_none(),
5903 "Old model name 'users' should not exist after rename"
5904 );
5905 assert!(
5906 state.get_model("myapp", "accounts").is_some(),
5907 "New model name 'accounts' should exist after rename"
5908 );
5909 }
5910
5911 #[test]
5912 fn test_state_forwards_rename_column() {
5913 let mut state = ProjectState::new();
5914 let mut model = ModelState::new("myapp", "users");
5915 model.add_field(FieldState::new(
5916 "name".to_string(),
5917 FieldType::VarChar(255),
5918 false,
5919 ));
5920 state.add_model(model);
5921
5922 let op = Operation::RenameColumn {
5923 table: "users".to_string(),
5924 old_name: "name".to_string(),
5925 new_name: "full_name".to_string(),
5926 };
5927
5928 op.state_forwards("myapp", &mut state);
5929 let model = state.get_model("myapp", "users").unwrap();
5930 assert!(
5931 !model.fields.contains_key("name"),
5932 "Old field name 'name' should not exist after rename"
5933 );
5934 assert!(
5935 model.fields.contains_key("full_name"),
5936 "New field name 'full_name' should exist after rename"
5937 );
5938 }
5939
5940 #[test]
5941 fn test_to_reverse_sql_create_table() {
5942 let op = Operation::CreateTable {
5943 name: "users".to_string(),
5944 columns: vec![],
5945 constraints: vec![],
5946 without_rowid: None,
5947 partition: None,
5948 interleave_in_parent: None,
5949 };
5950
5951 let state = ProjectState::default();
5952 let reverse = op.to_reverse_sql(&SqlDialect::Postgres, &state);
5953 assert!(
5954 reverse.is_ok() && reverse.as_ref().ok().unwrap().is_some(),
5955 "CreateTable should have reverse SQL operation"
5956 );
5957 let sql = reverse.unwrap().unwrap().join("\n");
5958 assert!(
5959 sql.contains("DROP TABLE"),
5960 "Reverse SQL should contain DROP TABLE, got: {}",
5961 sql
5962 );
5963 assert!(
5964 sql.contains("users"),
5965 "Reverse SQL should reference 'users' table, got: {}",
5966 sql
5967 );
5968 }
5969
5970 #[test]
5971 fn test_to_reverse_sql_drop_table() {
5972 let op = Operation::DropTable {
5973 name: "users".to_string(),
5974 };
5975
5976 let state = ProjectState::default();
5977 let reverse = op.to_reverse_sql(&SqlDialect::Postgres, &state);
5978 assert!(
5979 reverse.is_ok() && reverse.as_ref().ok().unwrap().is_none(),
5980 "DropTable should not have reverse SQL (cannot recreate table structure)"
5981 );
5982 }
5983
5984 #[test]
5985 fn test_to_reverse_sql_add_column() {
5986 let op = Operation::AddColumn {
5987 table: "users".to_string(),
5988 column: ColumnDefinition {
5989 name: "email".to_string(),
5990 type_definition: FieldType::VarChar(255),
5991 not_null: false,
5992 unique: false,
5993 primary_key: false,
5994 auto_increment: false,
5995 default: None,
5996 },
5997 mysql_options: None,
5998 };
5999
6000 let state = ProjectState::default();
6001 let reverse = op.to_reverse_sql(&SqlDialect::Postgres, &state);
6002 assert!(
6003 reverse.is_ok() && reverse.as_ref().ok().unwrap().is_some(),
6004 "AddColumn should have reverse SQL operation"
6005 );
6006 let sql = reverse.unwrap().unwrap().join("\n");
6007 assert!(
6008 sql.contains("DROP COLUMN"),
6009 "Reverse SQL should contain DROP COLUMN, got: {}",
6010 sql
6011 );
6012 assert!(
6013 sql.contains("email"),
6014 "Reverse SQL should reference 'email' column, got: {}",
6015 sql
6016 );
6017 }
6018
6019 fn alter_column_with_old_def() -> Operation {
6024 Operation::AlterColumn {
6025 table: "products".to_string(),
6026 column: "name".to_string(),
6027 old_definition: Some(ColumnDefinition {
6028 name: "name".to_string(),
6029 type_definition: FieldType::VarChar(50),
6030 not_null: false,
6031 unique: false,
6032 primary_key: false,
6033 auto_increment: false,
6034 default: None,
6035 }),
6036 new_definition: ColumnDefinition {
6037 name: "name".to_string(),
6038 type_definition: FieldType::Text,
6039 not_null: false,
6040 unique: false,
6041 primary_key: false,
6042 auto_increment: false,
6043 default: None,
6044 },
6045 mysql_options: None,
6046 }
6047 }
6048
6049 #[test]
6052 fn test_to_reverse_sql_alter_column_postgres() {
6053 let op = alter_column_with_old_def();
6055 let state = ProjectState::default();
6056
6057 let stmts = op
6061 .to_reverse_sql(&SqlDialect::Postgres, &state)
6062 .expect("reverse SQL should succeed")
6063 .expect("reverse SQL should be present");
6064 let sql = stmts.join("\n");
6065
6066 assert!(
6068 sql.contains("ALTER COLUMN") && sql.contains("TYPE"),
6069 "Postgres reverse SQL should use ALTER COLUMN ... TYPE syntax, got: {}",
6070 sql
6071 );
6072 assert!(
6073 sql.contains("VARCHAR(50)"),
6074 "Postgres reverse SQL should restore VARCHAR(50), got: {}",
6075 sql
6076 );
6077 assert_eq!(
6082 stmts.len(),
6083 2,
6084 "Postgres AlterColumn reverse SQL must emit two statements \
6085 (type + nullability), got: {:?}",
6086 stmts
6087 );
6088 assert!(
6089 stmts[1].contains("DROP NOT NULL"),
6090 "Postgres second statement must restore DROP NOT NULL (was_nullable), got: {}",
6091 stmts[1]
6092 );
6093 }
6094
6095 #[test]
6099 fn test_to_reverse_sql_alter_column_mysql() {
6100 let op = alter_column_with_old_def();
6102 let state = ProjectState::default();
6103
6104 let stmts = op
6107 .to_reverse_sql(&SqlDialect::Mysql, &state)
6108 .expect("reverse SQL should succeed")
6109 .expect("reverse SQL should be present");
6110 assert_eq!(
6111 stmts.len(),
6112 1,
6113 "MySQL AlterColumn reverse SQL should remain a single statement, got: {:?}",
6114 stmts
6115 );
6116 let sql = stmts.join("\n");
6117
6118 assert!(
6120 sql.contains("MODIFY COLUMN"),
6121 "MySQL reverse SQL should use MODIFY COLUMN syntax, got: {}",
6122 sql
6123 );
6124 assert!(
6125 !sql.contains("ALTER COLUMN"),
6126 "MySQL reverse SQL must not emit Postgres ALTER COLUMN syntax, got: {}",
6127 sql
6128 );
6129 assert!(
6130 !sql.contains(" TYPE "),
6131 "MySQL reverse SQL must not contain Postgres ' TYPE ' token, got: {}",
6132 sql
6133 );
6134 assert!(
6135 sql.contains("VARCHAR(50)"),
6136 "MySQL reverse SQL should restore VARCHAR(50), got: {}",
6137 sql
6138 );
6139 }
6140
6141 #[rstest]
6142 #[case::postgres(SqlDialect::Postgres)]
6143 #[case::cockroachdb(SqlDialect::Cockroachdb)]
6144 fn test_to_sql_alter_column_sets_default_for_postgres_family(#[case] dialect: SqlDialect) {
6145 let op = Operation::AlterColumn {
6147 table: "users".to_string(),
6148 column: "is_active".to_string(),
6149 old_definition: Some(ColumnDefinition {
6150 name: "is_active".to_string(),
6151 type_definition: FieldType::Boolean,
6152 not_null: true,
6153 unique: false,
6154 primary_key: false,
6155 auto_increment: false,
6156 default: None,
6157 }),
6158 new_definition: ColumnDefinition {
6159 name: "is_active".to_string(),
6160 type_definition: FieldType::Boolean,
6161 not_null: true,
6162 unique: false,
6163 primary_key: false,
6164 auto_increment: false,
6165 default: Some("true".to_string()),
6166 },
6167 mysql_options: None,
6168 };
6169
6170 let sql = op.to_sql(&dialect);
6172
6173 assert!(
6175 sql.contains("ALTER COLUMN is_active SET DEFAULT true"),
6176 "AlterColumn must apply new database defaults, got: {}",
6177 sql
6178 );
6179 }
6180
6181 #[rstest]
6182 #[case::postgres(SqlDialect::Postgres)]
6183 #[case::cockroachdb(SqlDialect::Cockroachdb)]
6184 fn test_to_sql_alter_column_drops_default_for_postgres_family(#[case] dialect: SqlDialect) {
6185 let op = Operation::AlterColumn {
6187 table: "users".to_string(),
6188 column: "is_active".to_string(),
6189 old_definition: Some(ColumnDefinition {
6190 name: "is_active".to_string(),
6191 type_definition: FieldType::Boolean,
6192 not_null: true,
6193 unique: false,
6194 primary_key: false,
6195 auto_increment: false,
6196 default: Some("true".to_string()),
6197 }),
6198 new_definition: ColumnDefinition {
6199 name: "is_active".to_string(),
6200 type_definition: FieldType::Boolean,
6201 not_null: true,
6202 unique: false,
6203 primary_key: false,
6204 auto_increment: false,
6205 default: None,
6206 },
6207 mysql_options: None,
6208 };
6209
6210 let sql = op.to_sql(&dialect);
6212
6213 assert!(
6215 sql.contains("ALTER COLUMN is_active DROP DEFAULT"),
6216 "AlterColumn must remove dropped database defaults, got: {}",
6217 sql
6218 );
6219 }
6220
6221 #[test]
6222 fn test_to_sql_alter_column_mysql_preserves_full_column_definition() {
6223 let op = Operation::AlterColumn {
6225 table: "users".to_string(),
6226 column: "is_active".to_string(),
6227 old_definition: None,
6228 new_definition: ColumnDefinition {
6229 name: "is_active".to_string(),
6230 type_definition: FieldType::Boolean,
6231 not_null: true,
6232 unique: false,
6233 primary_key: false,
6234 auto_increment: false,
6235 default: Some("true".to_string()),
6236 },
6237 mysql_options: None,
6238 };
6239
6240 let sql = op.to_sql(&SqlDialect::Mysql);
6242
6243 assert!(
6245 sql.contains("MODIFY COLUMN is_active TINYINT(1) NOT NULL DEFAULT true"),
6246 "MySQL AlterColumn must include type, nullability, and default, got: {}",
6247 sql
6248 );
6249 }
6250
6251 #[test]
6278 fn test_to_reverse_sql_alter_column_cockroachdb() {
6279 let op = alter_column_with_old_def();
6281 let state = ProjectState::default();
6282
6283 let stmts = op
6285 .to_reverse_sql(&SqlDialect::Cockroachdb, &state)
6286 .expect("reverse SQL should succeed")
6287 .expect("reverse SQL should be present");
6288
6289 assert_eq!(
6297 stmts,
6298 vec![
6299 "ALTER TABLE products ALTER COLUMN name TYPE VARCHAR(50);".to_string(),
6300 "ALTER TABLE products ALTER COLUMN name DROP NOT NULL;".to_string(),
6301 ],
6302 "CockroachDB reverse SQL must emit exactly [type_stmt, nullability_stmt], \
6303 got: {:?}",
6304 stmts
6305 );
6306
6307 for stmt in &stmts {
6314 let trimmed = stmt.trim().trim_end_matches(';').trim();
6315 assert!(
6316 !trimmed.contains(';'),
6317 "each emitted statement must be a single SQL statement, got: {}",
6318 stmt
6319 );
6320 assert!(
6321 !stmt.contains(", ALTER COLUMN"),
6322 "emitted statements must not use the Postgres comma-combined form \
6323 (CockroachDB rejects it), got: {}",
6324 stmt
6325 );
6326 }
6327 }
6328
6329 #[test]
6335 fn test_to_reverse_sql_alter_column_sqlite() {
6336 let op = alter_column_with_old_def();
6338 let state = ProjectState::default();
6339
6340 let stmts = op
6344 .to_reverse_sql(&SqlDialect::Sqlite, &state)
6345 .expect("reverse SQL should succeed")
6346 .expect("reverse SQL should be present");
6347 assert_eq!(
6348 stmts.len(),
6349 1,
6350 "SQLite AlterColumn reverse SQL should remain a single comment, got: {:?}",
6351 stmts
6352 );
6353 let sql = &stmts[0];
6354
6355 assert!(
6357 sql.trim_start().starts_with("--"),
6358 "SQLite reverse SQL should be a SQL comment (recreation handled by executor), got: {}",
6359 sql
6360 );
6361 let body = sql.trim_start_matches("--").trim_start();
6364 assert!(
6365 !body.to_uppercase().contains("ALTER TABLE"),
6366 "SQLite reverse SQL body must not emit executable ALTER TABLE statement, got: {}",
6367 sql
6368 );
6369 }
6370
6371 #[test]
6376 fn test_to_reverse_operation_alter_column_uses_old_definition() {
6377 let op = alter_column_with_old_def();
6379 let state = ProjectState::default();
6380
6381 let reverse = op
6383 .to_reverse_operation(&state)
6384 .expect("reverse operation should succeed")
6385 .expect("reverse operation should be present (old_definition is supplied)");
6386
6387 match reverse {
6389 Operation::AlterColumn { new_definition, .. } => {
6390 assert!(
6391 matches!(new_definition.type_definition, FieldType::VarChar(50)),
6392 "reverse AlterColumn should restore VARCHAR(50), got: {:?}",
6393 new_definition.type_definition
6394 );
6395 }
6396 other => panic!("reverse operation should be AlterColumn, got: {:?}", other),
6397 }
6398 }
6399
6400 #[test]
6401 fn test_to_reverse_sql_run_sql_with_reverse() {
6402 let op = Operation::RunSQL {
6403 sql: "CREATE INDEX idx_name ON users(name)".to_string(),
6404 reverse_sql: Some("DROP INDEX idx_name".to_string()),
6405 };
6406
6407 let state = ProjectState::default();
6408 let reverse = op.to_reverse_sql(&SqlDialect::Postgres, &state);
6409 assert!(
6410 reverse.is_ok() && reverse.as_ref().ok().unwrap().is_some(),
6411 "RunSQL with reverse_sql should have reverse SQL"
6412 );
6413 let sql = reverse.unwrap().unwrap().join("\n");
6414 assert!(
6415 sql.contains("DROP INDEX"),
6416 "Reverse SQL should contain provided reverse_sql, got: {}",
6417 sql
6418 );
6419 }
6420
6421 #[test]
6422 fn test_to_reverse_sql_run_sql_without_reverse() {
6423 let op = Operation::RunSQL {
6424 sql: "CREATE INDEX idx_name ON users(name)".to_string(),
6425 reverse_sql: None,
6426 };
6427
6428 let state = ProjectState::default();
6429 let reverse = op.to_reverse_sql(&SqlDialect::Postgres, &state);
6430 assert!(
6431 reverse.is_ok() && reverse.as_ref().ok().unwrap().is_none(),
6432 "RunSQL without reverse_sql should not have reverse SQL"
6433 );
6434 }
6435
6436 #[test]
6437 fn test_column_definition_new() {
6438 let col = ColumnDefinition::new("id", FieldType::Integer);
6439 assert_eq!(col.name, "id", "Column name should be 'id'");
6440 assert_eq!(
6441 col.type_definition,
6442 FieldType::Integer,
6443 "Column type should be Integer"
6444 );
6445 assert!(!col.not_null, "not_null should default to false");
6446 assert!(!col.unique, "unique should default to false");
6447 assert!(!col.primary_key, "primary_key should default to false");
6448 assert!(
6449 !col.auto_increment,
6450 "auto_increment should default to false"
6451 );
6452 assert!(col.default.is_none(), "default should be None");
6453 }
6454
6455 #[rstest]
6465 fn from_field_state_non_optional_bool_with_true_default() {
6466 let mut field_state = FieldState::new("is_active", FieldType::Boolean, false);
6472 field_state
6473 .params
6474 .insert("default".to_string(), "true".to_string());
6475
6476 let col = ColumnDefinition::from_field_state("is_active", &field_state);
6478
6479 assert_eq!(col.name, "is_active", "Column name should round-trip");
6481 assert_eq!(
6482 col.type_definition,
6483 FieldType::Boolean,
6484 "Boolean field type should round-trip"
6485 );
6486 assert!(
6487 col.not_null,
6488 "Non-Optional bool must emit NOT NULL (regression #4573)"
6489 );
6490 assert_eq!(
6491 col.default,
6492 Some("true".to_string()),
6493 "`#[field(default = true)]` must propagate as Some(\"true\")"
6494 );
6495 assert!(!col.primary_key, "Non-PK field must not be primary_key");
6496 }
6497
6498 #[rstest]
6499 fn from_field_state_non_optional_bool_with_false_default() {
6500 let mut field_state = FieldState::new("is_superuser", FieldType::Boolean, false);
6505 field_state
6506 .params
6507 .insert("default".to_string(), "false".to_string());
6508
6509 let col = ColumnDefinition::from_field_state("is_superuser", &field_state);
6511
6512 assert!(
6514 col.not_null,
6515 "Non-Optional bool with default=false must emit NOT NULL"
6516 );
6517 assert_eq!(
6518 col.default,
6519 Some("false".to_string()),
6520 "default=false must propagate as Some(\"false\")"
6521 );
6522 }
6523
6524 #[rstest]
6525 fn from_field_state_optional_bool_with_default() {
6526 let mut field_state = FieldState::new("maybe_flag", FieldType::Boolean, true);
6531 field_state
6532 .params
6533 .insert("default".to_string(), "true".to_string());
6534
6535 let col = ColumnDefinition::from_field_state("maybe_flag", &field_state);
6537
6538 assert!(
6540 !col.not_null,
6541 "Optional bool must remain NULLABLE — no regression on Option<T>"
6542 );
6543 assert_eq!(
6544 col.default,
6545 Some("true".to_string()),
6546 "Default propagation must work for Optional fields too"
6547 );
6548 }
6549
6550 #[rstest]
6551 fn from_field_state_non_optional_non_bool() {
6552 let field_state = FieldState::new("username", FieldType::VarChar(150), false);
6557
6558 let col = ColumnDefinition::from_field_state("username", &field_state);
6560
6561 assert!(
6563 col.not_null,
6564 "Non-Optional String must emit NOT NULL (regression #4573 — bug \
6565 affected all field types, not just bool)"
6566 );
6567 assert!(
6568 col.default.is_none(),
6569 "No default annotation → default = None"
6570 );
6571 }
6572
6573 #[rstest]
6574 fn from_field_state_primary_key_is_always_not_null() {
6575 let mut field_state = FieldState::new("id", FieldType::Uuid, true);
6580 field_state
6581 .params
6582 .insert("primary_key".to_string(), "true".to_string());
6583
6584 let col = ColumnDefinition::from_field_state("id", &field_state);
6586
6587 assert!(
6589 col.primary_key,
6590 "primary_key param must propagate to ColumnDefinition"
6591 );
6592 assert!(
6593 col.not_null,
6594 "Primary key must be NOT NULL regardless of nullable flag"
6595 );
6596 }
6597
6598 #[rstest]
6599 fn from_field_state_optional_field_remains_nullable() {
6600 let field_state = FieldState::new("last_login", FieldType::TimestampTz, true);
6604
6605 let col = ColumnDefinition::from_field_state("last_login", &field_state);
6607
6608 assert!(
6610 !col.not_null,
6611 "Optional field with no default must remain NULLABLE"
6612 );
6613 assert!(col.default.is_none(), "No default → default = None");
6614 assert!(!col.primary_key, "Non-PK field must not be primary_key");
6615 }
6616
6617 #[test]
6618 fn test_convert_default_value_null() {
6619 let op = Operation::CreateTable {
6620 name: "test".to_string(),
6621 columns: vec![],
6622 constraints: vec![],
6623 without_rowid: None,
6624 partition: None,
6625 interleave_in_parent: None,
6626 };
6627 let value = op.convert_default_value("null");
6628 assert!(
6629 matches!(value, Value::String(None)),
6630 "NULL value should be converted to Value::String(None)"
6631 );
6632 }
6633
6634 #[test]
6635 fn test_convert_default_value_bool() {
6636 let op = Operation::CreateTable {
6637 name: "test".to_string(),
6638 columns: vec![],
6639 constraints: vec![],
6640 without_rowid: None,
6641 partition: None,
6642 interleave_in_parent: None,
6643 };
6644 let value = op.convert_default_value("true");
6645 assert!(
6646 matches!(value, Value::Bool(Some(true))),
6647 "'true' should be converted to Value::Bool(Some(true))"
6648 );
6649
6650 let value = op.convert_default_value("false");
6651 assert!(
6652 matches!(value, Value::Bool(Some(false))),
6653 "'false' should be converted to Value::Bool(Some(false))"
6654 );
6655 }
6656
6657 #[test]
6658 fn test_convert_default_value_integer() {
6659 let op = Operation::CreateTable {
6660 name: "test".to_string(),
6661 columns: vec![],
6662 constraints: vec![],
6663 without_rowid: None,
6664 partition: None,
6665 interleave_in_parent: None,
6666 };
6667 let value = op.convert_default_value("42");
6668 assert!(
6669 matches!(value, Value::BigInt(Some(42))),
6670 "Integer '42' should be converted to Value::BigInt(Some(42))"
6671 );
6672 }
6673
6674 #[test]
6675 fn test_convert_default_value_float() {
6676 let op = Operation::CreateTable {
6677 name: "test".to_string(),
6678 columns: vec![],
6679 constraints: vec![],
6680 without_rowid: None,
6681 partition: None,
6682 interleave_in_parent: None,
6683 };
6684 let value = op.convert_default_value("3.15");
6685 assert!(
6686 matches!(value, Value::Double(_)),
6687 "Float '3.15' should be converted to Value::Double"
6688 );
6689 }
6690
6691 #[test]
6692 fn test_convert_default_value_string() {
6693 let op = Operation::CreateTable {
6694 name: "test".to_string(),
6695 columns: vec![],
6696 constraints: vec![],
6697 without_rowid: None,
6698 partition: None,
6699 interleave_in_parent: None,
6700 };
6701 let value = op.convert_default_value("'hello'");
6702 match value {
6703 Value::String(Some(s)) => assert_eq!(
6704 *s, "hello",
6705 "Quoted string should be unquoted and stored as 'hello'"
6706 ),
6707 _ => {
6708 panic!("Expected Value::String(Some(\"hello\")), got different variant")
6709 }
6710 }
6711 }
6712
6713 #[rstest]
6714 #[case("pending", "'pending'")]
6715 #[case("active", "'active'")]
6716 #[case("hello world", "'hello world'")]
6717 #[case("it's", "'it''s'")]
6718 fn test_convert_default_value_plain_string(#[case] input: &str, #[case] expected: &str) {
6719 let op = Operation::CreateTable {
6721 name: "test".to_string(),
6722 columns: vec![],
6723 constraints: vec![],
6724 without_rowid: None,
6725 partition: None,
6726 interleave_in_parent: None,
6727 };
6728
6729 let value = op.convert_default_value(input);
6731
6732 match value {
6734 Value::String(Some(s)) => assert_eq!(
6735 *s, expected,
6736 "Plain string '{input}' should be auto-quoted as SQL string literal"
6737 ),
6738 _ => {
6739 panic!("Expected Value::String(Some(\"{expected}\")), got {value:?}")
6740 }
6741 }
6742 }
6743
6744 #[rstest]
6745 #[case("CURRENT_TIMESTAMP")]
6746 #[case("current_timestamp")]
6747 #[case("CURRENT_DATE")]
6748 #[case("CURRENT_TIME")]
6749 #[case("CURRENT_USER")]
6750 #[case("SESSION_USER")]
6751 #[case("LOCALTIME")]
6752 #[case("LOCALTIMESTAMP")]
6753 fn test_convert_default_value_sql_constant(#[case] input: &str) {
6754 let op = Operation::CreateTable {
6756 name: "test".to_string(),
6757 columns: vec![],
6758 constraints: vec![],
6759 without_rowid: None,
6760 partition: None,
6761 interleave_in_parent: None,
6762 };
6763
6764 let value = op.convert_default_value(input);
6766
6767 match value {
6769 Value::String(Some(s)) => {
6770 assert_eq!(*s, input, "SQL constant '{input}' should remain unquoted")
6771 }
6772 _ => {
6773 panic!("Expected Value::String(Some(\"{input}\")), got {value:?}")
6774 }
6775 }
6776 }
6777
6778 #[rstest]
6779 #[case("NOW()")]
6780 #[case("uuid_generate_v4()")]
6781 #[case("gen_random_uuid()")]
6782 fn test_convert_default_value_sql_function(#[case] input: &str) {
6783 let op = Operation::CreateTable {
6785 name: "test".to_string(),
6786 columns: vec![],
6787 constraints: vec![],
6788 without_rowid: None,
6789 partition: None,
6790 interleave_in_parent: None,
6791 };
6792
6793 let value = op.convert_default_value(input);
6795
6796 match value {
6798 Value::String(Some(s)) => {
6799 assert_eq!(*s, input, "SQL function '{input}' should remain unquoted")
6800 }
6801 _ => {
6802 panic!("Expected Value::String(Some(\"{input}\")), got {value:?}")
6803 }
6804 }
6805 }
6806
6807 #[test]
6808 fn test_apply_column_type_integer() {
6809 let op = Operation::CreateTable {
6810 name: "test".to_string(),
6811 columns: vec![],
6812 constraints: vec![],
6813 without_rowid: None,
6814 partition: None,
6815 interleave_in_parent: None,
6816 };
6817 let col = ColumnDef::new(Alias::new("id"));
6818 let _col = op.apply_column_type(col, &FieldType::Integer);
6819 }
6822
6823 #[test]
6824 fn test_apply_column_type_varchar_with_length() {
6825 let op = Operation::CreateTable {
6826 name: "test".to_string(),
6827 columns: vec![],
6828 constraints: vec![],
6829 without_rowid: None,
6830 partition: None,
6831 interleave_in_parent: None,
6832 };
6833 let col = ColumnDef::new(Alias::new("name"));
6834 let _col = op.apply_column_type(col, &FieldType::VarChar(100));
6835 }
6838
6839 #[test]
6840 fn test_apply_column_type_custom() {
6841 let op = Operation::CreateTable {
6842 name: "test".to_string(),
6843 columns: vec![],
6844 constraints: vec![],
6845 without_rowid: None,
6846 partition: None,
6847 interleave_in_parent: None,
6848 };
6849 let col = ColumnDef::new(Alias::new("data"));
6850 let _col = op.apply_column_type(col, &FieldType::Custom("CUSTOM_TYPE".to_string()));
6851 }
6854
6855 #[test]
6856 fn test_create_index_composite() {
6857 let op = Operation::CreateIndex {
6858 table: "users".to_string(),
6859 columns: vec!["first_name".to_string(), "last_name".to_string()],
6860 unique: false,
6861 index_type: None,
6862 where_clause: None,
6863 concurrently: false,
6864 expressions: None,
6865 mysql_options: None,
6866 operator_class: None,
6867 };
6868
6869 let sql = op.to_sql(&SqlDialect::Postgres);
6870 assert!(
6871 sql.contains("first_name"),
6872 "SQL should include 'first_name' column, got: {}",
6873 sql
6874 );
6875 assert!(
6876 sql.contains("last_name"),
6877 "SQL should include 'last_name' column, got: {}",
6878 sql
6879 );
6880 assert!(
6881 sql.contains("idx_users_first_name_last_name"),
6882 "SQL should include composite index name, got: {}",
6883 sql
6884 );
6885 }
6886
6887 #[test]
6888 fn test_alter_table_comment_with_quotes() {
6889 let op = Operation::AlterTableComment {
6890 table: "users".to_string(),
6891 comment: Some("User's account table".to_string()),
6892 };
6893
6894 let stmt = op.to_statement();
6895 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
6896 assert!(
6897 sql.contains("COMMENT ON TABLE"),
6898 "SQL should contain COMMENT ON TABLE keywords, got: {}",
6899 sql
6900 );
6901 assert!(
6902 sql.contains("User''s account table"),
6903 "SQL should properly escape single quotes in comment, got: {}",
6904 sql
6905 );
6906 }
6907
6908 #[test]
6909 fn test_state_forwards_alter_column() {
6910 let mut state = ProjectState::new();
6911 let mut model = ModelState::new("myapp", "users");
6912 model.add_field(FieldState::new(
6913 "age".to_string(),
6914 FieldType::Integer,
6915 false,
6916 ));
6917 state.add_model(model);
6918
6919 let op = Operation::AlterColumn {
6920 table: "users".to_string(),
6921 column: "age".to_string(),
6922 old_definition: None,
6923 new_definition: ColumnDefinition {
6924 name: "age".to_string(),
6925 type_definition: FieldType::BigInteger,
6926 not_null: true,
6927 unique: false,
6928 primary_key: false,
6929 auto_increment: false,
6930 default: None,
6931 },
6932 mysql_options: None,
6933 };
6934
6935 op.state_forwards("myapp", &mut state);
6936 let model = state.get_model("myapp", "users").unwrap();
6937 let field = model.fields.get("age").unwrap();
6938 assert_eq!(
6939 field.field_type,
6940 FieldType::BigInteger,
6941 "Field type should be updated to BigInteger, got: {}",
6942 field.field_type
6943 );
6944 }
6945
6946 #[test]
6947 fn test_state_forwards_create_inherited_table() {
6948 let mut state = ProjectState::new();
6949 let op = Operation::CreateInheritedTable {
6950 name: "admin_users".to_string(),
6951 columns: vec![ColumnDefinition {
6952 name: "admin_level".to_string(),
6953 type_definition: FieldType::Integer,
6954 not_null: true,
6955 unique: false,
6956 primary_key: false,
6957 auto_increment: false,
6958 default: None,
6959 }],
6960 base_table: "users".to_string(),
6961 join_column: "user_id".to_string(),
6962 };
6963
6964 op.state_forwards("myapp", &mut state);
6965 let model = state.get_model("myapp", "admin_users");
6966 assert!(
6967 model.is_some(),
6968 "Inherited table 'admin_users' should exist in state"
6969 );
6970 let model = model.unwrap();
6971 assert_eq!(
6972 model.base_model,
6973 Some("users".to_string()),
6974 "base_model should be set to 'users'"
6975 );
6976 assert_eq!(
6977 model.inheritance_type,
6978 Some("joined_table".to_string()),
6979 "inheritance_type should be 'joined_table'"
6980 );
6981 }
6982
6983 #[test]
6984 fn test_state_forwards_add_discriminator_column() {
6985 let mut state = ProjectState::new();
6986 let mut model = ModelState::new("myapp", "users");
6987 model.add_field(FieldState::new("id".to_string(), FieldType::Integer, false));
6988 state.add_model(model);
6989
6990 let op = Operation::AddDiscriminatorColumn {
6991 table: "users".to_string(),
6992 column_name: "user_type".to_string(),
6993 default_value: "regular".to_string(),
6994 };
6995
6996 op.state_forwards("myapp", &mut state);
6997 let model = state.get_model("myapp", "users").unwrap();
6998 assert_eq!(
6999 model.discriminator_column,
7000 Some("user_type".to_string()),
7001 "discriminator_column should be set to 'user_type'"
7002 );
7003 assert_eq!(
7004 model.inheritance_type,
7005 Some("single_table".to_string()),
7006 "inheritance_type should be 'single_table'"
7007 );
7008 }
7009
7010 #[rstest]
7011 fn test_to_reverse_sql_create_table_quotes_identifiers() {
7012 let op = Operation::CreateTable {
7014 name: "user-data".to_string(),
7015 columns: vec![],
7016 constraints: vec![],
7017 without_rowid: None,
7018 partition: None,
7019 interleave_in_parent: None,
7020 };
7021 let state = ProjectState::default();
7022
7023 let sql = op
7025 .to_reverse_sql(&SqlDialect::Postgres, &state)
7026 .unwrap()
7027 .unwrap()
7028 .join("\n");
7029
7030 assert_eq!(
7032 sql, "DROP TABLE \"user-data\";",
7033 "Identifiers with special characters must be quoted"
7034 );
7035 }
7036
7037 #[rstest]
7038 fn test_to_reverse_sql_add_column_quotes_identifiers() {
7039 let op = Operation::AddColumn {
7041 table: "my table".to_string(),
7042 column: ColumnDefinition {
7043 name: "my column".to_string(),
7044 type_definition: FieldType::VarChar(255),
7045 not_null: false,
7046 unique: false,
7047 primary_key: false,
7048 auto_increment: false,
7049 default: None,
7050 },
7051 mysql_options: None,
7052 };
7053 let state = ProjectState::default();
7054
7055 let sql = op
7057 .to_reverse_sql(&SqlDialect::Postgres, &state)
7058 .unwrap()
7059 .unwrap()
7060 .join("\n");
7061
7062 assert_eq!(
7064 sql, "ALTER TABLE \"my table\" DROP COLUMN \"my column\";",
7065 "Table and column names with spaces must be quoted"
7066 );
7067 }
7068
7069 #[rstest]
7070 fn test_to_reverse_sql_rename_table_quotes_identifiers() {
7071 let op = Operation::RenameTable {
7073 old_name: "old; DROP TABLE users;--".to_string(),
7074 new_name: "new-name".to_string(),
7075 };
7076 let state = ProjectState::default();
7077
7078 let sql = op
7080 .to_reverse_sql(&SqlDialect::Postgres, &state)
7081 .unwrap()
7082 .unwrap()
7083 .join("\n");
7084
7085 assert_eq!(
7087 sql, "ALTER TABLE \"new-name\" RENAME TO \"old; DROP TABLE users;--\";",
7088 "SQL injection attempt must be quoted as identifier"
7089 );
7090 }
7091
7092 #[rstest]
7093 fn test_to_reverse_sql_rename_column_quotes_identifiers() {
7094 let op = Operation::RenameColumn {
7096 table: "my table".to_string(),
7097 old_name: "old col".to_string(),
7098 new_name: "new col".to_string(),
7099 };
7100 let state = ProjectState::default();
7101
7102 let sql = op
7104 .to_reverse_sql(&SqlDialect::Postgres, &state)
7105 .unwrap()
7106 .unwrap()
7107 .join("\n");
7108
7109 assert_eq!(
7111 sql, "ALTER TABLE \"my table\" RENAME COLUMN \"new col\" TO \"old col\";",
7112 "Identifiers with spaces must be quoted"
7113 );
7114 }
7115
7116 #[rstest]
7117 fn test_to_reverse_sql_create_index_quotes_identifiers() {
7118 let op = Operation::CreateIndex {
7120 table: "my-table".to_string(),
7121 columns: vec!["col a".to_string()],
7122 unique: false,
7123 index_type: None,
7124 where_clause: None,
7125 concurrently: false,
7126 expressions: None,
7127 mysql_options: None,
7128 operator_class: None,
7129 };
7130 let state = ProjectState::default();
7131
7132 let sql = op
7134 .to_reverse_sql(&SqlDialect::Postgres, &state)
7135 .unwrap()
7136 .unwrap()
7137 .join("\n");
7138
7139 assert!(
7141 sql.contains("DROP INDEX \"idx_my-table_col a\""),
7142 "Index name must be quoted, got: {}",
7143 sql
7144 );
7145 }
7146
7147 #[rstest]
7154 fn test_to_reverse_sql_create_index_emits_on_table_clause_for_mysql() {
7155 let op = Operation::CreateIndex {
7157 table: "users".to_string(),
7158 columns: vec!["email".to_string()],
7159 unique: false,
7160 index_type: None,
7161 where_clause: None,
7162 concurrently: false,
7163 expressions: None,
7164 mysql_options: None,
7165 operator_class: None,
7166 };
7167 let state = ProjectState::default();
7168
7169 let sql = op
7171 .to_reverse_sql(&SqlDialect::Mysql, &state)
7172 .unwrap()
7173 .unwrap()
7174 .join("\n");
7175
7176 assert_eq!(
7181 sql, "DROP INDEX idx_users_email ON users;",
7182 "MySQL reverse SQL must include `ON <table>` clause"
7183 );
7184 }
7185
7186 #[rstest]
7189 #[case(SqlDialect::Postgres, "DROP INDEX idx_users_email;")]
7190 #[case(SqlDialect::Sqlite, "DROP INDEX idx_users_email;")]
7191 #[case(SqlDialect::Cockroachdb, "DROP INDEX idx_users_email;")]
7192 fn test_to_reverse_sql_create_index_omits_on_table_for_non_mysql(
7193 #[case] dialect: SqlDialect,
7194 #[case] expected: &str,
7195 ) {
7196 let op = Operation::CreateIndex {
7198 table: "users".to_string(),
7199 columns: vec!["email".to_string()],
7200 unique: false,
7201 index_type: None,
7202 where_clause: None,
7203 concurrently: false,
7204 expressions: None,
7205 mysql_options: None,
7206 operator_class: None,
7207 };
7208 let state = ProjectState::default();
7209
7210 let sql = op
7212 .to_reverse_sql(&dialect, &state)
7213 .unwrap()
7214 .unwrap()
7215 .join("\n");
7216
7217 assert_eq!(
7219 sql, expected,
7220 "Non-MySQL reverse SQL must remain unchanged for dialect {:?}",
7221 dialect
7222 );
7223 }
7224
7225 #[rstest]
7226 fn test_to_reverse_sql_add_constraint_quotes_identifiers() {
7227 let op = Operation::AddConstraint {
7229 table: "my-table".to_string(),
7230 constraint_sql: "CONSTRAINT chk_positive CHECK (x > 0)".to_string(),
7231 };
7232 let state = ProjectState::default();
7233
7234 let sql = op
7236 .to_reverse_sql(&SqlDialect::Postgres, &state)
7237 .unwrap()
7238 .unwrap()
7239 .join("\n");
7240
7241 assert!(
7243 sql.contains("ALTER TABLE \"my-table\""),
7244 "Table name with special characters must be quoted, got: {}",
7245 sql
7246 );
7247 assert!(
7248 sql.contains("DROP CONSTRAINT"),
7249 "Should contain DROP CONSTRAINT, got: {}",
7250 sql
7251 );
7252 }
7253
7254 #[rstest]
7255 fn test_to_reverse_sql_bulk_load_quotes_identifiers() {
7256 let op = Operation::BulkLoad {
7258 table: "user-data".to_string(),
7259 source: BulkLoadSource::Stdin,
7260 format: BulkLoadFormat::default(),
7261 options: BulkLoadOptions::default(),
7262 };
7263 let state = ProjectState::default();
7264
7265 let sql = op
7267 .to_reverse_sql(&SqlDialect::Postgres, &state)
7268 .unwrap()
7269 .unwrap()
7270 .join("\n");
7271
7272 assert_eq!(
7274 sql, "TRUNCATE TABLE \"user-data\";",
7275 "Table name must be quoted"
7276 );
7277 }
7278
7279 #[rstest]
7284 #[case::postgres(SqlDialect::Postgres)]
7285 #[case::cockroachdb(SqlDialect::Cockroachdb)]
7286 fn test_set_auto_increment_postgres_uses_setval(#[case] dialect: SqlDialect) {
7287 let op = Operation::SetAutoIncrementValue {
7289 table: "users".to_string(),
7290 column: "id".to_string(),
7291 value: 1000,
7292 };
7293
7294 let sql = op.to_sql(&dialect);
7296
7297 assert_eq!(
7299 sql,
7300 "SELECT setval(pg_get_serial_sequence('users', 'id'), 1000, false);"
7301 );
7302 }
7303
7304 #[test]
7305 fn test_set_auto_increment_mysql_alters_table() {
7306 let op = Operation::SetAutoIncrementValue {
7308 table: "users".to_string(),
7309 column: "id".to_string(),
7310 value: 1000,
7311 };
7312
7313 let sql = op.to_sql(&SqlDialect::Mysql);
7315
7316 assert_eq!(sql, "ALTER TABLE users AUTO_INCREMENT = 1000;");
7321 }
7322
7323 #[test]
7324 fn test_set_auto_increment_sqlite_upserts_sqlite_sequence() {
7325 let op = Operation::SetAutoIncrementValue {
7327 table: "users".to_string(),
7328 column: "id".to_string(),
7329 value: 1000,
7330 };
7331
7332 let sql = op.to_sql(&SqlDialect::Sqlite);
7334
7335 assert_eq!(
7338 sql,
7339 "INSERT OR REPLACE INTO sqlite_sequence(name, seq) VALUES ('users', 1000);"
7340 );
7341 }
7342
7343 #[test]
7344 fn test_set_auto_increment_postgres_escapes_literals() {
7345 let op = Operation::SetAutoIncrementValue {
7347 table: "user's".to_string(),
7348 column: "id".to_string(),
7349 value: 42,
7350 };
7351
7352 let sql = op.to_sql(&SqlDialect::Postgres);
7354
7355 assert!(
7357 sql.contains("'user''s'"),
7358 "single quote in table name must be escaped: {}",
7359 sql
7360 );
7361 }
7362
7363 #[rstest]
7368 #[case::postgres(SqlDialect::Postgres)]
7369 #[case::mysql(SqlDialect::Mysql)]
7370 #[case::sqlite(SqlDialect::Sqlite)]
7371 #[case::cockroachdb(SqlDialect::Cockroachdb)]
7372 fn test_composite_pk_default_name(#[case] dialect: SqlDialect) {
7373 let op = Operation::CreateCompositePrimaryKey {
7375 table: "order_items".to_string(),
7376 columns: vec!["order_id".to_string(), "line_number".to_string()],
7377 constraint_name: None,
7378 };
7379
7380 let sql = op.to_sql(&dialect);
7382
7383 assert!(
7385 sql.contains("ALTER TABLE"),
7386 "SQL should use ALTER TABLE: {}",
7387 sql
7388 );
7389 assert!(
7390 sql.contains("ADD CONSTRAINT"),
7391 "SQL should add a named constraint: {}",
7392 sql
7393 );
7394 assert!(
7395 sql.contains("PRIMARY KEY"),
7396 "SQL should add PRIMARY KEY: {}",
7397 sql
7398 );
7399 assert!(
7400 sql.contains("order_items_pkey"),
7401 "Default constraint name should be table_pkey: {}",
7402 sql
7403 );
7404 assert!(
7405 sql.contains("order_id") && sql.contains("line_number"),
7406 "Both PK columns must appear: {}",
7407 sql
7408 );
7409 }
7410
7411 #[test]
7412 fn test_composite_pk_custom_name_and_quoting() {
7413 let op = Operation::CreateCompositePrimaryKey {
7415 table: "tbl".to_string(),
7416 columns: vec!["a".to_string(), "b".to_string()],
7417 constraint_name: Some("my_pk".to_string()),
7418 };
7419
7420 let sql = op.to_sql(&SqlDialect::Postgres);
7422
7423 assert_eq!(
7425 sql,
7426 "ALTER TABLE tbl ADD CONSTRAINT my_pk PRIMARY KEY (a, b);"
7427 );
7428 }
7429
7430 #[test]
7431 fn test_composite_pk_empty_columns_produces_failing_sql() {
7432 let op = Operation::CreateCompositePrimaryKey {
7438 table: "tbl".to_string(),
7439 columns: vec![],
7440 constraint_name: None,
7441 };
7442
7443 for dialect in [SqlDialect::Postgres, SqlDialect::Mysql, SqlDialect::Sqlite] {
7445 let sql = op.to_sql(&dialect);
7446
7447 assert!(
7450 sql.starts_with("SYNTAX_ERROR_create_composite_pk_on_")
7451 && sql.contains("requires_at_least_one_column"),
7452 "Empty column list must emit a syntax-error statement with diagnostic ({:?}): {}",
7453 dialect,
7454 sql
7455 );
7456 assert!(
7457 !sql.contains("SELECT 1/0"),
7458 "Must not fall back to SELECT 1/0 (silently passes on SQLite / lax MySQL): {}",
7459 sql
7460 );
7461 }
7462 }
7463
7464 #[rstest]
7475 #[case::big_integer(FieldType::BigInteger)]
7476 #[case::integer(FieldType::Integer)]
7477 #[case::small_integer(FieldType::SmallInteger)]
7478 fn test_column_to_sql_sqlite_auto_increment_pk_emits_integer(#[case] field_type: FieldType) {
7479 let mut col = ColumnDefinition::new("id", field_type);
7481 col.primary_key = true;
7482 col.auto_increment = true;
7483 col.not_null = true;
7484
7485 let sql = Operation::column_to_sql(&col, &SqlDialect::Sqlite);
7487
7488 assert!(
7491 sql.contains("INTEGER PRIMARY KEY AUTOINCREMENT"),
7492 "SQLite auto_increment PK must emit `INTEGER PRIMARY KEY AUTOINCREMENT`: {}",
7493 sql
7494 );
7495 assert!(
7496 !sql.contains("BIGINT"),
7497 "SQLite auto_increment must not emit BIGINT (rejected by SQLite): {}",
7498 sql
7499 );
7500 assert!(
7501 !sql.contains("SMALLINT"),
7502 "SQLite auto_increment must not emit SMALLINT (rejected by SQLite): {}",
7503 sql
7504 );
7505 }
7506
7507 #[test]
7508 fn test_column_to_sql_sqlite_big_integer_without_auto_increment_no_autoincrement() {
7509 let mut col = ColumnDefinition::new("count", FieldType::BigInteger);
7514 col.not_null = true;
7515
7516 let sql = Operation::column_to_sql(&col, &SqlDialect::Sqlite);
7518
7519 assert!(
7521 !sql.contains("AUTOINCREMENT"),
7522 "Non-auto_increment column must not emit AUTOINCREMENT: {}",
7523 sql
7524 );
7525 assert!(
7530 !sql.contains("BIGINT"),
7531 "emitter is expected to normalize BigInteger to INTEGER for SQLite: {}",
7532 sql
7533 );
7534 }
7535
7536 #[test]
7537 fn test_column_to_sql_postgres_big_integer_auto_increment_unchanged() {
7538 let mut col = ColumnDefinition::new("id", FieldType::BigInteger);
7540 col.primary_key = true;
7541 col.auto_increment = true;
7542 col.not_null = true;
7543
7544 let sql = Operation::column_to_sql(&col, &SqlDialect::Postgres);
7546
7547 assert!(
7549 sql.contains("BIGINT GENERATED BY DEFAULT AS IDENTITY"),
7550 "Postgres auto_increment BigInteger must emit identity syntax: {}",
7551 sql
7552 );
7553 }
7554
7555 #[test]
7556 fn test_column_to_sql_sqlite_auto_increment_uuid_pk_omits_autoincrement() {
7557 let mut col = ColumnDefinition::new("id", FieldType::Uuid);
7565 col.primary_key = true;
7566 col.auto_increment = true;
7567 col.not_null = true;
7568
7569 let sql = Operation::column_to_sql(&col, &SqlDialect::Sqlite);
7571
7572 assert!(
7574 sql.contains("PRIMARY KEY"),
7575 "UUID PK must still emit PRIMARY KEY: {}",
7576 sql
7577 );
7578 assert!(
7579 !sql.contains("AUTOINCREMENT"),
7580 "non-integer auto_increment PK must not emit AUTOINCREMENT (SQLite rejects it): {}",
7581 sql
7582 );
7583 assert!(
7588 !sql.contains("INTEGER"),
7589 "UUID column type must not be widened to INTEGER: {}",
7590 sql
7591 );
7592 }
7593
7594 #[test]
7595 fn test_column_to_sql_without_pk_sqlite_auto_increment_emits_integer() {
7596 let mut col = ColumnDefinition::new("id", FieldType::BigInteger);
7598 col.auto_increment = true;
7599 col.not_null = true;
7600
7601 let sql = Operation::column_to_sql_without_pk(&col, &SqlDialect::Sqlite);
7603
7604 assert!(
7606 sql.contains("INTEGER"),
7607 "SQLite auto_increment column (composite PK path) must emit INTEGER: {}",
7608 sql
7609 );
7610 assert!(
7611 !sql.contains("BIGINT"),
7612 "SQLite auto_increment must not emit BIGINT in composite PK path: {}",
7613 sql
7614 );
7615 }
7616
7617 mod resolve_foreign_key_column_type_tests {
7618 use super::super::resolve_foreign_key_column_type_with;
7619 use super::FieldType;
7620 use crate::migrations::autodetector::FieldState;
7621 use crate::migrations::model_registry::{FieldMetadata, ModelMetadata, ModelRegistry};
7622
7623 fn target_model(app: &str, name: &str, table: &str, pk_type: FieldType) -> ModelMetadata {
7626 let mut meta = ModelMetadata::new(app, name, table);
7627 meta.add_field(
7628 "id".to_string(),
7629 FieldMetadata::new(pk_type).with_param("primary_key", "true"),
7630 );
7631 meta
7632 }
7633
7634 fn fk_field_state(target_model: &str, target_app: Option<&str>) -> FieldState {
7638 let mut fs = FieldState::new("owner_id", FieldType::Uuid, false);
7639 fs.params
7640 .insert("fk_target".to_string(), target_model.to_string());
7641 if let Some(app) = target_app {
7642 fs.params
7643 .insert("fk_target_app".to_string(), app.to_string());
7644 }
7645 fs
7646 }
7647
7648 #[test]
7649 fn qualified_hit_resolves_to_target_pk_type() {
7650 let registry = ModelRegistry::new();
7652 registry.register_model(target_model(
7653 "auth",
7654 "User",
7655 "auth_user",
7656 FieldType::BigInteger,
7657 ));
7658 let fs = fk_field_state("User", Some("auth"));
7659
7660 let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7662
7663 assert_eq!(resolved, Some(FieldType::BigInteger));
7665 }
7666
7667 #[test]
7668 fn qualified_miss_falls_back_to_by_name_when_unambiguous() {
7669 let registry = ModelRegistry::new();
7672 registry.register_model(target_model(
7673 "reinhardt_auth",
7674 "User",
7675 "auth_user",
7676 FieldType::Uuid,
7677 ));
7678 let fs = fk_field_state("User", Some("blog"));
7680
7681 let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7683
7684 assert_eq!(resolved, Some(FieldType::Uuid));
7687 }
7688
7689 #[test]
7690 fn ambiguous_by_name_returns_none() {
7691 let registry = ModelRegistry::new();
7693 registry.register_model(target_model(
7694 "auth",
7695 "User",
7696 "auth_user",
7697 FieldType::BigInteger,
7698 ));
7699 registry.register_model(target_model(
7700 "billing",
7701 "User",
7702 "billing_user",
7703 FieldType::Uuid,
7704 ));
7705 let fs = fk_field_state("User", None);
7707
7708 let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7710
7711 assert_eq!(resolved, None);
7714 }
7715
7716 #[test]
7717 fn path_typed_disambiguates_ambiguous_name() {
7718 let registry = ModelRegistry::new();
7725 registry.register_model(target_model(
7726 "blog",
7727 "User",
7728 "blog_user",
7729 FieldType::BigInteger,
7730 ));
7731 registry.register_model(target_model(
7732 "reinhardt_auth",
7733 "User",
7734 "reinhardt_auth_user",
7735 FieldType::Uuid,
7736 ));
7737 let fs = fk_field_state("User", Some("reinhardt_auth"));
7738
7739 let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7741
7742 assert_eq!(resolved, Some(FieldType::Uuid));
7745 }
7746
7747 #[test]
7748 fn qualified_miss_with_ambiguous_by_name_returns_none() {
7749 let registry = ModelRegistry::new();
7753 registry.register_model(target_model(
7754 "auth",
7755 "User",
7756 "auth_user",
7757 FieldType::BigInteger,
7758 ));
7759 registry.register_model(target_model(
7760 "billing",
7761 "User",
7762 "billing_user",
7763 FieldType::Uuid,
7764 ));
7765 let fs = fk_field_state("User", Some("blog")); let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7769
7770 assert_eq!(resolved, None);
7772 }
7773
7774 #[test]
7775 fn no_fk_target_param_returns_none() {
7776 let registry = ModelRegistry::new();
7778 registry.register_model(target_model(
7779 "auth",
7780 "User",
7781 "auth_user",
7782 FieldType::BigInteger,
7783 ));
7784 let fs = FieldState::new("name", FieldType::VarChar(64), false);
7785
7786 let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7788
7789 assert_eq!(resolved, None);
7791 }
7792 }
7793}