1use std::str::FromStr;
2
3use async_trait::async_trait;
4use atuin_common::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus};
5use atuin_server_database::{
6 Database, DbError, DbResult, DbSettings, into_utc,
7 models::{History, NewHistory, NewSession, NewUser, Session, User},
8};
9use futures_util::TryStreamExt;
10use sqlx::{
11 Row,
12 sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions},
13 types::Uuid,
14};
15use tracing::instrument;
16use wrappers::{DbHistory, DbRecord, DbSession, DbUser};
17
18mod wrappers;
19
20#[derive(Clone)]
21pub struct Sqlite {
22 pool: sqlx::Pool<sqlx::sqlite::Sqlite>,
23}
24
25#[async_trait]
26impl Database for Sqlite {
27 async fn new(settings: &DbSettings) -> DbResult<Self> {
28 let opts = SqliteConnectOptions::from_str(&settings.db_uri)?
29 .journal_mode(SqliteJournalMode::Wal)
30 .create_if_missing(true);
31
32 let pool = SqlitePoolOptions::new().connect_with(opts).await?;
33
34 sqlx::migrate!("./migrations")
35 .run(&pool)
36 .await
37 .map_err(|error| DbError::Other(error.into()))?;
38
39 Ok(Self { pool })
40 }
41
42 #[instrument(skip_all)]
43 async fn get_session(&self, token: &str) -> DbResult<Session> {
44 sqlx::query_as("select id, user_id, token from sessions where token = $1")
45 .bind(token)
46 .fetch_one(&self.pool)
47 .await
48 .map_err(Into::into)
49 .map(|DbSession(session)| session)
50 }
51
52 #[instrument(skip_all)]
53 async fn get_session_user(&self, token: &str) -> DbResult<User> {
54 sqlx::query_as(
55 "select users.id, users.username, users.email, users.password from users
56 inner join sessions
57 on users.id = sessions.user_id
58 and sessions.token = $1",
59 )
60 .bind(token)
61 .fetch_one(&self.pool)
62 .await
63 .map_err(Into::into)
64 .map(|DbUser(user)| user)
65 }
66
67 #[instrument(skip_all)]
68 async fn add_session(&self, session: &NewSession) -> DbResult<()> {
69 let token: &str = &session.token;
70
71 sqlx::query(
72 "insert into sessions
73 (user_id, token)
74 values($1, $2)",
75 )
76 .bind(session.user_id)
77 .bind(token)
78 .execute(&self.pool)
79 .await?;
80
81 Ok(())
82 }
83
84 #[instrument(skip_all)]
85 async fn get_user(&self, username: &str) -> DbResult<User> {
86 sqlx::query_as("select id, username, email, password from users where username = $1")
87 .bind(username)
88 .fetch_one(&self.pool)
89 .await
90 .map_err(Into::into)
91 .map(|DbUser(user)| user)
92 }
93
94 #[instrument(skip_all)]
95 async fn get_user_session(&self, u: &User) -> DbResult<Session> {
96 sqlx::query_as("select id, user_id, token from sessions where user_id = $1")
97 .bind(u.id)
98 .fetch_one(&self.pool)
99 .await
100 .map_err(Into::into)
101 .map(|DbSession(session)| session)
102 }
103
104 #[instrument(skip_all)]
105 async fn add_user(&self, user: &NewUser) -> DbResult<i64> {
106 let email: &str = &user.email;
107 let username: &str = &user.username;
108 let password: &str = &user.password;
109
110 let res: (i64,) = sqlx::query_as(
111 "insert into users
112 (username, email, password)
113 values($1, $2, $3)
114 returning id",
115 )
116 .bind(username)
117 .bind(email)
118 .bind(password)
119 .fetch_one(&self.pool)
120 .await?;
121
122 Ok(res.0)
123 }
124
125 #[instrument(skip_all)]
126 async fn update_user_password(&self, user: &User) -> DbResult<()> {
127 sqlx::query(
128 "update users
129 set password = $1
130 where id = $2",
131 )
132 .bind(&user.password)
133 .bind(user.id)
134 .execute(&self.pool)
135 .await?;
136
137 Ok(())
138 }
139
140 #[instrument(skip_all)]
141 async fn count_history(&self, user: &User) -> DbResult<i64> {
142 let res: (i64,) = sqlx::query_as(
147 "select count(1) from history
148 where user_id = $1",
149 )
150 .bind(user.id)
151 .fetch_one(&self.pool)
152 .await?;
153
154 Ok(res.0)
155 }
156
157 #[instrument(skip_all)]
158 async fn count_history_cached(&self, _user: &User) -> DbResult<i64> {
159 Err(DbError::NotFound)
160 }
161
162 #[instrument(skip_all)]
163 async fn delete_user(&self, u: &User) -> DbResult<()> {
164 sqlx::query("delete from sessions where user_id = $1")
165 .bind(u.id)
166 .execute(&self.pool)
167 .await?;
168
169 sqlx::query("delete from users where id = $1")
170 .bind(u.id)
171 .execute(&self.pool)
172 .await?;
173
174 sqlx::query("delete from history where user_id = $1")
175 .bind(u.id)
176 .execute(&self.pool)
177 .await?;
178
179 Ok(())
180 }
181
182 async fn delete_history(&self, user: &User, id: String) -> DbResult<()> {
183 sqlx::query(
184 "update history
185 set deleted_at = $3
186 where user_id = $1
187 and client_id = $2
188 and deleted_at is null", )
190 .bind(user.id)
191 .bind(id)
192 .bind(time::OffsetDateTime::now_utc())
193 .fetch_all(&self.pool)
194 .await?;
195
196 Ok(())
197 }
198
199 #[instrument(skip_all)]
200 async fn deleted_history(&self, user: &User) -> DbResult<Vec<String>> {
201 let res = sqlx::query(
206 "select client_id from history
207 where user_id = $1
208 and deleted_at is not null",
209 )
210 .bind(user.id)
211 .fetch_all(&self.pool)
212 .await?;
213
214 let res = res.iter().map(|row| row.get("client_id")).collect();
215
216 Ok(res)
217 }
218
219 async fn delete_store(&self, user: &User) -> DbResult<()> {
220 sqlx::query(
221 "delete from store
222 where user_id = $1",
223 )
224 .bind(user.id)
225 .execute(&self.pool)
226 .await?;
227
228 Ok(())
229 }
230
231 #[instrument(skip_all)]
232 async fn add_records(&self, user: &User, records: &[Record<EncryptedData>]) -> DbResult<()> {
233 let mut tx = self.pool.begin().await?;
234
235 for i in records {
236 let id = atuin_common::utils::uuid_v7();
237
238 sqlx::query(
239 "insert into store
240 (id, client_id, host, idx, timestamp, version, tag, data, cek, user_id)
241 values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
242 on conflict do nothing
243 ",
244 )
245 .bind(id)
246 .bind(i.id)
247 .bind(i.host.id)
248 .bind(i.idx as i64)
249 .bind(i.timestamp as i64) .bind(&i.version)
251 .bind(&i.tag)
252 .bind(&i.data.data)
253 .bind(&i.data.content_encryption_key)
254 .bind(user.id)
255 .execute(&mut *tx)
256 .await?;
257 }
258
259 tx.commit().await?;
260
261 Ok(())
262 }
263
264 #[instrument(skip_all)]
265 async fn next_records(
266 &self,
267 user: &User,
268 host: HostId,
269 tag: String,
270 start: Option<RecordIdx>,
271 count: u64,
272 ) -> DbResult<Vec<Record<EncryptedData>>> {
273 tracing::debug!("{:?} - {:?} - {:?}", host, tag, start);
274 let start = start.unwrap_or(0);
275
276 let records: Result<Vec<DbRecord>, DbError> = sqlx::query_as(
277 "select client_id, host, idx, timestamp, version, tag, data, cek from store
278 where user_id = $1
279 and tag = $2
280 and host = $3
281 and idx >= $4
282 order by idx asc
283 limit $5",
284 )
285 .bind(user.id)
286 .bind(tag.clone())
287 .bind(host)
288 .bind(start as i64)
289 .bind(count as i64)
290 .fetch_all(&self.pool)
291 .await
292 .map_err(Into::into);
293
294 let ret = match records {
295 Ok(records) => {
296 let records: Vec<Record<EncryptedData>> = records
297 .into_iter()
298 .map(|f| {
299 let record: Record<EncryptedData> = f.into();
300 record
301 })
302 .collect();
303
304 records
305 }
306 Err(DbError::NotFound) => {
307 tracing::debug!("no records found in store: {:?}/{}", host, tag);
308 return Ok(vec![]);
309 }
310 Err(e) => return Err(e),
311 };
312
313 Ok(ret)
314 }
315
316 async fn status(&self, user: &User) -> DbResult<RecordStatus> {
317 const STATUS_SQL: &str =
318 "select host, tag, max(idx) from store where user_id = $1 group by host, tag";
319
320 let res: Vec<(Uuid, String, i64)> = sqlx::query_as(STATUS_SQL)
321 .bind(user.id)
322 .fetch_all(&self.pool)
323 .await?;
324
325 let mut status = RecordStatus::new();
326
327 for i in res {
328 status.set_raw(HostId(i.0), i.1, i.2 as u64);
329 }
330
331 Ok(status)
332 }
333
334 #[instrument(skip_all)]
335 async fn count_history_range(
336 &self,
337 user: &User,
338 range: std::ops::Range<time::OffsetDateTime>,
339 ) -> DbResult<i64> {
340 let res: (i64,) = sqlx::query_as(
341 "select count(1) from history
342 where user_id = $1
343 and timestamp >= $2::date
344 and timestamp < $3::date",
345 )
346 .bind(user.id)
347 .bind(into_utc(range.start))
348 .bind(into_utc(range.end))
349 .fetch_one(&self.pool)
350 .await?;
351
352 Ok(res.0)
353 }
354
355 #[instrument(skip_all)]
356 async fn list_history(
357 &self,
358 user: &User,
359 created_after: time::OffsetDateTime,
360 since: time::OffsetDateTime,
361 host: &str,
362 page_size: i64,
363 ) -> DbResult<Vec<History>> {
364 let res = sqlx::query_as(
365 "select id, client_id, user_id, hostname, timestamp, data, created_at from history
366 where user_id = $1
367 and hostname != $2
368 and created_at >= $3
369 and timestamp >= $4
370 order by timestamp asc
371 limit $5",
372 )
373 .bind(user.id)
374 .bind(host)
375 .bind(into_utc(created_after))
376 .bind(into_utc(since))
377 .bind(page_size)
378 .fetch(&self.pool)
379 .map_ok(|DbHistory(h)| h)
380 .try_collect()
381 .await?;
382
383 Ok(res)
384 }
385
386 #[instrument(skip_all)]
387 async fn add_history(&self, history: &[NewHistory]) -> DbResult<()> {
388 let mut tx = self.pool.begin().await?;
389
390 for i in history {
391 let client_id: &str = &i.client_id;
392 let hostname: &str = &i.hostname;
393 let data: &str = &i.data;
394
395 sqlx::query(
396 "insert into history
397 (client_id, user_id, hostname, timestamp, data)
398 values ($1, $2, $3, $4, $5)
399 on conflict do nothing
400 ",
401 )
402 .bind(client_id)
403 .bind(i.user_id)
404 .bind(hostname)
405 .bind(i.timestamp)
406 .bind(data)
407 .execute(&mut *tx)
408 .await?;
409 }
410
411 tx.commit().await?;
412
413 Ok(())
414 }
415
416 #[instrument(skip_all)]
417 async fn oldest_history(&self, user: &User) -> DbResult<History> {
418 sqlx::query_as(
419 "select id, client_id, user_id, hostname, timestamp, data, created_at from history
420 where user_id = $1
421 order by timestamp asc
422 limit 1",
423 )
424 .bind(user.id)
425 .fetch_one(&self.pool)
426 .await
427 .map_err(Into::into)
428 .map(|DbHistory(h)| h)
429 }
430}