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 old_definition,
1647 new_definition,
1648 mysql_options,
1649 ..
1650 } => {
1651 let sql_type = new_definition.type_definition.to_sql_for_dialect(dialect);
1652 match dialect {
1653 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
1654 let mut statements = Vec::new();
1655 if old_definition
1656 .as_ref()
1657 .is_some_and(|old_definition| old_definition.default.is_some())
1658 {
1659 statements.push(format!(
1660 "ALTER TABLE {} ALTER COLUMN {} DROP DEFAULT;",
1661 quote_identifier(table),
1662 quote_identifier(column)
1663 ));
1664 }
1665 statements.push(format!(
1666 "ALTER TABLE {} ALTER COLUMN {} TYPE {};",
1667 quote_identifier(table),
1668 quote_identifier(column),
1669 sql_type
1670 ));
1671 if let Some(default) = &new_definition.default {
1672 statements.push(format!(
1673 "ALTER TABLE {} ALTER COLUMN {} SET DEFAULT {};",
1674 quote_identifier(table),
1675 quote_identifier(column),
1676 default
1677 ));
1678 }
1679 statements.join(" ")
1680 }
1681 SqlDialect::Mysql => {
1682 let base_sql = format!(
1683 "ALTER TABLE {} MODIFY COLUMN {}",
1684 quote_identifier(table),
1685 Self::column_to_sql(new_definition, dialect)
1686 );
1687
1688 if let Some(opts) = mysql_options {
1690 let suffix = opts.to_sql_suffix();
1691 if !suffix.is_empty() {
1692 return format!("{}{};", base_sql, suffix);
1693 }
1694 }
1695
1696 format!("{};", base_sql)
1697 }
1698 SqlDialect::Sqlite => {
1699 format!(
1700 "-- SQLite does not support ALTER COLUMN, table recreation required for {}",
1701 quote_identifier(table)
1702 )
1703 }
1704 }
1705 }
1706 Operation::RenameColumn {
1707 table,
1708 old_name,
1709 new_name,
1710 } => {
1711 format!(
1712 "ALTER TABLE {} RENAME COLUMN {} TO {};",
1713 quote_identifier(table),
1714 quote_identifier(old_name),
1715 quote_identifier(new_name)
1716 )
1717 }
1718 Operation::RenameTable { old_name, new_name } => {
1719 format!(
1720 "ALTER TABLE {} RENAME TO {};",
1721 quote_identifier(old_name),
1722 quote_identifier(new_name)
1723 )
1724 }
1725 Operation::AddConstraint {
1726 table,
1727 constraint_sql,
1728 } => {
1729 format!(
1730 "ALTER TABLE {} ADD {};",
1731 quote_identifier(table),
1732 constraint_sql
1733 )
1734 }
1735 Operation::DropConstraint {
1736 table,
1737 constraint_name,
1738 } => {
1739 format!(
1740 "ALTER TABLE {} DROP CONSTRAINT {};",
1741 quote_identifier(table),
1742 quote_identifier(constraint_name)
1743 )
1744 }
1745 Operation::CreateIndex {
1746 table,
1747 columns,
1748 unique,
1749 index_type,
1750 where_clause,
1751 concurrently,
1752 expressions,
1753 mysql_options,
1754 operator_class,
1755 } => {
1756 let unique_str = if *unique { "UNIQUE " } else { "" };
1757
1758 let concurrent_str = if *concurrently && matches!(dialect, SqlDialect::Postgres) {
1760 "CONCURRENTLY "
1761 } else {
1762 ""
1763 };
1764
1765 let (mysql_prefix, effective_unique) = match (index_type, dialect) {
1767 (Some(IndexType::Fulltext), SqlDialect::Mysql) => ("FULLTEXT ", ""),
1768 (Some(IndexType::Spatial), SqlDialect::Mysql) => ("SPATIAL ", ""),
1769 _ => ("", unique_str),
1770 };
1771
1772 let (index_content, name_suffix) =
1774 if let Some(exprs) = expressions.as_ref().filter(|e| !e.is_empty()) {
1775 let content = exprs.join(", ");
1778 let suffix = "expr";
1779 (content, suffix.to_string())
1780 } else {
1781 let content = if let Some(op_class) = operator_class {
1783 if matches!(dialect, SqlDialect::Postgres) {
1785 columns
1786 .iter()
1787 .map(|c| format!("{} {}", quote_identifier(c), op_class))
1788 .collect::<Vec<_>>()
1789 .join(", ")
1790 } else {
1791 columns
1793 .iter()
1794 .map(|c| quote_identifier(c).to_string())
1795 .collect::<Vec<_>>()
1796 .join(", ")
1797 }
1798 } else {
1799 columns
1801 .iter()
1802 .map(|c| quote_identifier(c).to_string())
1803 .collect::<Vec<_>>()
1804 .join(", ")
1805 };
1806 (content, columns.join("_"))
1807 };
1808
1809 let idx_name = format!("idx_{}_{}", table, name_suffix);
1810
1811 let using_clause = match (index_type, dialect) {
1813 (Some(IndexType::BTree), _) => String::new(), (Some(idx_type), SqlDialect::Postgres | SqlDialect::Cockroachdb) => {
1815 format!(" USING {}", idx_type)
1816 }
1817 (Some(IndexType::Fulltext | IndexType::Spatial), SqlDialect::Mysql) => {
1819 String::new()
1820 }
1821 _ => String::new(),
1822 };
1823
1824 let mut sql = match dialect {
1829 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
1830 format!(
1832 "CREATE {}INDEX {}{}",
1833 effective_unique,
1834 concurrent_str,
1835 quote_identifier(&idx_name)
1836 )
1837 }
1838 SqlDialect::Mysql => {
1839 format!(
1841 "CREATE {}{}INDEX {}",
1842 mysql_prefix,
1843 effective_unique,
1844 quote_identifier(&idx_name)
1845 )
1846 }
1847 SqlDialect::Sqlite => {
1848 format!(
1850 "CREATE {}INDEX {}",
1851 effective_unique,
1852 quote_identifier(&idx_name)
1853 )
1854 }
1855 };
1856 sql.push_str(&format!(
1860 " ON {}{} ({})",
1861 quote_identifier(table),
1862 using_clause,
1863 index_content
1864 ));
1865
1866 if let Some(where_cond) = where_clause
1868 && !matches!(dialect, SqlDialect::Mysql)
1869 {
1870 sql.push_str(&format!(" WHERE {}", where_cond));
1871 }
1872
1873 if matches!(dialect, SqlDialect::Mysql)
1875 && let Some(opts) = mysql_options
1876 {
1877 let suffix = opts.to_sql_suffix();
1878 if !suffix.is_empty() {
1879 sql.push_str(&suffix);
1880 }
1881 }
1882
1883 sql.push(';');
1884 sql
1885 }
1886 Operation::DropIndex { table, columns } => {
1887 let idx_name = format!("idx_{}_{}", table, columns.join("_"));
1888 match dialect {
1889 SqlDialect::Mysql => {
1890 format!(
1891 "DROP INDEX {} ON {};",
1892 quote_identifier(&idx_name),
1893 quote_identifier(table)
1894 )
1895 }
1896 SqlDialect::Postgres | SqlDialect::Sqlite | SqlDialect::Cockroachdb => {
1897 format!("DROP INDEX {};", quote_identifier(&idx_name))
1898 }
1899 }
1900 }
1901 Operation::RunSQL { sql, .. } => sql.to_string(),
1902 Operation::RunRust { code, .. } => {
1903 format!("-- RunRust: {}", code.lines().next().unwrap_or(""))
1905 }
1906 Operation::AlterTableComment { table, comment } => match dialect {
1907 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
1908 if let Some(comment_text) = comment {
1909 format!(
1910 "COMMENT ON TABLE {} IS '{}';",
1911 quote_identifier(table),
1912 comment_text
1913 )
1914 } else {
1915 format!("COMMENT ON TABLE {} IS NULL;", quote_identifier(table))
1916 }
1917 }
1918 SqlDialect::Mysql => {
1919 if let Some(comment_text) = comment {
1920 format!(
1921 "ALTER TABLE {} COMMENT='{}';",
1922 quote_identifier(table),
1923 comment_text
1924 )
1925 } else {
1926 format!("ALTER TABLE {} COMMENT='';", quote_identifier(table))
1927 }
1928 }
1929 SqlDialect::Sqlite => String::new(),
1930 },
1931 Operation::AlterUniqueTogether {
1932 table,
1933 unique_together,
1934 } => {
1935 let mut sql = Vec::new();
1936 for (idx, fields) in unique_together.iter().enumerate() {
1937 let constraint_name = format!("{}_{}_uniq", table, idx);
1938 let fields_str = fields
1939 .iter()
1940 .map(|f| quote_identifier(f))
1941 .collect::<Vec<_>>()
1942 .join(", ");
1943 sql.push(format!(
1944 "ALTER TABLE {} ADD CONSTRAINT {} UNIQUE ({});",
1945 quote_identifier(table),
1946 quote_identifier(&constraint_name),
1947 fields_str
1948 ));
1949 }
1950 sql.join("\n")
1951 }
1952 Operation::AlterModelOptions { .. } => String::new(),
1953 Operation::CreateInheritedTable {
1954 name,
1955 columns,
1956 base_table,
1957 join_column,
1958 } => {
1959 let mut parts = Vec::new();
1960 parts.push(format!(
1961 " {} INTEGER REFERENCES {}(id)",
1962 quote_identifier(join_column),
1963 quote_identifier(base_table)
1964 ));
1965 for col in columns {
1966 parts.push(format!(" {}", Self::column_to_sql(col, dialect)));
1967 }
1968 format!(
1969 "CREATE TABLE {} (\n{}\n);",
1970 quote_identifier(name),
1971 parts.join(",\n")
1972 )
1973 }
1974 Operation::AddDiscriminatorColumn {
1975 table,
1976 column_name,
1977 default_value,
1978 } => {
1979 format!(
1980 "ALTER TABLE {} ADD COLUMN {} VARCHAR(50) DEFAULT '{}';",
1981 quote_identifier(table),
1982 quote_identifier(column_name),
1983 default_value
1984 )
1985 }
1986 Operation::MoveModel {
1987 rename_table,
1988 old_table_name,
1989 new_table_name,
1990 ..
1991 } => {
1992 if *rename_table {
1995 if let (Some(old_name), Some(new_name)) = (old_table_name, new_table_name) {
1996 match dialect {
1997 SqlDialect::Postgres | SqlDialect::Sqlite | SqlDialect::Cockroachdb => {
1998 format!(
1999 "ALTER TABLE {} RENAME TO {};",
2000 quote_identifier(old_name),
2001 quote_identifier(new_name)
2002 )
2003 }
2004 SqlDialect::Mysql => {
2005 format!(
2006 "RENAME TABLE {} TO {};",
2007 quote_identifier(old_name),
2008 quote_identifier(new_name)
2009 )
2010 }
2011 }
2012 } else {
2013 "-- MoveModel: No table rename specified".to_string()
2014 }
2015 } else {
2016 "-- MoveModel: State-only operation (no table rename)".to_string()
2018 }
2019 }
2020 Operation::CreateSchema {
2021 name,
2022 if_not_exists,
2023 } => {
2024 let if_not_exists_clause = if *if_not_exists { " IF NOT EXISTS" } else { "" };
2025 format!(
2026 "CREATE SCHEMA{} {};",
2027 if_not_exists_clause,
2028 quote_identifier(name)
2029 )
2030 }
2031 Operation::DropSchema {
2032 name,
2033 cascade,
2034 if_exists,
2035 } => {
2036 let if_exists_clause = if *if_exists { " IF EXISTS" } else { "" };
2037 let cascade_clause = if *cascade { " CASCADE" } else { "" };
2038 format!(
2039 "DROP SCHEMA{} {}{};",
2040 if_exists_clause,
2041 quote_identifier(name),
2042 cascade_clause
2043 )
2044 }
2045 Operation::CreateExtension {
2046 name,
2047 if_not_exists,
2048 schema,
2049 } => {
2050 let if_not_exists_clause = if *if_not_exists { " IF NOT EXISTS" } else { "" };
2052 let schema_clause = if let Some(s) = schema {
2053 format!(" SCHEMA {}", quote_identifier(s))
2054 } else {
2055 String::new()
2056 };
2057 format!(
2058 "CREATE EXTENSION{} {}{};",
2059 if_not_exists_clause,
2060 quote_identifier(name),
2061 schema_clause
2062 )
2063 }
2064 Operation::BulkLoad {
2065 table,
2066 source,
2067 format,
2068 options,
2069 } => Self::bulk_load_to_sql(table, source, format, options, dialect),
2070 Operation::SetAutoIncrementValue {
2071 table,
2072 column,
2073 value,
2074 } => Self::set_auto_increment_to_sql(table, column, *value, dialect),
2075 Operation::CreateCompositePrimaryKey {
2076 table,
2077 columns,
2078 constraint_name,
2079 } => Self::create_composite_pk_to_sql(table, columns, constraint_name.as_deref()),
2080 }
2081 }
2082
2083 fn set_auto_increment_to_sql(
2090 table: &str,
2091 column: &str,
2092 value: i64,
2093 dialect: &SqlDialect,
2094 ) -> String {
2095 match dialect {
2096 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
2097 format!(
2102 "SELECT setval(pg_get_serial_sequence({}, {}), {}, false);",
2103 quote_literal(table),
2104 quote_literal(column),
2105 value
2106 )
2107 }
2108 SqlDialect::Mysql => {
2109 format!(
2110 "ALTER TABLE {} AUTO_INCREMENT = {};",
2111 quote_identifier(table),
2112 value
2113 )
2114 }
2115 SqlDialect::Sqlite => {
2116 format!(
2121 "INSERT OR REPLACE INTO sqlite_sequence(name, seq) VALUES ({}, {});",
2122 quote_literal(table),
2123 value
2124 )
2125 }
2126 }
2127 }
2128
2129 fn create_composite_pk_to_sql(
2166 table: &str,
2167 columns: &[String],
2168 constraint_name: Option<&str>,
2169 ) -> String {
2170 if columns.is_empty() {
2171 return format!(
2178 "SYNTAX_ERROR_create_composite_pk_on_{}_requires_at_least_one_column;",
2179 table.replace(|c: char| !c.is_ascii_alphanumeric(), "_")
2180 );
2181 }
2182
2183 let default_name;
2184 let name: &str = match constraint_name {
2185 Some(n) => n,
2186 None => {
2187 default_name = format!("{}_pkey", table);
2188 &default_name
2189 }
2190 };
2191
2192 let quoted_columns = columns
2193 .iter()
2194 .map(|c| quote_identifier(c).to_string())
2195 .collect::<Vec<_>>()
2196 .join(", ");
2197
2198 format!(
2199 "ALTER TABLE {} ADD CONSTRAINT {} PRIMARY KEY ({});",
2200 quote_identifier(table),
2201 quote_identifier(name),
2202 quoted_columns
2203 )
2204 }
2205
2206 fn bulk_load_to_sql(
2208 table: &str,
2209 source: &BulkLoadSource,
2210 format: &BulkLoadFormat,
2211 options: &BulkLoadOptions,
2212 dialect: &SqlDialect,
2213 ) -> String {
2214 match dialect {
2215 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
2216 Self::postgres_copy_from_sql(table, source, format, options)
2217 }
2218 SqlDialect::Mysql => Self::mysql_load_data_sql(table, source, format, options),
2219 SqlDialect::Sqlite => {
2220 format!(
2222 "-- SQLite does not support bulk loading. Use INSERT statements instead for table {}",
2223 quote_identifier(table)
2224 )
2225 }
2226 }
2227 }
2228
2229 fn postgres_copy_from_sql(
2231 table: &str,
2232 source: &BulkLoadSource,
2233 format: &BulkLoadFormat,
2234 options: &BulkLoadOptions,
2235 ) -> String {
2236 let source_clause = match source {
2237 BulkLoadSource::File(path) => format!("'{}'", path),
2238 BulkLoadSource::Stdin => "STDIN".to_string(),
2239 BulkLoadSource::Program(cmd) => format!("PROGRAM '{}'", cmd),
2240 };
2241
2242 let columns_clause = if let Some(cols) = &options.columns {
2243 let quoted_cols = cols
2244 .iter()
2245 .map(|c| quote_identifier(c))
2246 .collect::<Vec<_>>()
2247 .join(", ");
2248 format!(" ({})", quoted_cols)
2249 } else {
2250 String::new()
2251 };
2252
2253 let mut with_options = Vec::new();
2254
2255 with_options.push(format!("FORMAT {}", format));
2257
2258 if let Some(delim) = options.delimiter {
2260 with_options.push(format!("DELIMITER '{}'", delim));
2261 }
2262
2263 if let Some(null_str) = &options.null_string {
2265 with_options.push(format!("NULL '{}'", null_str));
2266 }
2267
2268 if options.header {
2270 with_options.push("HEADER true".to_string());
2271 }
2272
2273 if let Some(quote) = options.quote {
2275 with_options.push(format!("QUOTE '{}'", quote));
2276 }
2277
2278 if let Some(escape) = options.escape {
2280 with_options.push(format!("ESCAPE '{}'", escape));
2281 }
2282
2283 format!(
2284 "COPY {}{} FROM {} WITH ({});",
2285 quote_identifier(table),
2286 columns_clause,
2287 source_clause,
2288 with_options.join(", ")
2289 )
2290 }
2291
2292 fn mysql_load_data_sql(
2294 table: &str,
2295 source: &BulkLoadSource,
2296 format: &BulkLoadFormat,
2297 options: &BulkLoadOptions,
2298 ) -> String {
2299 let local_clause = if options.local { " LOCAL" } else { "" };
2300
2301 let file_path = match source {
2302 BulkLoadSource::File(path) => path.clone(),
2303 BulkLoadSource::Stdin => {
2304 return format!(
2305 "-- MySQL does not support LOAD DATA from STDIN directly for table {}",
2306 quote_identifier(table)
2307 );
2308 }
2309 BulkLoadSource::Program(_) => {
2310 return format!(
2311 "-- MySQL does not support LOAD DATA from PROGRAM directly for table {}",
2312 quote_identifier(table)
2313 );
2314 }
2315 };
2316
2317 let columns_clause = if let Some(cols) = &options.columns {
2318 let quoted_cols = cols
2319 .iter()
2320 .map(|c| quote_identifier(c))
2321 .collect::<Vec<_>>()
2322 .join(", ");
2323 format!(" ({})", quoted_cols)
2324 } else {
2325 String::new()
2326 };
2327
2328 let delimiter = options.delimiter.unwrap_or(match format {
2330 BulkLoadFormat::Csv => ',',
2331 BulkLoadFormat::Text | BulkLoadFormat::Binary => '\t',
2332 });
2333
2334 let mut field_options = Vec::new();
2335 field_options.push(format!("TERMINATED BY '{}'", delimiter));
2336
2337 if *format == BulkLoadFormat::Csv {
2339 let quote = options.quote.unwrap_or('"');
2340 field_options.push(format!("ENCLOSED BY '{}'", quote));
2341 }
2342
2343 if let Some(escape) = options.escape {
2345 field_options.push(format!("ESCAPED BY '{}'", escape));
2346 }
2347
2348 let line_terminator = options
2350 .line_terminator
2351 .clone()
2352 .unwrap_or_else(|| "\\n".to_string());
2353
2354 let encoding_clause = if let Some(enc) = &options.encoding {
2356 format!(" CHARACTER SET {}", enc)
2357 } else {
2358 String::new()
2359 };
2360
2361 let ignore_clause = if options.header {
2363 " IGNORE 1 LINES"
2364 } else {
2365 ""
2366 };
2367
2368 format!(
2369 "LOAD DATA{} INFILE '{}'{} INTO TABLE {} FIELDS {} LINES TERMINATED BY '{}'{}{};",
2370 local_clause,
2371 file_path,
2372 encoding_clause,
2373 quote_identifier(table),
2374 field_options.join(" "),
2375 line_terminator,
2376 ignore_clause,
2377 columns_clause
2378 )
2379 }
2380
2381 pub fn to_reverse_sql(
2413 &self,
2414 dialect: &SqlDialect,
2415 project_state: &ProjectState,
2416 ) -> super::Result<Option<Vec<String>>> {
2417 match self {
2418 Operation::CreateTable { name, .. } => Ok(Some(vec![format!(
2419 "DROP TABLE {};",
2420 quote_identifier(name)
2421 )])),
2422 Operation::AddColumn { table, column, .. } => Ok(Some(vec![format!(
2423 "ALTER TABLE {} DROP COLUMN {};",
2424 quote_identifier(table),
2425 quote_identifier(&column.name)
2426 )])),
2427 Operation::RunSQL { reverse_sql, .. } => {
2428 Ok(reverse_sql.as_ref().map(|s| vec![s.to_string()]))
2429 }
2430 Operation::RunRust { reverse_code, .. } => Ok(reverse_code.as_ref().map(|code| {
2431 vec![format!(
2432 "-- RunRust (reverse): {}",
2433 code.lines().next().unwrap_or("")
2434 )]
2435 })),
2436 Operation::RenameTable { old_name, new_name } => Ok(Some(vec![format!(
2438 "ALTER TABLE {} RENAME TO {};",
2439 quote_identifier(new_name),
2440 quote_identifier(old_name)
2441 )])),
2442 Operation::RenameColumn {
2443 table,
2444 old_name,
2445 new_name,
2446 } => Ok(Some(vec![format!(
2447 "ALTER TABLE {} RENAME COLUMN {} TO {};",
2448 quote_identifier(table),
2449 quote_identifier(new_name),
2450 quote_identifier(old_name)
2451 )])),
2452 Operation::CreateIndex { table, columns, .. } => {
2453 let columns_joined = columns.join("_");
2456 let index_name = format!("idx_{}_{}", table, columns_joined);
2457 let sql = match dialect {
2461 SqlDialect::Mysql => format!(
2462 "DROP INDEX {} ON {};",
2463 quote_identifier(&index_name),
2464 quote_identifier(table)
2465 ),
2466 SqlDialect::Postgres | SqlDialect::Sqlite | SqlDialect::Cockroachdb => {
2467 format!("DROP INDEX {};", quote_identifier(&index_name))
2468 }
2469 };
2470 Ok(Some(vec![sql]))
2471 }
2472 Operation::AddConstraint {
2473 table,
2474 constraint_sql,
2475 } => {
2476 let constraint_name =
2479 Self::extract_constraint_name(constraint_sql).ok_or_else(|| {
2480 super::MigrationError::InvalidMigration(format!(
2481 "Cannot extract constraint name from: {}",
2482 constraint_sql
2483 ))
2484 })?;
2485 Ok(Some(vec![format!(
2486 "ALTER TABLE {} DROP CONSTRAINT {};",
2487 quote_identifier(table),
2488 quote_identifier(&constraint_name)
2489 )]))
2490 }
2491 Operation::DropColumn { table, column } => {
2493 if let Some(model) = project_state.find_model_by_table(table)
2495 && let Some(field) = model.get_field(column)
2496 {
2497 let col_def = ColumnDefinition::from_field_state(column.clone(), field);
2498 let col_sql = Self::column_to_sql(&col_def, dialect);
2499 return Ok(Some(vec![format!(
2500 "ALTER TABLE {} ADD COLUMN {};",
2501 quote_identifier(table),
2502 col_sql
2503 )]));
2504 }
2505 Ok(None)
2507 }
2508 Operation::AlterColumn {
2509 table,
2510 column,
2511 old_definition,
2512 new_definition: _,
2513 ..
2514 } => {
2515 let resolved_old_def = old_definition.clone().or_else(|| {
2518 project_state
2519 .find_model_by_table(table)
2520 .and_then(|model| model.get_field(column))
2521 .map(|field| ColumnDefinition::from_field_state(column.clone(), field))
2522 });
2523
2524 let Some(old_def) = resolved_old_def else {
2525 return Ok(None);
2527 };
2528
2529 let type_sql = old_def.type_definition.to_sql_for_dialect(dialect);
2530 let null_clause = if old_def.not_null { " NOT NULL" } else { "" };
2531
2532 let stmts = match dialect {
2539 SqlDialect::Postgres | SqlDialect::Cockroachdb => {
2540 let nullability_clause = if old_def.not_null {
2555 "SET NOT NULL"
2556 } else {
2557 "DROP NOT NULL"
2558 };
2559 vec![
2560 format!(
2561 "ALTER TABLE {table} ALTER COLUMN {column} TYPE {type_sql};",
2562 table = quote_identifier(table),
2563 column = quote_identifier(column),
2564 type_sql = type_sql,
2565 ),
2566 format!(
2567 "ALTER TABLE {table} ALTER COLUMN {column} {nullability_clause};",
2568 table = quote_identifier(table),
2569 column = quote_identifier(column),
2570 nullability_clause = nullability_clause,
2571 ),
2572 ]
2573 }
2574 SqlDialect::Mysql => vec![format!(
2575 "ALTER TABLE {} MODIFY COLUMN {} {}{};",
2576 quote_identifier(table),
2577 quote_identifier(column),
2578 type_sql,
2579 null_clause
2580 )],
2581 SqlDialect::Sqlite => vec![format!(
2582 "-- SQLite does not support ALTER COLUMN, table recreation required for {}",
2583 quote_identifier(table)
2584 )],
2585 };
2586 Ok(Some(stmts))
2587 }
2588 Operation::DropIndex { table, columns } => {
2589 let columns_joined = columns.join("_");
2593 let index_name = format!("idx_{}_{}", table, columns_joined);
2594 let columns_list = columns
2595 .iter()
2596 .map(|c| quote_identifier(c).to_string())
2597 .collect::<Vec<_>>()
2598 .join(", ");
2599 Ok(Some(vec![format!(
2600 "CREATE INDEX {} ON {} ({});",
2601 quote_identifier(&index_name),
2602 quote_identifier(table),
2603 columns_list
2604 )]))
2605 }
2606 Operation::DropConstraint {
2607 table,
2608 constraint_name,
2609 } => {
2610 if let Some(model) = project_state.find_model_by_table(table)
2612 && let Some(constraint_def) = model
2613 .constraints
2614 .iter()
2615 .find(|c| c.name == *constraint_name)
2616 {
2617 let constraint = constraint_def.to_constraint();
2618 return Ok(Some(vec![format!(
2619 "ALTER TABLE {} ADD {};",
2620 quote_identifier(table),
2621 constraint
2622 )]));
2623 }
2624 Ok(None)
2626 }
2627 Operation::DropTable { name } => {
2628 if let Some(model) = project_state.find_model_by_table(name) {
2630 let mut parts = Vec::new();
2631
2632 for (field_name, field) in &model.fields {
2634 let col_def = ColumnDefinition::from_field_state(field_name.clone(), field);
2635 parts.push(format!(" {}", Self::column_to_sql(&col_def, dialect)));
2636 }
2637
2638 for constraint_def in &model.constraints {
2640 let constraint = constraint_def.to_constraint();
2641 parts.push(format!(" {}", constraint));
2642 }
2643
2644 return Ok(Some(vec![format!(
2645 "CREATE TABLE {} (\n{}\n);",
2646 quote_identifier(name),
2647 parts.join(",\n")
2648 )]));
2649 }
2650 Ok(None)
2652 }
2653 Operation::BulkLoad { table, .. } => {
2654 Ok(Some(vec![format!(
2657 "TRUNCATE TABLE {};",
2658 quote_identifier(table)
2659 )]))
2660 }
2661 _ => Ok(None),
2662 }
2663 }
2664
2665 pub fn state_backwards(&self, app_label: &str, state: &mut ProjectState) {
2685 match self {
2686 Operation::CreateTable { name, .. } => {
2687 state
2689 .models
2690 .remove(&(app_label.to_string(), name.to_string()));
2691 }
2692 Operation::DropTable { name: _ } => {
2693 }
2696 Operation::RenameTable { old_name, new_name } => {
2697 if let Some(mut model) = state
2699 .models
2700 .remove(&(app_label.to_string(), new_name.to_string()))
2701 {
2702 model.table_name = old_name.to_string();
2703 state
2704 .models
2705 .insert((app_label.to_string(), old_name.to_string()), model);
2706 }
2707 }
2708 Operation::AddColumn { table, column, .. } => {
2709 if let Some(model) = state.find_model_by_table_mut(table) {
2711 model.remove_field(&column.name);
2712 }
2713 }
2714 Operation::DropColumn {
2715 table: _,
2716 column: _,
2717 } => {
2718 }
2721 Operation::AlterColumn {
2722 table: _,
2723 column: _,
2724 ..
2725 } => {
2726 }
2729 Operation::RenameColumn {
2730 table,
2731 old_name,
2732 new_name,
2733 } => {
2734 if let Some(model) = state.find_model_by_table_mut(table) {
2736 model.rename_field(new_name, old_name.to_string());
2737 }
2738 }
2739 Operation::AddConstraint { table, .. } => {
2740 if let Some(model) = state.find_model_by_table_mut(table) {
2743 let _ = model;
2746 }
2747 }
2748 Operation::DropConstraint {
2749 table: _,
2750 constraint_name: _,
2751 } => {
2752 }
2755 _ => {
2756 }
2758 }
2759 }
2760
2761 fn extract_constraint_name(constraint_sql: &str) -> Option<String> {
2767 let sql = constraint_sql.trim();
2768
2769 if sql.starts_with("CONSTRAINT ") || sql.contains(" CONSTRAINT ") {
2771 let parts: Vec<&str> = sql.split_whitespace().collect();
2772 if let Some(pos) = parts.iter().position(|&s| s == "CONSTRAINT")
2773 && pos + 1 < parts.len()
2774 {
2775 return Some(parts[pos + 1].to_string());
2776 }
2777 }
2778
2779 None
2780 }
2781}
2782
2783#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2785pub struct ColumnDefinition {
2786 pub name: String,
2788 pub type_definition: FieldType,
2790 #[serde(default)]
2791 pub not_null: bool,
2793 #[serde(default)]
2794 pub unique: bool,
2796 #[serde(default)]
2797 pub primary_key: bool,
2799 #[serde(default)]
2800 pub auto_increment: bool,
2802 #[serde(default)]
2803 pub default: Option<String>,
2805}
2806
2807impl ColumnDefinition {
2808 pub fn new(name: impl Into<String>, type_def: FieldType) -> Self {
2810 Self {
2811 name: name.into(),
2812 type_definition: type_def,
2813 not_null: false,
2814 unique: false,
2815 primary_key: false,
2816 auto_increment: false,
2817 default: None,
2818 }
2819 }
2820
2821 pub fn from_field_state(name: impl Into<String>, field_state: &FieldState) -> Self {
2840 let name_str = name.into();
2841 let params = &field_state.params;
2842
2843 let primary_key = params
2845 .get("primary_key")
2846 .and_then(|v| v.parse::<bool>().ok())
2847 .unwrap_or(false);
2848
2849 let not_null = !field_state.nullable || primary_key;
2866
2867 let unique = params
2868 .get("unique")
2869 .and_then(|v| v.parse::<bool>().ok())
2870 .unwrap_or(false);
2871
2872 let auto_increment = params
2873 .get("auto_increment")
2874 .and_then(|v| v.parse::<bool>().ok())
2875 .unwrap_or(false);
2876
2877 let default = params.get("default").cloned();
2878
2879 let type_definition = resolve_foreign_key_column_type(field_state)
2892 .unwrap_or_else(|| field_state.field_type.clone());
2893
2894 Self {
2895 name: name_str,
2896 type_definition,
2897 not_null,
2898 unique,
2899 primary_key,
2900 auto_increment,
2901 default,
2902 }
2903 }
2904}
2905
2906fn resolve_foreign_key_column_type(field_state: &FieldState) -> Option<FieldType> {
2948 resolve_foreign_key_column_type_with(field_state, super::model_registry::global_registry())
2949}
2950
2951fn resolve_foreign_key_column_type_with(
2959 field_state: &FieldState,
2960 registry: &super::model_registry::ModelRegistry,
2961) -> Option<FieldType> {
2962 let target_model = field_state.params.get("fk_target")?;
2963 let target = match field_state.params.get("fk_target_app") {
2969 Some(app) => registry
2970 .find_model_qualified(app, target_model)
2971 .or_else(|| registry.find_model_by_name(target_model)),
2972 None => registry.find_model_by_name(target_model),
2973 };
2974 let target = match target {
2975 Some(t) => t,
2976 None => {
2977 if registry.count_models_by_name(target_model) > 1 {
2982 tracing::warn!(
2983 model_name = %target_model,
2984 fk_target_app = ?field_state.params.get("fk_target_app"),
2985 "FK target name is ambiguous across apps and the qualified \
2986 lookup did not resolve a unique target. Refusing to resolve \
2987 to avoid silent wrong-target resolution. Ensure the FK \
2988 target type is registered and that its `Model::app_label()` \
2989 matches one of the registered apps.",
2990 );
2991 }
2992 return None;
2993 }
2994 };
2995 let pk_field = target
2997 .fields
2998 .values()
2999 .find(|f| f.params.get("primary_key").map(String::as_str) == Some("true"))?;
3000 Some(pk_field.field_type.clone())
3001}
3002
3003pub fn field_type_string_to_field_type(
3031 field_type: &str,
3032 attributes: &std::collections::HashMap<String, String>,
3033) -> Result<FieldType, String> {
3034 let type_name = field_type.split('.').next_back().unwrap_or(field_type);
3036
3037 match type_name {
3038 "IntegerField"
3040 | "PositiveIntegerField"
3041 | "SmallIntegerField"
3042 | "PositiveSmallIntegerField" => Ok(FieldType::Integer),
3043 "BigIntegerField" | "PositiveBigIntegerField" => Ok(FieldType::BigInteger),
3044 "AutoField" => Ok(FieldType::Integer),
3045 "BigAutoField" => Ok(FieldType::BigInteger),
3046 "SmallAutoField" => Ok(FieldType::SmallInteger),
3047
3048 "CharField" => {
3050 let max_length = attributes
3051 .get("max_length")
3052 .and_then(|v| v.parse::<u32>().ok())
3053 .ok_or_else(|| "CharField requires max_length attribute".to_string())?;
3054 Ok(FieldType::VarChar(max_length))
3055 }
3056 "TextField" => Ok(FieldType::Text),
3057 "SlugField" => {
3058 let max_length = attributes
3059 .get("max_length")
3060 .and_then(|v| v.parse::<u32>().ok())
3061 .unwrap_or(50);
3062 Ok(FieldType::VarChar(max_length))
3063 }
3064 "EmailField" => {
3065 let max_length = attributes
3066 .get("max_length")
3067 .and_then(|v| v.parse::<u32>().ok())
3068 .unwrap_or(254);
3069 Ok(FieldType::VarChar(max_length))
3070 }
3071 "URLField" => {
3072 let max_length = attributes
3073 .get("max_length")
3074 .and_then(|v| v.parse::<u32>().ok())
3075 .unwrap_or(200);
3076 Ok(FieldType::VarChar(max_length))
3077 }
3078
3079 "BooleanField" => Ok(FieldType::Boolean),
3081 "NullBooleanField" => Ok(FieldType::Boolean),
3082
3083 "DateField" => Ok(FieldType::Date),
3085 "TimeField" => Ok(FieldType::Time),
3086 "DateTimeField" => Ok(FieldType::DateTime),
3087 "DurationField" => Ok(FieldType::BigInteger), "FloatField" => Ok(FieldType::Float),
3091 "DecimalField" => {
3092 let precision = attributes
3093 .get("max_digits")
3094 .and_then(|v| v.parse::<u32>().ok())
3095 .unwrap_or(10);
3096 let scale = attributes
3097 .get("decimal_places")
3098 .and_then(|v| v.parse::<u32>().ok())
3099 .unwrap_or(2);
3100 Ok(FieldType::Decimal { precision, scale })
3101 }
3102
3103 "BinaryField" => Ok(FieldType::Binary),
3105
3106 "UUIDField" => Ok(FieldType::Uuid),
3108
3109 "JSONField" => Ok(FieldType::Json),
3111
3112 "FileField" | "ImageField" => {
3114 let max_length = attributes
3115 .get("max_length")
3116 .and_then(|v| v.parse::<u32>().ok())
3117 .unwrap_or(100);
3118 Ok(FieldType::VarChar(max_length))
3119 }
3120
3121 "GenericIPAddressField" | "IPAddressField" => {
3123 Ok(FieldType::VarChar(39)) }
3126
3127 "ForeignKey" => {
3129 Ok(FieldType::BigInteger)
3131 }
3132 "OneToOneField" => Ok(FieldType::BigInteger),
3133
3134 other => Err(format!("Unsupported field type: {}", other)),
3136 }
3137}
3138
3139#[derive(Debug, Clone, Copy)]
3141pub enum SqlDialect {
3142 Sqlite,
3144 Postgres,
3146 Mysql,
3148 Cockroachdb,
3150}
3151
3152#[derive(Debug, Clone)]
3171pub struct SqliteTableRecreation {
3172 pub table_name: String,
3174 pub new_columns: Vec<ColumnDefinition>,
3176 pub columns_to_copy: Vec<String>,
3178 pub constraints: Vec<Constraint>,
3180 pub raw_constraint_sqls: Vec<String>,
3182 pub without_rowid: bool,
3184}
3185
3186impl SqliteTableRecreation {
3187 pub fn for_drop_column(
3189 table_name: impl Into<String>,
3190 current_columns: Vec<ColumnDefinition>,
3191 column_to_drop: &str,
3192 current_constraints: Vec<Constraint>,
3193 ) -> Self {
3194 let table_name = table_name.into();
3195 let new_columns: Vec<_> = current_columns
3196 .into_iter()
3197 .filter(|c| c.name != column_to_drop)
3198 .collect();
3199 let columns_to_copy: Vec<_> = new_columns.iter().map(|c| c.name.to_string()).collect();
3200
3201 let constraints: Vec<_> = current_constraints
3203 .into_iter()
3204 .filter(|c| !Self::constraint_references_column(c, column_to_drop))
3205 .collect();
3206
3207 Self {
3208 table_name,
3209 new_columns,
3210 columns_to_copy,
3211 constraints,
3212 raw_constraint_sqls: Vec::new(),
3213 without_rowid: false,
3214 }
3215 }
3216
3217 pub fn for_alter_column(
3219 table_name: impl Into<String>,
3220 current_columns: Vec<ColumnDefinition>,
3221 column_name: &str,
3222 new_definition: ColumnDefinition,
3223 current_constraints: Vec<Constraint>,
3224 ) -> Self {
3225 let table_name = table_name.into();
3226 let new_columns: Vec<_> = current_columns
3227 .into_iter()
3228 .map(|c| {
3229 if c.name == column_name {
3230 new_definition.clone()
3231 } else {
3232 c
3233 }
3234 })
3235 .collect();
3236 let columns_to_copy: Vec<_> = new_columns.iter().map(|c| c.name.to_string()).collect();
3237
3238 Self {
3239 table_name,
3240 new_columns,
3241 columns_to_copy,
3242 constraints: current_constraints,
3243 raw_constraint_sqls: Vec::new(),
3244 without_rowid: false,
3245 }
3246 }
3247
3248 pub fn for_add_constraint(
3253 table_name: impl Into<String>,
3254 current_columns: Vec<ColumnDefinition>,
3255 current_constraints: Vec<Constraint>,
3256 constraint_sql: String,
3257 ) -> Self {
3258 let table_name = table_name.into();
3259 let columns_to_copy: Vec<_> = current_columns.iter().map(|c| c.name.to_string()).collect();
3260
3261 Self {
3262 table_name,
3263 new_columns: current_columns,
3264 columns_to_copy,
3265 constraints: current_constraints,
3266 raw_constraint_sqls: vec![constraint_sql],
3267 without_rowid: false,
3268 }
3269 }
3270
3271 pub fn for_drop_constraint(
3276 table_name: impl Into<String>,
3277 current_columns: Vec<ColumnDefinition>,
3278 current_constraints: Vec<Constraint>,
3279 constraint_name: &str,
3280 ) -> Self {
3281 let table_name = table_name.into();
3282 let columns_to_copy: Vec<_> = current_columns.iter().map(|c| c.name.to_string()).collect();
3283
3284 let constraints: Vec<_> = current_constraints
3286 .into_iter()
3287 .filter(|c| !Self::constraint_has_name(c, constraint_name))
3288 .collect();
3289
3290 Self {
3291 table_name,
3292 new_columns: current_columns,
3293 columns_to_copy,
3294 constraints,
3295 raw_constraint_sqls: Vec::new(),
3296 without_rowid: false,
3297 }
3298 }
3299
3300 pub fn to_sql_statements(&self) -> Vec<String> {
3302 let temp_table = format!("{}_new", self.table_name);
3303
3304 let column_defs: Vec<String> = self
3306 .new_columns
3307 .iter()
3308 .map(|c| Operation::column_to_sql(c, &SqlDialect::Sqlite))
3309 .collect();
3310
3311 let constraint_defs: Vec<String> = self.constraints.iter().map(|c| c.to_string()).collect();
3312
3313 let mut create_parts = column_defs;
3314 create_parts.extend(constraint_defs);
3315 create_parts.extend(self.raw_constraint_sqls.clone());
3317
3318 let mut create_sql = format!(
3319 "CREATE TABLE \"{}\" (\n {}\n)",
3320 temp_table,
3321 create_parts.join(",\n ")
3322 );
3323 if self.without_rowid {
3324 create_sql.push_str(" WITHOUT ROWID");
3325 }
3326 create_sql.push(';');
3327
3328 let columns_list = self
3330 .columns_to_copy
3331 .iter()
3332 .map(|c| format!("\"{}\"", c))
3333 .collect::<Vec<_>>()
3334 .join(", ");
3335 let insert_sql = format!(
3336 "INSERT INTO \"{}\" SELECT {} FROM \"{}\";",
3337 temp_table, columns_list, self.table_name
3338 );
3339
3340 let drop_sql = format!("DROP TABLE \"{}\";", self.table_name);
3342
3343 let rename_sql = format!(
3345 "ALTER TABLE \"{}\" RENAME TO \"{}\";",
3346 temp_table, self.table_name
3347 );
3348
3349 vec![create_sql, insert_sql, drop_sql, rename_sql]
3350 }
3351
3352 fn constraint_references_column(constraint: &Constraint, column_name: &str) -> bool {
3354 match constraint {
3355 Constraint::PrimaryKey { columns, .. } => columns.iter().any(|c| c == column_name),
3356 Constraint::ForeignKey { columns, .. } => columns.iter().any(|c| c == column_name),
3357 Constraint::Unique { columns, .. } => columns.iter().any(|c| c == column_name),
3358 Constraint::Check { expression, .. } => expression.contains(column_name),
3359 Constraint::OneToOne { column, .. } => column == column_name,
3360 Constraint::ManyToMany { source_column, .. } => source_column == column_name,
3361 Constraint::Exclude { elements, .. } => {
3362 elements.iter().any(|(col, _)| col == column_name)
3363 }
3364 }
3365 }
3366
3367 fn constraint_has_name(constraint: &Constraint, constraint_name: &str) -> bool {
3369 match constraint {
3370 Constraint::PrimaryKey { name, .. } => name == constraint_name,
3371 Constraint::ForeignKey { name, .. } => name == constraint_name,
3372 Constraint::Unique { name, .. } => name == constraint_name,
3373 Constraint::Check { name, .. } => name == constraint_name,
3374 Constraint::OneToOne { name, .. } => name == constraint_name,
3375 Constraint::ManyToMany { name, .. } => name == constraint_name,
3376 Constraint::Exclude { name, .. } => name == constraint_name,
3377 }
3378 }
3379}
3380
3381impl Operation {
3382 pub fn requires_sqlite_recreation(&self) -> bool {
3384 matches!(
3385 self,
3386 Operation::DropColumn { .. }
3387 | Operation::AlterColumn { .. }
3388 | Operation::AddConstraint { .. }
3389 | Operation::DropConstraint { .. }
3390 )
3391 }
3392
3393 pub fn reverse_requires_sqlite_recreation(&self) -> bool {
3405 matches!(
3406 self,
3407 Operation::AddColumn { .. }
3409 | Operation::AlterColumn { .. }
3411 | Operation::AddConstraint { .. }
3413 | Operation::DropConstraint { .. }
3415 )
3416 }
3417
3418 pub fn to_reverse_operation(
3434 &self,
3435 project_state: &ProjectState,
3436 ) -> super::Result<Option<Operation>> {
3437 match self {
3438 Operation::CreateTable { name, .. } => {
3439 Ok(Some(Operation::DropTable { name: name.clone() }))
3440 }
3441 Operation::DropTable { name } => {
3442 if let Some(model) = project_state.find_model_by_table(name) {
3444 let columns: Vec<ColumnDefinition> = model
3445 .fields
3446 .iter()
3447 .map(|(field_name, field)| {
3448 ColumnDefinition::from_field_state(field_name.clone(), field)
3449 })
3450 .collect();
3451 let constraints: Vec<Constraint> = model
3452 .constraints
3453 .iter()
3454 .map(|c| c.to_constraint())
3455 .collect();
3456 return Ok(Some(Operation::CreateTable {
3457 name: name.clone(),
3458 columns,
3459 constraints,
3460 without_rowid: None,
3461 interleave_in_parent: None,
3462 partition: None,
3463 }));
3464 }
3465 Ok(None)
3466 }
3467 Operation::AddColumn { table, column, .. } => Ok(Some(Operation::DropColumn {
3468 table: table.clone(),
3469 column: column.name.clone(),
3470 })),
3471 Operation::DropColumn { table, column } => {
3472 if let Some(model) = project_state.find_model_by_table(table)
3474 && let Some(field) = model.get_field(column)
3475 {
3476 let col_def = ColumnDefinition::from_field_state(column.clone(), field);
3477 return Ok(Some(Operation::AddColumn {
3478 table: table.clone(),
3479 column: col_def,
3480 mysql_options: None,
3481 }));
3482 }
3483 Ok(None)
3484 }
3485 Operation::AlterColumn {
3486 table,
3487 column,
3488 old_definition,
3489 new_definition: _,
3490 ..
3491 } => {
3492 let resolved_old_def = old_definition.clone().or_else(|| {
3496 project_state
3497 .find_model_by_table(table)
3498 .and_then(|model| model.get_field(column))
3499 .map(|field| ColumnDefinition::from_field_state(column.clone(), field))
3500 });
3501
3502 if let Some(col_def) = resolved_old_def {
3503 return Ok(Some(Operation::AlterColumn {
3504 table: table.clone(),
3505 column: column.clone(),
3506 old_definition: None,
3507 new_definition: col_def,
3508 mysql_options: None,
3509 }));
3510 }
3511 Ok(None)
3512 }
3513 Operation::AddConstraint {
3514 table,
3515 constraint_sql,
3516 } => {
3517 if let Some(constraint_name) = Self::extract_constraint_name(constraint_sql) {
3519 return Ok(Some(Operation::DropConstraint {
3520 table: table.clone(),
3521 constraint_name,
3522 }));
3523 }
3524 Err(super::MigrationError::InvalidMigration(format!(
3525 "Cannot extract constraint name from: {}",
3526 constraint_sql
3527 )))
3528 }
3529 Operation::DropConstraint {
3530 table,
3531 constraint_name,
3532 } => {
3533 if let Some(model) = project_state.find_model_by_table(table)
3535 && let Some(constraint_def) = model
3536 .constraints
3537 .iter()
3538 .find(|c| c.name == *constraint_name)
3539 {
3540 let constraint = constraint_def.to_constraint();
3541 return Ok(Some(Operation::AddConstraint {
3542 table: table.clone(),
3543 constraint_sql: format!("{}", constraint),
3544 }));
3545 }
3546 Ok(None)
3547 }
3548 Operation::RenameTable { old_name, new_name } => Ok(Some(Operation::RenameTable {
3549 old_name: new_name.clone(),
3550 new_name: old_name.clone(),
3551 })),
3552 Operation::RenameColumn {
3553 table,
3554 old_name,
3555 new_name,
3556 } => Ok(Some(Operation::RenameColumn {
3557 table: table.clone(),
3558 old_name: new_name.clone(),
3559 new_name: old_name.clone(),
3560 })),
3561 Operation::CreateIndex { table, columns, .. } => Ok(Some(Operation::DropIndex {
3562 table: table.clone(),
3563 columns: columns.clone(),
3564 })),
3565 Operation::DropIndex { table, columns } => {
3566 Ok(Some(Operation::CreateIndex {
3569 table: table.clone(),
3570 columns: columns.clone(),
3571 unique: false,
3572 index_type: None,
3573 where_clause: None,
3574 concurrently: false,
3575 expressions: None,
3576 mysql_options: None,
3577 operator_class: None,
3578 }))
3579 }
3580 Operation::RunSQL { .. } | Operation::RunRust { .. } | Operation::BulkLoad { .. } => {
3582 Ok(None)
3583 }
3584 _ => Ok(None),
3586 }
3587 }
3588}
3589
3590pub use Operation::{AddColumn, AlterColumn, CreateTable, DropColumn};
3592
3593pub enum OperationStatement {
3595 TableCreate(CreateTableStatement),
3597 TableDrop(DropTableStatement),
3599 TableAlter(AlterTableStatement),
3601 TableRename(AlterTableStatement),
3603 IndexCreate(CreateIndexStatement),
3605 IndexDrop(DropIndexStatement),
3607 RawSql(String),
3609}
3610
3611impl OperationStatement {
3612 pub async fn execute<'c, E>(&self, executor: E) -> Result<(), sqlx::Error>
3614 where
3615 E: sqlx::Executor<'c, Database = sqlx::Postgres>,
3616 {
3617 use crate::backends::sql_build_helpers;
3618 use crate::backends::types::DatabaseType;
3619 let db_type = DatabaseType::Postgres;
3620 match self {
3621 OperationStatement::TableCreate(stmt) => {
3622 let sql = sql_build_helpers::build_create_table_sql(db_type, stmt);
3623 sqlx::query(&sql).execute(executor).await?;
3624 }
3625 OperationStatement::TableDrop(stmt) => {
3626 let sql = sql_build_helpers::build_drop_table_sql(db_type, stmt);
3627 sqlx::query(&sql).execute(executor).await?;
3628 }
3629 OperationStatement::TableAlter(stmt) => {
3630 let sql = sql_build_helpers::build_alter_table_sql(db_type, stmt);
3631 sqlx::query(&sql).execute(executor).await?;
3632 }
3633 OperationStatement::TableRename(stmt) => {
3634 let sql = sql_build_helpers::build_alter_table_sql(db_type, stmt);
3635 sqlx::query(&sql).execute(executor).await?;
3636 }
3637 OperationStatement::IndexCreate(stmt) => {
3638 let sql = sql_build_helpers::build_create_index_sql(db_type, stmt);
3639 sqlx::query(&sql).execute(executor).await?;
3640 }
3641 OperationStatement::IndexDrop(stmt) => {
3642 let sql = sql_build_helpers::build_drop_index_sql(db_type, stmt);
3643 sqlx::query(&sql).execute(executor).await?;
3644 }
3645 OperationStatement::RawSql(sql) => {
3646 sqlx::query(sql).execute(executor).await?;
3648 }
3649 }
3650 Ok(())
3651 }
3652
3653 pub fn to_sql_string(&self, db_type: crate::backends::types::DatabaseType) -> String {
3659 use crate::backends::sql_build_helpers;
3660
3661 match self {
3662 OperationStatement::TableCreate(stmt) => {
3663 sql_build_helpers::build_create_table_sql(db_type, stmt)
3664 }
3665 OperationStatement::TableDrop(stmt) => {
3666 sql_build_helpers::build_drop_table_sql(db_type, stmt)
3667 }
3668 OperationStatement::TableAlter(stmt) => {
3669 sql_build_helpers::build_alter_table_sql(db_type, stmt)
3670 }
3671 OperationStatement::TableRename(stmt) => {
3672 sql_build_helpers::build_alter_table_sql(db_type, stmt)
3673 }
3674 OperationStatement::IndexCreate(stmt) => {
3675 sql_build_helpers::build_create_index_sql(db_type, stmt)
3676 }
3677 OperationStatement::IndexDrop(stmt) => {
3678 sql_build_helpers::build_drop_index_sql(db_type, stmt)
3679 }
3680 OperationStatement::RawSql(sql) => sql.clone(),
3681 }
3682 }
3683}
3684
3685impl Operation {
3686 pub fn to_statement(&self) -> OperationStatement {
3688 match self {
3689 Operation::CreateTable {
3690 name,
3691 columns,
3692 constraints,
3693 ..
3694 } => {
3695 OperationStatement::TableCreate(self.build_create_table(name, columns, constraints))
3696 }
3697 Operation::DropTable { name } => {
3698 OperationStatement::TableDrop(self.build_drop_table(name))
3699 }
3700 Operation::AddColumn { table, column, .. } => {
3701 OperationStatement::TableAlter(self.build_add_column(table, column))
3702 }
3703 Operation::DropColumn { table, column } => {
3704 OperationStatement::TableAlter(self.build_drop_column(table, column))
3705 }
3706 Operation::AlterColumn {
3707 table,
3708 column,
3709 new_definition,
3710 ..
3711 } => OperationStatement::TableAlter(self.build_alter_column(
3712 table,
3713 column,
3714 new_definition,
3715 )),
3716 Operation::RenameTable { old_name, new_name } => {
3717 OperationStatement::TableRename(self.build_rename_table(old_name, new_name))
3718 }
3719 Operation::RenameColumn {
3721 table,
3722 old_name,
3723 new_name,
3724 } => OperationStatement::RawSql(format!(
3725 "ALTER TABLE {} RENAME COLUMN {} TO {}",
3726 quote_identifier(table),
3727 quote_identifier(old_name),
3728 quote_identifier(new_name)
3729 )),
3730 Operation::AddConstraint {
3731 table,
3732 constraint_sql,
3733 } => {
3734 OperationStatement::RawSql(format!(
3736 "ALTER TABLE {} ADD {}",
3737 quote_identifier(table),
3738 constraint_sql
3739 ))
3740 }
3741 Operation::DropConstraint {
3742 table,
3743 constraint_name,
3744 } => OperationStatement::RawSql(format!(
3745 "ALTER TABLE {} DROP CONSTRAINT {}",
3746 quote_identifier(table),
3747 quote_identifier(constraint_name)
3748 )),
3749 Operation::CreateIndex {
3750 table,
3751 columns,
3752 unique,
3753 ..
3754 } => {
3755 let idx_name = format!("idx_{}_{}", table, columns.join("_"));
3756 OperationStatement::IndexCreate(
3757 self.build_create_index(&idx_name, table, columns, *unique),
3758 )
3759 }
3760 Operation::DropIndex { table, columns } => {
3761 let idx_name = format!("idx_{}_{}", table, columns.join("_"));
3762 OperationStatement::IndexDrop(self.build_drop_index(&idx_name))
3763 }
3764 Operation::RunSQL { sql, .. } => OperationStatement::RawSql(sql.to_string()),
3765 Operation::RunRust { code, .. } => {
3766 OperationStatement::RawSql(format!(
3768 "-- RunRust: {}",
3769 code.lines().next().unwrap_or("")
3770 ))
3771 }
3772 Operation::AlterTableComment { table, comment } => {
3773 OperationStatement::RawSql(if let Some(comment_text) = comment {
3775 format!(
3776 "COMMENT ON TABLE {} IS '{}'",
3777 quote_identifier(table),
3778 comment_text.replace('\'', "''") )
3780 } else {
3781 format!("COMMENT ON TABLE {} IS NULL", quote_identifier(table))
3782 })
3783 }
3784 Operation::AlterUniqueTogether {
3785 table,
3786 unique_together,
3787 } => {
3788 let mut sqls = Vec::new();
3789 for (idx, fields) in unique_together.iter().enumerate() {
3790 let constraint_name = format!("{}_{}_uniq", table, idx);
3791 let fields_str: Vec<String> = fields
3792 .iter()
3793 .map(|f| quote_identifier(f).to_string())
3794 .collect();
3795 sqls.push(format!(
3796 "ALTER TABLE {} ADD CONSTRAINT {} UNIQUE ({})",
3797 quote_identifier(table),
3798 quote_identifier(&constraint_name),
3799 fields_str.join(", ")
3800 ));
3801 }
3802 OperationStatement::RawSql(sqls.join(";\n"))
3803 }
3804 Operation::AlterModelOptions { .. } => OperationStatement::RawSql(String::new()),
3805 Operation::CreateInheritedTable {
3806 name,
3807 columns,
3808 base_table,
3809 join_column,
3810 } => {
3811 let mut stmt = Query::create_table();
3812 stmt.table(Alias::new(name.as_str())).if_not_exists();
3813
3814 let join_col = ColumnDef::new(Alias::new(join_column.as_str()));
3816 let join_col = join_col.integer();
3817 stmt.col(join_col);
3818
3819 for col in columns {
3821 let mut column = ColumnDef::new(Alias::new(col.name.as_str()));
3822 column = self.apply_column_type(column, &col.type_definition);
3823 stmt.col(column);
3824 }
3825
3826 let mut fk = reinhardt_query::prelude::ForeignKey::create();
3828 fk.from_tbl(Alias::new(name.as_str()))
3829 .from_col(Alias::new(join_column.as_str()))
3830 .to_tbl(Alias::new(base_table.as_str()))
3831 .to_col(Alias::new("id"));
3832 stmt.foreign_key_from_builder(&mut fk);
3833
3834 OperationStatement::TableCreate(stmt.to_owned())
3835 }
3836 Operation::AddDiscriminatorColumn {
3837 table,
3838 column_name,
3839 default_value,
3840 } => {
3841 let mut stmt = Query::alter_table();
3842 stmt.table(Alias::new(table.as_str()));
3843
3844 let mut col = ColumnDef::new(Alias::new(column_name.as_str()));
3845 col = col
3846 .string_len(50)
3847 .default(SimpleExpr::from(default_value.to_string()));
3848 stmt.add_column(col);
3849
3850 OperationStatement::TableAlter(stmt.to_owned())
3851 }
3852 Operation::MoveModel {
3853 rename_table,
3854 old_table_name,
3855 new_table_name,
3856 ..
3857 } => {
3858 if *rename_table {
3860 if let (Some(old_name), Some(new_name)) = (old_table_name, new_table_name) {
3861 OperationStatement::TableRename(self.build_rename_table(old_name, new_name))
3862 } else {
3863 OperationStatement::RawSql("-- MoveModel: State-only operation".to_string())
3865 }
3866 } else {
3867 OperationStatement::RawSql("-- MoveModel: State-only operation".to_string())
3869 }
3870 }
3871 Operation::CreateSchema {
3872 name,
3873 if_not_exists,
3874 } => {
3875 let sql = if *if_not_exists {
3877 format!("CREATE SCHEMA IF NOT EXISTS {}", quote_identifier(name))
3878 } else {
3879 format!("CREATE SCHEMA {}", quote_identifier(name))
3880 };
3881 OperationStatement::RawSql(sql)
3882 }
3883 Operation::DropSchema {
3884 name,
3885 cascade,
3886 if_exists,
3887 } => {
3888 let if_exists_clause = if *if_exists { " IF EXISTS" } else { "" };
3890 let cascade_clause = if *cascade { " CASCADE" } else { "" };
3891 let sql = format!(
3892 "DROP SCHEMA{} {}{}",
3893 if_exists_clause,
3894 quote_identifier(name),
3895 cascade_clause
3896 );
3897 OperationStatement::RawSql(sql)
3898 }
3899 Operation::CreateExtension {
3900 name,
3901 if_not_exists,
3902 schema,
3903 } => {
3904 let if_not_exists_clause = if *if_not_exists { " IF NOT EXISTS" } else { "" };
3906 let schema_clause = if let Some(s) = schema {
3907 format!(" SCHEMA {}", quote_identifier(s))
3908 } else {
3909 String::new()
3910 };
3911 let sql = format!(
3912 "CREATE EXTENSION{} {}{}",
3913 if_not_exists_clause,
3914 quote_identifier(name),
3915 schema_clause
3916 );
3917 OperationStatement::RawSql(sql)
3918 }
3919 Operation::BulkLoad {
3920 table,
3921 source,
3922 format,
3923 options,
3924 } => {
3925 OperationStatement::RawSql(Self::postgres_copy_from_sql(
3928 table, source, format, options,
3929 ))
3930 }
3931 Operation::SetAutoIncrementValue { table, .. } => {
3932 OperationStatement::RawSql(format!(
3944 "SELECT 1/0 AS \"SetAutoIncrementValue on {} requires dialect-aware rendering; call Operation::to_sql(&dialect) instead of to_statement()\";",
3945 table.replace('"', "\"\"")
3946 ))
3947 }
3948 Operation::CreateCompositePrimaryKey {
3949 table,
3950 columns,
3951 constraint_name,
3952 } => OperationStatement::RawSql(Self::create_composite_pk_to_sql(
3953 table,
3954 columns,
3955 constraint_name.as_deref(),
3956 )),
3957 }
3958 }
3959
3960 fn build_create_table(
3962 &self,
3963 name: &str,
3964 columns: &[ColumnDefinition],
3965 constraints: &[Constraint],
3966 ) -> CreateTableStatement {
3967 let mut stmt = Query::create_table();
3968 stmt.table(Alias::new(name)).if_not_exists();
3969
3970 for col in columns {
3971 let mut column = ColumnDef::new(Alias::new(col.name.as_str()));
3972 column = self.apply_column_type(column, &col.type_definition);
3973
3974 if col.not_null {
3975 column = column.not_null(true);
3976 }
3977 if col.unique {
3978 column = column.unique(true);
3979 }
3980 if col.primary_key {
3981 column = column.primary_key(true);
3982 }
3983 if col.auto_increment {
3984 column = column.auto_increment(true);
3985 }
3986 if let Some(default) = &col.default {
3987 column = column.default(SimpleExpr::from(self.convert_default_value(default)));
3988 }
3989
3990 stmt.col(column);
3991 }
3992
3993 for constraint in constraints {
3995 match constraint {
3996 Constraint::PrimaryKey { columns, .. } => {
3997 let col_idens: Vec<Alias> =
3998 columns.iter().map(|c| Alias::new(c.as_str())).collect();
3999 stmt.primary_key(col_idens);
4000 }
4001 Constraint::ForeignKey {
4002 name,
4003 columns,
4004 referenced_table,
4005 referenced_columns,
4006 on_delete,
4007 on_update,
4008 ..
4009 } => {
4010 let mut fk = reinhardt_query::prelude::ForeignKey::create();
4011 fk.name(Alias::new(name.as_str()))
4012 .from_tbl(Alias::new(name.as_str()))
4013 .to_tbl(Alias::new(referenced_table.as_str()));
4014
4015 for col in columns {
4016 fk.from_col(Alias::new(col.as_str()));
4017 }
4018 for col in referenced_columns {
4019 fk.to_col(Alias::new(col.as_str()));
4020 }
4021
4022 fk.on_delete((*on_delete).into());
4023 fk.on_update((*on_update).into());
4024
4025 stmt.foreign_key_from_builder(&mut fk);
4026 }
4027 Constraint::Unique { columns, .. } => {
4028 let col_idens: Vec<Alias> =
4029 columns.iter().map(|c| Alias::new(c.as_str())).collect();
4030 stmt.unique(col_idens);
4031 }
4032 Constraint::Check { name, expression } => {
4033 let _ = (name, expression); }
4037 Constraint::OneToOne {
4038 name,
4039 column,
4040 referenced_table,
4041 referenced_column,
4042 on_delete,
4043 on_update,
4044 ..
4045 } => {
4046 let mut fk = reinhardt_query::prelude::ForeignKey::create();
4048 fk.name(Alias::new(name.as_str()))
4049 .from_tbl(Alias::new(name.as_str()))
4050 .to_tbl(Alias::new(referenced_table.as_str()))
4051 .from_col(Alias::new(column.as_str()))
4052 .to_col(Alias::new(referenced_column.as_str()))
4053 .on_delete((*on_delete).into())
4054 .on_update((*on_update).into());
4055
4056 stmt.foreign_key_from_builder(&mut fk);
4057
4058 }
4061 Constraint::ManyToMany { .. } => {
4062 }
4065 Constraint::Exclude { .. } => {
4066 }
4069 }
4070 }
4071
4072 stmt.to_owned()
4073 }
4074
4075 fn build_drop_table(&self, name: &str) -> DropTableStatement {
4077 Query::drop_table()
4078 .table(Alias::new(name))
4079 .if_exists()
4080 .cascade()
4081 .to_owned()
4082 }
4083
4084 fn build_add_column(&self, table: &str, column: &ColumnDefinition) -> AlterTableStatement {
4086 let mut stmt = Query::alter_table();
4087 stmt.table(Alias::new(table));
4088
4089 let mut col_def = ColumnDef::new(Alias::new(column.name.as_str()));
4090 col_def = self.apply_column_type(col_def, &column.type_definition);
4091
4092 if column.not_null {
4093 col_def = col_def.not_null(true);
4094 }
4095 if let Some(default) = &column.default {
4096 col_def = col_def.default(SimpleExpr::from(self.convert_default_value(default)));
4097 }
4098
4099 stmt.add_column(col_def);
4100 stmt.to_owned()
4101 }
4102
4103 fn build_drop_column(&self, table: &str, column: &str) -> AlterTableStatement {
4105 Query::alter_table()
4106 .table(Alias::new(table))
4107 .drop_column(Alias::new(column))
4108 .to_owned()
4109 }
4110
4111 fn build_alter_column(
4113 &self,
4114 table: &str,
4115 column: &str,
4116 new_definition: &ColumnDefinition,
4117 ) -> AlterTableStatement {
4118 let mut stmt = Query::alter_table();
4119 stmt.table(Alias::new(table));
4120
4121 let mut col_def = ColumnDef::new(Alias::new(column));
4122 col_def = self.apply_column_type(col_def, &new_definition.type_definition);
4123
4124 if new_definition.not_null {
4125 col_def = col_def.not_null(true);
4126 }
4127
4128 stmt.modify_column(col_def);
4129 stmt.to_owned()
4130 }
4131
4132 fn build_rename_table(&self, old_name: &str, new_name: &str) -> AlterTableStatement {
4134 Query::alter_table()
4135 .table(Alias::new(old_name))
4136 .rename_table(Alias::new(new_name))
4137 .to_owned()
4138 }
4139
4140 fn build_create_index(
4142 &self,
4143 name: &str,
4144 table: &str,
4145 columns: &[String],
4146 unique: bool,
4147 ) -> CreateIndexStatement {
4148 let mut stmt = Query::create_index();
4149 stmt.name(Alias::new(name)).table(Alias::new(table));
4150
4151 for col in columns {
4152 stmt.col(Alias::new(col));
4153 }
4154
4155 if unique {
4156 stmt.unique();
4157 }
4158
4159 stmt.to_owned()
4160 }
4161
4162 fn build_drop_index(&self, name: &str) -> DropIndexStatement {
4164 Query::drop_index().name(Alias::new(name)).to_owned()
4165 }
4166
4167 fn apply_column_type(&self, col_def: ColumnDef, field_type: &FieldType) -> ColumnDef {
4169 use FieldType;
4170 match field_type {
4171 FieldType::Integer => col_def.integer(),
4172 FieldType::BigInteger => col_def.big_integer(),
4173 FieldType::SmallInteger => col_def.small_integer(),
4174 FieldType::TinyInt => col_def.tiny_integer(),
4175 FieldType::VarChar(max_length) => col_def.string_len(*max_length),
4176 FieldType::Char(max_length) => col_def.char_len(*max_length),
4177 FieldType::Text | FieldType::TinyText | FieldType::MediumText | FieldType::LongText => {
4178 col_def.text()
4179 }
4180 FieldType::Boolean => col_def.custom(Alias::new("BOOLEAN")),
4186 FieldType::DateTime => col_def.timestamp(),
4187 FieldType::TimestampTz => col_def.timestamp_with_time_zone(),
4188 FieldType::Date => col_def.date(),
4189 FieldType::Time => col_def.time(),
4190 FieldType::Decimal { precision, scale } => col_def.decimal(*precision, *scale),
4191 FieldType::Float => col_def.float(),
4192 FieldType::Double | FieldType::Real => col_def.double(),
4193 FieldType::Json => col_def.json(),
4194 FieldType::JsonBinary => col_def.json_binary(),
4195 FieldType::Uuid => col_def.uuid(),
4196 FieldType::Binary | FieldType::Bytea => col_def.binary(0),
4197 FieldType::Blob | FieldType::TinyBlob | FieldType::MediumBlob | FieldType::LongBlob => {
4198 col_def.binary(0)
4199 }
4200 FieldType::MediumInt => col_def.integer(),
4201 FieldType::Year => col_def.small_integer(),
4202 FieldType::Enum { values } => {
4203 col_def.custom(Alias::new(format!("ENUM({})", values.join(","))))
4204 }
4205 FieldType::Set { values } => {
4206 col_def.custom(Alias::new(format!("SET({})", values.join(","))))
4207 }
4208 FieldType::ForeignKey { .. } => {
4209 col_def.integer()
4211 }
4212 FieldType::OneToOne { .. } => {
4213 col_def.big_integer()
4216 }
4217 FieldType::ManyToMany { .. } => {
4218 col_def.big_integer()
4221 }
4222 FieldType::Array(inner) => {
4224 let inner_sql = inner.to_sql_string();
4226 col_def.custom(Alias::new(format!("{}[]", inner_sql)))
4227 }
4228 FieldType::HStore => col_def.custom(Alias::new("HSTORE")),
4229 FieldType::CIText => col_def.custom(Alias::new("CITEXT")),
4230 FieldType::Int4Range => col_def.custom(Alias::new("INT4RANGE")),
4231 FieldType::Int8Range => col_def.custom(Alias::new("INT8RANGE")),
4232 FieldType::NumRange => col_def.custom(Alias::new("NUMRANGE")),
4233 FieldType::DateRange => col_def.custom(Alias::new("DATERANGE")),
4234 FieldType::TsRange => col_def.custom(Alias::new("TSRANGE")),
4235 FieldType::TsTzRange => col_def.custom(Alias::new("TSTZRANGE")),
4236 FieldType::TsVector => col_def.custom(Alias::new("TSVECTOR")),
4237 FieldType::TsQuery => col_def.custom(Alias::new("TSQUERY")),
4238 FieldType::Custom(custom_type) => col_def.custom(Alias::new(custom_type)),
4239 }
4240 }
4241
4242 fn convert_default_value(&self, default: &str) -> Value {
4244 let trimmed = default.trim();
4245
4246 if trimmed.eq_ignore_ascii_case("null") {
4248 return Value::String(None);
4249 }
4250
4251 if trimmed.eq_ignore_ascii_case("true") {
4253 return Value::Bool(Some(true));
4254 }
4255 if trimmed.eq_ignore_ascii_case("false") {
4256 return Value::Bool(Some(false));
4257 }
4258
4259 if let Ok(i) = trimmed.parse::<i64>() {
4261 return Value::BigInt(Some(i));
4262 }
4263
4264 if let Ok(f) = trimmed.parse::<f64>() {
4266 return Value::Double(Some(f));
4267 }
4268
4269 if (trimmed.starts_with('"') && trimmed.ends_with('"'))
4271 || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
4272 {
4273 let unquoted = &trimmed[1..trimmed.len() - 1];
4274 return Value::String(Some(Box::new(unquoted.to_string())));
4275 }
4276
4277 if ((trimmed.starts_with('[') && trimmed.ends_with(']'))
4279 || (trimmed.starts_with('{') && trimmed.ends_with('}')))
4280 && let Ok(json) = serde_json::from_str::<serde_json::Value>(trimmed)
4281 {
4282 return json_to_sea_value(&json);
4283 }
4284
4285 const SQL_CONSTANTS: &[&str] = &[
4287 "CURRENT_TIMESTAMP",
4288 "CURRENT_DATE",
4289 "CURRENT_TIME",
4290 "CURRENT_USER",
4291 "SESSION_USER",
4292 "LOCALTIME",
4293 "LOCALTIMESTAMP",
4294 ];
4295
4296 if trimmed.ends_with("()") || trimmed.contains('(') {
4298 return Value::String(Some(Box::new(trimmed.to_string())));
4299 }
4300
4301 if SQL_CONSTANTS
4303 .iter()
4304 .any(|c| trimmed.eq_ignore_ascii_case(c))
4305 {
4306 return Value::String(Some(Box::new(trimmed.to_string())));
4307 }
4308
4309 Value::String(Some(Box::new(format!("'{}'", trimmed.replace('\'', "''")))))
4311 }
4312}
4313
4314fn json_to_sea_value(json: &serde_json::Value) -> Value {
4316 match json {
4317 serde_json::Value::Null => Value::String(None),
4318 serde_json::Value::Bool(b) => Value::Bool(Some(*b)),
4319 serde_json::Value::Number(n) => {
4320 if let Some(i) = n.as_i64() {
4321 Value::BigInt(Some(i))
4322 } else if let Some(f) = n.as_f64() {
4323 Value::Double(Some(f))
4324 } else {
4325 Value::String(Some(Box::new(n.to_string())))
4326 }
4327 }
4328 serde_json::Value::String(s) => Value::String(Some(Box::new(s.clone()))),
4329 serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
4330 Value::String(Some(Box::new(json.to_string())))
4332 }
4333 }
4334}
4335
4336use super::operation_trait::MigrationOperation;
4338
4339impl MigrationOperation for Operation {
4340 fn migration_name_fragment(&self) -> Option<String> {
4341 match self {
4342 Operation::CreateTable { name, .. } => Some(name.to_lowercase()),
4343 Operation::DropTable { name } => Some(format!("delete_{}", name.to_lowercase())),
4344 Operation::AddColumn { table, column, .. } => Some(format!(
4345 "{}_{}",
4346 table.to_lowercase(),
4347 column.name.to_lowercase()
4348 )),
4349 Operation::DropColumn { table, column } => Some(format!(
4350 "remove_{}_{}",
4351 table.to_lowercase(),
4352 column.to_lowercase()
4353 )),
4354 Operation::AlterColumn { table, column, .. } => Some(format!(
4355 "alter_{}_{}",
4356 table.to_lowercase(),
4357 column.to_lowercase()
4358 )),
4359 Operation::RenameTable { old_name, new_name } => Some(format!(
4360 "rename_{}_to_{}",
4361 old_name.to_lowercase(),
4362 new_name.to_lowercase()
4363 )),
4364 Operation::RenameColumn {
4365 table, new_name, ..
4366 } => Some(format!(
4367 "rename_{}_{}",
4368 table.to_lowercase(),
4369 new_name.to_lowercase()
4370 )),
4371 Operation::AddConstraint { table, .. } => {
4372 Some(format!("add_constraint_{}", table.to_lowercase()))
4373 }
4374 Operation::DropConstraint {
4375 table: _,
4376 constraint_name,
4377 } => Some(format!(
4378 "drop_constraint_{}",
4379 constraint_name.to_lowercase()
4380 )),
4381 Operation::CreateIndex { table, unique, .. } => {
4382 if *unique {
4383 Some(format!("create_unique_index_{}", table.to_lowercase()))
4384 } else {
4385 Some(format!("create_index_{}", table.to_lowercase()))
4386 }
4387 }
4388 Operation::DropIndex { table, .. } => {
4389 Some(format!("drop_index_{}", table.to_lowercase()))
4390 }
4391 Operation::RunSQL { .. } => None, Operation::RunRust { .. } => None, Operation::AlterTableComment { table, .. } => {
4394 Some(format!("alter_comment_{}", table.to_lowercase()))
4395 }
4396 Operation::AlterUniqueTogether { table, .. } => {
4397 Some(format!("alter_unique_{}", table.to_lowercase()))
4398 }
4399 Operation::AlterModelOptions { table, .. } => {
4400 Some(format!("alter_options_{}", table.to_lowercase()))
4401 }
4402 Operation::CreateInheritedTable { name, .. } => {
4403 Some(format!("create_inherited_{}", name.to_lowercase()))
4404 }
4405 Operation::AddDiscriminatorColumn { table, .. } => {
4406 Some(format!("add_discriminator_{}", table.to_lowercase()))
4407 }
4408 Operation::MoveModel {
4409 model_name,
4410 from_app,
4411 to_app,
4412 ..
4413 } => Some(format!(
4414 "move_{}_{}_{}_{}",
4415 from_app.to_lowercase(),
4416 model_name.to_lowercase(),
4417 to_app.to_lowercase(),
4418 model_name.to_lowercase()
4419 )),
4420 Operation::CreateSchema { name, .. } => {
4421 Some(format!("create_schema_{}", name.to_lowercase()))
4422 }
4423 Operation::DropSchema { name, .. } => {
4424 Some(format!("drop_schema_{}", name.to_lowercase()))
4425 }
4426 Operation::CreateExtension { name, .. } => {
4427 Some(format!("create_extension_{}", name.to_lowercase()))
4428 }
4429 Operation::BulkLoad { table, .. } => {
4430 Some(format!("bulk_load_{}", table.to_lowercase()))
4431 }
4432 Operation::SetAutoIncrementValue { table, column, .. } => Some(format!(
4433 "set_auto_increment_{}_{}",
4434 table.to_lowercase(),
4435 column.to_lowercase()
4436 )),
4437 Operation::CreateCompositePrimaryKey { table, .. } => {
4438 Some(format!("composite_pk_{}", table.to_lowercase()))
4439 }
4440 }
4441 }
4442
4443 fn describe(&self) -> String {
4444 match self {
4445 Operation::CreateTable { name, .. } => format!("Create table {}", name),
4446 Operation::DropTable { name } => format!("Drop table {}", name),
4447 Operation::AddColumn { table, column, .. } => {
4448 format!("Add column {} to {}", column.name, table)
4449 }
4450 Operation::DropColumn { table, column } => {
4451 format!("Drop column {} from {}", column, table)
4452 }
4453 Operation::AlterColumn { table, column, .. } => {
4454 format!("Alter column {} on {}", column, table)
4455 }
4456 Operation::RenameTable { old_name, new_name } => {
4457 format!("Rename table {} to {}", old_name, new_name)
4458 }
4459 Operation::RenameColumn {
4460 table,
4461 old_name,
4462 new_name,
4463 } => format!("Rename column {} to {} on {}", old_name, new_name, table),
4464 Operation::AddConstraint { table, .. } => format!("Add constraint on {}", table),
4465 Operation::DropConstraint {
4466 table,
4467 constraint_name,
4468 } => format!("Drop constraint {} from {}", constraint_name, table),
4469 Operation::CreateIndex { table, unique, .. } => {
4470 if *unique {
4471 format!("Create unique index on {}", table)
4472 } else {
4473 format!("Create index on {}", table)
4474 }
4475 }
4476 Operation::DropIndex { table, .. } => format!("Drop index on {}", table),
4477 Operation::RunSQL { sql, .. } => {
4478 let preview = if sql.len() > 50 {
4479 format!("{}...", &sql[..50])
4480 } else {
4481 (*sql).to_string()
4482 };
4483 format!("RunSQL: {}", preview)
4484 }
4485 Operation::RunRust { code, .. } => {
4486 let preview = if code.len() > 50 {
4487 format!("{}...", &code[..50])
4488 } else {
4489 (*code).to_string()
4490 };
4491 format!("RunRust: {}", preview)
4492 }
4493 Operation::AlterTableComment { table, comment } => match comment {
4494 Some(c) => format!("Set comment on {} to '{}'", table, c),
4495 None => format!("Remove comment from {}", table),
4496 },
4497 Operation::AlterUniqueTogether { table, .. } => {
4498 format!("Alter unique_together on {}", table)
4499 }
4500 Operation::AlterModelOptions { table, .. } => {
4501 format!("Alter model options on {}", table)
4502 }
4503 Operation::CreateInheritedTable {
4504 name, base_table, ..
4505 } => {
4506 format!("Create inherited table {} from {}", name, base_table)
4507 }
4508 Operation::AddDiscriminatorColumn {
4509 table, column_name, ..
4510 } => format!("Add discriminator column {} to {}", column_name, table),
4511 Operation::MoveModel {
4512 model_name,
4513 from_app,
4514 to_app,
4515 ..
4516 } => format!("Move model {} from {} to {}", model_name, from_app, to_app),
4517 Operation::CreateSchema { name, .. } => format!("Create schema {}", name),
4518 Operation::DropSchema { name, .. } => format!("Drop schema {}", name),
4519 Operation::CreateExtension { name, .. } => format!("Create extension {}", name),
4520 Operation::BulkLoad { table, source, .. } => {
4521 let source_desc = match source {
4522 BulkLoadSource::File(path) => format!("file '{}'", path),
4523 BulkLoadSource::Stdin => "STDIN".to_string(),
4524 BulkLoadSource::Program(cmd) => format!("program '{}'", cmd),
4525 };
4526 format!("Bulk load data into {} from {}", table, source_desc)
4527 }
4528 Operation::SetAutoIncrementValue {
4529 table,
4530 column,
4531 value,
4532 } => format!("Set auto-increment of {}.{} to {}", table, column, value),
4533 Operation::CreateCompositePrimaryKey { table, columns, .. } => format!(
4534 "Create composite primary key on {} ({})",
4535 table,
4536 columns.join(", ")
4537 ),
4538 }
4539 }
4540
4541 fn normalize(&self) -> Self
4546 where
4547 Self: Sized + Clone,
4548 {
4549 match self {
4550 Operation::CreateTable {
4552 name,
4553 columns,
4554 constraints,
4555 without_rowid,
4556 interleave_in_parent,
4557 partition,
4558 } => {
4559 let mut sorted_columns = columns.clone();
4560 sorted_columns.sort_by(|a, b| a.name.cmp(&b.name));
4561
4562 let mut sorted_constraints = constraints.clone();
4563 sorted_constraints.sort();
4564
4565 Operation::CreateTable {
4566 name: name.clone(),
4567 columns: sorted_columns,
4568 constraints: sorted_constraints,
4569 without_rowid: *without_rowid,
4570 interleave_in_parent: interleave_in_parent.clone(),
4571 partition: partition.clone(),
4572 }
4573 }
4574 Operation::CreateIndex {
4576 table,
4577 columns,
4578 unique,
4579 index_type,
4580 where_clause,
4581 concurrently,
4582 expressions,
4583 mysql_options,
4584 operator_class,
4585 } => {
4586 let mut sorted_columns = columns.clone();
4587 sorted_columns.sort();
4588
4589 Operation::CreateIndex {
4590 table: table.clone(),
4591 columns: sorted_columns,
4592 unique: *unique,
4593 index_type: *index_type,
4594 where_clause: where_clause.clone(),
4595 concurrently: *concurrently,
4596 expressions: expressions.clone(),
4597 mysql_options: *mysql_options,
4598 operator_class: operator_class.clone(),
4599 }
4600 }
4601 Operation::DropIndex { table, columns } => {
4603 let mut sorted_columns = columns.clone();
4604 sorted_columns.sort();
4605
4606 Operation::DropIndex {
4607 table: table.clone(),
4608 columns: sorted_columns,
4609 }
4610 }
4611 Operation::AlterUniqueTogether {
4613 table,
4614 unique_together,
4615 } => {
4616 let mut sorted_unique_together: Vec<Vec<String>> = unique_together
4617 .iter()
4618 .map(|field_list| {
4619 let mut sorted = field_list.clone();
4620 sorted.sort();
4621 sorted
4622 })
4623 .collect();
4624 sorted_unique_together.sort();
4625
4626 Operation::AlterUniqueTogether {
4627 table: table.clone(),
4628 unique_together: sorted_unique_together,
4629 }
4630 }
4631 Operation::AlterModelOptions { table, options } => Operation::AlterModelOptions {
4636 table: table.clone(),
4637 options: options.clone(),
4638 },
4639 _ => self.clone(),
4641 }
4642 }
4643}
4644
4645#[cfg(test)]
4646mod tests {
4647 use super::*;
4648 use FieldType;
4649 use rstest::rstest;
4650
4651 #[test]
4652 fn test_create_table_to_statement() {
4653 let op = Operation::CreateTable {
4654 name: "users".to_string(),
4655 columns: vec![
4656 ColumnDefinition {
4657 name: "id".to_string(),
4658 type_definition: FieldType::Integer,
4659 not_null: false,
4660 unique: false,
4661 primary_key: true,
4662 auto_increment: true,
4663 default: None,
4664 },
4665 ColumnDefinition {
4666 name: "name".to_string(),
4667 type_definition: FieldType::VarChar(100),
4668 not_null: true,
4669 unique: false,
4670 primary_key: false,
4671 auto_increment: false,
4672 default: None,
4673 },
4674 ],
4675 constraints: vec![],
4676 without_rowid: None,
4677 partition: None,
4678 interleave_in_parent: None,
4679 };
4680
4681 let stmt = op.to_statement();
4682 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4683 assert!(
4684 sql.contains("CREATE TABLE"),
4685 "SQL should contain CREATE TABLE keyword, got: {}",
4686 sql
4687 );
4688 assert!(
4689 sql.contains("users"),
4690 "SQL should reference 'users' table, got: {}",
4691 sql
4692 );
4693 assert!(
4694 sql.contains("id") && sql.contains("name"),
4695 "SQL should contain both 'id' and 'name' columns, got: {}",
4696 sql
4697 );
4698 }
4699
4700 #[test]
4701 fn test_drop_table_to_statement() {
4702 let op = Operation::DropTable {
4703 name: "users".to_string(),
4704 };
4705
4706 let stmt = op.to_statement();
4707 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4708 assert!(
4709 sql.contains("DROP TABLE"),
4710 "SQL should contain DROP TABLE keyword, got: {}",
4711 sql
4712 );
4713 assert!(
4714 sql.contains("users"),
4715 "SQL should reference 'users' table, got: {}",
4716 sql
4717 );
4718 assert!(
4719 sql.contains("CASCADE"),
4720 "SQL should include CASCADE option, got: {}",
4721 sql
4722 );
4723 }
4724
4725 #[test]
4726 fn test_add_column_to_statement() {
4727 let op = Operation::AddColumn {
4728 table: "users".to_string(),
4729 column: ColumnDefinition {
4730 name: "email".to_string(),
4731 type_definition: FieldType::VarChar(255),
4732 not_null: true,
4733 unique: false,
4734 primary_key: false,
4735 auto_increment: false,
4736 default: Some("''".to_string()),
4737 },
4738 mysql_options: None,
4739 };
4740
4741 let stmt = op.to_statement();
4742 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4743 assert!(
4744 sql.contains("ALTER TABLE"),
4745 "SQL should contain ALTER TABLE keyword, got: {}",
4746 sql
4747 );
4748 assert!(
4749 sql.contains("users"),
4750 "SQL should reference 'users' table, got: {}",
4751 sql
4752 );
4753 assert!(
4754 sql.contains("ADD COLUMN"),
4755 "SQL should contain ADD COLUMN clause, got: {}",
4756 sql
4757 );
4758 assert!(
4759 sql.contains("email"),
4760 "SQL should reference 'email' column, got: {}",
4761 sql
4762 );
4763 }
4764
4765 #[test]
4766 fn test_drop_column_to_statement() {
4767 let op = Operation::DropColumn {
4768 table: "users".to_string(),
4769 column: "email".to_string(),
4770 };
4771
4772 let stmt = op.to_statement();
4773 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4774 assert!(
4775 sql.contains("ALTER TABLE"),
4776 "SQL should contain ALTER TABLE keyword, got: {}",
4777 sql
4778 );
4779 assert!(
4780 sql.contains("users"),
4781 "SQL should reference 'users' table, got: {}",
4782 sql
4783 );
4784 assert!(
4785 sql.contains("DROP COLUMN"),
4786 "SQL should contain DROP COLUMN clause, got: {}",
4787 sql
4788 );
4789 assert!(
4790 sql.contains("email"),
4791 "SQL should reference 'email' column, got: {}",
4792 sql
4793 );
4794 }
4795
4796 #[test]
4797 fn test_alter_column_to_statement() {
4798 let op = Operation::AlterColumn {
4799 table: "users".to_string(),
4800 column: "age".to_string(),
4801 old_definition: None,
4802 new_definition: ColumnDefinition {
4803 name: "age".to_string(),
4804 type_definition: FieldType::BigInteger,
4805 not_null: true,
4806 unique: false,
4807 primary_key: false,
4808 auto_increment: false,
4809 default: None,
4810 },
4811 mysql_options: None,
4812 };
4813
4814 let stmt = op.to_statement();
4815 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4816 assert!(
4817 sql.contains("ALTER TABLE"),
4818 "SQL should contain ALTER TABLE keyword, got: {}",
4819 sql
4820 );
4821 assert!(
4822 sql.contains("users"),
4823 "SQL should reference 'users' table, got: {}",
4824 sql
4825 );
4826 assert!(
4827 sql.contains("age"),
4828 "SQL should reference 'age' column, got: {}",
4829 sql
4830 );
4831 }
4832
4833 #[test]
4834 fn test_rename_table_to_statement() {
4835 let op = Operation::RenameTable {
4836 old_name: "users".to_string(),
4837 new_name: "accounts".to_string(),
4838 };
4839
4840 let stmt = op.to_statement();
4841 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4842 assert!(
4843 sql.contains("users"),
4844 "SQL should reference old table name 'users', got: {}",
4845 sql
4846 );
4847 assert!(
4848 sql.contains("accounts"),
4849 "SQL should reference new table name 'accounts', got: {}",
4850 sql
4851 );
4852 }
4853
4854 #[test]
4855 fn test_rename_column_to_statement() {
4856 let op = Operation::RenameColumn {
4857 table: "users".to_string(),
4858 old_name: "name".to_string(),
4859 new_name: "full_name".to_string(),
4860 };
4861
4862 let stmt = op.to_statement();
4863 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4864 assert!(
4865 sql.contains("ALTER TABLE"),
4866 "SQL should contain ALTER TABLE keyword, got: {}",
4867 sql
4868 );
4869 assert!(
4870 sql.contains("users"),
4871 "SQL should reference 'users' table, got: {}",
4872 sql
4873 );
4874 assert!(
4875 sql.contains("RENAME COLUMN"),
4876 "SQL should contain RENAME COLUMN clause, got: {}",
4877 sql
4878 );
4879 assert!(
4880 sql.contains("name"),
4881 "SQL should reference old column name 'name', got: {}",
4882 sql
4883 );
4884 assert!(
4885 sql.contains("full_name"),
4886 "SQL should reference new column name 'full_name', got: {}",
4887 sql
4888 );
4889 }
4890
4891 #[test]
4892 fn test_add_constraint_to_statement() {
4893 let op = Operation::AddConstraint {
4894 table: "users".to_string(),
4895 constraint_sql: "CONSTRAINT age_check CHECK (age >= 0)".to_string(),
4896 };
4897
4898 let stmt = op.to_statement();
4899 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4900 assert!(
4901 sql.contains("ALTER TABLE"),
4902 "SQL should contain ALTER TABLE keyword, got: {}",
4903 sql
4904 );
4905 assert!(
4906 sql.contains("users"),
4907 "SQL should reference 'users' table, got: {}",
4908 sql
4909 );
4910 assert!(
4911 sql.contains("ADD"),
4912 "SQL should contain ADD keyword, got: {}",
4913 sql
4914 );
4915 assert!(
4916 sql.contains("age_check"),
4917 "SQL should contain constraint name 'age_check', got: {}",
4918 sql
4919 );
4920 }
4921
4922 #[test]
4923 fn test_drop_constraint_to_statement() {
4924 let op = Operation::DropConstraint {
4925 table: "users".to_string(),
4926 constraint_name: "age_check".to_string(),
4927 };
4928
4929 let stmt = op.to_statement();
4930 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4931 assert!(
4932 sql.contains("ALTER TABLE"),
4933 "SQL should contain ALTER TABLE keyword, got: {}",
4934 sql
4935 );
4936 assert!(
4937 sql.contains("users"),
4938 "SQL should reference 'users' table, got: {}",
4939 sql
4940 );
4941 assert!(
4942 sql.contains("DROP CONSTRAINT"),
4943 "SQL should contain DROP CONSTRAINT clause, got: {}",
4944 sql
4945 );
4946 assert!(
4947 sql.contains("age_check"),
4948 "SQL should reference constraint 'age_check', got: {}",
4949 sql
4950 );
4951 }
4952
4953 #[test]
4954 fn test_create_index_to_statement() {
4955 let op = Operation::CreateIndex {
4956 table: "users".to_string(),
4957 columns: vec!["email".to_string()],
4958 unique: false,
4959 index_type: None,
4960 where_clause: None,
4961 concurrently: false,
4962 expressions: None,
4963 mysql_options: None,
4964 operator_class: None,
4965 };
4966
4967 let stmt = op.to_statement();
4968 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
4969 assert!(
4970 sql.contains("CREATE INDEX"),
4971 "SQL should contain CREATE INDEX keywords, got: {}",
4972 sql
4973 );
4974 assert!(
4975 sql.contains("users"),
4976 "SQL should reference 'users' table, got: {}",
4977 sql
4978 );
4979 assert!(
4980 sql.contains("email"),
4981 "SQL should reference 'email' column, got: {}",
4982 sql
4983 );
4984 }
4985
4986 #[test]
4987 fn test_create_unique_index_to_statement() {
4988 let op = Operation::CreateIndex {
4989 table: "users".to_string(),
4990 columns: vec!["email".to_string()],
4991 unique: true,
4992 index_type: None,
4993 where_clause: None,
4994 concurrently: false,
4995 expressions: None,
4996 mysql_options: None,
4997 operator_class: None,
4998 };
4999
5000 let stmt = op.to_statement();
5001 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5002 assert!(
5003 sql.contains("CREATE UNIQUE INDEX"),
5004 "SQL should contain CREATE UNIQUE INDEX keywords, got: {}",
5005 sql
5006 );
5007 assert!(
5008 sql.contains("users"),
5009 "SQL should reference 'users' table, got: {}",
5010 sql
5011 );
5012 assert!(
5013 sql.contains("email"),
5014 "SQL should reference 'email' column, got: {}",
5015 sql
5016 );
5017 }
5018
5019 #[test]
5020 fn test_drop_index_to_statement() {
5021 let op = Operation::DropIndex {
5022 table: "users".to_string(),
5023 columns: vec!["email".to_string()],
5024 };
5025
5026 let stmt = op.to_statement();
5027 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5028 assert!(
5029 sql.contains("DROP INDEX"),
5030 "SQL should contain DROP INDEX keywords, got: {}",
5031 sql
5032 );
5033 assert!(
5034 sql.contains("idx_users_email"),
5035 "SQL should contain generated index name 'idx_users_email', got: {}",
5036 sql
5037 );
5038 }
5039
5040 #[test]
5041 fn test_run_sql_to_statement() {
5042 let op = Operation::RunSQL {
5043 sql: "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"".to_string(),
5044 reverse_sql: Some("DROP EXTENSION \"uuid-ossp\"".to_string()),
5045 };
5046
5047 let stmt = op.to_statement();
5048 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5049 assert!(
5050 sql.contains("CREATE EXTENSION"),
5051 "SQL should contain CREATE EXTENSION keywords, got: {}",
5052 sql
5053 );
5054 assert!(
5055 sql.contains("uuid-ossp"),
5056 "SQL should reference 'uuid-ossp' extension, got: {}",
5057 sql
5058 );
5059 }
5060
5061 #[test]
5062 fn test_alter_table_comment_to_statement() {
5063 let op = Operation::AlterTableComment {
5064 table: "users".to_string(),
5065 comment: Some("User accounts table".to_string()),
5066 };
5067
5068 let stmt = op.to_statement();
5069 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5070 assert!(
5071 sql.contains("COMMENT ON TABLE"),
5072 "SQL should contain COMMENT ON TABLE keywords, got: {}",
5073 sql
5074 );
5075 assert!(
5076 sql.contains("users"),
5077 "SQL should reference 'users' table, got: {}",
5078 sql
5079 );
5080 assert!(
5081 sql.contains("User accounts table"),
5082 "SQL should include comment text 'User accounts table', got: {}",
5083 sql
5084 );
5085 }
5086
5087 #[test]
5088 fn test_alter_table_comment_null_to_statement() {
5089 let op = Operation::AlterTableComment {
5090 table: "users".to_string(),
5091 comment: None,
5092 };
5093
5094 let stmt = op.to_statement();
5095 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5096 assert!(
5097 sql.contains("COMMENT ON TABLE"),
5098 "SQL should contain COMMENT ON TABLE keywords, got: {}",
5099 sql
5100 );
5101 assert!(
5102 sql.contains("users"),
5103 "SQL should reference 'users' table, got: {}",
5104 sql
5105 );
5106 assert!(
5107 sql.contains("NULL"),
5108 "SQL should include NULL for null comment, got: {}",
5109 sql
5110 );
5111 }
5112
5113 #[test]
5114 fn test_alter_unique_together_to_statement() {
5115 let op = Operation::AlterUniqueTogether {
5116 table: "users".to_string(),
5117 unique_together: vec![vec!["email".to_string(), "username".to_string()]],
5118 };
5119
5120 let stmt = op.to_statement();
5121 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5122 assert!(
5123 sql.contains("ALTER TABLE"),
5124 "SQL should contain ALTER TABLE keyword, got: {}",
5125 sql
5126 );
5127 assert!(
5128 sql.contains("users"),
5129 "SQL should reference 'users' table, got: {}",
5130 sql
5131 );
5132 assert!(
5133 sql.contains("ADD CONSTRAINT"),
5134 "SQL should contain ADD CONSTRAINT clause, got: {}",
5135 sql
5136 );
5137 assert!(
5138 sql.contains("UNIQUE"),
5139 "SQL should contain UNIQUE keyword, got: {}",
5140 sql
5141 );
5142 assert!(
5143 sql.contains("email") && sql.contains("username"),
5144 "SQL should reference both 'email' and 'username' columns, got: {}",
5145 sql
5146 );
5147 }
5148
5149 #[test]
5150 fn test_alter_unique_together_empty() {
5151 let op = Operation::AlterUniqueTogether {
5152 table: "users".to_string(),
5153 unique_together: vec![],
5154 };
5155
5156 let stmt = op.to_statement();
5157 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5158 assert_eq!(
5159 sql, "",
5160 "SQL should be empty for empty unique_together constraint"
5161 );
5162 }
5163
5164 #[test]
5165 fn test_alter_model_options_to_statement() {
5166 let mut options = std::collections::HashMap::new();
5167 options.insert("db_table".to_string(), "custom_users".to_string());
5168
5169 let op = Operation::AlterModelOptions {
5170 table: "users".to_string(),
5171 options,
5172 };
5173
5174 let stmt = op.to_statement();
5175 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5176 assert_eq!(sql, "", "SQL should be empty for model options operation");
5177 }
5178
5179 #[test]
5180 fn test_create_inherited_table_to_statement() {
5181 let op = Operation::CreateInheritedTable {
5182 name: "admin_users".to_string(),
5183 columns: vec![ColumnDefinition {
5184 name: "admin_level".to_string(),
5185 type_definition: FieldType::Integer,
5186 not_null: true,
5187 unique: false,
5188 primary_key: false,
5189 auto_increment: false,
5190 default: Some("1".to_string()),
5191 }],
5192 base_table: "users".to_string(),
5193 join_column: "user_id".to_string(),
5194 };
5195
5196 let stmt = op.to_statement();
5197 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5198 assert!(
5199 sql.contains("CREATE TABLE"),
5200 "SQL should contain CREATE TABLE keywords, got: {}",
5201 sql
5202 );
5203 assert!(
5204 sql.contains("admin_users"),
5205 "SQL should reference 'admin_users' table, got: {}",
5206 sql
5207 );
5208 assert!(
5209 sql.contains("user_id"),
5210 "SQL should include join column 'user_id', got: {}",
5211 sql
5212 );
5213 }
5214
5215 #[test]
5216 fn test_add_discriminator_column_to_statement() {
5217 let op = Operation::AddDiscriminatorColumn {
5218 table: "users".to_string(),
5219 column_name: "user_type".to_string(),
5220 default_value: "regular".to_string(),
5221 };
5222
5223 let stmt = op.to_statement();
5224 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
5225 assert!(
5226 sql.contains("ALTER TABLE"),
5227 "SQL should contain ALTER TABLE keyword, got: {}",
5228 sql
5229 );
5230 assert!(
5231 sql.contains("users"),
5232 "SQL should reference 'users' table, got: {}",
5233 sql
5234 );
5235 assert!(
5236 sql.contains("ADD COLUMN"),
5237 "SQL should contain ADD COLUMN clause, got: {}",
5238 sql
5239 );
5240 assert!(
5241 sql.contains("user_type"),
5242 "SQL should reference 'user_type' column, got: {}",
5243 sql
5244 );
5245 }
5246
5247 #[test]
5248 fn test_state_forwards_create_table() {
5249 let mut state = ProjectState::new();
5250 let op = Operation::CreateTable {
5251 name: "users".to_string(),
5252 columns: vec![
5253 ColumnDefinition {
5254 name: "id".to_string(),
5255 type_definition: FieldType::Integer,
5256 not_null: false,
5257 unique: false,
5258 primary_key: true,
5259 auto_increment: true,
5260 default: None,
5261 },
5262 ColumnDefinition {
5263 name: "name".to_string(),
5264 type_definition: FieldType::VarChar(100),
5265 not_null: true,
5266 unique: false,
5267 primary_key: false,
5268 auto_increment: false,
5269 default: None,
5270 },
5271 ],
5272 constraints: vec![],
5273 without_rowid: None,
5274 partition: None,
5275 interleave_in_parent: None,
5276 };
5277
5278 op.state_forwards("myapp", &mut state);
5279 let model = state.get_model("myapp", "users");
5280 assert!(model.is_some(), "Model 'users' should exist in state");
5281 let model = model.unwrap();
5282 assert_eq!(
5283 model.fields.len(),
5284 2,
5285 "Model should have exactly 2 fields, got: {}",
5286 model.fields.len()
5287 );
5288 assert!(
5289 model.fields.contains_key("id"),
5290 "Model should contain 'id' field"
5291 );
5292 assert!(
5293 model.fields.contains_key("name"),
5294 "Model should contain 'name' field"
5295 );
5296 }
5297
5298 #[test]
5299 fn test_state_forwards_drop_table() {
5300 let mut state = ProjectState::new();
5301 let mut model = ModelState::new("myapp", "users");
5302 model.add_field(FieldState::new("id".to_string(), FieldType::Integer, false));
5303 state.add_model(model);
5304
5305 let op = Operation::DropTable {
5306 name: "users".to_string(),
5307 };
5308
5309 op.state_forwards("myapp", &mut state);
5310 assert!(
5311 state.get_model("myapp", "users").is_none(),
5312 "Model 'users' should be removed from state after drop"
5313 );
5314 }
5315
5316 #[test]
5317 fn test_state_forwards_add_column() {
5318 let mut state = ProjectState::new();
5319 let mut model = ModelState::new("myapp", "users");
5320 model.add_field(FieldState::new("id".to_string(), FieldType::Integer, false));
5321 state.add_model(model);
5322
5323 let op = Operation::AddColumn {
5324 table: "users".to_string(),
5325 column: ColumnDefinition {
5326 name: "email".to_string(),
5327 type_definition: FieldType::VarChar(255),
5328 not_null: true,
5329 unique: false,
5330 primary_key: false,
5331 auto_increment: false,
5332 default: None,
5333 },
5334 mysql_options: None,
5335 };
5336
5337 op.state_forwards("myapp", &mut state);
5338 let model = state.get_model("myapp", "users").unwrap();
5339 assert_eq!(
5340 model.fields.len(),
5341 2,
5342 "Model should have 2 fields after adding 'email', got: {}",
5343 model.fields.len()
5344 );
5345 assert!(
5346 model.fields.contains_key("email"),
5347 "Model should contain newly added 'email' field"
5348 );
5349 }
5350
5351 #[test]
5352 fn test_state_forwards_drop_column() {
5353 let mut state = ProjectState::new();
5354 let mut model = ModelState::new("myapp", "users");
5355 model.add_field(FieldState::new("id".to_string(), FieldType::Integer, false));
5356 model.add_field(FieldState::new(
5357 "email".to_string(),
5358 FieldType::VarChar(255),
5359 false,
5360 ));
5361 state.add_model(model);
5362
5363 let op = Operation::DropColumn {
5364 table: "users".to_string(),
5365 column: "email".to_string(),
5366 };
5367
5368 op.state_forwards("myapp", &mut state);
5369 let model = state.get_model("myapp", "users").unwrap();
5370 assert_eq!(
5371 model.fields.len(),
5372 1,
5373 "Model should have 1 field after dropping 'email', got: {}",
5374 model.fields.len()
5375 );
5376 assert!(
5377 !model.fields.contains_key("email"),
5378 "Model should not contain dropped 'email' field"
5379 );
5380 }
5381
5382 #[test]
5383 fn test_state_forwards_rename_table() {
5384 let mut state = ProjectState::new();
5385 let mut model = ModelState::new("myapp", "users");
5386 model.add_field(FieldState::new("id".to_string(), FieldType::Integer, false));
5387 state.add_model(model);
5388
5389 let op = Operation::RenameTable {
5390 old_name: "users".to_string(),
5391 new_name: "accounts".to_string(),
5392 };
5393
5394 op.state_forwards("myapp", &mut state);
5395 assert!(
5396 state.get_model("myapp", "users").is_none(),
5397 "Old model name 'users' should not exist after rename"
5398 );
5399 assert!(
5400 state.get_model("myapp", "accounts").is_some(),
5401 "New model name 'accounts' should exist after rename"
5402 );
5403 }
5404
5405 #[test]
5406 fn test_state_forwards_rename_column() {
5407 let mut state = ProjectState::new();
5408 let mut model = ModelState::new("myapp", "users");
5409 model.add_field(FieldState::new(
5410 "name".to_string(),
5411 FieldType::VarChar(255),
5412 false,
5413 ));
5414 state.add_model(model);
5415
5416 let op = Operation::RenameColumn {
5417 table: "users".to_string(),
5418 old_name: "name".to_string(),
5419 new_name: "full_name".to_string(),
5420 };
5421
5422 op.state_forwards("myapp", &mut state);
5423 let model = state.get_model("myapp", "users").unwrap();
5424 assert!(
5425 !model.fields.contains_key("name"),
5426 "Old field name 'name' should not exist after rename"
5427 );
5428 assert!(
5429 model.fields.contains_key("full_name"),
5430 "New field name 'full_name' should exist after rename"
5431 );
5432 }
5433
5434 #[test]
5435 fn test_to_reverse_sql_create_table() {
5436 let op = Operation::CreateTable {
5437 name: "users".to_string(),
5438 columns: vec![],
5439 constraints: vec![],
5440 without_rowid: None,
5441 partition: None,
5442 interleave_in_parent: None,
5443 };
5444
5445 let state = ProjectState::default();
5446 let reverse = op.to_reverse_sql(&SqlDialect::Postgres, &state);
5447 assert!(
5448 reverse.is_ok() && reverse.as_ref().ok().unwrap().is_some(),
5449 "CreateTable should have reverse SQL operation"
5450 );
5451 let sql = reverse.unwrap().unwrap().join("\n");
5452 assert!(
5453 sql.contains("DROP TABLE"),
5454 "Reverse SQL should contain DROP TABLE, got: {}",
5455 sql
5456 );
5457 assert!(
5458 sql.contains("users"),
5459 "Reverse SQL should reference 'users' table, got: {}",
5460 sql
5461 );
5462 }
5463
5464 #[test]
5465 fn test_to_reverse_sql_drop_table() {
5466 let op = Operation::DropTable {
5467 name: "users".to_string(),
5468 };
5469
5470 let state = ProjectState::default();
5471 let reverse = op.to_reverse_sql(&SqlDialect::Postgres, &state);
5472 assert!(
5473 reverse.is_ok() && reverse.as_ref().ok().unwrap().is_none(),
5474 "DropTable should not have reverse SQL (cannot recreate table structure)"
5475 );
5476 }
5477
5478 #[test]
5479 fn test_to_reverse_sql_add_column() {
5480 let op = Operation::AddColumn {
5481 table: "users".to_string(),
5482 column: ColumnDefinition {
5483 name: "email".to_string(),
5484 type_definition: FieldType::VarChar(255),
5485 not_null: false,
5486 unique: false,
5487 primary_key: false,
5488 auto_increment: false,
5489 default: None,
5490 },
5491 mysql_options: None,
5492 };
5493
5494 let state = ProjectState::default();
5495 let reverse = op.to_reverse_sql(&SqlDialect::Postgres, &state);
5496 assert!(
5497 reverse.is_ok() && reverse.as_ref().ok().unwrap().is_some(),
5498 "AddColumn should have reverse SQL operation"
5499 );
5500 let sql = reverse.unwrap().unwrap().join("\n");
5501 assert!(
5502 sql.contains("DROP COLUMN"),
5503 "Reverse SQL should contain DROP COLUMN, got: {}",
5504 sql
5505 );
5506 assert!(
5507 sql.contains("email"),
5508 "Reverse SQL should reference 'email' column, got: {}",
5509 sql
5510 );
5511 }
5512
5513 fn alter_column_with_old_def() -> Operation {
5518 Operation::AlterColumn {
5519 table: "products".to_string(),
5520 column: "name".to_string(),
5521 old_definition: Some(ColumnDefinition {
5522 name: "name".to_string(),
5523 type_definition: FieldType::VarChar(50),
5524 not_null: false,
5525 unique: false,
5526 primary_key: false,
5527 auto_increment: false,
5528 default: None,
5529 }),
5530 new_definition: ColumnDefinition {
5531 name: "name".to_string(),
5532 type_definition: FieldType::Text,
5533 not_null: false,
5534 unique: false,
5535 primary_key: false,
5536 auto_increment: false,
5537 default: None,
5538 },
5539 mysql_options: None,
5540 }
5541 }
5542
5543 #[test]
5546 fn test_to_reverse_sql_alter_column_postgres() {
5547 let op = alter_column_with_old_def();
5549 let state = ProjectState::default();
5550
5551 let stmts = op
5555 .to_reverse_sql(&SqlDialect::Postgres, &state)
5556 .expect("reverse SQL should succeed")
5557 .expect("reverse SQL should be present");
5558 let sql = stmts.join("\n");
5559
5560 assert!(
5562 sql.contains("ALTER COLUMN") && sql.contains("TYPE"),
5563 "Postgres reverse SQL should use ALTER COLUMN ... TYPE syntax, got: {}",
5564 sql
5565 );
5566 assert!(
5567 sql.contains("VARCHAR(50)"),
5568 "Postgres reverse SQL should restore VARCHAR(50), got: {}",
5569 sql
5570 );
5571 assert_eq!(
5576 stmts.len(),
5577 2,
5578 "Postgres AlterColumn reverse SQL must emit two statements \
5579 (type + nullability), got: {:?}",
5580 stmts
5581 );
5582 assert!(
5583 stmts[1].contains("DROP NOT NULL"),
5584 "Postgres second statement must restore DROP NOT NULL (was_nullable), got: {}",
5585 stmts[1]
5586 );
5587 }
5588
5589 #[test]
5593 fn test_to_reverse_sql_alter_column_mysql() {
5594 let op = alter_column_with_old_def();
5596 let state = ProjectState::default();
5597
5598 let stmts = op
5601 .to_reverse_sql(&SqlDialect::Mysql, &state)
5602 .expect("reverse SQL should succeed")
5603 .expect("reverse SQL should be present");
5604 assert_eq!(
5605 stmts.len(),
5606 1,
5607 "MySQL AlterColumn reverse SQL should remain a single statement, got: {:?}",
5608 stmts
5609 );
5610 let sql = stmts.join("\n");
5611
5612 assert!(
5614 sql.contains("MODIFY COLUMN"),
5615 "MySQL reverse SQL should use MODIFY COLUMN syntax, got: {}",
5616 sql
5617 );
5618 assert!(
5619 !sql.contains("ALTER COLUMN"),
5620 "MySQL reverse SQL must not emit Postgres ALTER COLUMN syntax, got: {}",
5621 sql
5622 );
5623 assert!(
5624 !sql.contains(" TYPE "),
5625 "MySQL reverse SQL must not contain Postgres ' TYPE ' token, got: {}",
5626 sql
5627 );
5628 assert!(
5629 sql.contains("VARCHAR(50)"),
5630 "MySQL reverse SQL should restore VARCHAR(50), got: {}",
5631 sql
5632 );
5633 }
5634
5635 #[rstest]
5636 #[case::postgres(SqlDialect::Postgres)]
5637 #[case::cockroachdb(SqlDialect::Cockroachdb)]
5638 fn test_to_sql_alter_column_sets_default_for_postgres_family(#[case] dialect: SqlDialect) {
5639 let op = Operation::AlterColumn {
5641 table: "users".to_string(),
5642 column: "is_active".to_string(),
5643 old_definition: Some(ColumnDefinition {
5644 name: "is_active".to_string(),
5645 type_definition: FieldType::Boolean,
5646 not_null: true,
5647 unique: false,
5648 primary_key: false,
5649 auto_increment: false,
5650 default: None,
5651 }),
5652 new_definition: ColumnDefinition {
5653 name: "is_active".to_string(),
5654 type_definition: FieldType::Boolean,
5655 not_null: true,
5656 unique: false,
5657 primary_key: false,
5658 auto_increment: false,
5659 default: Some("true".to_string()),
5660 },
5661 mysql_options: None,
5662 };
5663
5664 let sql = op.to_sql(&dialect);
5666
5667 assert!(
5669 sql.contains("ALTER COLUMN is_active SET DEFAULT true"),
5670 "AlterColumn must apply new database defaults, got: {}",
5671 sql
5672 );
5673 }
5674
5675 #[rstest]
5676 #[case::postgres(SqlDialect::Postgres)]
5677 #[case::cockroachdb(SqlDialect::Cockroachdb)]
5678 fn test_to_sql_alter_column_drops_default_for_postgres_family(#[case] dialect: SqlDialect) {
5679 let op = Operation::AlterColumn {
5681 table: "users".to_string(),
5682 column: "is_active".to_string(),
5683 old_definition: Some(ColumnDefinition {
5684 name: "is_active".to_string(),
5685 type_definition: FieldType::Boolean,
5686 not_null: true,
5687 unique: false,
5688 primary_key: false,
5689 auto_increment: false,
5690 default: Some("true".to_string()),
5691 }),
5692 new_definition: ColumnDefinition {
5693 name: "is_active".to_string(),
5694 type_definition: FieldType::Boolean,
5695 not_null: true,
5696 unique: false,
5697 primary_key: false,
5698 auto_increment: false,
5699 default: None,
5700 },
5701 mysql_options: None,
5702 };
5703
5704 let sql = op.to_sql(&dialect);
5706
5707 assert!(
5709 sql.contains("ALTER COLUMN is_active DROP DEFAULT"),
5710 "AlterColumn must remove dropped database defaults, got: {}",
5711 sql
5712 );
5713 }
5714
5715 #[test]
5716 fn test_to_sql_alter_column_mysql_preserves_full_column_definition() {
5717 let op = Operation::AlterColumn {
5719 table: "users".to_string(),
5720 column: "is_active".to_string(),
5721 old_definition: None,
5722 new_definition: ColumnDefinition {
5723 name: "is_active".to_string(),
5724 type_definition: FieldType::Boolean,
5725 not_null: true,
5726 unique: false,
5727 primary_key: false,
5728 auto_increment: false,
5729 default: Some("true".to_string()),
5730 },
5731 mysql_options: None,
5732 };
5733
5734 let sql = op.to_sql(&SqlDialect::Mysql);
5736
5737 assert!(
5739 sql.contains("MODIFY COLUMN is_active TINYINT(1) NOT NULL DEFAULT true"),
5740 "MySQL AlterColumn must include type, nullability, and default, got: {}",
5741 sql
5742 );
5743 }
5744
5745 #[test]
5772 fn test_to_reverse_sql_alter_column_cockroachdb() {
5773 let op = alter_column_with_old_def();
5775 let state = ProjectState::default();
5776
5777 let stmts = op
5779 .to_reverse_sql(&SqlDialect::Cockroachdb, &state)
5780 .expect("reverse SQL should succeed")
5781 .expect("reverse SQL should be present");
5782
5783 assert_eq!(
5791 stmts,
5792 vec![
5793 "ALTER TABLE products ALTER COLUMN name TYPE VARCHAR(50);".to_string(),
5794 "ALTER TABLE products ALTER COLUMN name DROP NOT NULL;".to_string(),
5795 ],
5796 "CockroachDB reverse SQL must emit exactly [type_stmt, nullability_stmt], \
5797 got: {:?}",
5798 stmts
5799 );
5800
5801 for stmt in &stmts {
5808 let trimmed = stmt.trim().trim_end_matches(';').trim();
5809 assert!(
5810 !trimmed.contains(';'),
5811 "each emitted statement must be a single SQL statement, got: {}",
5812 stmt
5813 );
5814 assert!(
5815 !stmt.contains(", ALTER COLUMN"),
5816 "emitted statements must not use the Postgres comma-combined form \
5817 (CockroachDB rejects it), got: {}",
5818 stmt
5819 );
5820 }
5821 }
5822
5823 #[test]
5829 fn test_to_reverse_sql_alter_column_sqlite() {
5830 let op = alter_column_with_old_def();
5832 let state = ProjectState::default();
5833
5834 let stmts = op
5838 .to_reverse_sql(&SqlDialect::Sqlite, &state)
5839 .expect("reverse SQL should succeed")
5840 .expect("reverse SQL should be present");
5841 assert_eq!(
5842 stmts.len(),
5843 1,
5844 "SQLite AlterColumn reverse SQL should remain a single comment, got: {:?}",
5845 stmts
5846 );
5847 let sql = &stmts[0];
5848
5849 assert!(
5851 sql.trim_start().starts_with("--"),
5852 "SQLite reverse SQL should be a SQL comment (recreation handled by executor), got: {}",
5853 sql
5854 );
5855 let body = sql.trim_start_matches("--").trim_start();
5858 assert!(
5859 !body.to_uppercase().contains("ALTER TABLE"),
5860 "SQLite reverse SQL body must not emit executable ALTER TABLE statement, got: {}",
5861 sql
5862 );
5863 }
5864
5865 #[test]
5870 fn test_to_reverse_operation_alter_column_uses_old_definition() {
5871 let op = alter_column_with_old_def();
5873 let state = ProjectState::default();
5874
5875 let reverse = op
5877 .to_reverse_operation(&state)
5878 .expect("reverse operation should succeed")
5879 .expect("reverse operation should be present (old_definition is supplied)");
5880
5881 match reverse {
5883 Operation::AlterColumn { new_definition, .. } => {
5884 assert!(
5885 matches!(new_definition.type_definition, FieldType::VarChar(50)),
5886 "reverse AlterColumn should restore VARCHAR(50), got: {:?}",
5887 new_definition.type_definition
5888 );
5889 }
5890 other => panic!("reverse operation should be AlterColumn, got: {:?}", other),
5891 }
5892 }
5893
5894 #[test]
5895 fn test_to_reverse_sql_run_sql_with_reverse() {
5896 let op = Operation::RunSQL {
5897 sql: "CREATE INDEX idx_name ON users(name)".to_string(),
5898 reverse_sql: Some("DROP INDEX idx_name".to_string()),
5899 };
5900
5901 let state = ProjectState::default();
5902 let reverse = op.to_reverse_sql(&SqlDialect::Postgres, &state);
5903 assert!(
5904 reverse.is_ok() && reverse.as_ref().ok().unwrap().is_some(),
5905 "RunSQL with reverse_sql should have reverse SQL"
5906 );
5907 let sql = reverse.unwrap().unwrap().join("\n");
5908 assert!(
5909 sql.contains("DROP INDEX"),
5910 "Reverse SQL should contain provided reverse_sql, got: {}",
5911 sql
5912 );
5913 }
5914
5915 #[test]
5916 fn test_to_reverse_sql_run_sql_without_reverse() {
5917 let op = Operation::RunSQL {
5918 sql: "CREATE INDEX idx_name ON users(name)".to_string(),
5919 reverse_sql: None,
5920 };
5921
5922 let state = ProjectState::default();
5923 let reverse = op.to_reverse_sql(&SqlDialect::Postgres, &state);
5924 assert!(
5925 reverse.is_ok() && reverse.as_ref().ok().unwrap().is_none(),
5926 "RunSQL without reverse_sql should not have reverse SQL"
5927 );
5928 }
5929
5930 #[test]
5931 fn test_column_definition_new() {
5932 let col = ColumnDefinition::new("id", FieldType::Integer);
5933 assert_eq!(col.name, "id", "Column name should be 'id'");
5934 assert_eq!(
5935 col.type_definition,
5936 FieldType::Integer,
5937 "Column type should be Integer"
5938 );
5939 assert!(!col.not_null, "not_null should default to false");
5940 assert!(!col.unique, "unique should default to false");
5941 assert!(!col.primary_key, "primary_key should default to false");
5942 assert!(
5943 !col.auto_increment,
5944 "auto_increment should default to false"
5945 );
5946 assert!(col.default.is_none(), "default should be None");
5947 }
5948
5949 #[rstest]
5959 fn from_field_state_non_optional_bool_with_true_default() {
5960 let mut field_state = FieldState::new("is_active", FieldType::Boolean, false);
5966 field_state
5967 .params
5968 .insert("default".to_string(), "true".to_string());
5969
5970 let col = ColumnDefinition::from_field_state("is_active", &field_state);
5972
5973 assert_eq!(col.name, "is_active", "Column name should round-trip");
5975 assert_eq!(
5976 col.type_definition,
5977 FieldType::Boolean,
5978 "Boolean field type should round-trip"
5979 );
5980 assert!(
5981 col.not_null,
5982 "Non-Optional bool must emit NOT NULL (regression #4573)"
5983 );
5984 assert_eq!(
5985 col.default,
5986 Some("true".to_string()),
5987 "`#[field(default = true)]` must propagate as Some(\"true\")"
5988 );
5989 assert!(!col.primary_key, "Non-PK field must not be primary_key");
5990 }
5991
5992 #[rstest]
5993 fn from_field_state_non_optional_bool_with_false_default() {
5994 let mut field_state = FieldState::new("is_superuser", FieldType::Boolean, false);
5999 field_state
6000 .params
6001 .insert("default".to_string(), "false".to_string());
6002
6003 let col = ColumnDefinition::from_field_state("is_superuser", &field_state);
6005
6006 assert!(
6008 col.not_null,
6009 "Non-Optional bool with default=false must emit NOT NULL"
6010 );
6011 assert_eq!(
6012 col.default,
6013 Some("false".to_string()),
6014 "default=false must propagate as Some(\"false\")"
6015 );
6016 }
6017
6018 #[rstest]
6019 fn from_field_state_optional_bool_with_default() {
6020 let mut field_state = FieldState::new("maybe_flag", FieldType::Boolean, true);
6025 field_state
6026 .params
6027 .insert("default".to_string(), "true".to_string());
6028
6029 let col = ColumnDefinition::from_field_state("maybe_flag", &field_state);
6031
6032 assert!(
6034 !col.not_null,
6035 "Optional bool must remain NULLABLE — no regression on Option<T>"
6036 );
6037 assert_eq!(
6038 col.default,
6039 Some("true".to_string()),
6040 "Default propagation must work for Optional fields too"
6041 );
6042 }
6043
6044 #[rstest]
6045 fn from_field_state_non_optional_non_bool() {
6046 let field_state = FieldState::new("username", FieldType::VarChar(150), false);
6051
6052 let col = ColumnDefinition::from_field_state("username", &field_state);
6054
6055 assert!(
6057 col.not_null,
6058 "Non-Optional String must emit NOT NULL (regression #4573 — bug \
6059 affected all field types, not just bool)"
6060 );
6061 assert!(
6062 col.default.is_none(),
6063 "No default annotation → default = None"
6064 );
6065 }
6066
6067 #[rstest]
6068 fn from_field_state_primary_key_is_always_not_null() {
6069 let mut field_state = FieldState::new("id", FieldType::Uuid, true);
6074 field_state
6075 .params
6076 .insert("primary_key".to_string(), "true".to_string());
6077
6078 let col = ColumnDefinition::from_field_state("id", &field_state);
6080
6081 assert!(
6083 col.primary_key,
6084 "primary_key param must propagate to ColumnDefinition"
6085 );
6086 assert!(
6087 col.not_null,
6088 "Primary key must be NOT NULL regardless of nullable flag"
6089 );
6090 }
6091
6092 #[rstest]
6093 fn from_field_state_optional_field_remains_nullable() {
6094 let field_state = FieldState::new("last_login", FieldType::TimestampTz, true);
6098
6099 let col = ColumnDefinition::from_field_state("last_login", &field_state);
6101
6102 assert!(
6104 !col.not_null,
6105 "Optional field with no default must remain NULLABLE"
6106 );
6107 assert!(col.default.is_none(), "No default → default = None");
6108 assert!(!col.primary_key, "Non-PK field must not be primary_key");
6109 }
6110
6111 #[test]
6112 fn test_convert_default_value_null() {
6113 let op = Operation::CreateTable {
6114 name: "test".to_string(),
6115 columns: vec![],
6116 constraints: vec![],
6117 without_rowid: None,
6118 partition: None,
6119 interleave_in_parent: None,
6120 };
6121 let value = op.convert_default_value("null");
6122 assert!(
6123 matches!(value, Value::String(None)),
6124 "NULL value should be converted to Value::String(None)"
6125 );
6126 }
6127
6128 #[test]
6129 fn test_convert_default_value_bool() {
6130 let op = Operation::CreateTable {
6131 name: "test".to_string(),
6132 columns: vec![],
6133 constraints: vec![],
6134 without_rowid: None,
6135 partition: None,
6136 interleave_in_parent: None,
6137 };
6138 let value = op.convert_default_value("true");
6139 assert!(
6140 matches!(value, Value::Bool(Some(true))),
6141 "'true' should be converted to Value::Bool(Some(true))"
6142 );
6143
6144 let value = op.convert_default_value("false");
6145 assert!(
6146 matches!(value, Value::Bool(Some(false))),
6147 "'false' should be converted to Value::Bool(Some(false))"
6148 );
6149 }
6150
6151 #[test]
6152 fn test_convert_default_value_integer() {
6153 let op = Operation::CreateTable {
6154 name: "test".to_string(),
6155 columns: vec![],
6156 constraints: vec![],
6157 without_rowid: None,
6158 partition: None,
6159 interleave_in_parent: None,
6160 };
6161 let value = op.convert_default_value("42");
6162 assert!(
6163 matches!(value, Value::BigInt(Some(42))),
6164 "Integer '42' should be converted to Value::BigInt(Some(42))"
6165 );
6166 }
6167
6168 #[test]
6169 fn test_convert_default_value_float() {
6170 let op = Operation::CreateTable {
6171 name: "test".to_string(),
6172 columns: vec![],
6173 constraints: vec![],
6174 without_rowid: None,
6175 partition: None,
6176 interleave_in_parent: None,
6177 };
6178 let value = op.convert_default_value("3.15");
6179 assert!(
6180 matches!(value, Value::Double(_)),
6181 "Float '3.15' should be converted to Value::Double"
6182 );
6183 }
6184
6185 #[test]
6186 fn test_convert_default_value_string() {
6187 let op = Operation::CreateTable {
6188 name: "test".to_string(),
6189 columns: vec![],
6190 constraints: vec![],
6191 without_rowid: None,
6192 partition: None,
6193 interleave_in_parent: None,
6194 };
6195 let value = op.convert_default_value("'hello'");
6196 match value {
6197 Value::String(Some(s)) => assert_eq!(
6198 *s, "hello",
6199 "Quoted string should be unquoted and stored as 'hello'"
6200 ),
6201 _ => {
6202 panic!("Expected Value::String(Some(\"hello\")), got different variant")
6203 }
6204 }
6205 }
6206
6207 #[rstest]
6208 #[case("pending", "'pending'")]
6209 #[case("active", "'active'")]
6210 #[case("hello world", "'hello world'")]
6211 #[case("it's", "'it''s'")]
6212 fn test_convert_default_value_plain_string(#[case] input: &str, #[case] expected: &str) {
6213 let op = Operation::CreateTable {
6215 name: "test".to_string(),
6216 columns: vec![],
6217 constraints: vec![],
6218 without_rowid: None,
6219 partition: None,
6220 interleave_in_parent: None,
6221 };
6222
6223 let value = op.convert_default_value(input);
6225
6226 match value {
6228 Value::String(Some(s)) => assert_eq!(
6229 *s, expected,
6230 "Plain string '{input}' should be auto-quoted as SQL string literal"
6231 ),
6232 _ => {
6233 panic!("Expected Value::String(Some(\"{expected}\")), got {value:?}")
6234 }
6235 }
6236 }
6237
6238 #[rstest]
6239 #[case("CURRENT_TIMESTAMP")]
6240 #[case("current_timestamp")]
6241 #[case("CURRENT_DATE")]
6242 #[case("CURRENT_TIME")]
6243 #[case("CURRENT_USER")]
6244 #[case("SESSION_USER")]
6245 #[case("LOCALTIME")]
6246 #[case("LOCALTIMESTAMP")]
6247 fn test_convert_default_value_sql_constant(#[case] input: &str) {
6248 let op = Operation::CreateTable {
6250 name: "test".to_string(),
6251 columns: vec![],
6252 constraints: vec![],
6253 without_rowid: None,
6254 partition: None,
6255 interleave_in_parent: None,
6256 };
6257
6258 let value = op.convert_default_value(input);
6260
6261 match value {
6263 Value::String(Some(s)) => {
6264 assert_eq!(*s, input, "SQL constant '{input}' should remain unquoted")
6265 }
6266 _ => {
6267 panic!("Expected Value::String(Some(\"{input}\")), got {value:?}")
6268 }
6269 }
6270 }
6271
6272 #[rstest]
6273 #[case("NOW()")]
6274 #[case("uuid_generate_v4()")]
6275 #[case("gen_random_uuid()")]
6276 fn test_convert_default_value_sql_function(#[case] input: &str) {
6277 let op = Operation::CreateTable {
6279 name: "test".to_string(),
6280 columns: vec![],
6281 constraints: vec![],
6282 without_rowid: None,
6283 partition: None,
6284 interleave_in_parent: None,
6285 };
6286
6287 let value = op.convert_default_value(input);
6289
6290 match value {
6292 Value::String(Some(s)) => {
6293 assert_eq!(*s, input, "SQL function '{input}' should remain unquoted")
6294 }
6295 _ => {
6296 panic!("Expected Value::String(Some(\"{input}\")), got {value:?}")
6297 }
6298 }
6299 }
6300
6301 #[test]
6302 fn test_apply_column_type_integer() {
6303 let op = Operation::CreateTable {
6304 name: "test".to_string(),
6305 columns: vec![],
6306 constraints: vec![],
6307 without_rowid: None,
6308 partition: None,
6309 interleave_in_parent: None,
6310 };
6311 let col = ColumnDef::new(Alias::new("id"));
6312 let _col = op.apply_column_type(col, &FieldType::Integer);
6313 }
6316
6317 #[test]
6318 fn test_apply_column_type_varchar_with_length() {
6319 let op = Operation::CreateTable {
6320 name: "test".to_string(),
6321 columns: vec![],
6322 constraints: vec![],
6323 without_rowid: None,
6324 partition: None,
6325 interleave_in_parent: None,
6326 };
6327 let col = ColumnDef::new(Alias::new("name"));
6328 let _col = op.apply_column_type(col, &FieldType::VarChar(100));
6329 }
6332
6333 #[test]
6334 fn test_apply_column_type_custom() {
6335 let op = Operation::CreateTable {
6336 name: "test".to_string(),
6337 columns: vec![],
6338 constraints: vec![],
6339 without_rowid: None,
6340 partition: None,
6341 interleave_in_parent: None,
6342 };
6343 let col = ColumnDef::new(Alias::new("data"));
6344 let _col = op.apply_column_type(col, &FieldType::Custom("CUSTOM_TYPE".to_string()));
6345 }
6348
6349 #[test]
6350 fn test_create_index_composite() {
6351 let op = Operation::CreateIndex {
6352 table: "users".to_string(),
6353 columns: vec!["first_name".to_string(), "last_name".to_string()],
6354 unique: false,
6355 index_type: None,
6356 where_clause: None,
6357 concurrently: false,
6358 expressions: None,
6359 mysql_options: None,
6360 operator_class: None,
6361 };
6362
6363 let sql = op.to_sql(&SqlDialect::Postgres);
6364 assert!(
6365 sql.contains("first_name"),
6366 "SQL should include 'first_name' column, got: {}",
6367 sql
6368 );
6369 assert!(
6370 sql.contains("last_name"),
6371 "SQL should include 'last_name' column, got: {}",
6372 sql
6373 );
6374 assert!(
6375 sql.contains("idx_users_first_name_last_name"),
6376 "SQL should include composite index name, got: {}",
6377 sql
6378 );
6379 }
6380
6381 #[test]
6382 fn test_alter_table_comment_with_quotes() {
6383 let op = Operation::AlterTableComment {
6384 table: "users".to_string(),
6385 comment: Some("User's account table".to_string()),
6386 };
6387
6388 let stmt = op.to_statement();
6389 let sql = stmt.to_sql_string(crate::backends::types::DatabaseType::Postgres);
6390 assert!(
6391 sql.contains("COMMENT ON TABLE"),
6392 "SQL should contain COMMENT ON TABLE keywords, got: {}",
6393 sql
6394 );
6395 assert!(
6396 sql.contains("User''s account table"),
6397 "SQL should properly escape single quotes in comment, got: {}",
6398 sql
6399 );
6400 }
6401
6402 #[test]
6403 fn test_state_forwards_alter_column() {
6404 let mut state = ProjectState::new();
6405 let mut model = ModelState::new("myapp", "users");
6406 model.add_field(FieldState::new(
6407 "age".to_string(),
6408 FieldType::Integer,
6409 false,
6410 ));
6411 state.add_model(model);
6412
6413 let op = Operation::AlterColumn {
6414 table: "users".to_string(),
6415 column: "age".to_string(),
6416 old_definition: None,
6417 new_definition: ColumnDefinition {
6418 name: "age".to_string(),
6419 type_definition: FieldType::BigInteger,
6420 not_null: true,
6421 unique: false,
6422 primary_key: false,
6423 auto_increment: false,
6424 default: None,
6425 },
6426 mysql_options: None,
6427 };
6428
6429 op.state_forwards("myapp", &mut state);
6430 let model = state.get_model("myapp", "users").unwrap();
6431 let field = model.fields.get("age").unwrap();
6432 assert_eq!(
6433 field.field_type,
6434 FieldType::BigInteger,
6435 "Field type should be updated to BigInteger, got: {}",
6436 field.field_type
6437 );
6438 }
6439
6440 #[test]
6441 fn test_state_forwards_create_inherited_table() {
6442 let mut state = ProjectState::new();
6443 let op = Operation::CreateInheritedTable {
6444 name: "admin_users".to_string(),
6445 columns: vec![ColumnDefinition {
6446 name: "admin_level".to_string(),
6447 type_definition: FieldType::Integer,
6448 not_null: true,
6449 unique: false,
6450 primary_key: false,
6451 auto_increment: false,
6452 default: None,
6453 }],
6454 base_table: "users".to_string(),
6455 join_column: "user_id".to_string(),
6456 };
6457
6458 op.state_forwards("myapp", &mut state);
6459 let model = state.get_model("myapp", "admin_users");
6460 assert!(
6461 model.is_some(),
6462 "Inherited table 'admin_users' should exist in state"
6463 );
6464 let model = model.unwrap();
6465 assert_eq!(
6466 model.base_model,
6467 Some("users".to_string()),
6468 "base_model should be set to 'users'"
6469 );
6470 assert_eq!(
6471 model.inheritance_type,
6472 Some("joined_table".to_string()),
6473 "inheritance_type should be 'joined_table'"
6474 );
6475 }
6476
6477 #[test]
6478 fn test_state_forwards_add_discriminator_column() {
6479 let mut state = ProjectState::new();
6480 let mut model = ModelState::new("myapp", "users");
6481 model.add_field(FieldState::new("id".to_string(), FieldType::Integer, false));
6482 state.add_model(model);
6483
6484 let op = Operation::AddDiscriminatorColumn {
6485 table: "users".to_string(),
6486 column_name: "user_type".to_string(),
6487 default_value: "regular".to_string(),
6488 };
6489
6490 op.state_forwards("myapp", &mut state);
6491 let model = state.get_model("myapp", "users").unwrap();
6492 assert_eq!(
6493 model.discriminator_column,
6494 Some("user_type".to_string()),
6495 "discriminator_column should be set to 'user_type'"
6496 );
6497 assert_eq!(
6498 model.inheritance_type,
6499 Some("single_table".to_string()),
6500 "inheritance_type should be 'single_table'"
6501 );
6502 }
6503
6504 #[rstest]
6505 fn test_to_reverse_sql_create_table_quotes_identifiers() {
6506 let op = Operation::CreateTable {
6508 name: "user-data".to_string(),
6509 columns: vec![],
6510 constraints: vec![],
6511 without_rowid: None,
6512 partition: None,
6513 interleave_in_parent: None,
6514 };
6515 let state = ProjectState::default();
6516
6517 let sql = op
6519 .to_reverse_sql(&SqlDialect::Postgres, &state)
6520 .unwrap()
6521 .unwrap()
6522 .join("\n");
6523
6524 assert_eq!(
6526 sql, "DROP TABLE \"user-data\";",
6527 "Identifiers with special characters must be quoted"
6528 );
6529 }
6530
6531 #[rstest]
6532 fn test_to_reverse_sql_add_column_quotes_identifiers() {
6533 let op = Operation::AddColumn {
6535 table: "my table".to_string(),
6536 column: ColumnDefinition {
6537 name: "my column".to_string(),
6538 type_definition: FieldType::VarChar(255),
6539 not_null: false,
6540 unique: false,
6541 primary_key: false,
6542 auto_increment: false,
6543 default: None,
6544 },
6545 mysql_options: None,
6546 };
6547 let state = ProjectState::default();
6548
6549 let sql = op
6551 .to_reverse_sql(&SqlDialect::Postgres, &state)
6552 .unwrap()
6553 .unwrap()
6554 .join("\n");
6555
6556 assert_eq!(
6558 sql, "ALTER TABLE \"my table\" DROP COLUMN \"my column\";",
6559 "Table and column names with spaces must be quoted"
6560 );
6561 }
6562
6563 #[rstest]
6564 fn test_to_reverse_sql_rename_table_quotes_identifiers() {
6565 let op = Operation::RenameTable {
6567 old_name: "old; DROP TABLE users;--".to_string(),
6568 new_name: "new-name".to_string(),
6569 };
6570 let state = ProjectState::default();
6571
6572 let sql = op
6574 .to_reverse_sql(&SqlDialect::Postgres, &state)
6575 .unwrap()
6576 .unwrap()
6577 .join("\n");
6578
6579 assert_eq!(
6581 sql, "ALTER TABLE \"new-name\" RENAME TO \"old; DROP TABLE users;--\";",
6582 "SQL injection attempt must be quoted as identifier"
6583 );
6584 }
6585
6586 #[rstest]
6587 fn test_to_reverse_sql_rename_column_quotes_identifiers() {
6588 let op = Operation::RenameColumn {
6590 table: "my table".to_string(),
6591 old_name: "old col".to_string(),
6592 new_name: "new col".to_string(),
6593 };
6594 let state = ProjectState::default();
6595
6596 let sql = op
6598 .to_reverse_sql(&SqlDialect::Postgres, &state)
6599 .unwrap()
6600 .unwrap()
6601 .join("\n");
6602
6603 assert_eq!(
6605 sql, "ALTER TABLE \"my table\" RENAME COLUMN \"new col\" TO \"old col\";",
6606 "Identifiers with spaces must be quoted"
6607 );
6608 }
6609
6610 #[rstest]
6611 fn test_to_reverse_sql_create_index_quotes_identifiers() {
6612 let op = Operation::CreateIndex {
6614 table: "my-table".to_string(),
6615 columns: vec!["col a".to_string()],
6616 unique: false,
6617 index_type: None,
6618 where_clause: None,
6619 concurrently: false,
6620 expressions: None,
6621 mysql_options: None,
6622 operator_class: None,
6623 };
6624 let state = ProjectState::default();
6625
6626 let sql = op
6628 .to_reverse_sql(&SqlDialect::Postgres, &state)
6629 .unwrap()
6630 .unwrap()
6631 .join("\n");
6632
6633 assert!(
6635 sql.contains("DROP INDEX \"idx_my-table_col a\""),
6636 "Index name must be quoted, got: {}",
6637 sql
6638 );
6639 }
6640
6641 #[rstest]
6648 fn test_to_reverse_sql_create_index_emits_on_table_clause_for_mysql() {
6649 let op = Operation::CreateIndex {
6651 table: "users".to_string(),
6652 columns: vec!["email".to_string()],
6653 unique: false,
6654 index_type: None,
6655 where_clause: None,
6656 concurrently: false,
6657 expressions: None,
6658 mysql_options: None,
6659 operator_class: None,
6660 };
6661 let state = ProjectState::default();
6662
6663 let sql = op
6665 .to_reverse_sql(&SqlDialect::Mysql, &state)
6666 .unwrap()
6667 .unwrap()
6668 .join("\n");
6669
6670 assert_eq!(
6675 sql, "DROP INDEX idx_users_email ON users;",
6676 "MySQL reverse SQL must include `ON <table>` clause"
6677 );
6678 }
6679
6680 #[rstest]
6683 #[case(SqlDialect::Postgres, "DROP INDEX idx_users_email;")]
6684 #[case(SqlDialect::Sqlite, "DROP INDEX idx_users_email;")]
6685 #[case(SqlDialect::Cockroachdb, "DROP INDEX idx_users_email;")]
6686 fn test_to_reverse_sql_create_index_omits_on_table_for_non_mysql(
6687 #[case] dialect: SqlDialect,
6688 #[case] expected: &str,
6689 ) {
6690 let op = Operation::CreateIndex {
6692 table: "users".to_string(),
6693 columns: vec!["email".to_string()],
6694 unique: false,
6695 index_type: None,
6696 where_clause: None,
6697 concurrently: false,
6698 expressions: None,
6699 mysql_options: None,
6700 operator_class: None,
6701 };
6702 let state = ProjectState::default();
6703
6704 let sql = op
6706 .to_reverse_sql(&dialect, &state)
6707 .unwrap()
6708 .unwrap()
6709 .join("\n");
6710
6711 assert_eq!(
6713 sql, expected,
6714 "Non-MySQL reverse SQL must remain unchanged for dialect {:?}",
6715 dialect
6716 );
6717 }
6718
6719 #[rstest]
6720 fn test_to_reverse_sql_add_constraint_quotes_identifiers() {
6721 let op = Operation::AddConstraint {
6723 table: "my-table".to_string(),
6724 constraint_sql: "CONSTRAINT chk_positive CHECK (x > 0)".to_string(),
6725 };
6726 let state = ProjectState::default();
6727
6728 let sql = op
6730 .to_reverse_sql(&SqlDialect::Postgres, &state)
6731 .unwrap()
6732 .unwrap()
6733 .join("\n");
6734
6735 assert!(
6737 sql.contains("ALTER TABLE \"my-table\""),
6738 "Table name with special characters must be quoted, got: {}",
6739 sql
6740 );
6741 assert!(
6742 sql.contains("DROP CONSTRAINT"),
6743 "Should contain DROP CONSTRAINT, got: {}",
6744 sql
6745 );
6746 }
6747
6748 #[rstest]
6749 fn test_to_reverse_sql_bulk_load_quotes_identifiers() {
6750 let op = Operation::BulkLoad {
6752 table: "user-data".to_string(),
6753 source: BulkLoadSource::Stdin,
6754 format: BulkLoadFormat::default(),
6755 options: BulkLoadOptions::default(),
6756 };
6757 let state = ProjectState::default();
6758
6759 let sql = op
6761 .to_reverse_sql(&SqlDialect::Postgres, &state)
6762 .unwrap()
6763 .unwrap()
6764 .join("\n");
6765
6766 assert_eq!(
6768 sql, "TRUNCATE TABLE \"user-data\";",
6769 "Table name must be quoted"
6770 );
6771 }
6772
6773 #[rstest]
6778 #[case::postgres(SqlDialect::Postgres)]
6779 #[case::cockroachdb(SqlDialect::Cockroachdb)]
6780 fn test_set_auto_increment_postgres_uses_setval(#[case] dialect: SqlDialect) {
6781 let op = Operation::SetAutoIncrementValue {
6783 table: "users".to_string(),
6784 column: "id".to_string(),
6785 value: 1000,
6786 };
6787
6788 let sql = op.to_sql(&dialect);
6790
6791 assert_eq!(
6793 sql,
6794 "SELECT setval(pg_get_serial_sequence('users', 'id'), 1000, false);"
6795 );
6796 }
6797
6798 #[test]
6799 fn test_set_auto_increment_mysql_alters_table() {
6800 let op = Operation::SetAutoIncrementValue {
6802 table: "users".to_string(),
6803 column: "id".to_string(),
6804 value: 1000,
6805 };
6806
6807 let sql = op.to_sql(&SqlDialect::Mysql);
6809
6810 assert_eq!(sql, "ALTER TABLE users AUTO_INCREMENT = 1000;");
6815 }
6816
6817 #[test]
6818 fn test_set_auto_increment_sqlite_upserts_sqlite_sequence() {
6819 let op = Operation::SetAutoIncrementValue {
6821 table: "users".to_string(),
6822 column: "id".to_string(),
6823 value: 1000,
6824 };
6825
6826 let sql = op.to_sql(&SqlDialect::Sqlite);
6828
6829 assert_eq!(
6832 sql,
6833 "INSERT OR REPLACE INTO sqlite_sequence(name, seq) VALUES ('users', 1000);"
6834 );
6835 }
6836
6837 #[test]
6838 fn test_set_auto_increment_postgres_escapes_literals() {
6839 let op = Operation::SetAutoIncrementValue {
6841 table: "user's".to_string(),
6842 column: "id".to_string(),
6843 value: 42,
6844 };
6845
6846 let sql = op.to_sql(&SqlDialect::Postgres);
6848
6849 assert!(
6851 sql.contains("'user''s'"),
6852 "single quote in table name must be escaped: {}",
6853 sql
6854 );
6855 }
6856
6857 #[rstest]
6862 #[case::postgres(SqlDialect::Postgres)]
6863 #[case::mysql(SqlDialect::Mysql)]
6864 #[case::sqlite(SqlDialect::Sqlite)]
6865 #[case::cockroachdb(SqlDialect::Cockroachdb)]
6866 fn test_composite_pk_default_name(#[case] dialect: SqlDialect) {
6867 let op = Operation::CreateCompositePrimaryKey {
6869 table: "order_items".to_string(),
6870 columns: vec!["order_id".to_string(), "line_number".to_string()],
6871 constraint_name: None,
6872 };
6873
6874 let sql = op.to_sql(&dialect);
6876
6877 assert!(
6879 sql.contains("ALTER TABLE"),
6880 "SQL should use ALTER TABLE: {}",
6881 sql
6882 );
6883 assert!(
6884 sql.contains("ADD CONSTRAINT"),
6885 "SQL should add a named constraint: {}",
6886 sql
6887 );
6888 assert!(
6889 sql.contains("PRIMARY KEY"),
6890 "SQL should add PRIMARY KEY: {}",
6891 sql
6892 );
6893 assert!(
6894 sql.contains("order_items_pkey"),
6895 "Default constraint name should be table_pkey: {}",
6896 sql
6897 );
6898 assert!(
6899 sql.contains("order_id") && sql.contains("line_number"),
6900 "Both PK columns must appear: {}",
6901 sql
6902 );
6903 }
6904
6905 #[test]
6906 fn test_composite_pk_custom_name_and_quoting() {
6907 let op = Operation::CreateCompositePrimaryKey {
6909 table: "tbl".to_string(),
6910 columns: vec!["a".to_string(), "b".to_string()],
6911 constraint_name: Some("my_pk".to_string()),
6912 };
6913
6914 let sql = op.to_sql(&SqlDialect::Postgres);
6916
6917 assert_eq!(
6919 sql,
6920 "ALTER TABLE tbl ADD CONSTRAINT my_pk PRIMARY KEY (a, b);"
6921 );
6922 }
6923
6924 #[test]
6925 fn test_composite_pk_empty_columns_produces_failing_sql() {
6926 let op = Operation::CreateCompositePrimaryKey {
6932 table: "tbl".to_string(),
6933 columns: vec![],
6934 constraint_name: None,
6935 };
6936
6937 for dialect in [SqlDialect::Postgres, SqlDialect::Mysql, SqlDialect::Sqlite] {
6939 let sql = op.to_sql(&dialect);
6940
6941 assert!(
6944 sql.starts_with("SYNTAX_ERROR_create_composite_pk_on_")
6945 && sql.contains("requires_at_least_one_column"),
6946 "Empty column list must emit a syntax-error statement with diagnostic ({:?}): {}",
6947 dialect,
6948 sql
6949 );
6950 assert!(
6951 !sql.contains("SELECT 1/0"),
6952 "Must not fall back to SELECT 1/0 (silently passes on SQLite / lax MySQL): {}",
6953 sql
6954 );
6955 }
6956 }
6957
6958 #[rstest]
6969 #[case::big_integer(FieldType::BigInteger)]
6970 #[case::integer(FieldType::Integer)]
6971 #[case::small_integer(FieldType::SmallInteger)]
6972 fn test_column_to_sql_sqlite_auto_increment_pk_emits_integer(#[case] field_type: FieldType) {
6973 let mut col = ColumnDefinition::new("id", field_type);
6975 col.primary_key = true;
6976 col.auto_increment = true;
6977 col.not_null = true;
6978
6979 let sql = Operation::column_to_sql(&col, &SqlDialect::Sqlite);
6981
6982 assert!(
6985 sql.contains("INTEGER PRIMARY KEY AUTOINCREMENT"),
6986 "SQLite auto_increment PK must emit `INTEGER PRIMARY KEY AUTOINCREMENT`: {}",
6987 sql
6988 );
6989 assert!(
6990 !sql.contains("BIGINT"),
6991 "SQLite auto_increment must not emit BIGINT (rejected by SQLite): {}",
6992 sql
6993 );
6994 assert!(
6995 !sql.contains("SMALLINT"),
6996 "SQLite auto_increment must not emit SMALLINT (rejected by SQLite): {}",
6997 sql
6998 );
6999 }
7000
7001 #[test]
7002 fn test_column_to_sql_sqlite_big_integer_without_auto_increment_no_autoincrement() {
7003 let mut col = ColumnDefinition::new("count", FieldType::BigInteger);
7008 col.not_null = true;
7009
7010 let sql = Operation::column_to_sql(&col, &SqlDialect::Sqlite);
7012
7013 assert!(
7015 !sql.contains("AUTOINCREMENT"),
7016 "Non-auto_increment column must not emit AUTOINCREMENT: {}",
7017 sql
7018 );
7019 assert!(
7024 !sql.contains("BIGINT"),
7025 "emitter is expected to normalize BigInteger to INTEGER for SQLite: {}",
7026 sql
7027 );
7028 }
7029
7030 #[test]
7031 fn test_column_to_sql_postgres_big_integer_auto_increment_unchanged() {
7032 let mut col = ColumnDefinition::new("id", FieldType::BigInteger);
7034 col.primary_key = true;
7035 col.auto_increment = true;
7036 col.not_null = true;
7037
7038 let sql = Operation::column_to_sql(&col, &SqlDialect::Postgres);
7040
7041 assert!(
7043 sql.contains("BIGINT GENERATED BY DEFAULT AS IDENTITY"),
7044 "Postgres auto_increment BigInteger must emit identity syntax: {}",
7045 sql
7046 );
7047 }
7048
7049 #[test]
7050 fn test_column_to_sql_sqlite_auto_increment_uuid_pk_omits_autoincrement() {
7051 let mut col = ColumnDefinition::new("id", FieldType::Uuid);
7059 col.primary_key = true;
7060 col.auto_increment = true;
7061 col.not_null = true;
7062
7063 let sql = Operation::column_to_sql(&col, &SqlDialect::Sqlite);
7065
7066 assert!(
7068 sql.contains("PRIMARY KEY"),
7069 "UUID PK must still emit PRIMARY KEY: {}",
7070 sql
7071 );
7072 assert!(
7073 !sql.contains("AUTOINCREMENT"),
7074 "non-integer auto_increment PK must not emit AUTOINCREMENT (SQLite rejects it): {}",
7075 sql
7076 );
7077 assert!(
7082 !sql.contains("INTEGER"),
7083 "UUID column type must not be widened to INTEGER: {}",
7084 sql
7085 );
7086 }
7087
7088 #[test]
7089 fn test_column_to_sql_without_pk_sqlite_auto_increment_emits_integer() {
7090 let mut col = ColumnDefinition::new("id", FieldType::BigInteger);
7092 col.auto_increment = true;
7093 col.not_null = true;
7094
7095 let sql = Operation::column_to_sql_without_pk(&col, &SqlDialect::Sqlite);
7097
7098 assert!(
7100 sql.contains("INTEGER"),
7101 "SQLite auto_increment column (composite PK path) must emit INTEGER: {}",
7102 sql
7103 );
7104 assert!(
7105 !sql.contains("BIGINT"),
7106 "SQLite auto_increment must not emit BIGINT in composite PK path: {}",
7107 sql
7108 );
7109 }
7110
7111 mod resolve_foreign_key_column_type_tests {
7112 use super::super::resolve_foreign_key_column_type_with;
7113 use super::FieldType;
7114 use crate::migrations::autodetector::FieldState;
7115 use crate::migrations::model_registry::{FieldMetadata, ModelMetadata, ModelRegistry};
7116
7117 fn target_model(app: &str, name: &str, table: &str, pk_type: FieldType) -> ModelMetadata {
7120 let mut meta = ModelMetadata::new(app, name, table);
7121 meta.add_field(
7122 "id".to_string(),
7123 FieldMetadata::new(pk_type).with_param("primary_key", "true"),
7124 );
7125 meta
7126 }
7127
7128 fn fk_field_state(target_model: &str, target_app: Option<&str>) -> FieldState {
7132 let mut fs = FieldState::new("owner_id", FieldType::Uuid, false);
7133 fs.params
7134 .insert("fk_target".to_string(), target_model.to_string());
7135 if let Some(app) = target_app {
7136 fs.params
7137 .insert("fk_target_app".to_string(), app.to_string());
7138 }
7139 fs
7140 }
7141
7142 #[test]
7143 fn qualified_hit_resolves_to_target_pk_type() {
7144 let registry = ModelRegistry::new();
7146 registry.register_model(target_model(
7147 "auth",
7148 "User",
7149 "auth_user",
7150 FieldType::BigInteger,
7151 ));
7152 let fs = fk_field_state("User", Some("auth"));
7153
7154 let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7156
7157 assert_eq!(resolved, Some(FieldType::BigInteger));
7159 }
7160
7161 #[test]
7162 fn qualified_miss_falls_back_to_by_name_when_unambiguous() {
7163 let registry = ModelRegistry::new();
7166 registry.register_model(target_model(
7167 "reinhardt_auth",
7168 "User",
7169 "auth_user",
7170 FieldType::Uuid,
7171 ));
7172 let fs = fk_field_state("User", Some("blog"));
7174
7175 let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7177
7178 assert_eq!(resolved, Some(FieldType::Uuid));
7181 }
7182
7183 #[test]
7184 fn ambiguous_by_name_returns_none() {
7185 let registry = ModelRegistry::new();
7187 registry.register_model(target_model(
7188 "auth",
7189 "User",
7190 "auth_user",
7191 FieldType::BigInteger,
7192 ));
7193 registry.register_model(target_model(
7194 "billing",
7195 "User",
7196 "billing_user",
7197 FieldType::Uuid,
7198 ));
7199 let fs = fk_field_state("User", None);
7201
7202 let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7204
7205 assert_eq!(resolved, None);
7208 }
7209
7210 #[test]
7211 fn path_typed_disambiguates_ambiguous_name() {
7212 let registry = ModelRegistry::new();
7219 registry.register_model(target_model(
7220 "blog",
7221 "User",
7222 "blog_user",
7223 FieldType::BigInteger,
7224 ));
7225 registry.register_model(target_model(
7226 "reinhardt_auth",
7227 "User",
7228 "reinhardt_auth_user",
7229 FieldType::Uuid,
7230 ));
7231 let fs = fk_field_state("User", Some("reinhardt_auth"));
7232
7233 let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7235
7236 assert_eq!(resolved, Some(FieldType::Uuid));
7239 }
7240
7241 #[test]
7242 fn qualified_miss_with_ambiguous_by_name_returns_none() {
7243 let registry = ModelRegistry::new();
7247 registry.register_model(target_model(
7248 "auth",
7249 "User",
7250 "auth_user",
7251 FieldType::BigInteger,
7252 ));
7253 registry.register_model(target_model(
7254 "billing",
7255 "User",
7256 "billing_user",
7257 FieldType::Uuid,
7258 ));
7259 let fs = fk_field_state("User", Some("blog")); let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7263
7264 assert_eq!(resolved, None);
7266 }
7267
7268 #[test]
7269 fn no_fk_target_param_returns_none() {
7270 let registry = ModelRegistry::new();
7272 registry.register_model(target_model(
7273 "auth",
7274 "User",
7275 "auth_user",
7276 FieldType::BigInteger,
7277 ));
7278 let fs = FieldState::new("name", FieldType::VarChar(64), false);
7279
7280 let resolved = resolve_foreign_key_column_type_with(&fs, ®istry);
7282
7283 assert_eq!(resolved, None);
7285 }
7286 }
7287}