1use crate::ir::{Column, Index, IndexColumn, SchemaModel, Table, TableConstraint};
3
4pub fn render(model: &SchemaModel, enable_foreign_keys: bool) -> String {
6 let mut output = String::new();
7
8 if enable_foreign_keys {
10 output.push_str("PRAGMA foreign_keys = ON;\n\n");
11 }
12
13 for table in &model.tables {
15 render_table(table, &mut output);
16 output.push('\n');
17 }
18
19 let mut indexes: Vec<&Index> = model.indexes.iter().collect();
21 indexes.sort_by(|a, b| {
22 a.table
23 .name
24 .normalized
25 .cmp(&b.table.name.normalized)
26 .then_with(|| a.name.normalized.cmp(&b.name.normalized))
27 });
28
29 for index in indexes {
30 render_index(index, &mut output);
31 output.push('\n');
32 }
33
34 while output.ends_with("\n\n") {
36 output.pop();
37 }
38 if !output.ends_with('\n') {
39 output.push('\n');
40 }
41
42 output
43}
44
45fn render_table(table: &Table, out: &mut String) {
46 out.push_str(&format!("CREATE TABLE {} (\n", table.name.to_sql()));
47
48 let mut parts: Vec<String> = Vec::new();
49
50 for col in &table.columns {
52 parts.push(render_column(col));
53 }
54
55 let mut sorted_constraints = table.constraints.clone();
57 sorted_constraints.sort_by_key(|c| match c {
58 TableConstraint::PrimaryKey { .. } => 0,
59 TableConstraint::Unique { .. } => 1,
60 TableConstraint::Check { .. } => 2,
61 TableConstraint::ForeignKey { .. } => 3,
62 });
63
64 for constraint in &sorted_constraints {
65 parts.push(render_table_constraint(constraint));
66 }
67
68 out.push_str(&parts.join(",\n"));
69 out.push_str("\n);\n");
70}
71
72fn render_column(col: &Column) -> String {
73 let mut parts = Vec::new();
74
75 parts.push(format!(" {}", col.name.to_sql()));
76
77 if let Some(sqlite_type) = &col.sqlite_type {
79 parts.push(sqlite_type.to_string());
80 }
81
82 if col.is_primary_key {
84 parts.push("PRIMARY KEY".to_string());
85 }
86
87 if col.not_null && !col.is_primary_key {
89 parts.push("NOT NULL".to_string());
90 }
91
92 if col.is_unique {
94 parts.push("UNIQUE".to_string());
95 }
96
97 if let Some(default) = &col.default {
99 let sql = default.to_sql();
100 if needs_default_parens(&sql) {
102 parts.push(format!("DEFAULT ({sql})"));
103 } else {
104 parts.push(format!("DEFAULT {sql}"));
105 }
106 }
107
108 if let Some(check) = &col.check {
110 parts.push(format!("CHECK ({})", check.to_sql()));
111 }
112
113 if let Some(fk) = &col.references {
115 let mut fk_str = format!("REFERENCES {}", fk.table.to_sql());
116 if let Some(col_ref) = &fk.column {
117 fk_str.push_str(&format!("({})", col_ref.to_sql()));
118 }
119 if let Some(action) = &fk.on_delete {
120 fk_str.push_str(&format!(" ON DELETE {action}"));
121 }
122 if let Some(action) = &fk.on_update {
123 fk_str.push_str(&format!(" ON UPDATE {action}"));
124 }
125 parts.push(fk_str);
126 }
127
128 parts.join(" ")
129}
130
131fn render_table_constraint(constraint: &TableConstraint) -> String {
132 match constraint {
133 TableConstraint::PrimaryKey { columns, .. } => {
134 let cols: Vec<String> = columns.iter().map(|c| c.to_sql()).collect();
135 format!(" PRIMARY KEY ({})", cols.join(", "))
136 }
137 TableConstraint::Unique { columns, .. } => {
138 let cols: Vec<String> = columns.iter().map(|c| c.to_sql()).collect();
139 format!(" UNIQUE ({})", cols.join(", "))
140 }
141 TableConstraint::Check { expr, .. } => {
142 format!(" CHECK ({})", expr.to_sql())
143 }
144 TableConstraint::ForeignKey {
145 columns,
146 ref_table,
147 ref_columns,
148 on_delete,
149 on_update,
150 ..
151 } => {
152 let cols: Vec<String> = columns.iter().map(|c| c.to_sql()).collect();
153 let ref_cols: Vec<String> = ref_columns.iter().map(|c| c.to_sql()).collect();
154 let mut s = format!(
155 " FOREIGN KEY ({}) REFERENCES {}({})",
156 cols.join(", "),
157 ref_table.to_sql(),
158 ref_cols.join(", ")
159 );
160 if let Some(action) = on_delete {
161 s.push_str(&format!(" ON DELETE {action}"));
162 }
163 if let Some(action) = on_update {
164 s.push_str(&format!(" ON UPDATE {action}"));
165 }
166 s
167 }
168 }
169}
170
171fn render_index(index: &Index, out: &mut String) {
172 if index.unique {
173 out.push_str("CREATE UNIQUE INDEX ");
174 } else {
175 out.push_str("CREATE INDEX ");
176 }
177
178 out.push_str(&index.name.to_sql());
179 out.push_str(" ON ");
180 out.push_str(&index.table.to_sql());
181 out.push_str(" (");
182
183 let cols: Vec<String> = index
184 .columns
185 .iter()
186 .map(|c| match c {
187 IndexColumn::Column(ident) => ident.to_sql(),
188 IndexColumn::Expression(expr) => expr.to_sql(),
189 })
190 .collect();
191
192 out.push_str(&cols.join(", "));
193 out.push(')');
194
195 if let Some(where_clause) = &index.where_clause {
196 out.push_str(&format!(" WHERE {}", where_clause.to_sql()));
197 }
198
199 out.push_str(";\n");
200}
201
202fn needs_default_parens(sql: &str) -> bool {
203 sql.contains('(') || sql == "CURRENT_TIMESTAMP"
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use crate::ir::*;
210
211 fn make_column(name: &str, sqlite_type: SqliteType) -> Column {
212 Column {
213 name: Ident::new(name),
214 pg_type: PgType::Integer,
215 sqlite_type: Some(sqlite_type),
216 not_null: false,
217 default: None,
218 is_primary_key: false,
219 is_unique: false,
220 references: None,
221 check: None,
222 }
223 }
224
225 #[test]
226 fn test_render_simple_table() {
227 let model = SchemaModel {
228 tables: vec![Table {
229 name: QualifiedName::new(Ident::new("users")),
230 columns: vec![
231 {
232 let mut c = make_column("id", SqliteType::Integer);
233 c.is_primary_key = true;
234 c
235 },
236 {
237 let mut c = make_column("name", SqliteType::Text);
238 c.not_null = true;
239 c
240 },
241 ],
242 constraints: vec![],
243 }],
244 ..Default::default()
245 };
246
247 let sql = render(&model, false);
248 assert!(sql.contains("CREATE TABLE users"));
249 assert!(sql.contains("id INTEGER PRIMARY KEY"));
250 assert!(sql.contains("name TEXT NOT NULL"));
251 }
252
253 #[test]
254 fn test_render_with_pragma() {
255 let model = SchemaModel::default();
256 let sql = render(&model, true);
257 assert!(sql.starts_with("PRAGMA foreign_keys = ON;"));
258 }
259
260 #[test]
261 fn test_render_composite_pk() {
262 let model = SchemaModel {
263 tables: vec![Table {
264 name: QualifiedName::new(Ident::new("user_roles")),
265 columns: vec![
266 make_column("user_id", SqliteType::Integer),
267 make_column("role_id", SqliteType::Integer),
268 ],
269 constraints: vec![TableConstraint::PrimaryKey {
270 name: None,
271 columns: vec![Ident::new("user_id"), Ident::new("role_id")],
272 }],
273 }],
274 ..Default::default()
275 };
276
277 let sql = render(&model, false);
278 assert!(sql.contains("PRIMARY KEY (user_id, role_id)"));
279 }
280
281 #[test]
282 fn test_render_fk_with_cascade() {
283 let model = SchemaModel {
284 tables: vec![Table {
285 name: QualifiedName::new(Ident::new("orders")),
286 columns: vec![make_column("user_id", SqliteType::Integer)],
287 constraints: vec![TableConstraint::ForeignKey {
288 name: None,
289 columns: vec![Ident::new("user_id")],
290 ref_table: QualifiedName::new(Ident::new("users")),
291 ref_columns: vec![Ident::new("id")],
292 on_delete: Some(FkAction::Cascade),
293 on_update: None,
294 deferrable: false,
295 }],
296 }],
297 ..Default::default()
298 };
299
300 let sql = render(&model, true);
301 assert!(sql.contains("FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE"));
302 }
303
304 #[test]
305 fn test_render_index() {
306 let model = SchemaModel {
307 indexes: vec![Index {
308 name: Ident::new("idx_email"),
309 table: QualifiedName::new(Ident::new("users")),
310 columns: vec![IndexColumn::Column(Ident::new("email"))],
311 unique: true,
312 method: None,
313 where_clause: None,
314 }],
315 ..Default::default()
316 };
317
318 let sql = render(&model, false);
319 assert!(sql.contains("CREATE UNIQUE INDEX idx_email ON users (email);"));
320 }
321
322 #[test]
323 fn test_render_default_current_timestamp() {
324 let model = SchemaModel {
325 tables: vec![Table {
326 name: QualifiedName::new(Ident::new("events")),
327 columns: vec![{
328 let mut c = make_column("created_at", SqliteType::Text);
329 c.default = Some(Expr::CurrentTimestamp);
330 c
331 }],
332 constraints: vec![],
333 }],
334 ..Default::default()
335 };
336
337 let sql = render(&model, false);
338 assert!(sql.contains("DEFAULT (CURRENT_TIMESTAMP)"));
339 }
340}