1use std::collections::HashMap;
2
3use rand::Rng;
4
5use async_trait::async_trait;
6use atuin_common::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus};
7use atuin_server_database::models::{NewSession, NewUser, Session, User};
8use atuin_server_database::{Database, DbError, DbResult, DbSettings};
9use sqlx::postgres::PgPoolOptions;
10
11use tracing::instrument;
12use uuid::Uuid;
13use wrappers::{DbRecord, DbSession, DbUser};
14
15mod wrappers;
16
17const MIN_PG_VERSION: u32 = 14;
18
19#[derive(Clone)]
20pub struct Postgres {
21 pool: sqlx::Pool<sqlx::postgres::Postgres>,
22 read_pool: Option<sqlx::Pool<sqlx::postgres::Postgres>>,
24}
25
26impl Postgres {
27 fn read_pool(&self) -> &sqlx::Pool<sqlx::postgres::Postgres> {
30 self.read_pool.as_ref().unwrap_or(&self.pool)
31 }
32}
33
34#[async_trait]
35impl Database for Postgres {
36 async fn new(settings: &DbSettings) -> DbResult<Self> {
37 let pool = PgPoolOptions::new()
38 .max_connections(100)
39 .connect(settings.db_uri.as_str())
40 .await?;
41
42 let pg_major_version: u32 =
45 pool.acquire()
46 .await?
47 .server_version_num()
48 .ok_or(DbError::Other(eyre::Report::msg(
49 "could not get PostgreSQL version",
50 )))?
51 / 10000;
52
53 if pg_major_version < MIN_PG_VERSION {
54 return Err(DbError::Other(eyre::Report::msg(format!(
55 "unsupported PostgreSQL version {pg_major_version}, minimum required is {MIN_PG_VERSION}"
56 ))));
57 }
58
59 sqlx::migrate!("./migrations")
60 .run(&pool)
61 .await
62 .map_err(|error| DbError::Other(error.into()))?;
63
64 let read_pool = if let Some(read_db_uri) = &settings.read_db_uri {
66 tracing::info!("Connecting to read replica database");
67 let read_pool = PgPoolOptions::new()
68 .max_connections(100)
69 .connect(read_db_uri.as_str())
70 .await?;
71
72 let read_pg_major_version: u32 = read_pool
74 .acquire()
75 .await?
76 .server_version_num()
77 .ok_or(DbError::Other(eyre::Report::msg(
78 "could not get PostgreSQL version from read replica",
79 )))?
80 / 10000;
81
82 if read_pg_major_version < MIN_PG_VERSION {
83 return Err(DbError::Other(eyre::Report::msg(format!(
84 "unsupported PostgreSQL version {read_pg_major_version} on read replica, minimum required is {MIN_PG_VERSION}"
85 ))));
86 }
87
88 Some(read_pool)
89 } else {
90 None
91 };
92
93 Ok(Self { pool, read_pool })
94 }
95
96 #[instrument(skip_all)]
97 async fn get_session(&self, token: &str) -> DbResult<Session> {
98 sqlx::query_as("select id, user_id, token from sessions where token = $1")
99 .bind(token)
100 .fetch_one(self.read_pool())
101 .await
102 .map_err(Into::into)
103 .map(|DbSession(session)| session)
104 }
105
106 #[instrument(skip_all)]
107 async fn get_user(&self, username: &str) -> DbResult<User> {
108 sqlx::query_as("select id, username, email, password from users where username = $1")
109 .bind(username)
110 .fetch_one(self.read_pool())
111 .await
112 .map_err(Into::into)
113 .map(|DbUser(user)| user)
114 }
115
116 #[instrument(skip_all)]
117 async fn get_session_user(&self, token: &str) -> DbResult<User> {
118 sqlx::query_as(
119 "select users.id, users.username, users.email, users.password from users
120 inner join sessions
121 on users.id = sessions.user_id
122 and sessions.token = $1",
123 )
124 .bind(token)
125 .fetch_one(self.read_pool())
126 .await
127 .map_err(Into::into)
128 .map(|DbUser(user)| user)
129 }
130
131 async fn delete_store(&self, user: &User) -> DbResult<()> {
132 let mut tx = self.pool.begin().await?;
133
134 sqlx::query(
135 "delete from store
136 where user_id = $1",
137 )
138 .bind(user.id)
139 .execute(&mut *tx)
140 .await?;
141
142 sqlx::query(
143 "delete from store_idx_cache
144 where user_id = $1",
145 )
146 .bind(user.id)
147 .execute(&mut *tx)
148 .await?;
149
150 tx.commit().await?;
151
152 Ok(())
153 }
154
155 #[instrument(skip_all)]
156 async fn delete_user(&self, u: &User) -> DbResult<()> {
157 sqlx::query("delete from sessions where user_id = $1")
158 .bind(u.id)
159 .execute(&self.pool)
160 .await?;
161
162 sqlx::query("delete from history where user_id = $1")
163 .bind(u.id)
164 .execute(&self.pool)
165 .await?;
166
167 sqlx::query("delete from store where user_id = $1")
168 .bind(u.id)
169 .execute(&self.pool)
170 .await?;
171
172 sqlx::query("delete from total_history_count_user where user_id = $1")
173 .bind(u.id)
174 .execute(&self.pool)
175 .await?;
176
177 sqlx::query("delete from users where id = $1")
178 .bind(u.id)
179 .execute(&self.pool)
180 .await?;
181
182 Ok(())
183 }
184
185 #[instrument(skip_all)]
186 async fn update_user_password(&self, user: &User) -> DbResult<()> {
187 sqlx::query(
188 "update users
189 set password = $1
190 where id = $2",
191 )
192 .bind(&user.password)
193 .bind(user.id)
194 .execute(&self.pool)
195 .await?;
196
197 Ok(())
198 }
199
200 #[instrument(skip_all)]
201 async fn add_user(&self, user: &NewUser) -> DbResult<i64> {
202 let email: &str = &user.email;
203 let username: &str = &user.username;
204 let password: &str = &user.password;
205
206 let res: (i64,) = sqlx::query_as(
207 "insert into users
208 (username, email, password)
209 values($1, $2, $3)
210 returning id",
211 )
212 .bind(username)
213 .bind(email)
214 .bind(password)
215 .fetch_one(&self.pool)
216 .await?;
217
218 Ok(res.0)
219 }
220
221 #[instrument(skip_all)]
222 async fn add_session(&self, session: &NewSession) -> DbResult<()> {
223 let token: &str = &session.token;
224
225 sqlx::query(
226 "insert into sessions
227 (user_id, token)
228 values($1, $2)",
229 )
230 .bind(session.user_id)
231 .bind(token)
232 .execute(&self.pool)
233 .await?;
234
235 Ok(())
236 }
237
238 #[instrument(skip_all)]
239 async fn get_user_session(&self, u: &User) -> DbResult<Session> {
240 sqlx::query_as("select id, user_id, token from sessions where user_id = $1")
241 .bind(u.id)
242 .fetch_one(self.read_pool())
243 .await
244 .map_err(Into::into)
245 .map(|DbSession(session)| session)
246 }
247
248 #[instrument(skip_all)]
249 async fn add_records(&self, user: &User, records: &[Record<EncryptedData>]) -> DbResult<()> {
250 let mut tx = self.pool.begin().await?;
251
252 let mut heads = HashMap::<(HostId, &str), u64>::new();
260
261 for i in records {
262 let id = atuin_common::utils::uuid_v7();
263
264 let result = sqlx::query(
265 "insert into store
266 (id, client_id, host, idx, timestamp, version, tag, data, cek, user_id)
267 values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
268 on conflict do nothing
269 ",
270 )
271 .bind(id)
272 .bind(i.id)
273 .bind(i.host.id)
274 .bind(i.idx as i64)
275 .bind(i.timestamp as i64) .bind(&i.version)
277 .bind(&i.tag)
278 .bind(&i.data.data)
279 .bind(&i.data.content_encryption_key)
280 .bind(user.id)
281 .execute(&mut *tx)
282 .await?;
283
284 if result.rows_affected() > 0 {
286 heads
287 .entry((i.host.id, &i.tag))
288 .and_modify(|e| {
289 if i.idx > *e {
290 *e = i.idx
291 }
292 })
293 .or_insert(i.idx);
294 }
295 }
296
297 for ((host, tag), idx) in heads {
299 sqlx::query(
300 "insert into store_idx_cache
301 (user_id, host, tag, idx)
302 values ($1, $2, $3, $4)
303 on conflict(user_id, host, tag) do update set idx = greatest(store_idx_cache.idx, $4)
304 ",
305 )
306 .bind(user.id)
307 .bind(host)
308 .bind(tag)
309 .bind(idx as i64)
310 .execute(&mut *tx)
311 .await
312 ?;
313 }
314
315 tx.commit().await?;
316
317 Ok(())
318 }
319
320 #[instrument(skip_all)]
321 async fn next_records(
322 &self,
323 user: &User,
324 host: HostId,
325 tag: String,
326 start: Option<RecordIdx>,
327 count: u64,
328 ) -> DbResult<Vec<Record<EncryptedData>>> {
329 tracing::debug!("{:?} - {:?} - {:?}", host, tag, start);
330 let start = start.unwrap_or(0);
331
332 let records: Result<Vec<DbRecord>, DbError> = sqlx::query_as(
333 "select client_id, host, idx, timestamp, version, tag, data, cek from store
334 where user_id = $1
335 and tag = $2
336 and host = $3
337 and idx >= $4
338 order by idx asc
339 limit $5",
340 )
341 .bind(user.id)
342 .bind(tag.clone())
343 .bind(host)
344 .bind(start as i64)
345 .bind(count as i64)
346 .fetch_all(self.read_pool())
347 .await
348 .map_err(Into::into);
349
350 let ret = match records {
351 Ok(records) => {
352 let records: Vec<Record<EncryptedData>> = records
353 .into_iter()
354 .map(|f| {
355 let record: Record<EncryptedData> = f.into();
356 record
357 })
358 .collect();
359
360 records
361 }
362 Err(DbError::NotFound) => {
363 tracing::debug!("no records found in store: {:?}/{}", host, tag);
364 return Ok(vec![]);
365 }
366 Err(e) => return Err(e),
367 };
368
369 Ok(ret)
370 }
371
372 async fn status(&self, user: &User) -> DbResult<RecordStatus> {
373 const STATUS_SQL: &str =
374 "select host, tag, max(idx) from store where user_id = $1 group by host, tag";
375
376 let idx_cache_rollout = std::env::var("IDX_CACHE_ROLLOUT").unwrap_or("0".to_string());
383 let idx_cache_rollout = idx_cache_rollout.parse::<f64>().unwrap_or(0.0);
384 let use_idx_cache = rand::thread_rng().gen_bool(idx_cache_rollout / 100.0);
385
386 let mut res: Vec<(Uuid, String, i64)> = if use_idx_cache {
387 tracing::debug!("using idx cache for user {}", user.id);
388 sqlx::query_as("select host, tag, idx from store_idx_cache where user_id = $1")
389 .bind(user.id)
390 .fetch_all(self.read_pool())
391 .await?
392 } else {
393 tracing::debug!("using aggregate query for user {}", user.id);
394 sqlx::query_as(STATUS_SQL)
395 .bind(user.id)
396 .fetch_all(self.read_pool())
397 .await?
398 };
399
400 res.sort();
401
402 let mut status = RecordStatus::new();
403
404 for i in res.iter() {
405 status.set_raw(HostId(i.0), i.1.clone(), i.2 as u64);
406 }
407
408 Ok(status)
409 }
410}