1use cqlite_core::storage::sstable::bulletproof_reader::SSTableEntry;
7
8pub const COLUMN_SEPARATOR: &str = " | ";
10pub const HEADER_BORDER_CHAR: char = '-';
11pub const HEADER_SEPARATOR_JUNCTION: &str = "-+-";
12pub const ROW_PREFIX: &str = " ";
13
14pub struct CqlshTableFormatter {
16 pub column_headers: Vec<String>,
17 pub rows: Vec<Vec<String>>,
18 pub show_row_count: bool,
19 pub color_support: bool,
20}
21
22impl Default for CqlshTableFormatter {
23 fn default() -> Self {
24 Self {
25 column_headers: Vec::new(),
26 rows: Vec::new(),
27 show_row_count: true,
28 color_support: false,
29 }
30 }
31}
32
33impl CqlshTableFormatter {
34 pub fn new() -> Self {
36 Self::default()
37 }
38
39 pub fn set_headers(&mut self, headers: Vec<String>) {
41 self.column_headers = headers;
42 }
43
44 pub fn add_row(&mut self, row: Vec<String>) {
46 self.rows.push(row);
47 }
48
49 #[allow(dead_code)]
51 pub fn add_rows(&mut self, rows: Vec<Vec<String>>) {
52 self.rows.extend(rows);
53 }
54
55 #[allow(dead_code)]
57 pub fn from_sstable_entries(&mut self, entries: &[SSTableEntry], table_name: &str) {
58 self.column_headers = vec!["id".to_string(), "data".to_string()];
60
61 for entry in entries {
63 let mut row = Vec::new();
64
65 row.push(hex::encode(entry.key.as_bytes()));
67
68 row.push(entry.format_info.clone());
70
71 while row.len() < self.column_headers.len() {
73 row.push(String::new());
74 }
75
76 self.rows.push(row);
77 }
78
79 println!(
80 "📊 Formatted {} entries from {} into table format",
81 entries.len(),
82 table_name
83 );
84 }
85
86 fn calculate_column_widths(&self) -> Vec<usize> {
88 let column_count = self
89 .column_headers
90 .len()
91 .max(self.rows.first().map(|r| r.len()).unwrap_or(0));
92
93 let mut widths = vec![0; column_count];
94
95 for (i, header) in self.column_headers.iter().enumerate() {
97 if i < widths.len() {
98 widths[i] = header.chars().count();
99 }
100 }
101
102 for row in &self.rows {
104 for (i, cell) in row.iter().enumerate() {
105 if i < widths.len() {
106 widths[i] = widths[i].max(cell.chars().count());
107 }
108 }
109 }
110
111 widths
112 }
113
114 pub fn format(&self) -> String {
116 if self.rows.is_empty() && self.column_headers.is_empty() {
117 return String::new();
118 }
119
120 let widths = self.calculate_column_widths();
121 let mut result = String::new();
122
123 if !self.column_headers.is_empty() {
125 result.push_str(ROW_PREFIX);
126 for (i, header) in self.column_headers.iter().enumerate() {
127 if i > 0 {
128 result.push_str(COLUMN_SEPARATOR);
129 }
130 let width = widths.get(i).copied().unwrap_or(header.len());
131 result.push_str(&format!("{:<width$}", header, width = width));
132 }
133 result.push('\n');
134
135 result.push_str(&self.format_separator_line(&widths));
137 result.push('\n');
138 }
139
140 for row in &self.rows {
142 result.push_str(ROW_PREFIX);
143 for (i, cell) in row.iter().enumerate() {
144 if i > 0 {
145 result.push_str(COLUMN_SEPARATOR);
146 }
147 let width = widths.get(i).copied().unwrap_or(cell.len());
148 result.push_str(&format!("{:>width$}", cell, width = width));
149 }
150 result.push('\n');
151 }
152
153 if self.show_row_count && !self.rows.is_empty() {
155 result.push('\n');
156 result.push_str(&format!("({} rows)", self.rows.len()));
157 }
158
159 result
160 }
161
162 fn format_separator_line(&self, widths: &[usize]) -> String {
164 let mut separator = String::new();
165 separator.push(HEADER_BORDER_CHAR);
166
167 for (i, &width) in widths.iter().enumerate() {
168 if i > 0 {
169 separator.push_str(HEADER_SEPARATOR_JUNCTION);
170 }
171 separator.push_str(&HEADER_BORDER_CHAR.to_string().repeat(width));
172 }
173
174 separator.push(HEADER_BORDER_CHAR);
175 separator
176 }
177
178 #[allow(dead_code)]
180 pub fn clear(&mut self) {
181 self.column_headers.clear();
182 self.rows.clear();
183 }
184
185 #[allow(dead_code)]
187 pub fn row_count(&self) -> usize {
188 self.rows.len()
189 }
190
191 #[allow(dead_code)]
193 pub fn column_count(&self) -> usize {
194 self.column_headers.len()
195 }
196
197 #[allow(dead_code)]
199 pub fn is_empty(&self) -> bool {
200 self.rows.is_empty() && self.column_headers.is_empty()
201 }
202
203 #[allow(dead_code)]
205 pub fn format_as_json(&self) -> serde_json::Value {
206 let mut result = serde_json::Map::new();
207
208 result.insert(
209 "format".to_string(),
210 serde_json::Value::String("table".to_string()),
211 );
212 result.insert(
213 "headers".to_string(),
214 serde_json::json!(self.column_headers),
215 );
216 result.insert("rows".to_string(), serde_json::json!(self.rows));
217 result.insert(
218 "row_count".to_string(),
219 serde_json::Value::Number(self.rows.len().into()),
220 );
221
222 serde_json::Value::Object(result)
223 }
224
225 #[allow(dead_code)]
227 pub fn from_json(value: &serde_json::Value) -> Result<Self, String> {
228 let mut formatter = Self::new();
229
230 if let Some(headers) = value.get("headers").and_then(|h| h.as_array()) {
231 formatter.column_headers = headers
232 .iter()
233 .filter_map(|h| h.as_str())
234 .map(|s| s.to_string())
235 .collect();
236 }
237
238 if let Some(rows) = value.get("rows").and_then(|r| r.as_array()) {
239 for row in rows {
240 if let Some(row_array) = row.as_array() {
241 let row_data: Vec<String> = row_array
242 .iter()
243 .filter_map(|cell| cell.as_str())
244 .map(|s| s.to_string())
245 .collect();
246 formatter.rows.push(row_data);
247 }
248 }
249 }
250
251 Ok(formatter)
252 }
253
254 #[allow(dead_code)]
256 pub fn format_cell_value(&self, value: &str, column_name: &str) -> String {
257 match column_name.to_lowercase().as_str() {
259 "id" | "uuid" => {
260 if self.is_uuid_like(value) {
262 value.to_lowercase()
263 } else {
264 value.to_string()
265 }
266 }
267 "timestamp" | "created_at" | "updated_at" => {
268 value.to_string()
270 }
271 _ => {
272 value.to_string()
274 }
275 }
276 }
277
278 fn is_uuid_like(&self, value: &str) -> bool {
280 value.len() == 36
281 && value.chars().filter(|&c| c == '-').count() == 4
282 && value.chars().all(|c| c.is_ascii_hexdigit() || c == '-')
283 }
284
285 pub fn set_color_support(&mut self, enabled: bool) {
287 self.color_support = enabled;
288 }
289
290 #[allow(dead_code)]
292 pub fn set_show_row_count(&mut self, show: bool) {
293 self.show_row_count = show;
294 }
295}
296
297#[allow(dead_code)]
299pub fn format_sstable_entries_as_table(entries: &[SSTableEntry], table_name: &str) -> String {
300 let mut formatter = CqlshTableFormatter::new();
301 formatter.from_sstable_entries(entries, table_name);
302 formatter.format()
303}
304
305#[allow(dead_code)]
307pub fn format_for_cqlsh_comparison(entries: &[SSTableEntry]) -> String {
308 let mut formatter = CqlshTableFormatter::new();
309 formatter.set_headers(vec!["id".to_string(), "data".to_string()]);
310
311 for entry in entries {
312 let mut row = vec![hex::encode(entry.key.as_bytes())];
313
314 if entry.format_info.is_empty() {
316 row.push(String::new());
317 } else {
318 row.push(entry.format_info.clone());
319 }
320
321 formatter.add_row(row);
322 }
323
324 formatter.format()
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn test_basic_table_formatting() {
333 let mut formatter = CqlshTableFormatter::new();
334 formatter.set_headers(vec!["id".to_string(), "name".to_string()]);
335 formatter.add_row(vec!["1".to_string(), "John".to_string()]);
336 formatter.add_row(vec!["2".to_string(), "Jane".to_string()]);
337
338 let output = formatter.format();
339 assert!(output.contains("id | name"));
340 assert!(output.contains("---+-----"));
341 assert!(output.contains("(2 rows)"));
342 }
343
344 #[test]
345 fn test_column_width_calculation() {
346 let mut formatter = CqlshTableFormatter::new();
347 formatter.set_headers(vec!["short".to_string(), "very_long_header".to_string()]);
348 formatter.add_row(vec!["test".to_string(), "x".to_string()]);
349
350 let widths = formatter.calculate_column_widths();
351 assert_eq!(widths[0], 5); assert_eq!(widths[1], 16); }
354
355 #[test]
356 fn test_right_aligned_data() {
357 let mut formatter = CqlshTableFormatter::new();
358 formatter.set_headers(vec!["id".to_string()]);
359 formatter.add_row(vec!["123".to_string()]);
360
361 let output = formatter.format();
362 let lines: Vec<&str> = output.lines().collect();
364 assert!(lines.len() >= 3);
365 assert!(lines[2].ends_with("123"));
367 }
368
369 #[test]
370 fn test_empty_table() {
371 let formatter = CqlshTableFormatter::new();
372 let output = formatter.format();
373 assert!(output.is_empty());
374 }
375
376 #[test]
377 fn test_uuid_formatting() {
378 let formatter = CqlshTableFormatter::new();
379 let uuid = "A8F167F0-EBE7-4F20-A386-31FF138BEC3B";
380 let formatted = formatter.format_cell_value(uuid, "id");
381 assert_eq!(formatted, "a8f167f0-ebe7-4f20-a386-31ff138bec3b");
382 }
383
384 #[test]
385 fn test_json_conversion() {
386 let mut formatter = CqlshTableFormatter::new();
387 formatter.set_headers(vec!["id".to_string(), "name".to_string()]);
388 formatter.add_row(vec!["1".to_string(), "John".to_string()]);
389
390 let json = formatter.format_as_json();
391 assert!(json.get("headers").is_some());
392 assert!(json.get("rows").is_some());
393 assert!(json.get("row_count").is_some());
394 }
395}