use clap::Parser;
use datafusion::prelude::*;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use tableski::{
ACCEPT_STREAMABLE, AppState, HeaderMode, IngestOptions, TableEntry, app_router, register_path,
register_workbook,
};
#[derive(Parser, Debug)]
#[command(name = "tableski")]
#[command(
about = "Every spreadsheet is a table — SQL, schema, and column stats over CSV/Excel via MCP"
)]
struct Args {
#[arg(long, default_value = "0.0.0.0:8080")]
bind: String,
#[arg(long)]
csv: Option<PathBuf>,
#[arg(long)]
xlsx: Option<PathBuf>,
#[arg(long, default_value = "data")]
table: String,
#[arg(long, value_enum, default_value_t = HeaderMode::Auto)]
headers: HeaderMode,
#[arg(long, default_value_t = 1_000_000)]
max_rows: usize,
#[arg(long = "file")]
files: Vec<PathBuf>,
#[arg(long)]
export_dir: Option<PathBuf>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
if args.csv.is_none() && args.xlsx.is_none() && args.files.is_empty() {
return Err("nothing to serve: pass --file <data-file> (or --csv/--xlsx)".into());
}
let ctx = SessionContext::new();
let mut tables = Vec::new();
if let Some(csv) = &args.csv {
if !csv.exists() {
return Err(format!("CSV not found: {}", csv.display()).into());
}
let path = csv.to_str().ok_or("CSV path must be valid UTF-8")?;
ctx.register_csv(&args.table, path, CsvReadOptions::new())
.await?;
tables.push(TableEntry::csv(&args.table, path));
}
if let Some(xlsx) = &args.xlsx {
if !xlsx.exists() {
return Err(format!("workbook not found: {}", xlsx.display()).into());
}
let opts = IngestOptions {
headers: args.headers,
max_rows: args.max_rows,
};
let infos = register_workbook(&ctx, xlsx, &opts)?;
for info in &infos {
tables.push(TableEntry::sheet(info, xlsx.display().to_string()));
}
}
let opts = IngestOptions {
headers: args.headers,
max_rows: args.max_rows,
};
for file in &args.files {
tables.extend(register_path(&ctx, file, &opts).await?);
}
for t in &tables {
match &t.sheet {
Some(sheet) => eprintln!(
"tableski: table `{}` <- sheet `{}` of `{}` ({} rows)",
t.name,
sheet,
t.source,
t.rows.unwrap_or(0)
),
None => eprintln!("tableski: table `{}` <- `{}`", t.name, t.source),
}
}
let mut state = AppState::new(Arc::new(ctx), tables);
if let Some(dir) = &args.export_dir {
std::fs::create_dir_all(dir)?;
eprintln!("tableski: export_result enabled -> {}", dir.display());
state = state.with_export_dir(dir);
}
let app = app_router(state);
let addr: SocketAddr = args.bind.parse()?;
eprintln!("tableski: stateless Streamable HTTP on http://{addr}");
eprintln!("Accept header for clients: `{ACCEPT_STREAMABLE}`");
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}