use cqlite_core::storage::sstable::bulletproof_reader::SSTableEntry;
pub const COLUMN_SEPARATOR: &str = " | ";
pub const HEADER_BORDER_CHAR: char = '-';
pub const HEADER_SEPARATOR_JUNCTION: &str = "-+-";
pub const ROW_PREFIX: &str = " ";
pub struct CqlshTableFormatter {
pub column_headers: Vec<String>,
pub rows: Vec<Vec<String>>,
pub show_row_count: bool,
pub color_support: bool,
}
impl Default for CqlshTableFormatter {
fn default() -> Self {
Self {
column_headers: Vec::new(),
rows: Vec::new(),
show_row_count: true,
color_support: false,
}
}
}
impl CqlshTableFormatter {
pub fn new() -> Self {
Self::default()
}
pub fn set_headers(&mut self, headers: Vec<String>) {
self.column_headers = headers;
}
pub fn add_row(&mut self, row: Vec<String>) {
self.rows.push(row);
}
#[allow(dead_code)]
pub fn add_rows(&mut self, rows: Vec<Vec<String>>) {
self.rows.extend(rows);
}
#[allow(dead_code)]
pub fn from_sstable_entries(&mut self, entries: &[SSTableEntry], table_name: &str) {
self.column_headers = vec!["id".to_string(), "data".to_string()];
for entry in entries {
let mut row = Vec::new();
row.push(hex::encode(entry.key.as_bytes()));
row.push(entry.format_info.clone());
while row.len() < self.column_headers.len() {
row.push(String::new());
}
self.rows.push(row);
}
println!(
"📊 Formatted {} entries from {} into table format",
entries.len(),
table_name
);
}
fn calculate_column_widths(&self) -> Vec<usize> {
let column_count = self
.column_headers
.len()
.max(self.rows.first().map(|r| r.len()).unwrap_or(0));
let mut widths = vec![0; column_count];
for (i, header) in self.column_headers.iter().enumerate() {
if i < widths.len() {
widths[i] = header.chars().count();
}
}
for row in &self.rows {
for (i, cell) in row.iter().enumerate() {
if i < widths.len() {
widths[i] = widths[i].max(cell.chars().count());
}
}
}
widths
}
pub fn format(&self) -> String {
if self.rows.is_empty() && self.column_headers.is_empty() {
return String::new();
}
let widths = self.calculate_column_widths();
let mut result = String::new();
if !self.column_headers.is_empty() {
result.push_str(ROW_PREFIX);
for (i, header) in self.column_headers.iter().enumerate() {
if i > 0 {
result.push_str(COLUMN_SEPARATOR);
}
let width = widths.get(i).copied().unwrap_or(header.len());
result.push_str(&format!("{:<width$}", header, width = width));
}
result.push('\n');
result.push_str(&self.format_separator_line(&widths));
result.push('\n');
}
for row in &self.rows {
result.push_str(ROW_PREFIX);
for (i, cell) in row.iter().enumerate() {
if i > 0 {
result.push_str(COLUMN_SEPARATOR);
}
let width = widths.get(i).copied().unwrap_or(cell.len());
result.push_str(&format!("{:>width$}", cell, width = width));
}
result.push('\n');
}
if self.show_row_count && !self.rows.is_empty() {
result.push('\n');
result.push_str(&format!("({} rows)", self.rows.len()));
}
result
}
fn format_separator_line(&self, widths: &[usize]) -> String {
let mut separator = String::new();
separator.push(HEADER_BORDER_CHAR);
for (i, &width) in widths.iter().enumerate() {
if i > 0 {
separator.push_str(HEADER_SEPARATOR_JUNCTION);
}
separator.push_str(&HEADER_BORDER_CHAR.to_string().repeat(width));
}
separator.push(HEADER_BORDER_CHAR);
separator
}
#[allow(dead_code)]
pub fn clear(&mut self) {
self.column_headers.clear();
self.rows.clear();
}
#[allow(dead_code)]
pub fn row_count(&self) -> usize {
self.rows.len()
}
#[allow(dead_code)]
pub fn column_count(&self) -> usize {
self.column_headers.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.rows.is_empty() && self.column_headers.is_empty()
}
#[allow(dead_code)]
pub fn format_as_json(&self) -> serde_json::Value {
let mut result = serde_json::Map::new();
result.insert(
"format".to_string(),
serde_json::Value::String("table".to_string()),
);
result.insert(
"headers".to_string(),
serde_json::json!(self.column_headers),
);
result.insert("rows".to_string(), serde_json::json!(self.rows));
result.insert(
"row_count".to_string(),
serde_json::Value::Number(self.rows.len().into()),
);
serde_json::Value::Object(result)
}
#[allow(dead_code)]
pub fn from_json(value: &serde_json::Value) -> Result<Self, String> {
let mut formatter = Self::new();
if let Some(headers) = value.get("headers").and_then(|h| h.as_array()) {
formatter.column_headers = headers
.iter()
.filter_map(|h| h.as_str())
.map(|s| s.to_string())
.collect();
}
if let Some(rows) = value.get("rows").and_then(|r| r.as_array()) {
for row in rows {
if let Some(row_array) = row.as_array() {
let row_data: Vec<String> = row_array
.iter()
.filter_map(|cell| cell.as_str())
.map(|s| s.to_string())
.collect();
formatter.rows.push(row_data);
}
}
}
Ok(formatter)
}
#[allow(dead_code)]
pub fn format_cell_value(&self, value: &str, column_name: &str) -> String {
match column_name.to_lowercase().as_str() {
"id" | "uuid" => {
if self.is_uuid_like(value) {
value.to_lowercase()
} else {
value.to_string()
}
}
"timestamp" | "created_at" | "updated_at" => {
value.to_string()
}
_ => {
value.to_string()
}
}
}
fn is_uuid_like(&self, value: &str) -> bool {
value.len() == 36
&& value.chars().filter(|&c| c == '-').count() == 4
&& value.chars().all(|c| c.is_ascii_hexdigit() || c == '-')
}
pub fn set_color_support(&mut self, enabled: bool) {
self.color_support = enabled;
}
#[allow(dead_code)]
pub fn set_show_row_count(&mut self, show: bool) {
self.show_row_count = show;
}
}
#[allow(dead_code)]
pub fn format_sstable_entries_as_table(entries: &[SSTableEntry], table_name: &str) -> String {
let mut formatter = CqlshTableFormatter::new();
formatter.from_sstable_entries(entries, table_name);
formatter.format()
}
#[allow(dead_code)]
pub fn format_for_cqlsh_comparison(entries: &[SSTableEntry]) -> String {
let mut formatter = CqlshTableFormatter::new();
formatter.set_headers(vec!["id".to_string(), "data".to_string()]);
for entry in entries {
let mut row = vec![hex::encode(entry.key.as_bytes())];
if entry.format_info.is_empty() {
row.push(String::new());
} else {
row.push(entry.format_info.clone());
}
formatter.add_row(row);
}
formatter.format()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_table_formatting() {
let mut formatter = CqlshTableFormatter::new();
formatter.set_headers(vec!["id".to_string(), "name".to_string()]);
formatter.add_row(vec!["1".to_string(), "John".to_string()]);
formatter.add_row(vec!["2".to_string(), "Jane".to_string()]);
let output = formatter.format();
assert!(output.contains("id | name"));
assert!(output.contains("---+-----"));
assert!(output.contains("(2 rows)"));
}
#[test]
fn test_column_width_calculation() {
let mut formatter = CqlshTableFormatter::new();
formatter.set_headers(vec!["short".to_string(), "very_long_header".to_string()]);
formatter.add_row(vec!["test".to_string(), "x".to_string()]);
let widths = formatter.calculate_column_widths();
assert_eq!(widths[0], 5); assert_eq!(widths[1], 16); }
#[test]
fn test_right_aligned_data() {
let mut formatter = CqlshTableFormatter::new();
formatter.set_headers(vec!["id".to_string()]);
formatter.add_row(vec!["123".to_string()]);
let output = formatter.format();
let lines: Vec<&str> = output.lines().collect();
assert!(lines.len() >= 3);
assert!(lines[2].ends_with("123"));
}
#[test]
fn test_empty_table() {
let formatter = CqlshTableFormatter::new();
let output = formatter.format();
assert!(output.is_empty());
}
#[test]
fn test_uuid_formatting() {
let formatter = CqlshTableFormatter::new();
let uuid = "A8F167F0-EBE7-4F20-A386-31FF138BEC3B";
let formatted = formatter.format_cell_value(uuid, "id");
assert_eq!(formatted, "a8f167f0-ebe7-4f20-a386-31ff138bec3b");
}
#[test]
fn test_json_conversion() {
let mut formatter = CqlshTableFormatter::new();
formatter.set_headers(vec!["id".to_string(), "name".to_string()]);
formatter.add_row(vec!["1".to_string(), "John".to_string()]);
let json = formatter.format_as_json();
assert!(json.get("headers").is_some());
assert!(json.get("rows").is_some());
assert!(json.get("row_count").is_some());
}
}