Skip to main content

atuin_server_sqlite/
lib.rs

1use std::str::FromStr;
2
3use async_trait::async_trait;
4use atuin_domain::record::{
5    EncryptedData, HostId, Record, RecordIdx, RecordSeriesKey, RecordStatus, RecordTag,
6};
7use atuin_server_database::models::{NewSession, NewUser, Session, User};
8use atuin_server_database::{Database, DbError, DbResult, DbSettings};
9use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions};
10use sqlx::types::Uuid;
11use tracing::instrument;
12use wrappers::DbRecord;
13
14mod wrappers;
15
16#[derive(Clone)]
17pub struct Sqlite {
18    pool: sqlx::Pool<sqlx::sqlite::Sqlite>,
19}
20
21#[async_trait]
22impl Database for Sqlite {
23    async fn new(settings: &DbSettings) -> DbResult<Self> {
24        let opts = SqliteConnectOptions::from_str(&settings.db_uri)?
25            .journal_mode(SqliteJournalMode::Wal)
26            .create_if_missing(true);
27
28        let pool = SqlitePoolOptions::new().connect_with(opts).await?;
29
30        sqlx::migrate!("./migrations")
31            .run(&pool)
32            .await
33            .map_err(|error| DbError::Other(error.into()))?;
34
35        Ok(Self { pool })
36    }
37
38    #[instrument(skip_all)]
39    async fn get_session(&self, token: &str) -> DbResult<Session> {
40        sqlx::query_as("select id, user_id, token from sessions where token = $1")
41            .bind(token)
42            .fetch_one(&self.pool)
43            .await
44            .map_err(Into::into)
45    }
46
47    #[instrument(skip_all)]
48    async fn get_session_user(&self, token: &str) -> DbResult<User> {
49        sqlx::query_as(
50            "select users.id, users.username, users.email, users.password from users
51            inner join sessions
52            on users.id = sessions.user_id
53            and sessions.token = $1",
54        )
55        .bind(token)
56        .fetch_one(&self.pool)
57        .await
58        .map_err(Into::into)
59    }
60
61    #[instrument(skip_all)]
62    async fn add_session(&self, session: &NewSession) -> DbResult<()> {
63        let token: &str = &session.token;
64
65        sqlx::query(
66            "insert into sessions
67                (user_id, token)
68            values($1, $2)",
69        )
70        .bind(session.user_id)
71        .bind(token)
72        .execute(&self.pool)
73        .await?;
74
75        Ok(())
76    }
77
78    #[instrument(skip_all)]
79    async fn get_user(&self, username: &str) -> DbResult<User> {
80        sqlx::query_as("select id, username, email, password from users where username = $1")
81            .bind(username)
82            .fetch_one(&self.pool)
83            .await
84            .map_err(Into::into)
85    }
86
87    #[instrument(skip_all)]
88    async fn get_user_session(&self, u: &User) -> DbResult<Session> {
89        sqlx::query_as("select id, user_id, token from sessions where user_id = $1")
90            .bind(u.id)
91            .fetch_one(&self.pool)
92            .await
93            .map_err(Into::into)
94    }
95
96    #[instrument(skip_all)]
97    async fn add_user(&self, user: &NewUser) -> DbResult<i64> {
98        let email: &str = &user.email;
99        let username: &str = &user.username;
100        let password: &str = &user.password;
101
102        let res: (i64,) = sqlx::query_as(
103            "insert into users
104                (username, email, password)
105            values($1, $2, $3)
106            returning id",
107        )
108        .bind(username)
109        .bind(email)
110        .bind(password)
111        .fetch_one(&self.pool)
112        .await?;
113
114        Ok(res.0)
115    }
116
117    #[instrument(skip_all)]
118    async fn update_user_password(&self, user: &User) -> DbResult<()> {
119        sqlx::query(
120            "update users
121            set password = $1
122            where id = $2",
123        )
124        .bind(&user.password)
125        .bind(user.id)
126        .execute(&self.pool)
127        .await?;
128
129        Ok(())
130    }
131
132    #[instrument(skip_all)]
133    async fn delete_user(&self, u: &User) -> DbResult<()> {
134        sqlx::query("delete from sessions where user_id = $1")
135            .bind(u.id)
136            .execute(&self.pool)
137            .await?;
138
139        sqlx::query("delete from users where id = $1").bind(u.id).execute(&self.pool).await?;
140
141        sqlx::query("delete from history where user_id = $1")
142            .bind(u.id)
143            .execute(&self.pool)
144            .await?;
145
146        Ok(())
147    }
148
149    async fn delete_store(&self, user: &User) -> DbResult<()> {
150        sqlx::query(
151            "delete from store
152            where user_id = $1",
153        )
154        .bind(user.id)
155        .execute(&self.pool)
156        .await?;
157
158        Ok(())
159    }
160
161    #[instrument(skip_all)]
162    async fn add_records(&self, user: &User, records: &[Record<EncryptedData>]) -> DbResult<()> {
163        let mut tx = self.pool.begin().await?;
164
165        for i in records {
166            let id = atuin_common::utils::uuid_v7();
167
168            sqlx::query(
169                "insert into store
170                    (id, client_id, host, idx, timestamp, version, tag, data, cek, user_id)
171                values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
172                on conflict do nothing
173                ",
174            )
175            .bind(id)
176            .bind(i.id)
177            .bind(i.host.id)
178            .bind(i.idx as i64)
179            .bind(i.timestamp as i64) // throwing away some data, but i64 is still big in terms of time
180            .bind(i.version.as_str())
181            .bind(i.tag.as_str())
182            .bind(&i.data.raw)
183            .bind(&i.data.cek)
184            .bind(user.id)
185            .execute(&mut *tx)
186            .await?;
187        }
188
189        tx.commit().await?;
190
191        Ok(())
192    }
193
194    #[instrument(skip_all)]
195    async fn next_records(
196        &self,
197        user: &User,
198        series: &RecordSeriesKey,
199        start: Option<RecordIdx>,
200        count: u64,
201    ) -> DbResult<Vec<Record<EncryptedData>>> {
202        tracing::debug!("{:?} - {:?} - {:?}", series.host_id, series.tag, start);
203        let start = start.unwrap_or(0);
204
205        let records: Result<Vec<DbRecord>, DbError> = sqlx::query_as(
206            "select client_id, host, idx, timestamp, version, tag, data, cek from store
207                    where user_id = $1
208                    and tag = $2
209                    and host = $3
210                    and idx >= $4
211                    order by idx asc
212                    limit $5",
213        )
214        .bind(user.id)
215        .bind(series.tag.as_str())
216        .bind(series.host_id)
217        .bind(start as i64)
218        .bind(count as i64)
219        .fetch_all(&self.pool)
220        .await
221        .map_err(Into::into);
222
223        let ret = match records {
224            Ok(records) => {
225                let records: Vec<Record<EncryptedData>> = records
226                    .into_iter()
227                    .map(|f| {
228                        let record: Record<EncryptedData> = f.into();
229                        record
230                    })
231                    .collect();
232
233                records
234            }
235            Err(DbError::NotFound) => {
236                tracing::debug!("no records found in store: {:?}/{}", series.host_id, series.tag);
237                return Ok(vec![]);
238            }
239            Err(e) => return Err(e),
240        };
241
242        Ok(ret)
243    }
244
245    async fn status(&self, user: &User) -> DbResult<RecordStatus> {
246        const STATUS_SQL: &str =
247            "select host, tag, max(idx) from store where user_id = $1 group by host, tag";
248
249        let res: Vec<(Uuid, String, i64)> =
250            sqlx::query_as(STATUS_SQL).bind(user.id).fetch_all(&self.pool).await?;
251
252        let mut status = RecordStatus::new();
253
254        for i in res {
255            status.set_raw(RecordSeriesKey::new(HostId(i.0), RecordTag::from(i.1)), i.2 as u64);
256        }
257
258        Ok(status)
259    }
260}