Skip to main content

prax_sqlite/
engine.rs

1//! SQLite query engine implementing `prax_query::QueryEngine`.
2
3use std::sync::Arc;
4
5use prax_query::error::{QueryError, QueryResult};
6use prax_query::filter::FilterValue;
7use prax_query::row::FromRow;
8use prax_query::traits::{BoxFuture, Model, QueryEngine};
9use rusqlite::types::Value as SqlValue;
10use tokio_rusqlite::Connection as RusqliteConnection;
11use tracing::trace;
12
13use crate::connection::SqliteConnection;
14use crate::pool::SqlitePool;
15use crate::row_ref::SqliteRowRef;
16use crate::types::filter_value_to_sqlite;
17
18/// SQLite query engine backed by `tokio_rusqlite`.
19///
20/// # Breaking changes (0.7)
21///
22/// `SqliteEngine` no longer has inherent `query` / `query_one` / `query_opt`
23/// methods that returned untyped `RowData` / `serde_json::Value`. It now
24/// implements [`prax_query::traits::QueryEngine`], whose row-returning
25/// methods are generic over `T: Model + FromRow` and return typed models.
26///
27/// Migration:
28/// - Replace `engine.query(sql, params)` with
29///   `engine.query_many::<YourType>(sql, params).await?`, where `YourType`
30///   carries `#[derive(prax_orm::Model)]` (which emits both `Model` and
31///   `FromRow`) or hand-written `impl Model + impl FromRow`.
32/// - For ad-hoc typed queries without a full `Model`, bridge through
33///   [`crate::row_ref::SqliteRowRef::from_rusqlite`] inside a custom
34///   `FromRow` impl.
35/// - For the legacy JSON-blob API, use [`crate::raw::SqliteRawEngine`] +
36///   [`crate::raw::SqliteJsonRow`].
37/// - To run side-effecting SQL that returns no rows, call
38///   [`prax_query::traits::QueryEngine::execute_raw`].
39///
40/// See `CHANGELOG.md` for the full migration guide.
41///
42/// # Transaction mode
43///
44/// `SqliteEngine` has two modes, controlled by `tx_conn`:
45///
46/// - **Pool mode** (`tx_conn == None`, default): each query acquires
47///   a fresh [`SqliteConnection`] from the pool and drops it after
48///   the call.
49/// - **Transaction mode** (`tx_conn == Some(..)`): each query routes
50///   through the same [`tokio_rusqlite::Connection`] handle so the
51///   whole BEGIN…COMMIT block serialises onto the connection's
52///   background thread in order. The `Arc<SqliteConnection>` keeps
53///   the pool permit alive for the transaction's lifetime; dropping
54///   the last clone returns the connection to the idle pool.
55#[derive(Clone)]
56pub struct SqliteEngine {
57    pool: SqlitePool,
58    /// Present when this engine is bound to an in-flight transaction.
59    /// `None` in the normal pool-backed case.
60    tx_conn: Option<Arc<SqliteConnection>>,
61}
62
63impl SqliteEngine {
64    /// Create a new engine with the given pool.
65    pub fn new(pool: SqlitePool) -> Self {
66        Self {
67            pool,
68            tx_conn: None,
69        }
70    }
71
72    /// Get a reference to the connection pool.
73    pub fn pool(&self) -> &SqlitePool {
74        &self.pool
75    }
76
77    /// Resolve which raw [`tokio_rusqlite::Connection`] to run the
78    /// query against, along with an optional owned
79    /// [`SqliteConnection`] guard whose `Drop` returns the
80    /// connection to the idle pool once we're done with it.
81    ///
82    /// In tx mode the guard is `None` because the pinned connection
83    /// is already kept alive by the `Arc<SqliteConnection>` on
84    /// `self.tx_conn`. In pool mode we hand back a fresh guard so
85    /// the caller can clone the inner handle, drive one `call(..)`
86    /// through it, and drop the guard on the way out.
87    async fn resolve_conn(&self) -> QueryResult<(RusqliteConnection, Option<SqliteConnection>)> {
88        if let Some(tx) = &self.tx_conn {
89            Ok((tx.inner().clone(), None))
90        } else {
91            let conn = self
92                .pool
93                .get()
94                .await
95                .map_err(|e| QueryError::connection(e.to_string()).with_source(e))?;
96            let handle = conn.inner().clone();
97            Ok((handle, Some(conn)))
98        }
99    }
100
101    fn bind(params: &[FilterValue]) -> Vec<SqlValue> {
102        params.iter().map(filter_value_to_sqlite).collect()
103    }
104
105    /// Decode multiple rows into typed models.
106    ///
107    /// # Short-circuit on decode error
108    ///
109    /// Uses `Result<Vec<T>, _>::collect`, which returns the first decode
110    /// error and discards every successfully-decoded row before it. A
111    /// row-level type mismatch therefore aborts the whole batch rather
112    /// than returning partial results. Callers that want per-row
113    /// recovery should manually iterate rows and handle each result.
114    async fn query_rows<T: Model + FromRow>(
115        &self,
116        sql: String,
117        params: Vec<FilterValue>,
118    ) -> QueryResult<Vec<T>> {
119        trace!(sql = %sql, "sqlite query_rows");
120        let (handle, _guard) = self.resolve_conn().await?;
121        let bound = Self::bind(&params);
122        let snapshots: Vec<SqliteRowRef> = handle
123            .call(move |c| {
124                let mut stmt = c.prepare(&sql)?;
125                let refs: Vec<&dyn rusqlite::ToSql> =
126                    bound.iter().map(|v| v as &dyn rusqlite::ToSql).collect();
127                let mut rows = stmt.query(refs.as_slice())?;
128                let mut out = Vec::new();
129                while let Some(row) = rows.next()? {
130                    out.push(SqliteRowRef::from_rusqlite(row).map_err(|e| {
131                        rusqlite::Error::ToSqlConversionFailure(Box::new(std::io::Error::other(
132                            e.to_string(),
133                        )))
134                    })?);
135                }
136                Ok(out)
137            })
138            .await
139            .map_err(|e| QueryError::database(e.to_string()).with_source(e))?;
140
141        snapshots
142            .into_iter()
143            .map(|r| {
144                T::from_row(&r).map_err(|e| {
145                    let msg = e.to_string();
146                    QueryError::deserialization(msg).with_source(e)
147                })
148            })
149            .collect()
150    }
151
152    /// Stop after the first row so callers that want a single row do not pay
153    /// for materializing the tail. Naively routing `query_one`/`query_optional`
154    /// through `query_rows` + `.pop()` would decode every matching row and
155    /// throw away all but one; a caller who accidentally asked for a single
156    /// row from a million-row table would allocate a million typed models.
157    async fn query_first_row<T: Model + FromRow>(
158        &self,
159        sql: String,
160        params: Vec<FilterValue>,
161    ) -> QueryResult<Option<T>> {
162        trace!(sql = %sql, "sqlite query_first_row");
163        let (handle, _guard) = self.resolve_conn().await?;
164        let bound = Self::bind(&params);
165        let snapshot: Option<SqliteRowRef> = handle
166            .call(move |c| {
167                let mut stmt = c.prepare(&sql)?;
168                let refs: Vec<&dyn rusqlite::ToSql> =
169                    bound.iter().map(|v| v as &dyn rusqlite::ToSql).collect();
170                let mut rows = stmt.query(refs.as_slice())?;
171                match rows.next()? {
172                    Some(row) => Ok(Some(SqliteRowRef::from_rusqlite(row).map_err(|e| {
173                        rusqlite::Error::ToSqlConversionFailure(Box::new(std::io::Error::other(
174                            e.to_string(),
175                        )))
176                    })?)),
177                    None => Ok(None),
178                }
179            })
180            .await
181            .map_err(|e| QueryError::database(e.to_string()).with_source(e))?;
182
183        snapshot
184            .map(|r| {
185                T::from_row(&r).map_err(|e| {
186                    let msg = e.to_string();
187                    QueryError::deserialization(msg).with_source(e)
188                })
189            })
190            .transpose()
191    }
192
193    async fn exec_raw(&self, sql: String, params: Vec<FilterValue>) -> QueryResult<u64> {
194        let (handle, _guard) = self.resolve_conn().await?;
195        let bound = Self::bind(&params);
196        let n = handle
197            .call(move |c| {
198                let refs: Vec<&dyn rusqlite::ToSql> =
199                    bound.iter().map(|v| v as &dyn rusqlite::ToSql).collect();
200                Ok(c.execute(&sql, refs.as_slice())?)
201            })
202            .await
203            .map_err(|e| QueryError::database(e.to_string()).with_source(e))?;
204        Ok(n as u64)
205    }
206
207    async fn count_rows(&self, sql: String, params: Vec<FilterValue>) -> QueryResult<u64> {
208        let (handle, _guard) = self.resolve_conn().await?;
209        let bound = Self::bind(&params);
210        let n = handle
211            .call(move |c| {
212                let refs: Vec<&dyn rusqlite::ToSql> =
213                    bound.iter().map(|v| v as &dyn rusqlite::ToSql).collect();
214                let mut stmt = c.prepare(&sql)?;
215                let n: i64 = stmt.query_row(refs.as_slice(), |r| r.get(0))?;
216                Ok(n)
217            })
218            .await
219            .map_err(|e| QueryError::database(e.to_string()).with_source(e))?;
220        Ok(n as u64)
221    }
222}
223
224impl QueryEngine for SqliteEngine {
225    fn dialect(&self) -> &dyn prax_query::dialect::SqlDialect {
226        &prax_query::dialect::Sqlite
227    }
228
229    fn query_many<T: Model + FromRow + Send + 'static>(
230        &self,
231        sql: &str,
232        params: Vec<FilterValue>,
233    ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
234        let sql = sql.to_string();
235        Box::pin(self.query_rows::<T>(sql, params))
236    }
237
238    fn query_one<T: Model + FromRow + Send + 'static>(
239        &self,
240        sql: &str,
241        params: Vec<FilterValue>,
242    ) -> BoxFuture<'_, QueryResult<T>> {
243        let sql = sql.to_string();
244        Box::pin(async move {
245            self.query_first_row::<T>(sql, params)
246                .await?
247                .ok_or_else(|| QueryError::not_found(T::MODEL_NAME))
248        })
249    }
250
251    fn query_optional<T: Model + FromRow + Send + 'static>(
252        &self,
253        sql: &str,
254        params: Vec<FilterValue>,
255    ) -> BoxFuture<'_, QueryResult<Option<T>>> {
256        let sql = sql.to_string();
257        Box::pin(self.query_first_row::<T>(sql, params))
258    }
259
260    fn execute_insert<T: Model + FromRow + Send + 'static>(
261        &self,
262        sql: &str,
263        params: Vec<FilterValue>,
264    ) -> BoxFuture<'_, QueryResult<T>> {
265        // SQLite 3.35+ supports INSERT ... RETURNING. INSERT RETURNING yields
266        // at most one row per inserted tuple; query_first_row avoids ever
267        // materializing a tail if the caller's SQL yields many (which would
268        // be a misuse, but the engine shouldn't punish it with unbounded
269        // allocation).
270        let sql = sql.to_string();
271        Box::pin(async move {
272            self.query_first_row::<T>(sql, params)
273                .await?
274                .ok_or_else(|| QueryError::not_found(T::MODEL_NAME))
275        })
276    }
277
278    fn execute_update<T: Model + FromRow + Send + 'static>(
279        &self,
280        sql: &str,
281        params: Vec<FilterValue>,
282    ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
283        let sql = sql.to_string();
284        Box::pin(self.query_rows::<T>(sql, params))
285    }
286
287    fn execute_delete(
288        &self,
289        sql: &str,
290        params: Vec<FilterValue>,
291    ) -> BoxFuture<'_, QueryResult<u64>> {
292        let sql = sql.to_string();
293        Box::pin(self.exec_raw(sql, params))
294    }
295
296    fn execute_raw(&self, sql: &str, params: Vec<FilterValue>) -> BoxFuture<'_, QueryResult<u64>> {
297        let sql = sql.to_string();
298        Box::pin(self.exec_raw(sql, params))
299    }
300
301    fn count(&self, sql: &str, params: Vec<FilterValue>) -> BoxFuture<'_, QueryResult<u64>> {
302        let sql = sql.to_string();
303        Box::pin(self.count_rows(sql, params))
304    }
305
306    fn aggregate_query(
307        &self,
308        sql: &str,
309        params: Vec<FilterValue>,
310    ) -> BoxFuture<'_, QueryResult<Vec<std::collections::HashMap<String, FilterValue>>>> {
311        let sql = sql.to_string();
312        Box::pin(async move {
313            trace!(sql = %sql, "sqlite aggregate_query");
314            let (handle, _guard) = self.resolve_conn().await?;
315            let bound = Self::bind(&params);
316            let rows: Vec<std::collections::HashMap<String, FilterValue>> = handle
317                .call(move |c| {
318                    let mut stmt = c.prepare(&sql)?;
319                    let column_names: Vec<String> =
320                        stmt.column_names().iter().map(|s| s.to_string()).collect();
321                    let refs: Vec<&dyn rusqlite::ToSql> =
322                        bound.iter().map(|v| v as &dyn rusqlite::ToSql).collect();
323                    let mut rows = stmt.query(refs.as_slice())?;
324                    let mut out = Vec::new();
325                    while let Some(row) = rows.next()? {
326                        let mut map = std::collections::HashMap::new();
327                        for (i, name) in column_names.iter().enumerate() {
328                            // SQLite storage classes are dynamic per-row:
329                            // INTEGER, REAL, TEXT, BLOB, NULL. Pull each
330                            // cell as an untyped `Value` and project into
331                            // the closest `FilterValue` variant. BLOB
332                            // becomes `Null` — aggregate results never
333                            // return BLOB in practice, and surfacing raw
334                            // bytes through FilterValue doesn't buy
335                            // anything for the caller.
336                            let v: SqlValue = row.get(i).unwrap_or(SqlValue::Null);
337                            let fv = match v {
338                                SqlValue::Null => FilterValue::Null,
339                                SqlValue::Integer(n) => FilterValue::Int(n),
340                                SqlValue::Real(f) => FilterValue::Float(f),
341                                SqlValue::Text(s) => FilterValue::String(s),
342                                SqlValue::Blob(_) => FilterValue::Null,
343                            };
344                            map.insert(name.clone(), fv);
345                        }
346                        out.push(map);
347                    }
348                    Ok(out)
349                })
350                .await
351                .map_err(|e| QueryError::database(e.to_string()).with_source(e))?;
352            Ok(rows)
353        })
354    }
355
356    fn transaction<'a, R, Fut, F>(&'a self, f: F) -> BoxFuture<'a, QueryResult<R>>
357    where
358        F: FnOnce(Self) -> Fut + Send + 'a,
359        Fut: std::future::Future<Output = QueryResult<R>> + Send + 'a,
360        R: Send + 'a,
361        Self: Clone,
362    {
363        Box::pin(async move {
364            // Refuse nested transactions until savepoint support
365            // lands. Callers can still drive SAVEPOINT / RELEASE
366            // manually via `execute_raw` if they need it.
367            if self.tx_conn.is_some() {
368                return Err(QueryError::internal(
369                    "nested transactions not yet implemented \
370                     (call .transaction() on the outer engine only, or \
371                     issue SAVEPOINT via execute_raw)",
372                ));
373            }
374
375            // Pin a single pooled connection. Wrapping it in `Arc`
376            // keeps the pool permit alive for the whole transaction
377            // and makes every engine clone share the same
378            // `tokio_rusqlite::Connection` handle — every query
379            // dispatched through the closure's engine therefore
380            // serialises onto the same background thread as our
381            // initial `BEGIN`.
382            let conn = self
383                .pool
384                .get()
385                .await
386                .map_err(|e| QueryError::connection(e.to_string()).with_source(e))?;
387            let handle = conn.inner().clone();
388            handle
389                .call(|c| {
390                    c.execute_batch("BEGIN")?;
391                    Ok(())
392                })
393                .await
394                .map_err(|e| QueryError::database(e.to_string()).with_source(e))?;
395
396            let tx_conn = Arc::new(conn);
397            let tx_engine = SqliteEngine {
398                pool: self.pool.clone(),
399                tx_conn: Some(tx_conn.clone()),
400            };
401
402            let result = f(tx_engine).await;
403
404            // Finalise: COMMIT on success, best-effort ROLLBACK on
405            // failure. Preserve the caller's error if ROLLBACK fails —
406            // SQLite's autocommit resumes on the next statement
407            // regardless, and dropping the connection releases any
408            // lingering tx state.
409            match result {
410                Ok(v) => {
411                    handle
412                        .call(|c| {
413                            c.execute_batch("COMMIT")?;
414                            Ok(())
415                        })
416                        .await
417                        .map_err(|e| QueryError::database(e.to_string()).with_source(e))?;
418                    Ok(v)
419                }
420                Err(e) => {
421                    let _ = handle
422                        .call(|c| {
423                            c.execute_batch("ROLLBACK")?;
424                            Ok(())
425                        })
426                        .await;
427                    Err(e)
428                }
429            }
430        })
431    }
432}