sql_splitter/differ/output/
sql.rs1use crate::differ::DiffResult;
6use crate::parser::SqlDialect;
7
8pub fn format_sql(result: &DiffResult, dialect: SqlDialect) -> String {
10 let mut output = String::new();
11
12 output.push_str("-- SQL Migration Script\n");
13 output.push_str(&format!(
14 "-- Generated by sql-splitter diff (dialect: {})\n",
15 dialect
16 ));
17 output.push_str("--\n\n");
18
19 let Some(ref schema) = result.schema else {
20 output.push_str("-- No schema changes detected\n");
21 return output;
22 };
23
24 if !schema.has_changes() {
25 output.push_str("-- No schema changes detected\n");
26 return output;
27 }
28
29 let quote = |name: &str| -> String {
31 match dialect {
32 SqlDialect::MySql => format!("`{}`", name),
33 SqlDialect::Postgres | SqlDialect::Sqlite => format!("\"{}\"", name),
34 }
35 };
36
37 for table in &schema.tables_added {
39 output.push_str(&format!("-- New table: {}\n", table.name));
40 if let Some(ref stmt) = table.create_statement {
41 output.push_str(stmt.trim());
42 if !stmt.trim().ends_with(';') {
43 output.push(';');
44 }
45 output.push_str("\n\n");
46 } else {
47 output.push_str(&format!("CREATE TABLE {} (\n", quote(&table.name)));
49
50 let col_defs: Vec<String> = table
51 .columns
52 .iter()
53 .map(|col| {
54 let nullable = if col.is_nullable { "" } else { " NOT NULL" };
55 format!(" {} {}{}", quote(&col.name), col.col_type, nullable)
56 })
57 .collect();
58
59 output.push_str(&col_defs.join(",\n"));
60
61 if !table.primary_key.is_empty() {
62 let pk_cols: Vec<String> = table.primary_key.iter().map(|c| quote(c)).collect();
63 output.push_str(&format!(",\n PRIMARY KEY ({})", pk_cols.join(", ")));
64 }
65
66 output.push_str("\n);\n\n");
67 }
68 }
69
70 for table_name in &schema.tables_removed {
72 output.push_str(&format!("-- Removed table: {}\n", table_name));
73 output.push_str(&format!("DROP TABLE IF EXISTS {};\n\n", quote(table_name)));
74 }
75
76 for modification in &schema.tables_modified {
78 let has_changes = !modification.columns_added.is_empty()
79 || !modification.columns_removed.is_empty()
80 || !modification.columns_modified.is_empty()
81 || !modification.fks_added.is_empty()
82 || !modification.fks_removed.is_empty()
83 || !modification.indexes_added.is_empty()
84 || !modification.indexes_removed.is_empty();
85
86 if !has_changes && !modification.pk_changed {
87 continue;
88 }
89
90 output.push_str(&format!("-- Modified table: {}\n", modification.table_name));
91
92 for col in &modification.columns_added {
94 let nullable = if col.is_nullable { "" } else { " NOT NULL" };
95 output.push_str(&format!(
96 "ALTER TABLE {} ADD COLUMN {} {}{};\n",
97 quote(&modification.table_name),
98 quote(&col.name),
99 col.col_type,
100 nullable
101 ));
102 }
103
104 for col in &modification.columns_removed {
106 output.push_str(&format!(
107 "ALTER TABLE {} DROP COLUMN {};\n",
108 quote(&modification.table_name),
109 quote(&col.name)
110 ));
111 }
112
113 for change in &modification.columns_modified {
115 let new_type = change.new_type.as_deref().unwrap_or("-- unknown type");
116 let nullable = match change.new_nullable {
117 Some(true) => "",
118 Some(false) => " NOT NULL",
119 None => "",
120 };
121
122 match dialect {
123 SqlDialect::MySql => {
124 output.push_str(&format!(
125 "ALTER TABLE {} MODIFY COLUMN {} {}{};\n",
126 quote(&modification.table_name),
127 quote(&change.name),
128 new_type,
129 nullable
130 ));
131 }
132 SqlDialect::Postgres => {
133 if change.new_type.is_some() {
135 output.push_str(&format!(
136 "ALTER TABLE {} ALTER COLUMN {} TYPE {};\n",
137 quote(&modification.table_name),
138 quote(&change.name),
139 new_type
140 ));
141 }
142 if let Some(is_nullable) = change.new_nullable {
143 let constraint = if is_nullable {
144 "DROP NOT NULL"
145 } else {
146 "SET NOT NULL"
147 };
148 output.push_str(&format!(
149 "ALTER TABLE {} ALTER COLUMN {} {};\n",
150 quote(&modification.table_name),
151 quote(&change.name),
152 constraint
153 ));
154 }
155 }
156 SqlDialect::Sqlite => {
157 output.push_str(&format!(
158 "-- SQLite does not support ALTER COLUMN; manual migration required for {}.{}\n",
159 modification.table_name, change.name
160 ));
161 }
162 }
163 }
164
165 for fk in &modification.fks_added {
167 let fk_cols: Vec<String> = fk.columns.iter().map(|c| quote(c)).collect();
168 let ref_cols: Vec<String> = fk.referenced_columns.iter().map(|c| quote(c)).collect();
169
170 let constraint_name = fk
171 .name
172 .as_ref()
173 .map(|n| format!("CONSTRAINT {} ", quote(n)))
174 .unwrap_or_default();
175
176 output.push_str(&format!(
177 "ALTER TABLE {} ADD {}FOREIGN KEY ({}) REFERENCES {}({});\n",
178 quote(&modification.table_name),
179 constraint_name,
180 fk_cols.join(", "),
181 quote(&fk.referenced_table),
182 ref_cols.join(", ")
183 ));
184 }
185
186 for fk in &modification.fks_removed {
188 if let Some(ref name) = fk.name {
189 match dialect {
190 SqlDialect::MySql => {
191 output.push_str(&format!(
192 "ALTER TABLE {} DROP FOREIGN KEY {};\n",
193 quote(&modification.table_name),
194 quote(name)
195 ));
196 }
197 SqlDialect::Postgres => {
198 output.push_str(&format!(
199 "ALTER TABLE {} DROP CONSTRAINT {};\n",
200 quote(&modification.table_name),
201 quote(name)
202 ));
203 }
204 SqlDialect::Sqlite => {
205 output.push_str(&format!(
206 "-- SQLite does not support DROP CONSTRAINT; manual migration required for FK {}\n",
207 name
208 ));
209 }
210 }
211 } else {
212 output.push_str(&format!(
213 "-- Cannot drop unnamed FK ({}) -> {}.({}) without constraint name\n",
214 fk.columns.join(", "),
215 fk.referenced_table,
216 fk.referenced_columns.join(", ")
217 ));
218 }
219 }
220
221 for idx in &modification.indexes_added {
223 let unique = if idx.is_unique { "UNIQUE " } else { "" };
224 let idx_cols: Vec<String> = idx.columns.iter().map(|c| quote(c)).collect();
225
226 match dialect {
227 SqlDialect::Postgres => {
228 let using = idx
229 .index_type
230 .as_ref()
231 .map(|t| format!(" USING {}", t.to_lowercase()))
232 .unwrap_or_default();
233 output.push_str(&format!(
234 "CREATE {}INDEX {} ON {}{}({});\n",
235 unique,
236 quote(&idx.name),
237 quote(&modification.table_name),
238 using,
239 idx_cols.join(", ")
240 ));
241 }
242 _ => {
243 output.push_str(&format!(
244 "CREATE {}INDEX {} ON {}({});\n",
245 unique,
246 quote(&idx.name),
247 quote(&modification.table_name),
248 idx_cols.join(", ")
249 ));
250 }
251 }
252 }
253
254 for idx in &modification.indexes_removed {
256 match dialect {
257 SqlDialect::MySql => {
258 output.push_str(&format!(
259 "DROP INDEX {} ON {};\n",
260 quote(&idx.name),
261 quote(&modification.table_name)
262 ));
263 }
264 SqlDialect::Postgres | SqlDialect::Sqlite => {
265 output.push_str(&format!("DROP INDEX IF EXISTS {};\n", quote(&idx.name)));
266 }
267 }
268 }
269
270 if modification.pk_changed {
272 let old_pk = modification
273 .old_pk
274 .as_ref()
275 .map(|pk| pk.join(", "))
276 .unwrap_or_else(|| "(none)".to_string());
277 let new_pk = modification
278 .new_pk
279 .as_ref()
280 .map(|pk| pk.join(", "))
281 .unwrap_or_else(|| "(none)".to_string());
282
283 output.push_str(&format!(
284 "-- WARNING: PRIMARY KEY change ({}) -> ({}) may require table recreation\n",
285 old_pk, new_pk
286 ));
287
288 match dialect {
289 SqlDialect::MySql => {
290 output.push_str(&format!(
291 "ALTER TABLE {} DROP PRIMARY KEY;\n",
292 quote(&modification.table_name)
293 ));
294 if let Some(ref new_pk_cols) = modification.new_pk {
295 if !new_pk_cols.is_empty() {
296 let pk_cols: Vec<String> =
297 new_pk_cols.iter().map(|c| quote(c)).collect();
298 output.push_str(&format!(
299 "ALTER TABLE {} ADD PRIMARY KEY ({});\n",
300 quote(&modification.table_name),
301 pk_cols.join(", ")
302 ));
303 }
304 }
305 }
306 SqlDialect::Postgres => {
307 output.push_str(&format!(
308 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {}_pkey;\n",
309 quote(&modification.table_name),
310 modification.table_name
311 ));
312 if let Some(ref new_pk_cols) = modification.new_pk {
313 if !new_pk_cols.is_empty() {
314 let pk_cols: Vec<String> =
315 new_pk_cols.iter().map(|c| quote(c)).collect();
316 output.push_str(&format!(
317 "ALTER TABLE {} ADD PRIMARY KEY ({});\n",
318 quote(&modification.table_name),
319 pk_cols.join(", ")
320 ));
321 }
322 }
323 }
324 SqlDialect::Sqlite => {
325 output.push_str(
326 "-- SQLite does not support ALTER PRIMARY KEY; table recreation required\n",
327 );
328 }
329 }
330 }
331
332 output.push('\n');
333 }
334
335 if let Some(ref data) = result.data {
337 let has_data_changes = data
338 .tables
339 .values()
340 .any(|t| t.added_count > 0 || t.removed_count > 0 || t.modified_count > 0);
341
342 if has_data_changes {
343 output.push_str("-- Data changes detected but not included in migration script\n");
344 output.push_str("-- Use text or JSON output format for data diff details\n");
345 }
346 }
347
348 output
349}