use std::io::Read;
use anyhow::{bail, Result};
use owo_colors::OwoColorize;
use crate::api::client::ApiClient;
use crate::api::models::{ExecutionOutcome, QueryOutcome};
use crate::exit::Refused;
use crate::output::{self, Format};
use crate::render::table::{truncate, Cell, Table};
use crate::render::theme;
const CELL_WIDTH: usize = 48;
pub async fn tables(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
format: Format,
) -> Result<()> {
let (_, service) = client.locate(project_reference, service_reference).await?;
let overview = client.data_overview(&service.id).await?;
if format.answered_in_json(&overview) {
return Ok(());
}
if !overview.status.reachable {
return Err(unreachable(&service.name, overview.status.reason.as_deref()).into());
}
if overview.relations.is_empty() {
println!(
"`{}` está vacía: ninguna migración se ha ejecutado todavía contra ella.",
service.name
);
return Ok(());
}
let mut table = Table::new(&["tabla", "tipo", "columnas", "filas"]);
for relation in &overview.relations {
table.push(vec![
Cell::styled(relation.name.clone(), theme::strong()),
Cell::styled(
if relation.kind == "view" {
"vista"
} else {
"tabla"
},
theme::muted(),
),
Cell::plain(relation.column_count.to_string()),
Cell::plain(relation.estimated_rows.map_or_else(
|| "—".to_owned(),
|rows| format!("~{}", rows.round() as u64),
)),
]);
}
table.print();
Ok(())
}
pub async fn query(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
sql: &str,
limit: Option<u64>,
format: Format,
) -> Result<()> {
let (_, service) = client.locate(project_reference, service_reference).await?;
let outcome = client
.query_data(&service.id, &statement_from(sql)?, limit)
.await?;
if format.is_json() {
output::emit(&outcome);
return refused_or_ok(outcome.failure.as_deref());
}
if let Some(failure) = &outcome.failure {
return Err(refused(failure).into());
}
print_rows(&outcome.columns, &outcome.rows);
print_query_footer(&outcome);
Ok(())
}
pub async fn run(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
sql: &str,
preview: bool,
confirmation: Option<&str>,
format: Format,
) -> Result<()> {
let (_, service) = client.locate(project_reference, service_reference).await?;
if !preview
&& confirmation != Some(service.name.as_str())
&& confirmation != Some(service.slug.as_str())
{
bail!(
"esto cambia los datos de `{}` y no se puede deshacer: pruébalo con --preview, y para ejecutarlo repite el nombre con --confirm {}",
service.name,
service.slug,
);
}
let outcome = client
.execute_sql(&service.id, &statement_from(sql)?, preview)
.await?;
if format.is_json() {
output::emit(&outcome);
return refused_or_ok(outcome.failure.as_deref());
}
if let Some(failure) = &outcome.failure {
return Err(refused(failure).into());
}
print_execution(&outcome);
Ok(())
}
fn statement_from(argument: &str) -> Result<String> {
if argument != "-" {
return Ok(argument.to_owned());
}
let mut read = String::new();
std::io::stdin().read_to_string(&mut read)?;
Ok(read)
}
fn print_rows(columns: &[String], rows: &[Vec<Option<String>>]) {
if rows.is_empty() {
return;
}
let headers: Vec<&str> = columns.iter().map(String::as_str).collect();
let mut table = Table::new(&headers);
for row in rows {
table.push(
row.iter()
.map(|cell| match cell {
None => Cell::styled("NULL", theme::muted()),
Some(text) => Cell::plain(truncate(text, CELL_WIDTH)),
})
.collect(),
);
}
table.print();
}
fn print_query_footer(outcome: &QueryOutcome) {
let count = outcome.rows.len();
let mut line = format!("{count} {}", if count == 1 { "fila" } else { "filas" });
if outcome.has_more {
line.push_str(&format!(
" · hay más, se muestran las primeras {}",
outcome.limit
));
}
println!(" {}", line.style(theme::muted()));
print_truncations(outcome.cells_truncated, outcome.rows_dropped);
}
fn print_execution(outcome: &ExecutionOutcome) {
let command = outcome.command.as_deref().unwrap_or("OK");
let touched = match outcome.affected_rows {
Some(1) => "1 fila".to_owned(),
Some(rows) => format!("{rows} filas"),
None => String::new(),
};
if outcome.committed {
println!(
" {} {} {}",
theme::CHECK.style(theme::success()),
command.style(theme::strong()),
touched.style(theme::muted()),
);
} else {
println!(
" {} {} {} {}",
"ensayo".style(theme::warning()),
command.style(theme::strong()),
touched.style(theme::muted()),
"· deshecho, nada ha cambiado".style(theme::muted()),
);
}
print_rows(&outcome.columns, &outcome.rows);
print_truncations(outcome.cells_truncated, outcome.rows_dropped);
}
fn print_truncations(cells: bool, rows: bool) {
if cells {
println!(
" {}",
"los valores muy largos se han cortado (acaban en …)".style(theme::muted())
);
}
if rows {
println!(
" {}",
"la respuesta llegó a su tamaño máximo y quedaron filas fuera".style(theme::muted())
);
}
}
fn refused(failure: &str) -> Refused {
Refused::invalid(format!("la base de datos lo rechazó: {failure}"))
}
fn refused_or_ok(failure: Option<&str>) -> Result<()> {
match failure {
Some(failure) => Err(refused(failure).into()),
None => Ok(()),
}
}
fn unreachable(service: &str, reason: Option<&str>) -> Refused {
let words = match reason {
Some("not-a-database") => "no es una base de datos que la plataforma sepa leer: solo los componentes PostgreSQL del catálogo",
Some("never-deployed") => "todavía no se ha desplegado",
Some("asleep") => "está durmiendo, y este comando no la despierta a propósito: arráncala con `fundaia deploy`",
Some("stopped") => "no tiene el contenedor en marcha",
Some("unconfigured") => "no tiene POSTGRES_USER, POSTGRES_PASSWORD y POSTGRES_DB en sus variables",
_ => "no se puede leer ahora mismo",
};
Refused::invalid(format!("`{service}` {words}"))
}