use std::pin::Pin;
use fraiseql_error::{FraiseQLError, Result};
use futures::{Stream, StreamExt as _, TryStreamExt as _};
use tokio::sync::{Semaphore, mpsc};
use tokio_postgres::Row;
use super::PostgresAdapter;
use crate::{postgres::pg_detail, types::QueryParam};
const STREAM_BUFFER_ROWS: usize = 64;
pub(super) async fn stream_rows<T, F>(
adapter: &PostgresAdapter,
sql: String,
params: Vec<QueryParam>,
session_vars: &[(&str, &str)],
routing: crate::types::ReadRouting,
decode: F,
) -> Result<Pin<Box<dyn Stream<Item = Result<T>> + Send>>>
where
T: Send + 'static,
F: Fn(&Row, &str) -> Result<T> + Send + 'static,
{
let permit = Semaphore::acquire_owned(std::sync::Arc::clone(&adapter.streaming_permits))
.await
.map_err(|_| FraiseQLError::ConnectionPool {
message: "Streaming read slots are closed; the adapter is shutting down".to_string(),
})?;
let mut client = adapter.acquire_read_connection_with_retry(routing).await?;
let owned_vars: Vec<(String, String)> =
session_vars.iter().map(|(k, v)| ((*k).to_string(), (*v).to_string())).collect();
let (tx, mut rx) = mpsc::channel::<Result<T>>(STREAM_BUFFER_ROWS);
tokio::spawn(async move {
let _permit = permit;
let txn = match client.build_transaction().read_only(true).start().await {
Ok(txn) => txn,
Err(e) => {
let _ = tx
.send(Err(FraiseQLError::Database {
message: format!(
"Failed to start streaming read transaction: {}",
pg_detail(&e)
),
sql_state: e.code().map(|c| c.code().to_string()),
}))
.await;
return;
},
};
let pairs: Vec<(&str, &str)> =
owned_vars.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
if let Err(e) = super::database::apply_session_vars(&txn, &pairs).await {
let _ = tx.send(Err(e)).await;
return;
}
let param_refs = crate::types::as_sql_param_refs(¶ms);
let rows = match txn.query_raw(&sql, param_refs).await {
Ok(rows) => rows,
Err(e) => {
let _ = tx
.send(Err(FraiseQLError::Database {
message: format!("Streaming query failed: {}", pg_detail(&e)),
sql_state: e.code().map(|c| c.code().to_string()),
}))
.await;
return;
},
};
let mut rows = std::pin::pin!(rows);
loop {
let item = match rows.try_next().await {
Ok(Some(row)) => decode(&row, &sql),
Ok(None) => break,
Err(e) => Err(FraiseQLError::Database {
message: format!("Streaming query failed mid-read: {}", pg_detail(&e)),
sql_state: e.code().map(|c| c.code().to_string()),
}),
};
let failed = item.is_err();
if tx.send(item).await.is_err() || failed {
break;
}
}
if let Err(e) = txn.rollback().await {
tracing::debug!(error = %e, "streaming read transaction rollback failed");
}
});
let Some(first) = rx.recv().await else {
return Ok(Box::pin(futures::stream::empty()));
};
let first = first?;
let rest =
futures::stream::unfold(rx, |mut rx| async move { rx.recv().await.map(|i| (i, rx)) });
Ok(Box::pin(futures::stream::once(async move { Ok(first) }).chain(rest)))
}