1use crate::config::OutputConfig;
7use crate::formatter::CqlshTableFormatter;
8use crate::output::value_fmt::ValueFormatter;
9use cqlite_core::query::QueryResult;
10
11pub struct TableWriter;
16
17impl TableWriter {
18 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 formatter.set_color_support(config.color_enabled);
42
43 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 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 for row in rows_to_display {
63 let row_data: Vec<String> = result
64 .metadata
65 .columns
66 .iter()
67 .map(|col| {
68 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 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 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 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 assert!(output.contains("id"), "Output should contain 'id' header");
128 assert!(
129 output.contains("name"),
130 "Output should contain 'name' header"
131 );
132
133 assert!(output.contains("Alice"), "Output should contain 'Alice'");
135 assert!(output.contains("Bob"), "Output should contain 'Bob'");
136
137 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 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 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 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 let mut row = QueryRow::new(RowKey::new(vec![1]));
185 row.set("id".to_string(), Value::Integer(1));
186 result.rows = vec![row];
189
190 let config = OutputConfig::default();
191 let output = TableWriter::write(&result, &config).unwrap();
192
193 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 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 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 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 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 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 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 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 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 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 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 assert!(
329 !output.contains("\x1b["),
330 "Output should not contain ANSI color codes when colors are disabled"
331 );
332 }
333}