use a2a_protocol_types::error::{A2aError, A2aResult};
use sqlx::postgres::{PgPool, PgPoolOptions};
#[allow(unused_imports)] use super::task_store::TaskStore;
#[allow(unused_imports)]
use super::tenant::TenantContext;
use super::tenant_event_log as evlog;
use super::tenant_idempotency as idem;
#[derive(Debug, Clone)]
pub struct TenantAwarePostgresTaskStore {
pool: PgPool,
max_page_size: u32,
metrics: crate::metrics::MetricsHandle,
}
impl TenantAwarePostgresTaskStore {
#[must_use]
pub const fn with_max_page_size(mut self, max: u32) -> Self {
self.max_page_size = max;
self
}
#[must_use]
pub fn with_metrics(mut self, metrics: crate::metrics::MetricsHandle) -> Self {
self.metrics = metrics;
self
}
pub async fn new(url: &str) -> Result<Self, sqlx::Error> {
let pool = PgPoolOptions::new()
.max_connections(10)
.connect(url)
.await?;
Self::from_pool(pool).await
}
pub async fn from_pool(pool: PgPool) -> Result<Self, sqlx::Error> {
crate::store::pg_schema::apply(
&pool,
&[
"CREATE TABLE IF NOT EXISTS tenant_tasks (
tenant_id TEXT NOT NULL DEFAULT '',
id TEXT NOT NULL,
context_id TEXT NOT NULL,
state TEXT NOT NULL,
data JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (tenant_id, id)
)",
idem::PG_CREATE_TABLE,
evlog::PG_CREATE_TABLE,
"CREATE INDEX IF NOT EXISTS idx_tenant_tasks_ctx ON tenant_tasks(tenant_id, context_id)",
"CREATE INDEX IF NOT EXISTS idx_tenant_tasks_state ON tenant_tasks(tenant_id, state)",
"CREATE INDEX IF NOT EXISTS idx_tenant_tasks_updated_at ON tenant_tasks(tenant_id, updated_at DESC, id DESC)",
],
)
.await?;
Ok(Self {
pool,
max_page_size: crate::store::DEFAULT_MAX_PAGE_SIZE,
metrics: crate::metrics::MetricsHandle::default(),
})
}
pub async fn purge_expired(
&self,
policy: &super::retention::RetentionPolicy,
) -> A2aResult<super::retention::PurgeReport> {
super::retention::postgres::purge(
&self.pool,
"tenant_tasks",
crate::store::tenant_idempotency::PG_EXPIRE,
policy,
)
.await
.map_err(|e| to_a2a_error(&e))
}
}
fn to_a2a_error(e: &sqlx::Error) -> A2aError {
A2aError::internal(format!("postgres error: {e}"))
}
mod store_impl;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_a2a_error_formats_message() {
let pg_err = sqlx::Error::RowNotFound;
let a2a_err = to_a2a_error(&pg_err);
let msg = format!("{a2a_err}");
assert!(
msg.contains("postgres error"),
"error message should contain 'postgres error': {msg}"
);
}
}