use std::io::Write as _;
use serde_json::json;
use crate::cli::MetadataFormat;
pub(crate) struct InputTableMeta {
pub(crate) table_name: String,
pub(crate) source_path: String,
pub(crate) source_selector: Option<String>,
}
pub(crate) struct QueryMetadata {
pub(crate) row_count: usize,
pub(crate) columns: Vec<String>,
pub(crate) input_tables: Vec<InputTableMeta>,
pub(crate) execution_time_ms: u128,
}
pub(crate) fn write_query_metadata(meta: &QueryMetadata, format: Option<&MetadataFormat>) {
let rendered = match format {
Some(MetadataFormat::Json) => render_meta_json(meta),
_ => render_meta_text(meta),
};
let stderr = std::io::stderr();
let mut handle = stderr.lock();
let _ = writeln!(handle, "{rendered}");
}
fn render_meta_text(meta: &QueryMetadata) -> String {
let mut out = String::new();
out.push_str(&format!("rows: {}\n", meta.row_count));
out.push_str(&format!(
"columns: {}\n",
if meta.columns.is_empty() {
"-".to_owned()
} else {
meta.columns.join(", ")
}
));
let w_table = meta
.input_tables
.iter()
.map(|t| t.table_name.len())
.max()
.unwrap_or(0)
.max("table".len());
let w_source = meta
.input_tables
.iter()
.map(|t| t.source_path.len())
.max()
.unwrap_or(0)
.max("source".len());
for (i, t) in meta.input_tables.iter().enumerate() {
let label = if i == 0 { "tables: " } else { " " };
out.push_str(&format!(
"{}{:<w_table$} {:<w_source$} {}\n",
label,
t.table_name,
t.source_path,
t.source_selector.as_deref().unwrap_or("-"),
));
}
if meta.input_tables.is_empty() {
out.push_str("tables: -\n");
}
out.push_str(&format!("time: {}ms", meta.execution_time_ms));
out
}
fn render_meta_json(meta: &QueryMetadata) -> String {
let value = json!({
"row_count": meta.row_count,
"columns": meta.columns,
"input_tables": meta.input_tables.iter().map(|t| json!({
"table_name": t.table_name,
"source_path": t.source_path,
"source_selector": t.source_selector,
})).collect::<Vec<_>>(),
"execution_time_ms": meta.execution_time_ms,
});
serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_owned())
}
#[cfg(test)]
mod tests {
fn make_meta() -> super::QueryMetadata {
super::QueryMetadata {
row_count: 3,
columns: vec!["id".to_owned(), "name".to_owned()],
input_tables: vec![
super::InputTableMeta {
table_name: "table".to_owned(),
source_path: "./data.csv".to_owned(),
source_selector: None,
},
super::InputTableMeta {
table_name: "sales".to_owned(),
source_path: "./sales.xlsx".to_owned(),
source_selector: Some("Q1".to_owned()),
},
],
execution_time_ms: 42,
}
}
#[test]
fn meta_text_contains_row_count_and_columns() {
let meta = make_meta();
let rendered = super::render_meta_text(&meta);
assert!(rendered.contains("rows: 3"), "missing row count: {rendered}");
assert!(rendered.contains("id, name"), "missing columns: {rendered}");
assert!(rendered.contains("42ms"), "missing timing: {rendered}");
}
#[test]
fn meta_text_contains_input_tables() {
let meta = make_meta();
let rendered = super::render_meta_text(&meta);
assert!(rendered.contains("table"), "missing table name: {rendered}");
assert!(rendered.contains("./data.csv"), "missing source path: {rendered}");
assert!(rendered.contains("sales"), "missing table name: {rendered}");
assert!(rendered.contains("./sales.xlsx"), "missing source path: {rendered}");
assert!(rendered.contains("Q1"), "missing selector: {rendered}");
}
#[test]
fn meta_text_shows_dash_for_missing_selector() {
let meta = make_meta();
let rendered = super::render_meta_text(&meta);
assert!(
rendered.contains(" -\n") || rendered.contains(" -"),
"missing dash for no selector: {rendered}"
);
}
#[test]
fn meta_json_is_valid_structure() {
let meta = make_meta();
let rendered = super::render_meta_json(&meta);
let parsed: serde_json::Value =
serde_json::from_str(&rendered).expect("metadata JSON should be valid");
assert_eq!(parsed["row_count"], 3);
assert_eq!(parsed["execution_time_ms"], 42);
assert_eq!(parsed["columns"][0], "id");
assert_eq!(parsed["columns"][1], "name");
assert_eq!(parsed["input_tables"][0]["table_name"], "table");
assert_eq!(parsed["input_tables"][0]["source_selector"], serde_json::Value::Null);
assert_eq!(parsed["input_tables"][1]["table_name"], "sales");
assert_eq!(parsed["input_tables"][1]["source_selector"], "Q1");
}
}