Skip to main content

reinhardt_db/migrations/
migration.rs

1//! Migration definition
2
3use super::Operation;
4use super::dependency::{OptionalDependency, SwappableDependency};
5use serde::{Deserialize, Serialize};
6
7/// A database migration
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
9pub struct Migration {
10	/// Migration name (e.g., "0001_initial")
11	pub name: String,
12
13	/// App label
14	pub app_label: String,
15
16	/// Operations to apply
17	pub operations: Vec<Operation>,
18
19	/// Dependencies (app_label, migration_name)
20	pub dependencies: Vec<(String, String)>,
21
22	/// Migrations this replaces
23	pub replaces: Vec<(String, String)>,
24
25	/// Whether this is wrapped in a transaction
26	pub atomic: bool,
27
28	/// Whether this is an initial migration (explicit or inferred from dependencies)
29	/// - `Some(true)`: Explicitly marked as initial
30	/// - `Some(false)`: Explicitly marked as non-initial
31	/// - `None`: Auto-infer from `dependencies.is_empty()`
32	pub initial: Option<bool>,
33
34	/// Whether to update only ProjectState without executing database operations
35	/// (Django's SeparateDatabaseAndState equivalent with state_operations only)
36	#[serde(default)]
37	pub state_only: bool,
38
39	/// Whether to execute only database operations without updating ProjectState
40	/// (Django's SeparateDatabaseAndState equivalent with database_operations only)
41	#[serde(default)]
42	pub database_only: bool,
43
44	/// Swappable dependencies (e.g., AUTH_USER_MODEL pattern)
45	///
46	/// These dependencies resolve to different apps based on settings.
47	/// For example, a migration depending on the User model might use:
48	/// ```ignore
49	/// swappable_dependencies: vec![
50	///     SwappableDependency::new("AUTH_USER_MODEL", "auth", "User", "0001_initial")
51	/// ]
52	/// ```
53	#[serde(default)]
54	pub swappable_dependencies: Vec<SwappableDependency>,
55
56	/// Optional dependencies (conditional based on app installation or settings)
57	///
58	/// These dependencies are only enforced when their condition is met.
59	/// For example, a migration might optionally depend on PostGIS:
60	/// ```ignore
61	/// optional_dependencies: vec![
62	///     OptionalDependency::new(
63	///         "gis_extension",
64	///         "0001_enable_postgis",
65	///         DependencyCondition::AppInstalled("gis_extension".to_string())
66	///     )
67	/// ]
68	/// ```
69	#[serde(default)]
70	pub optional_dependencies: Vec<OptionalDependency>,
71}
72
73impl Migration {
74	/// Create a new migration
75	///
76	/// # Examples
77	///
78	/// ```rust,ignore
79	/// use reinhardt_db::migrations::Migration;
80	///
81	/// let migration = Migration::new("0001_initial", "myapp");
82	/// assert_eq!(migration.name, "0001_initial");
83	/// assert_eq!(migration.app_label, "myapp");
84	/// assert!(migration.atomic);
85	/// ```
86	pub fn new(name: impl Into<String>, app_label: impl Into<String>) -> Self {
87		Self {
88			name: name.into(),
89			app_label: app_label.into(),
90			operations: Vec::new(),
91			dependencies: Vec::new(),
92			replaces: Vec::new(),
93			atomic: true,
94			initial: None,
95			state_only: false,
96			database_only: false,
97			swappable_dependencies: Vec::new(),
98			optional_dependencies: Vec::new(),
99		}
100	}
101	/// Add an operation to this migration
102	///
103	/// # Examples
104	///
105	/// ```rust,ignore
106	/// use reinhardt_db::migrations::{Migration, Operation, ColumnDefinition, FieldType};
107	///
108	/// let migration = Migration::new("0001_initial", "myapp")
109	///     .add_operation(Operation::CreateTable {
110	///         name: "users".to_string(),
111	///         columns: vec![ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string()))],
112	///         constraints: vec![],
113	///         without_rowid: None,
114	///         interleave_in_parent: None,
115	///         partition: None,
116	///     });
117	///
118	/// assert_eq!(migration.operations.len(), 1);
119	/// ```
120	pub fn add_operation(mut self, operation: Operation) -> Self {
121		self.operations.push(operation);
122		self
123	}
124	/// Add a dependency to this migration
125	///
126	/// # Examples
127	///
128	/// ```rust,ignore
129	/// use reinhardt_db::migrations::Migration;
130	///
131	/// let migration = Migration::new("0002_add_field", "myapp")
132	///     .add_dependency("myapp", "0001_initial");
133	///
134	/// assert_eq!(migration.dependencies.len(), 1);
135	/// assert_eq!(migration.dependencies[0].0, "myapp");
136	/// assert_eq!(migration.dependencies[0].1, "0001_initial");
137	/// ```
138	pub fn add_dependency(mut self, app_label: impl Into<String>, name: impl Into<String>) -> Self {
139		self.dependencies.push((app_label.into(), name.into()));
140		self
141	}
142
143	/// Add a swappable dependency to this migration
144	///
145	/// Swappable dependencies resolve to different apps based on settings.
146	/// This is used for Django's AUTH_USER_MODEL pattern.
147	///
148	/// # Examples
149	///
150	/// ```rust,ignore
151	/// use reinhardt_db::migrations::Migration;
152	/// use reinhardt_db::migrations::dependency::SwappableDependency;
153	///
154	/// let migration = Migration::new("0001_create_profile", "profiles")
155	///     .add_swappable_dependency(SwappableDependency::new(
156	///         "AUTH_USER_MODEL",
157	///         "auth",
158	///         "User",
159	///         "0001_initial",
160	///     ));
161	///
162	/// assert_eq!(migration.swappable_dependencies.len(), 1);
163	/// ```
164	pub fn add_swappable_dependency(mut self, dependency: SwappableDependency) -> Self {
165		self.swappable_dependencies.push(dependency);
166		self
167	}
168
169	/// Add an optional dependency to this migration
170	///
171	/// Optional dependencies are only enforced when their condition is met.
172	/// This is useful for migrations that depend on optional features or apps.
173	///
174	/// # Examples
175	///
176	/// ```rust,ignore
177	/// use reinhardt_db::migrations::Migration;
178	/// use reinhardt_db::migrations::dependency::{OptionalDependency, DependencyCondition};
179	///
180	/// let migration = Migration::new("0002_add_location", "geo_app")
181	///     .add_optional_dependency(OptionalDependency::new(
182	///         "gis_extension",
183	///         "0001_enable_postgis",
184	///         DependencyCondition::AppInstalled("gis_extension".to_string()),
185	///     ));
186	///
187	/// assert_eq!(migration.optional_dependencies.len(), 1);
188	/// ```
189	pub fn add_optional_dependency(mut self, dependency: OptionalDependency) -> Self {
190		self.optional_dependencies.push(dependency);
191		self
192	}
193
194	/// Set whether this migration should run in a transaction
195	///
196	/// # Examples
197	///
198	/// ```rust,ignore
199	/// use reinhardt_db::migrations::Migration;
200	///
201	/// let migration = Migration::new("0001_initial", "myapp")
202	///     .atomic(false);
203	///
204	/// assert!(!migration.atomic);
205	/// ```
206	pub fn atomic(mut self, atomic: bool) -> Self {
207		self.atomic = atomic;
208		self
209	}
210	/// Get full migration identifier
211	///
212	/// # Examples
213	///
214	/// ```rust,ignore
215	/// use reinhardt_db::migrations::Migration;
216	///
217	/// let migration = Migration::new("0001_initial", "myapp");
218	/// assert_eq!(migration.id(), "myapp.0001_initial");
219	/// ```
220	pub fn id(&self) -> String {
221		format!("{}.{}", self.app_label, self.name)
222	}
223
224	/// Set initial attribute explicitly
225	///
226	/// # Examples
227	///
228	/// ```rust,ignore
229	/// use reinhardt_db::migrations::Migration;
230	///
231	/// let migration = Migration::new("0001_initial", "myapp")
232	///     .initial(true);
233	///
234	/// assert!(migration.is_initial());
235	/// ```
236	pub fn initial(mut self, initial: bool) -> Self {
237		self.initial = Some(initial);
238		self
239	}
240
241	/// Set whether to update only ProjectState without database operations
242	///
243	/// # Examples
244	///
245	/// ```rust,ignore
246	/// use reinhardt_db::migrations::Migration;
247	///
248	/// let migration = Migration::new("0001_state_sync", "myapp")
249	///     .state_only(true);
250	///
251	/// assert!(migration.state_only);
252	/// assert!(!migration.database_only);
253	/// ```
254	pub fn state_only(mut self, value: bool) -> Self {
255		self.state_only = value;
256		self
257	}
258
259	/// Set whether to execute only database operations without ProjectState updates
260	///
261	/// # Examples
262	///
263	/// ```rust,ignore
264	/// use reinhardt_db::migrations::Migration;
265	///
266	/// let migration = Migration::new("0001_db_only", "myapp")
267	///     .database_only(true);
268	///
269	/// assert!(migration.database_only);
270	/// assert!(!migration.state_only);
271	/// ```
272	pub fn database_only(mut self, value: bool) -> Self {
273		self.database_only = value;
274		self
275	}
276
277	/// Check if this is an initial migration
278	///
279	/// Returns `true` if:
280	/// - `initial` is explicitly set to `Some(true)`, OR
281	/// - `initial` is `None` and `dependencies` is empty
282	///
283	/// # Examples
284	///
285	/// ```rust,ignore
286	/// use reinhardt_db::migrations::Migration;
287	///
288	/// // Explicitly marked as initial
289	/// let migration1 = Migration::new("0001_initial", "myapp")
290	///     .initial(true);
291	/// assert!(migration1.is_initial());
292	///
293	/// // Auto-inferred from empty dependencies
294	/// let migration2 = Migration::new("0001_initial", "myapp");
295	/// assert!(migration2.is_initial());
296	///
297	/// // Has dependencies, not initial
298	/// let migration3 = Migration::new("0002_add_field", "myapp")
299	///     .add_dependency("myapp", "0001_initial");
300	/// assert!(!migration3.is_initial());
301	///
302	/// // Explicitly marked as non-initial
303	/// let migration4 = Migration::new("0001_custom", "myapp")
304	///     .initial(false);
305	/// assert!(!migration4.is_initial());
306	/// ```
307	pub fn is_initial(&self) -> bool {
308		match self.initial {
309			Some(true) => true,
310			Some(false) => false,
311			None => self.dependencies.is_empty(),
312		}
313	}
314}
315
316// Auto-generated tests for migrations module
317// Translated from Django/SQLAlchemy test suite
318// Total available: 1618 | Included: 100
319
320#[cfg(test)]
321mod migrations_extended_tests {
322	use crate::migrations::operations;
323	use crate::migrations::{FieldType, ForeignKeyAction};
324
325	#[test]
326	// From: Django/migrations
327	fn test_add_alter_order_with_respect_to() {
328		use crate::migrations::ProjectState;
329		use crate::migrations::operations::*;
330
331		let mut state = ProjectState::new();
332
333		// Create parent table
334		let create_categories = Operation::CreateTable {
335			name: "categories".to_string(),
336			columns: vec![
337				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
338				ColumnDefinition::new("name", FieldType::VarChar(100)),
339			],
340			constraints: vec![],
341			without_rowid: None,
342			partition: None,
343			interleave_in_parent: None,
344		};
345		create_categories.state_forwards("testapp", &mut state);
346
347		// Create child table with FK to parent
348		let create_items = Operation::CreateTable {
349			name: "items".to_string(),
350			columns: vec![
351				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
352				ColumnDefinition::new("name", FieldType::VarChar(200)),
353				ColumnDefinition::new(
354					"category_id",
355					FieldType::Custom("INTEGER REFERENCES categories(id)".to_string()),
356				),
357			],
358			constraints: vec![],
359			without_rowid: None,
360			partition: None,
361			interleave_in_parent: None,
362		};
363		create_items.state_forwards("testapp", &mut state);
364
365		// Add order_with_respect_to field (_order)
366		let add_order = Operation::AddColumn {
367			table: "items".to_string(),
368			column: ColumnDefinition::new(
369				"_order",
370				FieldType::Custom("INTEGER NOT NULL DEFAULT 0".to_string()),
371			),
372			mysql_options: None,
373		};
374		add_order.state_forwards("testapp", &mut state);
375
376		// Create composite index on (category_id, _order)
377		let _create_index = Operation::CreateIndex {
378			table: "items".to_string(),
379			columns: vec!["category_id".to_string(), "_order".to_string()],
380			unique: false,
381			index_type: None,
382			where_clause: None,
383			concurrently: false,
384			expressions: None,
385			mysql_options: None,
386			operator_class: None,
387		};
388
389		let model = state.get_model("testapp", "items").unwrap();
390		assert!(model.fields.contains_key("_order"));
391		assert!(model.fields.contains_key("category_id"));
392	}
393
394	#[test]
395	// From: Django/migrations
396	fn test_add_alter_order_with_respect_to_1() {
397		use crate::migrations::ProjectState;
398		use crate::migrations::operations::*;
399
400		let mut state = ProjectState::new();
401
402		// Create parent
403		let create_parent = Operation::CreateTable {
404			name: "authors".to_string(),
405			columns: vec![ColumnDefinition::new(
406				"id",
407				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
408			)],
409			constraints: vec![],
410			without_rowid: None,
411			partition: None,
412			interleave_in_parent: None,
413		};
414		create_parent.state_forwards("app", &mut state);
415
416		// Create child with FK
417		let create_child = Operation::CreateTable {
418			name: "books".to_string(),
419			columns: vec![
420				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
421				ColumnDefinition::new("title", FieldType::VarChar(255)),
422				ColumnDefinition::new(
423					"author_id",
424					FieldType::Custom("INTEGER REFERENCES authors(id)".to_string()),
425				),
426			],
427			constraints: vec![],
428			without_rowid: None,
429			partition: None,
430			interleave_in_parent: None,
431		};
432		create_child.state_forwards("app", &mut state);
433
434		// Add _order field for order_with_respect_to
435		let add_order = Operation::AddColumn {
436			table: "books".to_string(),
437			column: ColumnDefinition::new(
438				"_order",
439				FieldType::Custom("INTEGER NOT NULL DEFAULT 0".to_string()),
440			),
441			mysql_options: None,
442		};
443		add_order.state_forwards("app", &mut state);
444
445		assert!(
446			state
447				.get_model("app", "books")
448				.unwrap()
449				.fields
450				.contains_key("_order")
451		);
452	}
453
454	#[test]
455	// From: Django/migrations
456	fn test_add_auto_field_does_not_request_default() {
457		use crate::migrations::ProjectState;
458		use crate::migrations::operations::*;
459
460		let mut state = ProjectState::new();
461
462		let create_op = Operation::CreateTable {
463			name: "items".to_string(),
464			columns: vec![ColumnDefinition::new("name", FieldType::VarChar(255))],
465			constraints: vec![],
466			without_rowid: None,
467			partition: None,
468			interleave_in_parent: None,
469		};
470		create_op.state_forwards("testapp", &mut state);
471
472		// AutoField doesn't need default - it's auto-incrementing
473		let add_op = Operation::AddColumn {
474			table: "items".to_string(),
475			column: ColumnDefinition::new(
476				"id",
477				FieldType::Custom("INTEGER PRIMARY KEY AUTOINCREMENT".to_string()),
478			),
479			mysql_options: None,
480		};
481		add_op.state_forwards("testapp", &mut state);
482
483		assert!(
484			state
485				.get_model("testapp", "items")
486				.unwrap()
487				.fields
488				.contains_key("id")
489		);
490	}
491
492	#[test]
493	// From: Django/migrations
494	fn test_add_auto_field_does_not_request_default_1() {
495		use crate::migrations::ProjectState;
496		use crate::migrations::operations::*;
497
498		let mut state = ProjectState::new();
499
500		let create_op = Operation::CreateTable {
501			name: "entries".to_string(),
502			columns: vec![ColumnDefinition::new("title", FieldType::Text)],
503			constraints: vec![],
504			without_rowid: None,
505			partition: None,
506			interleave_in_parent: None,
507		};
508		create_op.state_forwards("app", &mut state);
509
510		let add_op = Operation::AddColumn {
511			table: "entries".to_string(),
512			column: ColumnDefinition::new(
513				"entry_id",
514				FieldType::Custom("SERIAL PRIMARY KEY".to_string()),
515			),
516			mysql_options: None,
517		};
518		add_op.state_forwards("app", &mut state);
519
520		assert!(state.get_model("app", "entries").is_some());
521	}
522
523	#[test]
524	// From: Django/migrations
525	fn test_add_blank_textfield_and_charfield() {
526		use crate::migrations::ProjectState;
527		use crate::migrations::operations::*;
528
529		let mut state = ProjectState::new();
530
531		let create_op = Operation::CreateTable {
532			name: "articles".to_string(),
533			columns: vec![ColumnDefinition::new(
534				"id",
535				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
536			)],
537			constraints: vec![],
538			without_rowid: None,
539			partition: None,
540			interleave_in_parent: None,
541		};
542		create_op.state_forwards("testapp", &mut state);
543
544		// Add blank=True text fields (nullable)
545		let add_text = Operation::AddColumn {
546			table: "articles".to_string(),
547			column: ColumnDefinition::new("content", FieldType::Text),
548			mysql_options: None,
549		};
550		add_text.state_forwards("testapp", &mut state);
551
552		let add_char = Operation::AddColumn {
553			table: "articles".to_string(),
554			column: ColumnDefinition::new("title", FieldType::VarChar(255)),
555			mysql_options: None,
556		};
557		add_char.state_forwards("testapp", &mut state);
558
559		let model = state.get_model("testapp", "articles").unwrap();
560		assert!(model.fields.contains_key("content"));
561		assert!(model.fields.contains_key("title"));
562	}
563
564	#[test]
565	// From: Django/migrations
566	fn test_add_blank_textfield_and_charfield_1() {
567		use crate::migrations::ProjectState;
568		use crate::migrations::operations::*;
569
570		let mut state = ProjectState::new();
571
572		let create_op = Operation::CreateTable {
573			name: "posts".to_string(),
574			columns: vec![ColumnDefinition::new(
575				"id",
576				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
577			)],
578			constraints: vec![],
579			without_rowid: None,
580			partition: None,
581			interleave_in_parent: None,
582		};
583		create_op.state_forwards("app", &mut state);
584
585		let add_op = Operation::AddColumn {
586			table: "posts".to_string(),
587			column: ColumnDefinition::new(
588				"description",
589				FieldType::Custom("TEXT NULL".to_string()),
590			),
591			mysql_options: None,
592		};
593		add_op.state_forwards("app", &mut state);
594
595		assert!(
596			state
597				.get_model("app", "posts")
598				.unwrap()
599				.fields
600				.contains_key("description")
601		);
602	}
603
604	#[test]
605	// From: Django/migrations
606	fn test_add_composite_pk() {
607		use crate::migrations::ProjectState;
608		use crate::migrations::operations::*;
609
610		let mut state = ProjectState::new();
611
612		// Create table with composite primary key
613		// Note: Composite primary keys are handled via column definitions, not constraints
614		let create_op = Operation::CreateTable {
615			name: "order_items".to_string(),
616			columns: vec![
617				ColumnDefinition::new("order_id", FieldType::Integer),
618				ColumnDefinition::new("product_id", FieldType::Integer),
619				ColumnDefinition::new("quantity", FieldType::Integer),
620			],
621			constraints: vec![],
622			without_rowid: None,
623			partition: None,
624			interleave_in_parent: None,
625		};
626		create_op.state_forwards("testapp", &mut state);
627
628		let model = state.get_model("testapp", "order_items").unwrap();
629		assert!(model.fields.contains_key("order_id"));
630		assert!(model.fields.contains_key("product_id"));
631	}
632
633	#[test]
634	// From: Django/migrations
635	fn test_add_composite_pk_1() {
636		use crate::migrations::ProjectState;
637		use crate::migrations::operations::*;
638
639		let mut state = ProjectState::new();
640
641		// Note: Composite primary keys are handled via column definitions, not constraints
642		let create_op = Operation::CreateTable {
643			name: "user_roles".to_string(),
644			columns: vec![
645				ColumnDefinition::new("user_id", FieldType::Integer),
646				ColumnDefinition::new("role_id", FieldType::Integer),
647			],
648			constraints: vec![],
649			without_rowid: None,
650			partition: None,
651			interleave_in_parent: None,
652		};
653		create_op.state_forwards("app", &mut state);
654
655		assert!(state.get_model("app", "user_roles").is_some());
656	}
657
658	#[test]
659	// From: Django/migrations
660	fn test_add_constraints() {
661		use crate::migrations::operations::*;
662
663		// Test AddConstraint operation SQL generation
664		let op = Operation::AddConstraint {
665			table: "users".to_string(),
666			constraint_sql: "CHECK (age >= 18)".to_string(),
667		};
668
669		let sql = op.to_sql(&SqlDialect::Postgres);
670		assert!(sql.contains("ALTER TABLE users"));
671		assert!(sql.contains("ADD CHECK (age >= 18)"));
672	}
673
674	#[test]
675	// From: Django/migrations
676	fn test_add_constraints_1() {
677		use crate::migrations::operations::*;
678
679		// Test adding a unique constraint
680		let op = Operation::AddConstraint {
681			table: "products".to_string(),
682			constraint_sql: "UNIQUE (sku)".to_string(),
683		};
684
685		let sql = op.to_sql(&SqlDialect::Postgres);
686		assert!(sql.contains("ALTER TABLE products"));
687		assert!(sql.contains("ADD UNIQUE (sku)"));
688	}
689
690	#[test]
691	// From: Django/migrations
692	fn test_add_constraints_with_dict_keys() {
693		use crate::migrations::ProjectState;
694		use crate::migrations::operations::*;
695
696		let mut state = ProjectState::new();
697
698		let create_op = Operation::CreateTable {
699			name: "products".to_string(),
700			columns: vec![
701				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
702				ColumnDefinition::new(
703					"price",
704					FieldType::Decimal {
705						precision: 10,
706						scale: 2,
707					},
708				),
709				ColumnDefinition::new(
710					"discount_price",
711					FieldType::Decimal {
712						precision: 10,
713						scale: 2,
714					},
715				),
716			],
717			constraints: vec![
718				Constraint::Check {
719					name: "price_positive".to_string(),
720					expression: "price >= 0".to_string(),
721				},
722				Constraint::Check {
723					name: "discount_price_valid".to_string(),
724					expression: "discount_price <= price".to_string(),
725				},
726			],
727			without_rowid: None,
728			partition: None,
729			interleave_in_parent: None,
730		};
731		create_op.state_forwards("testapp", &mut state);
732
733		let model = state.get_model("testapp", "products").unwrap();
734		assert!(model.fields.contains_key("price"));
735		assert!(model.fields.contains_key("discount_price"));
736	}
737
738	#[test]
739	// From: Django/migrations
740	fn test_add_constraints_with_dict_keys_1() {
741		use crate::migrations::ProjectState;
742		use crate::migrations::operations::*;
743
744		let mut state = ProjectState::new();
745
746		let create_op = Operation::CreateTable {
747			name: "users".to_string(),
748			columns: vec![
749				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
750				ColumnDefinition::new("age", FieldType::Integer),
751			],
752			constraints: vec![Constraint::Check {
753				name: "age_valid_range".to_string(),
754				expression: "age >= 0 AND age <= 150".to_string(),
755			}],
756			without_rowid: None,
757			partition: None,
758			interleave_in_parent: None,
759		};
760		create_op.state_forwards("app", &mut state);
761
762		assert!(state.get_model("app", "users").is_some());
763	}
764
765	#[test]
766	// From: Django/migrations
767	fn test_add_constraints_with_new_model() {
768		use crate::migrations::ProjectState;
769		use crate::migrations::operations::*;
770
771		let mut state = ProjectState::new();
772
773		// Create a table with constraints
774		let create_op = Operation::CreateTable {
775			name: "users".to_string(),
776			columns: vec![
777				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
778				ColumnDefinition::new("age", FieldType::Integer),
779			],
780			constraints: vec![Constraint::Check {
781				name: "age_adult".to_string(),
782				expression: "age >= 18".to_string(),
783			}],
784			without_rowid: None,
785			partition: None,
786			interleave_in_parent: None,
787		};
788		create_op.state_forwards("testapp", &mut state);
789
790		let model = state.get_model("testapp", "users").unwrap();
791		assert!(model.fields.contains_key("id"));
792		assert!(model.fields.contains_key("age"));
793	}
794
795	#[test]
796	// From: Django/migrations
797	fn test_add_constraints_with_new_model_1() {
798		use crate::migrations::ProjectState;
799		use crate::migrations::operations::*;
800
801		let mut state = ProjectState::new();
802
803		let create_op = Operation::CreateTable {
804			name: "products".to_string(),
805			columns: vec![
806				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
807				ColumnDefinition::new(
808					"price",
809					FieldType::Decimal {
810						precision: 10,
811						scale: 2,
812					},
813				),
814			],
815			constraints: vec![Constraint::Check {
816				name: "price_positive".to_string(),
817				expression: "price > 0".to_string(),
818			}],
819			without_rowid: None,
820			partition: None,
821			interleave_in_parent: None,
822		};
823		create_op.state_forwards("app", &mut state);
824
825		assert!(state.get_model("app", "products").is_some());
826	}
827
828	#[test]
829	// From: Django/migrations
830	fn test_add_custom_fk_with_hardcoded_to() {
831		use crate::migrations::ProjectState;
832		use crate::migrations::operations::*;
833
834		let mut state = ProjectState::new();
835
836		// Create referenced table first
837		let create_users = Operation::CreateTable {
838			name: "users".to_string(),
839			columns: vec![ColumnDefinition::new(
840				"id",
841				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
842			)],
843			constraints: vec![],
844			without_rowid: None,
845			partition: None,
846			interleave_in_parent: None,
847		};
848		create_users.state_forwards("testapp", &mut state);
849
850		// Create table with FK
851		let create_posts = Operation::CreateTable {
852			name: "posts".to_string(),
853			columns: vec![
854				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
855				ColumnDefinition::new("author_id", FieldType::Integer),
856			],
857			constraints: vec![Constraint::ForeignKey {
858				name: "fk_posts_author".to_string(),
859				columns: vec!["author_id".to_string()],
860				referenced_table: "users".to_string(),
861				referenced_columns: vec!["id".to_string()],
862				on_delete: ForeignKeyAction::Cascade,
863				on_update: ForeignKeyAction::Cascade,
864				deferrable: None,
865			}],
866			without_rowid: None,
867			interleave_in_parent: None,
868			partition: None,
869		};
870		create_posts.state_forwards("testapp", &mut state);
871
872		assert!(
873			state
874				.get_model("testapp", "posts")
875				.unwrap()
876				.fields
877				.contains_key("author_id")
878		);
879	}
880
881	#[test]
882	// From: Django/migrations
883	fn test_add_custom_fk_with_hardcoded_to_1() {
884		use crate::migrations::ProjectState;
885		use crate::migrations::operations::*;
886
887		let mut state = ProjectState::new();
888
889		let create_categories = Operation::CreateTable {
890			name: "categories".to_string(),
891			columns: vec![ColumnDefinition::new(
892				"id",
893				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
894			)],
895			constraints: vec![],
896			without_rowid: None,
897			partition: None,
898			interleave_in_parent: None,
899		};
900		create_categories.state_forwards("app", &mut state);
901
902		let create_products = Operation::CreateTable {
903			name: "products".to_string(),
904			columns: vec![
905				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
906				ColumnDefinition::new(
907					"category_id",
908					FieldType::Custom("INTEGER REFERENCES categories(id)".to_string()),
909				),
910			],
911			constraints: vec![],
912			without_rowid: None,
913			partition: None,
914			interleave_in_parent: None,
915		};
916		create_products.state_forwards("app", &mut state);
917
918		assert!(state.get_model("app", "products").is_some());
919	}
920
921	#[test]
922	// From: Django/migrations
923	fn test_add_date_fields_with_auto_now_add_asking_for_default() {
924		use crate::migrations::ProjectState;
925		use crate::migrations::operations::*;
926
927		let mut state = ProjectState::new();
928
929		let create_op = Operation::CreateTable {
930			name: "posts".to_string(),
931			columns: vec![ColumnDefinition::new(
932				"id",
933				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
934			)],
935			constraints: vec![],
936			without_rowid: None,
937			partition: None,
938			interleave_in_parent: None,
939		};
940		create_op.state_forwards("testapp", &mut state);
941
942		// auto_now_add simulated with DEFAULT CURRENT_TIMESTAMP
943		let add_op = Operation::AddColumn {
944			table: "posts".to_string(),
945			column: ColumnDefinition::new(
946				"created_at",
947				FieldType::Custom(
948					FieldType::Custom("TIMESTAMP DEFAULT CURRENT_TIMESTAMP".to_string())
949						.to_string(),
950				),
951			),
952			mysql_options: None,
953		};
954		add_op.state_forwards("testapp", &mut state);
955
956		assert!(
957			state
958				.get_model("testapp", "posts")
959				.unwrap()
960				.fields
961				.contains_key("created_at")
962		);
963	}
964
965	#[test]
966	// From: Django/migrations
967	fn test_add_date_fields_with_auto_now_add_asking_for_default_1() {
968		use crate::migrations::ProjectState;
969		use crate::migrations::operations::*;
970
971		let mut state = ProjectState::new();
972
973		let create_op = Operation::CreateTable {
974			name: "articles".to_string(),
975			columns: vec![ColumnDefinition::new(
976				"id",
977				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
978			)],
979			constraints: vec![],
980			without_rowid: None,
981			partition: None,
982			interleave_in_parent: None,
983		};
984		create_op.state_forwards("app", &mut state);
985
986		let add_op = Operation::AddColumn {
987			table: "articles".to_string(),
988			column: ColumnDefinition::new(
989				"published_at",
990				FieldType::Custom("TIMESTAMP DEFAULT NOW()".to_string()),
991			),
992			mysql_options: None,
993		};
994		add_op.state_forwards("app", &mut state);
995
996		assert!(state.get_model("app", "articles").is_some());
997	}
998
999	#[test]
1000	// From: Django/migrations
1001	fn test_add_date_fields_with_auto_now_add_not_asking_for_null_addition() {
1002		use crate::migrations::ProjectState;
1003		use crate::migrations::operations::*;
1004
1005		let mut state = ProjectState::new();
1006
1007		let create_op = Operation::CreateTable {
1008			name: "events".to_string(),
1009			columns: vec![ColumnDefinition::new(
1010				"id",
1011				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
1012			)],
1013			constraints: vec![],
1014			without_rowid: None,
1015			partition: None,
1016			interleave_in_parent: None,
1017		};
1018		create_op.state_forwards("testapp", &mut state);
1019
1020		// auto_now_add with NOT NULL
1021		let add_op = Operation::AddColumn {
1022			table: "events".to_string(),
1023			column: ColumnDefinition::new(
1024				"created_at",
1025				FieldType::Custom("TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP".to_string()),
1026			),
1027			mysql_options: None,
1028		};
1029		add_op.state_forwards("testapp", &mut state);
1030
1031		assert!(
1032			state
1033				.get_model("testapp", "events")
1034				.unwrap()
1035				.fields
1036				.contains_key("created_at")
1037		);
1038	}
1039
1040	#[test]
1041	// From: Django/migrations
1042	fn test_add_date_fields_with_auto_now_add_not_asking_for_null_addition_1() {
1043		use crate::migrations::ProjectState;
1044		use crate::migrations::operations::*;
1045
1046		let mut state = ProjectState::new();
1047
1048		let create_op = Operation::CreateTable {
1049			name: "logs".to_string(),
1050			columns: vec![ColumnDefinition::new(
1051				"id",
1052				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
1053			)],
1054			constraints: vec![],
1055			without_rowid: None,
1056			partition: None,
1057			interleave_in_parent: None,
1058		};
1059		create_op.state_forwards("app", &mut state);
1060
1061		let add_op = Operation::AddColumn {
1062			table: "logs".to_string(),
1063			column: ColumnDefinition::new(
1064				"timestamp",
1065				FieldType::Custom("DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP".to_string()),
1066			),
1067			mysql_options: None,
1068		};
1069		add_op.state_forwards("app", &mut state);
1070
1071		assert!(state.get_model("app", "logs").is_some());
1072	}
1073
1074	#[test]
1075	// From: Django/migrations
1076	fn test_add_date_fields_with_auto_now_not_asking_for_default() {
1077		use crate::migrations::ProjectState;
1078		use crate::migrations::operations::*;
1079
1080		let mut state = ProjectState::new();
1081
1082		let create_op = Operation::CreateTable {
1083			name: "records".to_string(),
1084			columns: vec![ColumnDefinition::new(
1085				"id",
1086				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
1087			)],
1088			constraints: vec![],
1089			without_rowid: None,
1090			partition: None,
1091			interleave_in_parent: None,
1092		};
1093		create_op.state_forwards("testapp", &mut state);
1094
1095		// auto_now typically uses triggers or application-level updates
1096		// For migration testing, we just add the field
1097		let add_op = Operation::AddColumn {
1098			table: "records".to_string(),
1099			column: ColumnDefinition::new("updated_at", FieldType::Custom("TIMESTAMP".to_string())),
1100			mysql_options: None,
1101		};
1102		add_op.state_forwards("testapp", &mut state);
1103
1104		assert!(
1105			state
1106				.get_model("testapp", "records")
1107				.unwrap()
1108				.fields
1109				.contains_key("updated_at")
1110		);
1111	}
1112
1113	#[test]
1114	// From: Django/migrations
1115	fn test_add_date_fields_with_auto_now_not_asking_for_default_1() {
1116		use crate::migrations::ProjectState;
1117		use crate::migrations::operations::*;
1118
1119		let mut state = ProjectState::new();
1120
1121		let create_op = Operation::CreateTable {
1122			name: "profiles".to_string(),
1123			columns: vec![ColumnDefinition::new(
1124				"id",
1125				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
1126			)],
1127			constraints: vec![],
1128			without_rowid: None,
1129			partition: None,
1130			interleave_in_parent: None,
1131		};
1132		create_op.state_forwards("app", &mut state);
1133
1134		let add_op = Operation::AddColumn {
1135			table: "profiles".to_string(),
1136			column: ColumnDefinition::new("modified", FieldType::Custom("DATETIME".to_string())),
1137			mysql_options: None,
1138		};
1139		add_op.state_forwards("app", &mut state);
1140
1141		assert!(state.get_model("app", "profiles").is_some());
1142	}
1143
1144	#[test]
1145	// From: Django/migrations
1146	fn test_add_field() {
1147		use crate::migrations::ProjectState;
1148		use crate::migrations::operations::*;
1149
1150		let mut state = ProjectState::new();
1151
1152		// Create a table first
1153		let create_op = Operation::CreateTable {
1154			name: "test_table".to_string(),
1155			columns: vec![ColumnDefinition::new(
1156				"id",
1157				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
1158			)],
1159			constraints: vec![],
1160			without_rowid: None,
1161			partition: None,
1162			interleave_in_parent: None,
1163		};
1164		create_op.state_forwards("testapp", &mut state);
1165
1166		// Add a field
1167		let add_op = Operation::AddColumn {
1168			table: "test_table".to_string(),
1169			column: ColumnDefinition::new("name", FieldType::VarChar(255)),
1170			mysql_options: None,
1171		};
1172		add_op.state_forwards("testapp", &mut state);
1173
1174		let model = state.get_model("testapp", "test_table").unwrap();
1175		assert!(model.fields.contains_key("name"));
1176	}
1177
1178	#[test]
1179	// From: Django/migrations
1180	fn test_add_field_1() {
1181		use crate::migrations::ProjectState;
1182		use crate::migrations::operations::*;
1183
1184		let mut state = ProjectState::new();
1185
1186		let create_op = Operation::CreateTable {
1187			name: "users".to_string(),
1188			columns: vec![ColumnDefinition::new(
1189				"id",
1190				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
1191			)],
1192			constraints: vec![],
1193			without_rowid: None,
1194			partition: None,
1195			interleave_in_parent: None,
1196		};
1197		create_op.state_forwards("app", &mut state);
1198
1199		let add_op = Operation::AddColumn {
1200			table: "users".to_string(),
1201			column: ColumnDefinition::new("email", FieldType::VarChar(255)),
1202			mysql_options: None,
1203		};
1204		add_op.state_forwards("app", &mut state);
1205
1206		assert!(
1207			state
1208				.get_model("app", "users")
1209				.unwrap()
1210				.fields
1211				.contains_key("email")
1212		);
1213	}
1214
1215	#[test]
1216	// From: Django/migrations
1217	fn test_add_field_and_unique_together() {
1218		use crate::migrations::ProjectState;
1219		use crate::migrations::operations::*;
1220
1221		let mut state = ProjectState::new();
1222
1223		let create_op = Operation::CreateTable {
1224			name: "users".to_string(),
1225			columns: vec![
1226				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1227				ColumnDefinition::new("email", FieldType::VarChar(255)),
1228			],
1229			constraints: vec![],
1230			without_rowid: None,
1231			partition: None,
1232			interleave_in_parent: None,
1233		};
1234		create_op.state_forwards("app", &mut state);
1235
1236		let add_op = Operation::AddColumn {
1237			table: "users".to_string(),
1238			column: ColumnDefinition::new("username", FieldType::VarChar(100)),
1239			mysql_options: None,
1240		};
1241		add_op.state_forwards("app", &mut state);
1242
1243		let unique_op = Operation::AlterUniqueTogether {
1244			table: "users".to_string(),
1245			unique_together: vec![vec!["email".to_string(), "username".to_string()]],
1246		};
1247		unique_op.state_forwards("app", &mut state);
1248
1249		assert!(
1250			state
1251				.get_model("app", "users")
1252				.unwrap()
1253				.fields
1254				.contains_key("username")
1255		);
1256	}
1257
1258	#[test]
1259	// From: Django/migrations
1260	fn test_add_field_and_unique_together_1() {
1261		use crate::migrations::ProjectState;
1262		use crate::migrations::operations::*;
1263
1264		let mut state = ProjectState::new();
1265
1266		let create_op = Operation::CreateTable {
1267			name: "posts".to_string(),
1268			columns: vec![
1269				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1270				ColumnDefinition::new("title", FieldType::VarChar(255)),
1271			],
1272			constraints: vec![],
1273			without_rowid: None,
1274			partition: None,
1275			interleave_in_parent: None,
1276		};
1277		create_op.state_forwards("app", &mut state);
1278
1279		let add_op = Operation::AddColumn {
1280			table: "posts".to_string(),
1281			column: ColumnDefinition::new("slug", FieldType::VarChar(255)),
1282			mysql_options: None,
1283		};
1284		add_op.state_forwards("app", &mut state);
1285
1286		let unique_op = Operation::AlterUniqueTogether {
1287			table: "posts".to_string(),
1288			unique_together: vec![vec!["slug".to_string()]],
1289		};
1290		unique_op.state_forwards("app", &mut state);
1291
1292		assert!(
1293			state
1294				.get_model("app", "posts")
1295				.unwrap()
1296				.fields
1297				.contains_key("slug")
1298		);
1299	}
1300
1301	#[test]
1302	// From: Django/migrations
1303	fn test_add_field_before_generated_field() {
1304		use crate::migrations::ProjectState;
1305		use crate::migrations::operations::*;
1306
1307		let mut state = ProjectState::new();
1308
1309		// Create a table
1310		let create_op = Operation::CreateTable {
1311			name: "products".to_string(),
1312			columns: vec![
1313				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1314				ColumnDefinition::new(
1315					"price",
1316					FieldType::Decimal {
1317						precision: 10,
1318						scale: 2,
1319					},
1320				),
1321				ColumnDefinition::new("quantity", FieldType::Integer),
1322			],
1323			constraints: vec![],
1324			without_rowid: None,
1325			partition: None,
1326			interleave_in_parent: None,
1327		};
1328		create_op.state_forwards("testapp", &mut state);
1329
1330		// Add a regular field before adding a generated field
1331		let add_discount = Operation::AddColumn {
1332			table: "products".to_string(),
1333			column: ColumnDefinition::new(
1334				"discount",
1335				FieldType::Custom("DECIMAL(10,2) DEFAULT 0".to_string()),
1336			),
1337			mysql_options: None,
1338		};
1339		add_discount.state_forwards("testapp", &mut state);
1340
1341		// Add a generated field (total = price * quantity)
1342		// Generated columns are supported using GENERATED ALWAYS AS syntax
1343		let add_generated = Operation::AddColumn {
1344			table: "products".to_string(),
1345			column: ColumnDefinition::new(
1346				"total",
1347				FieldType::Custom(
1348					"DECIMAL(10,2) GENERATED ALWAYS AS (price * quantity) STORED".to_string(),
1349				),
1350			),
1351			mysql_options: None,
1352		};
1353		add_generated.state_forwards("testapp", &mut state);
1354
1355		let model = state.get_model("testapp", "products").unwrap();
1356		assert!(model.fields.contains_key("discount"));
1357		assert!(model.fields.contains_key("total"));
1358	}
1359
1360	#[test]
1361	// From: Django/migrations
1362	fn test_add_field_before_generated_field_1() {
1363		use crate::migrations::ProjectState;
1364		use crate::migrations::operations::*;
1365
1366		let mut state = ProjectState::new();
1367
1368		let create_op = Operation::CreateTable {
1369			name: "users".to_string(),
1370			columns: vec![
1371				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1372				ColumnDefinition::new("first_name", FieldType::VarChar(100)),
1373				ColumnDefinition::new("last_name", FieldType::VarChar(100)),
1374			],
1375			constraints: vec![],
1376			without_rowid: None,
1377			partition: None,
1378			interleave_in_parent: None,
1379		};
1380		create_op.state_forwards("app", &mut state);
1381
1382		// Add regular field
1383		let add_email = Operation::AddColumn {
1384			table: "users".to_string(),
1385			column: ColumnDefinition::new("email", FieldType::VarChar(255)),
1386			mysql_options: None,
1387		};
1388		add_email.state_forwards("app", &mut state);
1389
1390		// Add generated field (full_name = first_name || ' ' || last_name)
1391		let add_generated = Operation::AddColumn {
1392			table: "users".to_string(),
1393			column: ColumnDefinition::new(
1394				"full_name",
1395				FieldType::Custom(
1396					"VARCHAR(200) GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED"
1397						.to_string(),
1398				),
1399			),
1400			mysql_options: None,
1401		};
1402		add_generated.state_forwards("app", &mut state);
1403
1404		assert!(
1405			state
1406				.get_model("app", "users")
1407				.unwrap()
1408				.fields
1409				.contains_key("full_name")
1410		);
1411	}
1412
1413	#[test]
1414	// From: Django/migrations
1415	fn test_add_field_with_default() {
1416		use crate::migrations::ProjectState;
1417		use crate::migrations::operations::*;
1418
1419		let mut state = ProjectState::new();
1420
1421		// Create a table
1422		let create_op = Operation::CreateTable {
1423			name: "users".to_string(),
1424			columns: vec![ColumnDefinition::new(
1425				"id",
1426				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
1427			)],
1428			constraints: vec![],
1429			without_rowid: None,
1430			partition: None,
1431			interleave_in_parent: None,
1432		};
1433		create_op.state_forwards("testapp", &mut state);
1434
1435		// Add a field with default value in type definition
1436		let add_op = Operation::AddColumn {
1437			table: "users".to_string(),
1438			column: ColumnDefinition::new(
1439				"status",
1440				FieldType::Custom("VARCHAR(50) DEFAULT 'active'".to_string()),
1441			),
1442			mysql_options: None,
1443		};
1444		add_op.state_forwards("testapp", &mut state);
1445
1446		let model = state.get_model("testapp", "users").unwrap();
1447		assert!(model.fields.contains_key("status"));
1448	}
1449
1450	#[test]
1451	// From: Django/migrations
1452	fn test_add_field_with_default_1() {
1453		use crate::migrations::ProjectState;
1454		use crate::migrations::operations::*;
1455
1456		let mut state = ProjectState::new();
1457
1458		let create_op = Operation::CreateTable {
1459			name: "products".to_string(),
1460			columns: vec![ColumnDefinition::new(
1461				"id",
1462				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
1463			)],
1464			constraints: vec![],
1465			without_rowid: None,
1466			partition: None,
1467			interleave_in_parent: None,
1468		};
1469		create_op.state_forwards("app", &mut state);
1470
1471		let add_op = Operation::AddColumn {
1472			table: "products".to_string(),
1473			column: ColumnDefinition::new(
1474				"price",
1475				FieldType::Custom("DECIMAL(10,2) DEFAULT 0.00".to_string()),
1476			),
1477			mysql_options: None,
1478		};
1479		add_op.state_forwards("app", &mut state);
1480
1481		assert!(
1482			state
1483				.get_model("app", "products")
1484				.unwrap()
1485				.fields
1486				.contains_key("price")
1487		);
1488	}
1489
1490	#[test]
1491	// From: Django/migrations
1492	fn test_add_fk_before_generated_field() {
1493		use crate::migrations::ProjectState;
1494		use crate::migrations::operations::*;
1495
1496		let mut state = ProjectState::new();
1497
1498		// Create referenced table
1499		let create_categories = Operation::CreateTable {
1500			name: "categories".to_string(),
1501			columns: vec![
1502				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1503				ColumnDefinition::new("name", FieldType::VarChar(100)),
1504			],
1505			constraints: vec![],
1506			without_rowid: None,
1507			partition: None,
1508			interleave_in_parent: None,
1509		};
1510		create_categories.state_forwards("testapp", &mut state);
1511
1512		// Create main table
1513		let create_products = Operation::CreateTable {
1514			name: "products".to_string(),
1515			columns: vec![
1516				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1517				ColumnDefinition::new("name", FieldType::VarChar(200)),
1518				ColumnDefinition::new(
1519					"price",
1520					FieldType::Decimal {
1521						precision: 10,
1522						scale: 2,
1523					},
1524				),
1525			],
1526			constraints: vec![],
1527			without_rowid: None,
1528			partition: None,
1529			interleave_in_parent: None,
1530		};
1531		create_products.state_forwards("testapp", &mut state);
1532
1533		// Add FK field
1534		let add_fk = Operation::AddColumn {
1535			table: "products".to_string(),
1536			column: ColumnDefinition::new(
1537				"category_id",
1538				FieldType::Custom("INTEGER REFERENCES categories(id)".to_string()),
1539			),
1540			mysql_options: None,
1541		};
1542		add_fk.state_forwards("testapp", &mut state);
1543
1544		// Add generated field that uses the FK
1545		let add_generated = Operation::AddColumn {
1546			table: "products".to_string(),
1547			column: ColumnDefinition::new(
1548				"display_price",
1549				FieldType::Custom(
1550					"VARCHAR(50) GENERATED ALWAYS AS ('$' || CAST(price AS TEXT)) STORED"
1551						.to_string(),
1552				),
1553			),
1554			mysql_options: None,
1555		};
1556		add_generated.state_forwards("testapp", &mut state);
1557
1558		let model = state.get_model("testapp", "products").unwrap();
1559		assert!(model.fields.contains_key("category_id"));
1560		assert!(model.fields.contains_key("display_price"));
1561	}
1562
1563	#[test]
1564	// From: Django/migrations
1565	fn test_add_fk_before_generated_field_1() {
1566		use crate::migrations::ProjectState;
1567		use crate::migrations::operations::*;
1568
1569		let mut state = ProjectState::new();
1570
1571		let create_users = Operation::CreateTable {
1572			name: "users".to_string(),
1573			columns: vec![ColumnDefinition::new(
1574				"id",
1575				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
1576			)],
1577			constraints: vec![],
1578			without_rowid: None,
1579			partition: None,
1580			interleave_in_parent: None,
1581		};
1582		create_users.state_forwards("app", &mut state);
1583
1584		let create_orders = Operation::CreateTable {
1585			name: "orders".to_string(),
1586			columns: vec![
1587				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1588				ColumnDefinition::new(
1589					"total",
1590					FieldType::Decimal {
1591						precision: 10,
1592						scale: 2,
1593					},
1594				),
1595			],
1596			constraints: vec![],
1597			without_rowid: None,
1598			partition: None,
1599			interleave_in_parent: None,
1600		};
1601		create_orders.state_forwards("app", &mut state);
1602
1603		// Add FK
1604		let add_fk = Operation::AddColumn {
1605			table: "orders".to_string(),
1606			column: ColumnDefinition::new(
1607				"user_id",
1608				FieldType::Custom("INTEGER REFERENCES users(id)".to_string()),
1609			),
1610			mysql_options: None,
1611		};
1612		add_fk.state_forwards("app", &mut state);
1613
1614		// Add generated field
1615		let add_generated = Operation::AddColumn {
1616			table: "orders".to_string(),
1617			column: ColumnDefinition::new(
1618				"total_with_tax",
1619				FieldType::Custom(
1620					"DECIMAL(10,2) GENERATED ALWAYS AS (total * 1.1) STORED".to_string(),
1621				),
1622			),
1623			mysql_options: None,
1624		};
1625		add_generated.state_forwards("app", &mut state);
1626
1627		assert!(state.get_model("app", "orders").is_some());
1628	}
1629
1630	#[test]
1631	// From: Django/migrations
1632	fn test_add_index_with_new_model() {
1633		use crate::migrations::ProjectState;
1634		use crate::migrations::operations::*;
1635
1636		let mut state = ProjectState::new();
1637
1638		// Create a table
1639		let create_op = Operation::CreateTable {
1640			name: "users".to_string(),
1641			columns: vec![
1642				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1643				ColumnDefinition::new("email", FieldType::VarChar(255)),
1644			],
1645			constraints: vec![],
1646			without_rowid: None,
1647			partition: None,
1648			interleave_in_parent: None,
1649		};
1650		create_op.state_forwards("testapp", &mut state);
1651
1652		// Add an index (doesn't affect state but generates SQL)
1653		let index_op = Operation::CreateIndex {
1654			table: "users".to_string(),
1655			columns: vec!["email".to_string()],
1656			unique: true,
1657			index_type: None,
1658			where_clause: None,
1659			concurrently: false,
1660			expressions: None,
1661			mysql_options: None,
1662			operator_class: None,
1663		};
1664		let sql = index_op.to_sql(&operations::SqlDialect::Postgres);
1665
1666		assert!(sql.contains("CREATE UNIQUE INDEX"));
1667		assert!(state.get_model("testapp", "users").is_some());
1668	}
1669
1670	#[test]
1671	// From: Django/migrations
1672	fn test_add_index_with_new_model_1() {
1673		use crate::migrations::ProjectState;
1674		use crate::migrations::operations::*;
1675
1676		let mut state = ProjectState::new();
1677
1678		let create_op = Operation::CreateTable {
1679			name: "products".to_string(),
1680			columns: vec![
1681				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1682				ColumnDefinition::new("sku", FieldType::VarChar(100)),
1683			],
1684			constraints: vec![],
1685			without_rowid: None,
1686			partition: None,
1687			interleave_in_parent: None,
1688		};
1689		create_op.state_forwards("app", &mut state);
1690
1691		let index_op = Operation::CreateIndex {
1692			table: "products".to_string(),
1693			columns: vec!["sku".to_string()],
1694			unique: true,
1695			index_type: None,
1696			where_clause: None,
1697			concurrently: false,
1698			expressions: None,
1699			mysql_options: None,
1700			operator_class: None,
1701		};
1702		let sql = index_op.to_sql(&operations::SqlDialect::Sqlite);
1703
1704		assert!(sql.contains("CREATE UNIQUE INDEX"));
1705		assert!(state.get_model("app", "products").is_some());
1706	}
1707
1708	#[test]
1709	// From: Django/migrations
1710	fn test_add_indexes() {
1711		use crate::migrations::operations::*;
1712
1713		// Test CreateIndex operation SQL generation
1714		let op = Operation::CreateIndex {
1715			table: "users".to_string(),
1716			columns: vec!["email".to_string()],
1717			unique: false,
1718			index_type: None,
1719			where_clause: None,
1720			concurrently: false,
1721			expressions: None,
1722			mysql_options: None,
1723			operator_class: None,
1724		};
1725
1726		let sql = op.to_sql(&SqlDialect::Postgres);
1727		assert!(sql.contains("CREATE INDEX"));
1728		assert!(sql.contains("users"));
1729		assert!(sql.contains("email"));
1730	}
1731
1732	#[test]
1733	// From: Django/migrations
1734	fn test_add_indexes_1() {
1735		use crate::migrations::operations::*;
1736
1737		// Test unique index creation
1738		let op = Operation::CreateIndex {
1739			table: "products".to_string(),
1740			columns: vec!["sku".to_string()],
1741			unique: true,
1742			index_type: None,
1743			where_clause: None,
1744			concurrently: false,
1745			expressions: None,
1746			mysql_options: None,
1747			operator_class: None,
1748		};
1749
1750		let sql = op.to_sql(&SqlDialect::Postgres);
1751		assert!(sql.contains("CREATE UNIQUE INDEX"));
1752		assert!(sql.contains("products"));
1753		assert!(sql.contains("sku"));
1754	}
1755
1756	#[test]
1757	// From: Django/migrations
1758	fn test_add_many_to_many() {
1759		use crate::migrations::ProjectState;
1760		use crate::migrations::operations::*;
1761
1762		let mut state = ProjectState::new();
1763
1764		// Create first table (e.g., students)
1765		let create_students = Operation::CreateTable {
1766			name: "students".to_string(),
1767			columns: vec![
1768				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1769				ColumnDefinition::new("name", FieldType::VarChar(100)),
1770			],
1771			constraints: vec![],
1772			without_rowid: None,
1773			partition: None,
1774			interleave_in_parent: None,
1775		};
1776		create_students.state_forwards("testapp", &mut state);
1777
1778		// Create second table (e.g., courses)
1779		let create_courses = Operation::CreateTable {
1780			name: "courses".to_string(),
1781			columns: vec![
1782				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1783				ColumnDefinition::new("title", FieldType::VarChar(200)),
1784			],
1785			constraints: vec![],
1786			without_rowid: None,
1787			partition: None,
1788			interleave_in_parent: None,
1789		};
1790		create_courses.state_forwards("testapp", &mut state);
1791
1792		// Create many-to-many association table
1793		// Note: Composite primary keys are handled via column definitions, not constraints
1794		let create_m2m = Operation::CreateTable {
1795			name: "students_courses".to_string(),
1796			columns: vec![
1797				ColumnDefinition::new(
1798					"student_id",
1799					FieldType::Custom("INTEGER REFERENCES students(id)".to_string()),
1800				),
1801				ColumnDefinition::new(
1802					"course_id",
1803					FieldType::Custom("INTEGER REFERENCES courses(id)".to_string()),
1804				),
1805			],
1806			constraints: vec![],
1807			without_rowid: None,
1808			partition: None,
1809			interleave_in_parent: None,
1810		};
1811		create_m2m.state_forwards("testapp", &mut state);
1812
1813		assert!(state.get_model("testapp", "students_courses").is_some());
1814	}
1815
1816	#[test]
1817	// From: Django/migrations
1818	fn test_add_many_to_many_1() {
1819		use crate::migrations::ProjectState;
1820		use crate::migrations::operations::*;
1821
1822		let mut state = ProjectState::new();
1823
1824		let create_tags = Operation::CreateTable {
1825			name: "tags".to_string(),
1826			columns: vec![
1827				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1828				ColumnDefinition::new("name", FieldType::VarChar(50)),
1829			],
1830			constraints: vec![],
1831			without_rowid: None,
1832			partition: None,
1833			interleave_in_parent: None,
1834		};
1835		create_tags.state_forwards("app", &mut state);
1836
1837		let create_posts = Operation::CreateTable {
1838			name: "posts".to_string(),
1839			columns: vec![
1840				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1841				ColumnDefinition::new("title", FieldType::VarChar(255)),
1842			],
1843			constraints: vec![],
1844			without_rowid: None,
1845			partition: None,
1846			interleave_in_parent: None,
1847		};
1848		create_posts.state_forwards("app", &mut state);
1849
1850		// Create association table for many-to-many
1851		// Note: Composite primary keys are handled via column definitions, not constraints
1852		let create_assoc = Operation::CreateTable {
1853			name: "posts_tags".to_string(),
1854			columns: vec![
1855				ColumnDefinition::new(
1856					"post_id",
1857					FieldType::Custom("INTEGER REFERENCES posts(id)".to_string()),
1858				),
1859				ColumnDefinition::new(
1860					"tag_id",
1861					FieldType::Custom("INTEGER REFERENCES tags(id)".to_string()),
1862				),
1863			],
1864			constraints: vec![],
1865			without_rowid: None,
1866			partition: None,
1867			interleave_in_parent: None,
1868		};
1869		create_assoc.state_forwards("app", &mut state);
1870
1871		assert!(state.get_model("app", "posts_tags").is_some());
1872	}
1873
1874	#[test]
1875	// From: Django/migrations
1876	fn test_add_model_order_with_respect_to() {
1877		use crate::migrations::ProjectState;
1878		use crate::migrations::operations::*;
1879
1880		let mut state = ProjectState::new();
1881
1882		// Create with order_with_respect_to from the start
1883		let create_parent = Operation::CreateTable {
1884			name: "parent".to_string(),
1885			columns: vec![ColumnDefinition::new(
1886				"id",
1887				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
1888			)],
1889			constraints: vec![],
1890			without_rowid: None,
1891			partition: None,
1892			interleave_in_parent: None,
1893		};
1894		create_parent.state_forwards("app", &mut state);
1895
1896		let create_child = Operation::CreateTable {
1897			name: "child".to_string(),
1898			columns: vec![
1899				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1900				ColumnDefinition::new(
1901					"parent_id",
1902					FieldType::Custom("INTEGER REFERENCES parent(id)".to_string()),
1903				),
1904				ColumnDefinition::new(
1905					"_order",
1906					FieldType::Custom("INTEGER NOT NULL DEFAULT 0".to_string()),
1907				),
1908			],
1909			constraints: vec![],
1910			without_rowid: None,
1911			partition: None,
1912			interleave_in_parent: None,
1913		};
1914		create_child.state_forwards("app", &mut state);
1915
1916		assert!(
1917			state
1918				.get_model("app", "child")
1919				.unwrap()
1920				.fields
1921				.contains_key("_order")
1922		);
1923	}
1924
1925	#[test]
1926	// From: Django/migrations
1927	fn test_add_model_order_with_respect_to_1() {
1928		use crate::migrations::ProjectState;
1929		use crate::migrations::operations::*;
1930
1931		let mut state = ProjectState::new();
1932
1933		let create_op = Operation::CreateTable {
1934			name: "ordered_items".to_string(),
1935			columns: vec![
1936				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1937				ColumnDefinition::new("container_id", FieldType::Integer),
1938				ColumnDefinition::new(
1939					"_order",
1940					FieldType::Custom("INTEGER NOT NULL DEFAULT 0".to_string()),
1941				),
1942			],
1943			constraints: vec![],
1944			without_rowid: None,
1945			partition: None,
1946			interleave_in_parent: None,
1947		};
1948		create_op.state_forwards("app", &mut state);
1949
1950		assert!(state.get_model("app", "ordered_items").is_some());
1951	}
1952
1953	#[test]
1954	// From: Django/migrations
1955	fn test_add_model_order_with_respect_to_constraint() {
1956		use crate::migrations::ProjectState;
1957		use crate::migrations::operations::*;
1958
1959		let mut state = ProjectState::new();
1960
1961		let create_op = Operation::CreateTable {
1962			name: "items".to_string(),
1963			columns: vec![
1964				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1965				ColumnDefinition::new("parent_id", FieldType::Integer),
1966				ColumnDefinition::new(
1967					"_order",
1968					FieldType::Custom("INTEGER NOT NULL DEFAULT 0".to_string()),
1969				),
1970			],
1971			constraints: vec![Constraint::Check {
1972				name: "order_non_negative".to_string(),
1973				expression: "_order >= 0".to_string(),
1974			}],
1975			without_rowid: None,
1976			partition: None,
1977			interleave_in_parent: None,
1978		};
1979		create_op.state_forwards("app", &mut state);
1980
1981		assert!(state.get_model("app", "items").is_some());
1982	}
1983
1984	#[test]
1985	// From: Django/migrations
1986	fn test_add_model_order_with_respect_to_constraint_1() {
1987		use crate::migrations::ProjectState;
1988		use crate::migrations::operations::*;
1989
1990		let mut state = ProjectState::new();
1991
1992		let create_op = Operation::CreateTable {
1993			name: "entries".to_string(),
1994			columns: vec![
1995				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
1996				ColumnDefinition::new("group_id", FieldType::Integer),
1997				ColumnDefinition::new("_order", FieldType::Custom("INTEGER NOT NULL".to_string())),
1998			],
1999			constraints: vec![Constraint::Check {
2000				name: "order_non_negative".to_string(),
2001				expression: "_order >= 0".to_string(),
2002			}],
2003			without_rowid: None,
2004			partition: None,
2005			interleave_in_parent: None,
2006		};
2007		create_op.state_forwards("app", &mut state);
2008
2009		assert!(state.get_model("app", "entries").is_some());
2010	}
2011
2012	#[test]
2013	// From: Django/migrations
2014	fn test_add_model_order_with_respect_to_index() {
2015		use crate::migrations::ProjectState;
2016		use crate::migrations::operations::*;
2017
2018		let mut state = ProjectState::new();
2019
2020		let create_op = Operation::CreateTable {
2021			name: "items".to_string(),
2022			columns: vec![
2023				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
2024				ColumnDefinition::new("parent_id", FieldType::Integer),
2025				ColumnDefinition::new(
2026					"_order",
2027					FieldType::Custom("INTEGER NOT NULL DEFAULT 0".to_string()),
2028				),
2029			],
2030			constraints: vec![],
2031			without_rowid: None,
2032			partition: None,
2033			interleave_in_parent: None,
2034		};
2035		create_op.state_forwards("app", &mut state);
2036
2037		// Add index on (parent_id, _order)
2038		let _create_index = Operation::CreateIndex {
2039			table: "items".to_string(),
2040			columns: vec!["parent_id".to_string(), "_order".to_string()],
2041			unique: false,
2042			index_type: None,
2043			where_clause: None,
2044			concurrently: false,
2045			expressions: None,
2046			mysql_options: None,
2047			operator_class: None,
2048		};
2049
2050		assert!(state.get_model("app", "items").is_some());
2051	}
2052
2053	#[test]
2054	// From: Django/migrations
2055	fn test_add_model_order_with_respect_to_index_1() {
2056		use crate::migrations::ProjectState;
2057		use crate::migrations::operations::*;
2058
2059		let mut state = ProjectState::new();
2060
2061		let create_op = Operation::CreateTable {
2062			name: "tasks".to_string(),
2063			columns: vec![
2064				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
2065				ColumnDefinition::new("project_id", FieldType::Integer),
2066				ColumnDefinition::new("_order", FieldType::Custom("INTEGER NOT NULL".to_string())),
2067			],
2068			constraints: vec![],
2069			without_rowid: None,
2070			partition: None,
2071			interleave_in_parent: None,
2072		};
2073		create_op.state_forwards("app", &mut state);
2074
2075		assert!(state.get_model("app", "tasks").is_some());
2076	}
2077
2078	#[test]
2079	// From: Django/migrations
2080	fn test_add_model_order_with_respect_to_unique_together() {
2081		use crate::migrations::ProjectState;
2082		use crate::migrations::operations::*;
2083
2084		let mut state = ProjectState::new();
2085
2086		let create_op = Operation::CreateTable {
2087			name: "items".to_string(),
2088			columns: vec![
2089				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
2090				ColumnDefinition::new("parent_id", FieldType::Integer),
2091				ColumnDefinition::new("_order", FieldType::Custom("INTEGER NOT NULL".to_string())),
2092			],
2093			constraints: vec![],
2094			without_rowid: None,
2095			partition: None,
2096			interleave_in_parent: None,
2097		};
2098		create_op.state_forwards("app", &mut state);
2099
2100		let unique_op = Operation::AlterUniqueTogether {
2101			table: "items".to_string(),
2102			unique_together: vec![vec!["parent_id".to_string(), "_order".to_string()]],
2103		};
2104		unique_op.state_forwards("app", &mut state);
2105
2106		assert!(state.get_model("app", "items").is_some());
2107	}
2108
2109	#[test]
2110	// From: Django/migrations
2111	fn test_add_model_order_with_respect_to_unique_together_1() {
2112		use crate::migrations::ProjectState;
2113		use crate::migrations::operations::*;
2114
2115		let mut state = ProjectState::new();
2116
2117		let create_op = Operation::CreateTable {
2118			name: "slides".to_string(),
2119			columns: vec![
2120				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
2121				ColumnDefinition::new("deck_id", FieldType::Integer),
2122				ColumnDefinition::new("_order", FieldType::Custom("INTEGER NOT NULL".to_string())),
2123			],
2124			constraints: vec![],
2125			without_rowid: None,
2126			partition: None,
2127			interleave_in_parent: None,
2128		};
2129		create_op.state_forwards("app", &mut state);
2130
2131		let unique_op = Operation::AlterUniqueTogether {
2132			table: "slides".to_string(),
2133			unique_together: vec![vec!["deck_id".to_string(), "_order".to_string()]],
2134		};
2135		unique_op.state_forwards("app", &mut state);
2136
2137		assert!(state.get_model("app", "slides").is_some());
2138	}
2139
2140	#[test]
2141	// From: Django/migrations
2142	fn test_add_model_with_field_removed_from_base_model() {
2143		// Tests joined table inheritance where child model has its own table
2144		// linked to parent table via foreign key
2145		use crate::migrations::ProjectState;
2146		use crate::migrations::operations::*;
2147
2148		let mut state = ProjectState::new();
2149
2150		// Create base (parent) model
2151		let create_base = Operation::CreateTable {
2152			name: "employees".to_string(),
2153			columns: vec![
2154				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
2155				ColumnDefinition::new("name", FieldType::VarChar(100)),
2156				ColumnDefinition::new("email", FieldType::VarChar(100)),
2157			],
2158			constraints: vec![],
2159			without_rowid: None,
2160			partition: None,
2161			interleave_in_parent: None,
2162		};
2163		create_base.state_forwards("company", &mut state);
2164
2165		// Create inherited (child) model using joined table inheritance
2166		let create_inherited = Operation::CreateInheritedTable {
2167			name: "managers".to_string(),
2168			columns: vec![
2169				ColumnDefinition::new("department", FieldType::VarChar(100)),
2170				ColumnDefinition::new(
2171					"budget",
2172					FieldType::Decimal {
2173						precision: 10,
2174						scale: 2,
2175					},
2176				),
2177			],
2178			base_table: "employees".to_string(),
2179			join_column: "employee_id".to_string(),
2180		};
2181		create_inherited.state_forwards("company", &mut state);
2182
2183		let manager_model = state.get_model("company", "managers").unwrap();
2184		assert!(manager_model.fields.contains_key("employee_id"));
2185		assert!(manager_model.fields.contains_key("department"));
2186		assert!(manager_model.fields.contains_key("budget"));
2187		assert_eq!(manager_model.base_model, Some("employees".to_string()));
2188		assert_eq!(
2189			manager_model.inheritance_type,
2190			Some("joined_table".to_string())
2191		);
2192	}
2193
2194	#[test]
2195	// From: Django/migrations
2196	fn test_add_model_with_field_removed_from_base_model_1() {
2197		// Tests single table inheritance where parent and children share one table
2198		// using a discriminator column to distinguish types
2199		use crate::migrations::ProjectState;
2200		use crate::migrations::operations::*;
2201
2202		let mut state = ProjectState::new();
2203
2204		// Create base (parent) model with all fields
2205		let create_base = Operation::CreateTable {
2206			name: "persons".to_string(),
2207			columns: vec![
2208				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
2209				ColumnDefinition::new("name", FieldType::VarChar(100)),
2210				ColumnDefinition::new("email", FieldType::VarChar(100)),
2211				// Fields for all child types in single table
2212				ColumnDefinition::new("student_id", FieldType::VarChar(20)),
2213				ColumnDefinition::new("grade", FieldType::VarChar(10)),
2214				ColumnDefinition::new("employee_id", FieldType::VarChar(20)),
2215				ColumnDefinition::new("department", FieldType::VarChar(100)),
2216			],
2217			constraints: vec![],
2218			without_rowid: None,
2219			partition: None,
2220			interleave_in_parent: None,
2221		};
2222		create_base.state_forwards("school", &mut state);
2223
2224		// Add discriminator column for single table inheritance
2225		let add_discriminator = Operation::AddDiscriminatorColumn {
2226			table: "persons".to_string(),
2227			column_name: "person_type".to_string(),
2228			default_value: "person".to_string(),
2229		};
2230		add_discriminator.state_forwards("school", &mut state);
2231
2232		let person_model = state.get_model("school", "persons").unwrap();
2233		assert!(person_model.fields.contains_key("person_type"));
2234		assert_eq!(
2235			person_model.discriminator_column,
2236			Some("person_type".to_string())
2237		);
2238		assert_eq!(
2239			person_model.inheritance_type,
2240			Some("single_table".to_string())
2241		);
2242	}
2243
2244	#[test]
2245	// From: Django/migrations
2246	fn test_add_non_blank_textfield_and_charfield() {
2247		use crate::migrations::ProjectState;
2248		use crate::migrations::operations::*;
2249
2250		let mut state = ProjectState::new();
2251
2252		let create_op = Operation::CreateTable {
2253			name: "articles".to_string(),
2254			columns: vec![ColumnDefinition::new(
2255				"id",
2256				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
2257			)],
2258			constraints: vec![],
2259			without_rowid: None,
2260			partition: None,
2261			interleave_in_parent: None,
2262		};
2263		create_op.state_forwards("testapp", &mut state);
2264
2265		// Add non-blank fields (NOT NULL with defaults or constraints)
2266		let add_text = Operation::AddColumn {
2267			table: "articles".to_string(),
2268			column: ColumnDefinition::new(
2269				"content",
2270				FieldType::Custom("TEXT NOT NULL DEFAULT ''".to_string()),
2271			),
2272			mysql_options: None,
2273		};
2274		add_text.state_forwards("testapp", &mut state);
2275
2276		let add_char = Operation::AddColumn {
2277			table: "articles".to_string(),
2278			column: ColumnDefinition::new(
2279				"title",
2280				FieldType::Custom("VARCHAR(255) NOT NULL DEFAULT ''".to_string()),
2281			),
2282			mysql_options: None,
2283		};
2284		add_char.state_forwards("testapp", &mut state);
2285
2286		let model = state.get_model("testapp", "articles").unwrap();
2287		assert!(model.fields.contains_key("content"));
2288		assert!(model.fields.contains_key("title"));
2289	}
2290
2291	#[test]
2292	// From: Django/migrations
2293	fn test_add_non_blank_textfield_and_charfield_1() {
2294		use crate::migrations::ProjectState;
2295		use crate::migrations::operations::*;
2296
2297		let mut state = ProjectState::new();
2298
2299		let create_op = Operation::CreateTable {
2300			name: "posts".to_string(),
2301			columns: vec![ColumnDefinition::new(
2302				"id",
2303				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
2304			)],
2305			constraints: vec![],
2306			without_rowid: None,
2307			partition: None,
2308			interleave_in_parent: None,
2309		};
2310		create_op.state_forwards("app", &mut state);
2311
2312		let add_op = Operation::AddColumn {
2313			table: "posts".to_string(),
2314			column: ColumnDefinition::new("body", FieldType::Custom("TEXT NOT NULL".to_string())),
2315			mysql_options: None,
2316		};
2317		add_op.state_forwards("app", &mut state);
2318
2319		assert!(
2320			state
2321				.get_model("app", "posts")
2322				.unwrap()
2323				.fields
2324				.contains_key("body")
2325		);
2326	}
2327
2328	#[test]
2329	// From: Django/migrations
2330	fn test_add_not_null_field_with_db_default() {
2331		use crate::migrations::ProjectState;
2332		use crate::migrations::operations::*;
2333
2334		let mut state = ProjectState::new();
2335
2336		let create_op = Operation::CreateTable {
2337			name: "users".to_string(),
2338			columns: vec![ColumnDefinition::new(
2339				"id",
2340				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
2341			)],
2342			constraints: vec![],
2343			without_rowid: None,
2344			partition: None,
2345			interleave_in_parent: None,
2346		};
2347		create_op.state_forwards("testapp", &mut state);
2348
2349		// Add NOT NULL field with database-level default
2350		let add_op = Operation::AddColumn {
2351			table: "users".to_string(),
2352			column: ColumnDefinition::new(
2353				"created_at",
2354				FieldType::Custom("TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP".to_string()),
2355			),
2356			mysql_options: None,
2357		};
2358		add_op.state_forwards("testapp", &mut state);
2359
2360		let model = state.get_model("testapp", "users").unwrap();
2361		assert!(model.fields.contains_key("created_at"));
2362	}
2363
2364	#[test]
2365	// From: Django/migrations
2366	fn test_add_not_null_field_with_db_default_1() {
2367		use crate::migrations::ProjectState;
2368		use crate::migrations::operations::*;
2369
2370		let mut state = ProjectState::new();
2371
2372		let create_op = Operation::CreateTable {
2373			name: "orders".to_string(),
2374			columns: vec![ColumnDefinition::new(
2375				"id",
2376				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
2377			)],
2378			constraints: vec![],
2379			without_rowid: None,
2380			partition: None,
2381			interleave_in_parent: None,
2382		};
2383		create_op.state_forwards("app", &mut state);
2384
2385		let add_op = Operation::AddColumn {
2386			table: "orders".to_string(),
2387			column: ColumnDefinition::new(
2388				"status",
2389				FieldType::Custom("VARCHAR(50) NOT NULL DEFAULT 'pending'".to_string()),
2390			),
2391			mysql_options: None,
2392		};
2393		add_op.state_forwards("app", &mut state);
2394
2395		assert!(
2396			state
2397				.get_model("app", "orders")
2398				.unwrap()
2399				.fields
2400				.contains_key("status")
2401		);
2402	}
2403
2404	#[test]
2405	// From: Django/migrations
2406	fn test_add_unique_together() {
2407		use crate::migrations::ProjectState;
2408		use crate::migrations::operations::*;
2409
2410		let mut state = ProjectState::new();
2411
2412		let create_op = Operation::CreateTable {
2413			name: "products".to_string(),
2414			columns: vec![
2415				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
2416				ColumnDefinition::new("name", FieldType::VarChar(255)),
2417				ColumnDefinition::new("sku", FieldType::VarChar(50)),
2418			],
2419			constraints: vec![],
2420			without_rowid: None,
2421			partition: None,
2422			interleave_in_parent: None,
2423		};
2424		create_op.state_forwards("app", &mut state);
2425
2426		let unique_op = Operation::AlterUniqueTogether {
2427			table: "products".to_string(),
2428			unique_together: vec![vec!["name".to_string(), "sku".to_string()]],
2429		};
2430		unique_op.state_forwards("app", &mut state);
2431
2432		assert!(state.get_model("app", "products").is_some());
2433	}
2434
2435	#[test]
2436	// From: Django/migrations
2437	fn test_add_unique_together_1() {
2438		use crate::migrations::ProjectState;
2439		use crate::migrations::operations::*;
2440
2441		let mut state = ProjectState::new();
2442
2443		let create_op = Operation::CreateTable {
2444			name: "books".to_string(),
2445			columns: vec![
2446				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
2447				ColumnDefinition::new("title", FieldType::VarChar(255)),
2448				ColumnDefinition::new("author", FieldType::VarChar(255)),
2449				ColumnDefinition::new("isbn", FieldType::VarChar(20)),
2450			],
2451			constraints: vec![],
2452			without_rowid: None,
2453			partition: None,
2454			interleave_in_parent: None,
2455		};
2456		create_op.state_forwards("app", &mut state);
2457
2458		let unique_op = Operation::AlterUniqueTogether {
2459			table: "books".to_string(),
2460			unique_together: vec![
2461				vec!["title".to_string(), "author".to_string()],
2462				vec!["isbn".to_string()],
2463			],
2464		};
2465		unique_op.state_forwards("app", &mut state);
2466
2467		assert!(state.get_model("app", "books").is_some());
2468	}
2469
2470	#[test]
2471	// From: Django/migrations
2472	fn test_alter_constraint() {
2473		use crate::migrations::operations::*;
2474
2475		// Test dropping and adding a constraint (simulating alteration)
2476		let drop_op = Operation::DropConstraint {
2477			table: "users".to_string(),
2478			constraint_name: "old_constraint".to_string(),
2479		};
2480
2481		let add_op = Operation::AddConstraint {
2482			table: "users".to_string(),
2483			constraint_sql: "CHECK (age >= 21)".to_string(),
2484		};
2485
2486		let drop_sql = drop_op.to_sql(&SqlDialect::Postgres);
2487		let add_sql = add_op.to_sql(&SqlDialect::Postgres);
2488
2489		assert!(drop_sql.contains("DROP CONSTRAINT"));
2490		assert!(add_sql.contains("ADD CHECK (age >= 21)"));
2491	}
2492
2493	#[test]
2494	// From: Django/migrations
2495	fn test_alter_constraint_1() {
2496		use crate::migrations::operations::*;
2497
2498		// Test constraint alteration with different constraint
2499		let drop_op = Operation::DropConstraint {
2500			table: "products".to_string(),
2501			constraint_name: "price_check".to_string(),
2502		};
2503
2504		let add_op = Operation::AddConstraint {
2505			table: "products".to_string(),
2506			constraint_sql: "CHECK (price > 0)".to_string(),
2507		};
2508
2509		let drop_sql = drop_op.to_sql(&SqlDialect::Postgres);
2510		let add_sql = add_op.to_sql(&SqlDialect::Postgres);
2511
2512		assert!(drop_sql.contains("DROP CONSTRAINT price_check"));
2513		assert!(add_sql.contains("ADD CHECK (price > 0)"));
2514	}
2515
2516	#[test]
2517	// From: Django/migrations
2518	fn test_alter_db_table_add() {
2519		use crate::migrations::ProjectState;
2520		use crate::migrations::operations::*;
2521
2522		let mut state = ProjectState::new();
2523
2524		// Create with default name
2525		let create_op = Operation::CreateTable {
2526			name: "myapp_user".to_string(),
2527			columns: vec![ColumnDefinition::new(
2528				"id",
2529				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
2530			)],
2531			constraints: vec![],
2532			without_rowid: None,
2533			partition: None,
2534			interleave_in_parent: None,
2535		};
2536		create_op.state_forwards("testapp", &mut state);
2537
2538		// Simulate db_table change by renaming
2539		let rename_op = Operation::RenameTable {
2540			old_name: "myapp_user".to_string(),
2541			new_name: "custom_users".to_string(),
2542		};
2543		rename_op.state_forwards("testapp", &mut state);
2544
2545		assert!(state.get_model("testapp", "custom_users").is_some());
2546	}
2547
2548	#[test]
2549	// From: Django/migrations
2550	fn test_alter_db_table_add_1() {
2551		use crate::migrations::ProjectState;
2552		use crate::migrations::operations::*;
2553
2554		let mut state = ProjectState::new();
2555
2556		let create_op = Operation::CreateTable {
2557			name: "app_product".to_string(),
2558			columns: vec![ColumnDefinition::new(
2559				"id",
2560				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
2561			)],
2562			constraints: vec![],
2563			without_rowid: None,
2564			partition: None,
2565			interleave_in_parent: None,
2566		};
2567		create_op.state_forwards("app", &mut state);
2568
2569		let rename_op = Operation::RenameTable {
2570			old_name: "app_product".to_string(),
2571			new_name: "products_table".to_string(),
2572		};
2573		rename_op.state_forwards("app", &mut state);
2574
2575		assert!(state.get_model("app", "products_table").is_some());
2576	}
2577
2578	#[test]
2579	// From: Django/migrations
2580	fn test_alter_db_table_change() {
2581		use crate::migrations::ProjectState;
2582		use crate::migrations::operations::*;
2583
2584		let mut state = ProjectState::new();
2585
2586		// Create a table
2587		let create_op = Operation::CreateTable {
2588			name: "old_table".to_string(),
2589			columns: vec![ColumnDefinition::new(
2590				"id",
2591				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
2592			)],
2593			constraints: vec![],
2594			without_rowid: None,
2595			partition: None,
2596			interleave_in_parent: None,
2597		};
2598		create_op.state_forwards("testapp", &mut state);
2599
2600		// Rename the table
2601		let rename_op = Operation::RenameTable {
2602			old_name: "old_table".to_string(),
2603			new_name: "new_table".to_string(),
2604		};
2605		rename_op.state_forwards("testapp", &mut state);
2606
2607		assert!(state.get_model("testapp", "old_table").is_none());
2608		assert!(state.get_model("testapp", "new_table").is_some());
2609	}
2610
2611	#[test]
2612	// From: Django/migrations
2613	fn test_alter_db_table_change_1() {
2614		use crate::migrations::ProjectState;
2615		use crate::migrations::operations::*;
2616
2617		let mut state = ProjectState::new();
2618
2619		let create_op = Operation::CreateTable {
2620			name: "users".to_string(),
2621			columns: vec![ColumnDefinition::new(
2622				"id",
2623				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
2624			)],
2625			constraints: vec![],
2626			without_rowid: None,
2627			partition: None,
2628			interleave_in_parent: None,
2629		};
2630		create_op.state_forwards("app", &mut state);
2631
2632		let rename_op = Operation::RenameTable {
2633			old_name: "users".to_string(),
2634			new_name: "customers".to_string(),
2635		};
2636		rename_op.state_forwards("app", &mut state);
2637
2638		assert!(state.get_model("app", "customers").is_some());
2639	}
2640
2641	#[test]
2642	// From: Django/migrations
2643	fn test_alter_db_table_comment_add() {
2644		use crate::migrations::operations::*;
2645
2646		let op = Operation::AlterTableComment {
2647			table: "users".to_string(),
2648			comment: Some("User accounts table".to_string()),
2649		};
2650
2651		let sql = op.to_sql(&SqlDialect::Postgres);
2652		assert!(sql.contains("COMMENT ON TABLE users"));
2653		assert!(sql.contains("User accounts table"));
2654	}
2655
2656	#[test]
2657	// From: Django/migrations
2658	fn test_alter_db_table_comment_add_1() {
2659		use crate::migrations::operations::*;
2660
2661		let op = Operation::AlterTableComment {
2662			table: "products".to_string(),
2663			comment: Some("Product catalog".to_string()),
2664		};
2665
2666		let sql = op.to_sql(&SqlDialect::Mysql);
2667		assert!(sql.contains("ALTER TABLE products"));
2668		assert!(sql.contains("COMMENT='Product catalog'"));
2669	}
2670
2671	#[test]
2672	// From: Django/migrations
2673	fn test_alter_db_table_comment_change() {
2674		use crate::migrations::operations::*;
2675
2676		let op = Operation::AlterTableComment {
2677			table: "users".to_string(),
2678			comment: Some("Updated user table".to_string()),
2679		};
2680
2681		let sql = op.to_sql(&SqlDialect::Postgres);
2682		assert!(sql.contains("COMMENT ON TABLE users"));
2683		assert!(sql.contains("Updated user table"));
2684	}
2685
2686	#[test]
2687	// From: Django/migrations
2688	fn test_alter_db_table_comment_change_1() {
2689		use crate::migrations::operations::*;
2690
2691		let op = Operation::AlterTableComment {
2692			table: "orders".to_string(),
2693			comment: Some("Order history".to_string()),
2694		};
2695
2696		let sql = op.to_sql(&SqlDialect::Mysql);
2697		assert!(sql.contains("ALTER TABLE orders"));
2698	}
2699
2700	#[test]
2701	// From: Django/migrations
2702	fn test_alter_db_table_comment_no_changes() {
2703		use crate::migrations::operations::*;
2704
2705		// Setting same comment - this is a no-op test
2706		let op = Operation::AlterTableComment {
2707			table: "users".to_string(),
2708			comment: Some("Same comment".to_string()),
2709		};
2710
2711		let sql = op.to_sql(&SqlDialect::Postgres);
2712		assert!(sql.contains("COMMENT ON TABLE users"));
2713	}
2714
2715	#[test]
2716	// From: Django/migrations
2717	fn test_alter_db_table_comment_no_changes_1() {
2718		use crate::migrations::operations::*;
2719
2720		let op = Operation::AlterTableComment {
2721			table: "products".to_string(),
2722			comment: Some("No change".to_string()),
2723		};
2724
2725		let sql = op.to_sql(&SqlDialect::Mysql);
2726		assert!(!sql.is_empty());
2727	}
2728
2729	#[test]
2730	// From: Django/migrations
2731	fn test_alter_db_table_comment_remove() {
2732		use crate::migrations::operations::*;
2733
2734		let op = Operation::AlterTableComment {
2735			table: "users".to_string(),
2736			comment: None,
2737		};
2738
2739		let sql = op.to_sql(&SqlDialect::Postgres);
2740		assert!(sql.contains("COMMENT ON TABLE users IS NULL"));
2741	}
2742
2743	#[test]
2744	// From: Django/migrations
2745	fn test_alter_db_table_comment_remove_1() {
2746		use crate::migrations::operations::*;
2747
2748		let op = Operation::AlterTableComment {
2749			table: "orders".to_string(),
2750			comment: None,
2751		};
2752
2753		let sql = op.to_sql(&SqlDialect::Mysql);
2754		assert!(sql.contains("COMMENT=''"));
2755	}
2756
2757	#[test]
2758	// From: Django/migrations
2759	fn test_alter_db_table_no_changes() {
2760		use crate::migrations::ProjectState;
2761		use crate::migrations::operations::*;
2762
2763		let mut state = ProjectState::new();
2764
2765		// Create a table
2766		let create_op = Operation::CreateTable {
2767			name: "users".to_string(),
2768			columns: vec![ColumnDefinition::new(
2769				"id",
2770				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
2771			)],
2772			constraints: vec![],
2773			without_rowid: None,
2774			partition: None,
2775			interleave_in_parent: None,
2776		};
2777		create_op.state_forwards("testapp", &mut state);
2778
2779		// Rename to same name (no-op)
2780		let rename_op = Operation::RenameTable {
2781			old_name: "users".to_string(),
2782			new_name: "users".to_string(),
2783		};
2784		rename_op.state_forwards("testapp", &mut state);
2785
2786		// Table should still exist with same name
2787		assert!(state.get_model("testapp", "users").is_some());
2788	}
2789
2790	#[test]
2791	// From: Django/migrations
2792	fn test_alter_db_table_no_changes_1() {
2793		use crate::migrations::ProjectState;
2794		use crate::migrations::operations::*;
2795
2796		let mut state = ProjectState::new();
2797
2798		let create_op = Operation::CreateTable {
2799			name: "products".to_string(),
2800			columns: vec![ColumnDefinition::new(
2801				"id",
2802				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
2803			)],
2804			constraints: vec![],
2805			without_rowid: None,
2806			partition: None,
2807			interleave_in_parent: None,
2808		};
2809		create_op.state_forwards("app", &mut state);
2810
2811		// No actual change
2812		let rename_op = Operation::RenameTable {
2813			old_name: "products".to_string(),
2814			new_name: "products".to_string(),
2815		};
2816		rename_op.state_forwards("app", &mut state);
2817
2818		assert!(state.get_model("app", "products").is_some());
2819	}
2820
2821	#[test]
2822	// From: Django/migrations
2823	fn test_alter_db_table_remove() {
2824		use crate::migrations::ProjectState;
2825		use crate::migrations::operations::*;
2826
2827		let mut state = ProjectState::new();
2828
2829		let create_op = Operation::CreateTable {
2830			name: "custom_table".to_string(),
2831			columns: vec![ColumnDefinition::new(
2832				"id",
2833				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
2834			)],
2835			constraints: vec![],
2836			without_rowid: None,
2837			partition: None,
2838			interleave_in_parent: None,
2839		};
2840		create_op.state_forwards("testapp", &mut state);
2841
2842		// Removing db_table means reverting to default name
2843		let rename_op = Operation::RenameTable {
2844			old_name: "custom_table".to_string(),
2845			new_name: "myapp_model".to_string(),
2846		};
2847		rename_op.state_forwards("testapp", &mut state);
2848
2849		assert!(state.get_model("testapp", "myapp_model").is_some());
2850	}
2851
2852	#[test]
2853	// From: Django/migrations
2854	fn test_alter_db_table_remove_1() {
2855		use crate::migrations::ProjectState;
2856		use crate::migrations::operations::*;
2857
2858		let mut state = ProjectState::new();
2859
2860		let create_op = Operation::CreateTable {
2861			name: "old_custom".to_string(),
2862			columns: vec![ColumnDefinition::new(
2863				"id",
2864				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
2865			)],
2866			constraints: vec![],
2867			without_rowid: None,
2868			partition: None,
2869			interleave_in_parent: None,
2870		};
2871		create_op.state_forwards("app", &mut state);
2872
2873		let rename_op = Operation::RenameTable {
2874			old_name: "old_custom".to_string(),
2875			new_name: "app_default".to_string(),
2876		};
2877		rename_op.state_forwards("app", &mut state);
2878
2879		assert!(state.get_model("app", "app_default").is_some());
2880	}
2881
2882	#[test]
2883	// From: Django/migrations
2884	fn test_alter_db_table_with_model_change() {
2885		use crate::migrations::ProjectState;
2886		use crate::migrations::operations::*;
2887
2888		let mut state = ProjectState::new();
2889
2890		let create_op = Operation::CreateTable {
2891			name: "users".to_string(),
2892			columns: vec![
2893				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
2894				ColumnDefinition::new("name", FieldType::VarChar(100)),
2895			],
2896			constraints: vec![],
2897			without_rowid: None,
2898			partition: None,
2899			interleave_in_parent: None,
2900		};
2901		create_op.state_forwards("testapp", &mut state);
2902
2903		// Change table name and add field in same migration
2904		let rename_op = Operation::RenameTable {
2905			old_name: "users".to_string(),
2906			new_name: "custom_users".to_string(),
2907		};
2908		rename_op.state_forwards("testapp", &mut state);
2909
2910		let add_field = Operation::AddColumn {
2911			table: "custom_users".to_string(),
2912			column: ColumnDefinition::new("email", FieldType::VarChar(255)),
2913			mysql_options: None,
2914		};
2915		add_field.state_forwards("testapp", &mut state);
2916
2917		let model = state.get_model("testapp", "custom_users").unwrap();
2918		assert!(model.fields.contains_key("email"));
2919	}
2920
2921	#[test]
2922	// From: Django/migrations
2923	fn test_alter_db_table_with_model_change_1() {
2924		use crate::migrations::ProjectState;
2925		use crate::migrations::operations::*;
2926
2927		let mut state = ProjectState::new();
2928
2929		let create_op = Operation::CreateTable {
2930			name: "items".to_string(),
2931			columns: vec![ColumnDefinition::new(
2932				"id",
2933				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
2934			)],
2935			constraints: vec![],
2936			without_rowid: None,
2937			partition: None,
2938			interleave_in_parent: None,
2939		};
2940		create_op.state_forwards("app", &mut state);
2941
2942		let rename_op = Operation::RenameTable {
2943			old_name: "items".to_string(),
2944			new_name: "products".to_string(),
2945		};
2946		rename_op.state_forwards("app", &mut state);
2947
2948		assert!(state.get_model("app", "products").is_some());
2949	}
2950
2951	#[test]
2952	// From: Django/migrations
2953	fn test_alter_field() {
2954		use crate::migrations::ProjectState;
2955		use crate::migrations::operations::*;
2956
2957		let mut state = ProjectState::new();
2958
2959		// Create a table
2960		let create_op = Operation::CreateTable {
2961			name: "test_table".to_string(),
2962			columns: vec![
2963				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
2964				ColumnDefinition::new("name", FieldType::VarChar(100)),
2965			],
2966			constraints: vec![],
2967			without_rowid: None,
2968			partition: None,
2969			interleave_in_parent: None,
2970		};
2971		create_op.state_forwards("testapp", &mut state);
2972
2973		// Alter the field
2974		let alter_op = Operation::AlterColumn {
2975			table: "test_table".to_string(),
2976			column: "name".to_string(),
2977			old_definition: None,
2978			new_definition: ColumnDefinition::new("name", FieldType::VarChar(255)),
2979			mysql_options: None,
2980		};
2981		alter_op.state_forwards("testapp", &mut state);
2982
2983		let model = state.get_model("testapp", "test_table").unwrap();
2984		assert!(model.fields.contains_key("name"));
2985	}
2986
2987	#[test]
2988	// From: Django/migrations
2989	fn test_alter_field_1() {
2990		use crate::migrations::ProjectState;
2991		use crate::migrations::operations::*;
2992
2993		let mut state = ProjectState::new();
2994
2995		let create_op = Operation::CreateTable {
2996			name: "users".to_string(),
2997			columns: vec![
2998				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
2999				ColumnDefinition::new("email", FieldType::VarChar(100)),
3000			],
3001			constraints: vec![],
3002			without_rowid: None,
3003			partition: None,
3004			interleave_in_parent: None,
3005		};
3006		create_op.state_forwards("app", &mut state);
3007
3008		let alter_op = Operation::AlterColumn {
3009			table: "users".to_string(),
3010			column: "email".to_string(),
3011			old_definition: None,
3012			new_definition: ColumnDefinition::new("email", FieldType::Text),
3013			mysql_options: None,
3014		};
3015		alter_op.state_forwards("app", &mut state);
3016
3017		assert!(
3018			state
3019				.get_model("app", "users")
3020				.unwrap()
3021				.fields
3022				.contains_key("email")
3023		);
3024	}
3025
3026	#[test]
3027	// From: Django/migrations
3028	fn test_alter_field_and_unique_together() {
3029		use crate::migrations::ProjectState;
3030		use crate::migrations::operations::*;
3031
3032		let mut state = ProjectState::new();
3033
3034		let create_op = Operation::CreateTable {
3035			name: "items".to_string(),
3036			columns: vec![
3037				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3038				ColumnDefinition::new("code", FieldType::VarChar(50)),
3039				ColumnDefinition::new("category", FieldType::VarChar(50)),
3040			],
3041			constraints: vec![],
3042			without_rowid: None,
3043			partition: None,
3044			interleave_in_parent: None,
3045		};
3046		create_op.state_forwards("app", &mut state);
3047
3048		let unique_op = Operation::AlterUniqueTogether {
3049			table: "items".to_string(),
3050			unique_together: vec![vec!["code".to_string(), "category".to_string()]],
3051		};
3052		unique_op.state_forwards("app", &mut state);
3053
3054		let alter_op = Operation::AlterColumn {
3055			table: "items".to_string(),
3056			column: "code".to_string(),
3057			old_definition: None,
3058			new_definition: ColumnDefinition::new("code", FieldType::VarChar(100)),
3059			mysql_options: None,
3060		};
3061		alter_op.state_forwards("app", &mut state);
3062
3063		assert!(state.get_model("app", "items").is_some());
3064	}
3065
3066	#[test]
3067	// From: Django/migrations
3068	fn test_alter_field_and_unique_together_1() {
3069		use crate::migrations::ProjectState;
3070		use crate::migrations::operations::*;
3071
3072		let mut state = ProjectState::new();
3073
3074		let create_op = Operation::CreateTable {
3075			name: "orders".to_string(),
3076			columns: vec![
3077				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3078				ColumnDefinition::new("order_number", FieldType::VarChar(20)),
3079				ColumnDefinition::new("year", FieldType::Integer),
3080			],
3081			constraints: vec![],
3082			without_rowid: None,
3083			partition: None,
3084			interleave_in_parent: None,
3085		};
3086		create_op.state_forwards("app", &mut state);
3087
3088		let unique_op = Operation::AlterUniqueTogether {
3089			table: "orders".to_string(),
3090			unique_together: vec![vec!["order_number".to_string(), "year".to_string()]],
3091		};
3092		unique_op.state_forwards("app", &mut state);
3093
3094		let alter_op = Operation::AlterColumn {
3095			table: "orders".to_string(),
3096			column: "year".to_string(),
3097			old_definition: None,
3098			new_definition: ColumnDefinition::new(
3099				"year",
3100				FieldType::Custom("SMALLINT".to_string()),
3101			),
3102			mysql_options: None,
3103		};
3104		alter_op.state_forwards("app", &mut state);
3105
3106		assert!(state.get_model("app", "orders").is_some());
3107	}
3108
3109	#[test]
3110	// From: Django/migrations
3111	fn test_alter_field_to_fk_dependency_other_app() {
3112		use crate::migrations::ProjectState;
3113		use crate::migrations::operations::*;
3114
3115		let mut state = ProjectState::new();
3116
3117		// Create referenced table in another "app"
3118		let create_users = Operation::CreateTable {
3119			name: "users".to_string(),
3120			columns: vec![ColumnDefinition::new(
3121				"id",
3122				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
3123			)],
3124			constraints: vec![],
3125			without_rowid: None,
3126			partition: None,
3127			interleave_in_parent: None,
3128		};
3129		create_users.state_forwards("auth_app", &mut state);
3130
3131		// Create table with regular field
3132		let create_posts = Operation::CreateTable {
3133			name: "posts".to_string(),
3134			columns: vec![
3135				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3136				ColumnDefinition::new("author_id", FieldType::Integer),
3137			],
3138			constraints: vec![],
3139			without_rowid: None,
3140			partition: None,
3141			interleave_in_parent: None,
3142		};
3143		create_posts.state_forwards("blog_app", &mut state);
3144
3145		// Alter to FK (in practice, this would add FK constraint)
3146		let alter_op = Operation::AlterColumn {
3147			table: "posts".to_string(),
3148			column: "author_id".to_string(),
3149			old_definition: None,
3150			new_definition: ColumnDefinition::new(
3151				"author_id",
3152				FieldType::Custom("INTEGER REFERENCES users(id)".to_string()),
3153			),
3154			mysql_options: None,
3155		};
3156		alter_op.state_forwards("blog_app", &mut state);
3157
3158		assert!(
3159			state
3160				.get_model("blog_app", "posts")
3161				.unwrap()
3162				.fields
3163				.contains_key("author_id")
3164		);
3165	}
3166
3167	#[test]
3168	// From: Django/migrations
3169	fn test_alter_field_to_fk_dependency_other_app_1() {
3170		use crate::migrations::ProjectState;
3171		use crate::migrations::operations::*;
3172
3173		let mut state = ProjectState::new();
3174
3175		let create_categories = Operation::CreateTable {
3176			name: "categories".to_string(),
3177			columns: vec![ColumnDefinition::new(
3178				"id",
3179				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
3180			)],
3181			constraints: vec![],
3182			without_rowid: None,
3183			partition: None,
3184			interleave_in_parent: None,
3185		};
3186		create_categories.state_forwards("catalog", &mut state);
3187
3188		let create_items = Operation::CreateTable {
3189			name: "items".to_string(),
3190			columns: vec![
3191				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3192				ColumnDefinition::new("cat_id", FieldType::Integer),
3193			],
3194			constraints: vec![],
3195			without_rowid: None,
3196			partition: None,
3197			interleave_in_parent: None,
3198		};
3199		create_items.state_forwards("store", &mut state);
3200
3201		let alter_op = Operation::AlterColumn {
3202			table: "items".to_string(),
3203			column: "cat_id".to_string(),
3204			old_definition: None,
3205			new_definition: ColumnDefinition::new(
3206				"cat_id",
3207				FieldType::Custom("INTEGER REFERENCES categories(id)".to_string()),
3208			),
3209			mysql_options: None,
3210		};
3211		alter_op.state_forwards("store", &mut state);
3212
3213		assert!(state.get_model("store", "items").is_some());
3214	}
3215
3216	#[test]
3217	// From: Django/migrations
3218	fn test_alter_field_to_not_null_oneoff_default() {
3219		use crate::migrations::ProjectState;
3220		use crate::migrations::operations::*;
3221
3222		let mut state = ProjectState::new();
3223
3224		let create_op = Operation::CreateTable {
3225			name: "users".to_string(),
3226			columns: vec![
3227				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3228				ColumnDefinition::new("nickname", FieldType::VarChar(100)),
3229			],
3230			constraints: vec![],
3231			without_rowid: None,
3232			partition: None,
3233			interleave_in_parent: None,
3234		};
3235		create_op.state_forwards("testapp", &mut state);
3236
3237		// This simulates a two-step process:
3238		// 1. Add default temporarily
3239		// 2. Make field NOT NULL
3240		// In practice, this would be done with RunSQL or a combined operation
3241		let alter_op = Operation::AlterColumn {
3242			table: "users".to_string(),
3243			column: "nickname".to_string(),
3244			old_definition: None,
3245			new_definition: ColumnDefinition::new(
3246				"nickname",
3247				FieldType::Custom("VARCHAR(100) NOT NULL".to_string()),
3248			),
3249			mysql_options: None,
3250		};
3251		alter_op.state_forwards("testapp", &mut state);
3252
3253		assert!(
3254			state
3255				.get_model("testapp", "users")
3256				.unwrap()
3257				.fields
3258				.contains_key("nickname")
3259		);
3260	}
3261
3262	#[test]
3263	// From: Django/migrations
3264	fn test_alter_field_to_not_null_oneoff_default_1() {
3265		use crate::migrations::ProjectState;
3266		use crate::migrations::operations::*;
3267
3268		let mut state = ProjectState::new();
3269
3270		let create_op = Operation::CreateTable {
3271			name: "profiles".to_string(),
3272			columns: vec![
3273				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3274				ColumnDefinition::new("bio", FieldType::Text),
3275			],
3276			constraints: vec![],
3277			without_rowid: None,
3278			partition: None,
3279			interleave_in_parent: None,
3280		};
3281		create_op.state_forwards("app", &mut state);
3282
3283		let alter_op = Operation::AlterColumn {
3284			table: "profiles".to_string(),
3285			column: "bio".to_string(),
3286			old_definition: None,
3287			new_definition: ColumnDefinition::new(
3288				"bio",
3289				FieldType::Custom("TEXT NOT NULL".to_string()),
3290			),
3291			mysql_options: None,
3292		};
3293		alter_op.state_forwards("app", &mut state);
3294
3295		assert!(
3296			state
3297				.get_model("app", "profiles")
3298				.unwrap()
3299				.fields
3300				.contains_key("bio")
3301		);
3302	}
3303
3304	#[test]
3305	// From: Django/migrations
3306	fn test_alter_field_to_not_null_with_db_default() {
3307		use crate::migrations::ProjectState;
3308		use crate::migrations::operations::*;
3309
3310		let mut state = ProjectState::new();
3311
3312		let create_op = Operation::CreateTable {
3313			name: "products".to_string(),
3314			columns: vec![
3315				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3316				ColumnDefinition::new("quantity", FieldType::Integer),
3317			],
3318			constraints: vec![],
3319			without_rowid: None,
3320			partition: None,
3321			interleave_in_parent: None,
3322		};
3323		create_op.state_forwards("testapp", &mut state);
3324
3325		// Alter to NOT NULL with database default
3326		let alter_op = Operation::AlterColumn {
3327			table: "products".to_string(),
3328			column: "quantity".to_string(),
3329			old_definition: None,
3330			new_definition: ColumnDefinition::new(
3331				"quantity",
3332				FieldType::Custom("INTEGER NOT NULL DEFAULT 0".to_string()),
3333			),
3334			mysql_options: None,
3335		};
3336		alter_op.state_forwards("testapp", &mut state);
3337
3338		assert!(
3339			state
3340				.get_model("testapp", "products")
3341				.unwrap()
3342				.fields
3343				.contains_key("quantity")
3344		);
3345	}
3346
3347	#[test]
3348	// From: Django/migrations
3349	fn test_alter_field_to_not_null_with_db_default_1() {
3350		use crate::migrations::ProjectState;
3351		use crate::migrations::operations::*;
3352
3353		let mut state = ProjectState::new();
3354
3355		let create_op = Operation::CreateTable {
3356			name: "items".to_string(),
3357			columns: vec![
3358				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3359				ColumnDefinition::new("available", FieldType::Boolean),
3360			],
3361			constraints: vec![],
3362			without_rowid: None,
3363			partition: None,
3364			interleave_in_parent: None,
3365		};
3366		create_op.state_forwards("app", &mut state);
3367
3368		let alter_op = Operation::AlterColumn {
3369			table: "items".to_string(),
3370			column: "available".to_string(),
3371			old_definition: None,
3372			new_definition: ColumnDefinition::new(
3373				"available",
3374				FieldType::Custom("BOOLEAN NOT NULL DEFAULT TRUE".to_string()),
3375			),
3376			mysql_options: None,
3377		};
3378		alter_op.state_forwards("app", &mut state);
3379
3380		assert!(
3381			state
3382				.get_model("app", "items")
3383				.unwrap()
3384				.fields
3385				.contains_key("available")
3386		);
3387	}
3388
3389	#[test]
3390	// From: Django/migrations
3391	fn test_alter_field_to_not_null_with_default() {
3392		use crate::migrations::ProjectState;
3393		use crate::migrations::operations::*;
3394
3395		let mut state = ProjectState::new();
3396
3397		let create_op = Operation::CreateTable {
3398			name: "users".to_string(),
3399			columns: vec![
3400				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3401				ColumnDefinition::new("status", FieldType::VarChar(50)),
3402			],
3403			constraints: vec![],
3404			without_rowid: None,
3405			partition: None,
3406			interleave_in_parent: None,
3407		};
3408		create_op.state_forwards("testapp", &mut state);
3409
3410		// Alter field to NOT NULL with default
3411		let alter_op = Operation::AlterColumn {
3412			table: "users".to_string(),
3413			column: "status".to_string(),
3414			old_definition: None,
3415			new_definition: ColumnDefinition::new(
3416				"status",
3417				FieldType::Custom("VARCHAR(50) NOT NULL DEFAULT 'active'".to_string()),
3418			),
3419			mysql_options: None,
3420		};
3421		alter_op.state_forwards("testapp", &mut state);
3422
3423		assert!(
3424			state
3425				.get_model("testapp", "users")
3426				.unwrap()
3427				.fields
3428				.contains_key("status")
3429		);
3430	}
3431
3432	#[test]
3433	// From: Django/migrations
3434	fn test_alter_field_to_not_null_with_default_1() {
3435		use crate::migrations::ProjectState;
3436		use crate::migrations::operations::*;
3437
3438		let mut state = ProjectState::new();
3439
3440		let create_op = Operation::CreateTable {
3441			name: "products".to_string(),
3442			columns: vec![
3443				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3444				ColumnDefinition::new("active", FieldType::Boolean),
3445			],
3446			constraints: vec![],
3447			without_rowid: None,
3448			partition: None,
3449			interleave_in_parent: None,
3450		};
3451		create_op.state_forwards("app", &mut state);
3452
3453		let alter_op = Operation::AlterColumn {
3454			table: "products".to_string(),
3455			column: "active".to_string(),
3456			old_definition: None,
3457			new_definition: ColumnDefinition::new(
3458				"active",
3459				FieldType::Custom("BOOLEAN NOT NULL DEFAULT TRUE".to_string()),
3460			),
3461			mysql_options: None,
3462		};
3463		alter_op.state_forwards("app", &mut state);
3464
3465		assert!(
3466			state
3467				.get_model("app", "products")
3468				.unwrap()
3469				.fields
3470				.contains_key("active")
3471		);
3472	}
3473
3474	#[test]
3475	// From: Django/migrations
3476	fn test_alter_field_to_not_null_without_default() {
3477		use crate::migrations::ProjectState;
3478		use crate::migrations::operations::*;
3479
3480		let mut state = ProjectState::new();
3481
3482		let create_op = Operation::CreateTable {
3483			name: "users".to_string(),
3484			columns: vec![
3485				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3486				ColumnDefinition::new("email", FieldType::VarChar(255)),
3487			],
3488			constraints: vec![],
3489			without_rowid: None,
3490			partition: None,
3491			interleave_in_parent: None,
3492		};
3493		create_op.state_forwards("testapp", &mut state);
3494
3495		// Alter field to NOT NULL without default (assumes data exists)
3496		let alter_op = Operation::AlterColumn {
3497			table: "users".to_string(),
3498			column: "email".to_string(),
3499			old_definition: None,
3500			new_definition: ColumnDefinition::new(
3501				"email",
3502				FieldType::Custom("VARCHAR(255) NOT NULL".to_string()),
3503			),
3504			mysql_options: None,
3505		};
3506		alter_op.state_forwards("testapp", &mut state);
3507
3508		assert!(
3509			state
3510				.get_model("testapp", "users")
3511				.unwrap()
3512				.fields
3513				.contains_key("email")
3514		);
3515	}
3516
3517	#[test]
3518	// From: Django/migrations
3519	fn test_alter_field_to_not_null_without_default_1() {
3520		use crate::migrations::ProjectState;
3521		use crate::migrations::operations::*;
3522
3523		let mut state = ProjectState::new();
3524
3525		let create_op = Operation::CreateTable {
3526			name: "orders".to_string(),
3527			columns: vec![
3528				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3529				ColumnDefinition::new("customer_id", FieldType::Integer),
3530			],
3531			constraints: vec![],
3532			without_rowid: None,
3533			partition: None,
3534			interleave_in_parent: None,
3535		};
3536		create_op.state_forwards("app", &mut state);
3537
3538		let alter_op = Operation::AlterColumn {
3539			table: "orders".to_string(),
3540			column: "customer_id".to_string(),
3541			old_definition: None,
3542			new_definition: ColumnDefinition::new(
3543				"customer_id",
3544				FieldType::Custom("INTEGER NOT NULL".to_string()),
3545			),
3546			mysql_options: None,
3547		};
3548		alter_op.state_forwards("app", &mut state);
3549
3550		assert!(
3551			state
3552				.get_model("app", "orders")
3553				.unwrap()
3554				.fields
3555				.contains_key("customer_id")
3556		);
3557	}
3558
3559	#[test]
3560	// From: Django/migrations
3561	fn test_alter_fk_before_model_deletion() {
3562		use crate::migrations::ProjectState;
3563		use crate::migrations::operations::*;
3564
3565		let mut state = ProjectState::new();
3566
3567		let create_old = Operation::CreateTable {
3568			name: "old_table".to_string(),
3569			columns: vec![ColumnDefinition::new(
3570				"id",
3571				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
3572			)],
3573			constraints: vec![],
3574			without_rowid: None,
3575			partition: None,
3576			interleave_in_parent: None,
3577		};
3578		create_old.state_forwards("testapp", &mut state);
3579
3580		let create_new = Operation::CreateTable {
3581			name: "new_table".to_string(),
3582			columns: vec![ColumnDefinition::new(
3583				"id",
3584				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
3585			)],
3586			constraints: vec![],
3587			without_rowid: None,
3588			partition: None,
3589			interleave_in_parent: None,
3590		};
3591		create_new.state_forwards("testapp", &mut state);
3592
3593		let create_ref = Operation::CreateTable {
3594			name: "referencing".to_string(),
3595			columns: vec![
3596				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3597				ColumnDefinition::new(
3598					"ref_id",
3599					FieldType::Custom("INTEGER REFERENCES old_table(id)".to_string()),
3600				),
3601			],
3602			constraints: vec![],
3603			without_rowid: None,
3604			partition: None,
3605			interleave_in_parent: None,
3606		};
3607		create_ref.state_forwards("testapp", &mut state);
3608
3609		// Change FK to point to new_table before deleting old_table
3610		let alter_fk = Operation::AlterColumn {
3611			table: "referencing".to_string(),
3612			column: "ref_id".to_string(),
3613			old_definition: None,
3614			new_definition: ColumnDefinition::new(
3615				"ref_id",
3616				FieldType::Custom("INTEGER REFERENCES new_table(id)".to_string()),
3617			),
3618			mysql_options: None,
3619		};
3620		alter_fk.state_forwards("testapp", &mut state);
3621
3622		// Now safe to delete old_table
3623		let drop_old = Operation::DropTable {
3624			name: "old_table".to_string(),
3625		};
3626		drop_old.state_forwards("testapp", &mut state);
3627
3628		assert!(state.get_model("testapp", "old_table").is_none());
3629		assert!(state.get_model("testapp", "referencing").is_some());
3630	}
3631
3632	#[test]
3633	// From: Django/migrations
3634	fn test_alter_fk_before_model_deletion_1() {
3635		use crate::migrations::ProjectState;
3636		use crate::migrations::operations::*;
3637
3638		let mut state = ProjectState::new();
3639
3640		let create_categories = Operation::CreateTable {
3641			name: "categories".to_string(),
3642			columns: vec![ColumnDefinition::new(
3643				"id",
3644				FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
3645			)],
3646			constraints: vec![],
3647			without_rowid: None,
3648			partition: None,
3649			interleave_in_parent: None,
3650		};
3651		create_categories.state_forwards("app", &mut state);
3652
3653		let create_products = Operation::CreateTable {
3654			name: "products".to_string(),
3655			columns: vec![
3656				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3657				ColumnDefinition::new("cat_id", FieldType::Integer),
3658			],
3659			constraints: vec![],
3660			without_rowid: None,
3661			partition: None,
3662			interleave_in_parent: None,
3663		};
3664		create_products.state_forwards("app", &mut state);
3665
3666		// Remove FK constraint or set to NULL before deletion
3667		let alter_op = Operation::AlterColumn {
3668			table: "products".to_string(),
3669			column: "cat_id".to_string(),
3670			old_definition: None,
3671			new_definition: ColumnDefinition::new(
3672				"cat_id",
3673				FieldType::Custom("INTEGER NULL".to_string()),
3674			),
3675			mysql_options: None,
3676		};
3677		alter_op.state_forwards("app", &mut state);
3678
3679		assert!(state.get_model("app", "products").is_some());
3680	}
3681
3682	#[test]
3683	// From: Django/migrations
3684	fn test_alter_many_to_many() {
3685		// Tests altering a many-to-many association table by adding extra fields
3686		use crate::migrations::ProjectState;
3687		use crate::migrations::operations::*;
3688
3689		let mut state = ProjectState::new();
3690
3691		// Create two models
3692		let create_authors = Operation::CreateTable {
3693			name: "authors".to_string(),
3694			columns: vec![
3695				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3696				ColumnDefinition::new("name", FieldType::VarChar(100)),
3697			],
3698			constraints: vec![],
3699			without_rowid: None,
3700			partition: None,
3701			interleave_in_parent: None,
3702		};
3703		create_authors.state_forwards("library", &mut state);
3704
3705		let create_books = Operation::CreateTable {
3706			name: "books".to_string(),
3707			columns: vec![
3708				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3709				ColumnDefinition::new("title", FieldType::VarChar(200)),
3710			],
3711			constraints: vec![],
3712			without_rowid: None,
3713			partition: None,
3714			interleave_in_parent: None,
3715		};
3716		create_books.state_forwards("library", &mut state);
3717
3718		// Create association table for many-to-many
3719		let create_assoc = Operation::CreateTable {
3720			name: "authors_books".to_string(),
3721			columns: vec![
3722				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3723				ColumnDefinition::new(
3724					"author_id",
3725					FieldType::Custom("INTEGER REFERENCES authors(id)".to_string()),
3726				),
3727				ColumnDefinition::new(
3728					"book_id",
3729					FieldType::Custom("INTEGER REFERENCES books(id)".to_string()),
3730				),
3731			],
3732			constraints: vec![Constraint::Unique {
3733				name: "unique_author_book".to_string(),
3734				columns: vec!["author_id".to_string(), "book_id".to_string()],
3735			}],
3736			without_rowid: None,
3737			partition: None,
3738			interleave_in_parent: None,
3739		};
3740		create_assoc.state_forwards("library", &mut state);
3741
3742		// Alter the association table by adding extra metadata fields
3743		let add_created_at = Operation::AddColumn {
3744			table: "authors_books".to_string(),
3745			column: ColumnDefinition::new(
3746				"created_at",
3747				FieldType::Custom(
3748					FieldType::Custom("TIMESTAMP DEFAULT CURRENT_TIMESTAMP".to_string())
3749						.to_string(),
3750				),
3751			),
3752			mysql_options: None,
3753		};
3754		add_created_at.state_forwards("library", &mut state);
3755
3756		let add_role = Operation::AddColumn {
3757			table: "authors_books".to_string(),
3758			column: ColumnDefinition::new("contribution_role", FieldType::VarChar(50)),
3759			mysql_options: None,
3760		};
3761		add_role.state_forwards("library", &mut state);
3762
3763		// Verify the association table has been altered
3764		let assoc_model = state.get_model("library", "authors_books").unwrap();
3765		assert!(assoc_model.fields.contains_key("author_id"));
3766		assert!(assoc_model.fields.contains_key("book_id"));
3767		assert!(assoc_model.fields.contains_key("created_at"));
3768		assert!(assoc_model.fields.contains_key("contribution_role"));
3769	}
3770
3771	#[test]
3772	// From: Django/migrations
3773	fn test_alter_many_to_many_1() {
3774		// Tests altering a many-to-many by changing field types in association table
3775		use crate::migrations::ProjectState;
3776		use crate::migrations::operations::*;
3777
3778		let mut state = ProjectState::new();
3779
3780		// Create two models
3781		let create_students = Operation::CreateTable {
3782			name: "students".to_string(),
3783			columns: vec![
3784				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3785				ColumnDefinition::new("name", FieldType::VarChar(100)),
3786			],
3787			constraints: vec![],
3788			without_rowid: None,
3789			partition: None,
3790			interleave_in_parent: None,
3791		};
3792		create_students.state_forwards("school", &mut state);
3793
3794		let create_courses = Operation::CreateTable {
3795			name: "courses".to_string(),
3796			columns: vec![
3797				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3798				ColumnDefinition::new("title", FieldType::VarChar(200)),
3799			],
3800			constraints: vec![],
3801			without_rowid: None,
3802			partition: None,
3803			interleave_in_parent: None,
3804		};
3805		create_courses.state_forwards("school", &mut state);
3806
3807		// Create association table
3808		let create_enrollment = Operation::CreateTable {
3809			name: "enrollments".to_string(),
3810			columns: vec![
3811				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3812				ColumnDefinition::new(
3813					"student_id",
3814					FieldType::Custom("INTEGER REFERENCES students(id)".to_string()),
3815				),
3816				ColumnDefinition::new(
3817					"course_id",
3818					FieldType::Custom("INTEGER REFERENCES courses(id)".to_string()),
3819				),
3820				ColumnDefinition::new("grade", FieldType::VarChar(2)),
3821			],
3822			constraints: vec![],
3823			without_rowid: None,
3824			partition: None,
3825			interleave_in_parent: None,
3826		};
3827		create_enrollment.state_forwards("school", &mut state);
3828
3829		// Alter the grade field to use a numeric type instead
3830		let alter_grade = Operation::AlterColumn {
3831			table: "enrollments".to_string(),
3832			column: "grade".to_string(),
3833			old_definition: None,
3834			new_definition: ColumnDefinition::new(
3835				"grade",
3836				FieldType::Decimal {
3837					precision: 3,
3838					scale: 2,
3839				},
3840			),
3841			mysql_options: None,
3842		};
3843		alter_grade.state_forwards("school", &mut state);
3844
3845		// Add an index on the association table
3846		let add_index = Operation::CreateIndex {
3847			table: "enrollments".to_string(),
3848			columns: vec!["student_id".to_string(), "course_id".to_string()],
3849			unique: true,
3850			index_type: None,
3851			where_clause: None,
3852			concurrently: false,
3853			expressions: None,
3854			mysql_options: None,
3855			operator_class: None,
3856		};
3857		add_index.state_forwards("school", &mut state);
3858
3859		let enrollment_model = state.get_model("school", "enrollments").unwrap();
3860		assert!(enrollment_model.fields.contains_key("student_id"));
3861		assert!(enrollment_model.fields.contains_key("course_id"));
3862		assert!(enrollment_model.fields.contains_key("grade"));
3863	}
3864
3865	#[test]
3866	#[should_panic(expected = "runtime-only")]
3867	// From: Django/migrations
3868	fn test_alter_model_managers() {
3869		// Model managers are application-level constructs that don't affect database schema.
3870		// They define custom query methods and default querysets for models.
3871		// Migrations don't need to handle manager changes since they're runtime-only.
3872		//
3873		// Use reinhardt-migrations types
3874		use crate::migrations::operations::Operation;
3875		let _ = std::any::type_name::<Operation>();
3876
3877		// This test intentionally panics to demonstrate that managers are not a migration concern.
3878		// Managers are defined in application code and only affect how queries are built at runtime.
3879		panic!(
3880			"Model managers are runtime-only and don't require migration support. See reinhardt-orm manager module"
3881		)
3882	}
3883
3884	#[test]
3885	#[should_panic(expected = "runtime-only")]
3886	// From: Django/migrations
3887	fn test_alter_model_managers_1() {
3888		// See test_alter_model_managers for details
3889		// Use reinhardt-migrations types
3890		use crate::migrations::ProjectState;
3891		let _ = std::any::type_name::<ProjectState>();
3892
3893		// This test also intentionally panics for the same reason.
3894		panic!(
3895			"Model managers are runtime-only and don't require migration support. See reinhardt-orm manager module"
3896		)
3897	}
3898
3899	#[test]
3900	// From: Django/migrations
3901	fn test_alter_model_options() {
3902		use crate::migrations::ProjectState;
3903		use crate::migrations::operations::*;
3904		use std::collections::HashMap;
3905
3906		let mut state = ProjectState::new();
3907
3908		let create_op = Operation::CreateTable {
3909			name: "articles".to_string(),
3910			columns: vec![
3911				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3912				ColumnDefinition::new("title", FieldType::VarChar(255)),
3913				ColumnDefinition::new("created_at", FieldType::Custom("TIMESTAMP".to_string())),
3914			],
3915			constraints: vec![],
3916			without_rowid: None,
3917			partition: None,
3918			interleave_in_parent: None,
3919		};
3920		create_op.state_forwards("app", &mut state);
3921
3922		let mut options = HashMap::new();
3923		options.insert("ordering".to_string(), "created_at".to_string());
3924		options.insert("verbose_name".to_string(), "Article".to_string());
3925
3926		let alter_op = Operation::AlterModelOptions {
3927			table: "articles".to_string(),
3928			options,
3929		};
3930		alter_op.state_forwards("app", &mut state);
3931
3932		assert!(state.get_model("app", "articles").is_some());
3933	}
3934
3935	#[test]
3936	// From: Django/migrations
3937	fn test_alter_model_options_1() {
3938		use crate::migrations::ProjectState;
3939		use crate::migrations::operations::*;
3940		use std::collections::HashMap;
3941
3942		let mut state = ProjectState::new();
3943
3944		let create_op = Operation::CreateTable {
3945			name: "products".to_string(),
3946			columns: vec![
3947				ColumnDefinition::new("id", FieldType::Custom("INTEGER PRIMARY KEY".to_string())),
3948				ColumnDefinition::new("name", FieldType::VarChar(255)),
3949				ColumnDefinition::new(
3950					"price",
3951					FieldType::Decimal {
3952						precision: 10,
3953						scale: 2,
3954					},
3955				),
3956			],
3957			constraints: vec![],
3958			without_rowid: None,
3959			partition: None,
3960			interleave_in_parent: None,
3961		};
3962		create_op.state_forwards("app", &mut state);
3963
3964		let mut options = HashMap::new();
3965		options.insert("ordering".to_string(), "-price".to_string());
3966		options.insert("verbose_name_plural".to_string(), "Products".to_string());
3967
3968		let alter_op = Operation::AlterModelOptions {
3969			table: "products".to_string(),
3970			options,
3971		};
3972		alter_op.state_forwards("app", &mut state);
3973
3974		assert!(state.get_model("app", "products").is_some());
3975	}
3976
3977	#[test]
3978	#[should_panic(expected = "don't affect database schema")]
3979	// From: Django/migrations
3980	fn test_alter_model_options_proxy() {
3981		// Proxy models in Django are models that don't have their own database table.
3982		// They inherit from a concrete model and can have different behavior/methods.
3983		// Migrations typically ignore proxy models since they don't affect schema.
3984		// Note: This is primarily a Django ORM feature for model organization
3985		//
3986		// Use reinhardt-migrations Migration type
3987		use super::Migration;
3988		let _ = std::any::type_name::<Migration>();
3989
3990		// This test intentionally panics to demonstrate that proxy models are schema-independent.
3991		// Proxy models are purely for code organization and behavior customization.
3992		// They share the parent model's table and therefore require no migrations.
3993		panic!("Proxy models don't require migrations as they don't affect database schema")
3994	}
3995
3996	#[test]
3997	#[should_panic(expected = "don't affect database schema")]
3998	// From: Django/migrations
3999	fn test_alter_model_options_proxy_1() {
4000		// See test_alter_model_options_proxy for details
4001		// Use reinhardt-migrations ColumnDefinition type
4002		use crate::migrations::ColumnDefinition;
4003		let _ = std::any::type_name::<ColumnDefinition>();
4004
4005		// This test also intentionally panics for the same reason.
4006		panic!("Proxy models don't require migrations as they don't affect database schema")
4007	}
4008
4009	#[test]
4010	// From: Django/migrations
4011	fn test_alter_regex_string_to_compiled_regex() {
4012		// Regex validators are application-level validation, not database schema.
4013		// They validate input before it reaches the database.
4014		// Note: reinhardt-orm has RegexValidator in src/validators.rs
4015		// Migrations don't need to handle validator changes as they're runtime-only.
4016
4017		// This test would verify that changing a regex validator doesn't generate migrations
4018		// In practice, we just ensure no migration operations are generated
4019		use crate::migrations::ProjectState;
4020
4021		let state = ProjectState::new();
4022		// No operations needed - validators are not part of schema
4023		assert!(state.models.is_empty());
4024	}
4025
4026	#[test]
4027	// From: Django/migrations
4028	fn test_alter_regex_string_to_compiled_regex_1() {
4029		// Validators (including regex) are runtime-only and don't affect schema
4030		use crate::migrations::ProjectState;
4031
4032		let state = ProjectState::new();
4033		// Changing a regex validator doesn't require any migration
4034		assert!(state.models.is_empty());
4035	}
4036}