use std::fmt::Write;
use crate::types::Value;
const MAX_COLUMN_WIDTH: usize = 40;
#[must_use]
pub fn format_result_table(
columns: &[String],
rows: &[Vec<Value>],
execution_time_ms: Option<f64>,
status_message: Option<&str>,
) -> String {
if let Some(msg) = status_message
&& columns.is_empty()
{
return if let Some(ms) = execution_time_ms {
format!("{msg}\n({ms:.2} ms)")
} else {
msg.to_string()
};
}
if columns.is_empty() {
return "(empty)".to_string();
}
let formatted_rows: Vec<Vec<String>> = rows
.iter()
.map(|row| row.iter().map(|v| v.to_string()).collect())
.collect();
let widths: Vec<usize> = columns
.iter()
.enumerate()
.map(|(i, col)| {
let header_width = col.len();
let max_cell = formatted_rows
.iter()
.map(|row| row.get(i).map_or(0, String::len))
.max()
.unwrap_or(0);
header_width.max(max_cell).min(MAX_COLUMN_WIDTH)
})
.collect();
let mut out = String::new();
write_border(&mut out, &widths, '┌', '┬', '┐');
write_row(&mut out, columns, &widths);
write_border(&mut out, &widths, '├', '┼', '┤');
for row in &formatted_rows {
let cells: Vec<&str> = row.iter().map(String::as_str).collect();
write_row(&mut out, &cells, &widths);
}
write_border(&mut out, &widths, '└', '┴', '┘');
let row_count = rows.len();
let row_label = if row_count == 1 { "row" } else { "rows" };
if let Some(ms) = execution_time_ms {
write!(out, "({row_count} {row_label}, {ms:.2} ms)").unwrap();
} else {
write!(out, "({row_count} {row_label})").unwrap();
}
out
}
fn write_border(out: &mut String, widths: &[usize], left: char, mid: char, right: char) {
out.push(left);
for (i, &w) in widths.iter().enumerate() {
if i > 0 {
out.push(mid);
}
for _ in 0..w + 2 {
out.push('─');
}
}
out.push(right);
out.push('\n');
}
fn write_row(out: &mut String, cells: &[impl AsRef<str>], widths: &[usize]) {
out.push('│');
for (i, cell) in cells.iter().enumerate() {
let w = widths.get(i).copied().unwrap_or(0);
let text = cell.as_ref();
if text.len() > w {
let truncated: String = text.chars().take(w.saturating_sub(1)).collect();
write!(out, " {truncated}… ").unwrap();
} else {
write!(out, " {text:<w$} ", w = w).unwrap();
}
out.push('│');
}
out.push('\n');
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Value;
#[test]
fn test_empty_result_no_columns() {
let result = format_result_table(&[], &[], None, None);
assert_eq!(result, "(empty)");
}
#[test]
fn test_status_only() {
let result = format_result_table(&[], &[], None, Some("Created node type 'Person'"));
assert_eq!(result, "Created node type 'Person'");
}
#[test]
fn test_status_with_timing() {
let result = format_result_table(&[], &[], Some(1.23), Some("Created node type 'Person'"));
assert_eq!(result, "Created node type 'Person'\n(1.23 ms)");
}
#[test]
fn test_single_column_no_rows() {
let cols = vec!["name".to_string()];
let result = format_result_table(&cols, &[], None, None);
assert!(result.contains("name"));
assert!(result.contains("(0 rows)"));
}
#[test]
fn test_basic_table() {
let cols = vec!["name".to_string(), "age".to_string()];
let rows = vec![
vec![Value::from("Alix"), Value::Int64(30)],
vec![Value::from("Gus"), Value::Int64(28)],
];
let result = format_result_table(&cols, &rows, None, None);
assert!(result.contains("┌"));
assert!(result.contains("┐"));
assert!(result.contains("├"));
assert!(result.contains("┤"));
assert!(result.contains("└"));
assert!(result.contains("┘"));
assert!(result.contains("name"));
assert!(result.contains("age"));
assert!(result.contains("\"Alix\""));
assert!(result.contains("30"));
assert!(result.contains("\"Gus\""));
assert!(result.contains("28"));
assert!(result.contains("(2 rows)"));
}
#[test]
fn test_single_row_label() {
let cols = vec!["x".to_string()];
let rows = vec![vec![Value::Int64(1)]];
let result = format_result_table(&cols, &rows, None, None);
assert!(result.contains("(1 row)"));
}
#[test]
fn test_with_execution_time() {
let cols = vec!["n".to_string()];
let rows = vec![vec![Value::Int64(42)]];
let result = format_result_table(&cols, &rows, Some(1.50), None);
assert!(result.contains("(1 row, 1.50 ms)"));
}
#[test]
fn test_null_and_bool_values() {
let cols = vec!["a".to_string(), "b".to_string()];
let rows = vec![vec![Value::Null, Value::Bool(true)]];
let result = format_result_table(&cols, &rows, None, None);
assert!(result.contains("NULL"));
assert!(result.contains("true"));
}
#[test]
fn test_long_value_truncated() {
let cols = vec!["data".to_string()];
let long_str = "a".repeat(100);
let rows = vec![vec![Value::from(long_str.as_str())]];
let result = format_result_table(&cols, &rows, None, None);
assert!(result.contains('…'));
}
}