mod render;
use std::io::Read;
use std::path::PathBuf;
use clap::{ArgGroup, Args as ClapArgs, Subcommand};
use comfy_table::Cell;
use quicknode_sdk::{ChainSchema, QueryParams, QueryResponse};
use serde::Serialize;
use serde_json::Value;
use crate::context::Ctx;
use crate::errors::CliError;
use crate::output::{new_table, set_header_bold, write_table, Format, Render};
use crate::retry::retrying;
use render::json_cell;
#[derive(Debug, ClapArgs)]
pub struct Args {
#[command(subcommand)]
pub cmd: SqlCmd,
}
#[derive(Debug, Subcommand)]
pub enum SqlCmd {
#[command(after_help = "Examples:\n \
qn sql query \"SELECT 1\" --cluster-id hyperliquid-core-mainnet\n \
qn sql query --file query.sql --cluster-id hyperliquid-core-mainnet\n \
cat query.sql | qn sql query --file - --cluster-id hyperliquid-core-mainnet")]
Query(QueryArgs),
Schema(SchemaArgs),
}
#[derive(Debug, ClapArgs)]
#[command(group(ArgGroup::new("source").args(["query", "file"]).required(true)))]
pub struct QueryArgs {
#[arg(value_name = "SQL")]
pub query: Option<String>,
#[arg(long, short = 'f', value_name = "PATH")]
pub file: Option<PathBuf>,
#[arg(long, value_name = "CLUSTER_ID")]
pub cluster_id: String,
}
#[derive(Debug, ClapArgs)]
pub struct SchemaArgs {
#[arg(value_name = "CLUSTER_ID")]
pub cluster_id: String,
}
pub async fn run(args: Args, ctx: Ctx) -> Result<(), CliError> {
match args.cmd {
SqlCmd::Query(a) => query(a, ctx).await,
SqlCmd::Schema(a) => schema(a, ctx).await,
}
}
async fn query(a: QueryArgs, ctx: Ctx) -> Result<(), CliError> {
let sql = resolve_query(a.query, a.file)?;
let params = QueryParams {
query: sql,
cluster_id: a.cluster_id,
};
let resp = ctx.sdk.sql.query(¶ms).await?;
if matches!(ctx.out.format, Format::Table | Format::Md) {
ctx.out.note(&stats_line(&resp));
}
crate::output::emit(&ctx.out, &QueryView(resp))
}
async fn schema(a: SchemaArgs, ctx: Ctx) -> Result<(), CliError> {
let resp = retrying(ctx.global.retries, || ctx.sdk.sql.get_schema(&a.cluster_id)).await?;
crate::output::emit(&ctx.out, &SchemaView(resp))
}
fn resolve_query(query: Option<String>, file: Option<PathBuf>) -> Result<String, CliError> {
if let Some(q) = query {
return Ok(q);
}
let path = file.expect("clap ArgGroup guarantees one of query/file");
if path.as_os_str() == "-" {
let mut buf = String::new();
std::io::stdin()
.read_to_string(&mut buf)
.map_err(|e| CliError::Arg(format!("could not read query from stdin: {e}")))?;
return Ok(buf);
}
std::fs::read_to_string(&path).map_err(|e| {
CliError::Arg(format!(
"could not read query file '{}': {e}",
path.display()
))
})
}
fn stats_line(resp: &QueryResponse) -> String {
let mut line = format!(
"✓ {} rows · {} credits · {:.3}s",
resp.rows, resp.credits, resp.statistics.elapsed
);
if resp.rows_before_limit_at_least > resp.rows {
line.push_str(&format!(
" · {} matched (use LIMIT/OFFSET to page)",
resp.rows_before_limit_at_least
));
}
line
}
#[derive(Serialize)]
struct QueryView(QueryResponse);
impl Render for QueryView {
fn render_table(
&self,
w: &mut dyn std::io::Write,
ctx: &crate::output::OutputCtx,
) -> std::io::Result<()> {
let mut t = new_table(ctx);
set_header_bold(
&mut t,
ctx,
self.0.meta.iter().map(|c| c.name.to_uppercase()),
);
for row in &self.0.data {
let cells = self.0.meta.iter().map(|col| {
let v = row.get(&col.name).unwrap_or(&Value::Null);
Cell::new(json_cell(v))
});
t.add_row(cells);
}
write_table(w, &t)
}
}
#[derive(Serialize)]
struct SchemaView(ChainSchema);
impl Render for SchemaView {
fn render_table(
&self,
w: &mut dyn std::io::Write,
ctx: &crate::output::OutputCtx,
) -> std::io::Result<()> {
let s = &self.0;
let n = s.tables.len();
writeln!(
w,
"{} · {} · {} table{}",
s.chain,
s.cluster_id,
n,
if n == 1 { "" } else { "s" }
)?;
for table in &s.tables {
writeln!(w)?;
writeln!(
w,
"{} ({}, {} rows)",
table.name, table.engine, table.total_rows
)?;
let partition = if table.partition_key.is_empty() {
"—".to_string()
} else {
table.partition_key.clone()
};
let sorting = if table.sorting_key.is_empty() {
"—".to_string()
} else {
table.sorting_key.join(", ")
};
writeln!(w, " partition: {partition}")?;
writeln!(w, " sorting: {sorting}")?;
let mut t = new_table(ctx);
set_header_bold(&mut t, ctx, vec!["COLUMN", "TYPE"]);
for col in &table.columns {
t.add_row(vec![Cell::new(&col.name), Cell::new(&col.column_type)]);
}
write_table(w, &t)?;
}
Ok(())
}
}