Skip to main content

reinhardt_db/migrations/operations/
models.rs

1//! Model operations for migrations
2//!
3//! This module provides operations for creating, deleting, and renaming models,
4//! inspired by Django's `django/db/migrations/operations/models.py`.
5//!
6//! # Example
7//!
8//! ```rust
9//! use reinhardt_db::migrations::operations::models::{CreateModel, DeleteModel};
10//! use reinhardt_db::migrations::operations::FieldDefinition;
11//! use reinhardt_db::migrations::{ProjectState, FieldType};
12//!
13//! let mut state = ProjectState::new();
14//!
15//! // Create a model
16//! let create = CreateModel::new(
17//!     "User",
18//!     vec![
19//!         FieldDefinition::new("id", FieldType::Integer, true, false, Option::<&str>::None),
20//!         FieldDefinition::new("email", FieldType::VarChar(255), false, false, Option::<&str>::None),
21//!     ],
22//! );
23//! create.state_forwards("myapp", &mut state);
24//! assert!(state.get_model("myapp", "User").is_some());
25//!
26//! // Delete a model
27//! let delete = DeleteModel::new("User");
28//! delete.state_forwards("myapp", &mut state);
29//! assert!(state.get_model("myapp", "User").is_none());
30//! ```
31
32use super::{FieldState, ModelState, ProjectState};
33use crate::backends::schema::BaseDatabaseSchemaEditor;
34use crate::backends::types::DatabaseType;
35use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37use std::fmt;
38
39/// Validation errors that can occur during migration operations
40#[non_exhaustive]
41#[derive(Debug, Clone, PartialEq)]
42pub enum ValidationError {
43	/// Composite primary key list is empty
44	EmptyCompositePrimaryKey {
45		/// The table name.
46		table_name: String,
47	},
48	/// Field specified in composite primary key does not exist in table
49	NonExistentField {
50		/// The missing field name.
51		field_name: String,
52		/// The table name.
53		table_name: String,
54		/// The available field names in the table.
55		available_fields: Vec<String>,
56	},
57}
58
59impl fmt::Display for ValidationError {
60	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61		match self {
62			ValidationError::EmptyCompositePrimaryKey { table_name } => {
63				write!(
64					f,
65					"Composite primary key for table '{}' cannot be empty",
66					table_name
67				)
68			}
69			ValidationError::NonExistentField {
70				field_name,
71				table_name,
72				available_fields,
73			} => {
74				write!(
75					f,
76					"Field '{}' does not exist in table '{}'. Available fields: [{}]",
77					field_name,
78					table_name,
79					available_fields.join(", ")
80				)
81			}
82		}
83	}
84}
85
86impl std::error::Error for ValidationError {}
87
88/// Result type for migration operations
89pub type ValidationResult<T> = Result<T, ValidationError>;
90
91/// Quote an identifier for the given database type
92///
93/// # Arguments
94///
95/// * `identifier` - The identifier to quote (table name, column name, etc.)
96/// * `database_type` - The database type to use for quoting
97///
98/// # Returns
99///
100/// Quoted identifier suitable for the database type:
101/// - PostgreSQL/SQLite: `"identifier"`
102/// - MySQL: `` `identifier` ``
103///
104/// # Example
105///
106/// ```rust
107/// use reinhardt_db::migrations::operations::models::quote_identifier;
108/// use reinhardt_db::backends::types::DatabaseType;
109///
110/// let postgres_quoted = quote_identifier("user", DatabaseType::Postgres);
111/// assert_eq!(postgres_quoted, "\"user\"");
112///
113/// let mysql_quoted = quote_identifier("order", DatabaseType::Mysql);
114/// assert_eq!(mysql_quoted, "`order`");
115/// ```
116pub fn quote_identifier(identifier: &str, database_type: DatabaseType) -> String {
117	match database_type {
118		DatabaseType::Postgres | DatabaseType::Sqlite => {
119			// PostgreSQL and SQLite use double quotes
120			// Escape existing double quotes by doubling them
121			format!("\"{}\"", identifier.replace('"', "\"\""))
122		}
123		DatabaseType::Mysql => {
124			// MySQL uses backticks
125			// Escape existing backticks by doubling them
126			format!("`{}`", identifier.replace('`', "``"))
127		}
128	}
129}
130
131/// Field definition for model operations
132///
133/// # Example
134///
135/// ```rust
136/// use reinhardt_db::migrations::operations::FieldDefinition;
137/// use reinhardt_db::migrations::FieldType;
138///
139/// let field = FieldDefinition::new("email", FieldType::VarChar(255), false, false, Some("''"));
140/// assert_eq!(field.name, "email");
141/// assert_eq!(field.field_type, FieldType::VarChar(255));
142/// assert!(!field.primary_key);
143/// assert!(!field.unique);
144/// assert_eq!(field.default, Some("''".to_string()));
145/// ```
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
147pub struct FieldDefinition {
148	/// The name.
149	pub name: String,
150	/// The field type.
151	pub field_type: crate::migrations::FieldType,
152	/// The primary key.
153	pub primary_key: bool,
154	/// The unique.
155	pub unique: bool,
156	/// The default.
157	pub default: Option<String>,
158	/// The null.
159	pub null: bool,
160
161	// Generated Columns (all DBMS)
162	/// The generated.
163	pub generated: Option<String>,
164	/// The generated stored.
165	pub generated_stored: Option<bool>,
166	/// Whether the generated column is virtual (MySQL/SQLite).
167	#[cfg(any(feature = "db-mysql", feature = "db-sqlite"))]
168	pub generated_virtual: Option<bool>,
169
170	// Identity/Auto-increment
171	/// Whether IDENTITY ALWAYS is set (PostgreSQL).
172	#[cfg(feature = "db-postgres")]
173	pub identity_always: Option<bool>,
174	/// Whether IDENTITY BY DEFAULT is set (PostgreSQL).
175	#[cfg(feature = "db-postgres")]
176	pub identity_by_default: Option<bool>,
177	/// Whether AUTO_INCREMENT is set (MySQL).
178	#[cfg(feature = "db-mysql")]
179	pub auto_increment: Option<bool>,
180	/// Whether AUTOINCREMENT is set (SQLite).
181	#[cfg(feature = "db-sqlite")]
182	pub autoincrement: Option<bool>,
183
184	// Character Set & Collation
185	/// The collation for the column.
186	pub collate: Option<String>,
187	/// The character set (MySQL).
188	#[cfg(feature = "db-mysql")]
189	pub character_set: Option<String>,
190
191	// Comment
192	/// Optional column comment (PostgreSQL/MySQL).
193	#[cfg(any(feature = "db-postgres", feature = "db-mysql"))]
194	pub comment: Option<String>,
195
196	// Storage Optimization (PostgreSQL)
197	/// The storage strategy (PostgreSQL).
198	#[cfg(feature = "db-postgres")]
199	pub storage: Option<String>,
200	/// The compression method (PostgreSQL).
201	#[cfg(feature = "db-postgres")]
202	pub compression: Option<String>,
203
204	// ON UPDATE Trigger (MySQL)
205	/// Whether ON UPDATE CURRENT_TIMESTAMP is set (MySQL).
206	#[cfg(feature = "db-mysql")]
207	pub on_update_current_timestamp: Option<bool>,
208
209	// Invisible Columns (MySQL)
210	/// Whether the column is invisible (MySQL).
211	#[cfg(feature = "db-mysql")]
212	pub invisible: Option<bool>,
213
214	// Full-Text Index (PostgreSQL, MySQL)
215	/// Whether fulltext indexing is enabled (PostgreSQL/MySQL).
216	#[cfg(any(feature = "db-postgres", feature = "db-mysql"))]
217	pub fulltext: Option<bool>,
218
219	// Numeric Attributes (MySQL, deprecated)
220	/// Whether the numeric column is unsigned (MySQL, deprecated).
221	#[cfg(feature = "db-mysql")]
222	pub unsigned: Option<bool>,
223	/// Whether zerofill is set (MySQL, deprecated).
224	#[cfg(feature = "db-mysql")]
225	pub zerofill: Option<bool>,
226}
227
228impl FieldDefinition {
229	/// Create a new field definition
230	///
231	/// # Example
232	///
233	/// ```rust
234	/// use reinhardt_db::migrations::operations::FieldDefinition;
235	/// use reinhardt_db::migrations::FieldType;
236	///
237	/// let field = FieldDefinition::new("id", FieldType::Integer, true, false, Option::<&str>::None);
238	/// assert_eq!(field.name, "id");
239	/// assert!(field.primary_key);
240	/// ```
241	pub fn new(
242		name: impl Into<String>,
243		field_type: crate::migrations::FieldType,
244		primary_key: bool,
245		unique: bool,
246		default: Option<impl Into<String>>,
247	) -> Self {
248		Self {
249			name: name.into(),
250			field_type,
251			primary_key,
252			unique,
253			default: default.map(|d| d.into()),
254			null: false,
255			// Generated Columns
256			generated: None,
257			generated_stored: None,
258			#[cfg(any(feature = "db-mysql", feature = "db-sqlite"))]
259			generated_virtual: None,
260			// Identity/Auto-increment
261			#[cfg(feature = "db-postgres")]
262			identity_always: None,
263			#[cfg(feature = "db-postgres")]
264			identity_by_default: None,
265			#[cfg(feature = "db-mysql")]
266			auto_increment: None,
267			#[cfg(feature = "db-sqlite")]
268			autoincrement: None,
269			// Character Set & Collation
270			collate: None,
271			#[cfg(feature = "db-mysql")]
272			character_set: None,
273			// Comment
274			#[cfg(any(feature = "db-postgres", feature = "db-mysql"))]
275			comment: None,
276			// Storage Optimization
277			#[cfg(feature = "db-postgres")]
278			storage: None,
279			#[cfg(feature = "db-postgres")]
280			compression: None,
281			// ON UPDATE Trigger
282			#[cfg(feature = "db-mysql")]
283			on_update_current_timestamp: None,
284			// Invisible Columns
285			#[cfg(feature = "db-mysql")]
286			invisible: None,
287			// Full-Text Index
288			#[cfg(any(feature = "db-postgres", feature = "db-mysql"))]
289			fulltext: None,
290			// Numeric Attributes (MySQL, deprecated)
291			#[cfg(feature = "db-mysql")]
292			unsigned: None,
293			#[cfg(feature = "db-mysql")]
294			zerofill: None,
295		}
296	}
297
298	/// Set nullable
299	pub fn nullable(mut self, null: bool) -> Self {
300		self.null = null;
301		self
302	}
303
304	/// Generate SQL column definition
305	///
306	/// # Example
307	///
308	/// ```rust
309	/// use reinhardt_db::migrations::operations::FieldDefinition;
310	/// use reinhardt_db::migrations::FieldType;
311	///
312	/// let field = FieldDefinition::new("email", FieldType::VarChar(255), false, true, Some("''"));
313	/// let sql = field.to_sql_definition();
314	/// assert!(sql.contains("VARCHAR(255)"));
315	/// assert!(sql.contains("UNIQUE"));
316	/// assert!(sql.contains("DEFAULT ''"));
317	/// ```
318	pub fn to_sql_definition(&self) -> String {
319		let mut parts = vec![self.field_type.to_sql_string()];
320
321		// Generated Columns (GENERATED ALWAYS AS ... STORED/VIRTUAL)
322		if let Some(ref generated_expr) = self.generated {
323			parts.push(format!("GENERATED ALWAYS AS ({})", generated_expr));
324
325			// Determine STORED or VIRTUAL
326			let is_stored = self.generated_stored.unwrap_or(false);
327
328			#[cfg(any(feature = "db-mysql", feature = "db-sqlite"))]
329			let is_virtual = self.generated_virtual.unwrap_or(false);
330			#[cfg(not(any(feature = "db-mysql", feature = "db-sqlite")))]
331			let is_virtual = false;
332
333			if is_stored {
334				parts.push("STORED".to_string());
335			} else if is_virtual {
336				parts.push("VIRTUAL".to_string());
337			}
338		}
339
340		// Identity/Auto-increment
341		#[cfg(feature = "db-postgres")]
342		if self.identity_always.unwrap_or(false) {
343			parts.push("GENERATED ALWAYS AS IDENTITY".to_string());
344		}
345		#[cfg(feature = "db-postgres")]
346		if self.identity_by_default.unwrap_or(false) {
347			parts.push("GENERATED BY DEFAULT AS IDENTITY".to_string());
348		}
349		#[cfg(feature = "db-mysql")]
350		if self.auto_increment.unwrap_or(false) {
351			parts.push("AUTO_INCREMENT".to_string());
352		}
353		#[cfg(feature = "db-sqlite")]
354		if self.autoincrement.unwrap_or(false) {
355			parts.push("AUTOINCREMENT".to_string());
356		}
357
358		if self.primary_key {
359			parts.push("PRIMARY KEY".to_string());
360		}
361
362		if self.unique && !self.primary_key {
363			parts.push("UNIQUE".to_string());
364		}
365
366		if !self.null && !self.primary_key {
367			parts.push("NOT NULL".to_string());
368		}
369
370		if let Some(ref default) = self.default {
371			parts.push(format!("DEFAULT {}", default));
372		}
373
374		// Character Set & Collation
375		#[cfg(feature = "db-mysql")]
376		if let Some(ref character_set) = self.character_set {
377			parts.push(format!("CHARACTER SET {}", character_set));
378		}
379
380		if let Some(ref collate) = self.collate {
381			parts.push(format!("COLLATE {}", collate));
382		}
383
384		// Comment (MySQL only in column definition)
385		#[cfg(feature = "db-mysql")]
386		if let Some(ref comment) = self.comment {
387			parts.push(format!("COMMENT '{}'", comment.replace('\'', "''")));
388		}
389
390		// Storage Optimization (PostgreSQL)
391		#[cfg(feature = "db-postgres")]
392		if let Some(ref storage) = self.storage {
393			parts.push(format!("STORAGE {}", storage.to_uppercase()));
394		}
395		#[cfg(feature = "db-postgres")]
396		if let Some(ref compression) = self.compression {
397			parts.push(format!("COMPRESSION {}", compression));
398		}
399
400		// ON UPDATE Trigger (MySQL)
401		#[cfg(feature = "db-mysql")]
402		if self.on_update_current_timestamp.unwrap_or(false) {
403			parts.push("ON UPDATE CURRENT_TIMESTAMP".to_string());
404		}
405
406		// Invisible Columns (MySQL)
407		#[cfg(feature = "db-mysql")]
408		if self.invisible.unwrap_or(false) {
409			parts.push("INVISIBLE".to_string());
410		}
411
412		// Full-Text Index
413		// Note: Full-text index is typically created separately as an index,
414		// not as part of the column definition. This field is used to mark
415		// columns that should have full-text indexes created for them.
416		// The actual index creation will be handled by the migration system.
417
418		// Numeric Attributes (MySQL, deprecated)
419		#[cfg(feature = "db-mysql")]
420		if self.unsigned.unwrap_or(false) {
421			parts.push("UNSIGNED".to_string());
422		}
423		#[cfg(feature = "db-mysql")]
424		if self.zerofill.unwrap_or(false) {
425			parts.push("ZEROFILL".to_string());
426		}
427
428		parts.join(" ")
429	}
430}
431
432/// Create a new model (table)
433///
434/// # Example
435///
436/// ```rust
437/// use reinhardt_db::migrations::operations::models::CreateModel;
438/// use reinhardt_db::migrations::operations::FieldDefinition;
439/// use reinhardt_db::migrations::{ProjectState, FieldType};
440///
441/// let mut state = ProjectState::new();
442/// let create = CreateModel::new(
443///     "User",
444///     vec![
445///         FieldDefinition::new("id", FieldType::Integer, true, false, Option::<&str>::None),
446///         FieldDefinition::new("name", FieldType::VarChar(100), false, false, Option::<&str>::None),
447///     ],
448/// );
449///
450/// create.state_forwards("myapp", &mut state);
451/// let model = state.get_model("myapp", "User").unwrap();
452/// assert_eq!(model.fields.len(), 2);
453/// ```
454#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct CreateModel {
456	/// The name.
457	pub name: String,
458	/// The fields.
459	pub fields: Vec<FieldDefinition>,
460	/// The options.
461	pub options: HashMap<String, String>,
462	/// The bases.
463	pub bases: Vec<String>,
464	/// Composite primary key fields (field names)
465	pub composite_primary_key: Option<Vec<String>>,
466}
467
468impl CreateModel {
469	/// Create a new CreateModel operation
470	pub fn new(name: impl Into<String>, fields: Vec<FieldDefinition>) -> Self {
471		Self {
472			name: name.into(),
473			fields,
474			options: HashMap::new(),
475			bases: vec![],
476			composite_primary_key: None,
477		}
478	}
479
480	/// Add model options
481	pub fn with_options(mut self, options: HashMap<String, String>) -> Self {
482		self.options = options;
483		self
484	}
485
486	/// Add base classes for inheritance
487	pub fn with_bases(mut self, bases: Vec<String>) -> Self {
488		self.bases = bases;
489		self
490	}
491
492	/// Set composite primary key with validation
493	///
494	/// # Errors
495	///
496	/// Returns `ValidationError::EmptyCompositePrimaryKey` if the fields list is empty.
497	/// Returns `ValidationError::NonExistentField` if any field name doesn't exist in the table.
498	///
499	/// # Example
500	///
501	/// ```rust
502	/// use reinhardt_db::migrations::operations::models::CreateModel;
503	/// use reinhardt_db::migrations::operations::FieldDefinition;
504	/// use reinhardt_db::migrations::FieldType;
505	///
506	/// let create = CreateModel::new(
507	///     "post_tags",
508	///     vec![
509	///         FieldDefinition::new("post_id", FieldType::Integer, false, false, Option::<&str>::None),
510	///         FieldDefinition::new("tag_id", FieldType::Integer, false, false, Option::<&str>::None),
511	///     ],
512	/// )
513	/// .with_composite_primary_key(vec!["post_id".to_string(), "tag_id".to_string()])
514	/// .expect("Valid composite primary key");
515	///
516	/// assert!(create.composite_primary_key.is_some());
517	/// ```
518	pub fn with_composite_primary_key(mut self, fields: Vec<String>) -> ValidationResult<Self> {
519		// Validation: Check for empty list
520		if fields.is_empty() {
521			return Err(ValidationError::EmptyCompositePrimaryKey {
522				table_name: self.name.clone(),
523			});
524		}
525
526		// Validation: Verify all fields exist in table schema
527		let available_field_names: Vec<String> =
528			self.fields.iter().map(|f| f.name.clone()).collect();
529
530		for field_name in &fields {
531			if !available_field_names.contains(field_name) {
532				return Err(ValidationError::NonExistentField {
533					field_name: field_name.clone(),
534					table_name: self.name.clone(),
535					available_fields: available_field_names.clone(),
536				});
537			}
538		}
539
540		self.composite_primary_key = Some(fields);
541		Ok(self)
542	}
543
544	/// Apply to project state (forward)
545	pub fn state_forwards(&self, app_label: &str, state: &mut ProjectState) {
546		let mut model = ModelState::new(app_label, &self.name);
547
548		for field_def in &self.fields {
549			let field = FieldState::new(
550				field_def.name.clone(),
551				field_def.field_type.clone(),
552				field_def.primary_key,
553			);
554			model.add_field(field);
555		}
556
557		state.add_model(model);
558	}
559
560	/// Generate SQL using schema editor
561	///
562	/// # Example
563	///
564	/// ```rust
565	/// use reinhardt_db::migrations::operations::models::CreateModel;
566	/// use reinhardt_db::migrations::operations::FieldDefinition;
567	/// use reinhardt_db::migrations::FieldType;
568	///
569	/// let create = CreateModel::new(
570	///     "users",
571	///     vec![
572	///         FieldDefinition::new("id", FieldType::Integer, true, false, Option::<&str>::None),
573	///         FieldDefinition::new("email", FieldType::VarChar(255), false, false, Option::<&str>::None),
574	///     ],
575	/// );
576	///
577	/// // Convert to SQL - actual DB operations would use schema editor
578	/// let columns: Vec<(&str, String)> = create.fields
579	///     .iter()
580	///     .map(|f| (f.name.as_str(), f.to_sql_definition()))
581	///     .collect();
582	///
583	/// assert_eq!(columns.len(), 2);
584	/// assert_eq!(columns[0].0, "id");
585	/// assert!(columns[0].1.contains("PRIMARY KEY"));
586	/// ```
587	pub fn database_forwards(&self, schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
588		let mut sql_statements = Vec::new();
589
590		// If composite primary key is defined, don't mark individual fields as primary keys
591		let has_composite_pk = self.composite_primary_key.is_some();
592
593		// Convert field definitions to column specifications for schema editor
594		let column_defs: Vec<String> = self
595			.fields
596			.iter()
597			.map(|f| {
598				// For composite PKs, don't add PRIMARY KEY to individual field definitions
599				if has_composite_pk && f.primary_key {
600					// Build field definition without PRIMARY KEY keyword
601					let mut parts = vec![f.field_type.to_sql_string()];
602
603					// Primary key fields are always NOT NULL
604					parts.push("NOT NULL".to_string());
605
606					if f.unique {
607						parts.push("UNIQUE".to_string());
608					}
609					if let Some(ref default) = f.default {
610						parts.push(format!("DEFAULT {}", default));
611					}
612					parts.join(" ")
613				} else {
614					f.to_sql_definition()
615				}
616			})
617			.collect();
618
619		// Build column pairs: (name, type_definition)
620		let columns: Vec<(&str, &str)> = self
621			.fields
622			.iter()
623			.zip(column_defs.iter())
624			.map(|(f, def)| (f.name.as_str(), def.as_str()))
625			.collect();
626
627		// Generate CREATE TABLE SQL using database-specific query builder
628		let stmt = schema_editor.create_table_statement(&self.name, &columns);
629		let mut create_sql = schema_editor.build_create_table_sql(&stmt);
630
631		// Add composite primary key constraint if defined
632		if let Some(ref pk_fields) = self.composite_primary_key {
633			let db_type = schema_editor.database_type();
634			let pk_name = format!("{}_pkey", self.name);
635			let quoted_pk_name = quote_identifier(&pk_name, db_type);
636			let quoted_pk_fields = pk_fields
637				.iter()
638				.map(|f| quote_identifier(f, db_type))
639				.collect::<Vec<_>>()
640				.join(", ");
641			let constraint_sql = format!(
642				"CONSTRAINT {} PRIMARY KEY ({})",
643				quoted_pk_name, quoted_pk_fields
644			);
645
646			// Insert constraint before closing parenthesis
647			// CREATE TABLE foo (col1 INT, col2 INT); becomes
648			// CREATE TABLE foo (col1 INT, col2 INT, CONSTRAINT foo_pkey PRIMARY KEY (col1, col2));
649			if create_sql.ends_with(");") {
650				let insert_pos = create_sql.len() - 2; // Before ");"
651				create_sql.insert_str(insert_pos, &format!(", {}", constraint_sql));
652			} else if create_sql.ends_with(")") {
653				let insert_pos = create_sql.len() - 1; // Before ")"
654				create_sql.insert_str(insert_pos, &format!(", {}", constraint_sql));
655			}
656		}
657
658		// Table-level attributes (SQLite)
659		// Add STRICT and/or WITHOUT ROWID if specified
660		#[cfg(feature = "db-sqlite")]
661		{
662			let mut table_options = Vec::new();
663
664			if let Some(strict_val) = self.options.get("strict")
665				&& strict_val == "true"
666			{
667				table_options.push("STRICT");
668			}
669
670			if let Some(without_rowid_val) = self.options.get("without_rowid")
671				&& without_rowid_val == "true"
672			{
673				table_options.push("WITHOUT ROWID");
674			}
675
676			if !table_options.is_empty() {
677				// Remove trailing semicolon if present
678				if create_sql.ends_with(';') {
679					create_sql.pop();
680				}
681				// Add table options
682				create_sql.push(' ');
683				create_sql.push_str(&table_options.join(" "));
684				create_sql.push(';');
685			}
686		}
687
688		sql_statements.push(create_sql);
689		sql_statements
690	}
691}
692
693/// Delete a model (drop table)
694///
695/// # Example
696///
697/// ```rust
698/// use reinhardt_db::migrations::operations::models::{CreateModel, DeleteModel};
699/// use reinhardt_db::migrations::operations::FieldDefinition;
700/// use reinhardt_db::migrations::{ProjectState, FieldType};
701///
702/// let mut state = ProjectState::new();
703///
704/// // First create a model
705/// let create = CreateModel::new(
706///     "User",
707///     vec![FieldDefinition::new("id", FieldType::Integer, true, false, Option::<&str>::None)],
708/// );
709/// create.state_forwards("myapp", &mut state);
710/// assert!(state.get_model("myapp", "User").is_some());
711///
712/// // Then delete it
713/// let delete = DeleteModel::new("User");
714/// delete.state_forwards("myapp", &mut state);
715/// assert!(state.get_model("myapp", "User").is_none());
716/// ```
717#[derive(Debug, Clone, Serialize, Deserialize)]
718pub struct DeleteModel {
719	/// The name.
720	pub name: String,
721}
722
723impl DeleteModel {
724	/// Create a new DeleteModel operation
725	pub fn new(name: impl Into<String>) -> Self {
726		Self { name: name.into() }
727	}
728
729	/// Apply to project state (forward)
730	pub fn state_forwards(&self, app_label: &str, state: &mut ProjectState) {
731		state.remove_model(app_label, &self.name);
732	}
733
734	/// Generate SQL using schema editor
735	///
736	/// # Example
737	///
738	/// ```rust,no_run
739	/// use reinhardt_db::migrations::operations::models::DeleteModel;
740	/// use reinhardt_db::backends::schema::factory::{SchemaEditorFactory, DatabaseType};
741	///
742	/// let delete = DeleteModel::new("users");
743	/// let factory = SchemaEditorFactory::new();
744	/// let editor = factory.create_for_database(DatabaseType::PostgreSQL);
745	///
746	/// let sql = delete.database_forwards(editor.as_ref());
747	/// assert_eq!(sql.len(), 1);
748	/// assert!(sql[0].contains("DROP TABLE"));
749	/// assert!(sql[0].contains("\"users\""));
750	/// ```
751	pub fn database_forwards(&self, schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
752		let stmt = schema_editor.drop_table_statement(&self.name, false);
753		vec![schema_editor.build_drop_table_sql(&stmt)]
754	}
755}
756
757/// Rename a model (rename table)
758///
759/// # Example
760///
761/// ```rust
762/// use reinhardt_db::migrations::operations::models::{CreateModel, RenameModel};
763/// use reinhardt_db::migrations::operations::FieldDefinition;
764/// use reinhardt_db::migrations::{ProjectState, FieldType};
765///
766/// let mut state = ProjectState::new();
767///
768/// // Create a model
769/// let create = CreateModel::new(
770///     "User",
771///     vec![FieldDefinition::new("id", FieldType::Integer, true, false, Option::<&str>::None)],
772/// );
773/// create.state_forwards("myapp", &mut state);
774///
775/// // Rename it
776/// let rename = RenameModel::new("User", "Customer");
777/// rename.state_forwards("myapp", &mut state);
778///
779/// assert!(state.get_model("myapp", "User").is_none());
780/// assert!(state.get_model("myapp", "Customer").is_some());
781/// ```
782#[derive(Debug, Clone, Serialize, Deserialize)]
783pub struct RenameModel {
784	/// The old name.
785	pub old_name: String,
786	/// The new name.
787	pub new_name: String,
788}
789
790impl RenameModel {
791	/// Create a new RenameModel operation
792	pub fn new(old_name: impl Into<String>, new_name: impl Into<String>) -> Self {
793		Self {
794			old_name: old_name.into(),
795			new_name: new_name.into(),
796		}
797	}
798
799	/// Apply to project state (forward)
800	pub fn state_forwards(&self, app_label: &str, state: &mut ProjectState) {
801		state.rename_model(app_label, &self.old_name, self.new_name.clone());
802	}
803
804	/// Generate SQL using schema editor
805	///
806	/// # Example
807	///
808	/// ```rust,no_run
809	/// use reinhardt_db::migrations::operations::models::RenameModel;
810	/// use reinhardt_db::backends::schema::factory::{SchemaEditorFactory, DatabaseType};
811	///
812	/// let rename = RenameModel::new("users", "customers");
813	/// let factory = SchemaEditorFactory::new();
814	/// let editor = factory.create_for_database(DatabaseType::PostgreSQL);
815	///
816	/// let sql = rename.database_forwards(editor.as_ref());
817	/// assert_eq!(sql.len(), 1);
818	/// assert!(sql[0].contains("ALTER TABLE"));
819	/// assert!(sql[0].contains("\"users\""));
820	/// assert!(sql[0].contains("\"customers\""));
821	/// ```
822	pub fn database_forwards(&self, schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
823		// Quote identifiers based on database type
824		let db_type = schema_editor.database_type();
825		let old_name = quote_identifier(&self.old_name, db_type);
826		let new_name = quote_identifier(&self.new_name, db_type);
827
828		vec![format!("ALTER TABLE {} RENAME TO {}", old_name, new_name)]
829	}
830}
831
832/// Move a model from one app to another
833///
834/// This operation moves a model from one application to another while preserving
835/// its data and structure. Unlike Django which requires manual migration steps,
836/// Reinhardt provides an explicit MoveModel operation.
837///
838/// # Example
839///
840/// ```rust
841/// use reinhardt_db::migrations::operations::models::{CreateModel, MoveModel};
842/// use reinhardt_db::migrations::operations::FieldDefinition;
843/// use reinhardt_db::migrations::{ProjectState, FieldType};
844///
845/// let mut state = ProjectState::new();
846///
847/// // Create a model in myapp
848/// let create = CreateModel::new(
849///     "User",
850///     vec![FieldDefinition::new("id", FieldType::Integer, true, false, Option::<&str>::None)],
851/// );
852/// create.state_forwards("myapp", &mut state);
853///
854/// // Move it to auth app
855/// let move_op = MoveModel::new("User", "myapp", "auth");
856/// move_op.state_forwards("auth", &mut state);
857///
858/// assert!(state.get_model("myapp", "User").is_none());
859/// assert!(state.get_model("auth", "User").is_some());
860/// ```
861#[derive(Debug, Clone, Serialize, Deserialize)]
862pub struct MoveModel {
863	/// The model name.
864	pub model_name: String,
865	/// The from app.
866	pub from_app: String,
867	/// The to app.
868	pub to_app: String,
869	/// The rename table.
870	pub rename_table: bool,
871	/// The old table name.
872	pub old_table_name: Option<String>,
873	/// The new table name.
874	pub new_table_name: Option<String>,
875}
876
877impl MoveModel {
878	/// Create a new MoveModel operation
879	///
880	/// # Example
881	///
882	/// ```rust
883	/// use reinhardt_db::migrations::operations::models::MoveModel;
884	///
885	/// let move_op = MoveModel::new("User", "myapp", "auth");
886	/// assert_eq!(move_op.model_name, "User");
887	/// assert_eq!(move_op.from_app, "myapp");
888	/// assert_eq!(move_op.to_app, "auth");
889	/// assert!(!move_op.rename_table);
890	/// ```
891	pub fn new(
892		model_name: impl Into<String>,
893		from_app: impl Into<String>,
894		to_app: impl Into<String>,
895	) -> Self {
896		Self {
897			model_name: model_name.into(),
898			from_app: from_app.into(),
899			to_app: to_app.into(),
900			rename_table: false,
901			old_table_name: None,
902			new_table_name: None,
903		}
904	}
905
906	/// Enable table renaming during the move
907	///
908	/// # Example
909	///
910	/// ```rust
911	/// use reinhardt_db::migrations::operations::models::MoveModel;
912	///
913	/// let move_op = MoveModel::new("User", "myapp", "auth")
914	///     .with_table_rename("myapp_user", "auth_user");
915	///
916	/// assert!(move_op.rename_table);
917	/// assert_eq!(move_op.old_table_name, Some("myapp_user".to_string()));
918	/// assert_eq!(move_op.new_table_name, Some("auth_user".to_string()));
919	/// ```
920	pub fn with_table_rename(
921		mut self,
922		old_table: impl Into<String>,
923		new_table: impl Into<String>,
924	) -> Self {
925		self.rename_table = true;
926		self.old_table_name = Some(old_table.into());
927		self.new_table_name = Some(new_table.into());
928		self
929	}
930
931	/// Apply to project state (forward)
932	///
933	/// This removes the model from the source app and adds it to the target app.
934	pub fn state_forwards(&self, _app_label: &str, state: &mut ProjectState) {
935		// Remove from source app
936		if let Some(model) = state
937			.models
938			.remove(&(self.from_app.clone(), self.model_name.clone()))
939		{
940			// Update app_label
941			let mut new_model = model;
942			new_model.app_label = self.to_app.clone();
943
944			// Add to target app
945			state
946				.models
947				.insert((self.to_app.clone(), self.model_name.clone()), new_model);
948		}
949	}
950
951	/// Apply to project state (backward/reverse)
952	///
953	/// This moves the model back to its original app.
954	pub fn state_backwards(&self, _app_label: &str, state: &mut ProjectState) {
955		// Remove from target app
956		if let Some(model) = state
957			.models
958			.remove(&(self.to_app.clone(), self.model_name.clone()))
959		{
960			// Restore original app_label
961			let mut original_model = model;
962			original_model.app_label = self.from_app.clone();
963
964			// Add back to source app
965			state.models.insert(
966				(self.from_app.clone(), self.model_name.clone()),
967				original_model,
968			);
969		}
970	}
971
972	/// Generate SQL using schema editor
973	///
974	/// If rename_table is true, generates ALTER TABLE RENAME statement.
975	/// Otherwise, no SQL is needed since app_label is a Python/Rust concept
976	/// that doesn't affect the database schema.
977	///
978	/// # Example
979	///
980	/// ```rust,no_run
981	/// use reinhardt_db::migrations::operations::models::MoveModel;
982	/// use reinhardt_db::backends::schema::factory::{SchemaEditorFactory, DatabaseType};
983	///
984	/// // Without table rename
985	/// let move_op1 = MoveModel::new("User", "myapp", "auth");
986	/// let factory = SchemaEditorFactory::new();
987	/// let editor = factory.create_for_database(DatabaseType::PostgreSQL);
988	/// let sql1 = move_op1.database_forwards(editor.as_ref());
989	/// assert!(sql1.is_empty()); // No SQL needed
990	///
991	/// // With table rename
992	/// let move_op2 = MoveModel::new("User", "myapp", "auth")
993	///     .with_table_rename("myapp_user", "auth_user");
994	/// let sql2 = move_op2.database_forwards(editor.as_ref());
995	/// assert_eq!(sql2.len(), 1);
996	/// assert!(sql2[0].contains("ALTER TABLE"));
997	/// ```
998	pub fn database_forwards(&self, schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
999		if self.rename_table {
1000			if let (Some(old_table), Some(new_table)) = (&self.old_table_name, &self.new_table_name)
1001			{
1002				// Quote identifiers based on database type
1003				let db_type = schema_editor.database_type();
1004				let old_name = quote_identifier(old_table, db_type);
1005				let new_name = quote_identifier(new_table, db_type);
1006
1007				vec![format!("ALTER TABLE {} RENAME TO {}", old_name, new_name)]
1008			} else {
1009				vec![]
1010			}
1011		} else {
1012			// App label is a framework concept, no database changes needed
1013			vec![]
1014		}
1015	}
1016
1017	/// Generate reverse SQL
1018	pub fn database_backwards(&self, schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
1019		if self.rename_table {
1020			if let (Some(old_table), Some(new_table)) = (&self.old_table_name, &self.new_table_name)
1021			{
1022				// Reverse: rename back to original
1023				// Quote identifiers based on database type
1024				let db_type = schema_editor.database_type();
1025				let old_name = quote_identifier(old_table, db_type);
1026				let new_name = quote_identifier(new_table, db_type);
1027
1028				vec![format!("ALTER TABLE {} RENAME TO {}", new_name, old_name)]
1029			} else {
1030				vec![]
1031			}
1032		} else {
1033			vec![]
1034		}
1035	}
1036}
1037
1038// MigrationOperation trait implementation for Django-style naming
1039use crate::migrations::operation_trait::MigrationOperation;
1040
1041impl MigrationOperation for CreateModel {
1042	fn migration_name_fragment(&self) -> Option<String> {
1043		Some(self.name.to_lowercase())
1044	}
1045
1046	fn describe(&self) -> String {
1047		format!("Create model {}", self.name)
1048	}
1049}
1050
1051impl MigrationOperation for DeleteModel {
1052	fn migration_name_fragment(&self) -> Option<String> {
1053		Some(format!("delete_{}", self.name.to_lowercase()))
1054	}
1055
1056	fn describe(&self) -> String {
1057		format!("Delete model {}", self.name)
1058	}
1059}
1060
1061impl MigrationOperation for RenameModel {
1062	fn migration_name_fragment(&self) -> Option<String> {
1063		Some(format!(
1064			"rename_{}_to_{}",
1065			self.old_name.to_lowercase(),
1066			self.new_name.to_lowercase()
1067		))
1068	}
1069
1070	fn describe(&self) -> String {
1071		format!("Rename model {} to {}", self.old_name, self.new_name)
1072	}
1073}
1074
1075impl MigrationOperation for MoveModel {
1076	fn migration_name_fragment(&self) -> Option<String> {
1077		Some(format!(
1078			"move_{}_to_{}",
1079			self.model_name.to_lowercase(),
1080			self.to_app.to_lowercase()
1081		))
1082	}
1083
1084	fn describe(&self) -> String {
1085		format!(
1086			"Move model {} from {} to {}",
1087			self.model_name, self.from_app, self.to_app
1088		)
1089	}
1090}
1091
1092#[cfg(test)]
1093mod tests {
1094	use super::*;
1095	use crate::migrations::FieldType;
1096
1097	#[test]
1098	fn test_field_definition_to_sql() {
1099		let field = FieldDefinition::new("id", FieldType::Integer, true, false, None::<String>);
1100		let sql = field.to_sql_definition();
1101		assert_eq!(sql, "INTEGER PRIMARY KEY");
1102
1103		let field2 =
1104			FieldDefinition::new("email", FieldType::VarChar(255), false, true, Some("''"));
1105		let sql2 = field2.to_sql_definition();
1106		assert_eq!(sql2, "VARCHAR(255) UNIQUE NOT NULL DEFAULT ''");
1107	}
1108
1109	#[test]
1110	fn test_create_model_state_forwards() {
1111		let mut state = ProjectState::new();
1112		let create = CreateModel::new(
1113			"User",
1114			vec![
1115				FieldDefinition::new("id", FieldType::Integer, true, false, None::<String>),
1116				FieldDefinition::new(
1117					"name",
1118					FieldType::VarChar(100),
1119					false,
1120					false,
1121					None::<String>,
1122				),
1123			],
1124		);
1125
1126		create.state_forwards("myapp", &mut state);
1127
1128		let model = state.get_model("myapp", "User").unwrap();
1129		assert_eq!(model.name, "User");
1130		assert_eq!(model.fields.len(), 2);
1131		assert_eq!(model.fields.get("id").unwrap().name, "id");
1132		assert_eq!(model.fields.get("name").unwrap().name, "name");
1133	}
1134
1135	#[test]
1136	fn test_delete_model_state_forwards() {
1137		let mut state = ProjectState::new();
1138
1139		// Create a model first
1140		let create = CreateModel::new(
1141			"User",
1142			vec![FieldDefinition::new(
1143				"id",
1144				FieldType::Integer,
1145				true,
1146				false,
1147				None::<String>,
1148			)],
1149		);
1150		create.state_forwards("myapp", &mut state);
1151		assert!(state.get_model("myapp", "User").is_some());
1152
1153		// Delete it
1154		let delete = DeleteModel::new("User");
1155		delete.state_forwards("myapp", &mut state);
1156		assert!(state.get_model("myapp", "User").is_none());
1157	}
1158
1159	#[test]
1160	fn test_rename_model_state_forwards() {
1161		let mut state = ProjectState::new();
1162
1163		// Create a model first
1164		let create = CreateModel::new(
1165			"User",
1166			vec![FieldDefinition::new(
1167				"id",
1168				FieldType::Integer,
1169				true,
1170				false,
1171				None::<String>,
1172			)],
1173		);
1174		create.state_forwards("myapp", &mut state);
1175
1176		// Rename it
1177		let rename = RenameModel::new("User", "Customer");
1178		rename.state_forwards("myapp", &mut state);
1179
1180		assert!(state.get_model("myapp", "User").is_none());
1181		let model = state.get_model("myapp", "Customer").unwrap();
1182		assert_eq!(model.name, "Customer");
1183	}
1184
1185	#[cfg(feature = "db-postgres")]
1186	#[test]
1187	fn test_delete_model_database_forwards() {
1188		use crate::backends::schema::test_utils::MockSchemaEditor;
1189
1190		let delete = DeleteModel::new("users");
1191		let editor = MockSchemaEditor::new();
1192
1193		let sql = delete.database_forwards(&editor);
1194		assert_eq!(sql.len(), 1);
1195		assert_eq!(sql[0], "DROP TABLE IF EXISTS \"users\"");
1196	}
1197
1198	#[cfg(feature = "db-postgres")]
1199	#[test]
1200	fn test_rename_model_database_forwards() {
1201		use crate::backends::schema::test_utils::MockSchemaEditor;
1202
1203		let rename = RenameModel::new("users", "customers");
1204		let editor = MockSchemaEditor::new();
1205
1206		let sql = rename.database_forwards(&editor);
1207		assert_eq!(sql.len(), 1);
1208		assert_eq!(sql[0], "ALTER TABLE \"users\" RENAME TO \"customers\"");
1209	}
1210
1211	#[test]
1212	fn test_field_definition_nullable() {
1213		let field = FieldDefinition::new(
1214			"email",
1215			FieldType::VarChar(255),
1216			false,
1217			false,
1218			None::<String>,
1219		)
1220		.nullable(true);
1221
1222		assert!(field.null);
1223		let sql = field.to_sql_definition();
1224		assert_eq!(sql, "VARCHAR(255)");
1225	}
1226
1227	#[test]
1228	fn test_create_model_with_options() {
1229		let mut options = HashMap::new();
1230		options.insert("db_table".to_string(), "custom_users".to_string());
1231
1232		let create = CreateModel::new(
1233			"User",
1234			vec![FieldDefinition::new(
1235				"id",
1236				FieldType::Integer,
1237				true,
1238				false,
1239				None::<String>,
1240			)],
1241		)
1242		.with_options(options.clone());
1243
1244		assert_eq!(create.options, options);
1245		assert_eq!(
1246			create.options.get("db_table"),
1247			Some(&"custom_users".to_string())
1248		);
1249	}
1250
1251	#[test]
1252	fn test_create_model_with_bases() {
1253		let bases = vec!["BaseModel".to_string(), "Timestamped".to_string()];
1254
1255		let create = CreateModel::new(
1256			"User",
1257			vec![FieldDefinition::new(
1258				"id",
1259				FieldType::Integer,
1260				true,
1261				false,
1262				None::<String>,
1263			)],
1264		)
1265		.with_bases(bases.clone());
1266
1267		assert_eq!(create.bases, bases);
1268		assert_eq!(create.bases.len(), 2);
1269	}
1270
1271	#[test]
1272	fn test_create_model_multiple_fields() {
1273		let mut state = ProjectState::new();
1274
1275		let create = CreateModel::new(
1276			"User",
1277			vec![
1278				FieldDefinition::new("id", FieldType::Integer, true, false, None::<String>),
1279				FieldDefinition::new(
1280					"username",
1281					FieldType::VarChar(50),
1282					false,
1283					true,
1284					None::<String>,
1285				),
1286				FieldDefinition::new(
1287					"email",
1288					FieldType::VarChar(255),
1289					false,
1290					true,
1291					None::<String>,
1292				),
1293				FieldDefinition::new("is_active", FieldType::Boolean, false, false, Some("true")),
1294			],
1295		);
1296
1297		create.state_forwards("myapp", &mut state);
1298
1299		let model = state.get_model("myapp", "User").unwrap();
1300		assert_eq!(model.fields.len(), 4);
1301		assert_eq!(model.fields.get("id").unwrap().name, "id");
1302		assert_eq!(model.fields.get("username").unwrap().name, "username");
1303		assert_eq!(model.fields.get("email").unwrap().name, "email");
1304		assert_eq!(model.fields.get("is_active").unwrap().name, "is_active");
1305	}
1306
1307	#[test]
1308	fn test_field_definition_with_default() {
1309		let field = FieldDefinition::new(
1310			"status",
1311			FieldType::VarChar(20),
1312			false,
1313			false,
1314			Some("'pending'"),
1315		);
1316
1317		assert_eq!(field.default, Some("'pending'".to_string()));
1318
1319		let sql = field.to_sql_definition();
1320		assert_eq!(sql, "VARCHAR(20) NOT NULL DEFAULT 'pending'");
1321	}
1322
1323	#[test]
1324	fn test_delete_model_removes_from_state() {
1325		let mut state = ProjectState::new();
1326
1327		// Create multiple models
1328		let create1 = CreateModel::new(
1329			"User",
1330			vec![FieldDefinition::new(
1331				"id",
1332				FieldType::Integer,
1333				true,
1334				false,
1335				None::<String>,
1336			)],
1337		);
1338		let create2 = CreateModel::new(
1339			"Post",
1340			vec![FieldDefinition::new(
1341				"id",
1342				FieldType::Integer,
1343				true,
1344				false,
1345				None::<String>,
1346			)],
1347		);
1348
1349		create1.state_forwards("myapp", &mut state);
1350		create2.state_forwards("myapp", &mut state);
1351
1352		assert!(state.get_model("myapp", "User").is_some());
1353		assert!(state.get_model("myapp", "Post").is_some());
1354
1355		// Delete only User
1356		let delete = DeleteModel::new("User");
1357		delete.state_forwards("myapp", &mut state);
1358
1359		assert!(state.get_model("myapp", "User").is_none());
1360		assert!(state.get_model("myapp", "Post").is_some());
1361	}
1362
1363	#[test]
1364	fn test_rename_model_preserves_fields() {
1365		let mut state = ProjectState::new();
1366
1367		// Create a model with multiple fields
1368		let create = CreateModel::new(
1369			"User",
1370			vec![
1371				FieldDefinition::new("id", FieldType::Integer, true, false, None::<String>),
1372				FieldDefinition::new(
1373					"name",
1374					FieldType::VarChar(100),
1375					false,
1376					false,
1377					None::<String>,
1378				),
1379			],
1380		);
1381		create.state_forwards("myapp", &mut state);
1382
1383		// Rename it
1384		let rename = RenameModel::new("User", "Account");
1385		rename.state_forwards("myapp", &mut state);
1386
1387		// Check that fields are preserved
1388		let model = state.get_model("myapp", "Account").unwrap();
1389		assert_eq!(model.fields.len(), 2);
1390		assert_eq!(model.fields.get("id").unwrap().name, "id");
1391		assert_eq!(model.fields.get("name").unwrap().name, "name");
1392	}
1393
1394	#[test]
1395	fn test_move_model_basic() {
1396		let mut state = ProjectState::new();
1397
1398		// Create a model in myapp
1399		let create = CreateModel::new(
1400			"User",
1401			vec![FieldDefinition::new(
1402				"id",
1403				FieldType::Integer,
1404				true,
1405				false,
1406				None::<String>,
1407			)],
1408		);
1409		create.state_forwards("myapp", &mut state);
1410		assert!(state.get_model("myapp", "User").is_some());
1411
1412		// Move to auth app
1413		let move_op = MoveModel::new("User", "myapp", "auth");
1414		move_op.state_forwards("auth", &mut state);
1415
1416		// Check model is moved
1417		assert!(state.get_model("myapp", "User").is_none());
1418		assert!(state.get_model("auth", "User").is_some());
1419
1420		// Check app_label is updated
1421		let model = state.get_model("auth", "User").unwrap();
1422		assert_eq!(model.app_label, "auth");
1423	}
1424
1425	#[test]
1426	fn test_move_model_preserves_fields() {
1427		let mut state = ProjectState::new();
1428
1429		// Create a model with multiple fields
1430		let create = CreateModel::new(
1431			"User",
1432			vec![
1433				FieldDefinition::new("id", FieldType::Integer, true, false, None::<String>),
1434				FieldDefinition::new(
1435					"email",
1436					FieldType::VarChar(255),
1437					false,
1438					false,
1439					None::<String>,
1440				),
1441				FieldDefinition::new(
1442					"name",
1443					FieldType::VarChar(100),
1444					false,
1445					false,
1446					None::<String>,
1447				),
1448			],
1449		);
1450		create.state_forwards("myapp", &mut state);
1451
1452		// Move it
1453		let move_op = MoveModel::new("User", "myapp", "auth");
1454		move_op.state_forwards("auth", &mut state);
1455
1456		// Check fields are preserved
1457		let model = state.get_model("auth", "User").unwrap();
1458		assert_eq!(model.fields.len(), 3);
1459		assert_eq!(model.fields.get("id").unwrap().name, "id");
1460		assert_eq!(model.fields.get("email").unwrap().name, "email");
1461		assert_eq!(model.fields.get("name").unwrap().name, "name");
1462	}
1463
1464	#[test]
1465	fn test_move_model_backwards() {
1466		let mut state = ProjectState::new();
1467
1468		// Create and move model
1469		let create = CreateModel::new(
1470			"User",
1471			vec![FieldDefinition::new(
1472				"id",
1473				FieldType::Integer,
1474				true,
1475				false,
1476				None::<String>,
1477			)],
1478		);
1479		create.state_forwards("myapp", &mut state);
1480
1481		let move_op = MoveModel::new("User", "myapp", "auth");
1482		move_op.state_forwards("auth", &mut state);
1483		assert!(state.get_model("auth", "User").is_some());
1484
1485		// Reverse the move
1486		move_op.state_backwards("myapp", &mut state);
1487
1488		// Check model is back in original app
1489		assert!(state.get_model("auth", "User").is_none());
1490		assert!(state.get_model("myapp", "User").is_some());
1491
1492		let model = state.get_model("myapp", "User").unwrap();
1493		assert_eq!(model.app_label, "myapp");
1494	}
1495
1496	#[cfg(feature = "db-postgres")]
1497	#[test]
1498	fn test_move_model_without_table_rename() {
1499		use crate::backends::schema::test_utils::MockSchemaEditor;
1500
1501		let move_op = MoveModel::new("User", "myapp", "auth");
1502		let editor = MockSchemaEditor::new();
1503
1504		let sql = move_op.database_forwards(&editor);
1505		// No SQL needed when not renaming table
1506		assert_eq!(sql.len(), 0);
1507	}
1508
1509	#[cfg(feature = "db-postgres")]
1510	#[test]
1511	fn test_move_model_with_table_rename() {
1512		use crate::backends::schema::test_utils::MockSchemaEditor;
1513
1514		let move_op =
1515			MoveModel::new("User", "myapp", "auth").with_table_rename("myapp_user", "auth_user");
1516
1517		let editor = MockSchemaEditor::new();
1518
1519		let sql = move_op.database_forwards(&editor);
1520		assert_eq!(sql.len(), 1);
1521		assert_eq!(sql[0], "ALTER TABLE \"myapp_user\" RENAME TO \"auth_user\"");
1522	}
1523
1524	#[cfg(feature = "db-postgres")]
1525	#[test]
1526	fn test_move_model_backward_sql() {
1527		use crate::backends::schema::test_utils::MockSchemaEditor;
1528
1529		let move_op =
1530			MoveModel::new("User", "myapp", "auth").with_table_rename("myapp_user", "auth_user");
1531
1532		let editor = MockSchemaEditor::new();
1533
1534		let sql = move_op.database_backwards(&editor);
1535		assert_eq!(sql.len(), 1);
1536		// Reverse: auth_user back to myapp_user
1537		assert_eq!(sql[0], "ALTER TABLE \"auth_user\" RENAME TO \"myapp_user\"");
1538	}
1539}