elif_orm/migrations/
schema_builder.rs

1//! Schema Builder - DSL for creating database schema changes
2//!
3//! Provides a fluent interface for building SQL schema modification statements
4//! commonly used in migrations.
5
6/// Basic schema operations for migrations
7pub struct SchemaBuilder {
8    statements: Vec<String>,
9}
10
11impl SchemaBuilder {
12    /// Create a new schema builder
13    pub fn new() -> Self {
14        Self {
15            statements: Vec::new(),
16        }
17    }
18
19    /// Create a new table
20    pub fn create_table<F>(&mut self, table_name: &str, callback: F) -> &mut Self
21    where
22        F: FnOnce(&mut TableBuilder),
23    {
24        let mut table_builder = TableBuilder::new(table_name);
25        callback(&mut table_builder);
26
27        let sql = table_builder.to_sql();
28        self.statements.push(sql);
29        self
30    }
31
32    /// Drop a table
33    pub fn drop_table(&mut self, table_name: &str) -> &mut Self {
34        self.statements
35            .push(format!("DROP TABLE IF EXISTS {};", table_name));
36        self
37    }
38
39    /// Add a column to existing table
40    pub fn add_column(
41        &mut self,
42        table_name: &str,
43        column_name: &str,
44        column_type: &str,
45    ) -> &mut Self {
46        self.statements.push(format!(
47            "ALTER TABLE {} ADD COLUMN {} {};",
48            table_name, column_name, column_type
49        ));
50        self
51    }
52
53    /// Drop a column from existing table
54    pub fn drop_column(&mut self, table_name: &str, column_name: &str) -> &mut Self {
55        self.statements.push(format!(
56            "ALTER TABLE {} DROP COLUMN {};",
57            table_name, column_name
58        ));
59        self
60    }
61
62    /// Create an index
63    pub fn create_index(
64        &mut self,
65        table_name: &str,
66        column_names: &[&str],
67        index_name: Option<&str>,
68    ) -> &mut Self {
69        let default_name = format!("idx_{}_{}", table_name, column_names.join("_"));
70        let index_name = index_name.unwrap_or(&default_name);
71        self.statements.push(format!(
72            "CREATE INDEX {} ON {} ({});",
73            index_name,
74            table_name,
75            column_names.join(", ")
76        ));
77        self
78    }
79
80    /// Drop an index
81    pub fn drop_index(&mut self, index_name: &str) -> &mut Self {
82        self.statements
83            .push(format!("DROP INDEX IF EXISTS {};", index_name));
84        self
85    }
86
87    /// Get all SQL statements
88    pub fn to_sql(&self) -> Vec<String> {
89        self.statements.clone()
90    }
91
92    /// Execute all statements as a single SQL string
93    pub fn build(&self) -> String {
94        self.statements.join("\n")
95    }
96}
97
98/// Table builder for CREATE TABLE statements
99pub struct TableBuilder {
100    table_name: String,
101    columns: Vec<String>,
102    constraints: Vec<String>,
103}
104
105impl TableBuilder {
106    pub fn new(table_name: &str) -> Self {
107        Self {
108            table_name: table_name.to_string(),
109            columns: Vec::new(),
110            constraints: Vec::new(),
111        }
112    }
113
114    /// Add a column
115    pub fn column(&mut self, name: &str, column_type: &str) -> &mut Self {
116        self.columns.push(format!("{} {}", name, column_type));
117        self
118    }
119
120    /// Add an ID column (auto-increment primary key)
121    pub fn id(&mut self, name: &str) -> &mut Self {
122        self.columns.push(format!("{} SERIAL PRIMARY KEY", name));
123        self
124    }
125
126    /// Add a UUID column
127    pub fn uuid(&mut self, name: &str) -> &mut Self {
128        self.columns
129            .push(format!("{} UUID DEFAULT gen_random_uuid()", name));
130        self
131    }
132
133    /// Add a string column
134    pub fn string(&mut self, name: &str, length: Option<u32>) -> &mut Self {
135        let column_type = match length {
136            Some(len) => format!("VARCHAR({})", len),
137            None => "TEXT".to_string(),
138        };
139        self.columns.push(format!("{} {}", name, column_type));
140        self
141    }
142
143    /// Add an integer column
144    pub fn integer(&mut self, name: &str) -> &mut Self {
145        self.columns.push(format!("{} INTEGER", name));
146        self
147    }
148
149    /// Add a boolean column
150    pub fn boolean(&mut self, name: &str) -> &mut Self {
151        self.columns.push(format!("{} BOOLEAN", name));
152        self
153    }
154
155    /// Add timestamp columns
156    pub fn timestamps(&mut self) -> &mut Self {
157        self.columns
158            .push("created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP".to_string());
159        self.columns
160            .push("updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP".to_string());
161        self
162    }
163
164    /// Add a primary key constraint
165    pub fn primary_key(&mut self, columns: &[&str]) -> &mut Self {
166        self.constraints
167            .push(format!("PRIMARY KEY ({})", columns.join(", ")));
168        self
169    }
170
171    /// Add a foreign key constraint
172    pub fn foreign_key(
173        &mut self,
174        column: &str,
175        references_table: &str,
176        references_column: &str,
177    ) -> &mut Self {
178        self.constraints.push(format!(
179            "FOREIGN KEY ({}) REFERENCES {} ({})",
180            column, references_table, references_column
181        ));
182        self
183    }
184
185    /// Add a unique constraint
186    pub fn unique(&mut self, columns: &[&str]) -> &mut Self {
187        self.constraints
188            .push(format!("UNIQUE ({})", columns.join(", ")));
189        self
190    }
191
192    /// Build the CREATE TABLE SQL
193    pub fn to_sql(&self) -> String {
194        let mut parts = self.columns.clone();
195        parts.extend(self.constraints.clone());
196
197        format!(
198            "CREATE TABLE {} (\n    {}\n);",
199            self.table_name,
200            parts.join(",\n    ")
201        )
202    }
203}
204
205impl Default for SchemaBuilder {
206    fn default() -> Self {
207        Self::new()
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_schema_builder() {
217        let mut builder = SchemaBuilder::new();
218        builder.create_table("users", |table| {
219            table.id("id");
220            table.string("name", Some(255));
221            table.string("email", Some(255));
222            table.timestamps();
223            table.unique(&["email"]);
224        });
225
226        let sql = builder.build();
227        assert!(sql.contains("CREATE TABLE users"));
228        assert!(sql.contains("id SERIAL PRIMARY KEY"));
229        assert!(sql.contains("name VARCHAR(255)"));
230        assert!(sql.contains("email VARCHAR(255)"));
231        assert!(sql.contains("created_at TIMESTAMP"));
232        assert!(sql.contains("UNIQUE (email)"));
233    }
234
235    #[test]
236    fn test_table_builder() {
237        let mut table = TableBuilder::new("posts");
238        table.id("id");
239        table.string("title", Some(255));
240        table.string("content", None);
241        table.integer("user_id");
242        table.timestamps();
243        table.foreign_key("user_id", "users", "id");
244
245        let sql = table.to_sql();
246        assert!(sql.contains("CREATE TABLE posts"));
247        assert!(sql.contains("id SERIAL PRIMARY KEY"));
248        assert!(sql.contains("title VARCHAR(255)"));
249        assert!(sql.contains("content TEXT"));
250        assert!(sql.contains("user_id INTEGER"));
251        assert!(sql.contains("FOREIGN KEY (user_id) REFERENCES users (id)"));
252    }
253}