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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
148#[serde(rename_all = "UPPERCASE")]
149pub enum MySqlAlgorithm {
150 Instant,
152 Inplace,
154 Copy,
156 #[default]
157 Default,
159}
160
161impl std::fmt::Display for MySqlAlgorithm {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 match self {
164 MySqlAlgorithm::Instant => write!(f, "INSTANT"),
165 MySqlAlgorithm::Inplace => write!(f, "INPLACE"),
166 MySqlAlgorithm::Copy => write!(f, "COPY"),
167 MySqlAlgorithm::Default => write!(f, "DEFAULT"),
168 }
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
174#[serde(rename_all = "UPPERCASE")]
175pub enum MySqlLock {
176 None,
178 Shared,
180 Exclusive,
182 #[default]
183 Default,
185}
186
187impl std::fmt::Display for MySqlLock {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 match self {
190 MySqlLock::None => write!(f, "NONE"),
191 MySqlLock::Shared => write!(f, "SHARED"),
192 MySqlLock::Exclusive => write!(f, "EXCLUSIVE"),
193 MySqlLock::Default => write!(f, "DEFAULT"),
194 }
195 }
196}
197
198#[non_exhaustive]
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
201pub struct AlterTableOptions {
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub algorithm: Option<MySqlAlgorithm>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub lock: Option<MySqlLock>,
208}
209
210impl AlterTableOptions {
211 pub fn new() -> Self {
213 Self::default()
214 }
215 pub fn with_algorithm(mut self, algorithm: MySqlAlgorithm) -> Self {
217 self.algorithm = Some(algorithm);
218 self
219 }
220 pub fn with_lock(mut self, lock: MySqlLock) -> Self {
222 self.lock = Some(lock);
223 self
224 }
225 pub fn is_empty(&self) -> bool {
227 self.algorithm.is_none() && self.lock.is_none()
228 }
229 pub fn to_sql_suffix(&self) -> String {
231 let mut parts = Vec::new();
232 if let Some(algo) = &self.algorithm
233 && *algo != MySqlAlgorithm::Default
234 {
235 parts.push(format!("ALGORITHM={}", algo));
236 }
237 if let Some(lock) = &self.lock
238 && *lock != MySqlLock::Default
239 {
240 parts.push(format!("LOCK={}", lock));
241 }
242 if parts.is_empty() {
243 String::new()
244 } else {
245 format!(", {}", parts.join(", "))
246 }
247 }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
256#[serde(rename_all = "UPPERCASE")]
257pub enum PartitionType {
258 Range,
260 List,
262 Hash,
264 Key,
266}
267
268impl std::fmt::Display for PartitionType {
269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270 match self {
271 PartitionType::Range => write!(f, "RANGE"),
272 PartitionType::List => write!(f, "LIST"),
273 PartitionType::Hash => write!(f, "HASH"),
274 PartitionType::Key => write!(f, "KEY"),
275 }
276 }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
281#[serde(tag = "type")]
282pub enum PartitionValues {
283 LessThan(String),
285 In(Vec<String>),
287 ModuloCount(u32),
289}
290
291#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
293pub struct PartitionDef {
294 pub name: String,
296 pub values: PartitionValues,
298}
299
300impl PartitionDef {
301 pub fn new(name: impl Into<String>, values: PartitionValues) -> Self {
303 Self {
304 name: name.into(),
305 values,
306 }
307 }
308 pub fn less_than(name: impl Into<String>, value: impl Into<String>) -> Self {
310 Self::new(name, PartitionValues::LessThan(value.into()))
311 }
312 pub fn maxvalue(name: impl Into<String>) -> Self {
314 Self::new(name, PartitionValues::LessThan("MAXVALUE".to_string()))
315 }
316 pub fn list_in(name: impl Into<String>, values: Vec<String>) -> Self {
318 Self::new(name, PartitionValues::In(values))
319 }
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
329pub struct InterleaveSpec {
330 pub parent_table: String,
332 pub parent_columns: Vec<String>,
334}
335
336#[non_exhaustive]
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
339pub struct PartitionOptions {
340 pub partition_type: PartitionType,
342 pub column: String,
344 pub partitions: Vec<PartitionDef>,
346}
347
348impl PartitionOptions {
349 pub fn new(
351 partition_type: PartitionType,
352 column: impl Into<String>,
353 partitions: Vec<PartitionDef>,
354 ) -> Self {
355 Self {
356 partition_type,
357 column: column.into(),
358 partitions,
359 }
360 }
361 pub fn range(column: impl Into<String>, partitions: Vec<PartitionDef>) -> Self {
363 Self::new(PartitionType::Range, column, partitions)
364 }
365 pub fn list(column: impl Into<String>, partitions: Vec<PartitionDef>) -> Self {
367 Self::new(PartitionType::List, column, partitions)
368 }
369 pub fn hash(column: impl Into<String>, num_partitions: u32) -> Self {
371 Self::new(
372 PartitionType::Hash,
373 column,
374 vec![PartitionDef::new(
375 "",
376 PartitionValues::ModuloCount(num_partitions),
377 )],
378 )
379 }
380 pub fn key(column: impl Into<String>, num_partitions: u32) -> Self {
382 Self::new(
383 PartitionType::Key,
384 column,
385 vec![PartitionDef::new(
386 "",
387 PartitionValues::ModuloCount(num_partitions),
388 )],
389 )
390 }
391 pub fn to_sql(&self) -> String {
393 let mut sql = format!("PARTITION BY {}({})", self.partition_type, self.column);
394 match self.partition_type {
395 PartitionType::Hash | PartitionType::Key => {
396 if let Some(p) = self.partitions.first()
397 && let PartitionValues::ModuloCount(n) = &p.values
398 {
399 sql.push_str(&format!(" PARTITIONS {}", n));
400 }
401 }
402 PartitionType::Range | PartitionType::List => {
403 sql.push_str(" (");
404 let defs: Vec<String> = self
405 .partitions
406 .iter()
407 .map(|p| {
408 let vals = match &p.values {
409 PartitionValues::LessThan(v) => {
410 if v == "MAXVALUE" {
411 "VALUES LESS THAN MAXVALUE".to_string()
412 } else {
413 format!("VALUES LESS THAN ('{}')", v)
414 }
415 }
416 PartitionValues::In(v) => format!(
417 "VALUES IN ({})",
418 v.iter()
419 .map(|x| format!("'{}'", x))
420 .collect::<Vec<_>>()
421 .join(", ")
422 ),
423 PartitionValues::ModuloCount(_) => String::new(),
424 };
425 format!("PARTITION {} {}", p.name, vals)
426 })
427 .collect();
428 sql.push_str(&defs.join(", "));
429 sql.push(')');
430 }
431 }
432 sql
433 }
434}
435
436#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
440#[serde(rename_all = "lowercase")]
441pub enum DeferrableOption {
442 Immediate,
444 Deferred,
446}
447
448impl std::fmt::Display for DeferrableOption {
449 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450 match self {
451 DeferrableOption::Immediate => write!(f, "DEFERRABLE INITIALLY IMMEDIATE"),
452 DeferrableOption::Deferred => write!(f, "DEFERRABLE INITIALLY DEFERRED"),
453 }
454 }
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
459#[serde(tag = "type")]
460pub enum Constraint {
461 PrimaryKey {
466 name: String,
468 columns: Vec<String>,
470 },
471 ForeignKey {
473 name: String,
475 columns: Vec<String>,
477 referenced_table: String,
479 referenced_columns: Vec<String>,
481 on_delete: super::ForeignKeyAction,
483 on_update: super::ForeignKeyAction,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
487 deferrable: Option<DeferrableOption>,
488 },
489 Unique {
491 name: String,
493 columns: Vec<String>,
495 },
496 Check {
498 name: String,
500 expression: String,
502 },
503 OneToOne {
505 name: String,
507 column: String,
509 referenced_table: String,
511 referenced_column: String,
513 on_delete: super::ForeignKeyAction,
515 on_update: super::ForeignKeyAction,
517 #[serde(default, skip_serializing_if = "Option::is_none")]
519 deferrable: Option<DeferrableOption>,
520 },
521 ManyToMany {
523 name: String,
525 through_table: String,
527 source_column: String,
529 target_column: String,
531 target_table: String,
533 },
534 Exclude {
536 name: String,
538 elements: Vec<(String, String)>,
540 #[serde(default, skip_serializing_if = "Option::is_none")]
541 using: Option<String>,
543 #[serde(default, skip_serializing_if = "Option::is_none")]
544 where_clause: Option<String>,
546 },
547}
548
549impl std::fmt::Display for Constraint {
550 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
551 match self {
552 Constraint::PrimaryKey { name, columns } => {
553 write!(
554 f,
555 "CONSTRAINT {} PRIMARY KEY ({})",
556 name,
557 columns.join(", ")
558 )
559 }
560 Constraint::ForeignKey {
561 name,
562 columns,
563 referenced_table,
564 referenced_columns,
565 on_delete,
566 on_update,
567 deferrable,
568 } => {
569 write!(
570 f,
571 "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {}({}) ON DELETE {} ON UPDATE {}",
572 name,
573 columns.join(", "),
574 referenced_table,
575 referenced_columns.join(", "),
576 on_delete.to_sql_keyword(),
577 on_update.to_sql_keyword()
578 )?;
579 if let Some(defer_opt) = deferrable {
580 write!(f, " {}", defer_opt)?;
581 }
582 Ok(())
583 }
584 Constraint::Unique { name, columns } => {
585 write!(f, "CONSTRAINT {} UNIQUE ({})", name, columns.join(", "))
586 }
587 Constraint::Check { name, expression } => {
588 write!(f, "CONSTRAINT {} CHECK ({})", name, expression)
589 }
590 Constraint::OneToOne {
591 name,
592 column,
593 referenced_table,
594 referenced_column,
595 on_delete,
596 on_update,
597 deferrable,
598 } => {
599 write!(
600 f,
601 "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {}({}) ON DELETE {} ON UPDATE {}",
602 name,
603 column,
604 referenced_table,
605 referenced_column,
606 on_delete.to_sql_keyword(),
607 on_update.to_sql_keyword()
608 )?;
609 if let Some(defer_opt) = deferrable {
610 write!(f, " {}", defer_opt)?;
611 }
612 write!(f, ", CONSTRAINT {}_unique UNIQUE ({})", name, column)
613 }
614 Constraint::ManyToMany { through_table, .. } => {
615 write!(f, "-- ManyToMany via {}", through_table)
616 }
617 Constraint::Exclude {
618 name,
619 elements,
620 using,
621 where_clause,
622 } => {
623 let elements_str: Vec<String> = elements
624 .iter()
625 .map(|(col, op)| format!("{} WITH {}", col, op))
626 .collect();
627 let using_str = using.as_deref().unwrap_or("gist");
628 if let Some(where_cl) = where_clause {
629 write!(
630 f,
631 "CONSTRAINT {} EXCLUDE USING {} ({}) WHERE ({})",
632 name,
633 using_str,
634 elements_str.join(", "),
635 where_cl
636 )
637 } else {
638 write!(
639 f,
640 "CONSTRAINT {} EXCLUDE USING {} ({})",
641 name,
642 using_str,
643 elements_str.join(", ")
644 )
645 }
646 }
647 }
648 }
649}
650
651#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
655#[serde(tag = "type", content = "value")]
656pub enum BulkLoadSource {
657 File(String),
659 Stdin,
661 Program(String),
663}
664
665#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
669#[serde(rename_all = "lowercase")]
670pub enum BulkLoadFormat {
671 #[default]
673 Text,
674 Csv,
676 Binary,
678}
679
680impl std::fmt::Display for BulkLoadFormat {
681 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
682 match self {
683 BulkLoadFormat::Text => write!(f, "TEXT"),
684 BulkLoadFormat::Csv => write!(f, "CSV"),
685 BulkLoadFormat::Binary => write!(f, "BINARY"),
686 }
687 }
688}
689
690#[non_exhaustive]
694#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
695pub struct BulkLoadOptions {
696 #[serde(default, skip_serializing_if = "Option::is_none")]
698 pub delimiter: Option<char>,
699 #[serde(default, skip_serializing_if = "Option::is_none")]
701 pub null_string: Option<String>,
702 #[serde(default)]
704 pub header: bool,
705 #[serde(default, skip_serializing_if = "Option::is_none")]
707 pub columns: Option<Vec<String>>,
708 #[serde(default)]
710 pub local: bool,
711 #[serde(default, skip_serializing_if = "Option::is_none")]
713 pub quote: Option<char>,
714 #[serde(default, skip_serializing_if = "Option::is_none")]
716 pub escape: Option<char>,
717 #[serde(default, skip_serializing_if = "Option::is_none")]
719 pub line_terminator: Option<String>,
720 #[serde(default, skip_serializing_if = "Option::is_none")]
722 pub encoding: Option<String>,
723}
724
725impl BulkLoadOptions {
726 pub fn new() -> Self {
728 Self::default()
729 }
730
731 pub fn with_delimiter(mut self, delimiter: char) -> Self {
733 self.delimiter = Some(delimiter);
734 self
735 }
736
737 pub fn with_null_string(mut self, null_string: impl Into<String>) -> Self {
739 self.null_string = Some(null_string.into());
740 self
741 }
742
743 pub fn with_header(mut self, header: bool) -> Self {
745 self.header = header;
746 self
747 }
748
749 pub fn with_columns(mut self, columns: Vec<String>) -> Self {
751 self.columns = Some(columns);
752 self
753 }
754
755 pub fn with_local(mut self, local: bool) -> Self {
757 self.local = local;
758 self
759 }
760
761 pub fn with_quote(mut self, quote: char) -> Self {
763 self.quote = Some(quote);
764 self
765 }
766
767 pub fn with_escape(mut self, escape: char) -> Self {
769 self.escape = Some(escape);
770 self
771 }
772
773 pub fn with_line_terminator(mut self, terminator: impl Into<String>) -> Self {
775 self.line_terminator = Some(terminator.into());
776 self
777 }
778
779 pub fn with_encoding(mut self, encoding: impl Into<String>) -> Self {
781 self.encoding = Some(encoding.into());
782 self
783 }
784}
785
786#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
792#[serde(tag = "type")]
793pub enum Operation {
794 CreateTable {
796 name: String,
798 columns: Vec<ColumnDefinition>,
800 #[serde(default)]
801 constraints: Vec<Constraint>,
803 #[serde(default, skip_serializing_if = "Option::is_none")]
804 without_rowid: Option<bool>,
806 #[serde(default, skip_serializing_if = "Option::is_none")]
807 interleave_in_parent: Option<InterleaveSpec>,
809 #[serde(default, skip_serializing_if = "Option::is_none")]
810 partition: Option<PartitionOptions>,
812 },
813 DropTable {
815 name: String,
817 },
818 AddColumn {
820 table: String,
822 column: ColumnDefinition,
824 #[serde(default, skip_serializing_if = "Option::is_none")]
825 mysql_options: Option<AlterTableOptions>,
827 },
828 DropColumn {
830 table: String,
832 column: String,
834 },
835 AlterColumn {
837 table: String,
839 column: String,
841 #[serde(default, skip_serializing_if = "Option::is_none")]
845 old_definition: Option<ColumnDefinition>,
846 new_definition: ColumnDefinition,
848 #[serde(default, skip_serializing_if = "Option::is_none")]
849 mysql_options: Option<AlterTableOptions>,
851 },
852 RenameTable {
854 old_name: String,
856 new_name: String,
858 },
859 RenameColumn {
861 table: String,
863 old_name: String,
865 new_name: String,
867 },
868 AddConstraint {
870 table: String,
872 constraint_sql: String,
874 },
875 DropConstraint {
877 table: String,
879 constraint_name: String,
881 },
882 CreateIndex {
884 table: String,
886 columns: Vec<String>,
888 unique: bool,
890 #[serde(default, skip_serializing_if = "Option::is_none")]
894 index_type: Option<IndexType>,
895 #[serde(default, skip_serializing_if = "Option::is_none")]
900 where_clause: Option<String>,
901 #[serde(default)]
906 concurrently: bool,
907 #[serde(default, skip_serializing_if = "Option::is_none")]
921 expressions: Option<Vec<String>>,
922 #[serde(default, skip_serializing_if = "Option::is_none")]
924 mysql_options: Option<AlterTableOptions>,
925 #[serde(default, skip_serializing_if = "Option::is_none")]
944 operator_class: Option<String>,
945 },
946 DropIndex {
948 table: String,
950 columns: Vec<String>,
952 },
953 RunSQL {
955 sql: String,
957 reverse_sql: Option<String>,
959 },
960 RunRust {
962 code: String,
964 reverse_code: Option<String>,
966 },
967 AlterTableComment {
969 table: String,
971 comment: Option<String>,
973 },
974 AlterUniqueTogether {
976 table: String,
978 unique_together: Vec<Vec<String>>,
980 },
981 AlterModelOptions {
983 table: String,
985 options: std::collections::HashMap<String, String>,
987 },
988 CreateInheritedTable {
990 name: String,
992 columns: Vec<ColumnDefinition>,
994 base_table: String,
996 join_column: String,
998 },
999 AddDiscriminatorColumn {
1001 table: String,
1003 column_name: String,
1005 default_value: String,
1007 },
1008 MoveModel {
1017 model_name: String,
1019 from_app: String,
1021 to_app: String,
1023 rename_table: bool,
1025 old_table_name: Option<String>,
1027 new_table_name: Option<String>,
1029 },
1030 CreateSchema {
1034 name: String,
1036 #[serde(default)]
1038 if_not_exists: bool,
1039 },
1040 DropSchema {
1044 name: String,
1046 #[serde(default)]
1048 cascade: bool,
1049 #[serde(default = "default_true")]
1051 if_exists: bool,
1052 },
1053 CreateExtension {
1058 name: String,
1060 #[serde(default = "default_true")]
1062 if_not_exists: bool,
1063 #[serde(default)]
1065 schema: Option<String>,
1066 },
1067 BulkLoad {
1105 table: String,
1107 source: BulkLoadSource,
1109 #[serde(default)]
1111 format: BulkLoadFormat,
1112 #[serde(default)]
1114 options: BulkLoadOptions,
1115 },
1116 SetAutoIncrementValue {
1133 table: String,
1135 column: String,
1138 value: i64,
1140 },
1141 CreateCompositePrimaryKey {
1152 table: String,
1154 columns: Vec<String>,
1156 #[serde(default, skip_serializing_if = "Option::is_none")]
1159 constraint_name: Option<String>,
1160 },
1161}
1162
1163const fn default_true() -> bool {
1165 true
1166}
1167
1168impl Operation {
1169 pub fn state_forwards(&self, app_label: &str, state: &mut ProjectState) {
1171 match self {
1172 Operation::CreateTable { name, columns, .. } => {
1173 let mut model = ModelState::new(app_label, name.clone());
1174 for column in columns {
1175 let field = FieldState::new(
1176 column.name.to_string(),
1177 column.type_definition.clone(),
1178 false,
1179 );
1180 model.add_field(field);
1181 }
1182 state.add_model(model);
1183 }
1184 Operation::DropTable { name } => {
1185 state.remove_model(app_label, name);
1186 }
1187 Operation::AddColumn { table, column, .. } => {
1188 if let Some(model) = state.get_model_mut(app_label, table) {
1189 let field = FieldState::new(
1190 column.name.to_string(),
1191 column.type_definition.clone(),
1192 false,
1193 );
1194 model.add_field(field);
1195 }
1196 }
1197 Operation::DropColumn { table, column } => {
1198 if let Some(model) = state.get_model_mut(app_label, table) {
1199 model.remove_field(column);
1200 }
1201 }
1202 Operation::AlterColumn {
1203 table,
1204 column,
1205 new_definition,
1206 ..
1207 } => {
1208 if let Some(model) = state.get_model_mut(app_label, table) {
1209 let field = FieldState::new(
1210 column.to_string(),
1211 new_definition.type_definition.clone(),
1212 false,
1213 );
1214 model.alter_field(column, field);
1215 }
1216 }
1217 Operation::RenameTable { old_name, new_name } => {
1218 state.rename_model(app_label, old_name, new_name.to_string());
1219 }
1220 Operation::RenameColumn {
1221 table,
1222 old_name,
1223 new_name,
1224 } => {
1225 if let Some(model) = state.get_model_mut(app_label, table) {
1226 model.rename_field(old_name, new_name.to_string());
1227 }
1228 }
1229 Operation::CreateInheritedTable {
1230 name,
1231 columns,
1232 base_table,
1233 join_column,
1234 } => {
1235 let mut model = ModelState::new(app_label, name.clone());
1236 model.base_model = Some(base_table.to_string());
1237 model.inheritance_type = Some("joined_table".to_string());
1238
1239 let join_field = FieldState::new(
1240 join_column.to_string(),
1241 FieldType::Custom(format!("INTEGER REFERENCES {}(id)", base_table)),
1242 false,
1243 );
1244 model.add_field(join_field);
1245
1246 for column in columns {
1247 let field = FieldState::new(
1248 column.name.to_string(),
1249 column.type_definition.clone(),
1250 false,
1251 );
1252 model.add_field(field);
1253 }
1254 state.add_model(model);
1255 }
1256 Operation::AddDiscriminatorColumn {
1257 table,
1258 column_name,
1259 default_value,
1260 } => {
1261 if let Some(model) = state.get_model_mut(app_label, table) {
1262 model.discriminator_column = Some(column_name.to_string());
1263 model.inheritance_type = Some("single_table".to_string());
1264 let field = FieldState::new(
1265 column_name.to_string(),
1266 FieldType::Custom(format!("VARCHAR(50) DEFAULT '{}'", default_value)),
1267 false,
1268 );
1269 model.add_field(field);
1270 }
1271 }
1272 Operation::AddConstraint { .. }
1273 | Operation::DropConstraint { .. }
1274 | Operation::CreateIndex { .. }
1275 | Operation::DropIndex { .. }
1276 | Operation::RunSQL { .. }
1277 | Operation::RunRust { .. }
1278 | Operation::AlterTableComment { .. }
1279 | Operation::AlterUniqueTogether { .. }
1280 | Operation::AlterModelOptions { .. }
1281 | Operation::SetAutoIncrementValue { .. }
1282 | Operation::CreateCompositePrimaryKey { .. } => {
1283 }
1286 Operation::MoveModel {
1287 model_name,
1288 from_app,
1289 to_app,
1290 rename_table,
1291 old_table_name,
1292 new_table_name,
1293 } => {
1294 if let Some(model) = state.get_model(from_app, model_name).cloned() {
1297 state.remove_model(from_app, model_name);
1298
1299 let mut new_model = model;
1301 new_model.app_label = to_app.to_string();
1302
1303 if *rename_table
1305 && let (Some(_old_name), Some(new_name)) = (old_table_name, new_table_name)
1306 {
1307 new_model.table_name = new_name.to_string();
1308 }
1309
1310 state.add_model(new_model);
1311 }
1312 }
1313 Operation::CreateSchema { .. }
1315 | Operation::DropSchema { .. }
1316 | Operation::CreateExtension { .. } => {
1317 }
1319 Operation::BulkLoad { .. } => {
1321 }
1323 }
1324 }
1325
1326 fn column_to_sql_without_pk(col: &ColumnDefinition, dialect: &SqlDialect) -> String {
1331 let mut parts = Vec::new();
1332
1333 parts.push(quote_identifier(&col.name));
1335
1336 if col.auto_increment {
1338 match dialect {
1339 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
1340 match &col.type_definition {
1342 FieldType::BigInteger => {
1343 parts
1344 .push("BIGINT GENERATED BY DEFAULT AS IDENTITY".to_string().into());
1345 }
1346 FieldType::Integer => {
1347 parts.push(
1348 "INTEGER GENERATED BY DEFAULT AS IDENTITY"
1349 .to_string()
1350 .into(),
1351 );
1352 }
1353 FieldType::SmallInteger => {
1354 parts.push(
1355 "SMALLINT GENERATED BY DEFAULT AS IDENTITY"
1356 .to_string()
1357 .into(),
1358 );
1359 }
1360 _ => {
1361 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1363 }
1364 }
1365 }
1366 SqlDialect::Mysql => {
1367 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1368 parts.push("AUTO_INCREMENT".to_string().into());
1369 }
1370 SqlDialect::Sqlite => {
1371 match &col.type_definition {
1375 FieldType::BigInteger | FieldType::Integer | FieldType::SmallInteger => {
1376 parts.push("INTEGER".to_string().into());
1377 }
1378 _ => {
1379 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1383 }
1384 }
1385 }
1388 }
1389 } else {
1390 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1391 }
1392
1393 if col.not_null {
1395 parts.push("NOT NULL".to_string().into());
1396 }
1397
1398 if col.unique {
1400 parts.push("UNIQUE".to_string().into());
1401 }
1402
1403 if let Some(default) = &col.default {
1405 parts.push(format!("DEFAULT {}", default).into());
1406 }
1407
1408 parts.join(" ")
1409 }
1410
1411 fn column_to_sql(col: &ColumnDefinition, dialect: &SqlDialect) -> String {
1413 let mut parts = Vec::new();
1414
1415 parts.push(quote_identifier(&col.name));
1417
1418 if col.auto_increment {
1420 match dialect {
1421 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
1422 match &col.type_definition {
1424 FieldType::BigInteger => {
1425 parts
1426 .push("BIGINT GENERATED BY DEFAULT AS IDENTITY".to_string().into());
1427 }
1428 FieldType::Integer => {
1429 parts.push(
1430 "INTEGER GENERATED BY DEFAULT AS IDENTITY"
1431 .to_string()
1432 .into(),
1433 );
1434 }
1435 FieldType::SmallInteger => {
1436 parts.push(
1437 "SMALLINT GENERATED BY DEFAULT AS IDENTITY"
1438 .to_string()
1439 .into(),
1440 );
1441 }
1442 _ => {
1443 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1445 }
1446 }
1447 }
1448 SqlDialect::Mysql => {
1449 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1450 parts.push("AUTO_INCREMENT".to_string().into());
1451 }
1452 SqlDialect::Sqlite => {
1453 let widened_to_integer = matches!(
1464 &col.type_definition,
1465 FieldType::BigInteger | FieldType::Integer | FieldType::SmallInteger
1466 );
1467 if widened_to_integer {
1468 parts.push("INTEGER".to_string().into());
1469 } else {
1470 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1471 }
1472 if col.primary_key {
1477 if widened_to_integer {
1478 parts.push("PRIMARY KEY AUTOINCREMENT".to_string().into());
1479 } else {
1480 parts.push("PRIMARY KEY".to_string().into());
1481 }
1482 if col.unique {
1484 parts.push("UNIQUE".to_string().into());
1485 }
1486 if let Some(default) = &col.default {
1487 parts.push(format!("DEFAULT {}", default).into());
1488 }
1489 return parts.join(" ");
1490 }
1491 }
1492 }
1493 } else {
1494 parts.push(col.type_definition.to_sql_for_dialect(dialect).into());
1495 }
1496
1497 if col.not_null {
1499 parts.push("NOT NULL".to_string().into());
1500 }
1501
1502 if col.primary_key {
1504 parts.push("PRIMARY KEY".to_string().into());
1505 }
1506
1507 if col.unique {
1509 parts.push("UNIQUE".to_string().into());
1510 }
1511
1512 if let Some(default) = &col.default {
1514 parts.push(format!("DEFAULT {}", default).into());
1515 }
1516
1517 parts.join(" ")
1518 }
1519
1520 pub fn to_sql(&self, dialect: &SqlDialect) -> String {
1522 match self {
1523 Operation::CreateTable {
1524 name,
1525 columns,
1526 constraints,
1527 without_rowid,
1528 interleave_in_parent,
1529 partition,
1530 } => {
1531 let pk_columns: Vec<&String> = columns
1533 .iter()
1534 .filter(|col| col.primary_key)
1535 .map(|col| &col.name)
1536 .collect();
1537 let has_composite_pk = pk_columns.len() > 1;
1538
1539 let mut parts = Vec::new();
1540 for col in columns {
1541 if has_composite_pk {
1543 parts.push(format!(
1544 " {}",
1545 Self::column_to_sql_without_pk(col, dialect)
1546 ));
1547 } else {
1548 parts.push(format!(" {}", Self::column_to_sql(col, dialect)));
1549 }
1550 }
1551
1552 if has_composite_pk {
1554 let pk_constraint_name = format!("{}_pkey", name);
1555 let quoted_pk_columns = pk_columns
1556 .iter()
1557 .map(|s| quote_identifier(s))
1558 .collect::<Vec<_>>()
1559 .join(", ");
1560 let pk_constraint = format!(
1561 " CONSTRAINT {} PRIMARY KEY ({})",
1562 quote_identifier(&pk_constraint_name),
1563 quoted_pk_columns
1564 );
1565 parts.push(pk_constraint);
1566 }
1567
1568 for constraint in constraints {
1569 parts.push(format!(" {}", constraint));
1570 }
1571 let mut sql = format!(
1572 "CREATE TABLE {} (\n{}\n)",
1573 quote_identifier(name),
1574 parts.join(",\n")
1575 );
1576
1577 if matches!(dialect, SqlDialect::Sqlite)
1579 && let Some(true) = without_rowid
1580 {
1581 sql.push_str(" WITHOUT ROWID");
1582 }
1583
1584 if matches!(dialect, SqlDialect::Mysql)
1586 && let Some(partition_opts) = partition
1587 {
1588 sql.push(' ');
1589 sql.push_str(&partition_opts.to_sql());
1590 }
1591
1592 if matches!(dialect, SqlDialect::Cockroachdb)
1594 && let Some(interleave) = interleave_in_parent
1595 {
1596 let quoted_columns = interleave
1597 .parent_columns
1598 .iter()
1599 .map(|col| quote_identifier(col))
1600 .collect::<Vec<_>>()
1601 .join(", ");
1602 sql.push_str(&format!(
1603 " INTERLEAVE IN PARENT {} ({})",
1604 quote_identifier(&interleave.parent_table),
1605 quoted_columns
1606 ));
1607 }
1608
1609 sql.push(';');
1610 sql
1611 }
1612 Operation::DropTable { name } => format!("DROP TABLE {};", quote_identifier(name)),
1613 Operation::AddColumn {
1614 table,
1615 column,
1616 mysql_options,
1617 } => {
1618 let base_sql = format!(
1619 "ALTER TABLE {} ADD COLUMN {}",
1620 quote_identifier(table),
1621 Self::column_to_sql(column, dialect)
1622 );
1623
1624 if matches!(dialect, SqlDialect::Mysql)
1626 && let Some(opts) = mysql_options
1627 {
1628 let suffix = opts.to_sql_suffix();
1629 if !suffix.is_empty() {
1630 return format!("{}{};", base_sql, suffix);
1631 }
1632 }
1633
1634 format!("{};", base_sql)
1635 }
1636 Operation::DropColumn { table, column } => {
1637 format!(
1638 "ALTER TABLE {} DROP COLUMN {};",
1639 quote_identifier(table),
1640 quote_identifier(column)
1641 )
1642 }
1643 Operation::AlterColumn {
1644 table,
1645 column,
1646 new_definition,
1647 mysql_options,
1648 ..
1649 } => {
1650 let sql_type = new_definition.type_definition.to_sql_for_dialect(dialect);
1651 match dialect {
1652 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
1653 format!(
1654 "ALTER TABLE {} ALTER COLUMN {} TYPE {};",
1655 quote_identifier(table),
1656 quote_identifier(column),
1657 sql_type
1658 )
1659 }
1660 SqlDialect::Mysql => {
1661 let base_sql = format!(
1662 "ALTER TABLE {} MODIFY COLUMN {} {}",
1663 quote_identifier(table),
1664 quote_identifier(column),
1665 sql_type
1666 );
1667
1668 if let Some(opts) = mysql_options {
1670 let suffix = opts.to_sql_suffix();
1671 if !suffix.is_empty() {
1672 return format!("{}{};", base_sql, suffix);
1673 }
1674 }
1675
1676 format!("{};", base_sql)
1677 }
1678 SqlDialect::Sqlite => {
1679 format!(
1680 "-- SQLite does not support ALTER COLUMN, table recreation required for {}",
1681 quote_identifier(table)
1682 )
1683 }
1684 }
1685 }
1686 Operation::RenameColumn {
1687 table,
1688 old_name,
1689 new_name,
1690 } => {
1691 format!(
1692 "ALTER TABLE {} RENAME COLUMN {} TO {};",
1693 quote_identifier(table),
1694 quote_identifier(old_name),
1695 quote_identifier(new_name)
1696 )
1697 }
1698 Operation::RenameTable { old_name, new_name } => {
1699 format!(
1700 "ALTER TABLE {} RENAME TO {};",
1701 quote_identifier(old_name),
1702 quote_identifier(new_name)
1703 )
1704 }
1705 Operation::AddConstraint {
1706 table,
1707 constraint_sql,
1708 } => {
1709 format!(
1710 "ALTER TABLE {} ADD {};",
1711 quote_identifier(table),
1712 constraint_sql
1713 )
1714 }
1715 Operation::DropConstraint {
1716 table,
1717 constraint_name,
1718 } => {
1719 format!(
1720 "ALTER TABLE {} DROP CONSTRAINT {};",
1721 quote_identifier(table),
1722 quote_identifier(constraint_name)
1723 )
1724 }
1725 Operation::CreateIndex {
1726 table,
1727 columns,
1728 unique,
1729 index_type,
1730 where_clause,
1731 concurrently,
1732 expressions,
1733 mysql_options,
1734 operator_class,
1735 } => {
1736 let unique_str = if *unique { "UNIQUE " } else { "" };
1737
1738 let concurrent_str = if *concurrently && matches!(dialect, SqlDialect::Postgres) {
1740 "CONCURRENTLY "
1741 } else {
1742 ""
1743 };
1744
1745 let (mysql_prefix, effective_unique) = match (index_type, dialect) {
1747 (Some(IndexType::Fulltext), SqlDialect::Mysql) => ("FULLTEXT ", ""),
1748 (Some(IndexType::Spatial), SqlDialect::Mysql) => ("SPATIAL ", ""),
1749 _ => ("", unique_str),
1750 };
1751
1752 let (index_content, name_suffix) =
1754 if let Some(exprs) = expressions.as_ref().filter(|e| !e.is_empty()) {
1755 let content = exprs.join(", ");
1758 let suffix = "expr";
1759 (content, suffix.to_string())
1760 } else {
1761 let content = if let Some(op_class) = operator_class {
1763 if matches!(dialect, SqlDialect::Postgres) {
1765 columns
1766 .iter()
1767 .map(|c| format!("{} {}", quote_identifier(c), op_class))
1768 .collect::<Vec<_>>()
1769 .join(", ")
1770 } else {
1771 columns
1773 .iter()
1774 .map(|c| quote_identifier(c).to_string())
1775 .collect::<Vec<_>>()
1776 .join(", ")
1777 }
1778 } else {
1779 columns
1781 .iter()
1782 .map(|c| quote_identifier(c).to_string())
1783 .collect::<Vec<_>>()
1784 .join(", ")
1785 };
1786 (content, columns.join("_"))
1787 };
1788
1789 let idx_name = format!("idx_{}_{}", table, name_suffix);
1790
1791 let using_clause = match (index_type, dialect) {
1793 (Some(IndexType::BTree), _) => String::new(), (Some(idx_type), SqlDialect::Postgres | SqlDialect::Cockroachdb) => {
1795 format!(" USING {}", idx_type)
1796 }
1797 (Some(IndexType::Fulltext | IndexType::Spatial), SqlDialect::Mysql) => {
1799 String::new()
1800 }
1801 _ => String::new(),
1802 };
1803
1804 let mut sql = match dialect {
1809 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
1810 format!(
1812 "CREATE {}INDEX {}{}",
1813 effective_unique,
1814 concurrent_str,
1815 quote_identifier(&idx_name)
1816 )
1817 }
1818 SqlDialect::Mysql => {
1819 format!(
1821 "CREATE {}{}INDEX {}",
1822 mysql_prefix,
1823 effective_unique,
1824 quote_identifier(&idx_name)
1825 )
1826 }
1827 SqlDialect::Sqlite => {
1828 format!(
1830 "CREATE {}INDEX {}",
1831 effective_unique,
1832 quote_identifier(&idx_name)
1833 )
1834 }
1835 };
1836 sql.push_str(&format!(
1840 " ON {}{} ({})",
1841 quote_identifier(table),
1842 using_clause,
1843 index_content
1844 ));
1845
1846 if let Some(where_cond) = where_clause
1848 && !matches!(dialect, SqlDialect::Mysql)
1849 {
1850 sql.push_str(&format!(" WHERE {}", where_cond));
1851 }
1852
1853 if matches!(dialect, SqlDialect::Mysql)
1855 && let Some(opts) = mysql_options
1856 {
1857 let suffix = opts.to_sql_suffix();
1858 if !suffix.is_empty() {
1859 sql.push_str(&suffix);
1860 }
1861 }
1862
1863 sql.push(';');
1864 sql
1865 }
1866 Operation::DropIndex { table, columns } => {
1867 let idx_name = format!("idx_{}_{}", table, columns.join("_"));
1868 match dialect {
1869 SqlDialect::Mysql => {
1870 format!(
1871 "DROP INDEX {} ON {};",
1872 quote_identifier(&idx_name),
1873 quote_identifier(table)
1874 )
1875 }
1876 SqlDialect::Postgres | SqlDialect::Sqlite | SqlDialect::Cockroachdb => {
1877 format!("DROP INDEX {};", quote_identifier(&idx_name))
1878 }
1879 }
1880 }
1881 Operation::RunSQL { sql, .. } => sql.to_string(),
1882 Operation::RunRust { code, .. } => {
1883 format!("-- RunRust: {}", code.lines().next().unwrap_or(""))
1885 }
1886 Operation::AlterTableComment { table, comment } => match dialect {
1887 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
1888 if let Some(comment_text) = comment {
1889 format!(
1890 "COMMENT ON TABLE {} IS '{}';",
1891 quote_identifier(table),
1892 comment_text
1893 )
1894 } else {
1895 format!("COMMENT ON TABLE {} IS NULL;", quote_identifier(table))
1896 }
1897 }
1898 SqlDialect::Mysql => {
1899 if let Some(comment_text) = comment {
1900 format!(
1901 "ALTER TABLE {} COMMENT='{}';",
1902 quote_identifier(table),
1903 comment_text
1904 )
1905 } else {
1906 format!("ALTER TABLE {} COMMENT='';", quote_identifier(table))
1907 }
1908 }
1909 SqlDialect::Sqlite => String::new(),
1910 },
1911 Operation::AlterUniqueTogether {
1912 table,
1913 unique_together,
1914 } => {
1915 let mut sql = Vec::new();
1916 for (idx, fields) in unique_together.iter().enumerate() {
1917 let constraint_name = format!("{}_{}_uniq", table, idx);
1918 let fields_str = fields
1919 .iter()
1920 .map(|f| quote_identifier(f))
1921 .collect::<Vec<_>>()
1922 .join(", ");
1923 sql.push(format!(
1924 "ALTER TABLE {} ADD CONSTRAINT {} UNIQUE ({});",
1925 quote_identifier(table),
1926 quote_identifier(&constraint_name),
1927 fields_str
1928 ));
1929 }
1930 sql.join("\n")
1931 }
1932 Operation::AlterModelOptions { .. } => String::new(),
1933 Operation::CreateInheritedTable {
1934 name,
1935 columns,
1936 base_table,
1937 join_column,
1938 } => {
1939 let mut parts = Vec::new();
1940 parts.push(format!(
1941 " {} INTEGER REFERENCES {}(id)",
1942 quote_identifier(join_column),
1943 quote_identifier(base_table)
1944 ));
1945 for col in columns {
1946 parts.push(format!(" {}", Self::column_to_sql(col, dialect)));
1947 }
1948 format!(
1949 "CREATE TABLE {} (\n{}\n);",
1950 quote_identifier(name),
1951 parts.join(",\n")
1952 )
1953 }
1954 Operation::AddDiscriminatorColumn {
1955 table,
1956 column_name,
1957 default_value,
1958 } => {
1959 format!(
1960 "ALTER TABLE {} ADD COLUMN {} VARCHAR(50) DEFAULT '{}';",
1961 quote_identifier(table),
1962 quote_identifier(column_name),
1963 default_value
1964 )
1965 }
1966 Operation::MoveModel {
1967 rename_table,
1968 old_table_name,
1969 new_table_name,
1970 ..
1971 } => {
1972 if *rename_table {
1975 if let (Some(old_name), Some(new_name)) = (old_table_name, new_table_name) {
1976 match dialect {
1977 SqlDialect::Postgres | SqlDialect::Sqlite | SqlDialect::Cockroachdb => {
1978 format!(
1979 "ALTER TABLE {} RENAME TO {};",
1980 quote_identifier(old_name),
1981 quote_identifier(new_name)
1982 )
1983 }
1984 SqlDialect::Mysql => {
1985 format!(
1986 "RENAME TABLE {} TO {};",
1987 quote_identifier(old_name),
1988 quote_identifier(new_name)
1989 )
1990 }
1991 }
1992 } else {
1993 "-- MoveModel: No table rename specified".to_string()
1994 }
1995 } else {
1996 "-- MoveModel: State-only operation (no table rename)".to_string()
1998 }
1999 }
2000 Operation::CreateSchema {
2001 name,
2002 if_not_exists,
2003 } => {
2004 let if_not_exists_clause = if *if_not_exists { " IF NOT EXISTS" } else { "" };
2005 format!(
2006 "CREATE SCHEMA{} {};",
2007 if_not_exists_clause,
2008 quote_identifier(name)
2009 )
2010 }
2011 Operation::DropSchema {
2012 name,
2013 cascade,
2014 if_exists,
2015 } => {
2016 let if_exists_clause = if *if_exists { " IF EXISTS" } else { "" };
2017 let cascade_clause = if *cascade { " CASCADE" } else { "" };
2018 format!(
2019 "DROP SCHEMA{} {}{};",
2020 if_exists_clause,
2021 quote_identifier(name),
2022 cascade_clause
2023 )
2024 }
2025 Operation::CreateExtension {
2026 name,
2027 if_not_exists,
2028 schema,
2029 } => {
2030 let if_not_exists_clause = if *if_not_exists { " IF NOT EXISTS" } else { "" };
2032 let schema_clause = if let Some(s) = schema {
2033 format!(" SCHEMA {}", quote_identifier(s))
2034 } else {
2035 String::new()
2036 };
2037 format!(
2038 "CREATE EXTENSION{} {}{};",
2039 if_not_exists_clause,
2040 quote_identifier(name),
2041 schema_clause
2042 )
2043 }
2044 Operation::BulkLoad {
2045 table,
2046 source,
2047 format,
2048 options,
2049 } => Self::bulk_load_to_sql(table, source, format, options, dialect),
2050 Operation::SetAutoIncrementValue {
2051 table,
2052 column,
2053 value,
2054 } => Self::set_auto_increment_to_sql(table, column, *value, dialect),
2055 Operation::CreateCompositePrimaryKey {
2056 table,
2057 columns,
2058 constraint_name,
2059 } => Self::create_composite_pk_to_sql(table, columns, constraint_name.as_deref()),
2060 }
2061 }
2062
2063 fn set_auto_increment_to_sql(
2070 table: &str,
2071 column: &str,
2072 value: i64,
2073 dialect: &SqlDialect,
2074 ) -> String {
2075 match dialect {
2076 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
2077 format!(
2082 "SELECT setval(pg_get_serial_sequence({}, {}), {}, false);",
2083 quote_literal(table),
2084 quote_literal(column),
2085 value
2086 )
2087 }
2088 SqlDialect::Mysql => {
2089 format!(
2090 "ALTER TABLE {} AUTO_INCREMENT = {};",
2091 quote_identifier(table),
2092 value
2093 )
2094 }
2095 SqlDialect::Sqlite => {
2096 format!(
2101 "INSERT OR REPLACE INTO sqlite_sequence(name, seq) VALUES ({}, {});",
2102 quote_literal(table),
2103 value
2104 )
2105 }
2106 }
2107 }
2108
2109 fn create_composite_pk_to_sql(
2146 table: &str,
2147 columns: &[String],
2148 constraint_name: Option<&str>,
2149 ) -> String {
2150 if columns.is_empty() {
2151 return format!(
2158 "SYNTAX_ERROR_create_composite_pk_on_{}_requires_at_least_one_column;",
2159 table.replace(|c: char| !c.is_ascii_alphanumeric(), "_")
2160 );
2161 }
2162
2163 let default_name;
2164 let name: &str = match constraint_name {
2165 Some(n) => n,
2166 None => {
2167 default_name = format!("{}_pkey", table);
2168 &default_name
2169 }
2170 };
2171
2172 let quoted_columns = columns
2173 .iter()
2174 .map(|c| quote_identifier(c).to_string())
2175 .collect::<Vec<_>>()
2176 .join(", ");
2177
2178 format!(
2179 "ALTER TABLE {} ADD CONSTRAINT {} PRIMARY KEY ({});",
2180 quote_identifier(table),
2181 quote_identifier(name),
2182 quoted_columns
2183 )
2184 }
2185
2186 fn bulk_load_to_sql(
2188 table: &str,
2189 source: &BulkLoadSource,
2190 format: &BulkLoadFormat,
2191 options: &BulkLoadOptions,
2192 dialect: &SqlDialect,
2193 ) -> String {
2194 match dialect {
2195 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
2196 Self::postgres_copy_from_sql(table, source, format, options)
2197 }
2198 SqlDialect::Mysql => Self::mysql_load_data_sql(table, source, format, options),
2199 SqlDialect::Sqlite => {
2200 format!(
2202 "-- SQLite does not support bulk loading. Use INSERT statements instead for table {}",
2203 quote_identifier(table)
2204 )
2205 }
2206 }
2207 }
2208
2209 fn postgres_copy_from_sql(
2211 table: &str,
2212 source: &BulkLoadSource,
2213 format: &BulkLoadFormat,
2214 options: &BulkLoadOptions,
2215 ) -> String {
2216 let source_clause = match source {
2217 BulkLoadSource::File(path) => format!("'{}'", path),
2218 BulkLoadSource::Stdin => "STDIN".to_string(),
2219 BulkLoadSource::Program(cmd) => format!("PROGRAM '{}'", cmd),
2220 };
2221
2222 let columns_clause = if let Some(cols) = &options.columns {
2223 let quoted_cols = cols
2224 .iter()
2225 .map(|c| quote_identifier(c))
2226 .collect::<Vec<_>>()
2227 .join(", ");
2228 format!(" ({})", quoted_cols)
2229 } else {
2230 String::new()
2231 };
2232
2233 let mut with_options = Vec::new();
2234
2235 with_options.push(format!("FORMAT {}", format));
2237
2238 if let Some(delim) = options.delimiter {
2240 with_options.push(format!("DELIMITER '{}'", delim));
2241 }
2242
2243 if let Some(null_str) = &options.null_string {
2245 with_options.push(format!("NULL '{}'", null_str));
2246 }
2247
2248 if options.header {
2250 with_options.push("HEADER true".to_string());
2251 }
2252
2253 if let Some(quote) = options.quote {
2255 with_options.push(format!("QUOTE '{}'", quote));
2256 }
2257
2258 if let Some(escape) = options.escape {
2260 with_options.push(format!("ESCAPE '{}'", escape));
2261 }
2262
2263 format!(
2264 "COPY {}{} FROM {} WITH ({});",
2265 quote_identifier(table),
2266 columns_clause,
2267 source_clause,
2268 with_options.join(", ")
2269 )
2270 }
2271
2272 fn mysql_load_data_sql(
2274 table: &str,
2275 source: &BulkLoadSource,
2276 format: &BulkLoadFormat,
2277 options: &BulkLoadOptions,
2278 ) -> String {
2279 let local_clause = if options.local { " LOCAL" } else { "" };
2280
2281 let file_path = match source {
2282 BulkLoadSource::File(path) => path.clone(),
2283 BulkLoadSource::Stdin => {
2284 return format!(
2285 "-- MySQL does not support LOAD DATA from STDIN directly for table {}",
2286 quote_identifier(table)
2287 );
2288 }
2289 BulkLoadSource::Program(_) => {
2290 return format!(
2291 "-- MySQL does not support LOAD DATA from PROGRAM directly for table {}",
2292 quote_identifier(table)
2293 );
2294 }
2295 };
2296
2297 let columns_clause = if let Some(cols) = &options.columns {
2298 let quoted_cols = cols
2299 .iter()
2300 .map(|c| quote_identifier(c))
2301 .collect::<Vec<_>>()
2302 .join(", ");
2303 format!(" ({})", quoted_cols)
2304 } else {
2305 String::new()
2306 };
2307
2308 let delimiter = options.delimiter.unwrap_or(match format {
2310 BulkLoadFormat::Csv => ',',
2311 BulkLoadFormat::Text | BulkLoadFormat::Binary => '\t',
2312 });
2313
2314 let mut field_options = Vec::new();
2315 field_options.push(format!("TERMINATED BY '{}'", delimiter));
2316
2317 if *format == BulkLoadFormat::Csv {
2319 let quote = options.quote.unwrap_or('"');
2320 field_options.push(format!("ENCLOSED BY '{}'", quote));
2321 }
2322
2323 if let Some(escape) = options.escape {
2325 field_options.push(format!("ESCAPED BY '{}'", escape));
2326 }
2327
2328 let line_terminator = options
2330 .line_terminator
2331 .clone()
2332 .unwrap_or_else(|| "\\n".to_string());
2333
2334 let encoding_clause = if let Some(enc) = &options.encoding {
2336 format!(" CHARACTER SET {}", enc)
2337 } else {
2338 String::new()
2339 };
2340
2341 let ignore_clause = if options.header {
2343 " IGNORE 1 LINES"
2344 } else {
2345 ""
2346 };
2347
2348 format!(
2349 "LOAD DATA{} INFILE '{}'{} INTO TABLE {} FIELDS {} LINES TERMINATED BY '{}'{}{};",
2350 local_clause,
2351 file_path,
2352 encoding_clause,
2353 quote_identifier(table),
2354 field_options.join(" "),
2355 line_terminator,
2356 ignore_clause,
2357 columns_clause
2358 )
2359 }
2360
2361 pub fn to_reverse_sql(
2393 &self,
2394 dialect: &SqlDialect,
2395 project_state: &ProjectState,
2396 ) -> super::Result<Option<Vec<String>>> {
2397 match self {
2398 Operation::CreateTable { name, .. } => Ok(Some(vec![format!(
2399 "DROP TABLE {};",
2400 quote_identifier(name)
2401 )])),
2402 Operation::AddColumn { table, column, .. } => Ok(Some(vec![format!(
2403 "ALTER TABLE {} DROP COLUMN {};",
2404 quote_identifier(table),
2405 quote_identifier(&column.name)
2406 )])),
2407 Operation::RunSQL { reverse_sql, .. } => {
2408 Ok(reverse_sql.as_ref().map(|s| vec![s.to_string()]))
2409 }
2410 Operation::RunRust { reverse_code, .. } => Ok(reverse_code.as_ref().map(|code| {
2411 vec![format!(
2412 "-- RunRust (reverse): {}",
2413 code.lines().next().unwrap_or("")
2414 )]
2415 })),
2416 Operation::RenameTable { old_name, new_name } => Ok(Some(vec![format!(
2418 "ALTER TABLE {} RENAME TO {};",
2419 quote_identifier(new_name),
2420 quote_identifier(old_name)
2421 )])),
2422 Operation::RenameColumn {
2423 table,
2424 old_name,
2425 new_name,
2426 } => Ok(Some(vec![format!(
2427 "ALTER TABLE {} RENAME COLUMN {} TO {};",
2428 quote_identifier(table),
2429 quote_identifier(new_name),
2430 quote_identifier(old_name)
2431 )])),
2432 Operation::CreateIndex { table, columns, .. } => {
2433 let columns_joined = columns.join("_");
2436 let index_name = format!("idx_{}_{}", table, columns_joined);
2437 let sql = match dialect {
2441 SqlDialect::Mysql => format!(
2442 "DROP INDEX {} ON {};",
2443 quote_identifier(&index_name),
2444 quote_identifier(table)
2445 ),
2446 SqlDialect::Postgres | SqlDialect::Sqlite | SqlDialect::Cockroachdb => {
2447 format!("DROP INDEX {};", quote_identifier(&index_name))
2448 }
2449 };
2450 Ok(Some(vec![sql]))
2451 }
2452 Operation::AddConstraint {
2453 table,
2454 constraint_sql,
2455 } => {
2456 let constraint_name =
2459 Self::extract_constraint_name(constraint_sql).ok_or_else(|| {
2460 super::MigrationError::InvalidMigration(format!(
2461 "Cannot extract constraint name from: {}",
2462 constraint_sql
2463 ))
2464 })?;
2465 Ok(Some(vec![format!(
2466 "ALTER TABLE {} DROP CONSTRAINT {};",
2467 quote_identifier(table),
2468 quote_identifier(&constraint_name)
2469 )]))
2470 }
2471 Operation::DropColumn { table, column } => {
2473 if let Some(model) = project_state.find_model_by_table(table)
2475 && let Some(field) = model.get_field(column)
2476 {
2477 let col_def = ColumnDefinition::from_field_state(column.clone(), field);
2478 let col_sql = Self::column_to_sql(&col_def, dialect);
2479 return Ok(Some(vec![format!(
2480 "ALTER TABLE {} ADD COLUMN {};",
2481 quote_identifier(table),
2482 col_sql
2483 )]));
2484 }
2485 Ok(None)
2487 }
2488 Operation::AlterColumn {
2489 table,
2490 column,
2491 old_definition,
2492 new_definition: _,
2493 ..
2494 } => {
2495 let resolved_old_def = old_definition.clone().or_else(|| {
2498 project_state
2499 .find_model_by_table(table)
2500 .and_then(|model| model.get_field(column))
2501 .map(|field| ColumnDefinition::from_field_state(column.clone(), field))
2502 });
2503
2504 let Some(old_def) = resolved_old_def else {
2505 return Ok(None);
2507 };
2508
2509 let type_sql = old_def.type_definition.to_sql_for_dialect(dialect);
2510 let null_clause = if old_def.not_null { " NOT NULL" } else { "" };
2511
2512 let stmts = match dialect {
2519 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
2520 let nullability_clause = if old_def.not_null {
2535 "SET NOT NULL"
2536 } else {
2537 "DROP NOT NULL"
2538 };
2539 vec![
2540 format!(
2541 "ALTER TABLE {table} ALTER COLUMN {column} TYPE {type_sql};",
2542 table = quote_identifier(table),
2543 column = quote_identifier(column),
2544 type_sql = type_sql,
2545 ),
2546 format!(
2547 "ALTER TABLE {table} ALTER COLUMN {column} {nullability_clause};",
2548 table = quote_identifier(table),
2549 column = quote_identifier(column),
2550 nullability_clause = nullability_clause,
2551 ),
2552 ]
2553 }
2554 SqlDialect::Mysql => vec![format!(
2555 "ALTER TABLE {} MODIFY COLUMN {} {}{};",
2556 quote_identifier(table),
2557 quote_identifier(column),
2558 type_sql,
2559 null_clause
2560 )],
2561 SqlDialect::Sqlite => vec![format!(
2562 "-- SQLite does not support ALTER COLUMN, table recreation required for {}",
2563 quote_identifier(table)
2564 )],
2565 };
2566 Ok(Some(stmts))
2567 }
2568 Operation::DropIndex { table, columns } => {
2569 let columns_joined = columns.join("_");
2573 let index_name = format!("idx_{}_{}", table, columns_joined);
2574 let columns_list = columns
2575 .iter()
2576 .map(|c| quote_identifier(c).to_string())
2577 .collect::<Vec<_>>()
2578 .join(", ");
2579 Ok(Some(vec![format!(
2580 "CREATE INDEX {} ON {} ({});",
2581 quote_identifier(&index_name),
2582 quote_identifier(table),
2583 columns_list
2584 )]))
2585 }
2586 Operation::DropConstraint {
2587 table,
2588 constraint_name,
2589 } => {
2590 if let Some(model) = project_state.find_model_by_table(table)
2592 && let Some(constraint_def) = model
2593 .constraints
2594 .iter()
2595 .find(|c| c.name == *constraint_name)
2596 {
2597 let constraint = constraint_def.to_constraint();
2598 return Ok(Some(vec![format!(
2599 "ALTER TABLE {} ADD {};",
2600 quote_identifier(table),
2601 constraint
2602 )]));
2603 }
2604 Ok(None)
2606 }
2607 Operation::DropTable { name } => {
2608 if let Some(model) = project_state.find_model_by_table(name) {
2610 let mut parts = Vec::new();
2611
2612 for (field_name, field) in &model.fields {
2614 let col_def = ColumnDefinition::from_field_state(field_name.clone(), field);
2615 parts.push(format!(" {}", Self::column_to_sql(&col_def, dialect)));
2616 }
2617
2618 for constraint_def in &model.constraints {
2620 let constraint = constraint_def.to_constraint();
2621 parts.push(format!(" {}", constraint));
2622 }
2623
2624 return Ok(Some(vec![format!(
2625 "CREATE TABLE {} (\n{}\n);",
2626 quote_identifier(name),
2627 parts.join(",\n")
2628 )]));
2629 }
2630 Ok(None)
2632 }
2633 Operation::BulkLoad { table, .. } => {
2634 Ok(Some(vec![format!(
2637 "TRUNCATE TABLE {};",
2638 quote_identifier(table)
2639 )]))
2640 }
2641 _ => Ok(None),
2642 }
2643 }
2644
2645 pub fn state_backwards(&self, app_label: &str, state: &mut ProjectState) {
2665 match self {
2666 Operation::CreateTable { name, .. } => {
2667 state
2669 .models
2670 .remove(&(app_label.to_string(), name.to_string()));
2671 }
2672 Operation::DropTable { name: _ } => {
2673 }
2676 Operation::RenameTable { old_name, new_name } => {
2677 if let Some(mut model) = state
2679 .models
2680 .remove(&(app_label.to_string(), new_name.to_string()))
2681 {
2682 model.table_name = old_name.to_string();
2683 state
2684 .models
2685 .insert((app_label.to_string(), old_name.to_string()), model);
2686 }
2687 }
2688 Operation::AddColumn { table, column, .. } => {
2689 if let Some(model) = state.find_model_by_table_mut(table) {
2691 model.remove_field(&column.name);
2692 }
2693 }
2694 Operation::DropColumn {
2695 table: _,
2696 column: _,
2697 } => {
2698 }
2701 Operation::AlterColumn {
2702 table: _,
2703 column: _,
2704 ..
2705 } => {
2706 }
2709 Operation::RenameColumn {
2710 table,
2711 old_name,
2712 new_name,
2713 } => {
2714 if let Some(model) = state.find_model_by_table_mut(table) {
2716 model.rename_field(new_name, old_name.to_string());
2717 }
2718 }
2719 Operation::AddConstraint { table, .. } => {
2720 if let Some(model) = state.find_model_by_table_mut(table) {
2723 let _ = model;
2726 }
2727 }
2728 Operation::DropConstraint {
2729 table: _,
2730 constraint_name: _,
2731 } => {
2732 }
2735 _ => {
2736 }
2738 }
2739 }
2740
2741 fn extract_constraint_name(constraint_sql: &str) -> Option<String> {
2747 let sql = constraint_sql.trim();
2748
2749 if sql.starts_with("CONSTRAINT ") || sql.contains(" CONSTRAINT ") {
2751 let parts: Vec<&str> = sql.split_whitespace().collect();
2752 if let Some(pos) = parts.iter().position(|&s| s == "CONSTRAINT")
2753 && pos + 1 < parts.len()
2754 {
2755 return Some(parts[pos + 1].to_string());
2756 }
2757 }
2758
2759 None
2760 }
2761}
2762
2763#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2765pub struct ColumnDefinition {
2766 pub name: String,
2768 pub type_definition: FieldType,
2770 #[serde(default)]
2771 pub not_null: bool,
2773 #[serde(default)]
2774 pub unique: bool,
2776 #[serde(default)]
2777 pub primary_key: bool,
2779 #[serde(default)]
2780 pub auto_increment: bool,
2782 #[serde(default)]
2783 pub default: Option<String>,
2785}
2786
2787impl ColumnDefinition {
2788 pub fn new(name: impl Into<String>, type_def: FieldType) -> Self {
2790 Self {
2791 name: name.into(),
2792 type_definition: type_def,
2793 not_null: false,
2794 unique: false,
2795 primary_key: false,
2796 auto_increment: false,
2797 default: None,
2798 }
2799 }
2800
2801 pub fn from_field_state(name: impl Into<String>, field_state: &FieldState) -> Self {
2820 let name_str = name.into();
2821 let params = &field_state.params;
2822
2823 let primary_key = params
2825 .get("primary_key")
2826 .and_then(|v| v.parse::<bool>().ok())
2827 .unwrap_or(false);
2828
2829 let not_null = !field_state.nullable || primary_key;
2846
2847 let unique = params
2848 .get("unique")
2849 .and_then(|v| v.parse::<bool>().ok())
2850 .unwrap_or(false);
2851
2852 let auto_increment = params
2853 .get("auto_increment")
2854 .and_then(|v| v.parse::<bool>().ok())
2855 .unwrap_or(false);
2856
2857 let default = params.get("default").cloned();
2858
2859 let type_definition = resolve_foreign_key_column_type(field_state)
2872 .unwrap_or_else(|| field_state.field_type.clone());
2873
2874 Self {
2875 name: name_str,
2876 type_definition,
2877 not_null,
2878 unique,
2879 primary_key,
2880 auto_increment,
2881 default,
2882 }
2883 }
2884}
2885
2886fn resolve_foreign_key_column_type(field_state: &FieldState) -> Option<FieldType> {
2928 resolve_foreign_key_column_type_with(field_state, super::model_registry::global_registry())
2929}
2930
2931fn resolve_foreign_key_column_type_with(
2939 field_state: &FieldState,
2940 registry: &super::model_registry::ModelRegistry,
2941) -> Option<FieldType> {
2942 let target_model = field_state.params.get("fk_target")?;
2943 let target = match field_state.params.get("fk_target_app") {
2949 Some(app) => registry
2950 .find_model_qualified(app, target_model)
2951 .or_else(|| registry.find_model_by_name(target_model)),
2952 None => registry.find_model_by_name(target_model),
2953 };
2954 let target = match target {
2955 Some(t) => t,
2956 None => {
2957 if registry.count_models_by_name(target_model) > 1 {
2962 tracing::warn!(
2963 model_name = %target_model,
2964 fk_target_app = ?field_state.params.get("fk_target_app"),
2965 "FK target name is ambiguous across apps and the qualified \
2966 lookup did not resolve a unique target. Refusing to resolve \
2967 to avoid silent wrong-target resolution. Ensure the FK \
2968 target type is registered and that its `Model::app_label()` \
2969 matches one of the registered apps.",
2970 );
2971 }
2972 return None;
2973 }
2974 };
2975 let pk_field = target
2977 .fields
2978 .values()
2979 .find(|f| f.params.get("primary_key").map(String::as_str) == Some("true"))?;
2980 Some(pk_field.field_type.clone())
2981}
2982
2983pub fn field_type_string_to_field_type(
3011 field_type: &str,
3012 attributes: &std::collections::HashMap<String, String>,
3013) -> Result<FieldType, String> {
3014 let type_name = field_type.split('.').next_back().unwrap_or(field_type);
3016
3017 match type_name {
3018 "IntegerField"
3020 | "PositiveIntegerField"
3021 | "SmallIntegerField"
3022 | "PositiveSmallIntegerField" => Ok(FieldType::Integer),
3023 "BigIntegerField" | "PositiveBigIntegerField" => Ok(FieldType::BigInteger),
3024 "AutoField" => Ok(FieldType::Integer),
3025 "BigAutoField" => Ok(FieldType::BigInteger),
3026 "SmallAutoField" => Ok(FieldType::SmallInteger),
3027
3028 "CharField" => {
3030 let max_length = attributes
3031 .get("max_length")
3032 .and_then(|v| v.parse::<u32>().ok())
3033 .ok_or_else(|| "CharField requires max_length attribute".to_string())?;
3034 Ok(FieldType::VarChar(max_length))
3035 }
3036 "TextField" => Ok(FieldType::Text),
3037 "SlugField" => {
3038 let max_length = attributes
3039 .get("max_length")
3040 .and_then(|v| v.parse::<u32>().ok())
3041 .unwrap_or(50);
3042 Ok(FieldType::VarChar(max_length))
3043 }
3044 "EmailField" => {
3045 let max_length = attributes
3046 .get("max_length")
3047 .and_then(|v| v.parse::<u32>().ok())
3048 .unwrap_or(254);
3049 Ok(FieldType::VarChar(max_length))
3050 }
3051 "URLField" => {
3052 let max_length = attributes
3053 .get("max_length")
3054 .and_then(|v| v.parse::<u32>().ok())
3055 .unwrap_or(200);
3056 Ok(FieldType::VarChar(max_length))
3057 }
3058
3059 "BooleanField" => Ok(FieldType::Boolean),
3061 "NullBooleanField" => Ok(FieldType::Boolean),
3062
3063 "DateField" => Ok(FieldType::Date),
3065 "TimeField" => Ok(FieldType::Time),
3066 "DateTimeField" => Ok(FieldType::DateTime),
3067 "DurationField" => Ok(FieldType::BigInteger), "FloatField" => Ok(FieldType::Float),
3071 "DecimalField" => {
3072 let precision = attributes
3073 .get("max_digits")
3074 .and_then(|v| v.parse::<u32>().ok())
3075 .unwrap_or(10);
3076 let scale = attributes
3077 .get("decimal_places")
3078 .and_then(|v| v.parse::<u32>().ok())
3079 .unwrap_or(2);
3080 Ok(FieldType::Decimal { precision, scale })
3081 }
3082
3083 "BinaryField" => Ok(FieldType::Binary),
3085
3086 "UUIDField" => Ok(FieldType::Uuid),
3088
3089 "JSONField" => Ok(FieldType::Json),
3091
3092 "FileField" | "ImageField" => {
3094 let max_length = attributes
3095 .get("max_length")
3096 .and_then(|v| v.parse::<u32>().ok())
3097 .unwrap_or(100);
3098 Ok(FieldType::VarChar(max_length))
3099 }
3100
3101 "GenericIPAddressField" | "IPAddressField" => {
3103 Ok(FieldType::VarChar(39)) }
3106
3107 "ForeignKey" => {
3109 Ok(FieldType::BigInteger)
3111 }
3112 "OneToOneField" => Ok(FieldType::BigInteger),
3113
3114 other => Err(format!("Unsupported field type: {}", other)),
3116 }
3117}
3118
3119#[derive(Debug, Clone, Copy)]
3121pub enum SqlDialect {
3122 Sqlite,
3124 Postgres,
3126 Mysql,
3128 Cockroachdb,
3130}
3131
3132#[derive(Debug, Clone)]
3151pub struct SqliteTableRecreation {
3152 pub table_name: String,
3154 pub new_columns: Vec<ColumnDefinition>,
3156 pub columns_to_copy: Vec<String>,
3158 pub constraints: Vec<Constraint>,
3160 pub raw_constraint_sqls: Vec<String>,
3162 pub without_rowid: bool,
3164}
3165
3166impl SqliteTableRecreation {
3167 pub fn for_drop_column(
3169 table_name: impl Into<String>,
3170 current_columns: Vec<ColumnDefinition>,
3171 column_to_drop: &str,
3172 current_constraints: Vec<Constraint>,
3173 ) -> Self {
3174 let table_name = table_name.into();
3175 let new_columns: Vec<_> = current_columns
3176 .into_iter()
3177 .filter(|c| c.name != column_to_drop)
3178 .collect();
3179 let columns_to_copy: Vec<_> = new_columns.iter().map(|c| c.name.to_string()).collect();
3180
3181 let constraints: Vec<_> = current_constraints
3183 .into_iter()
3184 .filter(|c| !Self::constraint_references_column(c, column_to_drop))
3185 .collect();
3186
3187 Self {
3188 table_name,
3189 new_columns,
3190 columns_to_copy,
3191 constraints,
3192 raw_constraint_sqls: Vec::new(),
3193 without_rowid: false,
3194 }
3195 }
3196
3197 pub fn for_alter_column(
3199 table_name: impl Into<String>,
3200 current_columns: Vec<ColumnDefinition>,
3201 column_name: &str,
3202 new_definition: ColumnDefinition,
3203 current_constraints: Vec<Constraint>,
3204 ) -> Self {
3205 let table_name = table_name.into();
3206 let new_columns: Vec<_> = current_columns
3207 .into_iter()
3208 .map(|c| {
3209 if c.name == column_name {
3210 new_definition.clone()
3211 } else {
3212 c
3213 }
3214 })
3215 .collect();
3216 let columns_to_copy: Vec<_> = new_columns.iter().map(|c| c.name.to_string()).collect();
3217
3218 Self {
3219 table_name,
3220 new_columns,
3221 columns_to_copy,
3222 constraints: current_constraints,
3223 raw_constraint_sqls: Vec::new(),
3224 without_rowid: false,
3225 }
3226 }
3227
3228 pub fn for_add_constraint(
3233 table_name: impl Into<String>,
3234 current_columns: Vec<ColumnDefinition>,
3235 current_constraints: Vec<Constraint>,
3236 constraint_sql: String,
3237 ) -> Self {
3238 let table_name = table_name.into();
3239 let columns_to_copy: Vec<_> = current_columns.iter().map(|c| c.name.to_string()).collect();
3240
3241 Self {
3242 table_name,
3243 new_columns: current_columns,
3244 columns_to_copy,
3245 constraints: current_constraints,
3246 raw_constraint_sqls: vec![constraint_sql],
3247 without_rowid: false,
3248 }
3249 }
3250
3251 pub fn for_drop_constraint(
3256 table_name: impl Into<String>,
3257 current_columns: Vec<ColumnDefinition>,
3258 current_constraints: Vec<Constraint>,
3259 constraint_name: &str,
3260 ) -> Self {
3261 let table_name = table_name.into();
3262 let columns_to_copy: Vec<_> = current_columns.iter().map(|c| c.name.to_string()).collect();
3263
3264 let constraints: Vec<_> = current_constraints
3266 .into_iter()
3267 .filter(|c| !Self::constraint_has_name(c, constraint_name))
3268 .collect();
3269
3270 Self {
3271 table_name,
3272 new_columns: current_columns,
3273 columns_to_copy,
3274 constraints,
3275 raw_constraint_sqls: Vec::new(),
3276 without_rowid: false,
3277 }
3278 }
3279
3280 pub fn to_sql_statements(&self) -> Vec<String> {
3282 let temp_table = format!("{}_new", self.table_name);
3283
3284 let column_defs: Vec<String> = self
3286 .new_columns
3287 .iter()
3288 .map(|c| Operation::column_to_sql(c, &SqlDialect::Sqlite))
3289 .collect();
3290
3291 let constraint_defs: Vec<String> = self.constraints.iter().map(|c| c.to_string()).collect();
3292
3293 let mut create_parts = column_defs;
3294 create_parts.extend(constraint_defs);
3295 create_parts.extend(self.raw_constraint_sqls.clone());
3297
3298 let mut create_sql = format!(
3299 "CREATE TABLE \"{}\" (\n {}\n)",
3300 temp_table,
3301 create_parts.join(",\n ")
3302 );
3303 if self.without_rowid {
3304 create_sql.push_str(" WITHOUT ROWID");
3305 }
3306 create_sql.push(';');
3307
3308 let columns_list = self
3310 .columns_to_copy
3311 .iter()
3312 .map(|c| format!("\"{}\"", c))
3313 .collect::<Vec<_>>()
3314 .join(", ");
3315 let insert_sql = format!(
3316 "INSERT INTO \"{}\" SELECT {} FROM \"{}\";",
3317 temp_table, columns_list, self.table_name
3318 );
3319
3320 let drop_sql = format!("DROP TABLE \"{}\";", self.table_name);
3322
3323 let rename_sql = format!(
3325 "ALTER TABLE \"{}\" RENAME TO \"{}\";",
3326 temp_table, self.table_name
3327 );
3328
3329 vec![create_sql, insert_sql, drop_sql, rename_sql]
3330 }
3331
3332 fn constraint_references_column(constraint: &Constraint, column_name: &str) -> bool {
3334 match constraint {
3335 Constraint::PrimaryKey { columns, .. } => columns.iter().any(|c| c == column_name),
3336 Constraint::ForeignKey { columns, .. } => columns.iter().any(|c| c == column_name),
3337 Constraint::Unique { columns, .. } => columns.iter().any(|c| c == column_name),
3338 Constraint::Check { expression, .. } => expression.contains(column_name),
3339 Constraint::OneToOne { column, .. } => column == column_name,
3340 Constraint::ManyToMany { source_column, .. } => source_column == column_name,
3341 Constraint::Exclude { elements, .. } => {
3342 elements.iter().any(|(col, _)| col == column_name)
3343 }
3344 }
3345 }
3346
3347 fn constraint_has_name(constraint: &Constraint, constraint_name: &str) -> bool {
3349 match constraint {
3350 Constraint::PrimaryKey { name, .. } => name == constraint_name,
3351 Constraint::ForeignKey { name, .. } => name == constraint_name,
3352 Constraint::Unique { name, .. } => name == constraint_name,
3353 Constraint::Check { name, .. } => name == constraint_name,
3354 Constraint::OneToOne { name, .. } => name == constraint_name,
3355 Constraint::ManyToMany { name, .. } => name == constraint_name,
3356 Constraint::Exclude { name, .. } => name == constraint_name,
3357 }
3358 }
3359}
3360
3361impl Operation {
3362 pub fn requires_sqlite_recreation(&self) -> bool {
3364 matches!(
3365 self,
3366 Operation::DropColumn { .. }
3367 | Operation::AlterColumn { .. }
3368 | Operation::AddConstraint { .. }
3369 | Operation::DropConstraint { .. }
3370 )
3371 }
3372
3373 pub fn reverse_requires_sqlite_recreation(&self) -> bool {
3385 matches!(
3386 self,
3387 Operation::AddColumn { .. }
3389 | Operation::AlterColumn { .. }
3391 | Operation::AddConstraint { .. }
3393 | Operation::DropConstraint { .. }
3395 )
3396 }
3397
3398 pub fn to_reverse_operation(
3414 &self,
3415 project_state: &ProjectState,
3416 ) -> super::Result<Option<Operation>> {
3417 match self {
3418 Operation::CreateTable { name, .. } => {
3419 Ok(Some(Operation::DropTable { name: name.clone() }))
3420 }
3421 Operation::DropTable { name } => {
3422 if let Some(model) = project_state.find_model_by_table(name) {
3424 let columns: Vec<ColumnDefinition> = model
3425 .fields
3426 .iter()
3427 .map(|(field_name, field)| {
3428 ColumnDefinition::from_field_state(field_name.clone(), field)
3429 })
3430 .collect();
3431 let constraints: Vec<Constraint> = model
3432 .constraints
3433 .iter()
3434 .map(|c| c.to_constraint())
3435 .collect();
3436 return Ok(Some(Operation::CreateTable {
3437 name: name.clone(),
3438 columns,
3439 constraints,
3440 without_rowid: None,
3441 interleave_in_parent: None,
3442 partition: None,
3443 }));
3444 }
3445 Ok(None)
3446 }
3447 Operation::AddColumn { table, column, .. } => Ok(Some(Operation::DropColumn {
3448 table: table.clone(),
3449 column: column.name.clone(),
3450 })),
3451 Operation::DropColumn { table, column } => {
3452 if let Some(model) = project_state.find_model_by_table(table)
3454 && let Some(field) = model.get_field(column)
3455 {
3456 let col_def = ColumnDefinition::from_field_state(column.clone(), field);
3457 return Ok(Some(Operation::AddColumn {
3458 table: table.clone(),
3459 column: col_def,
3460 mysql_options: None,
3461 }));
3462 }
3463 Ok(None)
3464 }
3465 Operation::AlterColumn {
3466 table,
3467 column,
3468 old_definition,
3469 new_definition: _,
3470 ..
3471 } => {
3472 let resolved_old_def = old_definition.clone().or_else(|| {
3476 project_state
3477 .find_model_by_table(table)
3478 .and_then(|model| model.get_field(column))
3479 .map(|field| ColumnDefinition::from_field_state(column.clone(), field))
3480 });
3481
3482 if let Some(col_def) = resolved_old_def {
3483 return Ok(Some(Operation::AlterColumn {
3484 table: table.clone(),
3485 column: column.clone(),
3486 old_definition: None,
3487 new_definition: col_def,
3488 mysql_options: None,
3489 }));
3490 }
3491 Ok(None)
3492 }
3493 Operation::AddConstraint {
3494 table,
3495 constraint_sql,
3496 } => {
3497 if let Some(constraint_name) = Self::extract_constraint_name(constraint_sql) {
3499 return Ok(Some(Operation::DropConstraint {
3500 table: table.clone(),
3501 constraint_name,
3502 }));
3503 }
3504 Err(super::MigrationError::InvalidMigration(format!(
3505 "Cannot extract constraint name from: {}",
3506 constraint_sql
3507 )))
3508 }
3509 Operation::DropConstraint {
3510 table,
3511 constraint_name,
3512 } => {
3513 if let Some(model) = project_state.find_model_by_table(table)
3515 && let Some(constraint_def) = model
3516 .constraints
3517 .iter()
3518 .find(|c| c.name == *constraint_name)
3519 {
3520 let constraint = constraint_def.to_constraint();
3521 return Ok(Some(Operation::AddConstraint {
3522 table: table.clone(),
3523 constraint_sql: format!("{}", constraint),
3524 }));
3525 }
3526 Ok(None)
3527 }
3528 Operation::RenameTable { old_name, new_name } => Ok(Some(Operation::RenameTable {
3529 old_name: new_name.clone(),
3530 new_name: old_name.clone(),
3531 })),
3532 Operation::RenameColumn {
3533 table,
3534 old_name,
3535 new_name,
3536 } => Ok(Some(Operation::RenameColumn {
3537 table: table.clone(),
3538 old_name: new_name.clone(),
3539 new_name: old_name.clone(),
3540 })),
3541 Operation::CreateIndex { table, columns, .. } => Ok(Some(Operation::DropIndex {
3542 table: table.clone(),
3543 columns: columns.clone(),
3544 })),
3545 Operation::DropIndex { table, columns } => {
3546 Ok(Some(Operation::CreateIndex {
3549 table: table.clone(),
3550 columns: columns.clone(),
3551 unique: false,
3552 index_type: None,
3553 where_clause: None,
3554 concurrently: false,
3555 expressions: None,
3556 mysql_options: None,
3557 operator_class: None,
3558 }))
3559 }
3560 Operation::RunSQL { .. } | Operation::RunRust { .. } | Operation::BulkLoad { .. } => {
3562 Ok(None)
3563 }
3564 _ => Ok(None),
3566 }
3567 }
3568}
3569
3570pub use Operation::{AddColumn, AlterColumn, CreateTable, DropColumn};
3572
3573pub enum OperationStatement {
3575 TableCreate(CreateTableStatement),
3577 TableDrop(DropTableStatement),
3579 TableAlter(AlterTableStatement),
3581 TableRename(AlterTableStatement),
3583 IndexCreate(CreateIndexStatement),
3585 IndexDrop(DropIndexStatement),
3587 RawSql(String),
3589}
3590
3591impl OperationStatement {
3592 pub async fn execute<'c, E>(&self, executor: E) -> Result<(), sqlx::Error>
3594 where
3595 E: sqlx::Executor<'c, Database = sqlx::Postgres>,
3596 {
3597 use crate::backends::sql_build_helpers;
3598 use crate::backends::types::DatabaseType;
3599 let db_type = DatabaseType::Postgres;
3600 match self {
3601 OperationStatement::TableCreate(stmt) => {
3602 let sql = sql_build_helpers::build_create_table_sql(db_type, stmt);
3603 sqlx::query(&sql).execute(executor).await?;
3604 }
3605 OperationStatement::TableDrop(stmt) => {
3606 let sql = sql_build_helpers::build_drop_table_sql(db_type, stmt);
3607 sqlx::query(&sql).execute(executor).await?;
3608 }
3609 OperationStatement::TableAlter(stmt) => {
3610 let sql = sql_build_helpers::build_alter_table_sql(db_type, stmt);
3611 sqlx::query(&sql).execute(executor).await?;
3612 }
3613 OperationStatement::TableRename(stmt) => {
3614 let sql = sql_build_helpers::build_alter_table_sql(db_type, stmt);
3615 sqlx::query(&sql).execute(executor).await?;
3616 }
3617 OperationStatement::IndexCreate(stmt) => {
3618 let sql = sql_build_helpers::build_create_index_sql(db_type, stmt);
3619 sqlx::query(&sql).execute(executor).await?;
3620 }
3621 OperationStatement::IndexDrop(stmt) => {
3622 let sql = sql_build_helpers::build_drop_index_sql(db_type, stmt);
3623 sqlx::query(&sql).execute(executor).await?;
3624 }
3625 OperationStatement::RawSql(sql) => {
3626 sqlx::query(sql).execute(executor).await?;
3628 }
3629 }
3630 Ok(())
3631 }
3632
3633 pub fn to_sql_string(&self, db_type: crate::backends::types::DatabaseType) -> String {
3639 use crate::backends::sql_build_helpers;
3640
3641 match self {
3642 OperationStatement::TableCreate(stmt) => {
3643 sql_build_helpers::build_create_table_sql(db_type, stmt)
3644 }
3645 OperationStatement::TableDrop(stmt) => {
3646 sql_build_helpers::build_drop_table_sql(db_type, stmt)
3647 }
3648 OperationStatement::TableAlter(stmt) => {
3649 sql_build_helpers::build_alter_table_sql(db_type, stmt)
3650 }
3651 OperationStatement::TableRename(stmt) => {
3652 sql_build_helpers::build_alter_table_sql(db_type, stmt)
3653 }
3654 OperationStatement::IndexCreate(stmt) => {
3655 sql_build_helpers::build_create_index_sql(db_type, stmt)
3656 }
3657 OperationStatement::IndexDrop(stmt) => {
3658 sql_build_helpers::build_drop_index_sql(db_type, stmt)
3659 }
3660 OperationStatement::RawSql(sql) => sql.clone(),
3661 }
3662 }
3663}
3664
3665impl Operation {
3666 pub fn to_statement(&self) -> OperationStatement {
3668 match self {
3669 Operation::CreateTable {
3670 name,
3671 columns,
3672 constraints,
3673 ..
3674 } => {
3675 OperationStatement::TableCreate(self.build_create_table(name, columns, constraints))
3676 }
3677 Operation::DropTable { name } => {
3678 OperationStatement::TableDrop(self.build_drop_table(name))
3679 }
3680 Operation::AddColumn { table, column, .. } => {
3681 OperationStatement::TableAlter(self.build_add_column(table, column))
3682 }
3683 Operation::DropColumn { table, column } => {
3684 OperationStatement::TableAlter(self.build_drop_column(table, column))
3685 }
3686 Operation::AlterColumn {
3687 table,
3688 column,
3689 new_definition,
3690 ..
3691 } => OperationStatement::TableAlter(self.build_alter_column(
3692 table,
3693 column,
3694 new_definition,
3695 )),
3696 Operation::RenameTable { old_name, new_name } => {
3697 OperationStatement::TableRename(self.build_rename_table(old_name, new_name))
3698 }
3699 Operation::RenameColumn {
3701 table,
3702 old_name,
3703 new_name,
3704 } => OperationStatement::RawSql(format!(
3705 "ALTER TABLE {} RENAME COLUMN {} TO {}",
3706 quote_identifier(table),
3707 quote_identifier(old_name),
3708 quote_identifier(new_name)
3709 )),
3710 Operation::AddConstraint {
3711 table,
3712 constraint_sql,
3713 } => {
3714 OperationStatement::RawSql(format!(
3716 "ALTER TABLE {} ADD {}",
3717 quote_identifier(table),
3718 constraint_sql
3719 ))
3720 }
3721 Operation::DropConstraint {
3722 table,
3723 constraint_name,
3724 } => OperationStatement::RawSql(format!(
3725 "ALTER TABLE {} DROP CONSTRAINT {}",
3726 quote_identifier(table),
3727 quote_identifier(constraint_name)
3728 )),
3729 Operation::CreateIndex {
3730 table,
3731 columns,
3732 unique,
3733 ..
3734 } => {
3735 let idx_name = format!("idx_{}_{}", table, columns.join("_"));
3736 OperationStatement::IndexCreate(
3737 self.build_create_index(&idx_name, table, columns, *unique),
3738 )
3739 }
3740 Operation::DropIndex { table, columns } => {
3741 let idx_name = format!("idx_{}_{}", table, columns.join("_"));
3742 OperationStatement::IndexDrop(self.build_drop_index(&idx_name))
3743 }
3744 Operation::RunSQL { sql, .. } => OperationStatement::RawSql(sql.to_string()),
3745 Operation::RunRust { code, .. } => {
3746 OperationStatement::RawSql(format!(
3748 "-- RunRust: {}",
3749 code.lines().next().unwrap_or("")
3750 ))
3751 }
3752 Operation::AlterTableComment { table, comment } => {
3753 OperationStatement::RawSql(if let Some(comment_text) = comment {
3755 format!(
3756 "COMMENT ON TABLE {} IS '{}'",
3757 quote_identifier(table),
3758 comment_text.replace('\'', "''") )
3760 } else {
3761 format!("COMMENT ON TABLE {} IS NULL", quote_identifier(table))
3762 })
3763 }
3764 Operation::AlterUniqueTogether {
3765 table,
3766 unique_together,
3767 } => {
3768 let mut sqls = Vec::new();
3769 for (idx, fields) in unique_together.iter().enumerate() {
3770 let constraint_name = format!("{}_{}_uniq", table, idx);
3771 let fields_str: Vec<String> = fields
3772 .iter()
3773 .map(|f| quote_identifier(f).to_string())
3774 .collect();
3775 sqls.push(format!(
3776 "ALTER TABLE {} ADD CONSTRAINT {} UNIQUE ({})",
3777 quote_identifier(table),
3778 quote_identifier(&constraint_name),
3779 fields_str.join(", ")
3780 ));
3781 }
3782 OperationStatement::RawSql(sqls.join(";\n"))
3783 }
3784 Operation::AlterModelOptions { .. } => OperationStatement::RawSql(String::new()),
3785 Operation::CreateInheritedTable {
3786 name,
3787 columns,
3788 base_table,
3789 join_column,
3790 } => {
3791 let mut stmt = Query::create_table();
3792 stmt.table(Alias::new(name.as_str())).if_not_exists();
3793
3794 let join_col = ColumnDef::new(Alias::new(join_column.as_str()));
3796 let join_col = join_col.integer();
3797 stmt.col(join_col);
3798
3799 for col in columns {
3801 let mut column = ColumnDef::new(Alias::new(col.name.as_str()));
3802 column = self.apply_column_type(column, &col.type_definition);
3803 stmt.col(column);
3804 }
3805
3806 let mut fk = reinhardt_query::prelude::ForeignKey::create();
3808 fk.from_tbl(Alias::new(name.as_str()))
3809 .from_col(Alias::new(join_column.as_str()))
3810 .to_tbl(Alias::new(base_table.as_str()))
3811 .to_col(Alias::new("id"));
3812 stmt.foreign_key_from_builder(&mut fk);
3813
3814 OperationStatement::TableCreate(stmt.to_owned())
3815 }
3816 Operation::AddDiscriminatorColumn {
3817 table,
3818 column_name,
3819 default_value,
3820 } => {
3821 let mut stmt = Query::alter_table();
3822 stmt.table(Alias::new(table.as_str()));
3823
3824 let mut col = ColumnDef::new(Alias::new(column_name.as_str()));
3825 col = col
3826 .string_len(50)
3827 .default(SimpleExpr::from(default_value.to_string()));
3828 stmt.add_column(col);
3829
3830 OperationStatement::TableAlter(stmt.to_owned())
3831 }
3832 Operation::MoveModel {
3833 rename_table,
3834 old_table_name,
3835 new_table_name,
3836 ..
3837 } => {
3838 if *rename_table {
3840 if let (Some(old_name), Some(new_name)) = (old_table_name, new_table_name) {
3841 OperationStatement::TableRename(self.build_rename_table(old_name, new_name))
3842 } else {
3843 OperationStatement::RawSql("-- MoveModel: State-only operation".to_string())
3845 }
3846 } else {
3847 OperationStatement::RawSql("-- MoveModel: State-only operation".to_string())
3849 }
3850 }
3851 Operation::CreateSchema {
3852 name,
3853 if_not_exists,
3854 } => {
3855 let sql = if *if_not_exists {
3857 format!("CREATE SCHEMA IF NOT EXISTS {}", quote_identifier(name))
3858 } else {
3859 format!("CREATE SCHEMA {}", quote_identifier(name))
3860 };
3861 OperationStatement::RawSql(sql)
3862 }
3863 Operation::DropSchema {
3864 name,
3865 cascade,
3866 if_exists,
3867 } => {
3868 let if_exists_clause = if *if_exists { " IF EXISTS" } else { "" };
3870 let cascade_clause = if *cascade { " CASCADE" } else { "" };
3871 let sql = format!(
3872 "DROP SCHEMA{} {}{}",
3873 if_exists_clause,
3874 quote_identifier(name),
3875 cascade_clause
3876 );
3877 OperationStatement::RawSql(sql)
3878 }
3879 Operation::CreateExtension {
3880 name,
3881 if_not_exists,
3882 schema,
3883 } => {
3884 let if_not_exists_clause = if *if_not_exists { " IF NOT EXISTS" } else { "" };
3886 let schema_clause = if let Some(s) = schema {
3887 format!(" SCHEMA {}", quote_identifier(s))
3888 } else {
3889 String::new()
3890 };
3891 let sql = format!(
3892 "CREATE EXTENSION{} {}{}",
3893 if_not_exists_clause,
3894 quote_identifier(name),
3895 schema_clause
3896 );
3897 OperationStatement::RawSql(sql)
3898 }
3899 Operation::BulkLoad {
3900 table,
3901 source,
3902 format,
3903 options,
3904 } => {
3905 OperationStatement::RawSql(Self::postgres_copy_from_sql(
3908 table, source, format, options,
3909 ))
3910 }
3911 Operation::SetAutoIncrementValue { table, .. } => {
3912 OperationStatement::RawSql(format!(
3924 "SELECT 1/0 AS \"SetAutoIncrementValue on {} requires dialect-aware rendering; call Operation::to_sql(&dialect) instead of to_statement()\";",
3925 table.replace('"', "\"\"")
3926 ))
3927 }
3928 Operation::CreateCompositePrimaryKey {
3929 table,
3930 columns,
3931 constraint_name,
3932 } => OperationStatement::RawSql(Self::create_composite_pk_to_sql(
3933 table,
3934 columns,
3935 constraint_name.as_deref(),
3936 )),
3937 }
3938 }
3939
3940 fn build_create_table(
3942 &self,
3943 name: &str,
3944 columns: &[ColumnDefinition],
3945 constraints: &[Constraint],
3946 ) -> CreateTableStatement {
3947 let mut stmt = Query::create_table();
3948 stmt.table(Alias::new(name)).if_not_exists();
3949
3950 for col in columns {
3951 let mut column = ColumnDef::new(Alias::new(col.name.as_str()));
3952 column = self.apply_column_type(column, &col.type_definition);
3953
3954 if col.not_null {
3955 column = column.not_null(true);
3956 }
3957 if col.unique {
3958 column = column.unique(true);
3959 }
3960 if col.primary_key {
3961 column = column.primary_key(true);
3962 }
3963 if col.auto_increment {
3964 column = column.auto_increment(true);
3965 }
3966 if let Some(default) = &col.default {
3967 column = column.default(SimpleExpr::from(self.convert_default_value(default)));
3968 }
3969
3970 stmt.col(column);
3971 }
3972
3973 for constraint in constraints {
3975 match constraint {
3976 Constraint::PrimaryKey { columns, .. } => {
3977 let col_idens: Vec<Alias> =
3978 columns.iter().map(|c| Alias::new(c.as_str())).collect();
3979 stmt.primary_key(col_idens);
3980 }
3981 Constraint::ForeignKey {
3982 name,
3983 columns,
3984 referenced_table,
3985 referenced_columns,
3986 on_delete,
3987 on_update,
3988 ..
3989 } => {
3990 let mut fk = reinhardt_query::prelude::ForeignKey::create();
3991 fk.name(Alias::new(name.as_str()))
3992 .from_tbl(Alias::new(name.as_str()))
3993 .to_tbl(Alias::new(referenced_table.as_str()));
3994
3995 for col in columns {
3996 fk.from_col(Alias::new(col.as_str()));
3997 }
3998 for col in referenced_columns {
3999 fk.to_col(Alias::new(col.as_str()));
4000 }
4001
4002 fk.on_delete((*on_delete).into());
4003 fk.on_update((*on_update).into());
4004
4005 stmt.foreign_key_from_builder(&mut fk);
4006 }
4007 Constraint::Unique { columns, .. } => {
4008 let col_idens: Vec<Alias> =
4009 columns.iter().map(|c| Alias::new(c.as_str())).collect();
4010 stmt.unique(col_idens);
4011 }
4012 Constraint::Check { name, expression } => {
4013 let _ = (name, expression); }
4017 Constraint::OneToOne {
4018 name,
4019 column,
4020 referenced_table,
4021 referenced_column,
4022 on_delete,
4023 on_update,
4024 ..
4025 } => {
4026 let mut fk = reinhardt_query::prelude::ForeignKey::create();
4028 fk.name(Alias::new(name.as_str()))
4029 .from_tbl(Alias::new(name.as_str()))
4030 .to_tbl(Alias::new(referenced_table.as_str()))
4031 .from_col(Alias::new(column.as_str()))
4032 .to_col(Alias::new(referenced_column.as_str()))
4033 .on_delete((*on_delete).into())
4034 .on_update((*on_update).into());
4035
4036 stmt.foreign_key_from_builder(&mut fk);
4037
4038 }
4041 Constraint::ManyToMany { .. } => {
4042 }
4045 Constraint::Exclude { .. } => {
4046 }
4049 }
4050 }
4051
4052 stmt.to_owned()
4053 }
4054
4055 fn build_drop_table(&self, name: &str) -> DropTableStatement {
4057 Query::drop_table()
4058 .table(Alias::new(name))
4059 .if_exists()
4060 .cascade()
4061 .to_owned()
4062 }
4063
4064 fn build_add_column(&self, table: &str, column: &ColumnDefinition) -> AlterTableStatement {
4066 let mut stmt = Query::alter_table();
4067 stmt.table(Alias::new(table));
4068
4069 let mut col_def = ColumnDef::new(Alias::new(column.name.as_str()));
4070 col_def = self.apply_column_type(col_def, &column.type_definition);
4071
4072 if column.not_null {
4073 col_def = col_def.not_null(true);
4074 }
4075 if let Some(default) = &column.default {
4076 col_def = col_def.default(SimpleExpr::from(self.convert_default_value(default)));
4077 }
4078
4079 stmt.add_column(col_def);
4080 stmt.to_owned()
4081 }
4082
4083 fn build_drop_column(&self, table: &str, column: &str) -> AlterTableStatement {
4085 Query::alter_table()
4086 .table(Alias::new(table))
4087 .drop_column(Alias::new(column))
4088 .to_owned()
4089 }
4090
4091 fn build_alter_column(
4093 &self,
4094 table: &str,
4095 column: &str,
4096 new_definition: &ColumnDefinition,
4097 ) -> AlterTableStatement {
4098 let mut stmt = Query::alter_table();
4099 stmt.table(Alias::new(table));
4100
4101 let mut col_def = ColumnDef::new(Alias::new(column));
4102 col_def = self.apply_column_type(col_def, &new_definition.type_definition);
4103
4104 if new_definition.not_null {
4105 col_def = col_def.not_null(true);
4106 }
4107
4108 stmt.modify_column(col_def);
4109 stmt.to_owned()
4110 }
4111
4112 fn build_rename_table(&self, old_name: &str, new_name: &str) -> AlterTableStatement {
4114 Query::alter_table()
4115 .table(Alias::new(old_name))
4116 .rename_table(Alias::new(new_name))
4117 .to_owned()
4118 }
4119
4120 fn build_create_index(
4122 &self,
4123 name: &str,
4124 table: &str,
4125 columns: &[String],
4126 unique: bool,
4127 ) -> CreateIndexStatement {
4128 let mut stmt = Query::create_index();
4129 stmt.name(Alias::new(name)).table(Alias::new(table));
4130
4131 for col in columns {
4132 stmt.col(Alias::new(col));
4133 }
4134
4135 if unique {
4136 stmt.unique();
4137 }
4138
4139 stmt.to_owned()
4140 }
4141
4142 fn build_drop_index(&self, name: &str) -> DropIndexStatement {
4144 Query::drop_index().name(Alias::new(name)).to_owned()
4145 }
4146
4147 fn apply_column_type(&self, col_def: ColumnDef, field_type: &FieldType) -> ColumnDef {
4149 use FieldType;
4150 match field_type {
4151 FieldType::Integer => col_def.integer(),
4152 FieldType::BigInteger => col_def.big_integer(),
4153 FieldType::SmallInteger => col_def.small_integer(),
4154 FieldType::TinyInt => col_def.tiny_integer(),
4155 FieldType::VarChar(max_length) => col_def.string_len(*max_length),
4156 FieldType::Char(max_length) => col_def.char_len(*max_length),
4157 FieldType::Text | FieldType::TinyText | FieldType::MediumText | FieldType::LongText => {
4158 col_def.text()
4159 }
4160 FieldType::Boolean => col_def.custom(Alias::new("BOOLEAN")),
4166 FieldType::DateTime => col_def.timestamp(),
4167 FieldType::TimestampTz => col_def.timestamp_with_time_zone(),
4168 FieldType::Date => col_def.date(),
4169 FieldType::Time => col_def.time(),
4170 FieldType::Decimal { precision, scale } => col_def.decimal(*precision, *scale),
4171 FieldType::Float => col_def.float(),
4172 FieldType::Double | FieldType::Real => col_def.double(),
4173 FieldType::Json => col_def.json(),
4174 FieldType::JsonBinary => col_def.json_binary(),
4175 FieldType::Uuid => col_def.uuid(),
4176 FieldType::Binary | FieldType::Bytea => col_def.binary(0),
4177 FieldType::Blob | FieldType::TinyBlob | FieldType::MediumBlob | FieldType::LongBlob => {
4178 col_def.binary(0)
4179 }
4180 FieldType::MediumInt => col_def.integer(),
4181 FieldType::Year => col_def.small_integer(),
4182 FieldType::Enum { values } => {
4183 col_def.custom(Alias::new(format!("ENUM({})", values.join(","))))
4184 }
4185 FieldType::Set { values } => {
4186 col_def.custom(Alias::new(format!("SET({})", values.join(","))))
4187 }
4188 FieldType::ForeignKey { .. } => {
4189 col_def.integer()
4191 }
4192 FieldType::OneToOne { .. } => {
4193 col_def.big_integer()
4196 }
4197 FieldType::ManyToMany { .. } => {
4198 col_def.big_integer()
4201 }
4202 FieldType::Array(inner) => {
4204 let inner_sql = inner.to_sql_string();
4206 col_def.custom(Alias::new(format!("{}[]", inner_sql)))
4207 }
4208 FieldType::HStore => col_def.custom(Alias::new("HSTORE")),
4209 FieldType::CIText => col_def.custom(Alias::new("CITEXT")),
4210 FieldType::Int4Range => col_def.custom(Alias::new("INT4RANGE")),
4211 FieldType::Int8Range => col_def.custom(Alias::new("INT8RANGE")),
4212 FieldType::NumRange => col_def.custom(Alias::new("NUMRANGE")),
4213 FieldType::DateRange => col_def.custom(Alias::new("DATERANGE")),
4214 FieldType::TsRange => col_def.custom(Alias::new("TSRANGE")),
4215 FieldType::TsTzRange => col_def.custom(Alias::new("TSTZRANGE")),
4216 FieldType::TsVector => col_def.custom(Alias::new("TSVECTOR")),
4217 FieldType::TsQuery => col_def.custom(Alias::new("TSQUERY")),
4218 FieldType::Custom(custom_type) => col_def.custom(Alias::new(custom_type)),
4219 }
4220 }
4221
4222 fn convert_default_value(&self, default: &str) -> Value {
4224 let trimmed = default.trim();
4225
4226 if trimmed.eq_ignore_ascii_case("null") {
4228 return Value::String(None);
4229 }
4230
4231 if trimmed.eq_ignore_ascii_case("true") {
4233 return Value::Bool(Some(true));
4234 }
4235 if trimmed.eq_ignore_ascii_case("false") {
4236 return Value::Bool(Some(false));
4237 }
4238
4239 if let Ok(i) = trimmed.parse::<i64>() {
4241 return Value::BigInt(Some(i));
4242 }
4243
4244 if let Ok(f) = trimmed.parse::<f64>() {
4246 return Value::Double(Some(f));
4247 }
4248
4249 if (trimmed.starts_with('"') && trimmed.ends_with('"'))
4251 || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
4252 {
4253 let unquoted = &trimmed[1..trimmed.len() - 1];
4254 return Value::String(Some(Box::new(unquoted.to_string())));
4255 }
4256
4257 if ((trimmed.starts_with('[') && trimmed.ends_with(']'))
4259 || (trimmed.starts_with('{') && trimmed.ends_with('}')))
4260 && let Ok(json) = serde_json::from_str::<serde_json::Value>(trimmed)
4261 {
4262 return json_to_sea_value(&json);
4263 }
4264
4265 const SQL_CONSTANTS: &[&str] = &[
4267 "CURRENT_TIMESTAMP",
4268 "CURRENT_DATE",
4269 "CURRENT_TIME",
4270 "CURRENT_USER",
4271 "SESSION_USER",
4272 "LOCALTIME",
4273 "LOCALTIMESTAMP",
4274 ];
4275
4276 if trimmed.ends_with("()") || trimmed.contains('(') {
4278 return Value::String(Some(Box::new(trimmed.to_string())));
4279 }
4280
4281 if SQL_CONSTANTS
4283 .iter()
4284 .any(|c| trimmed.eq_ignore_ascii_case(c))
4285 {
4286 return Value::String(Some(Box::new(trimmed.to_string())));
4287 }
4288
4289 Value::String(Some(Box::new(format!("'{}'", trimmed.replace('\'', "''")))))
4291 }
4292}
4293
4294fn json_to_sea_value(json: &serde_json::Value) -> Value {
4296 match json {
4297 serde_json::Value::Null => Value::String(None),
4298 serde_json::Value::Bool(b) => Value::Bool(Some(*b)),
4299 serde_json::Value::Number(n) => {
4300 if let Some(i) = n.as_i64() {
4301 Value::BigInt(Some(i))
4302 } else if let Some(f) = n.as_f64() {
4303 Value::Double(Some(f))
4304 } else {
4305 Value::String(Some(Box::new(n.to_string())))
4306 }
4307 }
4308 serde_json::Value::String(s) => Value::String(Some(Box::new(s.clone()))),
4309 serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
4310 Value::String(Some(Box::new(json.to_string())))
4312 }
4313 }
4314}
4315
4316use super::operation_trait::MigrationOperation;
4318
4319impl MigrationOperation for Operation {
4320 fn migration_name_fragment(&self) -> Option<String> {
4321 match self {
4322 Operation::CreateTable { name, .. } => Some(name.to_lowercase()),
4323 Operation::DropTable { name } => Some(format!("delete_{}", name.to_lowercase())),
4324 Operation::AddColumn { table, column, .. } => Some(format!(
4325 "{}_{}",
4326 table.to_lowercase(),
4327 column.name.to_lowercase()
4328 )),
4329 Operation::DropColumn { table, column } => Some(format!(
4330 "remove_{}_{}",
4331 table.to_lowercase(),
4332 column.to_lowercase()
4333 )),
4334 Operation::AlterColumn { table, column, .. } => Some(format!(
4335 "alter_{}_{}",
4336 table.to_lowercase(),
4337 column.to_lowercase()
4338 )),
4339 Operation::RenameTable { old_name, new_name } => Some(format!(
4340 "rename_{}_to_{}",
4341 old_name.to_lowercase(),
4342 new_name.to_lowercase()
4343 )),
4344 Operation::RenameColumn {
4345 table, new_name, ..
4346 } => Some(format!(
4347 "rename_{}_{}",
4348 table.to_lowercase(),
4349 new_name.to_lowercase()
4350 )),
4351 Operation::AddConstraint { table, .. } => {
4352 Some(format!("add_constraint_{}", table.to_lowercase()))
4353 }
4354 Operation::DropConstraint {
4355 table: _,
4356 constraint_name,
4357 } => Some(format!(
4358 "drop_constraint_{}",
4359 constraint_name.to_lowercase()
4360 )),
4361 Operation::CreateIndex { table, unique, .. } => {
4362 if *unique {
4363 Some(format!("create_unique_index_{}", table.to_lowercase()))
4364 } else {
4365 Some(format!("create_index_{}", table.to_lowercase()))
4366 }
4367 }
4368 Operation::DropIndex { table, .. } => {
4369 Some(format!("drop_index_{}", table.to_lowercase()))
4370 }
4371 Operation::RunSQL { .. } => None, Operation::RunRust { .. } => None, Operation::AlterTableComment { table, .. } => {
4374 Some(format!("alter_comment_{}", table.to_lowercase()))
4375 }
4376 Operation::AlterUniqueTogether { table, .. } => {
4377 Some(format!("alter_unique_{}", table.to_lowercase()))
4378 }
4379 Operation::AlterModelOptions { table, .. } => {
4380 Some(format!("alter_options_{}", table.to_lowercase()))
4381 }
4382 Operation::CreateInheritedTable { name, .. } => {
4383 Some(format!("create_inherited_{}", name.to_lowercase()))
4384 }
4385 Operation::AddDiscriminatorColumn { table, .. } => {
4386 Some(format!("add_discriminator_{}", table.to_lowercase()))
4387 }
4388 Operation::MoveModel {
4389 model_name,
4390 from_app,
4391 to_app,
4392 ..
4393 } => Some(format!(
4394 "move_{}_{}_{}_{}",
4395 from_app.to_lowercase(),
4396 model_name.to_lowercase(),
4397 to_app.to_lowercase(),
4398 model_name.to_lowercase()
4399 )),
4400 Operation::CreateSchema { name, .. } => {
4401 Some(format!("create_schema_{}", name.to_lowercase()))
4402 }
4403 Operation::DropSchema { name, .. } => {
4404 Some(format!("drop_schema_{}", name.to_lowercase()))
4405 }
4406 Operation::CreateExtension { name, .. } => {
4407 Some(format!("create_extension_{}", name.to_lowercase()))
4408 }
4409 Operation::BulkLoad { table, .. } => {
4410 Some(format!("bulk_load_{}", table.to_lowercase()))
4411 }
4412 Operation::SetAutoIncrementValue { table, column, .. } => Some(format!(
4413 "set_auto_increment_{}_{}",
4414 table.to_lowercase(),
4415 column.to_lowercase()
4416 )),
4417 Operation::CreateCompositePrimaryKey { table, .. } => {
4418 Some(format!("composite_pk_{}", table.to_lowercase()))
4419 }
4420 }
4421 }
4422
4423 fn describe(&self) -> String {
4424 match self {
4425 Operation::CreateTable { name, .. } => format!("Create table {}", name),
4426 Operation::DropTable { name } => format!("Drop table {}", name),
4427 Operation::AddColumn { table, column, .. } => {
4428 format!("Add column {} to {}", column.name, table)
4429 }
4430 Operation::DropColumn { table, column } => {
4431 format!("Drop column {} from {}", column, table)
4432 }
4433 Operation::AlterColumn { table, column, .. } => {
4434 format!("Alter column {} on {}", column, table)
4435 }
4436 Operation::RenameTable { old_name, new_name } => {
4437 format!("Rename table {} to {}", old_name, new_name)
4438 }
4439 Operation::RenameColumn {
4440 table,
4441 old_name,
4442 new_name,
4443 } => format!("Rename column {} to {} on {}", old_name, new_name, table),
4444 Operation::AddConstraint { table, .. } => format!("Add constraint on {}", table),
4445 Operation::DropConstraint {
4446 table,
4447 constraint_name,
4448 } => format!("Drop constraint {} from {}", constraint_name, table),
4449 Operation::CreateIndex { table, unique, .. } => {
4450 if *unique {
4451 format!("Create unique index on {}", table)
4452 } else {
4453 format!("Create index on {}", table)
4454 }
4455 }
4456 Operation::DropIndex { table, .. } => format!("Drop index on {}", table),
4457 Operation::RunSQL { sql, .. } => {
4458 let preview = if sql.len() > 50 {
4459 format!("{}...", &sql[..50])
4460 } else {
4461 (*sql).to_string()
4462 };
4463 format!("RunSQL: {}", preview)
4464 }
4465 Operation::RunRust { code, .. } => {
4466 let preview = if code.len() > 50 {
4467 format!("{}...", &code[..50])
4468 } else {
4469 (*code).to_string()
4470 };
4471 format!("RunRust: {}", preview)
4472 }
4473 Operation::AlterTableComment { table, comment } => match comment {
4474 Some(c) => format!("Set comment on {} to '{}'", table, c),
4475 None => format!("Remove comment from {}", table),
4476 },
4477 Operation::AlterUniqueTogether { table, .. } => {
4478 format!("Alter unique_together on {}", table)
4479 }
4480 Operation::AlterModelOptions { table, .. } => {
4481 format!("Alter model options on {}", table)
4482 }
4483 Operation::CreateInheritedTable {
4484 name, base_table, ..
4485 } => {
4486 format!("Create inherited table {} from {}", name, base_table)
4487 }
4488 Operation::AddDiscriminatorColumn {
4489 table, column_name, ..
4490 } => format!("Add discriminator column {} to {}", column_name, table),
4491 Operation::MoveModel {
4492 model_name,
4493 from_app,
4494 to_app,
4495 ..
4496 } => format!("Move model {} from {} to {}", model_name, from_app, to_app),
4497 Operation::CreateSchema { name, .. } => format!("Create schema {}", name),
4498 Operation::DropSchema { name, .. } => format!("Drop schema {}", name),
4499 Operation::CreateExtension { name, .. } => format!("Create extension {}", name),
4500 Operation::BulkLoad { table, source, .. } => {
4501 let source_desc = match source {
4502 BulkLoadSource::File(path) => format!("file '{}'", path),
4503 BulkLoadSource::Stdin => "STDIN".to_string(),
4504 BulkLoadSource::Program(cmd) => format!("program '{}'", cmd),
4505 };
4506 format!("Bulk load data into {} from {}", table, source_desc)
4507 }
4508 Operation::SetAutoIncrementValue {
4509 table,
4510 column,
4511 value,
4512 } => format!("Set auto-increment of {}.{} to {}", table, column, value),
4513 Operation::CreateCompositePrimaryKey { table, columns, .. } => format!(
4514 "Create composite primary key on {} ({})",
4515 table,
4516 columns.join(", ")
4517 ),
4518 }
4519 }
4520
4521 fn normalize(&self) -> Self
4526 where
4527 Self: Sized + Clone,
4528 {
4529 match self {
4530 Operation::CreateTable {
4532 name,
4533 columns,
4534 constraints,
4535 without_rowid,
4536 interleave_in_parent,
4537 partition,
4538 } => {
4539 let mut sorted_columns = columns.clone();
4540 sorted_columns.sort_by(|a, b| a.name.cmp(&b.name));
4541
4542 let mut sorted_constraints = constraints.clone();
4543 sorted_constraints.sort();
4544
4545 Operation::CreateTable {
4546 name: name.clone(),
4547 columns: sorted_columns,
4548 constraints: sorted_constraints,
4549 without_rowid: *without_rowid,
4550 interleave_in_parent: interleave_in_parent.clone(),
4551 partition: partition.clone(),
4552 }
4553 }
4554 Operation::CreateIndex {
4556 table,
4557 columns,
4558 unique,
4559 index_type,
4560 where_clause,
4561 concurrently,
4562 expressions,
4563 mysql_options,
4564 operator_class,
4565 } => {
4566 let mut sorted_columns = columns.clone();
4567 sorted_columns.sort();
4568
4569 Operation::CreateIndex {
4570 table: table.clone(),
4571 columns: sorted_columns,
4572 unique: *unique,
4573 index_type: *index_type,
4574 where_clause: where_clause.clone(),
4575 concurrently: *concurrently,
4576 expressions: expressions.clone(),
4577 mysql_options: *mysql_options,
4578 operator_class: operator_class.clone(),
4579 }
4580 }
4581 Operation::DropIndex { table, columns } => {
4583 let mut sorted_columns = columns.clone();
4584 sorted_columns.sort();
4585
4586 Operation::DropIndex {
4587 table: table.clone(),
4588 columns: sorted_columns,
4589 }
4590 }
4591 Operation::AlterUniqueTogether {
4593 table,
4594 unique_together,
4595 } => {
4596 let mut sorted_unique_together: Vec<Vec<String>> = unique_together
4597 .iter()
4598 .map(|field_list| {
4599 let mut sorted = field_list.clone();
4600 sorted.sort();
4601 sorted
4602 })
4603 .collect();
4604 sorted_unique_together.sort();
4605
4606 Operation::AlterUniqueTogether {
4607 table: table.clone(),
4608 unique_together: sorted_unique_together,
4609 }
4610 }
4611 Operation::AlterModelOptions { table, options } => Operation::AlterModelOptions {
4616 table: table.clone(),
4617 options: options.clone(),
4618 },
4619 _ => self.clone(),
4621 }
4622 }
4623}
4624
4625#[cfg(test)]
4626mod tests {
4627 use super::*;
4628 use FieldType;
4629 use rstest::rstest;
4630
4631 #[test]
4632 fn test_create_table_to_statement() {
4633 let op = Operation::CreateTable {
4634 name: "users".to_string(),
4635 columns: vec![
4636 ColumnDefinition {
4637 name: "id".to_string(),
4638 type_definition: FieldType::Integer,
4639 not_null: false,
4640 unique: false,
4641 primary_key: true,
4642 auto_increment: true,
4643 default: None,
4644 },
4645 ColumnDefinition {
4646 name: "name".to_string(),
4647 type_definition: FieldType::VarChar(100),
4648 not_null: true,
4649 unique: false,
4650 primary_key: false,
4651 auto_increment: false,
4652 default: None,
4653 },
4654 ],
4655 constraints: vec![],
4656 without_rowid: None,
4657 partition: None,
4658 interleave_in_parent: None,
4659 };
4660
4661 let stmt = op.to_statement();
4662 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4663 assert!(
4664 sql.contains("CREATE TABLE"),
4665 "SQL should contain CREATE TABLE keyword, got: {}",
4666 sql
4667 );
4668 assert!(
4669 sql.contains("users"),
4670 "SQL should reference 'users' table, got: {}",
4671 sql
4672 );
4673 assert!(
4674 sql.contains("id") && sql.contains("name"),
4675 "SQL should contain both 'id' and 'name' columns, got: {}",
4676 sql
4677 );
4678 }
4679
4680 #[test]
4681 fn test_drop_table_to_statement() {
4682 let op = Operation::DropTable {
4683 name: "users".to_string(),
4684 };
4685
4686 let stmt = op.to_statement();
4687 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4688 assert!(
4689 sql.contains("DROP TABLE"),
4690 "SQL should contain DROP TABLE keyword, got: {}",
4691 sql
4692 );
4693 assert!(
4694 sql.contains("users"),
4695 "SQL should reference 'users' table, got: {}",
4696 sql
4697 );
4698 assert!(
4699 sql.contains("CASCADE"),
4700 "SQL should include CASCADE option, got: {}",
4701 sql
4702 );
4703 }
4704
4705 #[test]
4706 fn test_add_column_to_statement() {
4707 let op = Operation::AddColumn {
4708 table: "users".to_string(),
4709 column: ColumnDefinition {
4710 name: "email".to_string(),
4711 type_definition: FieldType::VarChar(255),
4712 not_null: true,
4713 unique: false,
4714 primary_key: false,
4715 auto_increment: false,
4716 default: Some("''".to_string()),
4717 },
4718 mysql_options: None,
4719 };
4720
4721 let stmt = op.to_statement();
4722 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4723 assert!(
4724 sql.contains("ALTER TABLE"),
4725 "SQL should contain ALTER TABLE keyword, got: {}",
4726 sql
4727 );
4728 assert!(
4729 sql.contains("users"),
4730 "SQL should reference 'users' table, got: {}",
4731 sql
4732 );
4733 assert!(
4734 sql.contains("ADD COLUMN"),
4735 "SQL should contain ADD COLUMN clause, got: {}",
4736 sql
4737 );
4738 assert!(
4739 sql.contains("email"),
4740 "SQL should reference 'email' column, got: {}",
4741 sql
4742 );
4743 }
4744
4745 #[test]
4746 fn test_drop_column_to_statement() {
4747 let op = Operation::DropColumn {
4748 table: "users".to_string(),
4749 column: "email".to_string(),
4750 };
4751
4752 let stmt = op.to_statement();
4753 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4754 assert!(
4755 sql.contains("ALTER TABLE"),
4756 "SQL should contain ALTER TABLE keyword, got: {}",
4757 sql
4758 );
4759 assert!(
4760 sql.contains("users"),
4761 "SQL should reference 'users' table, got: {}",
4762 sql
4763 );
4764 assert!(
4765 sql.contains("DROP COLUMN"),
4766 "SQL should contain DROP COLUMN clause, got: {}",
4767 sql
4768 );
4769 assert!(
4770 sql.contains("email"),
4771 "SQL should reference 'email' column, got: {}",
4772 sql
4773 );
4774 }
4775
4776 #[test]
4777 fn test_alter_column_to_statement() {
4778 let op = Operation::AlterColumn {
4779 table: "users".to_string(),
4780 column: "age".to_string(),
4781 old_definition: None,
4782 new_definition: ColumnDefinition {
4783 name: "age".to_string(),
4784 type_definition: FieldType::BigInteger,
4785 not_null: true,
4786 unique: false,
4787 primary_key: false,
4788 auto_increment: false,
4789 default: None,
4790 },
4791 mysql_options: None,
4792 };
4793
4794 let stmt = op.to_statement();
4795 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4796 assert!(
4797 sql.contains("ALTER TABLE"),
4798 "SQL should contain ALTER TABLE keyword, got: {}",
4799 sql
4800 );
4801 assert!(
4802 sql.contains("users"),
4803 "SQL should reference 'users' table, got: {}",
4804 sql
4805 );
4806 assert!(
4807 sql.contains("age"),
4808 "SQL should reference 'age' column, got: {}",
4809 sql
4810 );
4811 }
4812
4813 #[test]
4814 fn test_rename_table_to_statement() {
4815 let op = Operation::RenameTable {
4816 old_name: "users".to_string(),
4817 new_name: "accounts".to_string(),
4818 };
4819
4820 let stmt = op.to_statement();
4821 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4822 assert!(
4823 sql.contains("users"),
4824 "SQL should reference old table name 'users', got: {}",
4825 sql
4826 );
4827 assert!(
4828 sql.contains("accounts"),
4829 "SQL should reference new table name 'accounts', got: {}",
4830 sql
4831 );
4832 }
4833
4834 #[test]
4835 fn test_rename_column_to_statement() {
4836 let op = Operation::RenameColumn {
4837 table: "users".to_string(),
4838 old_name: "name".to_string(),
4839 new_name: "full_name".to_string(),
4840 };
4841
4842 let stmt = op.to_statement();
4843 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4844 assert!(
4845 sql.contains("ALTER TABLE"),
4846 "SQL should contain ALTER TABLE keyword, got: {}",
4847 sql
4848 );
4849 assert!(
4850 sql.contains("users"),
4851 "SQL should reference 'users' table, got: {}",
4852 sql
4853 );
4854 assert!(
4855 sql.contains("RENAME COLUMN"),
4856 "SQL should contain RENAME COLUMN clause, got: {}",
4857 sql
4858 );
4859 assert!(
4860 sql.contains("name"),
4861 "SQL should reference old column name 'name', got: {}",
4862 sql
4863 );
4864 assert!(
4865 sql.contains("full_name"),
4866 "SQL should reference new column name 'full_name', got: {}",
4867 sql
4868 );
4869 }
4870
4871 #[test]
4872 fn test_add_constraint_to_statement() {
4873 let op = Operation::AddConstraint {
4874 table: "users".to_string(),
4875 constraint_sql: "CONSTRAINT age_check CHECK (age >= 0)".to_string(),
4876 };
4877
4878 let stmt = op.to_statement();
4879 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4880 assert!(
4881 sql.contains("ALTER TABLE"),
4882 "SQL should contain ALTER TABLE keyword, got: {}",
4883 sql
4884 );
4885 assert!(
4886 sql.contains("users"),
4887 "SQL should reference 'users' table, got: {}",
4888 sql
4889 );
4890 assert!(
4891 sql.contains("ADD"),
4892 "SQL should contain ADD keyword, got: {}",
4893 sql
4894 );
4895 assert!(
4896 sql.contains("age_check"),
4897 "SQL should contain constraint name 'age_check', got: {}",
4898 sql
4899 );
4900 }
4901
4902 #[test]
4903 fn test_drop_constraint_to_statement() {
4904 let op = Operation::DropConstraint {
4905 table: "users".to_string(),
4906 constraint_name: "age_check".to_string(),
4907 };
4908
4909 let stmt = op.to_statement();
4910 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4911 assert!(
4912 sql.contains("ALTER TABLE"),
4913 "SQL should contain ALTER TABLE keyword, got: {}",
4914 sql
4915 );
4916 assert!(
4917 sql.contains("users"),
4918 "SQL should reference 'users' table, got: {}",
4919 sql
4920 );
4921 assert!(
4922 sql.contains("DROP CONSTRAINT"),
4923 "SQL should contain DROP CONSTRAINT clause, got: {}",
4924 sql
4925 );
4926 assert!(
4927 sql.contains("age_check"),
4928 "SQL should reference constraint 'age_check', got: {}",
4929 sql
4930 );
4931 }
4932
4933 #[test]
4934 fn test_create_index_to_statement() {
4935 let op = Operation::CreateIndex {
4936 table: "users".to_string(),
4937 columns: vec!["email".to_string()],
4938 unique: false,
4939 index_type: None,
4940 where_clause: None,
4941 concurrently: false,
4942 expressions: None,
4943 mysql_options: None,
4944 operator_class: None,
4945 };
4946
4947 let stmt = op.to_statement();
4948 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4949 assert!(
4950 sql.contains("CREATE INDEX"),
4951 "SQL should contain CREATE INDEX keywords, got: {}",
4952 sql
4953 );
4954 assert!(
4955 sql.contains("users"),
4956 "SQL should reference 'users' table, got: {}",
4957 sql
4958 );
4959 assert!(
4960 sql.contains("email"),
4961 "SQL should reference 'email' column, got: {}",
4962 sql
4963 );
4964 }
4965
4966 #[test]
4967 fn test_create_unique_index_to_statement() {
4968 let op = Operation::CreateIndex {
4969 table: "users".to_string(),
4970 columns: vec!["email".to_string()],
4971 unique: true,
4972 index_type: None,
4973 where_clause: None,
4974 concurrently: false,
4975 expressions: None,
4976 mysql_options: None,
4977 operator_class: None,
4978 };
4979
4980 let stmt = op.to_statement();
4981 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4982 assert!(
4983 sql.contains("CREATE UNIQUE INDEX"),
4984 "SQL should contain CREATE UNIQUE INDEX keywords, got: {}",
4985 sql
4986 );
4987 assert!(
4988 sql.contains("users"),
4989 "SQL should reference 'users' table, got: {}",
4990 sql
4991 );
4992 assert!(
4993 sql.contains("email"),
4994 "SQL should reference 'email' column, got: {}",
4995 sql
4996 );
4997 }
4998
4999 #[test]
5000 fn test_drop_index_to_statement() {
5001 let op = Operation::DropIndex {
5002 table: "users".to_string(),
5003 columns: vec!["email".to_string()],
5004 };
5005
5006 let stmt = op.to_statement();
5007 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5008 assert!(
5009 sql.contains("DROP INDEX"),
5010 "SQL should contain DROP INDEX keywords, got: {}",
5011 sql
5012 );
5013 assert!(
5014 sql.contains("idx_users_email"),
5015 "SQL should contain generated index name 'idx_users_email', got: {}",
5016 sql
5017 );
5018 }
5019
5020 #[test]
5021 fn test_run_sql_to_statement() {
5022 let op = Operation::RunSQL {
5023 sql: "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"".to_string(),
5024 reverse_sql: Some("DROP EXTENSION \"uuid-ossp\"".to_string()),
5025 };
5026
5027 let stmt = op.to_statement();
5028 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5029 assert!(
5030 sql.contains("CREATE EXTENSION"),
5031 "SQL should contain CREATE EXTENSION keywords, got: {}",
5032 sql
5033 );
5034 assert!(
5035 sql.contains("uuid-ossp"),
5036 "SQL should reference 'uuid-ossp' extension, got: {}",
5037 sql
5038 );
5039 }
5040
5041 #[test]
5042 fn test_alter_table_comment_to_statement() {
5043 let op = Operation::AlterTableComment {
5044 table: "users".to_string(),
5045 comment: Some("User accounts table".to_string()),
5046 };
5047
5048 let stmt = op.to_statement();
5049 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5050 assert!(
5051 sql.contains("COMMENT ON TABLE"),
5052 "SQL should contain COMMENT ON TABLE keywords, got: {}",
5053 sql
5054 );
5055 assert!(
5056 sql.contains("users"),
5057 "SQL should reference 'users' table, got: {}",
5058 sql
5059 );
5060 assert!(
5061 sql.contains("User accounts table"),
5062 "SQL should include comment text 'User accounts table', got: {}",
5063 sql
5064 );
5065 }
5066
5067 #[test]
5068 fn test_alter_table_comment_null_to_statement() {
5069 let op = Operation::AlterTableComment {
5070 table: "users".to_string(),
5071 comment: None,
5072 };
5073
5074 let stmt = op.to_statement();
5075 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5076 assert!(
5077 sql.contains("COMMENT ON TABLE"),
5078 "SQL should contain COMMENT ON TABLE keywords, got: {}",
5079 sql
5080 );
5081 assert!(
5082 sql.contains("users"),
5083 "SQL should reference 'users' table, got: {}",
5084 sql
5085 );
5086 assert!(
5087 sql.contains("NULL"),
5088 "SQL should include NULL for null comment, got: {}",
5089 sql
5090 );
5091 }
5092
5093 #[test]
5094 fn test_alter_unique_together_to_statement() {
5095 let op = Operation::AlterUniqueTogether {
5096 table: "users".to_string(),
5097 unique_together: vec![vec!["email".to_string(), "username".to_string()]],
5098 };
5099
5100 let stmt = op.to_statement();
5101 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5102 assert!(
5103 sql.contains("ALTER TABLE"),
5104 "SQL should contain ALTER TABLE keyword, got: {}",
5105 sql
5106 );
5107 assert!(
5108 sql.contains("users"),
5109 "SQL should reference 'users' table, got: {}",
5110 sql
5111 );
5112 assert!(
5113 sql.contains("ADD CONSTRAINT"),
5114 "SQL should contain ADD CONSTRAINT clause, got: {}",
5115 sql
5116 );
5117 assert!(
5118 sql.contains("UNIQUE"),
5119 "SQL should contain UNIQUE keyword, got: {}",
5120 sql
5121 );
5122 assert!(
5123 sql.contains("email") && sql.contains("username"),
5124 "SQL should reference both 'email' and 'username' columns, got: {}",
5125 sql
5126 );
5127 }
5128
5129 #[test]
5130 fn test_alter_unique_together_empty() {
5131 let op = Operation::AlterUniqueTogether {
5132 table: "users".to_string(),
5133 unique_together: vec![],
5134 };
5135
5136 let stmt = op.to_statement();
5137 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5138 assert_eq!(
5139 sql, "",
5140 "SQL should be empty for empty unique_together constraint"
5141 );
5142 }
5143
5144 #[test]
5145 fn test_alter_model_options_to_statement() {
5146 let mut options = std::collections::HashMap::new();
5147 options.insert("db_table".to_string(), "custom_users".to_string());
5148
5149 let op = Operation::AlterModelOptions {
5150 table: "users".to_string(),
5151 options,
5152 };
5153
5154 let stmt = op.to_statement();
5155 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5156 assert_eq!(sql, "", "SQL should be empty for model options operation");
5157 }
5158
5159 #[test]
5160 fn test_create_inherited_table_to_statement() {
5161 let op = Operation::CreateInheritedTable {
5162 name: "admin_users".to_string(),
5163 columns: vec![ColumnDefinition {
5164 name: "admin_level".to_string(),
5165 type_definition: FieldType::Integer,
5166 not_null: true,
5167 unique: false,
5168 primary_key: false,
5169 auto_increment: false,
5170 default: Some("1".to_string()),
5171 }],
5172 base_table: "users".to_string(),
5173 join_column: "user_id".to_string(),
5174 };
5175
5176 let stmt = op.to_statement();
5177 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5178 assert!(
5179 sql.contains("CREATE TABLE"),
5180 "SQL should contain CREATE TABLE keywords, got: {}",
5181 sql
5182 );
5183 assert!(
5184 sql.contains("admin_users"),
5185 "SQL should reference 'admin_users' table, got: {}",
5186 sql
5187 );
5188 assert!(
5189 sql.contains("user_id"),
5190 "SQL should include join column 'user_id', got: {}",
5191 sql
5192 );
5193 }
5194
5195 #[test]
5196 fn test_add_discriminator_column_to_statement() {
5197 let op = Operation::AddDiscriminatorColumn {
5198 table: "users".to_string(),
5199 column_name: "user_type".to_string(),
5200 default_value: "regular".to_string(),
5201 };
5202
5203 let stmt = op.to_statement();
5204 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5205 assert!(
5206 sql.contains("ALTER TABLE"),
5207 "SQL should contain ALTER TABLE keyword, got: {}",
5208 sql
5209 );
5210 assert!(
5211 sql.contains("users"),
5212 "SQL should reference 'users' table, got: {}",
5213 sql
5214 );
5215 assert!(
5216 sql.contains("ADD COLUMN"),
5217 "SQL should contain ADD COLUMN clause, got: {}",
5218 sql
5219 );
5220 assert!(
5221 sql.contains("user_type"),
5222 "SQL should reference 'user_type' column, got: {}",
5223 sql
5224 );
5225 }
5226
5227 #[test]
5228 fn test_state_forwards_create_table() {
5229 let mut state = ProjectState::new();
5230 let op = Operation::CreateTable {
5231 name: "users".to_string(),
5232 columns: vec![
5233 ColumnDefinition {
5234 name: "id".to_string(),
5235 type_definition: FieldType::Integer,
5236 not_null: false,
5237 unique: false,
5238 primary_key: true,
5239 auto_increment: true,
5240 default: None,
5241 },
5242 ColumnDefinition {
5243 name: "name".to_string(),
5244 type_definition: FieldType::VarChar(100),
5245 not_null: true,
5246 unique: false,
5247 primary_key: false,
5248 auto_increment: false,
5249 default: None,
5250 },
5251 ],
5252 constraints: vec![],
5253 without_rowid: None,
5254 partition: None,
5255 interleave_in_parent: None,
5256 };
5257
5258 op.state_forwards("myapp", &mut state);
5259 let model = state.get_model("myapp", "users");
5260 assert!(model.is_some(), "Model 'users' should exist in state");
5261 let model = model.unwrap();
5262 assert_eq!(
5263 model.fields.len(),
5264 2,
5265 "Model should have exactly 2 fields, got: {}",
5266 model.fields.len()
5267 );
5268 assert!(
5269 model.fields.contains_key("id"),
5270 "Model should contain 'id' field"
5271 );
5272 assert!(
5273 model.fields.contains_key("name"),
5274 "Model should contain 'name' field"
5275 );
5276 }
5277
5278 #[test]
5279 fn test_state_forwards_drop_table() {
5280 let mut state = ProjectState::new();
5281 let mut model = ModelState::new("myapp", "users");
5282 model.add_field(FieldState::new("id".to_string(), FieldType::Integer, false));
5283 state.add_model(model);
5284
5285 let op = Operation::DropTable {
5286 name: "users".to_string(),
5287 };
5288
5289 op.state_forwards("myapp", &mut state);
5290 assert!(
5291 state.get_model("myapp", "users").is_none(),
5292 "Model 'users' should be removed from state after drop"
5293 );
5294 }
5295
5296 #[test]
5297 fn test_state_forwards_add_column() {
5298 let mut state = ProjectState::new();
5299 let mut model = ModelState::new("myapp", "users");
5300 model.add_field(FieldState::new("id".to_string(), FieldType::Integer, false));
5301 state.add_model(model);
5302
5303 let op = Operation::AddColumn {
5304 table: "users".to_string(),
5305 column: ColumnDefinition {
5306 name: "email".to_string(),
5307 type_definition: FieldType::VarChar(255),
5308 not_null: true,
5309 unique: false,
5310 primary_key: false,
5311 auto_increment: false,
5312 default: None,
5313 },
5314 mysql_options: None,
5315 };
5316
5317 op.state_forwards("myapp", &mut state);
5318 let model = state.get_model("myapp", "users").unwrap();
5319 assert_eq!(
5320 model.fields.len(),
5321 2,
5322 "Model should have 2 fields after adding 'email', got: {}",
5323 model.fields.len()
5324 );
5325 assert!(
5326 model.fields.contains_key("email"),
5327 "Model should contain newly added 'email' field"
5328 );
5329 }
5330
5331 #[test]
5332 fn test_state_forwards_drop_column() {
5333 let mut state = ProjectState::new();
5334 let mut model = ModelState::new("myapp", "users");
5335 model.add_field(FieldState::new("id".to_string(), FieldType::Integer, false));
5336 model.add_field(FieldState::new(
5337 "email".to_string(),
5338 FieldType::VarChar(255),
5339 false,
5340 ));
5341 state.add_model(model);
5342
5343 let op = Operation::DropColumn {
5344 table: "users".to_string(),
5345 column: "email".to_string(),
5346 };
5347
5348 op.state_forwards("myapp", &mut state);
5349 let model = state.get_model("myapp", "users").unwrap();
5350 assert_eq!(
5351 model.fields.len(),
5352 1,
5353 "Model should have 1 field after dropping 'email', got: {}",
5354 model.fields.len()
5355 );
5356 assert!(
5357 !model.fields.contains_key("email"),
5358 "Model should not contain dropped 'email' field"
5359 );
5360 }
5361
5362 #[test]
5363 fn test_state_forwards_rename_table() {
5364 let mut state = ProjectState::new();
5365 let mut model = ModelState::new("myapp", "users");
5366 model.add_field(FieldState::new("id".to_string(), FieldType::Integer, false));
5367 state.add_model(model);
5368
5369 let op = Operation::RenameTable {
5370 old_name: "users".to_string(),
5371 new_name: "accounts".to_string(),
5372 };
5373
5374 op.state_forwards("myapp", &mut state);
5375 assert!(
5376 state.get_model("myapp", "users").is_none(),
5377 "Old model name 'users' should not exist after rename"
5378 );
5379 assert!(
5380 state.get_model("myapp", "accounts").is_some(),
5381 "New model name 'accounts' should exist after rename"
5382 );
5383 }
5384
5385 #[test]
5386 fn test_state_forwards_rename_column() {
5387 let mut state = ProjectState::new();
5388 let mut model = ModelState::new("myapp", "users");
5389 model.add_field(FieldState::new(
5390 "name".to_string(),
5391 FieldType::VarChar(255),
5392 false,
5393 ));
5394 state.add_model(model);
5395
5396 let op = Operation::RenameColumn {
5397 table: "users".to_string(),
5398 old_name: "name".to_string(),
5399 new_name: "full_name".to_string(),
5400 };
5401
5402 op.state_forwards("myapp", &mut state);
5403 let model = state.get_model("myapp", "users").unwrap();
5404 assert!(
5405 !model.fields.contains_key("name"),
5406 "Old field name 'name' should not exist after rename"
5407 );
5408 assert!(
5409 model.fields.contains_key("full_name"),
5410 "New field name 'full_name' should exist after rename"
5411 );
5412 }
5413
5414 #[test]
5415 fn test_to_reverse_sql_create_table() {
5416 let op = Operation::CreateTable {
5417 name: "users".to_string(),
5418 columns: vec![],
5419 constraints: vec![],
5420 without_rowid: None,
5421 partition: None,
5422 interleave_in_parent: None,
5423 };
5424
5425 let state = ProjectState::default();
5426 let reverse = op.to_reverse_sql(&SqlDialect::Postgres, &state);
5427 assert!(
5428 reverse.is_ok() && reverse.as_ref().ok().unwrap().is_some(),
5429 "CreateTable should have reverse SQL operation"
5430 );
5431 let sql = reverse.unwrap().unwrap().join("\n");
5432 assert!(
5433 sql.contains("DROP TABLE"),
5434 "Reverse SQL should contain DROP TABLE, got: {}",
5435 sql
5436 );
5437 assert!(
5438 sql.contains("users"),
5439 "Reverse SQL should reference 'users' table, got: {}",
5440 sql
5441 );
5442 }
5443
5444 #[test]
5445 fn test_to_reverse_sql_drop_table() {
5446 let op = Operation::DropTable {
5447 name: "users".to_string(),
5448 };
5449
5450 let state = ProjectState::default();
5451 let reverse = op.to_reverse_sql(&SqlDialect::Postgres, &state);
5452 assert!(
5453 reverse.is_ok() && reverse.as_ref().ok().unwrap().is_none(),
5454 "DropTable should not have reverse SQL (cannot recreate table structure)"
5455 );
5456 }
5457
5458 #[test]
5459 fn test_to_reverse_sql_add_column() {
5460 let op = Operation::AddColumn {
5461 table: "users".to_string(),
5462 column: ColumnDefinition {
5463 name: "email".to_string(),
5464 type_definition: FieldType::VarChar(255),
5465 not_null: false,
5466 unique: false,
5467 primary_key: false,
5468 auto_increment: false,
5469 default: None,
5470 },
5471 mysql_options: None,
5472 };
5473
5474 let state = ProjectState::default();
5475 let reverse = op.to_reverse_sql(&SqlDialect::Postgres, &state);
5476 assert!(
5477 reverse.is_ok() && reverse.as_ref().ok().unwrap().is_some(),
5478 "AddColumn should have reverse SQL operation"
5479 );
5480 let sql = reverse.unwrap().unwrap().join("\n");
5481 assert!(
5482 sql.contains("DROP COLUMN"),
5483 "Reverse SQL should contain DROP COLUMN, got: {}",
5484 sql
5485 );
5486 assert!(
5487 sql.contains("email"),
5488 "Reverse SQL should reference 'email' column, got: {}",
5489 sql
5490 );
5491 }
5492
5493 fn alter_column_with_old_def() -> Operation {
5498 Operation::AlterColumn {
5499 table: "products".to_string(),
5500 column: "name".to_string(),
5501 old_definition: Some(ColumnDefinition {
5502 name: "name".to_string(),
5503 type_definition: FieldType::VarChar(50),
5504 not_null: false,
5505 unique: false,
5506 primary_key: false,
5507 auto_increment: false,
5508 default: None,
5509 }),
5510 new_definition: ColumnDefinition {
5511 name: "name".to_string(),
5512 type_definition: FieldType::Text,
5513 not_null: false,
5514 unique: false,
5515 primary_key: false,
5516 auto_increment: false,
5517 default: None,
5518 },
5519 mysql_options: None,
5520 }
5521 }
5522
5523 #[test]
5526 fn test_to_reverse_sql_alter_column_postgres() {
5527 let op = alter_column_with_old_def();
5529 let state = ProjectState::default();
5530
5531 let stmts = op
5535 .to_reverse_sql(&SqlDialect::Postgres, &state)
5536 .expect("reverse SQL should succeed")
5537 .expect("reverse SQL should be present");
5538 let sql = stmts.join("\n");
5539
5540 assert!(
5542 sql.contains("ALTER COLUMN") && sql.contains("TYPE"),
5543 "Postgres reverse SQL should use ALTER COLUMN ... TYPE syntax, got: {}",
5544 sql
5545 );
5546 assert!(
5547 sql.contains("VARCHAR(50)"),
5548 "Postgres reverse SQL should restore VARCHAR(50), got: {}",
5549 sql
5550 );
5551 assert_eq!(
5556 stmts.len(),
5557 2,
5558 "Postgres AlterColumn reverse SQL must emit two statements \
5559 (type + nullability), got: {:?}",
5560 stmts
5561 );
5562 assert!(
5563 stmts[1].contains("DROP NOT NULL"),
5564 "Postgres second statement must restore DROP NOT NULL (was_nullable), got: {}",
5565 stmts[1]
5566 );
5567 }
5568
5569 #[test]
5573 fn test_to_reverse_sql_alter_column_mysql() {
5574 let op = alter_column_with_old_def();
5576 let state = ProjectState::default();
5577
5578 let stmts = op
5581 .to_reverse_sql(&SqlDialect::Mysql, &state)
5582 .expect("reverse SQL should succeed")
5583 .expect("reverse SQL should be present");
5584 assert_eq!(
5585 stmts.len(),
5586 1,
5587 "MySQL AlterColumn reverse SQL should remain a single statement, got: {:?}",
5588 stmts
5589 );
5590 let sql = stmts.join("\n");
5591
5592 assert!(
5594 sql.contains("MODIFY COLUMN"),
5595 "MySQL reverse SQL should use MODIFY COLUMN syntax, got: {}",
5596 sql
5597 );
5598 assert!(
5599 !sql.contains("ALTER COLUMN"),
5600 "MySQL reverse SQL must not emit Postgres ALTER COLUMN syntax, got: {}",
5601 sql
5602 );
5603 assert!(
5604 !sql.contains(" TYPE "),
5605 "MySQL reverse SQL must not contain Postgres ' TYPE ' token, got: {}",
5606 sql
5607 );
5608 assert!(
5609 sql.contains("VARCHAR(50)"),
5610 "MySQL reverse SQL should restore VARCHAR(50), got: {}",
5611 sql
5612 );
5613 }
5614
5615 #[test]
5642 fn test_to_reverse_sql_alter_column_cockroachdb() {
5643 let op = alter_column_with_old_def();
5645 let state = ProjectState::default();
5646
5647 let stmts = op
5649 .to_reverse_sql(&SqlDialect::Cockroachdb, &state)
5650 .expect("reverse SQL should succeed")
5651 .expect("reverse SQL should be present");
5652
5653 assert_eq!(
5661 stmts,
5662 vec![
5663 "ALTER TABLE products ALTER COLUMN name TYPE VARCHAR(50);".to_string(),
5664 "ALTER TABLE products ALTER COLUMN name DROP NOT NULL;".to_string(),
5665 ],
5666 "CockroachDB reverse SQL must emit exactly [type_stmt, nullability_stmt], \
5667 got: {:?}",
5668 stmts
5669 );
5670
5671 for stmt in &stmts {
5678 let trimmed = stmt.trim().trim_end_matches(';').trim();
5679 assert!(
5680 !trimmed.contains(';'),
5681 "each emitted statement must be a single SQL statement, got: {}",
5682 stmt
5683 );
5684 assert!(
5685 !stmt.contains(", ALTER COLUMN"),
5686 "emitted statements must not use the Postgres comma-combined form \
5687 (CockroachDB rejects it), got: {}",
5688 stmt
5689 );
5690 }
5691 }
5692
5693 #[test]
5699 fn test_to_reverse_sql_alter_column_sqlite() {
5700 let op = alter_column_with_old_def();
5702 let state = ProjectState::default();
5703
5704 let stmts = op
5708 .to_reverse_sql(&SqlDialect::Sqlite, &state)
5709 .expect("reverse SQL should succeed")
5710 .expect("reverse SQL should be present");
5711 assert_eq!(
5712 stmts.len(),
5713 1,
5714 "SQLite AlterColumn reverse SQL should remain a single comment, got: {:?}",
5715 stmts
5716 );
5717 let sql = &stmts[0];
5718
5719 assert!(
5721 sql.trim_start().starts_with("--"),
5722 "SQLite reverse SQL should be a SQL comment (recreation handled by executor), got: {}",
5723 sql
5724 );
5725 let body = sql.trim_start_matches("--").trim_start();
5728 assert!(
5729 !body.to_uppercase().contains("ALTER TABLE"),
5730 "SQLite reverse SQL body must not emit executable ALTER TABLE statement, got: {}",
5731 sql
5732 );
5733 }
5734
5735 #[test]
5740 fn test_to_reverse_operation_alter_column_uses_old_definition() {
5741 let op = alter_column_with_old_def();
5743 let state = ProjectState::default();
5744
5745 let reverse = op
5747 .to_reverse_operation(&state)
5748 .expect("reverse operation should succeed")
5749 .expect("reverse operation should be present (old_definition is supplied)");
5750
5751 match reverse {
5753 Operation::AlterColumn { new_definition, .. } => {
5754 assert!(
5755 matches!(new_definition.type_definition, FieldType::VarChar(50)),
5756 "reverse AlterColumn should restore VARCHAR(50), got: {:?}",
5757 new_definition.type_definition
5758 );
5759 }
5760 other => panic!("reverse operation should be AlterColumn, got: {:?}", other),
5761 }
5762 }
5763
5764 #[test]
5765 fn test_to_reverse_sql_run_sql_with_reverse() {
5766 let op = Operation::RunSQL {
5767 sql: "CREATE INDEX idx_name ON users(name)".to_string(),
5768 reverse_sql: Some("DROP INDEX idx_name".to_string()),
5769 };
5770
5771 let state = ProjectState::default();
5772 let reverse = op.to_reverse_sql(&SqlDialect::Postgres, &state);
5773 assert!(
5774 reverse.is_ok() && reverse.as_ref().ok().unwrap().is_some(),
5775 "RunSQL with reverse_sql should have reverse SQL"
5776 );
5777 let sql = reverse.unwrap().unwrap().join("\n");
5778 assert!(
5779 sql.contains("DROP INDEX"),
5780 "Reverse SQL should contain provided reverse_sql, got: {}",
5781 sql
5782 );
5783 }
5784
5785 #[test]
5786 fn test_to_reverse_sql_run_sql_without_reverse() {
5787 let op = Operation::RunSQL {
5788 sql: "CREATE INDEX idx_name ON users(name)".to_string(),
5789 reverse_sql: None,
5790 };
5791
5792 let state = ProjectState::default();
5793 let reverse = op.to_reverse_sql(&SqlDialect::Postgres, &state);
5794 assert!(
5795 reverse.is_ok() && reverse.as_ref().ok().unwrap().is_none(),
5796 "RunSQL without reverse_sql should not have reverse SQL"
5797 );
5798 }
5799
5800 #[test]
5801 fn test_column_definition_new() {
5802 let col = ColumnDefinition::new("id", FieldType::Integer);
5803 assert_eq!(col.name, "id", "Column name should be 'id'");
5804 assert_eq!(
5805 col.type_definition,
5806 FieldType::Integer,
5807 "Column type should be Integer"
5808 );
5809 assert!(!col.not_null, "not_null should default to false");
5810 assert!(!col.unique, "unique should default to false");
5811 assert!(!col.primary_key, "primary_key should default to false");
5812 assert!(
5813 !col.auto_increment,
5814 "auto_increment should default to false"
5815 );
5816 assert!(col.default.is_none(), "default should be None");
5817 }
5818
5819 #[rstest]
5829 fn from_field_state_non_optional_bool_with_true_default() {
5830 let mut field_state = FieldState::new("is_active", FieldType::Boolean, false);
5836 field_state
5837 .params
5838 .insert("default".to_string(), "true".to_string());
5839
5840 let col = ColumnDefinition::from_field_state("is_active", &field_state);
5842
5843 assert_eq!(col.name, "is_active", "Column name should round-trip");
5845 assert_eq!(
5846 col.type_definition,
5847 FieldType::Boolean,
5848 "Boolean field type should round-trip"
5849 );
5850 assert!(
5851 col.not_null,
5852 "Non-Optional bool must emit NOT NULL (regression #4573)"
5853 );
5854 assert_eq!(
5855 col.default,
5856 Some("true".to_string()),
5857 "`#[field(default = true)]` must propagate as Some(\"true\")"
5858 );
5859 assert!(!col.primary_key, "Non-PK field must not be primary_key");
5860 }
5861
5862 #[rstest]
5863 fn from_field_state_non_optional_bool_with_false_default() {
5864 let mut field_state = FieldState::new("is_superuser", FieldType::Boolean, false);
5869 field_state
5870 .params
5871 .insert("default".to_string(), "false".to_string());
5872
5873 let col = ColumnDefinition::from_field_state("is_superuser", &field_state);
5875
5876 assert!(
5878 col.not_null,
5879 "Non-Optional bool with default=false must emit NOT NULL"
5880 );
5881 assert_eq!(
5882 col.default,
5883 Some("false".to_string()),
5884 "default=false must propagate as Some(\"false\")"
5885 );
5886 }
5887
5888 #[rstest]
5889 fn from_field_state_optional_bool_with_default() {
5890 let mut field_state = FieldState::new("maybe_flag", FieldType::Boolean, true);
5895 field_state
5896 .params
5897 .insert("default".to_string(), "true".to_string());
5898
5899 let col = ColumnDefinition::from_field_state("maybe_flag", &field_state);
5901
5902 assert!(
5904 !col.not_null,
5905 "Optional bool must remain NULLABLE — no regression on Option<T>"
5906 );
5907 assert_eq!(
5908 col.default,
5909 Some("true".to_string()),
5910 "Default propagation must work for Optional fields too"
5911 );
5912 }
5913
5914 #[rstest]
5915 fn from_field_state_non_optional_non_bool() {
5916 let field_state = FieldState::new("username", FieldType::VarChar(150), false);
5921
5922 let col = ColumnDefinition::from_field_state("username", &field_state);
5924
5925 assert!(
5927 col.not_null,
5928 "Non-Optional String must emit NOT NULL (regression #4573 — bug \
5929 affected all field types, not just bool)"
5930 );
5931 assert!(
5932 col.default.is_none(),
5933 "No default annotation → default = None"
5934 );
5935 }
5936
5937 #[rstest]
5938 fn from_field_state_primary_key_is_always_not_null() {
5939 let mut field_state = FieldState::new("id", FieldType::Uuid, true);
5944 field_state
5945 .params
5946 .insert("primary_key".to_string(), "true".to_string());
5947
5948 let col = ColumnDefinition::from_field_state("id", &field_state);
5950
5951 assert!(
5953 col.primary_key,
5954 "primary_key param must propagate to ColumnDefinition"
5955 );
5956 assert!(
5957 col.not_null,
5958 "Primary key must be NOT NULL regardless of nullable flag"
5959 );
5960 }
5961
5962 #[rstest]
5963 fn from_field_state_optional_field_remains_nullable() {
5964 let field_state = FieldState::new("last_login", FieldType::TimestampTz, true);
5968
5969 let col = ColumnDefinition::from_field_state("last_login", &field_state);
5971
5972 assert!(
5974 !col.not_null,
5975 "Optional field with no default must remain NULLABLE"
5976 );
5977 assert!(col.default.is_none(), "No default → default = None");
5978 assert!(!col.primary_key, "Non-PK field must not be primary_key");
5979 }
5980
5981 #[test]
5982 fn test_convert_default_value_null() {
5983 let op = Operation::CreateTable {
5984 name: "test".to_string(),
5985 columns: vec![],
5986 constraints: vec![],
5987 without_rowid: None,
5988 partition: None,
5989 interleave_in_parent: None,
5990 };
5991 let value = op.convert_default_value("null");
5992 assert!(
5993 matches!(value, Value::String(None)),
5994 "NULL value should be converted to Value::String(None)"
5995 );
5996 }
5997
5998 #[test]
5999 fn test_convert_default_value_bool() {
6000 let op = Operation::CreateTable {
6001 name: "test".to_string(),
6002 columns: vec![],
6003 constraints: vec![],
6004 without_rowid: None,
6005 partition: None,
6006 interleave_in_parent: None,
6007 };
6008 let value = op.convert_default_value("true");
6009 assert!(
6010 matches!(value, Value::Bool(Some(true))),
6011 "'true' should be converted to Value::Bool(Some(true))"
6012 );
6013
6014 let value = op.convert_default_value("false");
6015 assert!(
6016 matches!(value, Value::Bool(Some(false))),
6017 "'false' should be converted to Value::Bool(Some(false))"
6018 );
6019 }
6020
6021 #[test]
6022 fn test_convert_default_value_integer() {
6023 let op = Operation::CreateTable {
6024 name: "test".to_string(),
6025 columns: vec![],
6026 constraints: vec![],
6027 without_rowid: None,
6028 partition: None,
6029 interleave_in_parent: None,
6030 };
6031 let value = op.convert_default_value("42");
6032 assert!(
6033 matches!(value, Value::BigInt(Some(42))),
6034 "Integer '42' should be converted to Value::BigInt(Some(42))"
6035 );
6036 }
6037
6038 #[test]
6039 fn test_convert_default_value_float() {
6040 let op = Operation::CreateTable {
6041 name: "test".to_string(),
6042 columns: vec![],
6043 constraints: vec![],
6044 without_rowid: None,
6045 partition: None,
6046 interleave_in_parent: None,
6047 };
6048 let value = op.convert_default_value("3.15");
6049 assert!(
6050 matches!(value, Value::Double(_)),
6051 "Float '3.15' should be converted to Value::Double"
6052 );
6053 }
6054
6055 #[test]
6056 fn test_convert_default_value_string() {
6057 let op = Operation::CreateTable {
6058 name: "test".to_string(),
6059 columns: vec![],
6060 constraints: vec![],
6061 without_rowid: None,
6062 partition: None,
6063 interleave_in_parent: None,
6064 };
6065 let value = op.convert_default_value("'hello'");
6066 match value {
6067 Value::String(Some(s)) => assert_eq!(
6068 *s, "hello",
6069 "Quoted string should be unquoted and stored as 'hello'"
6070 ),
6071 _ => {
6072 panic!("Expected Value::String(Some(\"hello\")), got different variant")
6073 }
6074 }
6075 }
6076
6077 #[rstest]
6078 #[case("pending", "'pending'")]
6079 #[case("active", "'active'")]
6080 #[case("hello world", "'hello world'")]
6081 #[case("it's", "'it''s'")]
6082 fn test_convert_default_value_plain_string(#[case] input: &str, #[case] expected: &str) {
6083 let op = Operation::CreateTable {
6085 name: "test".to_string(),
6086 columns: vec![],
6087 constraints: vec![],
6088 without_rowid: None,
6089 partition: None,
6090 interleave_in_parent: None,
6091 };
6092
6093 let value = op.convert_default_value(input);
6095
6096 match value {
6098 Value::String(Some(s)) => assert_eq!(
6099 *s, expected,
6100 "Plain string '{input}' should be auto-quoted as SQL string literal"
6101 ),
6102 _ => {
6103 panic!("Expected Value::String(Some(\"{expected}\")), got {value:?}")
6104 }
6105 }
6106 }
6107
6108 #[rstest]
6109 #[case("CURRENT_TIMESTAMP")]
6110 #[case("current_timestamp")]
6111 #[case("CURRENT_DATE")]
6112 #[case("CURRENT_TIME")]
6113 #[case("CURRENT_USER")]
6114 #[case("SESSION_USER")]
6115 #[case("LOCALTIME")]
6116 #[case("LOCALTIMESTAMP")]
6117 fn test_convert_default_value_sql_constant(#[case] input: &str) {
6118 let op = Operation::CreateTable {
6120 name: "test".to_string(),
6121 columns: vec![],
6122 constraints: vec![],
6123 without_rowid: None,
6124 partition: None,
6125 interleave_in_parent: None,
6126 };
6127
6128 let value = op.convert_default_value(input);
6130
6131 match value {
6133 Value::String(Some(s)) => {
6134 assert_eq!(*s, input, "SQL constant '{input}' should remain unquoted")
6135 }
6136 _ => {
6137 panic!("Expected Value::String(Some(\"{input}\")), got {value:?}")
6138 }
6139 }
6140 }
6141
6142 #[rstest]
6143 #[case("NOW()")]
6144 #[case("uuid_generate_v4()")]
6145 #[case("gen_random_uuid()")]
6146 fn test_convert_default_value_sql_function(#[case] input: &str) {
6147 let op = Operation::CreateTable {
6149 name: "test".to_string(),
6150 columns: vec![],
6151 constraints: vec![],
6152 without_rowid: None,
6153 partition: None,
6154 interleave_in_parent: None,
6155 };
6156
6157 let value = op.convert_default_value(input);
6159
6160 match value {
6162 Value::String(Some(s)) => {
6163 assert_eq!(*s, input, "SQL function '{input}' should remain unquoted")
6164 }
6165 _ => {
6166 panic!("Expected Value::String(Some(\"{input}\")), got {value:?}")
6167 }
6168 }
6169 }
6170
6171 #[test]
6172 fn test_apply_column_type_integer() {
6173 let op = Operation::CreateTable {
6174 name: "test".to_string(),
6175 columns: vec![],
6176 constraints: vec![],
6177 without_rowid: None,
6178 partition: None,
6179 interleave_in_parent: None,
6180 };
6181 let col = ColumnDef::new(Alias::new("id"));
6182 let _col = op.apply_column_type(col, &FieldType::Integer);
6183 }
6186
6187 #[test]
6188 fn test_apply_column_type_varchar_with_length() {
6189 let op = Operation::CreateTable {
6190 name: "test".to_string(),
6191 columns: vec![],
6192 constraints: vec![],
6193 without_rowid: None,
6194 partition: None,
6195 interleave_in_parent: None,
6196 };
6197 let col = ColumnDef::new(Alias::new("name"));
6198 let _col = op.apply_column_type(col, &FieldType::VarChar(100));
6199 }
6202
6203 #[test]
6204 fn test_apply_column_type_custom() {
6205 let op = Operation::CreateTable {
6206 name: "test".to_string(),
6207 columns: vec![],
6208 constraints: vec![],
6209 without_rowid: None,
6210 partition: None,
6211 interleave_in_parent: None,
6212 };
6213 let col = ColumnDef::new(Alias::new("data"));
6214 let _col = op.apply_column_type(col, &FieldType::Custom("CUSTOM_TYPE".to_string()));
6215 }
6218
6219 #[test]
6220 fn test_create_index_composite() {
6221 let op = Operation::CreateIndex {
6222 table: "users".to_string(),
6223 columns: vec!["first_name".to_string(), "last_name".to_string()],
6224 unique: false,
6225 index_type: None,
6226 where_clause: None,
6227 concurrently: false,
6228 expressions: None,
6229 mysql_options: None,
6230 operator_class: None,
6231 };
6232
6233 let sql = op.to_sql(&SqlDialect::Postgres);
6234 assert!(
6235 sql.contains("first_name"),
6236 "SQL should include 'first_name' column, got: {}",
6237 sql
6238 );
6239 assert!(
6240 sql.contains("last_name"),
6241 "SQL should include 'last_name' column, got: {}",
6242 sql
6243 );
6244 assert!(
6245 sql.contains("idx_users_first_name_last_name"),
6246 "SQL should include composite index name, got: {}",
6247 sql
6248 );
6249 }
6250
6251 #[test]
6252 fn test_alter_table_comment_with_quotes() {
6253 let op = Operation::AlterTableComment {
6254 table: "users".to_string(),
6255 comment: Some("User's account table".to_string()),
6256 };
6257
6258 let stmt = op.to_statement();
6259 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
6260 assert!(
6261 sql.contains("COMMENT ON TABLE"),
6262 "SQL should contain COMMENT ON TABLE keywords, got: {}",
6263 sql
6264 );
6265 assert!(
6266 sql.contains("User''s account table"),
6267 "SQL should properly escape single quotes in comment, got: {}",
6268 sql
6269 );
6270 }
6271
6272 #[test]
6273 fn test_state_forwards_alter_column() {
6274 let mut state = ProjectState::new();
6275 let mut model = ModelState::new("myapp", "users");
6276 model.add_field(FieldState::new(
6277 "age".to_string(),
6278 FieldType::Integer,
6279 false,
6280 ));
6281 state.add_model(model);
6282
6283 let op = Operation::AlterColumn {
6284 table: "users".to_string(),
6285 column: "age".to_string(),
6286 old_definition: None,
6287 new_definition: ColumnDefinition {
6288 name: "age".to_string(),
6289 type_definition: FieldType::BigInteger,
6290 not_null: true,
6291 unique: false,
6292 primary_key: false,
6293 auto_increment: false,
6294 default: None,
6295 },
6296 mysql_options: None,
6297 };
6298
6299 op.state_forwards("myapp", &mut state);
6300 let model = state.get_model("myapp", "users").unwrap();
6301 let field = model.fields.get("age").unwrap();
6302 assert_eq!(
6303 field.field_type,
6304 FieldType::BigInteger,
6305 "Field type should be updated to BigInteger, got: {}",
6306 field.field_type
6307 );
6308 }
6309
6310 #[test]
6311 fn test_state_forwards_create_inherited_table() {
6312 let mut state = ProjectState::new();
6313 let op = Operation::CreateInheritedTable {
6314 name: "admin_users".to_string(),
6315 columns: vec![ColumnDefinition {
6316 name: "admin_level".to_string(),
6317 type_definition: FieldType::Integer,
6318 not_null: true,
6319 unique: false,
6320 primary_key: false,
6321 auto_increment: false,
6322 default: None,
6323 }],
6324 base_table: "users".to_string(),
6325 join_column: "user_id".to_string(),
6326 };
6327
6328 op.state_forwards("myapp", &mut state);
6329 let model = state.get_model("myapp", "admin_users");
6330 assert!(
6331 model.is_some(),
6332 "Inherited table 'admin_users' should exist in state"
6333 );
6334 let model = model.unwrap();
6335 assert_eq!(
6336 model.base_model,
6337 Some("users".to_string()),
6338 "base_model should be set to 'users'"
6339 );
6340 assert_eq!(
6341 model.inheritance_type,
6342 Some("joined_table".to_string()),
6343 "inheritance_type should be 'joined_table'"
6344 );
6345 }
6346
6347 #[test]
6348 fn test_state_forwards_add_discriminator_column() {
6349 let mut state = ProjectState::new();
6350 let mut model = ModelState::new("myapp", "users");
6351 model.add_field(FieldState::new("id".to_string(), FieldType::Integer, false));
6352 state.add_model(model);
6353
6354 let op = Operation::AddDiscriminatorColumn {
6355 table: "users".to_string(),
6356 column_name: "user_type".to_string(),
6357 default_value: "regular".to_string(),
6358 };
6359
6360 op.state_forwards("myapp", &mut state);
6361 let model = state.get_model("myapp", "users").unwrap();
6362 assert_eq!(
6363 model.discriminator_column,
6364 Some("user_type".to_string()),
6365 "discriminator_column should be set to 'user_type'"
6366 );
6367 assert_eq!(
6368 model.inheritance_type,
6369 Some("single_table".to_string()),
6370 "inheritance_type should be 'single_table'"
6371 );
6372 }
6373
6374 #[rstest]
6375 fn test_to_reverse_sql_create_table_quotes_identifiers() {
6376 let op = Operation::CreateTable {
6378 name: "user-data".to_string(),
6379 columns: vec![],
6380 constraints: vec![],
6381 without_rowid: None,
6382 partition: None,
6383 interleave_in_parent: None,
6384 };
6385 let state = ProjectState::default();
6386
6387 let sql = op
6389 .to_reverse_sql(&SqlDialect::Postgres, &state)
6390 .unwrap()
6391 .unwrap()
6392 .join("\n");
6393
6394 assert_eq!(
6396 sql, "DROP TABLE \"user-data\";",
6397 "Identifiers with special characters must be quoted"
6398 );
6399 }
6400
6401 #[rstest]
6402 fn test_to_reverse_sql_add_column_quotes_identifiers() {
6403 let op = Operation::AddColumn {
6405 table: "my table".to_string(),
6406 column: ColumnDefinition {
6407 name: "my column".to_string(),
6408 type_definition: FieldType::VarChar(255),
6409 not_null: false,
6410 unique: false,
6411 primary_key: false,
6412 auto_increment: false,
6413 default: None,
6414 },
6415 mysql_options: None,
6416 };
6417 let state = ProjectState::default();
6418
6419 let sql = op
6421 .to_reverse_sql(&SqlDialect::Postgres, &state)
6422 .unwrap()
6423 .unwrap()
6424 .join("\n");
6425
6426 assert_eq!(
6428 sql, "ALTER TABLE \"my table\" DROP COLUMN \"my column\";",
6429 "Table and column names with spaces must be quoted"
6430 );
6431 }
6432
6433 #[rstest]
6434 fn test_to_reverse_sql_rename_table_quotes_identifiers() {
6435 let op = Operation::RenameTable {
6437 old_name: "old; DROP TABLE users;--".to_string(),
6438 new_name: "new-name".to_string(),
6439 };
6440 let state = ProjectState::default();
6441
6442 let sql = op
6444 .to_reverse_sql(&SqlDialect::Postgres, &state)
6445 .unwrap()
6446 .unwrap()
6447 .join("\n");
6448
6449 assert_eq!(
6451 sql, "ALTER TABLE \"new-name\" RENAME TO \"old; DROP TABLE users;--\";",
6452 "SQL injection attempt must be quoted as identifier"
6453 );
6454 }
6455
6456 #[rstest]
6457 fn test_to_reverse_sql_rename_column_quotes_identifiers() {
6458 let op = Operation::RenameColumn {
6460 table: "my table".to_string(),
6461 old_name: "old col".to_string(),
6462 new_name: "new col".to_string(),
6463 };
6464 let state = ProjectState::default();
6465
6466 let sql = op
6468 .to_reverse_sql(&SqlDialect::Postgres, &state)
6469 .unwrap()
6470 .unwrap()
6471 .join("\n");
6472
6473 assert_eq!(
6475 sql, "ALTER TABLE \"my table\" RENAME COLUMN \"new col\" TO \"old col\";",
6476 "Identifiers with spaces must be quoted"
6477 );
6478 }
6479
6480 #[rstest]
6481 fn test_to_reverse_sql_create_index_quotes_identifiers() {
6482 let op = Operation::CreateIndex {
6484 table: "my-table".to_string(),
6485 columns: vec!["col a".to_string()],
6486 unique: false,
6487 index_type: None,
6488 where_clause: None,
6489 concurrently: false,
6490 expressions: None,
6491 mysql_options: None,
6492 operator_class: None,
6493 };
6494 let state = ProjectState::default();
6495
6496 let sql = op
6498 .to_reverse_sql(&SqlDialect::Postgres, &state)
6499 .unwrap()
6500 .unwrap()
6501 .join("\n");
6502
6503 assert!(
6505 sql.contains("DROP INDEX \"idx_my-table_col a\""),
6506 "Index name must be quoted, got: {}",
6507 sql
6508 );
6509 }
6510
6511 #[rstest]
6518 fn test_to_reverse_sql_create_index_emits_on_table_clause_for_mysql() {
6519 let op = Operation::CreateIndex {
6521 table: "users".to_string(),
6522 columns: vec!["email".to_string()],
6523 unique: false,
6524 index_type: None,
6525 where_clause: None,
6526 concurrently: false,
6527 expressions: None,
6528 mysql_options: None,
6529 operator_class: None,
6530 };
6531 let state = ProjectState::default();
6532
6533 let sql = op
6535 .to_reverse_sql(&SqlDialect::Mysql, &state)
6536 .unwrap()
6537 .unwrap()
6538 .join("\n");
6539
6540 assert_eq!(
6545 sql, "DROP INDEX idx_users_email ON users;",
6546 "MySQL reverse SQL must include `ON <table>` clause"
6547 );
6548 }
6549
6550 #[rstest]
6553 #[case(SqlDialect::Postgres, "DROP INDEX idx_users_email;")]
6554 #[case(SqlDialect::Sqlite, "DROP INDEX idx_users_email;")]
6555 #[case(SqlDialect::Cockroachdb, "DROP INDEX idx_users_email;")]
6556 fn test_to_reverse_sql_create_index_omits_on_table_for_non_mysql(
6557 #[case] dialect: SqlDialect,
6558 #[case] expected: &str,
6559 ) {
6560 let op = Operation::CreateIndex {
6562 table: "users".to_string(),
6563 columns: vec!["email".to_string()],
6564 unique: false,
6565 index_type: None,
6566 where_clause: None,
6567 concurrently: false,
6568 expressions: None,
6569 mysql_options: None,
6570 operator_class: None,
6571 };
6572 let state = ProjectState::default();
6573
6574 let sql = op
6576 .to_reverse_sql(&dialect, &state)
6577 .unwrap()
6578 .unwrap()
6579 .join("\n");
6580
6581 assert_eq!(
6583 sql, expected,
6584 "Non-MySQL reverse SQL must remain unchanged for dialect {:?}",
6585 dialect
6586 );
6587 }
6588
6589 #[rstest]
6590 fn test_to_reverse_sql_add_constraint_quotes_identifiers() {
6591 let op = Operation::AddConstraint {
6593 table: "my-table".to_string(),
6594 constraint_sql: "CONSTRAINT chk_positive CHECK (x > 0)".to_string(),
6595 };
6596 let state = ProjectState::default();
6597
6598 let sql = op
6600 .to_reverse_sql(&SqlDialect::Postgres, &state)
6601 .unwrap()
6602 .unwrap()
6603 .join("\n");
6604
6605 assert!(
6607 sql.contains("ALTER TABLE \"my-table\""),
6608 "Table name with special characters must be quoted, got: {}",
6609 sql
6610 );
6611 assert!(
6612 sql.contains("DROP CONSTRAINT"),
6613 "Should contain DROP CONSTRAINT, got: {}",
6614 sql
6615 );
6616 }
6617
6618 #[rstest]
6619 fn test_to_reverse_sql_bulk_load_quotes_identifiers() {
6620 let op = Operation::BulkLoad {
6622 table: "user-data".to_string(),
6623 source: BulkLoadSource::Stdin,
6624 format: BulkLoadFormat::default(),
6625 options: BulkLoadOptions::default(),
6626 };
6627 let state = ProjectState::default();
6628
6629 let sql = op
6631 .to_reverse_sql(&SqlDialect::Postgres, &state)
6632 .unwrap()
6633 .unwrap()
6634 .join("\n");
6635
6636 assert_eq!(
6638 sql, "TRUNCATE TABLE \"user-data\";",
6639 "Table name must be quoted"
6640 );
6641 }
6642
6643 #[rstest]
6648 #[case::postgres(SqlDialect::Postgres)]
6649 #[case::cockroachdb(SqlDialect::Cockroachdb)]
6650 fn test_set_auto_increment_postgres_uses_setval(#[case] dialect: SqlDialect) {
6651 let op = Operation::SetAutoIncrementValue {
6653 table: "users".to_string(),
6654 column: "id".to_string(),
6655 value: 1000,
6656 };
6657
6658 let sql = op.to_sql(&dialect);
6660
6661 assert_eq!(
6663 sql,
6664 "SELECT setval(pg_get_serial_sequence('users', 'id'), 1000, false);"
6665 );
6666 }
6667
6668 #[test]
6669 fn test_set_auto_increment_mysql_alters_table() {
6670 let op = Operation::SetAutoIncrementValue {
6672 table: "users".to_string(),
6673 column: "id".to_string(),
6674 value: 1000,
6675 };
6676
6677 let sql = op.to_sql(&SqlDialect::Mysql);
6679
6680 assert_eq!(sql, "ALTER TABLE users AUTO_INCREMENT = 1000;");
6685 }
6686
6687 #[test]
6688 fn test_set_auto_increment_sqlite_upserts_sqlite_sequence() {
6689 let op = Operation::SetAutoIncrementValue {
6691 table: "users".to_string(),
6692 column: "id".to_string(),
6693 value: 1000,
6694 };
6695
6696 let sql = op.to_sql(&SqlDialect::Sqlite);
6698
6699 assert_eq!(
6702 sql,
6703 "INSERT OR REPLACE INTO sqlite_sequence(name, seq) VALUES ('users', 1000);"
6704 );
6705 }
6706
6707 #[test]
6708 fn test_set_auto_increment_postgres_escapes_literals() {
6709 let op = Operation::SetAutoIncrementValue {
6711 table: "user's".to_string(),
6712 column: "id".to_string(),
6713 value: 42,
6714 };
6715
6716 let sql = op.to_sql(&SqlDialect::Postgres);
6718
6719 assert!(
6721 sql.contains("'user''s'"),
6722 "single quote in table name must be escaped: {}",
6723 sql
6724 );
6725 }
6726
6727 #[rstest]
6732 #[case::postgres(SqlDialect::Postgres)]
6733 #[case::mysql(SqlDialect::Mysql)]
6734 #[case::sqlite(SqlDialect::Sqlite)]
6735 #[case::cockroachdb(SqlDialect::Cockroachdb)]
6736 fn test_composite_pk_default_name(#[case] dialect: SqlDialect) {
6737 let op = Operation::CreateCompositePrimaryKey {
6739 table: "order_items".to_string(),
6740 columns: vec!["order_id".to_string(), "line_number".to_string()],
6741 constraint_name: None,
6742 };
6743
6744 let sql = op.to_sql(&dialect);
6746
6747 assert!(
6749 sql.contains("ALTER TABLE"),
6750 "SQL should use ALTER TABLE: {}",
6751 sql
6752 );
6753 assert!(
6754 sql.contains("ADD CONSTRAINT"),
6755 "SQL should add a named constraint: {}",
6756 sql
6757 );
6758 assert!(
6759 sql.contains("PRIMARY KEY"),
6760 "SQL should add PRIMARY KEY: {}",
6761 sql
6762 );
6763 assert!(
6764 sql.contains("order_items_pkey"),
6765 "Default constraint name should be table_pkey: {}",
6766 sql
6767 );
6768 assert!(
6769 sql.contains("order_id") && sql.contains("line_number"),
6770 "Both PK columns must appear: {}",
6771 sql
6772 );
6773 }
6774
6775 #[test]
6776 fn test_composite_pk_custom_name_and_quoting() {
6777 let op = Operation::CreateCompositePrimaryKey {
6779 table: "tbl".to_string(),
6780 columns: vec!["a".to_string(), "b".to_string()],
6781 constraint_name: Some("my_pk".to_string()),
6782 };
6783
6784 let sql = op.to_sql(&SqlDialect::Postgres);
6786
6787 assert_eq!(
6789 sql,
6790 "ALTER TABLE tbl ADD CONSTRAINT my_pk PRIMARY KEY (a, b);"
6791 );
6792 }
6793
6794 #[test]
6795 fn test_composite_pk_empty_columns_produces_failing_sql() {
6796 let op = Operation::CreateCompositePrimaryKey {
6802 table: "tbl".to_string(),
6803 columns: vec![],
6804 constraint_name: None,
6805 };
6806
6807 for dialect in [SqlDialect::Postgres, SqlDialect::Mysql, SqlDialect::Sqlite] {
6809 let sql = op.to_sql(&dialect);
6810
6811 assert!(
6814 sql.starts_with("SYNTAX_ERROR_create_composite_pk_on_")
6815 && sql.contains("requires_at_least_one_column"),
6816 "Empty column list must emit a syntax-error statement with diagnostic ({:?}): {}",
6817 dialect,
6818 sql
6819 );
6820 assert!(
6821 !sql.contains("SELECT 1/0"),
6822 "Must not fall back to SELECT 1/0 (silently passes on SQLite / lax MySQL): {}",
6823 sql
6824 );
6825 }
6826 }
6827
6828 #[rstest]
6839 #[case::big_integer(FieldType::BigInteger)]
6840 #[case::integer(FieldType::Integer)]
6841 #[case::small_integer(FieldType::SmallInteger)]
6842 fn test_column_to_sql_sqlite_auto_increment_pk_emits_integer(#[case] field_type: FieldType) {
6843 let mut col = ColumnDefinition::new("id", field_type);
6845 col.primary_key = true;
6846 col.auto_increment = true;
6847 col.not_null = true;
6848
6849 let sql = Operation::column_to_sql(&col, &SqlDialect::Sqlite);
6851
6852 assert!(
6855 sql.contains("INTEGER PRIMARY KEY AUTOINCREMENT"),
6856 "SQLite auto_increment PK must emit `INTEGER PRIMARY KEY AUTOINCREMENT`: {}",
6857 sql
6858 );
6859 assert!(
6860 !sql.contains("BIGINT"),
6861 "SQLite auto_increment must not emit BIGINT (rejected by SQLite): {}",
6862 sql
6863 );
6864 assert!(
6865 !sql.contains("SMALLINT"),
6866 "SQLite auto_increment must not emit SMALLINT (rejected by SQLite): {}",
6867 sql
6868 );
6869 }
6870
6871 #[test]
6872 fn test_column_to_sql_sqlite_big_integer_without_auto_increment_no_autoincrement() {
6873 let mut col = ColumnDefinition::new("count", FieldType::BigInteger);
6878 col.not_null = true;
6879
6880 let sql = Operation::column_to_sql(&col, &SqlDialect::Sqlite);
6882
6883 assert!(
6885 !sql.contains("AUTOINCREMENT"),
6886 "Non-auto_increment column must not emit AUTOINCREMENT: {}",
6887 sql
6888 );
6889 assert!(
6894 !sql.contains("BIGINT"),
6895 "emitter is expected to normalize BigInteger to INTEGER for SQLite: {}",
6896 sql
6897 );
6898 }
6899
6900 #[test]
6901 fn test_column_to_sql_postgres_big_integer_auto_increment_unchanged() {
6902 let mut col = ColumnDefinition::new("id", FieldType::BigInteger);
6904 col.primary_key = true;
6905 col.auto_increment = true;
6906 col.not_null = true;
6907
6908 let sql = Operation::column_to_sql(&col, &SqlDialect::Postgres);
6910
6911 assert!(
6913 sql.contains("BIGINT GENERATED BY DEFAULT AS IDENTITY"),
6914 "Postgres auto_increment BigInteger must emit identity syntax: {}",
6915 sql
6916 );
6917 }
6918
6919 #[test]
6920 fn test_column_to_sql_sqlite_auto_increment_uuid_pk_omits_autoincrement() {
6921 let mut col = ColumnDefinition::new("id", FieldType::Uuid);
6929 col.primary_key = true;
6930 col.auto_increment = true;
6931 col.not_null = true;
6932
6933 let sql = Operation::column_to_sql(&col, &SqlDialect::Sqlite);
6935
6936 assert!(
6938 sql.contains("PRIMARY KEY"),
6939 "UUID PK must still emit PRIMARY KEY: {}",
6940 sql
6941 );
6942 assert!(
6943 !sql.contains("AUTOINCREMENT"),
6944 "non-integer auto_increment PK must not emit AUTOINCREMENT (SQLite rejects it): {}",
6945 sql
6946 );
6947 assert!(
6952 !sql.contains("INTEGER"),
6953 "UUID column type must not be widened to INTEGER: {}",
6954 sql
6955 );
6956 }
6957
6958 #[test]
6959 fn test_column_to_sql_without_pk_sqlite_auto_increment_emits_integer() {
6960 let mut col = ColumnDefinition::new("id", FieldType::BigInteger);
6962 col.auto_increment = true;
6963 col.not_null = true;
6964
6965 let sql = Operation::column_to_sql_without_pk(&col, &SqlDialect::Sqlite);
6967
6968 assert!(
6970 sql.contains("INTEGER"),
6971 "SQLite auto_increment column (composite PK path) must emit INTEGER: {}",
6972 sql
6973 );
6974 assert!(
6975 !sql.contains("BIGINT"),
6976 "SQLite auto_increment must not emit BIGINT in composite PK path: {}",
6977 sql
6978 );
6979 }
6980
6981 mod resolve_foreign_key_column_type_tests {
6982 use super::super::resolve_foreign_key_column_type_with;
6983 use super::FieldType;
6984 use crate::migrations::autodetector::FieldState;
6985 use crate::migrations::model_registry::{FieldMetadata, ModelMetadata, ModelRegistry};
6986
6987 fn target_model(app: &str, name: &str, table: &str, pk_type: FieldType) -> ModelMetadata {
6990 let mut meta = ModelMetadata::new(app, name, table);
6991 meta.add_field(
6992 "id".to_string(),
6993 FieldMetadata::new(pk_type).with_param("primary_key", "true"),
6994 );
6995 meta
6996 }
6997
6998 fn fk_field_state(target_model: &str, target_app: Option<&str>) -> FieldState {
7002 let mut fs = FieldState::new("owner_id", FieldType::Uuid, false);
7003 fs.params
7004 .insert("fk_target".to_string(), target_model.to_string());
7005 if let Some(app) = target_app {
7006 fs.params
7007 .insert("fk_target_app".to_string(), app.to_string());
7008 }
7009 fs
7010 }
7011
7012 #[test]
7013 fn qualified_hit_resolves_to_target_pk_type() {
7014 let registry = ModelRegistry::new();
7016 registry.register_model(target_model(
7017 "auth",
7018 "User",
7019 "auth_user",
7020 FieldType::BigInteger,
7021 ));
7022 let fs = fk_field_state("User", Some("auth"));
7023
7024 let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7026
7027 assert_eq!(resolved, Some(FieldType::BigInteger));
7029 }
7030
7031 #[test]
7032 fn qualified_miss_falls_back_to_by_name_when_unambiguous() {
7033 let registry = ModelRegistry::new();
7036 registry.register_model(target_model(
7037 "reinhardt_auth",
7038 "User",
7039 "auth_user",
7040 FieldType::Uuid,
7041 ));
7042 let fs = fk_field_state("User", Some("blog"));
7044
7045 let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7047
7048 assert_eq!(resolved, Some(FieldType::Uuid));
7051 }
7052
7053 #[test]
7054 fn ambiguous_by_name_returns_none() {
7055 let registry = ModelRegistry::new();
7057 registry.register_model(target_model(
7058 "auth",
7059 "User",
7060 "auth_user",
7061 FieldType::BigInteger,
7062 ));
7063 registry.register_model(target_model(
7064 "billing",
7065 "User",
7066 "billing_user",
7067 FieldType::Uuid,
7068 ));
7069 let fs = fk_field_state("User", None);
7071
7072 let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7074
7075 assert_eq!(resolved, None);
7078 }
7079
7080 #[test]
7081 fn path_typed_disambiguates_ambiguous_name() {
7082 let registry = ModelRegistry::new();
7089 registry.register_model(target_model(
7090 "blog",
7091 "User",
7092 "blog_user",
7093 FieldType::BigInteger,
7094 ));
7095 registry.register_model(target_model(
7096 "reinhardt_auth",
7097 "User",
7098 "reinhardt_auth_user",
7099 FieldType::Uuid,
7100 ));
7101 let fs = fk_field_state("User", Some("reinhardt_auth"));
7102
7103 let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7105
7106 assert_eq!(resolved, Some(FieldType::Uuid));
7109 }
7110
7111 #[test]
7112 fn qualified_miss_with_ambiguous_by_name_returns_none() {
7113 let registry = ModelRegistry::new();
7117 registry.register_model(target_model(
7118 "auth",
7119 "User",
7120 "auth_user",
7121 FieldType::BigInteger,
7122 ));
7123 registry.register_model(target_model(
7124 "billing",
7125 "User",
7126 "billing_user",
7127 FieldType::Uuid,
7128 ));
7129 let fs = fk_field_state("User", Some("blog")); let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7133
7134 assert_eq!(resolved, None);
7136 }
7137
7138 #[test]
7139 fn no_fk_target_param_returns_none() {
7140 let registry = ModelRegistry::new();
7142 registry.register_model(target_model(
7143 "auth",
7144 "User",
7145 "auth_user",
7146 FieldType::BigInteger,
7147 ));
7148 let fs = FieldState::new("name", FieldType::VarChar(64), false);
7149
7150 let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7152
7153 assert_eq!(resolved, None);
7155 }
7156 }
7157}