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}