1use deadpool_postgres::{Object, Pool, Runtime};
2use snafu::ResultExt;
3use tokio_postgres::types::ToSql;
4use tokio_postgres::{NoTls, Row, Statement};
5
6use radicle::cob::{ObjectId, TypeName};
7use radicle::git::Oid;
8use radicle::prelude::RepoId;
9
10use crate::entry::{OperationEntry, TimelineEntry};
11use crate::radicle_extra::sql;
12use crate::storage::{FeedStorage, StorageStats};
13
14pub struct PostgresStorage {
15 connection: Pool,
16}
17
18impl PostgresStorage {
19 pub fn new(db_url: String) -> Result<Self, snafu::Whatever> {
20 let pool = deadpool_postgres::Config {
21 url: Some(db_url),
22 ..Default::default()
23 }
24 .create_pool(Some(Runtime::Tokio1), NoTls)
25 .whatever_context("Unable to create pool")?;
26
27 Ok(Self { connection: pool })
28 }
29
30 pub async fn client(&self) -> Result<Object, snafu::Whatever> {
31 self.connection
32 .get()
33 .await
34 .whatever_context("Unable to get a client")
35 }
36
37 pub async fn statement(
38 &self,
39 client: &Object,
40 query: &str,
41 ) -> Result<Statement, snafu::Whatever> {
42 client
43 .prepare_cached(query)
44 .await
45 .whatever_context("Unable to prepare statement")
46 }
47
48 pub async fn query(
49 &self,
50 client: &Object,
51 stmt: &Statement,
52 params: &[&(dyn ToSql + Sync)],
53 ) -> Result<Vec<Row>, snafu::Whatever> {
54 client
55 .query(stmt, params)
56 .await
57 .whatever_context("Unable to query db")
58 }
59}
60
61impl FeedStorage for PostgresStorage {
62 type Error = snafu::Whatever;
63
64 async fn get_last_processed_operation(
65 &self,
66 rid: &RepoId,
67 cob_id: &ObjectId,
68 typename: &TypeName,
69 ) -> Result<Option<sql::Oid>, Self::Error> {
70 let client = self.client().await?;
71 let stmt = self
72 .statement(
73 &client,
74 "SELECT last_operation_id FROM activity_feed_timeline WHERE repo = $1 AND cob_id = $2 AND typename = $3",
75 )
76 .await?;
77 let rows = self
78 .query(
79 &client,
80 &stmt,
81 &[&rid.to_string(), &cob_id.to_string(), &typename.to_string()],
82 )
83 .await?;
84
85 if rows.is_empty() {
86 return Ok(None);
87 }
88
89 Ok(rows[0].get::<usize, Option<sql::Oid>>(0))
90 }
91
92 async fn operation_exists(&self, operation_id: &Oid) -> Result<bool, Self::Error> {
93 let client = self.client().await?;
94 let statement = self
95 .statement(
96 &client,
97 "SELECT 1 FROM activity_feed_operations WHERE operation_id = $1 LIMIT 1",
98 )
99 .await?;
100 let row = self
101 .query(&client, &statement, &[&operation_id.to_string()])
102 .await?;
103
104 Ok(!row.is_empty())
105 }
106
107 async fn insert_timeline_entry(&mut self, entry: &TimelineEntry) -> Result<(), Self::Error> {
109 let client = self.client().await?;
110 let stmt = self.statement(&client, "INSERT INTO activity_feed_timeline (repo, node, cob_id, cob_title, cob_status, typename, last_operation_id, operations)
111 VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (repo, node, cob_id) DO UPDATE SET last_operation_id = EXCLUDED.last_operation_id").await?;
112
113 self.query(
114 &client,
115 &stmt,
116 &[
117 &entry.repo.to_string(),
118 &entry.node.to_string(),
119 &entry.cob_id.to_string(),
120 &entry.cob_title,
121 &entry.cob_status,
122 &entry.typename.to_string(),
123 &entry.last_operation_id.map(|o| o.to_string()),
124 &entry.operations,
125 ],
126 )
127 .await?;
128
129 Ok(())
130 }
131
132 async fn insert_batch(&mut self, entries: &[OperationEntry]) -> Result<(), Self::Error> {
133 if entries.is_empty() {
134 return Ok(());
135 }
136
137 let client = self.client().await?;
138 let stmt = self.statement(&client, "INSERT INTO activity_feed_operations (rid, operation_id, author, author_alias, actions, created_at, typename)
139 VALUES ($1, $2, $3, $4, $5, $6, $7)").await?;
140
141 for entry in entries {
142 self.query(
143 &client,
144 &stmt,
145 &[
146 &entry.rid.to_string(),
147 &entry.operation_id.to_string(),
148 &entry.author.to_string(),
149 &entry.author_alias.as_ref().map(|alias| alias.to_string()),
150 &entry.actions,
151 &(entry.timestamp.as_secs() as i32),
152 &entry.typename.to_string(),
153 ],
154 )
155 .await?;
156 }
157
158 Ok(())
159 }
160
161 async fn get_stats(&self) -> Result<StorageStats, Self::Error> {
162 let mut stats = StorageStats::default();
163
164 let client = self.client().await?;
165 let statement_total_operations = self
166 .statement(&client, "SELECT COUNT(*) FROM activity_feed_operations")
167 .await?;
168 let rows = self
169 .query(&client, &statement_total_operations, &[])
170 .await?;
171 let count: i64 = rows[0].get(0);
172 stats.total_operations = count as u64;
173
174 let statement_operations_by_type = self
175 .statement(
176 &client,
177 "SELECT typename, COUNT(*) FROM activity_feed_operations GROUP BY typename",
178 )
179 .await?;
180 let rows = self
181 .query(&client, &statement_operations_by_type, &[])
182 .await?;
183
184 for row in rows {
185 let typename: String = row.get(0);
186 let count: i64 = row.get(1);
187 stats.operations_by_type.insert(typename, count as u64);
188 }
189
190 let statement_tracked_objects = self
191 .statement(&client, "SELECT COUNT(*) FROM activity_feed_operations")
192 .await?;
193 let rows = self.query(&client, &statement_tracked_objects, &[]).await?;
194
195 for row in rows {
196 let count: i64 = row.get(0);
197 stats.tracked_objects = count as u64;
198 }
199
200 Ok(stats)
201 }
202}