turnframe_store_postgres/
replay.rs1use async_trait::async_trait;
11use sqlx::PgConnection;
12use sqlx::postgres::PgRow;
13use turnframe_core::ids::{AccountId, ConversationId, TurnId};
14use turnframe_core::replay::ReplayRecord;
15use turnframe_store::error::StoreError;
16use turnframe_store::replay::{ReplayReader, ReplayWriter};
17
18use crate::codec::{column, from_json, label, limit_to_sql, to_json};
19use crate::error::store_error;
20use crate::store::{PgStores, commit};
21
22pub(crate) async fn put(conn: &mut PgConnection, record: ReplayRecord) -> Result<(), StoreError> {
24 sqlx::query(
25 "INSERT INTO tf_replay (account_id, turn_id, conversation_id, phase, recorded_at, record)
26 VALUES ($1, $2, $3, $4, $5, $6)
27 ON CONFLICT (account_id, turn_id) DO UPDATE
28 SET conversation_id = EXCLUDED.conversation_id,
29 phase = EXCLUDED.phase,
30 recorded_at = EXCLUDED.recorded_at,
31 record = EXCLUDED.record",
32 )
33 .bind(record.account_id.as_str())
34 .bind(record.turn_id.as_uuid())
35 .bind(record.conversation_id.as_uuid())
36 .bind(label(&record.phase)?)
37 .bind(record.recorded_at)
38 .bind(to_json(&record)?)
39 .execute(conn)
40 .await
41 .map_err(|error| store_error(&error))?;
42 Ok(())
43}
44
45pub(crate) async fn get(
47 conn: &mut PgConnection,
48 account: &AccountId,
49 turn_id: &TurnId,
50) -> Result<ReplayRecord, StoreError> {
51 let row = sqlx::query("SELECT record FROM tf_replay WHERE account_id = $1 AND turn_id = $2")
52 .bind(account.as_str())
53 .bind(turn_id.as_uuid())
54 .fetch_optional(conn)
55 .await
56 .map_err(|error| store_error(&error))?
57 .ok_or(StoreError::NotFound)?;
58 decode_record(&row)
59}
60
61pub(crate) async fn list_for_conversation(
63 conn: &mut PgConnection,
64 account: &AccountId,
65 conversation: &ConversationId,
66 limit: usize,
67) -> Result<Vec<ReplayRecord>, StoreError> {
68 let rows = sqlx::query(
69 "SELECT record FROM tf_replay
70 WHERE account_id = $1 AND conversation_id = $2
71 ORDER BY recorded_at DESC, turn_id DESC
72 LIMIT $3",
73 )
74 .bind(account.as_str())
75 .bind(conversation.as_uuid())
76 .bind(limit_to_sql(limit))
77 .fetch_all(conn)
78 .await
79 .map_err(|error| store_error(&error))?;
80 let mut records = rows
81 .iter()
82 .map(decode_record)
83 .collect::<Result<Vec<ReplayRecord>, StoreError>>()?;
84 records.reverse();
85 Ok(records)
86}
87
88fn decode_record(row: &PgRow) -> Result<ReplayRecord, StoreError> {
90 from_json(column(row, "record")?)
91}
92
93#[async_trait]
94impl ReplayReader for PgStores {
95 async fn get(&self, account: &AccountId, turn_id: &TurnId) -> Result<ReplayRecord, StoreError> {
96 let mut conn = self.connection().await?;
97 get(&mut conn, account, turn_id).await
98 }
99
100 async fn list_for_conversation(
101 &self,
102 account: &AccountId,
103 conversation: &ConversationId,
104 limit: usize,
105 ) -> Result<Vec<ReplayRecord>, StoreError> {
106 let mut conn = self.connection().await?;
107 list_for_conversation(&mut conn, account, conversation, limit).await
108 }
109}
110
111#[async_trait]
112impl ReplayWriter for PgStores {
113 async fn put(&self, record: ReplayRecord) -> Result<(), StoreError> {
114 let mut transaction = self.transaction().await?;
115 put(&mut transaction, record).await?;
116 commit(transaction).await
117 }
118}