Skip to main content

ag_store/
review.rs

1//! Session review-request persistence adapters and query helpers.
2
3use ag_session::ReviewRequest;
4use async_trait::async_trait;
5use sqlx::SqlitePool;
6
7use crate::DbError;
8
9/// Row returned when loading one `session_review_request`.
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct SessionReviewRequestRow {
12    /// Forge-native display identifier such as `#123` or `!123`.
13    pub display_id: String,
14    /// Persisted forge-family discriminator.
15    pub forge_kind: String,
16    /// Most recent successful refresh timestamp in Unix seconds.
17    pub last_refreshed_at: i64,
18    /// Review request source branch.
19    pub source_branch: String,
20    /// Persisted normalized lifecycle state.
21    pub state: String,
22    /// Optional normalized checks or merge-status summary.
23    pub status_summary: Option<String>,
24    /// Review request target branch.
25    pub target_branch: String,
26    /// Review request title.
27    pub title: String,
28    /// Browser-openable review request URL.
29    pub web_url: String,
30}
31
32/// Review-request persistence boundary used by app orchestration and tests.
33#[cfg_attr(test, mockall::automock)]
34#[async_trait]
35pub trait ReviewRepository: Send + Sync {
36    /// Loads the persisted forge review-request linkage for a session.
37    async fn load_session_review_request(
38        &self,
39        id: &str,
40    ) -> Result<Option<SessionReviewRequestRow>, DbError>;
41
42    /// Updates the persisted forge review-request linkage for a session.
43    async fn update_session_review_request(
44        &self,
45        id: &str,
46        review_request: Option<ReviewRequest>,
47    ) -> Result<(), DbError>;
48}
49
50/// `SQLite` implementation of [`ReviewRepository`].
51#[derive(Clone)]
52pub(crate) struct SqliteReviewRepository(SqlitePool);
53
54impl SqliteReviewRepository {
55    /// Creates a review repository backed by the provided pool.
56    pub(crate) fn new(pool: SqlitePool) -> Self {
57        Self(pool)
58    }
59}
60
61#[async_trait]
62impl ReviewRepository for SqliteReviewRepository {
63    async fn load_session_review_request(
64        &self,
65        id: &str,
66    ) -> Result<Option<SessionReviewRequestRow>, DbError> {
67        let review_request = sqlx::query_as!(
68            SessionReviewRequestRow,
69            r"
70SELECT display_id,
71       forge_kind,
72       last_refreshed_at,
73       source_branch,
74       state,
75       status_summary,
76       target_branch,
77       title,
78       web_url
79FROM session_review_request
80WHERE session_id = ?
81",
82            id
83        )
84        .fetch_optional(&self.0)
85        .await?;
86
87        Ok(review_request)
88    }
89
90    async fn update_session_review_request(
91        &self,
92        id: &str,
93        review_request: Option<ReviewRequest>,
94    ) -> Result<(), DbError> {
95        if let Some(review_request) = review_request.as_ref() {
96            sqlx::query!(
97                r"
98INSERT INTO session_review_request (
99    session_id,
100    display_id,
101    forge_kind,
102    last_refreshed_at,
103    source_branch,
104    state,
105    status_summary,
106    target_branch,
107    title,
108    web_url
109)
110VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
111ON CONFLICT(session_id) DO UPDATE
112SET display_id = excluded.display_id,
113    forge_kind = excluded.forge_kind,
114    last_refreshed_at = excluded.last_refreshed_at,
115    source_branch = excluded.source_branch,
116    state = excluded.state,
117    status_summary = excluded.status_summary,
118    target_branch = excluded.target_branch,
119    title = excluded.title,
120    web_url = excluded.web_url
121",
122                id,
123                review_request.summary.display_id.as_str(),
124                review_request.summary.forge_kind.as_str(),
125                review_request.last_refreshed_at,
126                review_request.summary.source_branch.as_str(),
127                review_request.summary.state.as_str(),
128                review_request.summary.status_summary.as_deref(),
129                review_request.summary.target_branch.as_str(),
130                review_request.summary.title.as_str(),
131                review_request.summary.web_url.as_str()
132            )
133            .execute(&self.0)
134            .await?;
135        } else {
136            sqlx::query!(
137                r"
138DELETE FROM session_review_request
139WHERE session_id = ?
140",
141                id
142            )
143            .execute(&self.0)
144            .await?;
145        }
146
147        Ok(())
148    }
149}