Skip to main content

reinhardt_db/migrations/operations/
fields.rs

1//! Field operations for migrations
2//!
3//! This module provides operations for adding, removing, altering, and renaming fields,
4//! inspired by Django's `django/db/migrations/operations/fields.py`.
5//!
6//! # Example
7//!
8//! ```rust
9//! use reinhardt_db::migrations::operations::fields::{AddField, RemoveField};
10//! use reinhardt_db::migrations::operations::FieldDefinition;
11//! use reinhardt_db::migrations::operations::models::CreateModel;
12//! use reinhardt_db::migrations::{ProjectState, FieldType};
13//!
14//! let mut state = ProjectState::new();
15//!
16//! // Create a model first
17//! let create = CreateModel::new(
18//!     "User",
19//!     vec![FieldDefinition::new("id", FieldType::Integer, true, false, Option::<&str>::None)],
20//! );
21//! create.state_forwards("myapp", &mut state);
22//!
23//! // Add a field
24//! let add = AddField::new("User", FieldDefinition::new("email", FieldType::VarChar(255), false, false, Option::<&str>::None));
25//! add.state_forwards("myapp", &mut state);
26//! assert_eq!(state.get_model("myapp", "User").unwrap().fields.len(), 2);
27//!
28//! // Remove a field
29//! let remove = RemoveField::new("User", "email");
30//! remove.state_forwards("myapp", &mut state);
31//! assert_eq!(state.get_model("myapp", "User").unwrap().fields.len(), 1);
32//! ```
33
34use super::{FieldState, ProjectState};
35use crate::backends::schema::BaseDatabaseSchemaEditor;
36use serde::{Deserialize, Serialize};
37
38pub use super::models::FieldDefinition;
39
40/// Add a field to an existing model
41///
42/// # Example
43///
44/// ```rust
45/// use reinhardt_db::migrations::operations::fields::AddField;
46/// use reinhardt_db::migrations::operations::FieldDefinition;
47/// use reinhardt_db::migrations::operations::models::CreateModel;
48/// use reinhardt_db::migrations::{ProjectState, FieldType};
49///
50/// let mut state = ProjectState::new();
51///
52/// // Create a model first
53/// let create = CreateModel::new(
54///     "User",
55///     vec![FieldDefinition::new("id", FieldType::Integer, true, false, Option::<&str>::None)],
56/// );
57/// create.state_forwards("myapp", &mut state);
58///
59/// // Add a field
60/// let add = AddField::new("User", FieldDefinition::new("email", FieldType::VarChar(255), false, false, Option::<&str>::None));
61/// add.state_forwards("myapp", &mut state);
62///
63/// let model = state.get_model("myapp", "User").unwrap();
64/// assert_eq!(model.fields.len(), 2);
65/// assert!(model.fields.contains_key("email"));
66/// ```
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct AddField {
69	/// The model name.
70	pub model_name: String,
71	/// The field.
72	pub field: FieldDefinition,
73	/// The preserve default.
74	pub preserve_default: bool,
75}
76
77impl AddField {
78	/// Create a new AddField operation
79	pub fn new(model_name: impl Into<String>, field: FieldDefinition) -> Self {
80		Self {
81			model_name: model_name.into(),
82			field,
83			preserve_default: true,
84		}
85	}
86
87	/// Set whether to preserve the default value after adding
88	pub fn with_preserve_default(mut self, preserve: bool) -> Self {
89		self.preserve_default = preserve;
90		self
91	}
92
93	/// Apply to project state (forward)
94	pub fn state_forwards(&self, app_label: &str, state: &mut ProjectState) {
95		if let Some(model) = state.get_model_mut(app_label, &self.model_name) {
96			let field = FieldState::new(
97				self.field.name.clone(),
98				self.field.field_type.clone(),
99				self.field.primary_key,
100			);
101			model.add_field(field);
102		}
103	}
104
105	/// Generate SQL using schema editor
106	///
107	/// # Example
108	///
109	/// ```rust,no_run
110	/// use reinhardt_db::migrations::operations::fields::AddField;
111	/// use reinhardt_db::migrations::operations::FieldDefinition;
112	/// use reinhardt_db::migrations::FieldType;
113	/// use reinhardt_db::backends::schema::factory::{SchemaEditorFactory, DatabaseType};
114	///
115	/// let add = AddField::new("users", FieldDefinition::new("email", FieldType::VarChar(255), false, false, Option::<&str>::None));
116	/// let factory = SchemaEditorFactory::new();
117	/// let editor = factory.create_for_database(DatabaseType::PostgreSQL);
118	///
119	/// let sql = add.database_forwards(editor.as_ref());
120	/// assert_eq!(sql.len(), 1);
121	/// assert!(sql[0].contains("ALTER TABLE"));
122	/// assert!(sql[0].contains("ADD COLUMN"));
123	/// assert!(sql[0].contains("\"email\""));
124	/// ```
125	pub fn database_forwards(&self, schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
126		let definition = self.field.to_sql_definition();
127		let stmt =
128			schema_editor.add_column_statement(&self.model_name, &self.field.name, &definition);
129		vec![schema_editor.build_alter_table_sql(&stmt)]
130	}
131}
132
133/// Remove a field from a model
134///
135/// # Example
136///
137/// ```rust
138/// use reinhardt_db::migrations::operations::fields::RemoveField;
139/// use reinhardt_db::migrations::operations::FieldDefinition;
140/// use reinhardt_db::migrations::operations::models::CreateModel;
141/// use reinhardt_db::migrations::{ProjectState, FieldType};
142///
143/// let mut state = ProjectState::new();
144///
145/// // Create a model with fields
146/// let create = CreateModel::new(
147///     "User",
148///     vec![
149///         FieldDefinition::new("id", FieldType::Integer, true, false, Option::<&str>::None),
150///         FieldDefinition::new("email", FieldType::VarChar(255), false, false, Option::<&str>::None),
151///     ],
152/// );
153/// create.state_forwards("myapp", &mut state);
154///
155/// // Remove a field
156/// let remove = RemoveField::new("User", "email");
157/// remove.state_forwards("myapp", &mut state);
158///
159/// let model = state.get_model("myapp", "User").unwrap();
160/// assert_eq!(model.fields.len(), 1);
161/// assert!(!model.fields.contains_key("email"));
162/// ```
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct RemoveField {
165	/// The model name.
166	pub model_name: String,
167	/// The field name.
168	pub field_name: String,
169}
170
171impl RemoveField {
172	/// Create a new RemoveField operation
173	pub fn new(model_name: impl Into<String>, field_name: impl Into<String>) -> Self {
174		Self {
175			model_name: model_name.into(),
176			field_name: field_name.into(),
177		}
178	}
179
180	/// Apply to project state (forward)
181	pub fn state_forwards(&self, app_label: &str, state: &mut ProjectState) {
182		if let Some(model) = state.get_model_mut(app_label, &self.model_name) {
183			model.remove_field(&self.field_name);
184		}
185	}
186
187	/// Generate SQL using schema editor
188	///
189	/// # Example
190	///
191	/// ```rust,no_run
192	/// use reinhardt_db::migrations::operations::fields::RemoveField;
193	/// use reinhardt_db::backends::schema::factory::{SchemaEditorFactory, DatabaseType};
194	///
195	/// let remove = RemoveField::new("users", "email");
196	/// let factory = SchemaEditorFactory::new();
197	/// let editor = factory.create_for_database(DatabaseType::PostgreSQL);
198	///
199	/// let sql = remove.database_forwards(editor.as_ref());
200	/// assert_eq!(sql.len(), 1);
201	/// assert!(sql[0].contains("ALTER TABLE"));
202	/// assert!(sql[0].contains("DROP COLUMN"));
203	/// assert!(sql[0].contains("\"email\""));
204	/// ```
205	pub fn database_forwards(&self, schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
206		let stmt = schema_editor.drop_column_statement(&self.model_name, &self.field_name);
207		vec![schema_editor.build_alter_table_sql(&stmt)]
208	}
209}
210
211/// Alter a field's definition
212///
213/// # Example
214///
215/// ```rust
216/// use reinhardt_db::migrations::operations::fields::AlterField;
217/// use reinhardt_db::migrations::operations::FieldDefinition;
218/// use reinhardt_db::migrations::operations::models::CreateModel;
219/// use reinhardt_db::migrations::{ProjectState, FieldType};
220///
221/// let mut state = ProjectState::new();
222///
223/// // Create a model with a field
224/// let create = CreateModel::new(
225///     "User",
226///     vec![
227///         FieldDefinition::new("id", FieldType::Integer, true, false, Option::<&str>::None),
228///         FieldDefinition::new("email", FieldType::VarChar(100), false, false, Option::<&str>::None),
229///     ],
230/// );
231/// create.state_forwards("myapp", &mut state);
232///
233/// // Alter the field to make it longer
234/// let alter = AlterField::new("User", FieldDefinition::new("email", FieldType::VarChar(255), false, false, Option::<&str>::None));
235/// alter.state_forwards("myapp", &mut state);
236///
237/// let model = state.get_model("myapp", "User").unwrap();
238/// let field = model.fields.get("email").unwrap();
239/// assert_eq!(field.field_type, FieldType::VarChar(255));
240/// ```
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct AlterField {
243	/// The model name.
244	pub model_name: String,
245	/// The field.
246	pub field: FieldDefinition,
247}
248
249impl AlterField {
250	/// Create a new AlterField operation
251	pub fn new(model_name: impl Into<String>, field: FieldDefinition) -> Self {
252		Self {
253			model_name: model_name.into(),
254			field,
255		}
256	}
257
258	/// Apply to project state (forward)
259	pub fn state_forwards(&self, app_label: &str, state: &mut ProjectState) {
260		if let Some(model) = state.get_model_mut(app_label, &self.model_name) {
261			let field = FieldState::new(
262				self.field.name.clone(),
263				self.field.field_type.clone(),
264				self.field.primary_key,
265			);
266			model.alter_field(&self.field.name, field);
267		}
268	}
269
270	/// Generate SQL using schema editor
271	///
272	/// Note: Altering columns is database-specific and complex.
273	/// This is a simplified version that may need enhancement.
274	///
275	/// # Example
276	///
277	/// ```rust,no_run
278	/// use reinhardt_db::migrations::operations::fields::AlterField;
279	/// use reinhardt_db::migrations::operations::FieldDefinition;
280	/// use reinhardt_db::migrations::FieldType;
281	/// use reinhardt_db::backends::schema::factory::{SchemaEditorFactory, DatabaseType};
282	///
283	/// let alter = AlterField::new("users", FieldDefinition::new("email", FieldType::VarChar(500), false, false, Option::<&str>::None));
284	/// let factory = SchemaEditorFactory::new();
285	/// let editor = factory.create_for_database(DatabaseType::PostgreSQL);
286	///
287	/// let sql = alter.database_forwards(editor.as_ref());
288	/// assert!(!sql.is_empty());
289	/// ```
290	pub fn database_forwards(&self, schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
291		// Use database-specific ALTER COLUMN statement from schema editor
292		// Each database backend (PostgreSQL, MySQL, SQLite, CockroachDB) provides
293		// its own implementation via the alter_column_statement() method
294		vec![schema_editor.alter_column_statement(
295			&self.model_name,
296			&self.field.name,
297			&self.field.field_type.to_sql_string(),
298		)]
299	}
300}
301
302/// Rename a field
303///
304/// # Example
305///
306/// ```rust
307/// use reinhardt_db::migrations::operations::fields::RenameField;
308/// use reinhardt_db::migrations::operations::FieldDefinition;
309/// use reinhardt_db::migrations::operations::models::CreateModel;
310/// use reinhardt_db::migrations::{ProjectState, FieldType};
311///
312/// let mut state = ProjectState::new();
313///
314/// // Create a model with a field
315/// let create = CreateModel::new(
316///     "User",
317///     vec![
318///         FieldDefinition::new("id", FieldType::Integer, true, false, Option::<&str>::None),
319///         FieldDefinition::new("email", FieldType::VarChar(255), false, false, Option::<&str>::None),
320///     ],
321/// );
322/// create.state_forwards("myapp", &mut state);
323///
324/// // Rename the field
325/// let rename = RenameField::new("User", "email", "email_address");
326/// rename.state_forwards("myapp", &mut state);
327///
328/// let model = state.get_model("myapp", "User").unwrap();
329/// assert!(!model.fields.contains_key("email"));
330/// assert!(model.fields.contains_key("email_address"));
331/// ```
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct RenameField {
334	/// The model name.
335	pub model_name: String,
336	/// The old name.
337	pub old_name: String,
338	/// The new name.
339	pub new_name: String,
340}
341
342impl RenameField {
343	/// Create a new RenameField operation
344	pub fn new(
345		model_name: impl Into<String>,
346		old_name: impl Into<String>,
347		new_name: impl Into<String>,
348	) -> Self {
349		Self {
350			model_name: model_name.into(),
351			old_name: old_name.into(),
352			new_name: new_name.into(),
353		}
354	}
355
356	/// Apply to project state (forward)
357	pub fn state_forwards(&self, app_label: &str, state: &mut ProjectState) {
358		if let Some(model) = state.get_model_mut(app_label, &self.model_name) {
359			model.rename_field(&self.old_name, self.new_name.clone());
360		}
361	}
362
363	/// Generate SQL using schema editor
364	///
365	/// # Example
366	///
367	/// ```rust,no_run
368	/// use reinhardt_db::migrations::operations::fields::RenameField;
369	/// use reinhardt_db::backends::schema::factory::{SchemaEditorFactory, DatabaseType};
370	///
371	/// let rename = RenameField::new("users", "email", "email_address");
372	/// let factory = SchemaEditorFactory::new();
373	/// let editor = factory.create_for_database(DatabaseType::PostgreSQL);
374	///
375	/// let sql = rename.database_forwards(editor.as_ref());
376	/// assert_eq!(sql.len(), 1);
377	/// assert!(sql[0].contains("ALTER TABLE"));
378	/// assert!(sql[0].contains("RENAME COLUMN"));
379	/// assert!(sql[0].contains("\"email\""));
380	/// assert!(sql[0].contains("\"email_address\""));
381	/// ```
382	pub fn database_forwards(&self, schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
383		vec![schema_editor.rename_column_statement(
384			&self.model_name,
385			&self.old_name,
386			&self.new_name,
387		)]
388	}
389}
390
391// MigrationOperation trait implementation for Django-style naming
392use crate::migrations::operation_trait::MigrationOperation;
393
394impl MigrationOperation for AddField {
395	fn migration_name_fragment(&self) -> Option<String> {
396		Some(format!(
397			"{}_{}",
398			self.model_name.to_lowercase(),
399			self.field.name.to_lowercase()
400		))
401	}
402
403	fn describe(&self) -> String {
404		format!("Add field {} to {}", self.field.name, self.model_name)
405	}
406}
407
408impl MigrationOperation for RemoveField {
409	fn migration_name_fragment(&self) -> Option<String> {
410		Some(format!(
411			"remove_{}_{}",
412			self.model_name.to_lowercase(),
413			self.field_name.to_lowercase()
414		))
415	}
416
417	fn describe(&self) -> String {
418		format!("Remove field {} from {}", self.field_name, self.model_name)
419	}
420}
421
422impl MigrationOperation for AlterField {
423	fn migration_name_fragment(&self) -> Option<String> {
424		Some(format!(
425			"alter_{}_{}",
426			self.model_name.to_lowercase(),
427			self.field.name.to_lowercase()
428		))
429	}
430
431	fn describe(&self) -> String {
432		format!("Alter field {} on {}", self.field.name, self.model_name)
433	}
434}
435
436impl MigrationOperation for RenameField {
437	fn migration_name_fragment(&self) -> Option<String> {
438		Some(format!(
439			"rename_{}_{}",
440			self.model_name.to_lowercase(),
441			self.new_name.to_lowercase()
442		))
443	}
444
445	fn describe(&self) -> String {
446		format!(
447			"Rename field {} to {} on {}",
448			self.old_name, self.new_name, self.model_name
449		)
450	}
451}
452
453#[cfg(test)]
454mod tests {
455	use super::*;
456	use crate::migrations::FieldType;
457	use crate::migrations::operations::models::CreateModel;
458
459	#[test]
460	fn test_add_field_state_forwards() {
461		let mut state = ProjectState::new();
462
463		// Create a model first
464		let create = CreateModel::new(
465			"User",
466			vec![FieldDefinition::new(
467				"id",
468				FieldType::Integer,
469				true,
470				false,
471				None::<String>,
472			)],
473		);
474		create.state_forwards("myapp", &mut state);
475
476		// Add a field
477		let add = AddField::new(
478			"User",
479			FieldDefinition::new(
480				"email",
481				FieldType::VarChar(255),
482				false,
483				false,
484				None::<String>,
485			),
486		);
487		add.state_forwards("myapp", &mut state);
488
489		let model = state.get_model("myapp", "User").unwrap();
490		assert_eq!(model.fields.len(), 2);
491		assert!(model.fields.contains_key("email"));
492	}
493
494	#[test]
495	fn test_remove_field_state_forwards() {
496		let mut state = ProjectState::new();
497
498		// Create a model with fields
499		let create = CreateModel::new(
500			"User",
501			vec![
502				FieldDefinition::new("id", FieldType::Integer, true, false, None::<String>),
503				FieldDefinition::new(
504					"email",
505					FieldType::VarChar(255),
506					false,
507					false,
508					None::<String>,
509				),
510			],
511		);
512		create.state_forwards("myapp", &mut state);
513
514		// Remove a field
515		let remove = RemoveField::new("User", "email");
516		remove.state_forwards("myapp", &mut state);
517
518		let model = state.get_model("myapp", "User").unwrap();
519		assert_eq!(model.fields.len(), 1);
520		assert!(!model.fields.contains_key("email"));
521	}
522
523	#[test]
524	fn test_alter_field_state_forwards() {
525		let mut state = ProjectState::new();
526
527		// Create a model with a field
528		let create = CreateModel::new(
529			"User",
530			vec![
531				FieldDefinition::new("id", FieldType::Integer, true, false, None::<String>),
532				FieldDefinition::new(
533					"email",
534					FieldType::VarChar(100),
535					false,
536					false,
537					None::<String>,
538				),
539			],
540		);
541		create.state_forwards("myapp", &mut state);
542
543		// Alter the field
544		let alter = AlterField::new(
545			"User",
546			FieldDefinition::new(
547				"email",
548				FieldType::VarChar(255),
549				false,
550				false,
551				None::<String>,
552			),
553		);
554		alter.state_forwards("myapp", &mut state);
555
556		let model = state.get_model("myapp", "User").unwrap();
557		let field = model.fields.get("email").unwrap();
558		assert_eq!(field.field_type, FieldType::VarChar(255));
559	}
560
561	#[test]
562	fn test_rename_field_state_forwards() {
563		let mut state = ProjectState::new();
564
565		// Create a model with a field
566		let create = CreateModel::new(
567			"User",
568			vec![
569				FieldDefinition::new("id", FieldType::Integer, true, false, None::<String>),
570				FieldDefinition::new(
571					"email",
572					FieldType::VarChar(255),
573					false,
574					false,
575					None::<String>,
576				),
577			],
578		);
579		create.state_forwards("myapp", &mut state);
580
581		// Rename the field
582		let rename = RenameField::new("User", "email", "email_address");
583		rename.state_forwards("myapp", &mut state);
584
585		let model = state.get_model("myapp", "User").unwrap();
586		assert!(!model.fields.contains_key("email"));
587		assert!(model.fields.contains_key("email_address"));
588	}
589
590	#[cfg(feature = "postgres")]
591	#[test]
592	fn test_add_field_database_forwards() {
593		use crate::backends::schema::test_utils::MockSchemaEditor;
594
595		let add = AddField::new(
596			"users",
597			FieldDefinition::new(
598				"email",
599				FieldType::VarChar(255),
600				false,
601				false,
602				None::<String>,
603			),
604		);
605		let editor = MockSchemaEditor::new();
606
607		let sql = add.database_forwards(&editor);
608		assert_eq!(sql.len(), 1);
609		assert!(sql[0].contains("ALTER TABLE"));
610		assert!(sql[0].contains("ADD COLUMN"));
611		assert!(sql[0].contains("\"email\""));
612	}
613
614	#[cfg(feature = "postgres")]
615	#[test]
616	fn test_remove_field_database_forwards() {
617		use crate::backends::schema::test_utils::MockSchemaEditor;
618
619		let remove = RemoveField::new("users", "email");
620		let editor = MockSchemaEditor::new();
621
622		let sql = remove.database_forwards(&editor);
623		assert_eq!(sql.len(), 1);
624		assert!(sql[0].contains("ALTER TABLE"));
625		assert!(sql[0].contains("DROP COLUMN"));
626		assert!(sql[0].contains("\"email\""));
627	}
628
629	#[cfg(feature = "postgres")]
630	#[test]
631	fn test_rename_field_database_forwards() {
632		use crate::backends::schema::test_utils::MockSchemaEditor;
633
634		let rename = RenameField::new("users", "email", "email_address");
635		let editor = MockSchemaEditor::new();
636
637		let sql = rename.database_forwards(&editor);
638		assert_eq!(sql.len(), 1);
639		assert!(sql[0].contains("ALTER TABLE"));
640		assert!(sql[0].contains("RENAME COLUMN"));
641		assert!(sql[0].contains("\"email\""));
642		assert!(sql[0].contains("\"email_address\""));
643	}
644}