Skip to main content

cqlite_cli/output/
table.rs

1//! Table output writer for QueryResult
2//!
3//! This module provides a writer that adapts QueryResult to the existing
4//! CqlshTableFormatter for consistent cqlsh-compatible table output.
5
6use crate::config::OutputConfig;
7use crate::formatter::CqlshTableFormatter;
8use crate::output::value_fmt::ValueFormatter;
9use cqlite_core::query::QueryResult;
10
11/// Table writer for QueryResult
12///
13/// Converts QueryResult instances to cqlsh-compatible table format
14/// using the existing CqlshTableFormatter infrastructure.
15pub struct TableWriter;
16
17impl TableWriter {
18    /// Write QueryResult as a cqlsh-compatible table
19    ///
20    /// # Arguments
21    /// * `result` - The query result to format
22    /// * `config` - Output configuration for color support and row limits
23    ///
24    /// # Returns
25    /// Formatted table string with headers, data rows, and row count footer
26    ///
27    /// # Contract Compliance
28    /// - Uses `metadata.columns` for headers and column order (single source of truth)
29    /// - Right-aligns numeric columns via CqlshTableFormatter
30    /// - Prints `(N rows)` footer matching cqlsh style
31    /// - Handles missing values as empty cells (null convention)
32    /// - Applies color settings from config
33    /// - Respects row limit from config (truncates display if set)
34    pub fn write(
35        result: &QueryResult,
36        config: &OutputConfig,
37    ) -> Result<String, Box<dyn std::error::Error>> {
38        let mut formatter = CqlshTableFormatter::new();
39
40        // Apply color support from config
41        formatter.set_color_support(config.color_enabled);
42
43        // Extract headers from metadata.columns in order
44        // This is the single source of truth for column names and order
45        let headers: Vec<String> = result
46            .metadata
47            .columns
48            .iter()
49            .map(|col| col.name.clone())
50            .collect();
51
52        formatter.set_headers(headers);
53
54        // Apply row limit if specified in config
55        let rows_to_display = if let Some(limit) = config.limit {
56            &result.rows[..result.rows.len().min(limit)]
57        } else {
58            &result.rows
59        };
60
61        // Add rows in column order from metadata
62        for row in rows_to_display {
63            let row_data: Vec<String> = result
64                .metadata
65                .columns
66                .iter()
67                .map(|col| {
68                    // Look up value by column name from metadata
69                    // If missing, treat as null (empty string per contract)
70                    row.values
71                        .get(&col.name)
72                        .map(|v| ValueFormatter::format_value(v))
73                        .unwrap_or_else(|| String::new())
74                })
75                .collect();
76            formatter.add_row(row_data);
77        }
78
79        // CqlshTableFormatter handles row count footer automatically
80        Ok(formatter.format())
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use cqlite_core::query::{ColumnInfo, QueryRow};
88    use cqlite_core::types::DataType;
89    use cqlite_core::{RowKey, Value};
90
91    #[test]
92    fn test_empty_result() {
93        let result = QueryResult::new();
94        let config = OutputConfig::default();
95        let output = TableWriter::write(&result, &config).unwrap();
96        assert!(
97            output.is_empty(),
98            "Empty result should produce empty output"
99        );
100    }
101
102    #[test]
103    fn test_basic_table() {
104        let mut result = QueryResult::new();
105
106        // Set up metadata with column order
107        result.metadata.columns = vec![
108            ColumnInfo::new("id".to_string(), DataType::Integer, false, 0),
109            ColumnInfo::new("name".to_string(), DataType::Text, true, 1),
110        ];
111
112        // Add rows
113        let mut row1 = QueryRow::new(RowKey::new(vec![1]));
114        row1.set("id".to_string(), Value::Integer(1));
115        row1.set("name".to_string(), Value::Text("Alice".to_string()));
116
117        let mut row2 = QueryRow::new(RowKey::new(vec![2]));
118        row2.set("id".to_string(), Value::Integer(2));
119        row2.set("name".to_string(), Value::Text("Bob".to_string()));
120
121        result.rows = vec![row1, row2];
122
123        let config = OutputConfig::default();
124        let output = TableWriter::write(&result, &config).unwrap();
125
126        // Verify headers are present
127        assert!(output.contains("id"), "Output should contain 'id' header");
128        assert!(
129            output.contains("name"),
130            "Output should contain 'name' header"
131        );
132
133        // Verify data is present
134        assert!(output.contains("Alice"), "Output should contain 'Alice'");
135        assert!(output.contains("Bob"), "Output should contain 'Bob'");
136
137        // Verify row count footer
138        assert!(
139            output.contains("(2 rows)"),
140            "Output should contain '(2 rows)' footer"
141        );
142    }
143
144    #[test]
145    fn test_column_order_from_metadata() {
146        let mut result = QueryResult::new();
147
148        // Set up metadata with specific column order
149        result.metadata.columns = vec![
150            ColumnInfo::new("z_last".to_string(), DataType::Integer, false, 0),
151            ColumnInfo::new("a_first".to_string(), DataType::Text, false, 1),
152        ];
153
154        // Add row with values (order in HashMap doesn't matter)
155        let mut row = QueryRow::new(RowKey::new(vec![1]));
156        row.set("a_first".to_string(), Value::Text("first".to_string()));
157        row.set("z_last".to_string(), Value::Integer(999));
158
159        result.rows = vec![row];
160
161        let config = OutputConfig::default();
162        let output = TableWriter::write(&result, &config).unwrap();
163
164        // The output should follow metadata.columns order (z_last before a_first)
165        // We can verify this by checking the position of headers in the output
166        let z_pos = output.find("z_last").expect("Should find z_last");
167        let a_pos = output.find("a_first").expect("Should find a_first");
168        assert!(
169            z_pos < a_pos,
170            "z_last should appear before a_first in output"
171        );
172    }
173
174    #[test]
175    fn test_null_values() {
176        let mut result = QueryResult::new();
177
178        result.metadata.columns = vec![
179            ColumnInfo::new("id".to_string(), DataType::Integer, false, 0),
180            ColumnInfo::new("optional".to_string(), DataType::Text, true, 1),
181        ];
182
183        // Row with missing value (treated as null)
184        let mut row = QueryRow::new(RowKey::new(vec![1]));
185        row.set("id".to_string(), Value::Integer(1));
186        // "optional" is not set, should be treated as null
187
188        result.rows = vec![row];
189
190        let config = OutputConfig::default();
191        let output = TableWriter::write(&result, &config).unwrap();
192
193        // Should not crash and should handle missing value
194        assert!(output.contains("id"));
195        assert!(output.contains("optional"));
196        assert!(output.contains("(1 rows)"));
197    }
198
199    #[test]
200    fn test_row_count_footer() {
201        let mut result = QueryResult::new();
202
203        result.metadata.columns = vec![ColumnInfo::new(
204            "id".to_string(),
205            DataType::Integer,
206            false,
207            0,
208        )];
209
210        // Add 5 rows
211        for i in 1..=5 {
212            let mut row = QueryRow::new(RowKey::new(vec![i as u8]));
213            row.set("id".to_string(), Value::Integer(i));
214            result.rows.push(row);
215        }
216
217        let config = OutputConfig::default();
218        let output = TableWriter::write(&result, &config).unwrap();
219
220        // Should show (5 rows)
221        assert!(
222            output.contains("(5 rows)"),
223            "Output should contain '(5 rows)' footer"
224        );
225    }
226
227    #[test]
228    fn test_config_limit() {
229        let mut result = QueryResult::new();
230
231        result.metadata.columns = vec![ColumnInfo::new(
232            "id".to_string(),
233            DataType::Integer,
234            false,
235            0,
236        )];
237
238        // Add 10 rows
239        for i in 1..=10 {
240            let mut row = QueryRow::new(RowKey::new(vec![i as u8]));
241            row.set("id".to_string(), Value::Integer(i));
242            result.rows.push(row);
243        }
244
245        // Apply limit of 3 rows
246        let config = OutputConfig {
247            color_enabled: true,
248            limit: Some(3),
249            page_size: None,
250            target: crate::output::OutputTarget::Stdout,
251            overwrite: false,
252        };
253        let output = TableWriter::write(&result, &config).unwrap();
254
255        // Should only show first 3 rows
256        assert!(output.contains("1"), "Output should contain row 1");
257        assert!(output.contains("2"), "Output should contain row 2");
258        assert!(output.contains("3"), "Output should contain row 3");
259
260        // Should show (3 rows) in footer, not (10 rows)
261        assert!(
262            output.contains("(3 rows)"),
263            "Output should contain '(3 rows)' footer"
264        );
265    }
266
267    #[test]
268    fn test_config_no_limit() {
269        let mut result = QueryResult::new();
270
271        result.metadata.columns = vec![ColumnInfo::new(
272            "id".to_string(),
273            DataType::Integer,
274            false,
275            0,
276        )];
277
278        // Add 5 rows
279        for i in 1..=5 {
280            let mut row = QueryRow::new(RowKey::new(vec![i as u8]));
281            row.set("id".to_string(), Value::Integer(i));
282            result.rows.push(row);
283        }
284
285        // No limit
286        let config = OutputConfig {
287            color_enabled: true,
288            limit: None,
289            page_size: None,
290            target: crate::output::OutputTarget::Stdout,
291            overwrite: false,
292        };
293        let output = TableWriter::write(&result, &config).unwrap();
294
295        // Should show all 5 rows
296        assert!(
297            output.contains("(5 rows)"),
298            "Output should contain '(5 rows)' footer"
299        );
300    }
301
302    #[test]
303    fn test_config_colors_disabled() {
304        let mut result = QueryResult::new();
305
306        result.metadata.columns = vec![ColumnInfo::new(
307            "id".to_string(),
308            DataType::Integer,
309            false,
310            0,
311        )];
312
313        let mut row = QueryRow::new(RowKey::new(vec![1]));
314        row.set("id".to_string(), Value::Integer(1));
315        result.rows = vec![row];
316
317        // Disable colors
318        let config = OutputConfig {
319            color_enabled: false,
320            limit: None,
321            page_size: None,
322            target: crate::output::OutputTarget::Stdout,
323            overwrite: false,
324        };
325        let output = TableWriter::write(&result, &config).unwrap();
326
327        // Output should not contain ANSI color codes (e.g., \x1b[)
328        assert!(
329            !output.contains("\x1b["),
330            "Output should not contain ANSI color codes when colors are disabled"
331        );
332    }
333}