Skip to main content

sql_splitter/differ/output/
text.rs

1//! Text output formatter for diff results.
2
3use crate::differ::DiffResult;
4
5/// Format diff result as human-readable text
6pub fn format_text(result: &DiffResult) -> String {
7    let mut output = String::new();
8
9    // Schema changes
10    if let Some(ref schema) = result.schema {
11        output.push_str("Schema Changes:\n");
12
13        if schema.tables_added.is_empty()
14            && schema.tables_removed.is_empty()
15            && schema.tables_modified.is_empty()
16        {
17            output.push_str("  (no schema changes)\n");
18        } else {
19            // Added tables
20            for table in &schema.tables_added {
21                output.push_str(&format!("  + Table '{}' (new)\n", table.name));
22                for col in &table.columns {
23                    let nullable = if col.is_nullable { "NULL" } else { "NOT NULL" };
24                    let pk = if col.is_primary_key { " [PK]" } else { "" };
25                    output.push_str(&format!(
26                        "      + {} {} {}{}\n",
27                        col.name, col.col_type, nullable, pk
28                    ));
29                }
30            }
31
32            // Removed tables
33            for table_name in &schema.tables_removed {
34                output.push_str(&format!("  - Table '{}' (removed)\n", table_name));
35            }
36
37            // Modified tables
38            for modification in &schema.tables_modified {
39                output.push_str(&format!("  ~ Table '{}':\n", modification.table_name));
40
41                for col in &modification.columns_added {
42                    let nullable = if col.is_nullable { "NULL" } else { "NOT NULL" };
43                    output.push_str(&format!(
44                        "      + Column '{}' {} {}\n",
45                        col.name, col.col_type, nullable
46                    ));
47                }
48
49                for col in &modification.columns_removed {
50                    output.push_str(&format!("      - Column '{}' {}\n", col.name, col.col_type));
51                }
52
53                for change in &modification.columns_modified {
54                    let mut changes = Vec::new();
55                    if let (Some(old_type), Some(new_type)) = (&change.old_type, &change.new_type) {
56                        changes.push(format!("{} → {}", old_type, new_type));
57                    }
58                    if let (Some(old_null), Some(new_null)) =
59                        (change.old_nullable, change.new_nullable)
60                    {
61                        let old_str = if old_null { "NULL" } else { "NOT NULL" };
62                        let new_str = if new_null { "NULL" } else { "NOT NULL" };
63                        changes.push(format!("{} → {}", old_str, new_str));
64                    }
65                    output.push_str(&format!(
66                        "      ~ Column '{}': {}\n",
67                        change.name,
68                        changes.join(", ")
69                    ));
70                }
71
72                if modification.pk_changed {
73                    let old_pk = modification
74                        .old_pk
75                        .as_ref()
76                        .map(|pk| pk.join(", "))
77                        .unwrap_or_else(|| "(none)".to_string());
78                    let new_pk = modification
79                        .new_pk
80                        .as_ref()
81                        .map(|pk| pk.join(", "))
82                        .unwrap_or_else(|| "(none)".to_string());
83                    output.push_str(&format!(
84                        "      ~ PRIMARY KEY: ({}) → ({})\n",
85                        old_pk, new_pk
86                    ));
87                }
88
89                for fk in &modification.fks_added {
90                    output.push_str(&format!(
91                        "      + FK ({}) → {}.({}))\n",
92                        fk.columns.join(", "),
93                        fk.referenced_table,
94                        fk.referenced_columns.join(", ")
95                    ));
96                }
97
98                for fk in &modification.fks_removed {
99                    output.push_str(&format!(
100                        "      - FK ({}) → {}.({}))\n",
101                        fk.columns.join(", "),
102                        fk.referenced_table,
103                        fk.referenced_columns.join(", ")
104                    ));
105                }
106
107                for idx in &modification.indexes_added {
108                    let unique_marker = if idx.is_unique { " [unique]" } else { "" };
109                    let type_marker = idx
110                        .index_type
111                        .as_ref()
112                        .map(|t| format!(" [{}]", t))
113                        .unwrap_or_default();
114                    output.push_str(&format!(
115                        "      + Index '{}' on ({}){}{}\n",
116                        idx.name,
117                        idx.columns.join(", "),
118                        unique_marker,
119                        type_marker
120                    ));
121                }
122
123                for idx in &modification.indexes_removed {
124                    let unique_marker = if idx.is_unique { " [unique]" } else { "" };
125                    let type_marker = idx
126                        .index_type
127                        .as_ref()
128                        .map(|t| format!(" [{}]", t))
129                        .unwrap_or_default();
130                    output.push_str(&format!(
131                        "      - Index '{}' on ({}){}{}\n",
132                        idx.name,
133                        idx.columns.join(", "),
134                        unique_marker,
135                        type_marker
136                    ));
137                }
138            }
139        }
140
141        output.push('\n');
142    }
143
144    // Data changes
145    if let Some(ref data) = result.data {
146        output.push_str("Data Changes:\n");
147
148        if data.tables.is_empty() {
149            output.push_str("  (no data changes)\n");
150        } else {
151            // Sort tables for consistent output
152            let mut table_names: Vec<_> = data.tables.keys().collect();
153            table_names.sort();
154
155            for table_name in table_names {
156                let diff = &data.tables[table_name];
157
158                // Skip tables with no changes
159                if diff.added_count == 0 && diff.removed_count == 0 && diff.modified_count == 0 {
160                    continue;
161                }
162
163                let mut parts = Vec::new();
164                if diff.added_count > 0 {
165                    parts.push(format!("+{} rows", diff.added_count));
166                }
167                if diff.removed_count > 0 {
168                    parts.push(format!("-{} rows", diff.removed_count));
169                }
170                if diff.modified_count > 0 {
171                    parts.push(format!("~{} modified", diff.modified_count));
172                }
173
174                let truncated_note = if diff.truncated { " [truncated]" } else { "" };
175
176                output.push_str(&format!(
177                    "  Table '{}': {}{}\n",
178                    table_name,
179                    parts.join(", "),
180                    truncated_note
181                ));
182
183                // Show sample PKs if available (verbose mode)
184                if !diff.sample_added_pks.is_empty() {
185                    let samples = &diff.sample_added_pks;
186                    let remaining = diff.added_count as usize - samples.len();
187                    let suffix = if remaining > 0 {
188                        format!("... (+{} more)", remaining)
189                    } else {
190                        String::new()
191                    };
192                    output.push_str(&format!(
193                        "    Added PKs: {}{}\n",
194                        samples.join(", "),
195                        suffix
196                    ));
197                }
198
199                if !diff.sample_removed_pks.is_empty() {
200                    let samples = &diff.sample_removed_pks;
201                    let remaining = diff.removed_count as usize - samples.len();
202                    let suffix = if remaining > 0 {
203                        format!("... (+{} more)", remaining)
204                    } else {
205                        String::new()
206                    };
207                    output.push_str(&format!(
208                        "    Removed PKs: {}{}\n",
209                        samples.join(", "),
210                        suffix
211                    ));
212                }
213
214                if !diff.sample_modified_pks.is_empty() {
215                    let samples = &diff.sample_modified_pks;
216                    let remaining = diff.modified_count as usize - samples.len();
217                    let suffix = if remaining > 0 {
218                        format!("... (+{} more)", remaining)
219                    } else {
220                        String::new()
221                    };
222                    output.push_str(&format!(
223                        "    Modified PKs: {}{}\n",
224                        samples.join(", "),
225                        suffix
226                    ));
227                }
228            }
229        }
230
231        output.push('\n');
232    }
233
234    // Warnings
235    if !result.warnings.is_empty() {
236        output.push_str("Warnings:\n");
237        for warning in &result.warnings {
238            if let Some(ref table) = warning.table {
239                output.push_str(&format!("  ⚠ Table '{}': {}\n", table, warning.message));
240            } else {
241                output.push_str(&format!("  ⚠ {}\n", warning.message));
242            }
243        }
244        output.push('\n');
245    }
246
247    // Summary
248    output.push_str("Summary:\n");
249    output.push_str(&format!(
250        "  {} tables added, {} removed, {} modified\n",
251        result.summary.tables_added, result.summary.tables_removed, result.summary.tables_modified
252    ));
253    output.push_str(&format!(
254        "  {} rows added, {} removed, {} modified\n",
255        result.summary.rows_added, result.summary.rows_removed, result.summary.rows_modified
256    ));
257
258    if result.summary.truncated {
259        output.push_str("  (some tables truncated due to memory limits)\n");
260    }
261
262    output
263}