use std::io::{self, Write};
use clap::{Parser, Subcommand};
use oxidelake_core::BackendKind;
use oxidelake_runtime::{OutputFormat, OxideSession, SessionOptions, dashboard};
use oxidelake_storage::{Compression, demo_write_options, write_demo_table};
use tracing_subscriber::EnvFilter;
fn write_out(text: &str) -> Result<(), io::Error> {
let mut stdout = io::stdout().lock();
match stdout
.write_all(text.as_bytes())
.and_then(|()| stdout.flush())
{
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(()),
other => other,
}
}
#[derive(Parser)]
#[command(
name = "oxide",
version,
about = "OxideLake: GPU-accelerated, Arrow-native query engine"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
GenData {
#[arg(long, default_value_t = 1_000_000)]
rows: u64,
#[arg(long)]
out: String,
#[arg(long, default_value_t = 42)]
seed: u64,
#[arg(long, default_value_t = 65_536)]
row_group_rows: usize,
#[arg(long, default_value = "zstd")]
compression: Compression,
},
Sql {
#[arg(short, long)]
query: String,
#[arg(short, long = "table", value_name = "NAME=PATH")]
tables: Vec<String>,
#[arg(long, value_name = "URL")]
cluster: Option<String>,
#[arg(long, value_name = "BACKEND")]
target: Option<BackendKind>,
#[arg(long, value_name = "ROWS")]
batch_size: Option<usize>,
#[arg(long, value_name = "FORMAT", default_value = "table")]
output: OutputFormat,
},
Explain {
#[arg(short, long)]
query: String,
#[arg(short, long = "table", value_name = "NAME=PATH")]
tables: Vec<String>,
#[arg(long, value_name = "BACKEND")]
target: Option<BackendKind>,
#[arg(long, value_name = "ROWS")]
batch_size: Option<usize>,
},
Tui {
#[arg(short, long)]
query: Option<String>,
#[arg(short, long = "table", value_name = "NAME=PATH")]
tables: Vec<String>,
#[arg(long, value_name = "BACKEND")]
target: Option<BackendKind>,
#[arg(long, value_name = "ROWS")]
batch_size: Option<usize>,
},
}
fn parse_tables(tables: &[String]) -> anyhow::Result<Vec<(&str, &str)>> {
tables
.iter()
.map(|spec| {
spec.split_once('=')
.ok_or_else(|| anyhow::anyhow!("--table expects NAME=PATH, got '{spec}'"))
})
.collect()
}
async fn session(
cluster: Option<&str>,
target: Option<BackendKind>,
batch_size: Option<usize>,
tables: &[String],
) -> anyhow::Result<OxideSession> {
let mut options = SessionOptions::new();
if let Some(rows) = batch_size {
options = options.with_batch_size(rows);
}
let session = match (cluster, target) {
(Some(_), Some(_)) => anyhow::bail!(
"--target picks the embedded placement target; on a cluster the \
scheduler's OXIDE_CLUSTER_BACKEND decides placement"
),
(Some(url), None) => OxideSession::connect_with_options(url, &options).await?,
(None, target) => {
if let Some(target) = target {
options = options.with_target(target);
}
OxideSession::local_with_options(&options)?
}
};
for (name, path) in parse_tables(tables)? {
session.register_parquet(name, path).await?;
}
Ok(session)
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(std::io::stderr)
.with_ansi(std::io::IsTerminal::is_terminal(&std::io::stderr()))
.init();
let cli = Cli::parse();
match cli.command {
Command::GenData {
rows,
out,
seed,
row_group_rows,
compression,
} => {
let options = demo_write_options(row_group_rows, compression);
let table = write_demo_table(std::path::Path::new(&out), rows, seed, &options)?;
write_out(&format!(
"wrote {} rows to {} (seed {seed}, {row_group_rows} rows per row group, {})\n",
table.rows,
table.path.display(),
compression.as_str(),
))?;
}
Command::Sql {
query,
tables,
cluster,
target,
batch_size,
output,
} => {
let session = session(cluster.as_deref(), target, batch_size, &tables).await?;
let (schema, batches) = session.collect(&query).await?;
write_out(&oxidelake_runtime::output::render(
&schema, &batches, output,
)?)?;
}
Command::Explain {
query,
tables,
target,
batch_size,
} => {
let session = session(None, target, batch_size, &tables).await?;
write_out(&session.explain(&query).await?)?;
}
Command::Tui {
query,
tables,
target,
batch_size,
} => {
if !oxidelake_tui::is_interactive_terminal() {
eprintln!("{}", oxidelake_tui::NON_INTERACTIVE_TERMINAL_MESSAGE);
std::process::exit(2);
}
let model = match &query {
Some(sql) => {
let session = session(None, target, batch_size, &tables).await?;
let names: Vec<String> = parse_tables(&tables)?
.into_iter()
.map(|(name, _)| name.to_owned())
.collect();
dashboard::query_dashboard(&session, sql, &names).await?
}
None => oxidelake_tui::demo_model(),
};
oxidelake_tui::run_terminal(model, None, |_| {})?;
}
}
Ok(())
}