use fraiseql_error::{FraiseQLError, Result};
use futures::TryStreamExt as _;
use tokio_postgres::types::ToSql;
use super::PostgresAdapter;
use crate::{
postgres::pg_detail,
traits::{AdminSqlOutcome, AdminSqlRequest},
};
pub(super) async fn execute_admin_sql_impl(
adapter: &PostgresAdapter,
request: &AdminSqlRequest,
) -> Result<AdminSqlOutcome> {
let mut client = adapter.acquire_connection_with_retry().await?;
let txn =
client
.build_transaction()
.read_only(request.read_only)
.start()
.await
.map_err(|e| FraiseQLError::Database {
message: format!("Failed to start admin SQL transaction: {}", pg_detail(&e)),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
let timeout = format!("{}ms", request.statement_timeout_ms);
txn.execute("SELECT set_config('statement_timeout', $1, true)", &[&timeout])
.await
.map_err(|e| FraiseQLError::Database {
message: format!("Failed to set admin SQL statement timeout: {}", pg_detail(&e)),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
let pairs: Vec<(&str, &str)> =
request.session_vars.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
super::database::apply_session_vars(&txn, &pairs).await?;
let outcome = read_statement(&txn, request).await;
let committed = if request.commit && outcome.is_ok() {
txn.commit().await.map_err(|e| FraiseQLError::Database {
message: format!("Failed to commit admin SQL transaction: {}", pg_detail(&e)),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
true
} else {
if let Err(e) = txn.rollback().await {
tracing::debug!(error = %e, "admin SQL transaction rollback failed");
}
false
};
let (columns, rows, truncated, rows_affected) = outcome?;
Ok(AdminSqlOutcome {
columns,
rows,
truncated,
rows_affected,
committed,
})
}
type StatementRead = (Vec<String>, Vec<Vec<serde_json::Value>>, bool, Option<u64>);
async fn read_statement(
txn: &tokio_postgres::Transaction<'_>,
request: &AdminSqlRequest,
) -> Result<StatementRead> {
let no_params: [&(dyn ToSql + Sync); 0] = [];
let stream = txn.query_raw(request.sql.as_str(), no_params).await.map_err(|e| {
FraiseQLError::Database {
message: format!("Admin SQL statement failed: {}", pg_detail(&e)),
sql_state: e.code().map(|c| c.code().to_string()),
}
})?;
let mut stream = std::pin::pin!(stream);
let mut columns: Vec<String> = Vec::new();
let mut rows: Vec<Vec<serde_json::Value>> = Vec::new();
let mut truncated = false;
while let Some(row) = stream.try_next().await.map_err(|e| FraiseQLError::Database {
message: format!("Admin SQL statement failed mid-read: {}", pg_detail(&e)),
sql_state: e.code().map(|c| c.code().to_string()),
})? {
if columns.is_empty() {
columns = row.columns().iter().map(|c| c.name().to_string()).collect();
}
if rows.len() >= request.max_rows {
truncated = true;
break;
}
rows.push(
(0..row.columns().len())
.map(|i| super::database::decode_cell(&row, i))
.collect(),
);
}
let rows_affected = if truncated {
None
} else {
stream.rows_affected()
};
Ok((columns, rows, truncated, rows_affected))
}