1use std::sync::Arc;
4use std::time::Duration;
5
6use runlight::BoxFuture;
7use runlight::store::{Db, DbError, Dialect, Hold, Param, Row};
8use sqlx::pool::PoolConnection;
9use sqlx::sqlite::{Sqlite, SqliteArguments};
10use sqlx::{AssertSqlSafe, SqlitePool};
11use tokio::sync::{Mutex, OwnedMutexGuard};
12
13use crate::rows::{db_error, sqlite_row};
14
15const BUSY_RETRY: Duration = Duration::from_secs(2);
17
18#[derive(Default)]
20struct Slot {
21 conn: Option<PoolConnection<Sqlite>>,
22 abandoned: bool,
26}
27
28#[derive(Clone)]
32pub struct SqliteDb {
33 pool: SqlitePool,
34 conn: Arc<Mutex<Slot>>,
35}
36
37impl SqliteDb {
38 pub fn new(pool: SqlitePool) -> SqliteDb {
40 SqliteDb { pool, conn: Arc::new(Mutex::new(Slot::default())) }
41 }
42}
43
44async fn open(pool: &SqlitePool, guard: &mut Slot) -> Result<(), DbError> {
46 if guard.abandoned {
47 if let Some(conn) = guard.conn.as_mut()
48 && let Err(e) = exec(conn, "ROLLBACK").await
49 && !e.to_string().contains("no transaction is active")
50 {
51 if let Some(conn) = guard.conn.take() {
53 drop(conn.detach());
54 }
55 }
56 guard.abandoned = false;
58 }
59 if guard.conn.is_none() {
60 let mut conn = pool.acquire().await.map_err(db_error)?;
61 wal(&mut conn).await.map_err(db_error)?;
64 exec(&mut conn, "PRAGMA synchronous = NORMAL").await.map_err(db_error)?;
65 exec(&mut conn, "PRAGMA secure_delete = FAST").await.map_err(db_error)?;
68 exec(&mut conn, "PRAGMA busy_timeout = 5000").await.map_err(db_error)?;
69 guard.conn = Some(conn);
70 }
71 Ok(())
72}
73
74fn arguments(params: Vec<Param>) -> Result<SqliteArguments, DbError> {
75 use sqlx::Arguments;
76 let mut args = SqliteArguments::default();
77 for p in params {
78 match p {
79 Param::Null => args.add(Option::<i64>::None),
80 Param::Int(n) => args.add(n),
81 Param::Float(f) => args.add(f),
82 Param::Text(s) => args.add(s),
83 }
84 .map_err(db_error)?;
85 }
86 Ok(args)
87}
88
89fn broken(err: &sqlx::Error) -> bool {
90 matches!(err, sqlx::Error::Io(_) | sqlx::Error::PoolClosed | sqlx::Error::WorkerCrashed)
91}
92
93async fn all_on(pool: &SqlitePool, slot: &mut Slot, sql: &str, params: Vec<Param>) -> Result<Vec<Row>, DbError> {
94 open(pool, slot).await?;
95 let conn = slot.conn.as_mut().expect("an open connection");
96 let result = sqlx::query_with(AssertSqlSafe(sql.to_string()), arguments(params)?).fetch_all(&mut **conn).await;
97 if let Err(e) = &result
98 && broken(e)
99 {
100 slot.conn = None;
101 }
102 result.map_err(db_error)?.iter().map(sqlite_row).collect()
103}
104
105async fn run_on(pool: &SqlitePool, slot: &mut Slot, sql: &str, params: Vec<Param>) -> Result<u64, DbError> {
106 open(pool, slot).await?;
107 let conn = slot.conn.as_mut().expect("an open connection");
108 let result = sqlx::query_with(AssertSqlSafe(sql.to_string()), arguments(params)?).execute(&mut **conn).await;
109 if let Err(e) = &result
110 && broken(e)
111 {
112 slot.conn = None;
113 }
114 Ok(result.map_err(db_error)?.rows_affected())
115}
116
117impl Db for SqliteDb {
118 fn dialect(&self) -> Dialect {
119 Dialect::Sqlite
120 }
121
122 fn all<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<Vec<Row>, DbError>> {
123 Box::pin(async move {
124 let mut slot = self.conn.lock().await;
125 all_on(&self.pool, &mut slot, sql, params).await
126 })
127 }
128
129 fn run<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<u64, DbError>> {
130 Box::pin(async move {
131 let mut slot = self.conn.lock().await;
132 run_on(&self.pool, &mut slot, sql, params).await
133 })
134 }
135
136 fn hold(&self, kind: Hold) -> BoxFuture<'_, Result<Arc<dyn Db>, DbError>> {
137 Box::pin(async move {
138 if kind == Hold::Exclusive {
140 return Ok(Arc::new(self.clone()) as Arc<dyn Db>);
141 }
142 let mut tx = Tx { slot: self.conn.clone().lock_owned().await, open: true };
144 run_on(&self.pool, &mut tx.slot, "BEGIN", vec![]).await?;
145 Ok(Arc::new(Held { pool: self.pool.clone(), guard: Arc::new(Mutex::new(Some(tx))), owner: true })
146 as Arc<dyn Db>)
147 })
148 }
149}
150
151struct Tx {
154 slot: OwnedMutexGuard<Slot>,
155 open: bool,
156}
157
158impl Drop for Tx {
159 fn drop(&mut self) {
160 if self.open {
161 self.slot.abandoned = true;
162 }
163 }
164}
165
166struct Held {
169 pool: SqlitePool,
170 guard: Arc<Mutex<Option<Tx>>>,
171 owner: bool,
172}
173
174impl Drop for Held {
175 fn drop(&mut self) {
176 if self.owner
179 && let Ok(mut held) = self.guard.try_lock()
180 {
181 held.take();
182 }
183 }
184}
185
186fn ended() -> DbError {
187 DbError("Runlight: the transaction has ended".into())
188}
189
190impl Db for Held {
191 fn dialect(&self) -> Dialect {
192 Dialect::Sqlite
193 }
194
195 fn all<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<Vec<Row>, DbError>> {
196 Box::pin(async move {
197 let mut held = self.guard.lock().await;
198 let slot = &mut held.as_mut().ok_or_else(ended)?.slot;
199 all_on(&self.pool, slot, sql, params).await
200 })
201 }
202
203 fn run<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<u64, DbError>> {
204 Box::pin(async move {
205 let mut held = self.guard.lock().await;
206 let slot = &mut held.as_mut().ok_or_else(ended)?.slot;
207 run_on(&self.pool, slot, sql, params).await
208 })
209 }
210
211 fn hold(&self, _kind: Hold) -> BoxFuture<'_, Result<Arc<dyn Db>, DbError>> {
212 Box::pin(async move {
214 Ok(Arc::new(Held { pool: self.pool.clone(), guard: self.guard.clone(), owner: false }) as Arc<dyn Db>)
215 })
216 }
217
218 fn finish(&self, ok: bool) -> BoxFuture<'_, Result<(), DbError>> {
219 Box::pin(async move {
220 if !self.owner {
221 return Ok(());
222 }
223 let mut held = self.guard.lock().await;
224 let Some(mut tx) = held.take() else { return Ok(()) };
225 let result = run_on(&self.pool, &mut tx.slot, if ok { "COMMIT" } else { "ROLLBACK" }, vec![]).await;
226 tx.open = result.is_err();
229 result.map(|_| ())
230 })
231 }
232}
233
234async fn exec(conn: &mut PoolConnection<Sqlite>, text: &'static str) -> Result<(), sqlx::Error> {
236 sqlx::query(text).fetch_all(&mut **conn).await.map(|_| ())
237}
238
239fn is_busy(err: &sqlx::Error) -> bool {
240 let text = err.to_string();
241 let code = match err {
242 sqlx::Error::Database(db) => db.code().map(|c| c.to_string()).unwrap_or_default(),
243 _ => String::new(),
244 };
245 let busy_code = code.parse::<i64>().is_ok_and(|c| c & 0xff == 5 || c & 0xff == 6);
247 busy_code || text.contains("SQLITE_BUSY") || text.contains("database is locked")
248}
249
250pub(crate) fn own_file(path: &std::path::Path) {
255 if path.exists() {
256 return;
257 }
258 let Ok(file) = std::fs::OpenOptions::new().write(true).create_new(true).open(path) else { return };
260 #[cfg(unix)]
261 {
262 use std::os::unix::fs::PermissionsExt;
263 let _ = file.set_permissions(std::fs::Permissions::from_mode(0o660));
265 }
266 drop(file);
267}
268
269async fn wal(conn: &mut PoolConnection<Sqlite>) -> Result<(), sqlx::Error> {
271 let mut waited = Duration::ZERO;
272 let mut attempt = 0u32;
273 loop {
274 match exec(conn, "PRAGMA journal_mode = WAL").await {
275 Err(err) if is_busy(&err) && waited < BUSY_RETRY => {
276 let pause = Duration::from_millis(10u64 << attempt.min(10))
277 .min(Duration::from_millis(200))
278 .min(BUSY_RETRY - waited);
279 tokio::time::sleep(pause).await;
280 waited += pause;
281 attempt += 1;
282 }
283 other => return other,
284 }
285 }
286}