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