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 parts.push("PRIMARY KEY".to_string());
88 }
89
90 if col.not_null && !col.is_primary_key {
92 parts.push("NOT NULL".to_string());
93 }
94
95 if col.is_unique {
97 parts.push("UNIQUE".to_string());
98 }
99
100 if let Some(default) = &col.default {
102 let sql = default.to_sql();
103 if needs_default_parens(&sql) {
105 parts.push(format!("DEFAULT ({sql})"));
106 } else {
107 parts.push(format!("DEFAULT {sql}"));
108 }
109 }
110
111 if let Some(check) = &col.check {
113 parts.push(format!("CHECK ({})", check.to_sql()));
114 }
115
116 if let Some(fk) = &col.references {
118 let mut fk_str = format!("REFERENCES {}", fk.table.to_sql());
119 if let Some(col_ref) = &fk.column {
120 fk_str.push_str(&format!("({})", col_ref.to_sql()));
121 }
122 if let Some(action) = &fk.on_delete {
123 fk_str.push_str(&format!(" ON DELETE {action}"));
124 }
125 if let Some(action) = &fk.on_update {
126 fk_str.push_str(&format!(" ON UPDATE {action}"));
127 }
128 parts.push(fk_str);
129 }
130
131 parts.join(" ")
132}
133
134fn render_table_constraint(constraint: &TableConstraint) -> String {
135 match constraint {
136 TableConstraint::PrimaryKey { columns, .. } => {
137 let cols: Vec<String> = columns.iter().map(|c| c.to_sql()).collect();
138 format!(" PRIMARY KEY ({})", cols.join(", "))
139 }
140 TableConstraint::Unique { columns, .. } => {
141 let cols: Vec<String> = columns.iter().map(|c| c.to_sql()).collect();
142 format!(" UNIQUE ({})", cols.join(", "))
143 }
144 TableConstraint::Check { expr, .. } => {
145 format!(" CHECK ({})", expr.to_sql())
146 }
147 TableConstraint::ForeignKey {
148 columns,
149 ref_table,
150 ref_columns,
151 on_delete,
152 on_update,
153 ..
154 } => {
155 let cols: Vec<String> = columns.iter().map(|c| c.to_sql()).collect();
156 let ref_cols: Vec<String> = ref_columns.iter().map(|c| c.to_sql()).collect();
157 let mut s = format!(
158 " FOREIGN KEY ({}) REFERENCES {}({})",
159 cols.join(", "),
160 ref_table.to_sql(),
161 ref_cols.join(", ")
162 );
163 if let Some(action) = on_delete {
164 s.push_str(&format!(" ON DELETE {action}"));
165 }
166 if let Some(action) = on_update {
167 s.push_str(&format!(" ON UPDATE {action}"));
168 }
169 s
170 }
171 }
172}
173
174fn render_index(index: &Index, out: &mut String) {
175 if index.unique {
176 out.push_str("CREATE UNIQUE INDEX ");
177 } else {
178 out.push_str("CREATE INDEX ");
179 }
180
181 out.push_str(&index.name.to_sql());
182 out.push_str(" ON ");
183 out.push_str(&index.table.to_sql());
184 out.push_str(" (");
185
186 let cols: Vec<String> = index
187 .columns
188 .iter()
189 .map(|c| match c {
190 IndexColumn::Column(ident) => ident.to_sql(),
191 IndexColumn::Expression(expr) => expr.to_sql(),
192 })
193 .collect();
194
195 out.push_str(&cols.join(", "));
196 out.push(')');
197
198 if let Some(where_clause) = &index.where_clause {
199 out.push_str(&format!(" WHERE {}", where_clause.to_sql()));
200 }
201
202 out.push_str(";\n");
203}
204
205fn needs_default_parens(sql: &str) -> bool {
206 sql.contains('(') || sql == "CURRENT_TIMESTAMP"
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::ir::*;
213
214 fn make_column(name: &str, sqlite_type: SqliteType) -> Column {
215 Column {
216 name: Ident::new(name),
217 pg_type: PgType::Integer,
218 sqlite_type: Some(sqlite_type),
219 not_null: false,
220 default: None,
221 is_primary_key: false,
222 is_unique: false,
223 references: None,
224 check: None,
225 }
226 }
227
228 #[test]
229 fn test_render_simple_table() {
230 let model = SchemaModel {
231 tables: vec![Table {
232 name: QualifiedName::new(Ident::new("users")),
233 columns: vec![
234 {
235 let mut c = make_column("id", SqliteType::Integer);
236 c.is_primary_key = true;
237 c
238 },
239 {
240 let mut c = make_column("name", SqliteType::Text);
241 c.not_null = true;
242 c
243 },
244 ],
245 constraints: vec![],
246 }],
247 ..Default::default()
248 };
249
250 let sql = render(&model, false);
251 assert!(sql.contains("CREATE TABLE users"));
252 assert!(sql.contains("id INTEGER PRIMARY KEY"));
253 assert!(sql.contains("name TEXT NOT NULL"));
254 }
255
256 #[test]
257 fn test_render_with_pragma() {
258 let model = SchemaModel::default();
259 let sql = render(&model, true);
260 assert!(sql.starts_with(
261 "-- Code generated by `pg2sqlite`. DO NOT EDIT.\n\nPRAGMA foreign_keys = ON;"
262 ));
263 }
264
265 #[test]
266 fn test_render_composite_pk() {
267 let model = SchemaModel {
268 tables: vec![Table {
269 name: QualifiedName::new(Ident::new("user_roles")),
270 columns: vec![
271 make_column("user_id", SqliteType::Integer),
272 make_column("role_id", SqliteType::Integer),
273 ],
274 constraints: vec![TableConstraint::PrimaryKey {
275 name: None,
276 columns: vec![Ident::new("user_id"), Ident::new("role_id")],
277 }],
278 }],
279 ..Default::default()
280 };
281
282 let sql = render(&model, false);
283 assert!(sql.contains("PRIMARY KEY (user_id, role_id)"));
284 }
285
286 #[test]
287 fn test_render_fk_with_cascade() {
288 let model = SchemaModel {
289 tables: vec![Table {
290 name: QualifiedName::new(Ident::new("orders")),
291 columns: vec![make_column("user_id", SqliteType::Integer)],
292 constraints: vec![TableConstraint::ForeignKey {
293 name: None,
294 columns: vec![Ident::new("user_id")],
295 ref_table: QualifiedName::new(Ident::new("users")),
296 ref_columns: vec![Ident::new("id")],
297 on_delete: Some(FkAction::Cascade),
298 on_update: None,
299 deferrable: false,
300 }],
301 }],
302 ..Default::default()
303 };
304
305 let sql = render(&model, true);
306 assert!(sql.contains("FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE"));
307 }
308
309 #[test]
310 fn test_render_index() {
311 let model = SchemaModel {
312 indexes: vec![Index {
313 name: Ident::new("idx_email"),
314 table: QualifiedName::new(Ident::new("users")),
315 columns: vec![IndexColumn::Column(Ident::new("email"))],
316 unique: true,
317 method: None,
318 where_clause: None,
319 }],
320 ..Default::default()
321 };
322
323 let sql = render(&model, false);
324 assert!(sql.contains("CREATE UNIQUE INDEX idx_email ON users (email);"));
325 }
326
327 #[test]
328 fn test_render_default_current_timestamp() {
329 let model = SchemaModel {
330 tables: vec![Table {
331 name: QualifiedName::new(Ident::new("events")),
332 columns: vec![{
333 let mut c = make_column("created_at", SqliteType::Text);
334 c.default = Some(Expr::CurrentTimestamp);
335 c
336 }],
337 constraints: vec![],
338 }],
339 ..Default::default()
340 };
341
342 let sql = render(&model, false);
343 assert!(sql.contains("DEFAULT (CURRENT_TIMESTAMP)"));
344 }
345}