Skip to main content

reinhardt_db/migrations/operations/
special.rs

1//! Special operations for migrations
2//!
3//! This module provides special operations like RunSQL and RunRust,
4//! inspired by Django's `django/db/migrations/operations/special.py`.
5//!
6//! # Example
7//!
8//! ```rust
9//! use reinhardt_db::migrations::operations::special::RunSQL;
10//!
11//! // Create a RunSQL operation
12//! let run_sql = RunSQL::new(
13//!     "INSERT INTO users (name, email) VALUES ('admin', 'admin@example.com')",
14//! ).with_reverse_sql("DELETE FROM users WHERE email = 'admin@example.com'");
15//!
16//! // Get forward SQL
17//! assert_eq!(run_sql.sql, "INSERT INTO users (name, email) VALUES ('admin', 'admin@example.com')");
18//! ```
19
20use crate::backends::connection::DatabaseConnection;
21use crate::backends::schema::BaseDatabaseSchemaEditor;
22use crate::migrations::ProjectState;
23use serde::{Deserialize, Serialize};
24
25/// Execute raw SQL
26///
27/// This operation allows you to execute arbitrary SQL statements during migration.
28/// It's useful for data migrations, custom schema modifications, or database-specific operations.
29///
30/// # Example
31///
32/// ```rust
33/// use reinhardt_db::migrations::operations::special::RunSQL;
34///
35/// // Simple SQL execution
36/// let sql = RunSQL::new("CREATE INDEX idx_email ON users(email)");
37///
38/// // With reverse SQL for rollback
39/// let sql_reversible = RunSQL::new("CREATE INDEX idx_email ON users(email)")
40///     .with_reverse_sql("DROP INDEX idx_email");
41///
42/// // Multiple statements
43/// let multi_sql = RunSQL::new_multi(vec![
44///     "INSERT INTO roles (name) VALUES ('admin')".to_string(),
45///     "INSERT INTO roles (name) VALUES ('user')".to_string(),
46/// ]);
47/// ```
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct RunSQL {
50	/// The sql.
51	pub sql: String,
52	/// The reverse sql.
53	pub reverse_sql: Option<String>,
54	/// The state operations.
55	pub state_operations: Vec<StateOperation>,
56}
57
58impl RunSQL {
59	/// Create a new RunSQL operation
60	///
61	/// # Example
62	///
63	/// ```rust
64	/// use reinhardt_db::migrations::operations::special::RunSQL;
65	///
66	/// let sql = RunSQL::new("INSERT INTO config (key, value) VALUES ('version', '1.0')");
67	/// assert!(sql.reverse_sql.is_none());
68	/// ```
69	pub fn new(sql: impl Into<String>) -> Self {
70		Self {
71			sql: sql.into(),
72			reverse_sql: None,
73			state_operations: vec![],
74		}
75	}
76
77	/// Create a RunSQL operation with multiple statements
78	///
79	/// # Example
80	///
81	/// ```rust
82	/// use reinhardt_db::migrations::operations::special::RunSQL;
83	///
84	/// let sql = RunSQL::new_multi(vec![
85	///     "UPDATE users SET active = true WHERE id = 1".to_string(),
86	///     "UPDATE users SET active = false WHERE id = 2".to_string(),
87	/// ]);
88	///
89	/// assert!(sql.sql.contains("UPDATE users SET active = true"));
90	/// assert!(sql.sql.contains("UPDATE users SET active = false"));
91	/// ```
92	pub fn new_multi(statements: Vec<String>) -> Self {
93		Self {
94			sql: statements.join(";\n"),
95			reverse_sql: None,
96			state_operations: vec![],
97		}
98	}
99
100	/// Set reverse SQL for rollback
101	///
102	/// # Example
103	///
104	/// ```rust
105	/// use reinhardt_db::migrations::operations::special::RunSQL;
106	///
107	/// let sql = RunSQL::new("CREATE INDEX idx_email ON users(email)")
108	///     .with_reverse_sql("DROP INDEX idx_email");
109	///
110	/// assert!(sql.reverse_sql.is_some());
111	/// assert_eq!(sql.reverse_sql.unwrap(), "DROP INDEX idx_email");
112	/// ```
113	pub fn with_reverse_sql(mut self, reverse_sql: impl Into<String>) -> Self {
114		self.reverse_sql = Some(reverse_sql.into());
115		self
116	}
117
118	/// Add state operations to be applied along with the SQL
119	///
120	/// This allows you to keep the project state in sync when running custom SQL.
121	pub fn with_state_operations(mut self, operations: Vec<StateOperation>) -> Self {
122		self.state_operations = operations;
123		self
124	}
125
126	/// Apply to project state (forward)
127	///
128	/// RunSQL doesn't modify state by default unless state_operations are specified
129	pub fn state_forwards(&self, app_label: &str, state: &mut ProjectState) {
130		for op in &self.state_operations {
131			op.apply(app_label, state);
132		}
133	}
134
135	/// Generate SQL
136	///
137	/// # Example
138	///
139	/// ```rust,no_run
140	/// use reinhardt_db::migrations::operations::special::RunSQL;
141	/// use reinhardt_db::backends::schema::factory::{SchemaEditorFactory, DatabaseType};
142	///
143	/// let sql = RunSQL::new("SELECT 1");
144	/// let factory = SchemaEditorFactory::new();
145	/// let editor = factory.create_for_database(DatabaseType::PostgreSQL);
146	///
147	/// let statements = sql.database_forwards(editor.as_ref());
148	/// assert_eq!(statements.len(), 1);
149	/// assert_eq!(statements[0], "SELECT 1");
150	/// ```
151	pub fn database_forwards(&self, _schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
152		vec![self.sql.clone()]
153	}
154
155	/// Get reverse SQL for rollback
156	///
157	/// # Example
158	///
159	/// ```rust
160	/// use reinhardt_db::migrations::operations::special::RunSQL;
161	///
162	/// let sql = RunSQL::new("CREATE TABLE temp (id INT)")
163	///     .with_reverse_sql("DROP TABLE temp");
164	///
165	/// assert_eq!(sql.get_reverse_sql(), Some("DROP TABLE temp"));
166	///
167	/// let irreversible = RunSQL::new("DROP TABLE important_data");
168	/// assert_eq!(irreversible.get_reverse_sql(), None);
169	/// ```
170	pub fn get_reverse_sql(&self) -> Option<&str> {
171		self.reverse_sql.as_deref()
172	}
173}
174
175/// State operation to apply alongside SQL
176///
177/// This allows RunSQL to update the project state appropriately
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub enum StateOperation {
180	/// Add a model to the project state.
181	AddModel {
182		/// The model name.
183		name: String,
184	},
185	/// Remove a model from the project state.
186	RemoveModel {
187		/// The model name.
188		name: String,
189	},
190	/// Add a field to a model in the project state.
191	AddField {
192		/// The model name.
193		model: String,
194		/// The field name.
195		field: String,
196	},
197	/// Remove a field from a model in the project state.
198	RemoveField {
199		/// The model name.
200		model: String,
201		/// The field name.
202		field: String,
203	},
204}
205
206impl StateOperation {
207	fn apply(&self, app_label: &str, state: &mut ProjectState) {
208		match self {
209			StateOperation::AddModel { .. } => {
210				// Would need model definition
211			}
212			StateOperation::RemoveModel { name } => {
213				state.remove_model(app_label, name);
214			}
215			StateOperation::AddField { .. } => {
216				// Would need field definition
217			}
218			StateOperation::RemoveField { model, field } => {
219				if let Some(model_state) = state.get_model_mut(app_label, model) {
220					model_state.remove_field(field);
221				}
222			}
223		}
224	}
225}
226
227/// Execute Rust code during migration
228///
229/// This is the Rust equivalent of Django's RunPython operation. It allows you to execute
230/// arbitrary Rust code during migration, useful for data transformations.
231///
232/// # Example
233///
234/// ```rust
235/// use reinhardt_db::migrations::operations::special::RunCode;
236/// use reinhardt_db::backends::connection::DatabaseConnection;
237///
238/// // Create a code operation with description
239/// let code = RunCode::new("Update user emails", |connection| {
240///     // Access database through connection
241///     println!("Updating emails...");
242///     Ok(())
243/// });
244/// ```
245pub struct RunCode {
246	/// The description.
247	pub description: String,
248	#[allow(clippy::type_complexity)]
249	/// The code.
250	pub code: Box<dyn Fn(&DatabaseConnection) -> Result<(), String> + Send + Sync>,
251	#[allow(clippy::type_complexity)]
252	/// The reverse code.
253	pub reverse_code: Option<Box<dyn Fn(&DatabaseConnection) -> Result<(), String> + Send + Sync>>,
254}
255
256impl std::fmt::Debug for RunCode {
257	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258		f.debug_struct("RunCode")
259			.field("description", &self.description)
260			.field("has_reverse", &self.reverse_code.is_some())
261			.finish()
262	}
263}
264
265impl RunCode {
266	/// Create a new RunCode operation
267	///
268	/// # Example
269	///
270	/// ```rust
271	/// use reinhardt_db::migrations::operations::special::RunCode;
272	///
273	/// let code = RunCode::new("Update user emails", |connection| {
274	///     // Database operations using connection
275	///     Ok(())
276	/// });
277	/// ```
278	pub fn new<F>(description: impl Into<String>, code: F) -> Self
279	where
280		F: Fn(&DatabaseConnection) -> Result<(), String> + Send + Sync + 'static,
281	{
282		Self {
283			description: description.into(),
284			code: Box::new(code),
285			reverse_code: None,
286		}
287	}
288
289	/// Set reverse code for rollback
290	///
291	/// # Example
292	///
293	/// ```rust
294	/// use reinhardt_db::migrations::operations::special::RunCode;
295	///
296	/// let code = RunCode::new("Update emails", |connection| Ok(()))
297	///     .with_reverse_code(|connection| {
298	///         // Rollback logic
299	///         Ok(())
300	///     });
301	/// ```
302	pub fn with_reverse_code<F>(mut self, reverse: F) -> Self
303	where
304		F: Fn(&DatabaseConnection) -> Result<(), String> + Send + Sync + 'static,
305	{
306		self.reverse_code = Some(Box::new(reverse));
307		self
308	}
309
310	/// Execute the code
311	///
312	/// # Example
313	///
314	/// ```no_run
315	/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
316	/// use reinhardt_db::migrations::operations::special::RunCode;
317	/// use reinhardt_db::backends::connection::DatabaseConnection;
318	///
319	/// let code = RunCode::new("Migrate data", |connection| Ok(()));
320	/// let connection = DatabaseConnection::connect_postgres("postgres://localhost/db").await?;
321	/// code.execute(&connection)?;
322	/// # Ok(())
323	/// # }
324	/// ```
325	pub fn execute(&self, connection: &DatabaseConnection) -> Result<(), String> {
326		(self.code)(connection)
327	}
328
329	/// Execute reverse code
330	///
331	/// # Example
332	///
333	/// ```no_run
334	/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
335	/// use reinhardt_db::migrations::operations::special::RunCode;
336	/// use reinhardt_db::backends::connection::DatabaseConnection;
337	///
338	/// let code = RunCode::new("Migrate data", |_| Ok(()))
339	///     .with_reverse_code(|_| Ok(()));
340	///
341	/// let connection = DatabaseConnection::connect_postgres("postgres://localhost/db").await?;
342	/// code.execute_reverse(&connection)?;
343	/// # Ok(())
344	/// # }
345	/// ```
346	pub fn execute_reverse(&self, connection: &DatabaseConnection) -> Result<(), String> {
347		if let Some(reverse) = &self.reverse_code {
348			reverse(connection)
349		} else {
350			Err("This operation is not reversible".to_string())
351		}
352	}
353
354	/// Apply to project state (forward)
355	///
356	/// RunCode doesn't modify state by default
357	pub fn state_forwards(&self, _app_label: &str, _state: &mut ProjectState) {
358		// Custom code operations don't modify state
359	}
360}
361
362#[cfg(test)]
363mod tests {
364	use super::*;
365	use crate::migrations::FieldType;
366
367	#[test]
368	fn test_run_sql_basic() {
369		let sql = RunSQL::new("CREATE INDEX idx_email ON users(email)");
370		assert_eq!(sql.sql, "CREATE INDEX idx_email ON users(email)");
371		assert!(sql.reverse_sql.is_none());
372	}
373
374	#[test]
375	fn test_run_sql_with_reverse() {
376		let sql = RunSQL::new("CREATE INDEX idx_email ON users(email)")
377			.with_reverse_sql("DROP INDEX idx_email");
378
379		assert_eq!(sql.sql, "CREATE INDEX idx_email ON users(email)");
380		assert_eq!(sql.reverse_sql, Some("DROP INDEX idx_email".to_string()));
381		assert_eq!(sql.get_reverse_sql(), Some("DROP INDEX idx_email"));
382	}
383
384	#[test]
385	fn test_run_sql_multi() {
386		let sql = RunSQL::new_multi(vec![
387			"INSERT INTO roles (name) VALUES ('admin')".to_string(),
388			"INSERT INTO roles (name) VALUES ('user')".to_string(),
389		]);
390
391		assert!(
392			sql.sql
393				.contains("INSERT INTO roles (name) VALUES ('admin')")
394		);
395		assert!(sql.sql.contains("INSERT INTO roles (name) VALUES ('user')"));
396	}
397
398	#[cfg(feature = "postgres")]
399	#[test]
400	fn test_run_sql_database_forwards() {
401		use crate::backends::schema::test_utils::MockSchemaEditor;
402
403		let sql = RunSQL::new("SELECT COUNT(*) FROM users");
404		let editor = MockSchemaEditor::new();
405
406		let statements = sql.database_forwards(&editor);
407		assert_eq!(statements.len(), 1);
408		assert_eq!(statements[0], "SELECT COUNT(*) FROM users");
409	}
410
411	#[test]
412	fn test_run_code_basic() {
413		let code = RunCode::new("Test migration", |_connection| Ok(()));
414		assert_eq!(code.description, "Test migration");
415		assert!(code.reverse_code.is_none());
416	}
417
418	#[test]
419	fn test_run_code_with_reverse() {
420		let code = RunCode::new("Test migration", |_connection| Ok(()))
421			.with_reverse_code(|_connection| Ok(()));
422		assert!(code.reverse_code.is_some());
423	}
424
425	#[test]
426	fn test_state_operation_remove_model() {
427		use crate::migrations::operations::FieldDefinition;
428		use crate::migrations::operations::models::CreateModel;
429
430		let mut state = ProjectState::new();
431
432		// Create a model first
433		let create = CreateModel::new(
434			"User",
435			vec![FieldDefinition::new(
436				"id",
437				FieldType::Integer,
438				true,
439				false,
440				None::<String>,
441			)],
442		);
443		create.state_forwards("myapp", &mut state);
444		assert!(state.get_model("myapp", "User").is_some());
445
446		// Remove it via state operation
447		let op = StateOperation::RemoveModel {
448			name: "User".to_string(),
449		};
450		op.apply("myapp", &mut state);
451		assert!(state.get_model("myapp", "User").is_none());
452	}
453}
454
455/// Complex data migration builder
456///
457/// Provides a fluent API for building complex data migrations with batching,
458/// progress tracking, and error handling.
459///
460/// # Example
461///
462/// ```rust
463/// use reinhardt_db::migrations::operations::special::DataMigration;
464///
465/// let migration = DataMigration::new("users", "Migrate user data")
466///     .batch_size(1000)
467///     .add_transformation("UPDATE users SET status = 'active' WHERE status IS NULL")
468///     .add_transformation("UPDATE users SET created_at = NOW() WHERE created_at IS NULL");
469/// ```
470#[derive(Debug, Clone)]
471pub struct DataMigration {
472	/// Table name
473	pub table: String,
474	/// Migration description
475	pub description: String,
476	/// Batch size for processing
477	pub batch_size: usize,
478	/// SQL transformations to apply
479	pub transformations: Vec<String>,
480	/// Whether to use transactions
481	pub use_transactions: bool,
482}
483
484impl DataMigration {
485	/// Create a new data migration
486	///
487	/// # Example
488	///
489	/// ```rust
490	/// use reinhardt_db::migrations::operations::special::DataMigration;
491	///
492	/// let migration = DataMigration::new("users", "Update user statuses");
493	/// assert_eq!(migration.table, "users");
494	/// assert_eq!(migration.batch_size, 1000);
495	/// ```
496	pub fn new(table: impl Into<String>, description: impl Into<String>) -> Self {
497		Self {
498			table: table.into(),
499			description: description.into(),
500			batch_size: 1000,
501			transformations: Vec::new(),
502			use_transactions: true,
503		}
504	}
505
506	/// Set batch size for processing
507	///
508	/// # Example
509	///
510	/// ```rust
511	/// use reinhardt_db::migrations::operations::special::DataMigration;
512	///
513	/// let migration = DataMigration::new("users", "Migrate data")
514	///     .batch_size(500);
515	/// assert_eq!(migration.batch_size, 500);
516	/// ```
517	pub fn batch_size(mut self, size: usize) -> Self {
518		self.batch_size = size;
519		self
520	}
521
522	/// Add a SQL transformation
523	///
524	/// # Example
525	///
526	/// ```rust
527	/// use reinhardt_db::migrations::operations::special::DataMigration;
528	///
529	/// let migration = DataMigration::new("users", "Clean data")
530	///     .add_transformation("UPDATE users SET email = LOWER(email)");
531	/// assert_eq!(migration.transformations.len(), 1);
532	/// ```
533	pub fn add_transformation(mut self, sql: impl Into<String>) -> Self {
534		self.transformations.push(sql.into());
535		self
536	}
537
538	/// Set whether to use transactions
539	///
540	/// # Example
541	///
542	/// ```rust
543	/// use reinhardt_db::migrations::operations::special::DataMigration;
544	///
545	/// let migration = DataMigration::new("users", "Migrate")
546	///     .use_transactions(false);
547	/// assert!(!migration.use_transactions);
548	/// ```
549	pub fn use_transactions(mut self, use_tx: bool) -> Self {
550		self.use_transactions = use_tx;
551		self
552	}
553
554	/// Generate batched SQL statements
555	///
556	/// # Example
557	///
558	/// ```rust
559	/// use reinhardt_db::migrations::operations::special::DataMigration;
560	///
561	/// let migration = DataMigration::new("users", "Update emails")
562	///     .batch_size(100)
563	///     .add_transformation("UPDATE users SET email = LOWER(email) WHERE id >= {start} AND id < {end}");
564	///
565	/// let statements = migration.generate_batched_sql(1000);
566	/// assert_eq!(statements.len(), 10); // 1000 / 100 = 10 batches
567	/// ```
568	pub fn generate_batched_sql(&self, total_rows: usize) -> Vec<String> {
569		let mut statements = Vec::new();
570		let num_batches = total_rows.div_ceil(self.batch_size);
571
572		for batch in 0..num_batches {
573			let start = batch * self.batch_size;
574			let end = ((batch + 1) * self.batch_size).min(total_rows);
575
576			for transformation in &self.transformations {
577				let sql = transformation
578					.replace("{start}", &start.to_string())
579					.replace("{end}", &end.to_string())
580					.replace("{batch_size}", &self.batch_size.to_string())
581					.replace("{table}", &self.table);
582
583				statements.push(sql);
584			}
585		}
586
587		statements
588	}
589
590	/// Convert to RunSQL operation
591	///
592	/// # Example
593	///
594	/// ```rust
595	/// use reinhardt_db::migrations::operations::special::DataMigration;
596	///
597	/// let migration = DataMigration::new("users", "Migrate")
598	///     .add_transformation("UPDATE users SET status = 'active'");
599	///
600	/// let run_sql = migration.to_run_sql();
601	/// assert!(run_sql.sql.contains("UPDATE users"));
602	/// ```
603	pub fn to_run_sql(&self) -> RunSQL {
604		let sql = if self.use_transactions {
605			format!("BEGIN;\n{}\nCOMMIT;", self.transformations.join(";\n"))
606		} else {
607			self.transformations.join(";\n")
608		};
609
610		RunSQL::new(sql)
611	}
612}
613
614// MigrationOperation trait implementation for Django-style naming
615use crate::migrations::operation_trait::MigrationOperation;
616
617impl MigrationOperation for RunSQL {
618	fn migration_name_fragment(&self) -> Option<String> {
619		// Return "run_sql" to indicate this is a custom SQL operation
620		Some("run_sql".to_string())
621	}
622
623	fn describe(&self) -> String {
624		let preview = if self.sql.len() > 50 {
625			format!("{}...", &self.sql[..50])
626		} else {
627			self.sql.clone()
628		};
629		format!("RunSQL: {}", preview)
630	}
631}
632
633impl MigrationOperation for RunCode {
634	fn migration_name_fragment(&self) -> Option<String> {
635		// Return "run_code" to indicate this is a custom code operation
636		Some("run_code".to_string())
637	}
638
639	fn describe(&self) -> String {
640		"RunCode: Custom code execution".to_string()
641	}
642}
643
644impl MigrationOperation for DataMigration {
645	fn migration_name_fragment(&self) -> Option<String> {
646		// Return "data_migration" to indicate this is a data transformation operation
647		Some("data_migration".to_string())
648	}
649
650	fn describe(&self) -> String {
651		format!("DataMigration: {}", self.description)
652	}
653}
654
655#[cfg(test)]
656mod data_migration_tests {
657	use super::*;
658
659	#[test]
660	fn test_data_migration_creation() {
661		let migration = DataMigration::new("users", "Migrate user data");
662		assert_eq!(migration.table, "users");
663		assert_eq!(migration.description, "Migrate user data");
664		assert_eq!(migration.batch_size, 1000);
665		assert!(migration.use_transactions);
666	}
667
668	#[test]
669	fn test_data_migration_batch_size() {
670		let migration = DataMigration::new("users", "Migrate").batch_size(500);
671		assert_eq!(migration.batch_size, 500);
672	}
673
674	#[test]
675	fn test_data_migration_add_transformation() {
676		let migration = DataMigration::new("users", "Clean")
677			.add_transformation("UPDATE users SET email = LOWER(email)")
678			.add_transformation("UPDATE users SET name = TRIM(name)");
679
680		assert_eq!(migration.transformations.len(), 2);
681	}
682
683	#[test]
684	fn test_data_migration_use_transactions() {
685		let migration = DataMigration::new("users", "Migrate").use_transactions(false);
686		assert!(!migration.use_transactions);
687	}
688
689	#[test]
690	fn test_generate_batched_sql() {
691		let migration = DataMigration::new("users", "Update")
692			.batch_size(100)
693			.add_transformation(
694				"UPDATE users SET processed = true WHERE id >= {start} AND id < {end}",
695			);
696
697		let statements = migration.generate_batched_sql(250);
698		assert_eq!(statements.len(), 3); // 250 / 100 = 3 batches
699
700		assert!(statements[0].contains("id >= 0 AND id < 100"));
701		assert!(statements[1].contains("id >= 100 AND id < 200"));
702		assert!(statements[2].contains("id >= 200 AND id < 250"));
703	}
704
705	#[test]
706	fn test_to_run_sql_with_transactions() {
707		let migration = DataMigration::new("users", "Migrate")
708			.add_transformation("UPDATE users SET status = 'active'")
709			.add_transformation("UPDATE users SET verified = true");
710
711		let run_sql = migration.to_run_sql();
712		assert!(run_sql.sql.contains("BEGIN"));
713		assert!(run_sql.sql.contains("COMMIT"));
714		assert!(run_sql.sql.contains("UPDATE users"));
715	}
716
717	#[test]
718	fn test_to_run_sql_without_transactions() {
719		let migration = DataMigration::new("users", "Migrate")
720			.use_transactions(false)
721			.add_transformation("UPDATE users SET status = 'active'");
722
723		let run_sql = migration.to_run_sql();
724		assert!(!run_sql.sql.contains("BEGIN"));
725		assert!(!run_sql.sql.contains("COMMIT"));
726	}
727}