a2a_protocol_server/store/postgres_store/
store_impl.rs1use std::future::Future;
14use std::pin::Pin;
15
16use a2a_protocol_types::error::{A2aError, A2aResult};
17use a2a_protocol_types::params::ListTasksParams;
18use a2a_protocol_types::responses::TaskListResponse;
19use a2a_protocol_types::task::{Task, TaskId};
20
21use super::pool::to_a2a_error;
22use super::PostgresTaskStore;
23use crate::store::task_store::{ArtifactDelta, TaskStore};
24
25#[allow(clippy::manual_async_fn)]
26impl TaskStore for PostgresTaskStore {
27 fn save<'a>(
28 &'a self,
29 task: &'a Task,
30 ) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
31 Box::pin(async move {
32 let id = task.id.0.as_str();
33 let context_id = task.context_id.0.as_str();
34 let state = task.status.state.to_string();
35 let data = serde_json::to_value(task)
36 .map_err(|e| A2aError::internal(format!("failed to serialize task: {e}")))?;
37 let status_ts =
41 crate::store::status_timestamp_rfc3339(task.status.timestamp.as_deref());
42
43 sqlx::query(
44 "INSERT INTO tasks (id, context_id, state, data, updated_at)
45 VALUES ($1, $2, $3, $4, COALESCE(($5)::timestamptz, now()))
46 ON CONFLICT(id) DO UPDATE SET
47 context_id = EXCLUDED.context_id,
48 state = EXCLUDED.state,
49 data = EXCLUDED.data,
50 updated_at = EXCLUDED.updated_at",
51 )
52 .bind(id)
53 .bind(context_id)
54 .bind(&state)
55 .bind(&data)
56 .bind(&status_ts)
57 .execute(&self.pool)
58 .await
59 .map_err(to_a2a_error)?;
60
61 Ok(())
62 })
63 }
64
65 fn save_artifact_delta<'a>(
100 &'a self,
101 task: &'a Task,
102 delta: ArtifactDelta,
103 ) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
104 Box::pin(async move {
105 let Some(artifacts) = task.artifacts.as_ref() else {
106 return self.save(task).await;
107 };
108
109 let affected = match delta {
110 ArtifactDelta::AppendedParts { index, count } => {
111 match self.append_parts(task, artifacts, index, count).await? {
112 Some(rows) => rows,
113 None => return self.save(task).await,
114 }
115 }
116 ArtifactDelta::Pushed { index } => {
117 match self.push_artifact(task, artifacts, index).await? {
118 Some(rows) => rows,
119 None => return self.save(task).await,
120 }
121 }
122 };
123
124 if affected == 0 {
128 return self.save(task).await;
129 }
130
131 Ok(())
132 })
133 }
134
135 fn get<'a>(
136 &'a self,
137 id: &'a TaskId,
138 ) -> Pin<Box<dyn Future<Output = A2aResult<Option<Task>>> + Send + 'a>> {
139 Box::pin(async move {
140 let row: Option<(serde_json::Value,)> =
141 sqlx::query_as("SELECT data FROM tasks WHERE id = $1")
142 .bind(id.0.as_str())
143 .fetch_optional(&self.pool)
144 .await
145 .map_err(to_a2a_error)?;
146
147 match row {
148 Some((data,)) => {
149 let task: Task = serde_json::from_value(data).map_err(|e| {
150 A2aError::internal(format!("failed to deserialize task: {e}"))
151 })?;
152 Ok(Some(task))
153 }
154 None => Ok(None),
155 }
156 })
157 }
158
159 #[allow(clippy::too_many_lines)]
160 fn list<'a>(
161 &'a self,
162 params: &'a ListTasksParams,
163 ) -> Pin<Box<dyn Future<Output = A2aResult<TaskListResponse>> + Send + 'a>> {
164 Box::pin(async move {
165 let mut conditions = Vec::new();
167 let mut bind_values: Vec<String> = Vec::new();
168
169 if let Some(ref ctx) = params.context_id {
170 bind_values.push(ctx.clone());
171 conditions.push(format!("context_id = ${}", bind_values.len()));
172 }
173 if let Some(ref status) = params.status {
174 bind_values.push(status.to_string());
175 conditions.push(format!("state = ${}", bind_values.len()));
176 }
177 if let Some(ref after) = params.status_timestamp_after {
182 let Some(after_ts) = crate::store::status_timestamp_rfc3339(Some(after)) else {
183 return Ok(TaskListResponse::new(Vec::new()));
184 };
185 bind_values.push(after_ts);
186 conditions.push(format!(
187 "updated_at > (${})::timestamptz",
188 bind_values.len()
189 ));
190 }
191 if let Some(ref token) = params.page_token {
198 let Some((cursor_ua, cursor_id)) = crate::store::cursor::decode(token) else {
199 return Ok(TaskListResponse::new(Vec::new()));
200 };
201 bind_values.push(cursor_ua.to_string());
202 let ua_idx = bind_values.len();
203 bind_values.push(cursor_id.to_string());
204 let id_idx = bind_values.len();
205 conditions.push(format!(
206 "(updated_at, id) < ((${ua_idx})::timestamp AT TIME ZONE 'UTC', ${id_idx})"
207 ));
208 }
209
210 let where_clause = if conditions.is_empty() {
211 String::new()
212 } else {
213 format!("WHERE {}", conditions.join(" AND "))
214 };
215
216 let page_size = match params.page_size {
217 Some(0) | None => 50_u32,
218 Some(n) => n.min(self.max_page_size),
219 };
220
221 let limit = crate::store::pagination::fetch_limit(page_size);
225 let sql = format!(
226 "SELECT to_char(updated_at AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS.US') AS ua, \
227 data FROM tasks {where_clause} ORDER BY updated_at DESC, id DESC LIMIT {limit}"
228 );
229
230 let mut query = sqlx::query_as::<_, (String, serde_json::Value)>(&sql);
231 for val in &bind_values {
232 query = query.bind(val);
233 }
234
235 let rows: Vec<(String, serde_json::Value)> =
236 query.fetch_all(&self.pool).await.map_err(to_a2a_error)?;
237
238 let mut rows: Vec<(String, Task)> = rows
239 .into_iter()
240 .map(|(updated_at, data)| {
241 serde_json::from_value::<Task>(data)
242 .map(|task| (updated_at, task))
243 .map_err(|e| A2aError::internal(format!("deserialize: {e}")))
244 })
245 .collect::<A2aResult<Vec<_>>>()?;
246
247 let next_page_token =
248 if crate::store::pagination::has_next_page(rows.len(), page_size as usize) {
249 rows.truncate(page_size as usize);
250 rows.last()
251 .map(|(ua, task)| crate::store::cursor::encode(ua, task.id.0.as_str()))
252 .unwrap_or_default()
253 } else {
254 String::new()
255 };
256
257 #[allow(clippy::cast_possible_truncation)]
258 let page_len = rows.len() as u32;
259 let tasks: Vec<Task> = rows.into_iter().map(|(_, task)| task).collect();
260 let mut response = TaskListResponse::new(tasks);
261 response.next_page_token = next_page_token;
262 response.page_size = page_len;
263 Ok(response)
264 })
265 }
266
267 fn insert_if_absent<'a>(
268 &'a self,
269 task: &'a Task,
270 ) -> Pin<Box<dyn Future<Output = A2aResult<bool>> + Send + 'a>> {
271 Box::pin(async move {
272 let id = task.id.0.as_str();
273 let context_id = task.context_id.0.as_str();
274 let state = task.status.state.to_string();
275 let data = serde_json::to_value(task)
276 .map_err(|e| A2aError::internal(format!("failed to serialize task: {e}")))?;
277
278 let status_ts =
279 crate::store::status_timestamp_rfc3339(task.status.timestamp.as_deref());
280 let result = sqlx::query(
281 "INSERT INTO tasks (id, context_id, state, data, updated_at)
282 VALUES ($1, $2, $3, $4, COALESCE(($5)::timestamptz, now()))
283 ON CONFLICT(id) DO NOTHING",
284 )
285 .bind(id)
286 .bind(context_id)
287 .bind(&state)
288 .bind(&data)
289 .bind(&status_ts)
290 .execute(&self.pool)
291 .await
292 .map_err(to_a2a_error)?;
293
294 Ok(result.rows_affected() > 0)
295 })
296 }
297
298 fn delete<'a>(
299 &'a self,
300 id: &'a TaskId,
301 ) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
302 Box::pin(async move {
303 sqlx::query("DELETE FROM tasks WHERE id = $1")
304 .bind(id.0.as_str())
305 .execute(&self.pool)
306 .await
307 .map_err(to_a2a_error)?;
308 Ok(())
309 })
310 }
311
312 fn count<'a>(&'a self) -> Pin<Box<dyn Future<Output = A2aResult<u64>> + Send + 'a>> {
313 Box::pin(async move {
314 let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks")
315 .fetch_one(&self.pool)
316 .await
317 .map_err(to_a2a_error)?;
318 #[allow(clippy::cast_sign_loss)]
319 Ok(row.0 as u64)
320 })
321 }
322}