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