use crate::{Tuple, Schema, Value, DataType};
use prettytable::{Table, Row, Cell, format};
use colored::Colorize;
pub fn format_results(tuples: &[Tuple], schema: &Schema) -> String {
if tuples.is_empty() {
return format!("{}", "(0 rows)".dimmed());
}
let mut table = Table::new();
table.set_format(*format::consts::FORMAT_BOX_CHARS);
let header: Vec<Cell> = schema.columns.iter()
.map(|col| Cell::new(&col.name).style_spec("Fb"))
.collect();
table.add_row(Row::new(header));
for tuple in tuples {
let cells: Vec<Cell> = tuple.values.iter()
.map(|val| Cell::new(&format_value(val)))
.collect();
table.add_row(Row::new(cells));
}
let mut output = String::new();
output.push_str(&table.to_string());
output.push('\n');
output.push_str(&format!("({} row{})", tuples.len(), if tuples.len() == 1 { "" } else { "s" }).dimmed().to_string());
output
}
fn format_value(value: &Value) -> String {
match value {
Value::Null => "NULL".dimmed().to_string(),
Value::Boolean(b) => if *b { "true".green().to_string() } else { "false".red().to_string() },
Value::Int4(i) => i.to_string(),
Value::Int8(i) => i.to_string(),
Value::Float4(f) => format_float(*f as f64),
Value::Float8(f) => format_float(*f),
Value::Numeric(n) => n.clone(),
Value::String(s) => s.clone(),
Value::Bytes(b) => format!("\\x{}", hex::encode(b)),
Value::Timestamp(d) => d.to_string(),
Value::Date(d) => d.format("%Y-%m-%d").to_string(),
Value::Time(t) => t.format("%H:%M:%S").to_string(),
Value::Json(j) => j.to_string(),
Value::Int2(i) => i.to_string(),
Value::Uuid(u) => u.to_string(),
Value::Array(arr) => format!("{:?}", arr),
Value::Vector(vec) => format!("{:?}", vec),
Value::DictRef { dict_id } => format!("<dict:{}>", dict_id).dimmed().to_string(),
Value::CasRef { hash } => format!("<cas:{}>", hex::encode(&hash[..8])).dimmed().to_string(),
Value::ColumnarRef => "<columnar>".dimmed().to_string(),
Value::Interval(microseconds) => {
let total_secs = *microseconds / 1_000_000;
let hours = total_secs / 3600;
let mins = (total_secs % 3600) / 60;
let secs = total_secs % 60;
let micros = *microseconds % 1_000_000;
if micros != 0 {
format!("{:02}:{:02}:{:02}.{:06}", hours, mins, secs, micros)
} else {
format!("{:02}:{:02}:{:02}", hours, mins, secs)
}
}
}
}
fn format_float(f: f64) -> String {
if f.abs() < 1e-10 && f != 0.0 {
format!("{:.2e}", f)
} else if f.fract() == 0.0 && f.abs() < 1e10 {
format!("{:.0}", f)
} else {
format!("{:.6}", f).trim_end_matches('0').trim_end_matches('.').to_string()
}
}
pub fn format_timing(duration_secs: f64) -> String {
if duration_secs < 0.001 {
format!("({:.3} ms)", duration_secs * 1000.0).dimmed().to_string()
} else if duration_secs < 1.0 {
format!("({:.2} ms)", duration_secs * 1000.0).dimmed().to_string()
} else {
format!("({:.3} s)", duration_secs).dimmed().to_string()
}
}
pub fn format_error(error: &str) -> String {
format!("{}: {}", "ERROR".red().bold(), error)
}
pub fn format_schema(schema: &Schema) -> String {
let mut output = String::new();
for (i, column) in schema.columns.iter().enumerate() {
if i > 0 {
output.push_str(", ");
}
output.push_str(&format!(
"{} {}{}",
column.name.green(),
format_datatype(&column.data_type).yellow(),
if column.nullable { "" } else { " NOT NULL" }
));
}
output
}
fn format_datatype(dt: &DataType) -> String {
match dt {
DataType::Int2 => "SMALLINT".to_string(),
DataType::Int4 => "INT".to_string(),
DataType::Int8 => "BIGINT".to_string(),
DataType::Float4 => "REAL".to_string(),
DataType::Float8 => "DOUBLE".to_string(),
DataType::Text => "TEXT".to_string(),
DataType::Varchar(n) => {
if let Some(len) = n {
format!("VARCHAR({})", len)
} else {
"VARCHAR".to_string()
}
},
DataType::Boolean => "BOOLEAN".to_string(),
DataType::Date => "DATE".to_string(),
DataType::Time => "TIME".to_string(),
DataType::Timestamp => "TIMESTAMP".to_string(),
DataType::Timestamptz => "TIMESTAMPTZ".to_string(),
DataType::Interval => "INTERVAL".to_string(),
DataType::Bytea => "BYTEA".to_string(),
DataType::Uuid => "UUID".to_string(),
DataType::Json => "JSON".to_string(),
DataType::Jsonb => "JSONB".to_string(),
DataType::Numeric => "NUMERIC".to_string(),
DataType::Char(n) => format!("CHAR({})", n),
DataType::Array(inner) => format!("{}[]", format_datatype(inner)),
DataType::Vector(dim) => format!("VECTOR({})", dim),
}
}
mod hex {
pub fn encode(bytes: &[u8]) -> String {
use std::fmt::Write;
bytes.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut acc, b| {
let _ = write!(acc, "{:02x}", b);
acc
})
}
}