Skip to main content

oxide_sql_core/migrations/dialect/
mod.rs

1//! Dialect-specific SQL generation for migrations.
2//!
3//! Different databases have different SQL syntax for DDL operations.
4//! This module provides dialect implementations for generating
5//! database-specific migration SQL.
6
7mod duckdb;
8mod postgres;
9mod sqlite;
10
11pub use duckdb::DuckDbDialect;
12pub use postgres::PostgresDialect;
13pub use sqlite::SqliteDialect;
14
15use crate::ast::DataType;
16
17use super::column_builder::{ColumnDefinition, DefaultValue};
18use super::operation::{
19    AddColumnOp, AlterColumnOp, CreateIndexOp, CreateTableOp, DropColumnOp, DropIndexOp,
20    DropTableOp, IndexType, Operation, RenameColumnOp, RenameTableOp, TableConstraint,
21};
22
23/// Trait for dialect-specific SQL generation for migrations.
24pub trait MigrationDialect {
25    /// Returns the dialect name.
26    fn name(&self) -> &'static str;
27
28    /// Generates SQL for an operation.
29    fn generate_sql(&self, operation: &Operation) -> String {
30        match operation {
31            Operation::CreateTable(op) => self.create_table(op),
32            Operation::DropTable(op) => self.drop_table(op),
33            Operation::RenameTable(op) => self.rename_table(op),
34            Operation::AddColumn(op) => self.add_column(op),
35            Operation::DropColumn(op) => self.drop_column(op),
36            Operation::AlterColumn(op) => self.alter_column(op),
37            Operation::RenameColumn(op) => self.rename_column(op),
38            Operation::CreateIndex(op) => self.create_index(op),
39            Operation::DropIndex(op) => self.drop_index(op),
40            Operation::AddForeignKey(op) => self.add_foreign_key(op),
41            Operation::DropForeignKey(op) => self.drop_foreign_key(op),
42            Operation::RunSql(op) => op.up_sql.clone(),
43        }
44    }
45
46    /// Generates SQL for CREATE TABLE.
47    fn create_table(&self, op: &CreateTableOp) -> String {
48        let mut sql = String::from("CREATE TABLE ");
49        if op.if_not_exists {
50            sql.push_str("IF NOT EXISTS ");
51        }
52        sql.push_str(&self.quote_identifier(&op.name));
53        sql.push_str(" (\n");
54
55        // Columns
56        let column_defs: Vec<String> = op
57            .columns
58            .iter()
59            .map(|c| format!("    {}", self.column_definition(c)))
60            .collect();
61        sql.push_str(&column_defs.join(",\n"));
62
63        // Constraints
64        if !op.constraints.is_empty() {
65            sql.push_str(",\n");
66            let constraint_defs: Vec<String> = op
67                .constraints
68                .iter()
69                .map(|c| format!("    {}", self.table_constraint(c)))
70                .collect();
71            sql.push_str(&constraint_defs.join(",\n"));
72        }
73
74        sql.push_str("\n)");
75        sql
76    }
77
78    /// Generates SQL for DROP TABLE.
79    fn drop_table(&self, op: &DropTableOp) -> String {
80        let mut sql = String::from("DROP TABLE ");
81        if op.if_exists {
82            sql.push_str("IF EXISTS ");
83        }
84        sql.push_str(&self.quote_identifier(&op.name));
85        if op.cascade {
86            sql.push_str(" CASCADE");
87        }
88        sql
89    }
90
91    /// Generates SQL for RENAME TABLE.
92    fn rename_table(&self, op: &RenameTableOp) -> String;
93
94    /// Generates SQL for ADD COLUMN.
95    fn add_column(&self, op: &AddColumnOp) -> String {
96        format!(
97            "ALTER TABLE {} ADD COLUMN {}",
98            self.quote_identifier(&op.table),
99            self.column_definition(&op.column)
100        )
101    }
102
103    /// Generates SQL for DROP COLUMN.
104    fn drop_column(&self, op: &DropColumnOp) -> String {
105        format!(
106            "ALTER TABLE {} DROP COLUMN {}",
107            self.quote_identifier(&op.table),
108            self.quote_identifier(&op.column)
109        )
110    }
111
112    /// Generates SQL for ALTER COLUMN.
113    fn alter_column(&self, op: &AlterColumnOp) -> String;
114
115    /// Generates SQL for RENAME COLUMN.
116    fn rename_column(&self, op: &RenameColumnOp) -> String;
117
118    /// Generates SQL for CREATE INDEX.
119    fn create_index(&self, op: &CreateIndexOp) -> String {
120        let mut sql = String::from("CREATE ");
121        if op.unique {
122            sql.push_str("UNIQUE ");
123        }
124        sql.push_str("INDEX ");
125        if op.if_not_exists {
126            sql.push_str("IF NOT EXISTS ");
127        }
128        sql.push_str(&self.quote_identifier(&op.name));
129        sql.push_str(" ON ");
130        sql.push_str(&self.quote_identifier(&op.table));
131
132        // Index type (if supported and not default)
133        if op.index_type != IndexType::BTree {
134            sql.push_str(&format!(" USING {}", self.index_type_sql(&op.index_type)));
135        }
136
137        // Columns
138        sql.push_str(" (");
139        let cols: Vec<String> = op
140            .columns
141            .iter()
142            .map(|c| self.quote_identifier(c))
143            .collect();
144        sql.push_str(&cols.join(", "));
145        sql.push(')');
146
147        // Partial index condition
148        if let Some(ref condition) = op.condition {
149            sql.push_str(" WHERE ");
150            sql.push_str(condition);
151        }
152
153        sql
154    }
155
156    /// Generates SQL for DROP INDEX.
157    fn drop_index(&self, op: &DropIndexOp) -> String;
158
159    /// Generates SQL for ADD FOREIGN KEY.
160    fn add_foreign_key(&self, op: &super::operation::AddForeignKeyOp) -> String {
161        let mut sql = format!("ALTER TABLE {} ADD ", self.quote_identifier(&op.table));
162        if let Some(ref name) = op.name {
163            sql.push_str(&format!("CONSTRAINT {} ", self.quote_identifier(name)));
164        }
165        sql.push_str("FOREIGN KEY (");
166        let cols: Vec<String> = op
167            .columns
168            .iter()
169            .map(|c| self.quote_identifier(c))
170            .collect();
171        sql.push_str(&cols.join(", "));
172        sql.push_str(") REFERENCES ");
173        sql.push_str(&self.quote_identifier(&op.references_table));
174        sql.push_str(" (");
175        let ref_cols: Vec<String> = op
176            .references_columns
177            .iter()
178            .map(|c| self.quote_identifier(c))
179            .collect();
180        sql.push_str(&ref_cols.join(", "));
181        sql.push(')');
182
183        if let Some(action) = op.on_delete {
184            sql.push_str(" ON DELETE ");
185            sql.push_str(action.as_sql());
186        }
187        if let Some(action) = op.on_update {
188            sql.push_str(" ON UPDATE ");
189            sql.push_str(action.as_sql());
190        }
191
192        sql
193    }
194
195    /// Generates SQL for DROP FOREIGN KEY.
196    fn drop_foreign_key(&self, op: &super::operation::DropForeignKeyOp) -> String;
197
198    /// Generates SQL for a column definition.
199    fn column_definition(&self, col: &ColumnDefinition) -> String {
200        let mut sql = format!(
201            "{} {}",
202            self.quote_identifier(&col.name),
203            self.map_data_type(&col.data_type)
204        );
205
206        if col.primary_key {
207            sql.push_str(" PRIMARY KEY");
208            if col.autoincrement {
209                sql.push_str(&self.autoincrement_keyword());
210            }
211        } else {
212            if !col.nullable {
213                sql.push_str(" NOT NULL");
214            }
215            if col.unique {
216                sql.push_str(" UNIQUE");
217            }
218            if col.autoincrement {
219                sql.push_str(&self.autoincrement_keyword());
220            }
221        }
222
223        if let Some(ref default) = col.default {
224            sql.push_str(" DEFAULT ");
225            sql.push_str(&self.render_default(default));
226        }
227
228        if let Some(ref fk) = col.references {
229            sql.push_str(" REFERENCES ");
230            sql.push_str(&self.quote_identifier(&fk.table));
231            sql.push_str(" (");
232            sql.push_str(&self.quote_identifier(&fk.column));
233            sql.push(')');
234            if let Some(action) = fk.on_delete {
235                sql.push_str(" ON DELETE ");
236                sql.push_str(action.as_sql());
237            }
238            if let Some(action) = fk.on_update {
239                sql.push_str(" ON UPDATE ");
240                sql.push_str(action.as_sql());
241            }
242        }
243
244        if let Some(ref check) = col.check {
245            sql.push_str(&format!(" CHECK ({})", check));
246        }
247
248        if let Some(ref collation) = col.collation {
249            sql.push_str(&format!(" COLLATE {}", collation));
250        }
251
252        sql
253    }
254
255    /// Generates SQL for a table constraint.
256    fn table_constraint(&self, constraint: &TableConstraint) -> String {
257        match constraint {
258            TableConstraint::PrimaryKey { name, columns } => {
259                let mut sql = String::new();
260                if let Some(n) = name {
261                    sql.push_str(&format!("CONSTRAINT {} ", self.quote_identifier(n)));
262                }
263                sql.push_str("PRIMARY KEY (");
264                let cols: Vec<String> = columns.iter().map(|c| self.quote_identifier(c)).collect();
265                sql.push_str(&cols.join(", "));
266                sql.push(')');
267                sql
268            }
269            TableConstraint::Unique { name, columns } => {
270                let mut sql = String::new();
271                if let Some(n) = name {
272                    sql.push_str(&format!("CONSTRAINT {} ", self.quote_identifier(n)));
273                }
274                sql.push_str("UNIQUE (");
275                let cols: Vec<String> = columns.iter().map(|c| self.quote_identifier(c)).collect();
276                sql.push_str(&cols.join(", "));
277                sql.push(')');
278                sql
279            }
280            TableConstraint::ForeignKey {
281                name,
282                columns,
283                references_table,
284                references_columns,
285                on_delete,
286                on_update,
287            } => {
288                let mut sql = String::new();
289                if let Some(n) = name {
290                    sql.push_str(&format!("CONSTRAINT {} ", self.quote_identifier(n)));
291                }
292                sql.push_str("FOREIGN KEY (");
293                let cols: Vec<String> = columns.iter().map(|c| self.quote_identifier(c)).collect();
294                sql.push_str(&cols.join(", "));
295                sql.push_str(") REFERENCES ");
296                sql.push_str(&self.quote_identifier(references_table));
297                sql.push_str(" (");
298                let ref_cols: Vec<String> = references_columns
299                    .iter()
300                    .map(|c| self.quote_identifier(c))
301                    .collect();
302                sql.push_str(&ref_cols.join(", "));
303                sql.push(')');
304                if let Some(action) = on_delete {
305                    sql.push_str(" ON DELETE ");
306                    sql.push_str(action.as_sql());
307                }
308                if let Some(action) = on_update {
309                    sql.push_str(" ON UPDATE ");
310                    sql.push_str(action.as_sql());
311                }
312                sql
313            }
314            TableConstraint::Check { name, expression } => {
315                let mut sql = String::new();
316                if let Some(n) = name {
317                    sql.push_str(&format!("CONSTRAINT {} ", self.quote_identifier(n)));
318                }
319                sql.push_str(&format!("CHECK ({})", expression));
320                sql
321            }
322        }
323    }
324
325    /// Maps a `DataType` to the dialect-specific SQL type.
326    fn map_data_type(&self, dt: &DataType) -> String;
327
328    /// Renders a default value.
329    fn render_default(&self, default: &DefaultValue) -> String {
330        default.to_sql()
331    }
332
333    /// Returns the identifier quote character.
334    fn quote_char(&self) -> char {
335        '"'
336    }
337
338    /// Quotes an identifier.
339    fn quote_identifier(&self, name: &str) -> String {
340        let q = self.quote_char();
341        format!("{q}{name}{q}")
342    }
343
344    /// Returns the AUTOINCREMENT keyword for this dialect.
345    fn autoincrement_keyword(&self) -> String;
346
347    /// Maps an index type to SQL.
348    fn index_type_sql(&self, index_type: &IndexType) -> &'static str {
349        match index_type {
350            IndexType::BTree => "BTREE",
351            IndexType::Hash => "HASH",
352            IndexType::Gist => "GIST",
353            IndexType::Gin => "GIN",
354        }
355    }
356}