sql_support/conn_ext.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use rusqlite::{
6 self,
7 types::{FromSql, ToSql},
8 Connection, Params, Result as SqlResult, Row, Savepoint, Transaction, TransactionBehavior,
9};
10use std::iter::FromIterator;
11use std::ops::Deref;
12use std::time::Instant;
13
14use crate::maybe_cached::MaybeCached;
15use crate::{debug, warn};
16
17/// This trait exists so that we can use these helpers on `rusqlite::{Transaction, Connection}`.
18/// Note that you must import ConnExt in order to call these methods on anything.
19pub trait ConnExt {
20 /// The method you need to implement to opt in to all of this.
21 fn conn(&self) -> &Connection;
22
23 /// Set the value of the pragma on the main database. Returns the same object, for chaining.
24 fn set_pragma<T>(&self, pragma_name: &str, pragma_value: T) -> SqlResult<&Self>
25 where
26 T: ToSql,
27 Self: Sized,
28 {
29 // None == Schema name, e.g. `PRAGMA some_attached_db.something = blah`
30 self.conn()
31 .pragma_update(None, pragma_name, &pragma_value)?;
32 Ok(self)
33 }
34
35 /// Get a cached or uncached statement based on a flag.
36 fn prepare_maybe_cached<'conn>(
37 &'conn self,
38 sql: &str,
39 cache: bool,
40 ) -> SqlResult<MaybeCached<'conn>> {
41 MaybeCached::prepare(self.conn(), sql, cache)
42 }
43
44 /// Execute all the provided statements.
45 fn execute_all(&self, stmts: &[&str]) -> SqlResult<()> {
46 let conn = self.conn();
47 for sql in stmts {
48 let r = conn.execute(sql, []);
49 match r {
50 Ok(_) => {}
51 // Ignore ExecuteReturnedResults error because they're pointless
52 // and annoying.
53 Err(rusqlite::Error::ExecuteReturnedResults) => {}
54 Err(e) => return Err(e),
55 }
56 }
57 Ok(())
58 }
59
60 /// Execute a single statement.
61 fn execute_one(&self, stmt: &str) -> SqlResult<()> {
62 self.execute_all(&[stmt])
63 }
64
65 /// Equivalent to `Connection::execute` but caches the statement so that subsequent
66 /// calls to `execute_cached` will have improved performance.
67 fn execute_cached<P: Params>(&self, sql: &str, params: P) -> SqlResult<usize> {
68 let mut stmt = self.conn().prepare_cached(sql)?;
69 stmt.execute(params)
70 }
71
72 /// Execute a query that returns a single result column, and return that result.
73 /// NOTE: rusqlite now has a builtin `query_one` (with not quite these semantics)
74 /// and a `one_column` (with these semantics but subtly different args) which should
75 /// generally be preferred. We've kept this to make upgrading easier.
76 fn conn_ext_query_one<T: FromSql>(&self, sql: &str) -> SqlResult<T> {
77 let res: T = self.conn().query_row_and_then(sql, [], |row| row.get(0))?;
78 Ok(res)
79 }
80
81 /// Return true if a query returns any rows
82 fn exists<P: Params>(&self, sql: &str, params: P) -> SqlResult<bool> {
83 let conn = self.conn();
84 let mut stmt = conn.prepare(sql)?;
85 let exists = stmt.query(params)?.next()?.is_some();
86 Ok(exists)
87 }
88
89 /// Execute a query that returns 0 or 1 result columns, returning None
90 /// if there were no rows, or if the only result was NULL.
91 fn try_query_one<T: FromSql, P: Params>(
92 &self,
93 sql: &str,
94 params: P,
95 cache: bool,
96 ) -> SqlResult<Option<T>>
97 where
98 Self: Sized,
99 {
100 use rusqlite::OptionalExtension;
101 // The outer option is if we got rows, the inner option is
102 // if the first row was null.
103 let res: Option<Option<T>> = self
104 .conn()
105 .query_row_and_then_cachable(sql, params, |row| row.get(0), cache)
106 .optional()?;
107 // go from Option<Option<T>> to Option<T>
108 Ok(res.unwrap_or_default())
109 }
110
111 /// Equivalent to `rusqlite::Connection::query_row_and_then` but allows
112 /// passing a flag to indicate that it's cached.
113 fn query_row_and_then_cachable<T, E, P, F>(
114 &self,
115 sql: &str,
116 params: P,
117 mapper: F,
118 cache: bool,
119 ) -> Result<T, E>
120 where
121 Self: Sized,
122 P: Params,
123 E: From<rusqlite::Error>,
124 F: FnOnce(&Row<'_>) -> Result<T, E>,
125 {
126 Ok(self
127 .try_query_row(sql, params, mapper, cache)?
128 .ok_or(rusqlite::Error::QueryReturnedNoRows)?)
129 }
130
131 /// Helper for when you'd like to get a `Vec<T>` of all the rows returned by a
132 /// query that takes named arguments. See also
133 /// `query_rows_and_then_cached`.
134 fn query_rows_and_then<T, E, P, F>(&self, sql: &str, params: P, mapper: F) -> Result<Vec<T>, E>
135 where
136 Self: Sized,
137 P: Params,
138 E: From<rusqlite::Error>,
139 F: FnMut(&Row<'_>) -> Result<T, E>,
140 {
141 query_rows_and_then_cachable(self.conn(), sql, params, mapper, false)
142 }
143
144 /// Helper for when you'd like to get a `Vec<T>` of all the rows returned by a
145 /// query that takes named arguments.
146 fn query_rows_and_then_cached<T, E, P, F>(
147 &self,
148 sql: &str,
149 params: P,
150 mapper: F,
151 ) -> Result<Vec<T>, E>
152 where
153 Self: Sized,
154 P: Params,
155 E: From<rusqlite::Error>,
156 F: FnMut(&Row<'_>) -> Result<T, E>,
157 {
158 query_rows_and_then_cachable(self.conn(), sql, params, mapper, true)
159 }
160
161 /// Like `query_rows_and_then_cachable`, but works if you want a non-Vec as a result.
162 /// # Example:
163 /// ```rust,no_run
164 /// # use std::collections::HashSet;
165 /// # use sql_support::ConnExt;
166 /// # use rusqlite::Connection;
167 /// fn get_visit_tombstones(conn: &Connection, id: i64) -> rusqlite::Result<HashSet<i64>> {
168 /// Ok(conn.query_rows_into(
169 /// "SELECT visit_date FROM moz_historyvisit_tombstones
170 /// WHERE place_id = :place_id",
171 /// &[(":place_id", &id)],
172 /// |row| row.get::<_, i64>(0))?)
173 /// }
174 /// ```
175 /// Note if the type isn't inferred, you'll have to do something gross like
176 /// `conn.query_rows_into::<HashSet<_>, _, _, _>(...)`.
177 fn query_rows_into<Coll, T, E, P, F>(&self, sql: &str, params: P, mapper: F) -> Result<Coll, E>
178 where
179 Self: Sized,
180 E: From<rusqlite::Error>,
181 F: FnMut(&Row<'_>) -> Result<T, E>,
182 Coll: FromIterator<T>,
183 P: Params,
184 {
185 query_rows_and_then_cachable(self.conn(), sql, params, mapper, false)
186 }
187
188 /// Same as `query_rows_into`, but caches the stmt if possible.
189 fn query_rows_into_cached<Coll, T, E, P, F>(
190 &self,
191 sql: &str,
192 params: P,
193 mapper: F,
194 ) -> Result<Coll, E>
195 where
196 Self: Sized,
197 P: Params,
198 E: From<rusqlite::Error>,
199 F: FnMut(&Row<'_>) -> Result<T, E>,
200 Coll: FromIterator<T>,
201 {
202 query_rows_and_then_cachable(self.conn(), sql, params, mapper, true)
203 }
204
205 // This should probably have a longer name...
206 /// Like `query_row_and_then_cacheable` but returns None instead of erroring
207 /// if no such row exists.
208 fn try_query_row<T, E, P, F>(
209 &self,
210 sql: &str,
211 params: P,
212 mapper: F,
213 cache: bool,
214 ) -> Result<Option<T>, E>
215 where
216 Self: Sized,
217 P: Params,
218 E: From<rusqlite::Error>,
219 F: FnOnce(&Row<'_>) -> Result<T, E>,
220 {
221 let conn = self.conn();
222 let mut stmt = MaybeCached::prepare(conn, sql, cache)?;
223 let mut rows = stmt.query(params)?;
224 rows.next()?.map(mapper).transpose()
225 }
226
227 /// Caveat: This won't actually get used most of the time, and calls will
228 /// usually invoke rusqlite's method with the same name. See comment on
229 /// `UncheckedTransaction` for details (generally you probably don't need to
230 /// care)
231 fn unchecked_transaction(&self) -> SqlResult<UncheckedTransaction<'_>> {
232 UncheckedTransaction::new(self.conn(), TransactionBehavior::Deferred)
233 }
234
235 /// Begin `unchecked_transaction` with `TransactionBehavior::Immediate`. Use
236 /// when the first operation will be a read operation, that further writes
237 /// depend on for correctness.
238 fn unchecked_transaction_imm(&self) -> SqlResult<UncheckedTransaction<'_>> {
239 UncheckedTransaction::new(self.conn(), TransactionBehavior::Immediate)
240 }
241
242 /// Get the DB size in bytes
243 fn get_db_size(&self) -> Result<u32, rusqlite::Error> {
244 let page_count: u32 = self.conn_ext_query_one("SELECT * from pragma_page_count()")?;
245 let page_size: u32 = self.conn_ext_query_one("SELECT * from pragma_page_size()")?;
246 let freelist_count: u32 =
247 self.conn_ext_query_one("SELECT * from pragma_freelist_count()")?;
248
249 Ok((page_count - freelist_count) * page_size)
250 }
251}
252
253impl ConnExt for Connection {
254 #[inline]
255 fn conn(&self) -> &Connection {
256 self
257 }
258}
259
260impl ConnExt for Transaction<'_> {
261 #[inline]
262 fn conn(&self) -> &Connection {
263 self
264 }
265}
266
267impl ConnExt for Savepoint<'_> {
268 #[inline]
269 fn conn(&self) -> &Connection {
270 self
271 }
272}
273
274/// rusqlite, in an attempt to save us from ourselves, needs a mutable ref to a
275/// connection to start a transaction. That is a bit of a PITA in some cases, so
276/// we offer this as an alternative - but the responsibility of ensuring there
277/// are no concurrent transactions is on our head.
278///
279/// This is very similar to the rusqlite `Transaction` - it doesn't prevent
280/// against nested transactions but does allow you to use an immutable
281/// `Connection`.
282///
283/// FIXME: This currently won't actually be used most of the time, because
284/// `rusqlite` added [`Connection::unchecked_transaction`] (and
285/// `Transaction::new_unchecked`, which can be used to reimplement
286/// `unchecked_transaction_imm`), which will be preferred in a call to
287/// `c.unchecked_transaction()`, because inherent methods have precedence over
288/// methods on extension traits. The exception here is that this will still be
289/// used by code which takes `&impl ConnExt` (I believe it would also be used if
290/// you attempted to call `unchecked_transaction()` on a non-Connection that
291/// implements ConnExt, such as a `Safepoint`, `UncheckedTransaction`, or
292/// `Transaction` itself, but such code is clearly broken, so is not worth
293/// considering).
294///
295/// The difference is that `rusqlite`'s version returns a normal
296/// `rusqlite::Transaction`, rather than the `UncheckedTransaction` from this
297/// crate. Aside from type's name and location (and the fact that `rusqlite`'s
298/// detects slightly more misuse at compile time, and has more features), the
299/// main difference is: `rusqlite`'s does not track when a transaction began,
300/// which unfortunately seems to be used by the coop-transaction management in
301/// places in some fashion.
302///
303/// There are at least two options for how to fix this:
304/// 1. Decide we don't need this version, and delete it, and moving the
305/// transaction timing into the coop-transaction code directly (or something
306/// like this).
307/// 2. Decide this difference *is* important, and rename
308/// `ConnExt::unchecked_transaction` to something like
309/// `ConnExt::transaction_unchecked`.
310pub struct UncheckedTransaction<'conn> {
311 pub conn: &'conn Connection,
312 pub started_at: Instant,
313 pub finished: bool,
314 // we could add drop_behavior etc too, but we don't need it yet - we
315 // always rollback.
316}
317
318impl<'conn> UncheckedTransaction<'conn> {
319 /// Begin a new unchecked transaction. Cannot be nested, but this is not
320 /// enforced by Rust (hence 'unchecked') - however, it is enforced by
321 /// SQLite; use a rusqlite `savepoint` for nested transactions.
322 pub fn new(conn: &'conn Connection, behavior: TransactionBehavior) -> SqlResult<Self> {
323 let query = match behavior {
324 TransactionBehavior::Deferred => "BEGIN DEFERRED",
325 TransactionBehavior::Immediate => "BEGIN IMMEDIATE",
326 TransactionBehavior::Exclusive => "BEGIN EXCLUSIVE",
327 _ => unreachable!(),
328 };
329 conn.execute_batch(query)
330 .map(move |_| UncheckedTransaction {
331 conn,
332 started_at: Instant::now(),
333 finished: false,
334 })
335 }
336
337 /// Consumes and commits an unchecked transaction.
338 pub fn commit(mut self) -> SqlResult<()> {
339 if self.finished {
340 warn!("ignoring request to commit an already finished transaction");
341 return Ok(());
342 }
343 self.finished = true;
344 self.conn.execute_batch("COMMIT")?;
345 debug!("Transaction commited after {:?}", self.started_at.elapsed());
346 Ok(())
347 }
348
349 /// Consumes and rolls back an unchecked transaction.
350 pub fn rollback(mut self) -> SqlResult<()> {
351 if self.finished {
352 warn!("ignoring request to rollback an already finished transaction");
353 return Ok(());
354 }
355 self.rollback_()
356 }
357
358 fn rollback_(&mut self) -> SqlResult<()> {
359 self.finished = true;
360 self.conn.execute_batch("ROLLBACK")?;
361 Ok(())
362 }
363
364 fn finish_(&mut self) -> SqlResult<()> {
365 if self.finished || self.conn.is_autocommit() {
366 return Ok(());
367 }
368 self.rollback_()?;
369 Ok(())
370 }
371}
372
373impl Deref for UncheckedTransaction<'_> {
374 type Target = Connection;
375
376 #[inline]
377 fn deref(&self) -> &Connection {
378 self.conn
379 }
380}
381
382impl Drop for UncheckedTransaction<'_> {
383 fn drop(&mut self) {
384 if let Err(e) = self.finish_() {
385 warn!("Error dropping an unchecked transaction: {}", e);
386 }
387 }
388}
389
390impl ConnExt for UncheckedTransaction<'_> {
391 #[inline]
392 fn conn(&self) -> &Connection {
393 self
394 }
395}
396
397fn query_rows_and_then_cachable<Coll, T, E, P, F>(
398 conn: &Connection,
399 sql: &str,
400 params: P,
401 mapper: F,
402 cache: bool,
403) -> Result<Coll, E>
404where
405 E: From<rusqlite::Error>,
406 F: FnMut(&Row<'_>) -> Result<T, E>,
407 Coll: FromIterator<T>,
408 P: Params,
409{
410 let mut stmt = conn.prepare_maybe_cached(sql, cache)?;
411 let iter = stmt.query_and_then(params, mapper)?;
412 iter.collect::<Result<Coll, E>>()
413}