Skip to main content

cqlite_cli/
formatter.rs

1//! CQLite table formatter for cqlsh-compatible output
2//!
3//! This module implements table formatting that exactly matches cqlsh output format,
4//! based on the comprehensive research in CQLSH_FORMAT_SPECIFICATION.md
5
6use cqlite_core::storage::sstable::bulletproof_reader::SSTableEntry;
7
8/// Constants for cqlsh-compatible formatting
9pub const COLUMN_SEPARATOR: &str = " | ";
10pub const HEADER_BORDER_CHAR: char = '-';
11pub const HEADER_SEPARATOR_JUNCTION: &str = "-+-";
12pub const ROW_PREFIX: &str = " ";
13
14/// Table formatter for cqlsh-compatible output
15pub 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    /// Create a new formatter
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    /// Set column headers
40    pub fn set_headers(&mut self, headers: Vec<String>) {
41        self.column_headers = headers;
42    }
43
44    /// Add a data row
45    pub fn add_row(&mut self, row: Vec<String>) {
46        self.rows.push(row);
47    }
48
49    /// Add multiple rows
50    #[allow(dead_code)]
51    pub fn add_rows(&mut self, rows: Vec<Vec<String>>) {
52        self.rows.extend(rows);
53    }
54
55    /// Convert SSTable entries to formatted table
56    #[allow(dead_code)]
57    pub fn from_sstable_entries(&mut self, entries: &[SSTableEntry], table_name: &str) {
58        // Set default headers based on known schema
59        self.column_headers = vec!["id".to_string(), "data".to_string()];
60
61        // Convert entries to rows
62        for entry in entries {
63            let mut row = Vec::new();
64
65            // Add partition key (using 'key' field from SSTableEntry, formatted as hex)
66            row.push(hex::encode(entry.key.as_bytes()));
67
68            // Add format info as a simple representation of the data
69            row.push(entry.format_info.clone());
70
71            // Ensure row has correct number of columns
72            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    /// Calculate optimal column widths (cqlsh algorithm)
87    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        // Start with header widths
96        for (i, header) in self.column_headers.iter().enumerate() {
97            if i < widths.len() {
98                widths[i] = header.chars().count();
99            }
100        }
101
102        // Expand based on data content
103        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    /// Format as cqlsh-compatible table
115    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        // Format headers (left-aligned)
124        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            // Format separator line
136            result.push_str(&self.format_separator_line(&widths));
137            result.push('\n');
138        }
139
140        // Format data rows (right-aligned)
141        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        // Add row count summary
154        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    /// Format the separator line between headers and data
163    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    /// Clear all data
179    #[allow(dead_code)]
180    pub fn clear(&mut self) {
181        self.column_headers.clear();
182        self.rows.clear();
183    }
184
185    /// Get row count
186    #[allow(dead_code)]
187    pub fn row_count(&self) -> usize {
188        self.rows.len()
189    }
190
191    /// Get column count
192    #[allow(dead_code)]
193    pub fn column_count(&self) -> usize {
194        self.column_headers.len()
195    }
196
197    /// Check if table is empty
198    #[allow(dead_code)]
199    pub fn is_empty(&self) -> bool {
200        self.rows.is_empty() && self.column_headers.is_empty()
201    }
202
203    /// Format as JSON for API compatibility
204    #[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    /// Create formatter from JSON data
226    #[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    /// Apply data type specific formatting
255    #[allow(dead_code)]
256    pub fn format_cell_value(&self, value: &str, column_name: &str) -> String {
257        // Handle special formatting based on column type/name
258        match column_name.to_lowercase().as_str() {
259            "id" | "uuid" => {
260                // UUID values should be lowercase
261                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                // Timestamp formatting (keep as-is for now)
269                value.to_string()
270            }
271            _ => {
272                // Default formatting
273                value.to_string()
274            }
275        }
276    }
277
278    /// Check if a value looks like a UUID
279    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    /// Set color support
286    pub fn set_color_support(&mut self, enabled: bool) {
287        self.color_support = enabled;
288    }
289
290    /// Enable/disable row count display
291    #[allow(dead_code)]
292    pub fn set_show_row_count(&mut self, show: bool) {
293        self.show_row_count = show;
294    }
295}
296
297/// Utility function to format SSTable entries for display
298#[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/// Format data for cqlsh comparison
306#[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        // Add format info as data representation
315        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); // "short".len()
352        assert_eq!(widths[1], 16); // "very_long_header".len()
353    }
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        // Should be right-aligned: " id\n----\n 123"
363        let lines: Vec<&str> = output.lines().collect();
364        assert!(lines.len() >= 3);
365        // Data should be right-aligned
366        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}