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) .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}