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