use colored::*;
use serde_json::{json, Value as JsonValue};
use base64::prelude::*;
use crate::utils::types::{QueryResult, Value};
use crate::cli::cli_args::OutputFormat;
pub struct OutputFormatter;
impl OutputFormatter {
pub fn format_result(result: &QueryResult, format: &OutputFormat) -> String {
match format {
OutputFormat::Table => Self::format_table(result),
OutputFormat::Json => Self::format_json(result),
OutputFormat::Csv => Self::format_csv(result),
}
}
fn format_table(result: &QueryResult) -> String {
if result.is_empty() {
return "No results found.".dimmed().to_string();
}
let mut output = String::new();
let mut col_widths = vec![0; result.columns.len()];
for (i, col) in result.columns.iter().enumerate() {
col_widths[i] = col.name.len();
}
for row in &result.rows {
for (i, value) in row.values.iter().enumerate() {
if i < col_widths.len() {
let value_str = Self::value_to_string(value);
col_widths[i] = col_widths[i].max(value_str.len());
}
}
}
for width in &mut col_widths {
*width = (*width).max(8);
}
output.push_str(&Self::format_table_separator(&col_widths, true));
output.push('|');
for (i, col) in result.columns.iter().enumerate() {
output.push_str(&format!(" {:<width$} |",
col.name.bold().cyan(),
width = col_widths[i]
));
}
output.push('\n');
output.push_str(&Self::format_table_separator(&col_widths, false));
for row in &result.rows {
output.push('|');
for (i, value) in row.values.iter().enumerate() {
if i < col_widths.len() {
let formatted_value = Self::format_value_colored(value);
output.push_str(&format!(" {:<width$} |",
formatted_value,
width = col_widths[i]
));
}
}
output.push('\n');
}
output.push_str(&Self::format_table_separator(&col_widths, true));
output.push_str(&format!("\n{} {} in {:.2}ms\n",
result.row_count().to_string().green().bold(),
if result.row_count() == 1 { "row" } else { "rows" },
result.execution_time.as_millis()
));
output
}
fn format_table_separator(col_widths: &[usize], is_border: bool) -> String {
let mut separator = String::new();
if is_border {
separator.push('+');
for &width in col_widths {
separator.push_str(&"-".repeat(width + 2));
separator.push('+');
}
} else {
separator.push('|');
for &width in col_widths {
separator.push_str(&"-".repeat(width + 2));
separator.push('|');
}
}
separator.push('\n');
separator
}
fn format_json(result: &QueryResult) -> String {
let mut rows = Vec::new();
for row in &result.rows {
let mut row_obj = serde_json::Map::new();
for (i, value) in row.values.iter().enumerate() {
if let Some(col) = result.columns.get(i) {
let json_value = Self::value_to_json(value);
row_obj.insert(col.name.clone(), json_value);
}
}
rows.push(JsonValue::Object(row_obj));
}
let output = json!({
"data": rows,
"metadata": {
"columns": result.columns.iter().map(|col| {
json!({
"name": col.name,
"type": format!("{:?}", col.data_type),
"nullable": col.nullable
})
}).collect::<Vec<_>>(),
"row_count": result.row_count(),
"execution_time_ms": result.execution_time.as_millis()
}
});
serde_json::to_string_pretty(&output).unwrap_or_else(|_| "{}".to_string())
}
fn format_csv(result: &QueryResult) -> String {
let mut output = String::new();
let headers: Vec<String> = result.columns.iter()
.map(|col| Self::escape_csv_field(&col.name))
.collect();
output.push_str(&headers.join(","));
output.push('\n');
for row in &result.rows {
let values: Vec<String> = row.values.iter()
.map(|value| Self::escape_csv_field(&Self::value_to_string(value)))
.collect();
output.push_str(&values.join(","));
output.push('\n');
}
output
}
fn value_to_string(value: &Value) -> String {
match value {
Value::Text(s) => s.clone(),
Value::Integer(i) => i.to_string(),
Value::Float(f) => format!("{:.2}", f),
Value::Boolean(b) => b.to_string(),
Value::Date(d) => d.clone(),
Value::DateTime(dt) => dt.clone(),
Value::Json(j) => j.clone(),
Value::Binary(b) => format!("<binary: {} bytes>", b.len()),
Value::Null => "NULL".to_string(),
}
}
fn format_value_colored(value: &Value) -> ColoredString {
match value {
Value::Text(s) => s.normal(),
Value::Integer(i) => i.to_string().blue(),
Value::Float(f) => format!("{:.2}", f).blue(),
Value::Boolean(true) => "true".green(),
Value::Boolean(false) => "false".red(),
Value::Date(d) => d.yellow(),
Value::DateTime(dt) => dt.yellow(),
Value::Json(j) => j.magenta(),
Value::Binary(b) => format!("<binary: {} bytes>", b.len()).cyan(),
Value::Null => "NULL".dimmed(),
}
}
fn value_to_json(value: &Value) -> JsonValue {
match value {
Value::Text(s) => JsonValue::String(s.clone()),
Value::Integer(i) => JsonValue::Number((*i).into()),
Value::Float(f) => {
if let Some(num) = serde_json::Number::from_f64(*f) {
JsonValue::Number(num)
} else {
JsonValue::Null
}
},
Value::Boolean(b) => JsonValue::Bool(*b),
Value::Date(d) => JsonValue::String(d.clone()),
Value::DateTime(dt) => JsonValue::String(dt.clone()),
Value::Json(j) => {
serde_json::from_str(j).unwrap_or(JsonValue::String(j.clone()))
},
Value::Binary(b) => JsonValue::String(BASE64_STANDARD.encode(b)),
Value::Null => JsonValue::Null,
}
}
fn escape_csv_field(field: &str) -> String {
if field.contains(',') || field.contains('"') || field.contains('\n') {
format!("\"{}\"", field.replace('"', "\"\""))
} else {
field.to_string()
}
}
pub fn format_error(error: &crate::utils::error::NirvError) -> String {
format!("{} {}", "Error:".red().bold(), error.to_string().red())
}
pub fn format_success(message: &str) -> String {
format!("{} {}", "Success:".green().bold(), message)
}
pub fn format_info(message: &str) -> String {
format!("{} {}", "Info:".blue().bold(), message)
}
}