Skip to main content

dinoco_engine/databases/sqlite/
mod.rs

1use async_trait::async_trait;
2
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6use std::time::Instant;
7
8use deadpool_sqlite::{Config, Pool, Runtime};
9use rusqlite::types::{ToSqlOutput, Value};
10
11mod dialect;
12mod handler;
13mod migration;
14mod row;
15
16use crate::{
17    ConstraintError, DinocoAdapter, DinocoClientConfig, DinocoError, DinocoQueryLog, DinocoQueryLogger, DinocoResult,
18    DinocoRow, DinocoTransactionAdapter, DinocoValue, ExecutionResult,
19};
20
21pub use dialect::SqliteDialect;
22
23static SQLITE_DIALECT: SqliteDialect = SqliteDialect;
24tokio::task_local! {
25    static SQLITE_TX_CONNECTION: Arc<tokio::sync::Mutex<deadpool_sqlite::Object>>;
26}
27
28#[derive(Clone)]
29pub struct SqliteAdapter {
30    pub url: String,
31    pub pool: Arc<Pool>,
32    pub query_logger: DinocoQueryLogger,
33}
34
35#[async_trait]
36impl DinocoAdapter for SqliteAdapter {
37    type Dialect = SqliteDialect;
38
39    fn dialect(&self) -> &Self::Dialect {
40        &SQLITE_DIALECT
41    }
42
43    async fn connect(url: String, config: DinocoClientConfig) -> DinocoResult<Self> {
44        if !url.starts_with("file:") {
45            return Err(DinocoError::ConnectionError(
46                "Invalid sqlite connection URL. Expected format: file:path".to_string(),
47            ));
48        }
49
50        let cfg = Config::new(&url);
51        let pool = cfg.create_pool(Runtime::Tokio1).map_err(DinocoError::from)?;
52
53        Ok(Self { url, pool: Arc::new(pool), query_logger: config.query_logger })
54    }
55
56    async fn execute_result(&self, query: &str, params: &[DinocoValue]) -> DinocoResult<ExecutionResult> {
57        if let Ok(tx_conn) = SQLITE_TX_CONNECTION.try_with(Clone::clone) {
58            let conn = tx_conn.lock().await;
59
60            return execute_result_with_connection(&conn, query, params, &self.query_logger).await;
61        }
62
63        let conn = self.pool.get().await.map_err(DinocoError::from)?;
64
65        execute_result_with_connection(&conn, query, params, &self.query_logger).await
66    }
67
68    async fn execute_script(&self, sql_content: &str) -> DinocoResult<()> {
69        for statement in sql_content.split(';') {
70            let clean_statement = statement.trim();
71
72            if clean_statement.is_empty() {
73                continue;
74            }
75
76            self.execute(clean_statement, &[]).await?;
77        }
78
79        Ok(())
80    }
81
82    async fn query_as<T: DinocoRow + Send + 'static>(
83        &self,
84        query: &str,
85        params: &[DinocoValue],
86    ) -> DinocoResult<Vec<T>> {
87        if let Ok(tx_conn) = SQLITE_TX_CONNECTION.try_with(Clone::clone) {
88            let conn = tx_conn.lock().await;
89
90            return query_as_with_connection::<T>(&conn, query, params, &self.query_logger).await;
91        }
92
93        let conn = self.pool.get().await.map_err(DinocoError::from)?;
94
95        query_as_with_connection::<T>(&conn, query, params, &self.query_logger).await
96    }
97}
98
99impl DinocoTransactionAdapter for SqliteAdapter {
100    fn with_transaction<'a, T, F>(&'a self, operation: F) -> Pin<Box<dyn Future<Output = DinocoResult<T>> + Send + 'a>>
101    where
102        T: Send + 'a,
103        F: FnOnce() -> Pin<Box<dyn Future<Output = DinocoResult<T>> + Send + 'a>> + Send + 'a,
104    {
105        Box::pin(async move {
106            if SQLITE_TX_CONNECTION.try_with(|_| ()).is_ok() {
107                return operation().await;
108            }
109
110            let connection = self.pool.get().await.map_err(DinocoError::from)?;
111            let tx_connection = Arc::new(tokio::sync::Mutex::new(connection));
112
113            {
114                let conn = tx_connection.lock().await;
115                conn.interact(|conn| conn.execute("BEGIN", []))
116                    .await
117                    .map_err(DinocoError::from)?
118                    .map_err(DinocoError::from)?;
119            }
120
121            let result = SQLITE_TX_CONNECTION.scope(tx_connection.clone(), async move { operation().await }).await;
122
123            match result {
124                Ok(output) => {
125                    let conn = tx_connection.lock().await;
126                    conn.interact(|conn| conn.execute("COMMIT", []))
127                        .await
128                        .map_err(DinocoError::from)?
129                        .map_err(DinocoError::from)?;
130
131                    Ok(output)
132                }
133                Err(error) => {
134                    let conn = tx_connection.lock().await;
135                    let _ = conn.interact(|conn| conn.execute("ROLLBACK", [])).await;
136
137                    Err(error)
138                }
139            }
140        })
141    }
142}
143
144async fn execute_result_with_connection(
145    conn: &deadpool_sqlite::Object,
146    query: &str,
147    params: &[DinocoValue],
148    query_logger: &DinocoQueryLogger,
149) -> DinocoResult<ExecutionResult> {
150    let query_owned = query.to_string();
151    let params_owned = params.to_vec();
152    let logged_query = query.to_string();
153    let logged_params = params.to_vec();
154    let started_at = Instant::now();
155
156    let affected_rows = conn
157        .interact(move |conn| {
158            let params_refs: Vec<&dyn rusqlite::ToSql> =
159                params_owned.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
160
161            conn.execute(&query_owned, params_refs.as_slice())
162                .map(|affected_rows| (affected_rows, conn.last_insert_rowid()))
163        })
164        .await
165        .map_err(DinocoError::from)?
166        .map_err(DinocoError::from)?;
167
168    query_logger.log(DinocoQueryLog {
169        adapter: "sqlite",
170        duration: started_at.elapsed(),
171        params: logged_params,
172        query: logged_query,
173    });
174
175    Ok(ExecutionResult { affected_rows: affected_rows.0 as u64, last_insert_id: Some(affected_rows.1) })
176}
177
178async fn query_as_with_connection<T: DinocoRow + Send + 'static>(
179    conn: &deadpool_sqlite::Object,
180    query: &str,
181    params: &[DinocoValue],
182    query_logger: &DinocoQueryLogger,
183) -> DinocoResult<Vec<T>> {
184    let query_owned = query.to_string();
185    let params_owned = params.to_vec();
186    let logged_query = query.to_string();
187    let logged_params = params.to_vec();
188    let started_at = Instant::now();
189
190    let results = conn
191        .interact(move |conn| -> DinocoResult<Vec<T>> {
192            let mut stmt = conn.prepare(&query_owned).map_err(DinocoError::from)?;
193            let params_refs: Vec<&dyn rusqlite::ToSql> =
194                params_owned.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
195
196            let mut rows = stmt.query(params_refs.as_slice()).map_err(DinocoError::from)?;
197            let mut results = Vec::new();
198
199            while let Some(row) = rows.next().map_err(DinocoError::from)? {
200                results.push(T::from_row(row)?);
201            }
202
203            Ok(results)
204        })
205        .await
206        .map_err(DinocoError::from)??;
207
208    query_logger.log(DinocoQueryLog {
209        adapter: "sqlite",
210        duration: started_at.elapsed(),
211        params: logged_params,
212        query: logged_query,
213    });
214
215    Ok(results)
216}
217
218impl rusqlite::ToSql for DinocoValue {
219    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
220        match self {
221            DinocoValue::Null => Ok(ToSqlOutput::Owned(Value::Null)),
222            DinocoValue::Integer(i) => Ok(ToSqlOutput::Owned(Value::Integer(*i))),
223            DinocoValue::Float(f) => Ok(ToSqlOutput::Owned(Value::Real(*f))),
224            DinocoValue::Boolean(b) => Ok(ToSqlOutput::Owned(Value::Integer(if *b { 1 } else { 0 }))),
225            DinocoValue::String(s) => Ok(ToSqlOutput::Owned(Value::Text(s.clone()))),
226            DinocoValue::Enum(_, s) => Ok(ToSqlOutput::Owned(Value::Text(s.clone()))),
227            DinocoValue::Json(v) => Ok(ToSqlOutput::Owned(Value::Text(v.to_string()))),
228            DinocoValue::Bytes(v) => Ok(ToSqlOutput::Owned(Value::Blob(v.clone()))),
229            DinocoValue::DateTime(dt) => Ok(ToSqlOutput::Owned(Value::Text(dt.to_string()))),
230            DinocoValue::Date(date) => Ok(ToSqlOutput::Owned(Value::Text(date.to_string()))),
231        }
232    }
233}
234
235impl From<deadpool_sqlite::CreatePoolError> for DinocoError {
236    fn from(e: deadpool_sqlite::CreatePoolError) -> Self {
237        Self::ConnectionError(format!("Failed to get connection from pool: {}", e))
238    }
239}
240
241impl From<deadpool_sqlite::PoolError> for DinocoError {
242    fn from(e: deadpool_sqlite::PoolError) -> Self {
243        Self::ConnectionError(format!("Failed to get connection from pool: {}", e))
244    }
245}
246
247impl From<deadpool_sqlite::BuildError> for DinocoError {
248    fn from(e: deadpool_sqlite::BuildError) -> Self {
249        Self::ConnectionError(format!("Failed to build connection pool: {}", e))
250    }
251}
252
253impl From<deadpool_sqlite::InteractError> for DinocoError {
254    fn from(e: deadpool_sqlite::InteractError) -> Self {
255        Self::ParseError(e.to_string())
256    }
257}
258
259impl From<rusqlite::Error> for DinocoError {
260    fn from(e: rusqlite::Error) -> Self {
261        if let Some(error) = map_sqlite_constraint_error(&e) {
262            return Self::Constraint(error);
263        }
264
265        Self::Sqlite(e)
266    }
267}
268
269fn map_sqlite_constraint_error(error: &rusqlite::Error) -> Option<ConstraintError> {
270    let rusqlite::Error::SqliteFailure(_, message) = error else {
271        return None;
272    };
273    let message = message.clone()?;
274    let normalized = message.to_ascii_lowercase();
275
276    if normalized.starts_with("unique constraint failed:") {
277        let targets = parse_sqlite_constraint_targets(&message, "UNIQUE constraint failed:");
278        let table = targets.first().and_then(|target| target.split('.').next()).map(str::to_string);
279        let columns =
280            targets.into_iter().map(|target| target.split('.').nth(1).unwrap_or(target.as_str()).to_string()).collect();
281
282        return Some(ConstraintError::unique(table, columns, None, message));
283    }
284
285    if normalized.starts_with("not null constraint failed:") {
286        let targets = parse_sqlite_constraint_targets(&message, "NOT NULL constraint failed:");
287        let table = targets.first().and_then(|target| target.split('.').next()).map(str::to_string);
288        let columns =
289            targets.into_iter().map(|target| target.split('.').nth(1).unwrap_or(target.as_str()).to_string()).collect();
290
291        return Some(ConstraintError::not_null(table, columns, None, message));
292    }
293
294    if normalized.starts_with("foreign key constraint failed") {
295        return Some(ConstraintError::foreign_key(None, Vec::new(), None, message));
296    }
297
298    if normalized.starts_with("check constraint failed:") {
299        let constraint =
300            message.split_once(':').map(|(_, rest)| rest.trim().to_string()).filter(|item| !item.is_empty());
301
302        return Some(ConstraintError::check(None, Vec::new(), constraint, message));
303    }
304
305    None
306}
307
308fn parse_sqlite_constraint_targets(message: &str, prefix: &str) -> Vec<String> {
309    message
310        .strip_prefix(prefix)
311        .unwrap_or(message)
312        .split(',')
313        .map(str::trim)
314        .filter(|item| !item.is_empty())
315        .map(str::to_string)
316        .collect()
317}