use crate::config::OutputConfig;
use crate::formatter::CqlshTableFormatter;
use crate::output::value_fmt::ValueFormatter;
use cqlite_core::query::QueryResult;
pub struct TableWriter;
impl TableWriter {
pub fn write(
result: &QueryResult,
config: &OutputConfig,
) -> Result<String, Box<dyn std::error::Error>> {
let mut formatter = CqlshTableFormatter::new();
formatter.set_color_support(config.color_enabled);
let headers: Vec<String> = result
.metadata
.columns
.iter()
.map(|col| col.name.clone())
.collect();
formatter.set_headers(headers);
let rows_to_display = if let Some(limit) = config.limit {
&result.rows[..result.rows.len().min(limit)]
} else {
&result.rows
};
for row in rows_to_display {
let row_data: Vec<String> = result
.metadata
.columns
.iter()
.map(|col| {
row.values
.get(&col.name)
.map(|v| ValueFormatter::format_value(v))
.unwrap_or_else(|| String::new())
})
.collect();
formatter.add_row(row_data);
}
Ok(formatter.format())
}
}
#[cfg(test)]
mod tests {
use super::*;
use cqlite_core::query::{ColumnInfo, QueryRow};
use cqlite_core::types::DataType;
use cqlite_core::{RowKey, Value};
#[test]
fn test_empty_result() {
let result = QueryResult::new();
let config = OutputConfig::default();
let output = TableWriter::write(&result, &config).unwrap();
assert!(
output.is_empty(),
"Empty result should produce empty output"
);
}
#[test]
fn test_basic_table() {
let mut result = QueryResult::new();
result.metadata.columns = vec![
ColumnInfo::new("id".to_string(), DataType::Integer, false, 0),
ColumnInfo::new("name".to_string(), DataType::Text, true, 1),
];
let mut row1 = QueryRow::new(RowKey::new(vec![1]));
row1.set("id".to_string(), Value::Integer(1));
row1.set("name".to_string(), Value::Text("Alice".to_string()));
let mut row2 = QueryRow::new(RowKey::new(vec![2]));
row2.set("id".to_string(), Value::Integer(2));
row2.set("name".to_string(), Value::Text("Bob".to_string()));
result.rows = vec![row1, row2];
let config = OutputConfig::default();
let output = TableWriter::write(&result, &config).unwrap();
assert!(output.contains("id"), "Output should contain 'id' header");
assert!(
output.contains("name"),
"Output should contain 'name' header"
);
assert!(output.contains("Alice"), "Output should contain 'Alice'");
assert!(output.contains("Bob"), "Output should contain 'Bob'");
assert!(
output.contains("(2 rows)"),
"Output should contain '(2 rows)' footer"
);
}
#[test]
fn test_column_order_from_metadata() {
let mut result = QueryResult::new();
result.metadata.columns = vec![
ColumnInfo::new("z_last".to_string(), DataType::Integer, false, 0),
ColumnInfo::new("a_first".to_string(), DataType::Text, false, 1),
];
let mut row = QueryRow::new(RowKey::new(vec![1]));
row.set("a_first".to_string(), Value::Text("first".to_string()));
row.set("z_last".to_string(), Value::Integer(999));
result.rows = vec![row];
let config = OutputConfig::default();
let output = TableWriter::write(&result, &config).unwrap();
let z_pos = output.find("z_last").expect("Should find z_last");
let a_pos = output.find("a_first").expect("Should find a_first");
assert!(
z_pos < a_pos,
"z_last should appear before a_first in output"
);
}
#[test]
fn test_null_values() {
let mut result = QueryResult::new();
result.metadata.columns = vec![
ColumnInfo::new("id".to_string(), DataType::Integer, false, 0),
ColumnInfo::new("optional".to_string(), DataType::Text, true, 1),
];
let mut row = QueryRow::new(RowKey::new(vec![1]));
row.set("id".to_string(), Value::Integer(1));
result.rows = vec![row];
let config = OutputConfig::default();
let output = TableWriter::write(&result, &config).unwrap();
assert!(output.contains("id"));
assert!(output.contains("optional"));
assert!(output.contains("(1 rows)"));
}
#[test]
fn test_row_count_footer() {
let mut result = QueryResult::new();
result.metadata.columns = vec![ColumnInfo::new(
"id".to_string(),
DataType::Integer,
false,
0,
)];
for i in 1..=5 {
let mut row = QueryRow::new(RowKey::new(vec![i as u8]));
row.set("id".to_string(), Value::Integer(i));
result.rows.push(row);
}
let config = OutputConfig::default();
let output = TableWriter::write(&result, &config).unwrap();
assert!(
output.contains("(5 rows)"),
"Output should contain '(5 rows)' footer"
);
}
#[test]
fn test_config_limit() {
let mut result = QueryResult::new();
result.metadata.columns = vec![ColumnInfo::new(
"id".to_string(),
DataType::Integer,
false,
0,
)];
for i in 1..=10 {
let mut row = QueryRow::new(RowKey::new(vec![i as u8]));
row.set("id".to_string(), Value::Integer(i));
result.rows.push(row);
}
let config = OutputConfig {
color_enabled: true,
limit: Some(3),
page_size: None,
target: crate::output::OutputTarget::Stdout,
overwrite: false,
};
let output = TableWriter::write(&result, &config).unwrap();
assert!(output.contains("1"), "Output should contain row 1");
assert!(output.contains("2"), "Output should contain row 2");
assert!(output.contains("3"), "Output should contain row 3");
assert!(
output.contains("(3 rows)"),
"Output should contain '(3 rows)' footer"
);
}
#[test]
fn test_config_no_limit() {
let mut result = QueryResult::new();
result.metadata.columns = vec![ColumnInfo::new(
"id".to_string(),
DataType::Integer,
false,
0,
)];
for i in 1..=5 {
let mut row = QueryRow::new(RowKey::new(vec![i as u8]));
row.set("id".to_string(), Value::Integer(i));
result.rows.push(row);
}
let config = OutputConfig {
color_enabled: true,
limit: None,
page_size: None,
target: crate::output::OutputTarget::Stdout,
overwrite: false,
};
let output = TableWriter::write(&result, &config).unwrap();
assert!(
output.contains("(5 rows)"),
"Output should contain '(5 rows)' footer"
);
}
#[test]
fn test_config_colors_disabled() {
let mut result = QueryResult::new();
result.metadata.columns = vec![ColumnInfo::new(
"id".to_string(),
DataType::Integer,
false,
0,
)];
let mut row = QueryRow::new(RowKey::new(vec![1]));
row.set("id".to_string(), Value::Integer(1));
result.rows = vec![row];
let config = OutputConfig {
color_enabled: false,
limit: None,
page_size: None,
target: crate::output::OutputTarget::Stdout,
overwrite: false,
};
let output = TableWriter::write(&result, &config).unwrap();
assert!(
!output.contains("\x1b["),
"Output should not contain ANSI color codes when colors are disabled"
);
}
}