Skip to main content

reinhardt_db/migrations/
operation_trait.rs

1//! Operation trait for migration operations
2//!
3//! This module provides a unified trait for all migration operations,
4//! enabling Django-style migration name generation via `migration_name_fragment()`.
5
6/// Trait for migration operations
7///
8/// This trait provides a unified interface for all migration operations,
9/// following Django's migration system design.
10///
11/// # Django Compatibility
12///
13/// This trait is designed to be compatible with Django's `Operation` class:
14/// - `migration_name_fragment()` → Django's `migration_name_fragment` property
15/// - `describe()` → Human-readable description for CLI output
16///
17/// # Example
18///
19/// ```rust,no_run
20/// # use reinhardt_db::migrations::operation_trait::MigrationOperation;
21/// struct AddField {
22///     model_name: String,
23///     field_name: String,
24/// }
25///
26/// impl MigrationOperation for AddField {
27///     fn migration_name_fragment(&self) -> Option<String> {
28///         Some(format!("{}_{}",
29///             self.model_name.to_lowercase(),
30///             self.field_name.to_lowercase()
31///         ))
32///     }
33///
34///     fn describe(&self) -> String {
35///         format!("Add field {} to {}", self.field_name, self.model_name)
36///     }
37/// }
38/// ```
39pub trait MigrationOperation {
40	/// Generate a fragment for the migration name
41	///
42	/// Returns `Some(String)` if this operation can contribute a meaningful
43	/// name fragment, or `None` if it should trigger fallback to auto-generated
44	/// timestamp-based naming (e.g., `auto_20251202_1430`).
45	///
46	/// # Django Compatibility
47	///
48	/// This follows Django's naming rules:
49	/// - `CreateModel { name: "User" }` → `Some("user")`
50	/// - `AddField { model: "User", field: "email" }` → `Some("user_email")`
51	/// - `RemoveField { model: "User", field: "age" }` → `Some("remove_user_age")`
52	/// - `RunSQL { ... }` → `None` (triggers auto-naming)
53	///
54	/// # Naming Conventions
55	///
56	/// - Use lowercase for model and field names
57	/// - Use underscores to separate words
58	/// - Use descriptive prefixes (e.g., `remove_`, `alter_`, `rename_`)
59	/// - Keep fragments concise (they will be combined with `_`)
60	///
61	/// # Returns
62	///
63	/// - `Some(fragment)` - This operation provides a name fragment
64	/// - `None` - This operation cannot provide a meaningful name (e.g., RunSQL, RunCode)
65	fn migration_name_fragment(&self) -> Option<String>;
66
67	/// Human-readable description of this operation
68	///
69	/// Used for CLI output when displaying migration contents.
70	///
71	/// # Example Output
72	///
73	/// ```text
74	/// - Add field email to User
75	/// - Remove field age from Profile
76	/// - Create model Post
77	/// ```
78	fn describe(&self) -> String;
79
80	/// Normalize operation for comparison
81	///
82	/// Returns a normalized version of this operation where order-independent
83	/// elements (like column lists, constraint lists) are sorted for consistent
84	/// comparison.
85	///
86	/// This is used for semantic equality checking to detect duplicate migrations
87	/// even when the order of elements differs.
88	fn normalize(&self) -> Self
89	where
90		Self: Sized + Clone,
91	{
92		self.clone()
93	}
94
95	/// Check if two operations are semantically equal
96	///
97	/// Two operations are semantically equal if they produce the same database
98	/// schema changes, even if their internal representation differs in ordering.
99	///
100	/// # Examples
101	///
102	/// ```rust,no_run
103	/// # use reinhardt_db::migrations::operation_trait::MigrationOperation;
104	/// # #[derive(Clone, PartialEq)]
105	/// # struct CreateIndex { table: &'static str, columns: Vec<&'static str> }
106	/// # impl MigrationOperation for CreateIndex {
107	/// #     fn migration_name_fragment(&self) -> Option<String> { None }
108	/// #     fn describe(&self) -> String { String::new() }
109	/// # }
110	/// let op1 = CreateIndex {
111	///     table: "users",
112	///     columns: vec!["email", "name"],
113	/// };
114	/// let op2 = CreateIndex {
115	///     table: "users",
116	///     columns: vec!["name", "email"],
117	/// };
118	///
119	/// assert!(op1.semantically_equal(&op2));
120	/// ```
121	fn semantically_equal(&self, other: &Self) -> bool
122	where
123		Self: Sized + Clone + PartialEq,
124	{
125		self.normalize() == other.normalize()
126	}
127}