use std::collections::HashMap;
use bytes::Bytes;
use sayiir_core::codec::{self, Decoder};
use sayiir_core::snapshot::WorkflowSnapshot;
use sayiir_persistence::BackendError;
use sha2::{Digest, Sha256};
use sqlx::{PgConnection, Row};
use crate::backend::PostgresBackend;
use crate::error::PgError;
pub(crate) async fn fetch_task_outputs<'e, E>(
executor: E,
instance_id: &str,
) -> Result<Vec<(sayiir_core::TaskId, Bytes)>, BackendError>
where
E: sqlx::PgExecutor<'e>,
{
let rows = sqlx::query(
"SELECT task_id, output FROM sayiir_workflow_tasks
WHERE instance_id = $1 AND status = 'completed' AND output IS NOT NULL",
)
.bind(instance_id)
.fetch_all(executor)
.await
.map_err(PgError)?;
let mut outputs = Vec::with_capacity(rows.len());
for row in rows {
let task_id_bytes: &[u8] = row.get("task_id");
let task_id = sayiir_core::TaskId::from_slice(task_id_bytes).map_err(|_| {
BackendError::Backend(format!(
"sayiir_workflow_tasks.task_id for instance {instance_id} is {} bytes (expected 32)",
task_id_bytes.len()
))
})?;
let output: Vec<u8> = row.get("output");
outputs.push((task_id, Bytes::from(output)));
}
Ok(outputs)
}
pub(crate) async fn fetch_task_outputs_batched<'e, E>(
executor: E,
instance_ids: &[&str],
) -> Result<HashMap<String, Vec<(sayiir_core::TaskId, Bytes)>>, BackendError>
where
E: sqlx::PgExecutor<'e>,
{
if instance_ids.is_empty() {
return Ok(HashMap::new());
}
let rows = sqlx::query(
"SELECT instance_id, task_id, output FROM sayiir_workflow_tasks
WHERE instance_id = ANY($1) AND status = 'completed' AND output IS NOT NULL",
)
.bind(instance_ids)
.fetch_all(executor)
.await
.map_err(PgError)?;
let mut grouped: HashMap<String, Vec<(sayiir_core::TaskId, Bytes)>> = HashMap::new();
for row in rows {
let instance_id: String = row.get("instance_id");
let task_id_bytes: &[u8] = row.get("task_id");
let task_id = sayiir_core::TaskId::from_slice(task_id_bytes).map_err(|_| {
BackendError::Backend(format!(
"sayiir_workflow_tasks.task_id for instance {instance_id} is {} bytes (expected 32)",
task_id_bytes.len()
))
})?;
let output: Vec<u8> = row.get("output");
grouped
.entry(instance_id)
.or_default()
.push((task_id, Bytes::from(output)));
}
Ok(grouped)
}
pub(crate) fn snapshot_hash(data: &[u8]) -> [u8; 32] {
Sha256::digest(data).into()
}
impl<C> PostgresBackend<C>
where
C: Decoder + codec::CodecIdentity + codec::sealed::DecodeValue<WorkflowSnapshot>,
{
pub(crate) async fn fetch_blob<'e, E>(
&self,
executor: E,
instance_id: &str,
history_version: i32,
) -> Result<Vec<u8>, BackendError>
where
E: sqlx::PgExecutor<'e>,
{
let row = sqlx::query(
"SELECT data FROM sayiir_workflow_snapshot_history
WHERE instance_id = $1 AND version = $2",
)
.bind(instance_id)
.bind(history_version)
.fetch_one(executor)
.await
.map_err(PgError)?;
Ok(row.get("data"))
}
pub(crate) async fn lock_snapshot_for_mutation(
&self,
tx: &mut PgConnection,
instance_id: &str,
) -> Result<Option<(WorkflowSnapshot, i32)>, BackendError> {
let row = sqlx::query(
"SELECT history_version FROM sayiir_workflow_snapshots
WHERE instance_id = $1
FOR UPDATE",
)
.bind(instance_id)
.fetch_optional(&mut *tx)
.await
.map_err(PgError)?;
let Some(row) = row else { return Ok(None) };
let history_version: i32 = row.get("history_version");
let row = sqlx::query(
"WITH outputs AS (
SELECT task_id, output FROM sayiir_workflow_tasks
WHERE instance_id = $1
AND status = 'completed'
AND output IS NOT NULL
)
SELECT
(SELECT data FROM sayiir_workflow_snapshot_history
WHERE instance_id = $1 AND version = $2) AS data,
COALESCE((SELECT array_agg(task_id ORDER BY task_id) FROM outputs),
ARRAY[]::bytea[]) AS task_ids,
COALESCE((SELECT array_agg(output ORDER BY task_id) FROM outputs),
ARRAY[]::bytea[]) AS outputs",
)
.bind(instance_id)
.bind(history_version)
.fetch_one(&mut *tx)
.await
.map_err(PgError)?;
let data: Option<Vec<u8>> = row.get("data");
let Some(data) = data else {
return Ok(None);
};
let mut snapshot = self.decode(&data)?;
let task_ids: Vec<Vec<u8>> = row.get("task_ids");
let outputs_vec: Vec<Vec<u8>> = row.get("outputs");
let mut outputs = Vec::with_capacity(task_ids.len());
for (tid_bytes, output) in task_ids.into_iter().zip(outputs_vec) {
let task_id = sayiir_core::TaskId::from_slice(&tid_bytes).map_err(|_| {
BackendError::Backend(format!(
"sayiir_workflow_tasks.task_id for instance {instance_id} is {} bytes (expected 32)",
tid_bytes.len()
))
})?;
outputs.push((task_id, Bytes::from(output)));
}
snapshot.hydrate_task_outputs(outputs);
Ok(Some((snapshot, history_version)))
}
}
pub(crate) async fn append_history(
tx: &mut PgConnection,
instance_id: &str,
version: i32,
status: &str,
current_task_id: Option<&[u8]>,
data: &[u8],
data_hash: &[u8; 32],
) -> Result<(), BackendError> {
sqlx::query(
"INSERT INTO sayiir_workflow_snapshot_history
(instance_id, version, status, current_task_id, data, data_hash)
VALUES ($1, $2, $3, $4, $5, $6)",
)
.bind(instance_id)
.bind(version)
.bind(status)
.bind(current_task_id)
.bind(data)
.bind(data_hash.as_slice())
.execute(&mut *tx)
.await
.map_err(PgError)?;
Ok(())
}