use colored::*;
use comfy_table::{presets::UTF8_FULL, Cell, Color, Table};
use graphlite::{QueryResult, Value};
pub struct ResultFormatter;
impl ResultFormatter {
pub fn format(result: &QueryResult, format: crate::cli::commands::OutputFormat) -> String {
match format {
crate::cli::commands::OutputFormat::Table => Self::format_table(result),
crate::cli::commands::OutputFormat::Json => Self::format_json(result),
crate::cli::commands::OutputFormat::Csv => Self::format_csv(result),
}
}
fn format_table(result: &QueryResult) -> String {
if result.is_session_command() {
if let Some(msg) = result.get_session_message() {
return format!("{}\n", format!("✅ {}", msg).green());
}
}
if result.rows.is_empty() {
return format!("{}\n", "No results found".yellow());
}
let mut output = String::new();
output.push_str(&format!("{}\n", "Query Results".bold().green()));
output.push_str(&format!(
"Execution time: {} ms\n",
result.execution_time_ms
));
output.push_str(&format!("Rows returned: {}\n\n", result.rows.len()));
let mut table = Table::new();
table.load_preset(UTF8_FULL);
let header_cells: Vec<Cell> = result
.variables
.iter()
.map(|col| Cell::new(col).fg(Color::Green))
.collect();
table.set_header(header_cells);
for row in &result.rows {
let row_values: Vec<String> = result
.variables
.iter()
.map(|col| {
row.get_value(col)
.map(|v| Self::value_to_string(v))
.unwrap_or_else(|| "NULL".to_string())
})
.collect();
table.add_row(row_values);
}
output.push_str(&table.to_string());
output.push('\n');
if !result.warnings.is_empty() {
output.push_str(&format!("\n{}\n", "Warnings:".bold().yellow()));
for (i, warning) in result.warnings.iter().enumerate() {
output.push_str(&format!(" {}. {}\n", i + 1, warning.yellow()));
}
}
output
}
fn format_json(result: &QueryResult) -> String {
let mut json_obj = serde_json::json!({
"status": "success",
"columns": result.variables,
"rows": result.rows.iter().map(|row| {
let mut row_map = serde_json::Map::new();
for col in &result.variables {
let value = row.get_value(col)
.map(|v| Self::value_to_json(v))
.unwrap_or(serde_json::Value::Null);
row_map.insert(col.clone(), value);
}
serde_json::Value::Object(row_map)
}).collect::<Vec<_>>(),
"rows_affected": result.rows.len(),
"execution_time_ms": result.execution_time_ms,
});
if !result.warnings.is_empty() {
if let serde_json::Value::Object(ref mut map) = json_obj {
map.insert("warnings".to_string(), serde_json::json!(result.warnings));
}
}
let json_result = json_obj;
serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| {
format!("{{\"status\": \"error\", \"error\": \"Could not serialize results to JSON\"}}")
})
}
fn format_csv(result: &QueryResult) -> String {
let mut output = String::new();
output.push_str(&result.variables.join(","));
output.push('\n');
for row in &result.rows {
let row_values: Vec<String> = result
.variables
.iter()
.map(|col| {
row.get_value(col)
.map(|v| Self::value_to_csv_string(v))
.unwrap_or_else(|| "".to_string())
})
.collect();
output.push_str(&row_values.join(","));
output.push('\n');
}
if !result.warnings.is_empty() {
output.push_str("\n# Warnings:\n");
for (i, warning) in result.warnings.iter().enumerate() {
output.push_str(&format!("# {}. {}\n", i + 1, warning));
}
}
output
}
fn value_to_string(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Boolean(b) => b.to_string(),
Value::Null => "NULL".to_string(),
Value::DateTime(dt) => dt.to_string(),
Value::DateTimeWithFixedOffset(dt) => dt.to_string(),
Value::DateTimeWithNamedTz(tz, dt) => format!("{} {}", dt, tz),
Value::TimeWindow(tw) => format!("TIME_WINDOW({} to {})", tw.start, tw.end),
Value::Path(path) => format!("{:?}", path),
Value::Array(arr) | Value::List(arr) => format!(
"[{}]",
arr.iter()
.map(|v| Self::value_to_string(v))
.collect::<Vec<_>>()
.join(", ")
),
Value::Vector(vec) => format!(
"VECTOR[{}]",
vec.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(", ")
),
Value::Node(node) => {
let labels = if node.labels.is_empty() {
String::new()
} else {
format!(":{}", node.labels.join(":"))
};
format!("({}{})", node.id, labels)
}
Value::Edge(edge) => {
format!("[{}:{}]", edge.id, edge.label)
}
Value::Temporal(temporal) => format!("TEMPORAL({:?})", temporal),
}
}
fn value_to_json(value: &Value) -> serde_json::Value {
match value {
Value::String(s) => serde_json::Value::String(s.clone()),
Value::Number(n) => serde_json::json!(n),
Value::Boolean(b) => serde_json::Value::Bool(*b),
Value::Null => serde_json::Value::Null,
Value::DateTime(dt) => serde_json::Value::String(dt.to_string()),
Value::DateTimeWithFixedOffset(dt) => serde_json::Value::String(dt.to_string()),
Value::DateTimeWithNamedTz(tz, dt) => serde_json::json!({
"datetime": dt.to_string(),
"timezone": tz,
}),
Value::TimeWindow(tw) => serde_json::json!({
"start": tw.start.to_string(),
"end": tw.end.to_string(),
}),
Value::Path(path) => serde_json::json!(format!("{:?}", path)),
Value::Array(arr) | Value::List(arr) => {
serde_json::Value::Array(arr.iter().map(|v| Self::value_to_json(v)).collect())
}
Value::Vector(vec) => {
serde_json::Value::Array(vec.iter().map(|v| serde_json::json!(v)).collect())
}
Value::Node(node) => {
serde_json::json!({
"type": "node",
"id": node.id,
"labels": node.labels,
"properties": node.properties,
})
}
Value::Edge(edge) => {
serde_json::json!({
"type": "edge",
"id": edge.id,
"label": edge.label,
"from": edge.from_node,
"to": edge.to_node,
"properties": edge.properties,
})
}
Value::Temporal(temporal) => {
serde_json::json!(format!("{:?}", temporal))
}
}
}
fn value_to_csv_string(value: &Value) -> String {
let s = Self::value_to_string(value);
if s.contains(',') || s.contains('"') || s.contains('\n') {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s
}
}
}