client_core/sql_diff/
generator.rs1use super::differ::generate_mysql_diff;
2use super::parser::parse_sql_tables;
3use super::types::{TableColumn, TableDefinition, TableIndex};
4use crate::error::DuckError;
5use crate::mysql_executor::MySqlExecutor;
6use tracing::info;
7
8pub fn generate_schema_diff(
11 from_sql: Option<&str>,
12 to_sql: &str,
13 from_version: Option<&str>,
14 to_version: &str,
15) -> Result<(String, String), DuckError> {
16 match from_sql {
17 None => {
18 info!(
20 "Generating complete database schema for initial version {}",
21 to_version
22 );
23 let description = format!(
24 "Complete database schema for initial version {}",
25 to_version
26 );
27 Ok((to_sql.to_string(), description))
28 }
29 Some(from_content) => {
30 info!(
31 "Starting to generate SQL diff from version {} to {}",
32 from_version.unwrap_or("unknown"),
33 to_version
34 );
35
36 if from_content.trim() == to_sql.trim() {
38 info!("Version content is identical, no diff needed");
39 return Ok((
40 String::new(),
41 format!(
42 "Version {} to {}: No changes",
43 from_version.unwrap_or("unknown"),
44 to_version
45 ),
46 ));
47 }
48
49 let from_tables = parse_sql_tables(from_content)?;
51 let to_tables = parse_sql_tables(to_sql)?;
52
53 let (diff_sql, _stats) = generate_mysql_diff(&from_tables, &to_tables)?;
55
56 let description = if diff_sql.trim().is_empty() {
57 format!(
58 "Version {} to {}: only comments or format changes, no actual schema differences",
59 from_version.unwrap_or("unknown"),
60 to_version
61 )
62 } else {
63 let lines_count = diff_sql
64 .lines()
65 .filter(|line| !line.trim().is_empty() && !line.trim().starts_with("--"))
66 .count();
67
68 let mut change_types = Vec::new();
70 if diff_sql.contains("CREATE TABLE") {
71 change_types.push("new tables");
72 }
73 if diff_sql.contains("DROP TABLE") {
74 change_types.push("dropped tables");
75 }
76 if diff_sql.contains("ALTER TABLE") && diff_sql.contains("ADD COLUMN") {
77 change_types.push("new columns");
78 }
79 if diff_sql.contains("ALTER TABLE") && diff_sql.contains("DROP COLUMN") {
80 change_types.push("dropped columns");
81 }
82 if diff_sql.contains("ALTER TABLE") && diff_sql.contains("MODIFY COLUMN") {
83 change_types.push("modified columns");
84 }
85 if diff_sql.contains("ALTER TABLE") && diff_sql.contains("ADD KEY") {
86 change_types.push("new indexes");
87 }
88 if diff_sql.contains("ALTER TABLE") && diff_sql.contains("DROP KEY") {
89 change_types.push("dropped indexes");
90 }
91
92 let change_summary = if change_types.is_empty() {
93 "schema changes".to_string()
94 } else {
95 change_types.join(", ")
96 };
97
98 format!(
99 "Version {} to {}: {} - generated {} lines of executable diff SQL",
100 from_version.unwrap_or("unknown"),
101 to_version,
102 change_summary,
103 lines_count
104 )
105 };
106
107 info!("Diff generation completed: {}", description);
108 Ok((diff_sql, description))
109 }
110 }
111}
112
113pub async fn generate_live_schema_diff(
116 executor: &MySqlExecutor,
117 to_sql: &str,
118 to_version: &str,
119) -> Result<super::types::SchemaDiffResult, DuckError> {
120 info!(
121 "Starting to generate online schema to {} SQL diff",
122 to_version
123 );
124
125 let to_tables = parse_sql_tables(to_sql)?;
127
128 let (live_tables, live_sql) = executor
130 .fetch_live_schema_with_sql()
131 .await
132 .map_err(|e| DuckError::custom(format!("Failed to fetch online schema: {e}")))?;
133
134 let (diff_sql, stats) = generate_mysql_diff(&live_tables, &to_tables)?;
135
136 let has_executable_sql = stats.has_executable_operations();
138 let has_warnings = stats.has_dangerous_operations();
139
140 let description = if !stats.has_changes() {
141 format!("Online schema to {to_version}: no actual schema differences")
142 } else if !has_executable_sql && has_warnings {
143 format!(
144 "Online schema to {to_version}: only includes delete operation warnings, no executable SQL"
145 )
146 } else {
147 let executable_lines = diff_sql
148 .lines()
149 .filter(|line| !line.trim().is_empty() && !line.trim().starts_with("--"))
150 .count();
151
152 format!(
153 "Online schema to {}: {} - generated {} lines of executable diff SQL",
154 to_version,
155 stats.summary(),
156 executable_lines
157 )
158 };
159
160 info!("Live Diff completed: {}", description);
161
162 Ok(super::types::SchemaDiffResult {
163 diff_sql,
164 description,
165 live_sql: Some(live_sql),
166 has_executable_sql,
167 has_warnings,
168 })
169}
170
171fn format_default_value_for_sql(default: &str) -> String {
173 let mysql_keywords = [
175 "CURRENT_TIMESTAMP",
176 "NOW()",
177 "CURRENT_DATE",
178 "CURRENT_TIME",
179 "LOCALTIMESTAMP",
180 "LOCALTIME",
181 "NULL",
182 "TRUE",
183 "FALSE",
184 ];
185
186 let upper_default = default.to_uppercase();
187
188 if mysql_keywords.contains(&upper_default.as_str()) {
190 return default.to_string();
191 }
192
193 if default
195 .chars()
196 .all(|c| c.is_ascii_digit() || c == '-' || c == '.')
197 {
198 return default.to_string();
199 }
200
201 if (default.starts_with('\'') && default.ends_with('\''))
203 || (default.starts_with('"') && default.ends_with('"'))
204 {
205 return default.to_string();
206 }
207
208 format!("'{}'", default)
210}
211
212pub fn generate_create_table_sql(table: &TableDefinition) -> String {
214 let mut sql = format!("CREATE TABLE `{}` (", table.name);
215
216 let mut parts = Vec::new();
218 for column in &table.columns {
219 parts.push(format!(" {}", generate_column_sql(column)));
220 }
221
222 for index in &table.indexes {
224 parts.push(format!(" {}", generate_index_sql(index)));
225 }
226
227 sql.push_str(&parts.join(",\n"));
228 sql.push_str("\n)");
229
230 if let Some(engine) = &table.engine {
232 sql.push_str(&format!(" ENGINE={engine}"));
233 }
234 if let Some(charset) = &table.charset {
235 sql.push_str(&format!(" DEFAULT CHARSET={charset}"));
236 }
237
238 sql.push(';');
239 sql
240}
241
242pub fn generate_column_sql(column: &TableColumn) -> String {
244 let mut sql = format!("`{}` {}", column.name, column.data_type);
245
246 if !column.nullable {
247 sql.push_str(" NOT NULL");
248 }
249
250 if let Some(default) = &column.default_value {
251 sql.push_str(&format!(
252 " DEFAULT {}",
253 format_default_value_for_sql(default)
254 ));
255 }
256
257 if column.auto_increment {
258 sql.push_str(" AUTO_INCREMENT");
259 }
260
261 if let Some(comment) = &column.comment {
262 sql.push_str(&format!(" COMMENT '{comment}'"));
263 }
264
265 sql
266}
267
268pub fn generate_index_sql(index: &TableIndex) -> String {
270 if index.is_primary {
271 format!(
272 "PRIMARY KEY ({})",
273 index
274 .columns
275 .iter()
276 .map(|c| format!("`{c}`"))
277 .collect::<Vec<_>>()
278 .join(", ")
279 )
280 } else if index.is_unique {
281 format!(
282 "UNIQUE KEY `{}` ({})",
283 index.name,
284 index
285 .columns
286 .iter()
287 .map(|c| format!("`{c}`"))
288 .collect::<Vec<_>>()
289 .join(", ")
290 )
291 } else {
292 format!(
293 "KEY `{}` ({})",
294 index.name,
295 index
296 .columns
297 .iter()
298 .map(|c| format!("`{c}`"))
299 .collect::<Vec<_>>()
300 .join(", ")
301 )
302 }
303}