Skip to main content

atuin_server_sqlite/
lib.rs

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        // The cache is new, and the user might not yet have a cache value.
143        // They will have one as soon as they post up some new history, but handle that
144        // edge case.
145
146        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", // don't just keep setting it
189        )
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        // The cache is new, and the user might not yet have a cache value.
202        // They will have one as soon as they post up some new history, but handle that
203        // edge case.
204
205        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) // throwing away some data, but i64 is still big in terms of time
250            .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}