Skip to main content

diesel/sqlite/connection/
hooks.rs

1use super::SqliteConnection;
2use super::update_hook::SqliteUpdateRouter;
3use core::num::NonZeroU32;
4
5pub(super) use super::authorizer::{AuthorizerContext, AuthorizerDecision};
6pub(super) use super::collation_needed::CollationNeededContext;
7pub(super) use super::{BusyDecision, CommitDecision, ProgressDecision};
8use super::{SqliteTraceEvent, SqliteTraceFlags};
9
10impl SqliteConnection {
11    /// Installs a [`SqliteUpdateRouter`](crate::sqlite::SqliteUpdateRouter) as
12    /// the update hook, invoked for every row change (insert, update, or delete)
13    /// on a [rowid table](https://www.sqlite.org/rowidtable.html). Replaces any
14    /// previously registered update hook, since SQLite allows only one per
15    /// connection.
16    ///
17    /// Build the router with [`SqliteUpdateRouter::on`] for typed per-table
18    /// routes and [`SqliteUpdateRouter::on_any`] for a table-agnostic route. A
19    /// single catch-all is `on_any(SqliteChangeOps::ALL, ...)`. Inside a
20    /// callback, [`is_from`](crate::sqlite::SqliteChangeEvent::is_from) and
21    /// [`rowid_in`](crate::sqlite::SqliteChangeEvent::rowid_in) match a `table!`
22    /// marker without a string.
23    ///
24    /// Callbacks run synchronously as part of the `sqlite3_step()` call that
25    /// performs the change, on the thread performing it, so they are never
26    /// invoked concurrently. Per SQLite, a callback must not use the connection
27    /// that triggered it (running any SQL, including a `SELECT`, counts as use)
28    /// and is not reentrant. A panic in a callback aborts the process. To act on
29    /// the changed row, capture its `rowid` and run the query after the
30    /// statement completes.
31    ///
32    /// # Limitations
33    ///
34    /// These come from the underlying
35    /// [`sqlite3_update_hook`](https://www.sqlite.org/c3ref/update_hook.html):
36    ///
37    /// - Only fires for [rowid tables](https://www.sqlite.org/rowidtable.html),
38    ///   not `WITHOUT ROWID` tables.
39    /// - Does not fire for changes to internal system tables, for `ON CONFLICT
40    ///   REPLACE` deletions, or for the truncate optimization (`DELETE` with no
41    ///   `WHERE` on a trigger-free table).
42    ///
43    /// See: [`sqlite3_update_hook`](https://www.sqlite.org/c3ref/update_hook.html)
44    ///
45    /// # Example
46    ///
47    /// ```rust
48    /// use diesel::prelude::*;
49    /// use diesel::sqlite::{SqliteChangeOps, SqliteConnection, SqliteUpdateRouter};
50    /// use std::sync::{Arc, Mutex};
51    ///
52    /// diesel::table! { users (id) { id -> Integer, name -> Text, } }
53    ///
54    /// # use diesel::connection::SimpleConnection;
55    /// # let conn = &mut SqliteConnection::establish(":memory:").unwrap();
56    /// # conn.batch_execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)").unwrap();
57    /// let changes = Arc::new(Mutex::new(Vec::new()));
58    /// let captured = changes.clone();
59    ///
60    /// conn.on_update(
61    ///     SqliteUpdateRouter::new().on(
62    ///         users::table,
63    ///         SqliteChangeOps::INSERT,
64    ///         move |change| captured.lock().unwrap().push(change.rowid),
65    ///     ),
66    /// );
67    ///
68    /// diesel::insert_into(users::table)
69    ///     .values(users::name.eq("Alice"))
70    ///     .execute(conn)
71    ///     .unwrap();
72    ///
73    /// assert_eq!(*changes.lock().unwrap(), vec![1]);
74    /// ```
75    pub fn on_update(&mut self, router: SqliteUpdateRouter) {
76        self.raw_connection.set_update_hook(router.into_hook());
77    }
78
79    /// Removes the update hook. Subsequent row changes will not invoke any
80    /// callback.
81    ///
82    /// See [`on_update`](Self::on_update) for usage.
83    pub fn remove_update_hook(&mut self) {
84        self.raw_connection.remove_update_hook();
85    }
86
87    /// Registers a callback invoked when a transaction is about to be
88    /// committed.
89    ///
90    /// The callback returns a [`CommitDecision`]: `Proceed` lets the commit
91    /// complete, `Rollback` converts it into a rollback.
92    ///
93    /// Only one commit hook can be active at a time per connection.
94    /// Registering a new one replaces the previous.
95    ///
96    /// The callback runs synchronously as part of the committing
97    /// `sqlite3_step()` call, on the thread performing the commit, so it is
98    /// never invoked concurrently. Per SQLite, the callback must not use the
99    /// connection that triggered it (running any SQL, including a `SELECT`,
100    /// counts as use) and is not reentrant. A panic in the callback aborts the
101    /// process.
102    ///
103    /// See: [`sqlite3_commit_hook`](https://www.sqlite.org/c3ref/commit_hook.html)
104    ///
105    /// # Example
106    ///
107    /// ```rust
108    /// use diesel::prelude::*;
109    /// use diesel::sqlite::{SqliteConnection, CommitDecision};
110    /// use std::sync::{Arc, Mutex};
111    ///
112    /// diesel::table! {
113    ///     users (id) {
114    ///         id -> Integer,
115    ///         name -> Text,
116    ///     }
117    /// }
118    ///
119    /// # use diesel::connection::SimpleConnection;
120    /// # let conn = &mut SqliteConnection::establish(":memory:").unwrap();
121    /// # conn.batch_execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)").unwrap();
122    /// let commits = Arc::new(Mutex::new(0u32));
123    /// let commits2 = commits.clone();
124    ///
125    /// conn.on_commit(move || {
126    ///     *commits2.lock().unwrap() += 1;
127    ///     CommitDecision::Proceed
128    /// });
129    ///
130    /// conn.immediate_transaction(|conn| {
131    ///     diesel::insert_into(users::table)
132    ///         .values(users::name.eq("Alice"))
133    ///         .execute(conn)?;
134    ///     Ok::<_, diesel::result::Error>(())
135    /// }).unwrap();
136    ///
137    /// assert_eq!(*commits.lock().unwrap(), 1);
138    /// ```
139    pub fn on_commit<F>(&mut self, hook: F)
140    where
141        F: FnMut() -> CommitDecision + Send + 'static,
142    {
143        self.raw_connection.set_commit_hook(hook);
144    }
145
146    /// Removes the commit hook. Subsequent commits will not invoke any
147    /// callback.
148    ///
149    /// See [`on_commit`](Self::on_commit) for usage example.
150    pub fn remove_commit_hook(&mut self) {
151        self.raw_connection.remove_commit_hook();
152    }
153
154    /// Registers a callback invoked after a transaction is rolled back.
155    ///
156    /// This is **not** invoked for the implicit rollback that occurs when
157    /// the connection is closed. It **is** invoked when a commit hook forces
158    /// a rollback by returning [`CommitDecision::Rollback`].
159    ///
160    /// Only one rollback hook can be active at a time per connection.
161    /// Registering a new one replaces the previous.
162    ///
163    /// The callback must not use the database connection. It is invoked
164    /// synchronously on the thread driving the connection, so it is never
165    /// called concurrently, and like the commit hook it is not reentrant.
166    /// Panics in the callback abort the process.
167    ///
168    /// See: [`sqlite3_rollback_hook`](https://www.sqlite.org/c3ref/commit_hook.html)
169    ///
170    /// # Example
171    ///
172    /// ```rust
173    /// use diesel::prelude::*;
174    /// use diesel::sqlite::SqliteConnection;
175    /// use std::sync::Arc;
176    /// use std::sync::atomic::{AtomicU32, Ordering};
177    ///
178    /// # use diesel::connection::SimpleConnection;
179    /// # let conn = &mut SqliteConnection::establish(":memory:").unwrap();
180    /// # conn.batch_execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)").unwrap();
181    /// let rollbacks = Arc::new(AtomicU32::new(0));
182    /// let rb2 = rollbacks.clone();
183    ///
184    /// conn.on_rollback(move || {
185    ///     rb2.fetch_add(1, Ordering::Relaxed);
186    /// });
187    ///
188    /// // Force a rollback by returning an error.
189    /// let _ = conn.immediate_transaction(|_conn| {
190    ///     Err::<(), _>(diesel::result::Error::RollbackTransaction)
191    /// });
192    ///
193    /// assert_eq!(rollbacks.load(Ordering::Relaxed), 1);
194    /// ```
195    pub fn on_rollback<F>(&mut self, hook: F)
196    where
197        F: FnMut() + Send + 'static,
198    {
199        self.raw_connection.set_rollback_hook(hook);
200    }
201
202    /// Removes the rollback hook. Subsequent rollbacks will not invoke any
203    /// callback.
204    ///
205    /// See [`on_rollback`](Self::on_rollback) for usage example.
206    pub fn remove_rollback_hook(&mut self) {
207        self.raw_connection.remove_rollback_hook();
208    }
209
210    /// Registers a progress handler that can interrupt long-running queries.
211    ///
212    /// The callback is invoked periodically while a query runs. `n` is the
213    /// approximate number of virtual-machine instructions between callbacks. It
214    /// is a [`NonZeroU32`] so the handler cannot be disabled implicitly by
215    /// passing zero. Use [`remove_progress_handler`](Self::remove_progress_handler)
216    /// to disable it. Since SQLite 3.41.0 the callback may also fire during
217    /// statement preparation.
218    ///
219    /// The callback returns a [`ProgressDecision`]: `Continue` lets the query
220    /// keep executing, `Interrupt` aborts it (causes `SQLITE_INTERRUPT`).
221    ///
222    /// Only one progress handler can be active at a time per connection.
223    /// Registering a new one replaces the previous.
224    ///
225    /// The callback must not use the database connection. It is invoked
226    /// synchronously on the thread driving the connection, so it is never
227    /// called concurrently. Panics in the callback abort the process.
228    ///
229    /// See: [`sqlite3_progress_handler`](https://www.sqlite.org/c3ref/progress_handler.html)
230    ///
231    /// # Example
232    ///
233    /// ```rust
234    /// use diesel::prelude::*;
235    /// use diesel::sqlite::{SqliteConnection, ProgressDecision};
236    /// use std::num::NonZeroU32;
237    /// use std::sync::Arc;
238    /// use std::sync::atomic::{AtomicBool, Ordering};
239    ///
240    /// # let conn = &mut SqliteConnection::establish(":memory:").unwrap();
241    /// let cancelled = Arc::new(AtomicBool::new(false));
242    /// let cancelled2 = cancelled.clone();
243    ///
244    /// conn.on_progress(NonZeroU32::new(1000).unwrap(), move || {
245    ///     if cancelled2.load(Ordering::Relaxed) {
246    ///         ProgressDecision::Interrupt
247    ///     } else {
248    ///         ProgressDecision::Continue
249    ///     }
250    /// });
251    ///
252    /// // Later: remove the handler.
253    /// conn.remove_progress_handler();
254    /// ```
255    pub fn on_progress<F>(&mut self, n: NonZeroU32, hook: F)
256    where
257        F: FnMut() -> ProgressDecision + Send + 'static,
258    {
259        self.raw_connection.set_progress_handler(n, hook);
260    }
261
262    /// Removes the progress handler. Subsequent queries will not invoke any
263    /// callback.
264    ///
265    /// See [`on_progress`](Self::on_progress) for usage example.
266    pub fn remove_progress_handler(&mut self) {
267        self.raw_connection.remove_progress_handler();
268    }
269
270    /// Registers a callback invoked after each commit of a transaction in
271    /// [WAL mode](https://www.sqlite.org/wal.html). It receives a borrowed
272    /// connection, the database name (`"main"`, `"temp"`, or an `ATTACH`
273    /// alias), and the current WAL page count.
274    ///
275    /// The hook fires after the commit completes and the write-lock is
276    /// released, so the callback may read, write, or
277    /// [checkpoint](https://www.sqlite.org/wal.html#ckpt) through `conn`,
278    /// provided it leaves no open transaction. A write that commits inside the
279    /// callback re-fires the hook re-entrantly, so a callback that writes
280    /// unconditionally will recurse until the stack overflows. Guard against
281    /// that yourself if the callback writes.
282    ///
283    /// Only one WAL hook is active at a time, and re-registering replaces it.
284    /// `PRAGMA wal_autocheckpoint` installs its own WAL hook and overwrites
285    /// this one. A panic in the callback aborts the process.
286    ///
287    /// See: [`sqlite3_wal_hook`](https://www.sqlite.org/c3ref/wal_hook.html)
288    ///
289    /// # Example
290    ///
291    /// ```rust,no_run
292    /// # use diesel::prelude::*;
293    /// # use diesel::connection::SimpleConnection;
294    /// # use diesel::sqlite::SqliteConnection;
295    /// # use diesel::sqlite::WalCheckpointMode;
296    /// # let conn = &mut SqliteConnection::establish("test.db").unwrap();
297    /// # conn.batch_execute("PRAGMA journal_mode = WAL;").unwrap();
298    /// conn.on_wal(|conn, db_name, n_pages| {
299    ///     println!("WAL for {db_name}: {n_pages} pages");
300    ///     if n_pages > 1000 {
301    ///         // The connection may be used here, e.g. to force a checkpoint.
302    ///         let _ = conn.wal_checkpoint(None, WalCheckpointMode::Truncate);
303    ///     }
304    /// });
305    /// ```
306    pub fn on_wal<F>(&mut self, hook: F)
307    where
308        F: Fn(&mut SqliteConnection, &str, u32) + Send + 'static,
309    {
310        self.raw_connection.set_wal_hook(hook);
311    }
312
313    /// Removes the WAL hook. Subsequent commits will not invoke any callback.
314    ///
315    /// See [`on_wal`](Self::on_wal) for usage example.
316    pub fn remove_wal_hook(&mut self) {
317        self.raw_connection.remove_wal_hook();
318    }
319
320    /// Registers a custom busy handler for lock contention.
321    ///
322    /// The callback receives the retry count (starting from 0) and returns a
323    /// [`BusyDecision`]: `Retry` retries the locked operation, `GiveUp` aborts
324    /// and returns `SQLITE_BUSY` to the caller.
325    ///
326    /// Setting this clears any timeout previously set with
327    /// [`set_busy_timeout`](Self::set_busy_timeout). Conversely, calling
328    /// `set_busy_timeout` clears this handler. Only one busy handler can be
329    /// active at a time per connection.
330    ///
331    /// The callback must not use the database connection. If the callback
332    /// modifies the database, behavior is undefined. SQLite may return
333    /// `SQLITE_BUSY` instead of calling the handler to prevent deadlocks.
334    ///
335    /// The callback is invoked synchronously on the thread driving the
336    /// connection, so it is never called concurrently, and per SQLite it is
337    /// not reentrant.
338    ///
339    /// Panics in the callback abort the process.
340    ///
341    /// See: [`sqlite3_busy_handler`](https://www.sqlite.org/c3ref/busy_handler.html)
342    ///
343    /// # Example
344    ///
345    /// ```rust
346    /// use diesel::prelude::*;
347    /// use diesel::sqlite::{SqliteConnection, BusyDecision};
348    /// use std::thread;
349    /// use std::time::Duration;
350    ///
351    /// # let conn = &mut SqliteConnection::establish(":memory:").unwrap();
352    /// conn.on_busy(|retry_count| {
353    ///     if retry_count < 5 {
354    ///         thread::sleep(Duration::from_millis(100));
355    ///         BusyDecision::Retry
356    ///     } else {
357    ///         BusyDecision::GiveUp
358    ///     }
359    /// });
360    ///
361    /// // Later: remove the handler
362    /// conn.remove_busy_handler();
363    /// ```
364    pub fn on_busy<F>(&mut self, hook: F)
365    where
366        F: FnMut(i32) -> BusyDecision + Send + 'static,
367    {
368        self.raw_connection.set_busy_handler(hook);
369    }
370
371    /// Removes the custom busy handler.
372    ///
373    /// See [`on_busy`](Self::on_busy) for usage example.
374    pub fn remove_busy_handler(&mut self) {
375        self.raw_connection.remove_busy_handler();
376    }
377
378    /// Sets a simple timeout-based busy handler.
379    ///
380    /// When a table is locked, SQLite will sleep and retry until `ms`
381    /// milliseconds have elapsed. Pass 0 to disable (return `SQLITE_BUSY`
382    /// immediately).
383    ///
384    /// Setting this clears any custom [`on_busy`](Self::on_busy) handler.
385    /// Conversely, calling `on_busy` clears this timeout. For most use cases,
386    /// this is simpler than a custom busy handler.
387    ///
388    /// See: [`sqlite3_busy_timeout`](https://www.sqlite.org/c3ref/busy_timeout.html)
389    ///
390    /// # Example
391    ///
392    /// ```rust
393    /// use diesel::prelude::*;
394    /// use diesel::sqlite::SqliteConnection;
395    ///
396    /// # let conn = &mut SqliteConnection::establish(":memory:").unwrap();
397    /// // Wait up to 5 seconds for locked tables
398    /// conn.set_busy_timeout(5000);
399    /// ```
400    pub fn set_busy_timeout(&mut self, ms: i32) {
401        self.raw_connection.set_busy_timeout(ms);
402    }
403
404    /// Registers an authorizer callback for SQL compilation access control.
405    /// Added in SQLite 3.0.0 (June 2004).
406    ///
407    /// The callback is invoked during `sqlite3_prepare()` (statement
408    /// compilation) to control access to database objects. It receives
409    /// an [`AuthorizerContext`] describing the operation and returns an
410    /// [`AuthorizerDecision`].
411    ///
412    /// The authorizer is consulted only at statement preparation, so treat it
413    /// as defense-in-depth rather than a complete sandbox. SQLite re-checks
414    /// already-prepared statements when an authorizer is installed. Removing the
415    /// authorizer clears diesel's statement cache so statements prepared while
416    /// it was active are re-prepared without it.
417    ///
418    /// The authorizer may be re-invoked during `sqlite3_step()` if a schema
419    /// change triggers statement recompilation.
420    ///
421    /// The callback must not modify the database connection. It is invoked
422    /// synchronously on the thread driving the connection, so it is never
423    /// called concurrently.
424    ///
425    /// Only one authorizer can be active at a time per connection.
426    /// Registering a new one replaces the previous. Panics in the callback
427    /// abort the process.
428    ///
429    /// See: [`sqlite3_set_authorizer`](https://sqlite.org/c3ref/set_authorizer.html)
430    ///
431    /// # Example
432    ///
433    /// ```rust
434    /// # use diesel::prelude::*;
435    /// use diesel::sqlite::{SqliteConnection, AuthorizerContext, AuthorizerDecision};
436    ///
437    /// # let conn = &mut SqliteConnection::establish(":memory:").unwrap();
438    /// conn.on_authorize(|ctx| match ctx {
439    ///     AuthorizerContext::Delete(_) => AuthorizerDecision::Deny,
440    ///     AuthorizerContext::DropTable(_) | AuthorizerContext::DropIndex(_) => {
441    ///         AuthorizerDecision::Deny
442    ///     }
443    ///     _ => AuthorizerDecision::Allow,
444    /// });
445    ///
446    /// // Later: remove the authorizer
447    /// conn.remove_authorizer();
448    /// ```
449    pub fn on_authorize<F>(&mut self, hook: F)
450    where
451        F: FnMut(AuthorizerContext<'_>) -> AuthorizerDecision + Send + 'static,
452    {
453        // No cache clear is needed here. Installing a non-null authorizer makes
454        // SQLite expire every prepared statement on the connection, and because
455        // diesel prepares with `sqlite3_prepare_v3` those statements are
456        // transparently re-prepared under the new authorizer on their next step.
457        self.raw_connection.set_authorizer(hook);
458        // The public documentation doesn't explicitly state what happens with existing
459        // prepared statements, so rather be safe and nuke them. We don't
460        // expect that people change the authorizer all the time, so this should be fine
461        self.statement_cache.clear();
462    }
463
464    /// Removes the authorizer callback.
465    ///
466    /// See [`on_authorize`](Self::on_authorize) for usage example.
467    pub fn remove_authorizer(&mut self) {
468        self.raw_connection.remove_authorizer();
469        // Removing an authorizer does not expire prepared statements: SQLite
470        // expires them only when a non-null authorizer is installed. A statement
471        // prepared while the authorizer was active can have its decisions
472        // compiled in (an `Ignore` on a column read, for example, bakes a `NULL`
473        // into the statement), and nothing invalidates that cached statement on
474        // removal, so it keeps returning the old result. Clear the cache to force
475        // the next query to re-prepare without the authorizer.
476        self.statement_cache.clear();
477    }
478
479    /// Registers a trace callback for SQL execution monitoring.
480    ///
481    /// The callback receives the [`SqliteTraceEvent`]s selected by the
482    /// [`SqliteTraceFlags`] mask. `ROW` fires once per returned row, so prefer
483    /// `STMT`/`PROFILE` for most logging.
484    ///
485    /// Only one trace callback can be active at a time per connection.
486    /// Registering a new one replaces the previous.
487    ///
488    /// The callback must not use the database connection. It is invoked
489    /// synchronously on the thread driving the connection, so it is never
490    /// called concurrently. Panics in the callback abort the process.
491    ///
492    /// See: [`sqlite3_trace_v2`](https://sqlite.org/c3ref/trace_v2.html)
493    ///
494    /// # Example
495    ///
496    /// ```rust
497    /// # use diesel::prelude::*;
498    /// use diesel::sqlite::{SqliteConnection, SqliteTraceFlags, SqliteTraceEvent};
499    ///
500    /// # let conn = &mut SqliteConnection::establish(":memory:").unwrap();
501    /// conn.on_trace(SqliteTraceFlags::STMT | SqliteTraceFlags::PROFILE, |event| {
502    ///     match event {
503    ///         SqliteTraceEvent::Statement { sql, readonly, .. } => {
504    ///             println!("Executing ({}): {}", if readonly { "read" } else { "write" }, sql);
505    ///         }
506    ///         SqliteTraceEvent::Profile { sql, duration_ns, .. } => {
507    ///             println!("{} took {} ns", sql, duration_ns);
508    ///         }
509    ///         _ => {}
510    ///     }
511    /// });
512    ///
513    /// // Later: remove the trace callback
514    /// conn.remove_trace();
515    /// ```
516    pub fn on_trace<F>(&mut self, mask: SqliteTraceFlags, hook: F)
517    where
518        F: FnMut(SqliteTraceEvent<'_>) + Send + 'static,
519    {
520        self.raw_connection.set_trace(mask, hook);
521    }
522
523    /// Removes the trace callback.
524    ///
525    /// See [`on_trace`](Self::on_trace) for usage example.
526    pub fn remove_trace(&mut self) {
527        self.raw_connection.remove_trace();
528    }
529
530    /// Registers a callback fired when SQLite encounters an unknown collation.
531    ///
532    /// The callback receives a borrowed `&mut SqliteConnection` and a
533    /// [`CollationNeededContext`] naming the missing collation. It should
534    /// install the missing collation via
535    /// [`register_collation`](Self::register_collation) and return, after
536    /// which SQLite retries the lookup. The callback body may also execute
537    /// arbitrary SQL through `conn`, provided it leaves no open transaction.
538    ///
539    /// Only one callback is active at a time, and re-registering replaces it.
540    /// Panics in the callback abort the process.
541    ///
542    /// If the callback registers a collation that is itself missing, SQLite
543    /// re-enters the callback. Guard against unbounded recursion in that case.
544    ///
545    /// Added in SQLite 3.0.0.
546    ///
547    /// See: [`sqlite3_collation_needed`](https://www.sqlite.org/c3ref/collation_needed.html)
548    ///
549    /// # Example
550    ///
551    /// ```rust
552    /// # use diesel::prelude::*;
553    /// # use diesel::sqlite::SqliteConnection;
554    /// # let conn = &mut SqliteConnection::establish(":memory:").unwrap();
555    /// conn.on_collation_needed(|conn, ctx| {
556    ///     if ctx.name.eq_ignore_ascii_case("RUSTNOCASE") {
557    ///         let _ = conn.register_collation("RUSTNOCASE", |a, b| {
558    ///             a.to_lowercase().cmp(&b.to_lowercase())
559    ///         });
560    ///     }
561    /// });
562    ///
563    /// // Later: remove the callback
564    /// conn.remove_collation_needed_hook();
565    /// ```
566    pub fn on_collation_needed<F>(&mut self, hook: F)
567    where
568        F: Fn(&mut SqliteConnection, CollationNeededContext<'_>) + Send + 'static,
569    {
570        self.raw_connection.set_collation_needed_hook(hook);
571    }
572
573    /// Removes the collation-needed callback.
574    ///
575    /// See [`on_collation_needed`](Self::on_collation_needed) for usage
576    /// example.
577    pub fn remove_collation_needed_hook(&mut self) {
578        self.raw_connection.remove_collation_needed_hook();
579    }
580}
581
582#[cfg(test)]
583mod tests {
584    use super::super::update_hook::{SqliteChangeOp, SqliteChangeOps, SqliteUpdateRouter};
585    use super::*;
586    use crate::connection::Connection;
587    use crate::prelude::*;
588    use crate::query_dsl::RunQueryDsl;
589    use std::sync::Arc;
590    use std::sync::atomic::{AtomicU32, Ordering};
591
592    fn connection() -> SqliteConnection {
593        SqliteConnection::establish(":memory:").unwrap()
594    }
595
596    #[derive(crate::QueryableByName)]
597    struct CountResult {
598        #[diesel(sql_type = crate::sql_types::BigInt)]
599        c: i64,
600    }
601
602    // ===================================================================
603    // Change-hook integration tests
604    // ===================================================================
605
606    table! {
607        hook_users {
608            id -> Integer,
609            name -> Text,
610        }
611    }
612
613    table! {
614        hook_posts {
615            id -> Integer,
616            title -> Text,
617        }
618    }
619
620    fn setup_hook_tables(conn: &mut SqliteConnection) {
621        crate::sql_query(
622            "CREATE TABLE hook_users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)",
623        )
624        .execute(conn)
625        .unwrap();
626        crate::sql_query(
627            "CREATE TABLE hook_posts (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL)",
628        )
629        .execute(conn)
630        .unwrap();
631    }
632
633    // A schema-qualified `table!` marker routes only to its attached database,
634    // even when a same-named table exists in `main`.
635    #[diesel_test_helper::test]
636    fn router_on_matches_schema_qualified_table() {
637        use std::sync::{Arc, Mutex};
638
639        table! {
640            attached.shared_items (id) {
641                id -> Integer,
642            }
643        }
644
645        let conn = &mut connection();
646        crate::sql_query("ATTACH DATABASE ':memory:' AS attached")
647            .execute(conn)
648            .unwrap();
649        crate::sql_query("CREATE TABLE shared_items (id INTEGER PRIMARY KEY)")
650            .execute(conn)
651            .unwrap();
652        crate::sql_query("CREATE TABLE attached.shared_items (id INTEGER PRIMARY KEY)")
653            .execute(conn)
654            .unwrap();
655
656        let fired = Arc::new(Mutex::new(Vec::new()));
657        let f2 = fired.clone();
658        conn.on_update(SqliteUpdateRouter::new().on(
659            shared_items::table,
660            SqliteChangeOps::ALL,
661            move |ev| {
662                f2.lock().unwrap().push((ev.db_name.to_owned(), ev.rowid));
663            },
664        ));
665
666        // A change in `main.shared_items` must not fire the attached-only route.
667        crate::sql_query("INSERT INTO main.shared_items (id) VALUES (1)")
668            .execute(conn)
669            .unwrap();
670        // A change in `attached.shared_items` must fire it.
671        crate::sql_query("INSERT INTO attached.shared_items (id) VALUES (2)")
672            .execute(conn)
673            .unwrap();
674
675        assert_eq!(
676            *fired.lock().unwrap(),
677            vec![("attached".to_owned(), 2)],
678            "a schema-qualified route matches only its attached database"
679        );
680    }
681
682    #[diesel_test_helper::test]
683    fn router_on_dispatches_to_typed_table() {
684        use std::sync::{Arc, Mutex};
685        let conn = &mut connection();
686        setup_hook_tables(conn);
687
688        let fired = Arc::new(Mutex::new(Vec::new()));
689        let fired2 = fired.clone();
690
691        conn.on_update(SqliteUpdateRouter::new().on(
692            hook_users::table,
693            SqliteChangeOps::INSERT,
694            move |change| {
695                fired2.lock().unwrap().push((change.op, change.rowid));
696            },
697        ));
698
699        // INSERT a row: the route fires immediately during sqlite3_step().
700        crate::sql_query("INSERT INTO hook_users (name) VALUES ('Alice')")
701            .execute(conn)
702            .unwrap();
703
704        let events = fired.lock().unwrap().clone();
705        assert_eq!(events.len(), 1);
706        assert_eq!(events[0].0, SqliteChangeOp::Insert);
707        assert_eq!(events[0].1, 1); // rowid
708    }
709
710    #[diesel_test_helper::test]
711    fn on_delete_fires_only_for_delete() {
712        use std::sync::{Arc, Mutex};
713        let conn = &mut connection();
714        setup_hook_tables(conn);
715
716        let fired = Arc::new(Mutex::new(Vec::new()));
717        let fired2 = fired.clone();
718
719        conn.on_update(SqliteUpdateRouter::new().on(
720            hook_users::table,
721            SqliteChangeOps::DELETE,
722            move |change| {
723                fired2.lock().unwrap().push(change.op);
724            },
725        ));
726
727        // INSERT + UPDATE + DELETE
728        crate::sql_query("INSERT INTO hook_users (name) VALUES ('Alice')")
729            .execute(conn)
730            .unwrap();
731        crate::sql_query("UPDATE hook_users SET name = 'Bob' WHERE id = 1")
732            .execute(conn)
733            .unwrap();
734        crate::sql_query("DELETE FROM hook_users WHERE id = 1")
735            .execute(conn)
736            .unwrap();
737
738        let events = fired.lock().unwrap().clone();
739        // Only the DELETE should have matched.
740        assert_eq!(events.len(), 1);
741        assert_eq!(events[0], SqliteChangeOp::Delete);
742    }
743
744    #[diesel_test_helper::test]
745    fn every_matching_route_fires_in_order() {
746        use std::sync::{Arc, Mutex};
747        let conn = &mut connection();
748        setup_hook_tables(conn);
749
750        let order = Arc::new(Mutex::new(Vec::new()));
751        let o1 = order.clone();
752        let o2 = order.clone();
753
754        conn.on_update(
755            SqliteUpdateRouter::new()
756                .on(hook_users::table, SqliteChangeOps::INSERT, move |_| {
757                    o1.lock().unwrap().push(1);
758                })
759                .on(hook_users::table, SqliteChangeOps::INSERT, move |_| {
760                    o2.lock().unwrap().push(2);
761                }),
762        );
763
764        crate::sql_query("INSERT INTO hook_users (name) VALUES ('X')")
765            .execute(conn)
766            .unwrap();
767
768        assert_eq!(*order.lock().unwrap(), vec![1, 2]);
769    }
770
771    #[diesel_test_helper::test]
772    fn remove_update_stops_dispatch() {
773        use std::sync::{Arc, Mutex};
774        let conn = &mut connection();
775        setup_hook_tables(conn);
776
777        let fired = Arc::new(Mutex::new(0u32));
778        let f2 = fired.clone();
779
780        conn.on_update(
781            SqliteUpdateRouter::new().on_any(SqliteChangeOps::ALL, move |_| {
782                *f2.lock().unwrap() += 1;
783            }),
784        );
785
786        crate::sql_query("INSERT INTO hook_users (name) VALUES ('A')")
787            .execute(conn)
788            .unwrap();
789        assert_eq!(*fired.lock().unwrap(), 1);
790
791        // Remove the hook.
792        conn.remove_update_hook();
793
794        crate::sql_query("INSERT INTO hook_users (name) VALUES ('B')")
795            .execute(conn)
796            .unwrap();
797        // Should still be 1, hook was removed.
798        assert_eq!(*fired.lock().unwrap(), 1);
799    }
800
801    #[diesel_test_helper::test]
802    fn events_fire_immediately_during_statement() {
803        use std::sync::{Arc, Mutex};
804        let conn = &mut connection();
805        setup_hook_tables(conn);
806
807        // Insert a row without a hook first.
808        crate::sql_query("INSERT INTO hook_users (name) VALUES ('Z')")
809            .execute(conn)
810            .unwrap();
811
812        let fired = Arc::new(Mutex::new(Vec::new()));
813        let f2 = fired.clone();
814
815        conn.on_update(SqliteUpdateRouter::new().on(
816            hook_users::table,
817            SqliteChangeOps::UPDATE,
818            move |event| {
819                f2.lock().unwrap().push(event.rowid);
820            },
821        ));
822
823        // UPDATE triggers the C hook immediately during sqlite3_step().
824        crate::sql_query("UPDATE hook_users SET name = 'W' WHERE id = 1")
825            .execute(conn)
826            .unwrap();
827
828        assert_eq!(*fired.lock().unwrap(), vec![1i64]);
829    }
830
831    #[diesel_test_helper::test]
832    fn on_update_fires_for_update_only() {
833        use std::sync::{Arc, Mutex};
834        let conn = &mut connection();
835        setup_hook_tables(conn);
836
837        let count = Arc::new(Mutex::new(0u32));
838        let c2 = count.clone();
839
840        conn.on_update(SqliteUpdateRouter::new().on(
841            hook_users::table,
842            SqliteChangeOps::UPDATE,
843            move |event| {
844                assert_eq!(event.op, SqliteChangeOp::Update);
845                *c2.lock().unwrap() += 1;
846            },
847        ));
848
849        crate::sql_query("INSERT INTO hook_users (name) VALUES ('A')")
850            .execute(conn)
851            .unwrap();
852        crate::sql_query("UPDATE hook_users SET name = 'B' WHERE id = 1")
853            .execute(conn)
854            .unwrap();
855        crate::sql_query("DELETE FROM hook_users WHERE id = 1")
856            .execute(conn)
857            .unwrap();
858
859        assert_eq!(*count.lock().unwrap(), 1);
860    }
861
862    #[diesel_test_helper::test]
863    fn on_update_receives_every_change() {
864        use std::sync::{Arc, Mutex};
865        let conn = &mut connection();
866        setup_hook_tables(conn);
867
868        let events = Arc::new(Mutex::new(Vec::new()));
869        let e2 = events.clone();
870
871        conn.on_update(
872            SqliteUpdateRouter::new().on_any(SqliteChangeOps::ALL, move |ev| {
873                e2.lock().unwrap().push((ev.op, ev.table_name.to_owned()));
874            }),
875        );
876
877        crate::sql_query("INSERT INTO hook_users (name) VALUES ('A')")
878            .execute(conn)
879            .unwrap();
880        crate::sql_query("INSERT INTO hook_posts (title) VALUES ('P')")
881            .execute(conn)
882            .unwrap();
883        crate::sql_query("UPDATE hook_users SET name = 'B' WHERE id = 1")
884            .execute(conn)
885            .unwrap();
886        crate::sql_query("DELETE FROM hook_posts WHERE id = 1")
887            .execute(conn)
888            .unwrap();
889
890        let evts = events.lock().unwrap().clone();
891        assert_eq!(evts.len(), 4);
892        assert_eq!(evts[0], (SqliteChangeOp::Insert, "hook_users".to_owned()));
893        assert_eq!(evts[1], (SqliteChangeOp::Insert, "hook_posts".to_owned()));
894        assert_eq!(evts[2], (SqliteChangeOp::Update, "hook_users".to_owned()));
895        assert_eq!(evts[3], (SqliteChangeOp::Delete, "hook_posts".to_owned()));
896    }
897
898    #[diesel_test_helper::test]
899    fn router_filters_by_op_mask() {
900        use std::sync::{Arc, Mutex};
901        let conn = &mut connection();
902        setup_hook_tables(conn);
903
904        let count = Arc::new(Mutex::new(0u32));
905        let c2 = count.clone();
906
907        conn.on_update(SqliteUpdateRouter::new().on_any(
908            SqliteChangeOps::INSERT | SqliteChangeOps::DELETE,
909            move |_| {
910                *c2.lock().unwrap() += 1;
911            },
912        ));
913
914        crate::sql_query("INSERT INTO hook_users (name) VALUES ('A')")
915            .execute(conn)
916            .unwrap();
917        crate::sql_query("UPDATE hook_users SET name = 'B' WHERE id = 1")
918            .execute(conn)
919            .unwrap();
920        crate::sql_query("DELETE FROM hook_users WHERE id = 1")
921            .execute(conn)
922            .unwrap();
923
924        // INSERT + DELETE = 2, not UPDATE
925        assert_eq!(*count.lock().unwrap(), 2);
926    }
927
928    #[diesel_test_helper::test]
929    fn router_dispatches_to_multiple_tables() {
930        use std::sync::{Arc, Mutex};
931        let conn = &mut connection();
932        setup_hook_tables(conn);
933
934        let user_count = Arc::new(Mutex::new(0u32));
935        let post_count = Arc::new(Mutex::new(0u32));
936        let uc = user_count.clone();
937        let pc = post_count.clone();
938
939        conn.on_update(
940            SqliteUpdateRouter::new()
941                .on(hook_users::table, SqliteChangeOps::ALL, move |_| {
942                    *uc.lock().unwrap() += 1;
943                })
944                .on(hook_posts::table, SqliteChangeOps::ALL, move |_| {
945                    *pc.lock().unwrap() += 1;
946                }),
947        );
948
949        crate::sql_query("INSERT INTO hook_users (name) VALUES ('X')")
950            .execute(conn)
951            .unwrap();
952        crate::sql_query("INSERT INTO hook_posts (title) VALUES ('Y')")
953            .execute(conn)
954            .unwrap();
955
956        assert_eq!(*user_count.lock().unwrap(), 1);
957        assert_eq!(*post_count.lock().unwrap(), 1);
958    }
959
960    #[diesel_test_helper::test]
961    fn on_any_audit_plus_specific_route() {
962        use std::sync::{Arc, Mutex};
963        let conn = &mut connection();
964        setup_hook_tables(conn);
965
966        let audit_count = Arc::new(Mutex::new(0u32));
967        let user_insert_count = Arc::new(Mutex::new(0u32));
968        let ac = audit_count.clone();
969        let uic = user_insert_count.clone();
970
971        conn.on_update(
972            SqliteUpdateRouter::new()
973                .on_any(SqliteChangeOps::ALL, move |_| {
974                    *ac.lock().unwrap() += 1;
975                })
976                .on(hook_users::table, SqliteChangeOps::INSERT, move |_| {
977                    *uic.lock().unwrap() += 1;
978                }),
979        );
980
981        // Hits both the audit route and the user-insert route.
982        crate::sql_query("INSERT INTO hook_users (name) VALUES ('X')")
983            .execute(conn)
984            .unwrap();
985        // Hits only the audit route.
986        crate::sql_query("INSERT INTO hook_posts (title) VALUES ('Y')")
987            .execute(conn)
988            .unwrap();
989
990        assert_eq!(*audit_count.lock().unwrap(), 2);
991        assert_eq!(*user_insert_count.lock().unwrap(), 1);
992    }
993
994    #[diesel_test_helper::test]
995    fn rowid_in_filters_by_table() {
996        use std::sync::{Arc, Mutex};
997        let conn = &mut connection();
998        setup_hook_tables(conn);
999
1000        let captured = Arc::new(Mutex::new(Vec::new()));
1001        let c2 = captured.clone();
1002
1003        conn.on_update(
1004            SqliteUpdateRouter::new().on_any(SqliteChangeOps::ALL, move |change| {
1005                if let Some(rowid) = change.rowid_in(hook_users::table) {
1006                    c2.lock().unwrap().push(rowid);
1007                }
1008            }),
1009        );
1010
1011        crate::sql_query("INSERT INTO hook_users (name) VALUES ('A')")
1012            .execute(conn)
1013            .unwrap();
1014        crate::sql_query("INSERT INTO hook_posts (title) VALUES ('P')")
1015            .execute(conn)
1016            .unwrap();
1017
1018        // Only the hook_users rowid was captured.
1019        assert_eq!(*captured.lock().unwrap(), vec![1i64]);
1020    }
1021
1022    #[diesel_test_helper::test]
1023    fn is_from_matches_table_marker() {
1024        use std::sync::{Arc, Mutex};
1025        let conn = &mut connection();
1026        setup_hook_tables(conn);
1027
1028        let captured = Arc::new(Mutex::new(Vec::new()));
1029        let c2 = captured.clone();
1030
1031        conn.on_update(
1032            SqliteUpdateRouter::new().on_any(SqliteChangeOps::ALL, move |change| {
1033                c2.lock().unwrap().push((
1034                    change.is_from(hook_users::table),
1035                    change.is_from(hook_posts::table),
1036                ));
1037            }),
1038        );
1039
1040        crate::sql_query("INSERT INTO hook_users (name) VALUES ('A')")
1041            .execute(conn)
1042            .unwrap();
1043
1044        assert_eq!(*captured.lock().unwrap(), vec![(true, false)]);
1045    }
1046
1047    #[diesel_test_helper::test]
1048    fn hooks_fire_across_transactions() {
1049        use std::sync::{Arc, Mutex};
1050        let conn = &mut connection();
1051        setup_hook_tables(conn);
1052
1053        let fired = Arc::new(Mutex::new(Vec::new()));
1054        let f2 = fired.clone();
1055
1056        // Register hook BEFORE the transaction.
1057        conn.on_update(SqliteUpdateRouter::new().on(
1058            hook_users::table,
1059            SqliteChangeOps::INSERT,
1060            move |event| {
1061                f2.lock().unwrap().push(event.rowid);
1062            },
1063        ));
1064
1065        conn.immediate_transaction(|conn| {
1066            crate::sql_query("INSERT INTO hook_users (name) VALUES ('TxUser')")
1067                .execute(conn)
1068                .unwrap();
1069            Ok::<_, crate::result::Error>(())
1070        })
1071        .unwrap();
1072
1073        assert_eq!(fired.lock().unwrap().len(), 1);
1074    }
1075
1076    // ===================================================================
1077    // Negative tests: cases where the update hook must NOT fire
1078    //
1079    // These are documented SQLite limitations of sqlite3_update_hook():
1080    // https://www.sqlite.org/c3ref/update_hook.html
1081    // ===================================================================
1082
1083    /// The update hook is not invoked for WITHOUT ROWID tables.
1084    /// See: https://www.sqlite.org/c3ref/update_hook.html
1085    #[diesel_test_helper::test]
1086    fn update_hook_silent_for_without_rowid_tables() {
1087        use std::sync::{Arc, Mutex};
1088        let conn = &mut connection();
1089
1090        crate::sql_query("CREATE TABLE kv (key TEXT PRIMARY KEY, val TEXT NOT NULL) WITHOUT ROWID")
1091            .execute(conn)
1092            .unwrap();
1093
1094        let events: Arc<Mutex<Vec<SqliteChangeOp>>> = Arc::new(Mutex::new(Vec::new()));
1095        let e2 = events.clone();
1096
1097        conn.on_update(
1098            SqliteUpdateRouter::new().on_any(SqliteChangeOps::ALL, move |ev| {
1099                if ev.table_name == "kv" {
1100                    e2.lock().unwrap().push(ev.op);
1101                }
1102            }),
1103        );
1104
1105        crate::sql_query("INSERT INTO kv (key, val) VALUES ('a', '1')")
1106            .execute(conn)
1107            .unwrap();
1108        crate::sql_query("UPDATE kv SET val = '2' WHERE key = 'a'")
1109            .execute(conn)
1110            .unwrap();
1111        crate::sql_query("DELETE FROM kv WHERE key = 'a'")
1112            .execute(conn)
1113            .unwrap();
1114
1115        assert!(
1116            events.lock().unwrap().is_empty(),
1117            "update hook must not fire for WITHOUT ROWID tables"
1118        );
1119    }
1120
1121    /// When a UNIQUE constraint conflict is resolved via ON CONFLICT REPLACE,
1122    /// the implicit deletion of the conflicting row does NOT fire the update
1123    /// hook. Only the INSERT for the new row fires.
1124    /// See: https://www.sqlite.org/c3ref/update_hook.html
1125    #[diesel_test_helper::test]
1126    fn update_hook_silent_for_on_conflict_replace_deletion() {
1127        use std::sync::{Arc, Mutex};
1128        let conn = &mut connection();
1129
1130        crate::sql_query("CREATE TABLE uq (id INTEGER PRIMARY KEY, val TEXT NOT NULL UNIQUE)")
1131            .execute(conn)
1132            .unwrap();
1133
1134        crate::sql_query("INSERT INTO uq (id, val) VALUES (1, 'original')")
1135            .execute(conn)
1136            .unwrap();
1137
1138        let events: Arc<Mutex<Vec<(SqliteChangeOp, i64)>>> = Arc::new(Mutex::new(Vec::new()));
1139        let e2 = events.clone();
1140
1141        conn.on_update(
1142            SqliteUpdateRouter::new().on_any(SqliteChangeOps::ALL, move |ev| {
1143                if ev.table_name == "uq" {
1144                    e2.lock().unwrap().push((ev.op, ev.rowid));
1145                }
1146            }),
1147        );
1148
1149        // INSERT OR REPLACE with a conflicting val: the old row (id=1) is
1150        // silently deleted by SQLite and the new row (id=2) is inserted.
1151        // The hook fires only for the INSERT of the new row.
1152        crate::sql_query("INSERT OR REPLACE INTO uq (id, val) VALUES (2, 'original')")
1153            .execute(conn)
1154            .unwrap();
1155
1156        let recorded = events.lock().unwrap();
1157        assert_eq!(
1158            recorded.len(),
1159            1,
1160            "expected only 1 event (INSERT), got: {:?}",
1161            *recorded
1162        );
1163        assert_eq!(recorded[0].0, SqliteChangeOp::Insert);
1164        assert_eq!(recorded[0].1, 2, "new row should have rowid 2");
1165    }
1166
1167    /// DELETE FROM without a WHERE clause triggers the truncate optimization,
1168    /// which bypasses the update hook entirely: no per-row DELETE events fire.
1169    /// See: https://www.sqlite.org/lang_delete.html#truncateopt
1170    #[diesel_test_helper::test]
1171    fn update_hook_silent_for_truncate_optimization() {
1172        use std::sync::{Arc, Mutex};
1173        let conn = &mut connection();
1174
1175        // The truncate optimization applies when:
1176        //   1. No WHERE clause
1177        //   2. No RETURNING clause
1178        //   3. No triggers on the table
1179        crate::sql_query("CREATE TABLE bulk (id INTEGER PRIMARY KEY, data TEXT NOT NULL)")
1180            .execute(conn)
1181            .unwrap();
1182
1183        crate::sql_query("INSERT INTO bulk (data) VALUES ('a'), ('b'), ('c')")
1184            .execute(conn)
1185            .unwrap();
1186
1187        let events: Arc<Mutex<Vec<SqliteChangeOp>>> = Arc::new(Mutex::new(Vec::new()));
1188        let e2 = events.clone();
1189
1190        conn.on_update(
1191            SqliteUpdateRouter::new().on_any(SqliteChangeOps::ALL, move |ev| {
1192                if ev.table_name == "bulk" {
1193                    e2.lock().unwrap().push(ev.op);
1194                }
1195            }),
1196        );
1197
1198        // DELETE without WHERE: truncate optimization kicks in.
1199        crate::sql_query("DELETE FROM bulk").execute(conn).unwrap();
1200
1201        assert!(
1202            events.lock().unwrap().is_empty(),
1203            "truncate optimization should bypass the update hook"
1204        );
1205    }
1206
1207    /// When a table has triggers, the truncate optimization is disabled, so
1208    /// DELETE without WHERE fires per-row DELETE events as normal.
1209    #[diesel_test_helper::test]
1210    fn update_hook_fires_for_delete_all_when_triggers_disable_truncate() {
1211        use std::sync::{Arc, Mutex};
1212        let conn = &mut connection();
1213
1214        crate::sql_query("CREATE TABLE triggered (id INTEGER PRIMARY KEY, data TEXT NOT NULL)")
1215            .execute(conn)
1216            .unwrap();
1217        // A no-op trigger is enough to disable the truncate optimization.
1218        crate::sql_query(
1219            "CREATE TRIGGER trg_triggered BEFORE DELETE ON triggered \
1220             BEGIN SELECT 1; END",
1221        )
1222        .execute(conn)
1223        .unwrap();
1224
1225        crate::sql_query("INSERT INTO triggered (data) VALUES ('x'), ('y'), ('z')")
1226            .execute(conn)
1227            .unwrap();
1228
1229        let deletes: Arc<Mutex<Vec<i64>>> = Arc::new(Mutex::new(Vec::new()));
1230        let d2 = deletes.clone();
1231
1232        conn.on_update(
1233            SqliteUpdateRouter::new().on_any(SqliteChangeOps::ALL, move |ev| {
1234                if ev.table_name == "triggered" && ev.op == SqliteChangeOp::Delete {
1235                    d2.lock().unwrap().push(ev.rowid);
1236                }
1237            }),
1238        );
1239
1240        // DELETE without WHERE, but triggers exist ⇒ no truncate optimization.
1241        crate::sql_query("DELETE FROM triggered")
1242            .execute(conn)
1243            .unwrap();
1244
1245        assert_eq!(
1246            deletes.lock().unwrap().len(),
1247            3,
1248            "with triggers present, DELETE without WHERE fires per-row hooks"
1249        );
1250    }
1251
1252    /// Modifications to internal system tables like sqlite_sequence
1253    /// (used by AUTOINCREMENT) do not trigger the update hook.
1254    /// See: https://www.sqlite.org/c3ref/update_hook.html
1255    #[diesel_test_helper::test]
1256    fn update_hook_silent_for_internal_sqlite_sequence() {
1257        use std::sync::{Arc, Mutex};
1258        let conn = &mut connection();
1259
1260        // AUTOINCREMENT causes SQLite to maintain sqlite_sequence.
1261        crate::sql_query(
1262            "CREATE TABLE seq_test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)",
1263        )
1264        .execute(conn)
1265        .unwrap();
1266
1267        let tables: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
1268        let t2 = tables.clone();
1269
1270        conn.on_update(
1271            SqliteUpdateRouter::new().on_any(SqliteChangeOps::ALL, move |ev| {
1272                t2.lock().unwrap().push(ev.table_name.to_owned());
1273            }),
1274        );
1275
1276        crate::sql_query("INSERT INTO seq_test (name) VALUES ('row1')")
1277            .execute(conn)
1278            .unwrap();
1279
1280        let recorded = tables.lock().unwrap();
1281        // Only the user table should appear, sqlite_sequence must be absent.
1282        assert!(
1283            recorded.iter().all(|t| t == "seq_test"),
1284            "expected only 'seq_test' events, got: {:?}",
1285            *recorded
1286        );
1287        assert!(
1288            !recorded.iter().any(|t| t == "sqlite_sequence"),
1289            "sqlite_sequence modifications must not trigger the update hook"
1290        );
1291    }
1292
1293    /// INSERT OR REPLACE on the primary key itself: when a row with the same
1294    /// PK already exists, the old row is silently deleted and the new row is
1295    /// inserted. The hook reports only the INSERT, not the implicit DELETE.
1296    #[diesel_test_helper::test]
1297    fn update_hook_silent_for_replace_into_on_pk_conflict() {
1298        use std::sync::{Arc, Mutex};
1299        let conn = &mut connection();
1300
1301        crate::sql_query("CREATE TABLE rep (id INTEGER PRIMARY KEY, val TEXT NOT NULL)")
1302            .execute(conn)
1303            .unwrap();
1304
1305        crate::sql_query("INSERT INTO rep (id, val) VALUES (1, 'old')")
1306            .execute(conn)
1307            .unwrap();
1308
1309        let events: Arc<Mutex<Vec<(SqliteChangeOp, i64)>>> = Arc::new(Mutex::new(Vec::new()));
1310        let e2 = events.clone();
1311
1312        conn.on_update(
1313            SqliteUpdateRouter::new().on_any(SqliteChangeOps::ALL, move |ev| {
1314                if ev.table_name == "rep" {
1315                    e2.lock().unwrap().push((ev.op, ev.rowid));
1316                }
1317            }),
1318        );
1319
1320        // REPLACE INTO with a conflicting PK.
1321        crate::sql_query("REPLACE INTO rep (id, val) VALUES (1, 'new')")
1322            .execute(conn)
1323            .unwrap();
1324
1325        let recorded = events.lock().unwrap();
1326        // Only one INSERT event, no DELETE for the old row.
1327        assert_eq!(
1328            recorded.len(),
1329            1,
1330            "expected 1 event for REPLACE INTO, got: {:?}",
1331            *recorded
1332        );
1333        assert_eq!(recorded[0].0, SqliteChangeOp::Insert);
1334        assert_eq!(recorded[0].1, 1);
1335    }
1336
1337    /// Regression test for the dangling-pointer soundness bug: after the
1338    /// connection is moved, a write still fires the registered callback.
1339    ///
1340    /// `sqlite3_update_hook` is handed the address of the connection's update
1341    /// hook state at registration time. Because that state is boxed, moving the
1342    /// (freely movable, `Sized`) `SqliteConnection` does not relocate it, so the
1343    /// pointer SQLite holds stays valid. On a buggy inline implementation the
1344    /// move would relocate the state and the trampoline would later dereference
1345    /// freed memory. This documents that the feature works through the move that
1346    /// real code performs (returning a connection, storing it in a struct,
1347    /// handing it to a pool, etc.).
1348    #[diesel_test_helper::test]
1349    fn change_hook_fires_after_connection_move() {
1350        use std::sync::{Arc, Mutex};
1351
1352        let count = Arc::new(Mutex::new(0u32));
1353        let count2 = count.clone();
1354
1355        let mut conn = connection();
1356        setup_hook_tables(&mut conn);
1357        conn.on_update(SqliteUpdateRouter::new().on(
1358            hook_users::table,
1359            SqliteChangeOps::INSERT,
1360            move |_| {
1361                *count2.lock().unwrap() += 1;
1362            },
1363        ));
1364
1365        // Move the connection onto the heap after the hook was registered.
1366        let mut boxed = Box::new(conn);
1367
1368        crate::sql_query("INSERT INTO hook_users (name) VALUES ('Alice')")
1369            .execute(&mut *boxed)
1370            .unwrap();
1371
1372        assert_eq!(
1373            *count.lock().unwrap(),
1374            1,
1375            "change hook did not fire after the connection was moved"
1376        );
1377    }
1378
1379    #[diesel_test_helper::test]
1380    fn router_filters_table_and_op() {
1381        use std::sync::{Arc, Mutex};
1382        let conn = &mut connection();
1383        setup_hook_tables(conn);
1384
1385        let fired = Arc::new(Mutex::new(Vec::new()));
1386        let fired2 = fired.clone();
1387
1388        // Fire for INSERT and UPDATE on hook_users only, not DELETE.
1389        conn.on_update(SqliteUpdateRouter::new().on(
1390            hook_users::table,
1391            SqliteChangeOps::INSERT | SqliteChangeOps::UPDATE,
1392            move |event| fired2.lock().unwrap().push(event.op),
1393        ));
1394
1395        crate::sql_query("INSERT INTO hook_users (name) VALUES ('Alice')")
1396            .execute(conn)
1397            .unwrap();
1398        crate::sql_query("UPDATE hook_users SET name = 'Bob' WHERE id = 1")
1399            .execute(conn)
1400            .unwrap();
1401        crate::sql_query("DELETE FROM hook_users WHERE id = 1")
1402            .execute(conn)
1403            .unwrap();
1404        // A change on a different table must not fire this hook.
1405        crate::sql_query("INSERT INTO hook_posts (title) VALUES ('Hello')")
1406            .execute(conn)
1407            .unwrap();
1408
1409        let events = fired.lock().unwrap().clone();
1410        assert_eq!(events, vec![SqliteChangeOp::Insert, SqliteChangeOp::Update]);
1411    }
1412
1413    #[diesel_test_helper::test]
1414    fn on_commit_fires_on_commit() {
1415        let conn = &mut connection();
1416
1417        let count = Arc::new(AtomicU32::new(0));
1418        let c2 = count.clone();
1419
1420        conn.on_commit(move || {
1421            c2.fetch_add(1, Ordering::Relaxed);
1422            CommitDecision::Proceed
1423        });
1424
1425        conn.immediate_transaction(|conn| {
1426            crate::sql_query("CREATE TABLE t1 (id INTEGER PRIMARY KEY)")
1427                .execute(conn)
1428                .unwrap();
1429            Ok::<_, crate::result::Error>(())
1430        })
1431        .unwrap();
1432
1433        assert_eq!(count.load(Ordering::Relaxed), 1);
1434    }
1435
1436    #[diesel_test_helper::test]
1437    fn on_commit_returning_true_forces_rollback() {
1438        let conn = &mut connection();
1439
1440        crate::sql_query("CREATE TABLE t_commit (id INTEGER PRIMARY KEY)")
1441            .execute(conn)
1442            .unwrap();
1443
1444        conn.on_commit(|| CommitDecision::Rollback);
1445
1446        // The transaction will attempt to commit, but the hook will convert
1447        // it to a rollback. diesel's AnsiTransactionManager will see the
1448        // failure from the COMMIT statement (sqlite returns an error when
1449        // the commit hook returns non-zero and the commit is aborted).
1450        let result = conn.immediate_transaction(|conn| {
1451            crate::sql_query("INSERT INTO t_commit (id) VALUES (1)")
1452                .execute(conn)
1453                .unwrap();
1454            Ok::<_, crate::result::Error>(())
1455        });
1456
1457        // The transaction should have been rolled back.
1458        assert!(result.is_err());
1459
1460        // Remove the hook so subsequent queries don't fail.
1461        conn.remove_commit_hook();
1462
1463        // Verify the row was not persisted.
1464        let cnt: i64 = crate::sql_query("SELECT COUNT(*) as c FROM t_commit")
1465            .get_result::<CountResult>(conn)
1466            .unwrap()
1467            .c;
1468        assert_eq!(cnt, 0);
1469    }
1470
1471    #[diesel_test_helper::test]
1472    fn replacing_commit_hook_drops_old() {
1473        let conn = &mut connection();
1474
1475        let old_count = Arc::new(AtomicU32::new(0));
1476        let new_count = Arc::new(AtomicU32::new(0));
1477        let oc = old_count.clone();
1478        let nc = new_count.clone();
1479
1480        conn.on_commit(move || {
1481            oc.fetch_add(1, Ordering::Relaxed);
1482            CommitDecision::Proceed
1483        });
1484
1485        // Replace with a new hook.
1486        conn.on_commit(move || {
1487            nc.fetch_add(1, Ordering::Relaxed);
1488            CommitDecision::Proceed
1489        });
1490
1491        conn.immediate_transaction(|conn| {
1492            crate::sql_query("CREATE TABLE t_replace (id INTEGER PRIMARY KEY)")
1493                .execute(conn)
1494                .unwrap();
1495            Ok::<_, crate::result::Error>(())
1496        })
1497        .unwrap();
1498
1499        assert_eq!(old_count.load(Ordering::Relaxed), 0);
1500        assert_eq!(new_count.load(Ordering::Relaxed), 1);
1501    }
1502
1503    #[diesel_test_helper::test]
1504    fn remove_commit_hook_disables_callback() {
1505        let conn = &mut connection();
1506
1507        let count = Arc::new(AtomicU32::new(0));
1508        let c2 = count.clone();
1509
1510        conn.on_commit(move || {
1511            c2.fetch_add(1, Ordering::Relaxed);
1512            CommitDecision::Proceed
1513        });
1514
1515        conn.remove_commit_hook();
1516
1517        conn.immediate_transaction(|conn| {
1518            crate::sql_query("CREATE TABLE t_rem (id INTEGER PRIMARY KEY)")
1519                .execute(conn)
1520                .unwrap();
1521            Ok::<_, crate::result::Error>(())
1522        })
1523        .unwrap();
1524
1525        assert_eq!(count.load(Ordering::Relaxed), 0);
1526    }
1527
1528    #[diesel_test_helper::test]
1529    fn on_rollback_fires_on_explicit_rollback() {
1530        let conn = &mut connection();
1531
1532        crate::sql_query("CREATE TABLE t_rb (id INTEGER PRIMARY KEY)")
1533            .execute(conn)
1534            .unwrap();
1535
1536        let count = Arc::new(AtomicU32::new(0));
1537        let c2 = count.clone();
1538
1539        conn.on_rollback(move || {
1540            c2.fetch_add(1, Ordering::Relaxed);
1541        });
1542
1543        // Force a rollback by returning Err from the transaction closure.
1544        let _ = conn.immediate_transaction(|conn| {
1545            crate::sql_query("INSERT INTO t_rb (id) VALUES (1)")
1546                .execute(conn)
1547                .unwrap();
1548            Err::<(), _>(crate::result::Error::RollbackTransaction)
1549        });
1550
1551        assert_eq!(count.load(Ordering::Relaxed), 1);
1552    }
1553
1554    #[diesel_test_helper::test]
1555    fn on_rollback_fires_when_commit_hook_forces_rollback() {
1556        let conn = &mut connection();
1557
1558        crate::sql_query("CREATE TABLE t_rb2 (id INTEGER PRIMARY KEY)")
1559            .execute(conn)
1560            .unwrap();
1561
1562        let rb_count = Arc::new(AtomicU32::new(0));
1563        let rb2 = rb_count.clone();
1564
1565        conn.on_commit(|| CommitDecision::Rollback);
1566        conn.on_rollback(move || {
1567            rb2.fetch_add(1, Ordering::Relaxed);
1568        });
1569
1570        let _ = conn.immediate_transaction(|conn| {
1571            crate::sql_query("INSERT INTO t_rb2 (id) VALUES (1)")
1572                .execute(conn)
1573                .unwrap();
1574            Ok::<_, crate::result::Error>(())
1575        });
1576
1577        // Rollback hook should have fired.
1578        assert_eq!(rb_count.load(Ordering::Relaxed), 1);
1579
1580        conn.remove_commit_hook();
1581        conn.remove_rollback_hook();
1582
1583        // Verify the row was not persisted.
1584        let cnt: i64 = crate::sql_query("SELECT COUNT(*) as c FROM t_rb2")
1585            .get_result::<CountResult>(conn)
1586            .unwrap()
1587            .c;
1588        assert_eq!(cnt, 0);
1589    }
1590
1591    #[diesel_test_helper::test]
1592    fn on_rollback_does_not_fire_on_connection_close() {
1593        let count = Arc::new(AtomicU32::new(0));
1594        let c2 = count.clone();
1595
1596        {
1597            let conn = &mut connection();
1598            conn.on_rollback(move || {
1599                c2.fetch_add(1, Ordering::Relaxed);
1600            });
1601            // conn is dropped here: implicit close, not a rollback.
1602        }
1603
1604        assert_eq!(count.load(Ordering::Relaxed), 0);
1605    }
1606
1607    #[diesel_test_helper::test]
1608    fn remove_rollback_hook_disables_callback() {
1609        let conn = &mut connection();
1610
1611        crate::sql_query("CREATE TABLE t_rem_rb (id INTEGER PRIMARY KEY)")
1612            .execute(conn)
1613            .unwrap();
1614
1615        let count = Arc::new(AtomicU32::new(0));
1616        let c2 = count.clone();
1617
1618        conn.on_rollback(move || {
1619            c2.fetch_add(1, Ordering::Relaxed);
1620        });
1621
1622        conn.remove_rollback_hook();
1623
1624        let _ = conn.immediate_transaction(|conn| {
1625            crate::sql_query("INSERT INTO t_rem_rb (id) VALUES (1)")
1626                .execute(conn)
1627                .unwrap();
1628            Err::<(), _>(crate::result::Error::RollbackTransaction)
1629        });
1630
1631        assert_eq!(count.load(Ordering::Relaxed), 0);
1632    }
1633
1634    // A recursive CTE heavy enough that the progress handler fires while it runs.
1635    const HEAVY_QUERY: &str = "WITH RECURSIVE c(x) AS \
1636        (SELECT 1 UNION ALL SELECT x + 1 FROM c WHERE x < 100000) SELECT count(*) FROM c";
1637
1638    #[diesel_test_helper::test]
1639    fn on_progress_interrupts_query() {
1640        let conn = &mut connection();
1641
1642        conn.on_progress(NonZeroU32::new(1).unwrap(), || ProgressDecision::Interrupt);
1643
1644        let result = crate::sql_query(HEAVY_QUERY).execute(conn);
1645        assert!(
1646            result.is_err(),
1647            "the query should be interrupted by the progress handler"
1648        );
1649    }
1650
1651    #[diesel_test_helper::test]
1652    fn remove_progress_handler_stops_interruption() {
1653        let conn = &mut connection();
1654
1655        conn.on_progress(NonZeroU32::new(1).unwrap(), || ProgressDecision::Interrupt);
1656        conn.remove_progress_handler();
1657
1658        // With the handler removed the same query runs to completion.
1659        let result = crate::sql_query(HEAVY_QUERY).execute(conn);
1660        assert!(
1661            result.is_ok(),
1662            "the query should complete after the handler is removed"
1663        );
1664    }
1665
1666    // WAL hook tests
1667    //
1668    // Gated out on WASM because these tests need a file-backed database
1669    // (WAL mode does not work with `:memory:`), and `tempfile::tempdir()`
1670    // panics on WASM due to the lack of a filesystem.
1671    //
1672    // The WAL API itself (`on_wal`, `remove_wal_hook`) is available on all
1673    // platforms, including WASM. The file-backed databases used here cannot
1674    // be created on WASM, so only the tests are gated out.
1675
1676    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1677    /// Helper: create a file-backed connection in WAL mode (WAL requires a real
1678    /// file, and every WAL test below wants the connection already in WAL mode).
1679    fn wal_connection() -> (SqliteConnection, tempfile::TempDir) {
1680        let dir = tempfile::tempdir().unwrap();
1681        let path = dir.path().join("test.db");
1682        let mut conn = SqliteConnection::establish(path.to_str().unwrap()).unwrap();
1683        crate::sql_query("PRAGMA journal_mode=WAL")
1684            .execute(&mut conn)
1685            .unwrap();
1686        (conn, dir)
1687    }
1688
1689    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1690    #[diesel_test_helper::test]
1691    fn on_wal_fires_in_wal_mode() {
1692        let (conn, _dir) = &mut wal_connection();
1693
1694        crate::sql_query("CREATE TABLE t_wal (id INTEGER PRIMARY KEY)")
1695            .execute(conn)
1696            .unwrap();
1697
1698        let events: Arc<std::sync::Mutex<Vec<(String, u32)>>> =
1699            Arc::new(std::sync::Mutex::new(Vec::new()));
1700        let events2 = events.clone();
1701
1702        conn.on_wal(move |_, db_name, n_pages| {
1703            events2.lock().unwrap().push((db_name.to_owned(), n_pages));
1704        });
1705
1706        crate::sql_query("INSERT INTO t_wal (id) VALUES (1)")
1707            .execute(conn)
1708            .unwrap();
1709
1710        let events = events.lock().unwrap();
1711        assert!(
1712            !events.is_empty(),
1713            "WAL hook should have fired at least once"
1714        );
1715        assert!(
1716            events.iter().all(|(db_name, _)| db_name == "main"),
1717            "db_name should always be \"main\""
1718        );
1719        assert!(
1720            events.iter().any(|(_, n_pages)| *n_pages > 0),
1721            "n_pages should be positive"
1722        );
1723    }
1724
1725    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1726    #[diesel_test_helper::test]
1727    fn replacing_wal_hook_drops_old() {
1728        let (conn, _dir) = &mut wal_connection();
1729
1730        crate::sql_query("CREATE TABLE t_wal2 (id INTEGER PRIMARY KEY)")
1731            .execute(conn)
1732            .unwrap();
1733
1734        let old_count = Arc::new(AtomicU32::new(0));
1735        let new_count = Arc::new(AtomicU32::new(0));
1736
1737        let c_old = old_count.clone();
1738        conn.on_wal(move |_, _, _| {
1739            c_old.fetch_add(1, Ordering::Relaxed);
1740        });
1741
1742        // Replace with a new hook.
1743        let c_new = new_count.clone();
1744        conn.on_wal(move |_, _, _| {
1745            c_new.fetch_add(1, Ordering::Relaxed);
1746        });
1747
1748        crate::sql_query("INSERT INTO t_wal2 (id) VALUES (1)")
1749            .execute(conn)
1750            .unwrap();
1751
1752        // Old hook should NOT have fired after replacement.
1753        let old_before = old_count.load(Ordering::Relaxed);
1754        crate::sql_query("INSERT INTO t_wal2 (id) VALUES (2)")
1755            .execute(conn)
1756            .unwrap();
1757        assert_eq!(
1758            old_count.load(Ordering::Relaxed),
1759            old_before,
1760            "old WAL hook should not fire after replacement"
1761        );
1762        assert!(
1763            new_count.load(Ordering::Relaxed) > 0,
1764            "new WAL hook should fire"
1765        );
1766    }
1767
1768    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1769    #[diesel_test_helper::test]
1770    fn remove_wal_hook_disables_callback() {
1771        let (conn, _dir) = &mut wal_connection();
1772
1773        crate::sql_query("CREATE TABLE t_wal3 (id INTEGER PRIMARY KEY)")
1774            .execute(conn)
1775            .unwrap();
1776
1777        let count = Arc::new(AtomicU32::new(0));
1778        let c2 = count.clone();
1779
1780        conn.on_wal(move |_, _, _| {
1781            c2.fetch_add(1, Ordering::Relaxed);
1782        });
1783
1784        conn.remove_wal_hook();
1785
1786        crate::sql_query("INSERT INTO t_wal3 (id) VALUES (1)")
1787            .execute(conn)
1788            .unwrap();
1789
1790        assert_eq!(count.load(Ordering::Relaxed), 0);
1791    }
1792
1793    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1794    #[diesel_test_helper::test]
1795    fn wal_hook_does_not_fire_in_default_journal_mode() {
1796        // A plain file connection left in the default journal mode ("delete"),
1797        // not WAL, so `wal_connection()` (which enables WAL) is not used here.
1798        let dir = tempfile::tempdir().unwrap();
1799        let path = dir.path().join("test.db");
1800        let conn = &mut SqliteConnection::establish(path.to_str().unwrap()).unwrap();
1801
1802        crate::sql_query("CREATE TABLE t_wal4 (id INTEGER PRIMARY KEY)")
1803            .execute(conn)
1804            .unwrap();
1805
1806        let count = Arc::new(AtomicU32::new(0));
1807        let c2 = count.clone();
1808
1809        conn.on_wal(move |_, _, _| {
1810            c2.fetch_add(1, Ordering::Relaxed);
1811        });
1812
1813        crate::sql_query("INSERT INTO t_wal4 (id) VALUES (1)")
1814            .execute(conn)
1815            .unwrap();
1816
1817        assert_eq!(
1818            count.load(Ordering::Relaxed),
1819            0,
1820            "WAL hook should not fire when not in WAL mode"
1821        );
1822    }
1823
1824    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1825    #[diesel_test_helper::test]
1826    fn on_wal_can_use_borrowed_connection() {
1827        let (conn, _dir) = &mut wal_connection();
1828
1829        crate::sql_query("CREATE TABLE t_wal_use (id INTEGER PRIMARY KEY)")
1830            .execute(conn)
1831            .unwrap();
1832
1833        let counts: Arc<std::sync::Mutex<Vec<i64>>> = Arc::new(std::sync::Mutex::new(Vec::new()));
1834        let counts2 = counts.clone();
1835
1836        conn.on_wal(move |conn, _db_name, _n_pages| {
1837            // Read through the borrowed connection, a fresh implicit read
1838            // transaction that finalizes on return.
1839            let c = crate::sql_query("SELECT COUNT(*) AS c FROM t_wal_use")
1840                .get_result::<CountResult>(conn)
1841                .unwrap()
1842                .c;
1843            counts2.lock().unwrap().push(c);
1844        });
1845
1846        crate::sql_query("INSERT INTO t_wal_use (id) VALUES (1)")
1847            .execute(conn)
1848            .unwrap();
1849
1850        let observed = counts.lock().unwrap();
1851        assert!(!observed.is_empty(), "WAL hook should have fired");
1852        assert!(
1853            observed.contains(&1),
1854            "callback should observe the committed row through the connection"
1855        );
1856    }
1857
1858    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1859    #[diesel_test_helper::test]
1860    fn on_wal_callback_write_re_enters_hook() {
1861        let (conn, _dir) = &mut wal_connection();
1862
1863        crate::sql_query("CREATE TABLE t_wal_re (id INTEGER PRIMARY KEY)")
1864            .execute(conn)
1865            .unwrap();
1866        crate::sql_query("CREATE TABLE t_wal_log (id INTEGER PRIMARY KEY AUTOINCREMENT)")
1867            .execute(conn)
1868            .unwrap();
1869
1870        let calls = Arc::new(AtomicU32::new(0));
1871        let calls2 = calls.clone();
1872
1873        conn.on_wal(move |conn, _db_name, _n_pages| {
1874            let n = calls2.fetch_add(1, Ordering::Relaxed);
1875            // Write on the first invocation only. The write commits in WAL mode
1876            // and re-fires the hook re-entrantly. Gating on `n == 0` bounds the
1877            // recursion to a single nested call instead of overflowing the stack.
1878            if n == 0 {
1879                crate::sql_query("INSERT INTO t_wal_log DEFAULT VALUES")
1880                    .execute(conn)
1881                    .unwrap();
1882            }
1883        });
1884
1885        crate::sql_query("INSERT INTO t_wal_re (id) VALUES (1)")
1886            .execute(conn)
1887            .unwrap();
1888
1889        // The outer commit fired the hook, and the write inside it re-fired the
1890        // hook once more, so the `Fn` callback ran twice.
1891        assert_eq!(
1892            calls.load(Ordering::Relaxed),
1893            2,
1894            "a committing write inside the callback re-enters the hook"
1895        );
1896
1897        // The write performed inside the callback took effect.
1898        let logged: i64 = crate::sql_query("SELECT COUNT(*) AS c FROM t_wal_log")
1899            .get_result::<CountResult>(conn)
1900            .unwrap()
1901            .c;
1902        assert_eq!(
1903            logged, 1,
1904            "the write performed inside the callback should persist"
1905        );
1906    }
1907
1908    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1909    #[diesel_test_helper::test]
1910    fn on_wal_fires_once_per_transaction_commit() {
1911        let (conn, _dir) = &mut wal_connection();
1912
1913        crate::sql_query("CREATE TABLE t_wal_txn (id INTEGER PRIMARY KEY)")
1914            .execute(conn)
1915            .unwrap();
1916
1917        let count = Arc::new(AtomicU32::new(0));
1918        let c2 = count.clone();
1919
1920        conn.on_wal(move |_, _, _| {
1921            c2.fetch_add(1, Ordering::Relaxed);
1922        });
1923
1924        // Several writes inside one explicit transaction commit together, so the
1925        // WAL hook fires once for the single commit, not once per statement.
1926        conn.immediate_transaction(|conn| {
1927            crate::sql_query("INSERT INTO t_wal_txn (id) VALUES (1)").execute(conn)?;
1928            crate::sql_query("INSERT INTO t_wal_txn (id) VALUES (2)").execute(conn)?;
1929            crate::sql_query("INSERT INTO t_wal_txn (id) VALUES (3)").execute(conn)?;
1930            Ok::<_, crate::result::Error>(())
1931        })
1932        .unwrap();
1933
1934        assert_eq!(
1935            count.load(Ordering::Relaxed),
1936            1,
1937            "the WAL hook should fire once per transaction commit, not per statement"
1938        );
1939    }
1940
1941    // Busy handler test.
1942    //
1943    // Gated out on WASM: it needs two connections to a shared file-backed
1944    // database (`:memory:` connections do not share a lock), and
1945    // `tempfile::tempdir()` panics on WASM due to the lack of a filesystem.
1946    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1947    #[diesel_test_helper::test]
1948    fn on_busy_handler_is_invoked_on_lock_contention() {
1949        let dir = tempfile::tempdir().unwrap();
1950        let path = dir.path().join("busy.db");
1951        let url = path.to_str().unwrap();
1952
1953        // One connection acquires and holds a write lock.
1954        let mut holder = SqliteConnection::establish(url).unwrap();
1955        crate::sql_query("CREATE TABLE t_busy (id INTEGER PRIMARY KEY)")
1956            .execute(&mut holder)
1957            .unwrap();
1958        crate::sql_query("BEGIN IMMEDIATE")
1959            .execute(&mut holder)
1960            .unwrap();
1961
1962        // A second connection registers a busy handler that records the call
1963        // and gives up.
1964        let mut contender = SqliteConnection::establish(url).unwrap();
1965        let calls = Arc::new(AtomicU32::new(0));
1966        let calls2 = calls.clone();
1967        contender.on_busy(move |_retry_count| {
1968            calls2.fetch_add(1, Ordering::Relaxed);
1969            BusyDecision::GiveUp
1970        });
1971
1972        // The write contends with the held lock, so the busy handler fires.
1973        // Because it gives up, the write fails instead of blocking.
1974        let result = crate::sql_query("INSERT INTO t_busy (id) VALUES (1)").execute(&mut contender);
1975
1976        assert!(
1977            result.is_err(),
1978            "the contended write should fail once the busy handler gives up"
1979        );
1980        assert!(
1981            calls.load(Ordering::Relaxed) >= 1,
1982            "the busy handler should have been invoked at least once"
1983        );
1984    }
1985
1986    #[diesel_test_helper::test]
1987    fn on_authorize_deny_rejects_statement() {
1988        let conn = &mut connection();
1989        crate::sql_query("CREATE TABLE auth_basic (id INTEGER PRIMARY KEY)")
1990            .execute(conn)
1991            .unwrap();
1992
1993        let calls = Arc::new(AtomicU32::new(0));
1994        let calls2 = calls.clone();
1995
1996        conn.on_authorize(move |_ctx| {
1997            calls2.fetch_add(1, Ordering::Relaxed);
1998            AuthorizerDecision::Deny
1999        });
2000
2001        // The authorizer is consulted while the statement is prepared, denies
2002        // it, and the statement is rejected.
2003        let denied = crate::sql_query("SELECT id FROM auth_basic").execute(conn);
2004        assert!(denied.is_err(), "a denied statement should fail to prepare");
2005        assert!(
2006            calls.load(Ordering::Relaxed) > 0,
2007            "the authorizer callback should have been invoked"
2008        );
2009
2010        // Removing the authorizer restores access.
2011        conn.remove_authorizer();
2012        crate::sql_query("SELECT id FROM auth_basic")
2013            .execute(conn)
2014            .unwrap();
2015    }
2016
2017    #[diesel_test_helper::test]
2018    fn remove_authorizer_re_prepares_cached_statements() {
2019        use crate::prelude::*;
2020        use crate::sqlite::AuthorizerContext;
2021
2022        crate::table! {
2023            auth_ignore_items (id) {
2024                id -> Integer,
2025            }
2026        }
2027
2028        let conn = &mut connection();
2029        crate::sql_query("CREATE TABLE auth_ignore_items (id INTEGER PRIMARY KEY)")
2030            .execute(conn)
2031            .unwrap();
2032        crate::sql_query("INSERT INTO auth_ignore_items (id) VALUES (42)")
2033            .execute(conn)
2034            .unwrap();
2035
2036        // An authorizer that ignores column reads. SQLite bakes a NULL
2037        // substitution for the column into the prepared (and cached) statement.
2038        // A typed query is used because raw `sql_query` is never cached.
2039        conn.on_authorize(|ctx| match ctx {
2040            AuthorizerContext::Read(_) => AuthorizerDecision::Ignore,
2041            _ => AuthorizerDecision::Allow,
2042        });
2043        let ignored = auth_ignore_items::table
2044            .select(auth_ignore_items::id.nullable())
2045            .load::<Option<i32>>(conn)
2046            .unwrap();
2047        assert_eq!(
2048            ignored,
2049            vec![None],
2050            "Ignore substitutes NULL for the column"
2051        );
2052
2053        // SQLite does not expire prepared statements when an authorizer is
2054        // removed, so `remove_authorizer` clears diesel's statement cache to
2055        // force the cached statement (with its baked-in NULL) to be re-prepared.
2056        conn.remove_authorizer();
2057        let restored = auth_ignore_items::table
2058            .select(auth_ignore_items::id.nullable())
2059            .load::<Option<i32>>(conn)
2060            .unwrap();
2061        assert_eq!(
2062            restored,
2063            vec![Some(42)],
2064            "after removing the authorizer the real value is returned"
2065        );
2066    }
2067
2068    #[diesel_test_helper::test]
2069    fn on_authorize_re_prepares_cached_statements() {
2070        use crate::prelude::*;
2071        use crate::sqlite::AuthorizerContext;
2072
2073        crate::table! {
2074            auth_replace_items (id) {
2075                id -> Integer,
2076            }
2077        }
2078
2079        let conn = &mut connection();
2080        crate::sql_query("CREATE TABLE auth_replace_items (id INTEGER PRIMARY KEY)")
2081            .execute(conn)
2082            .unwrap();
2083        crate::sql_query("INSERT INTO auth_replace_items (id) VALUES (42)")
2084            .execute(conn)
2085            .unwrap();
2086
2087        // A first authorizer that allows everything. The typed query is
2088        // prepared and cached while it is active, returning the real value.
2089        conn.on_authorize(|_ctx| AuthorizerDecision::Allow);
2090        let allowed = auth_replace_items::table
2091            .select(auth_replace_items::id.nullable())
2092            .load::<Option<i32>>(conn)
2093            .unwrap();
2094        assert_eq!(
2095            allowed,
2096            vec![Some(42)],
2097            "the allow-all authorizer returns the real value"
2098        );
2099
2100        // Installing the replacement authorizer expires the cached statement
2101        // (SQLite expires all prepared statements when a non-null authorizer is
2102        // set), so `sqlite3_prepare_v3` re-prepares it under the new authorizer
2103        // and it now yields NULL. This documents that no diesel-side cache clear
2104        // is required on the install path.
2105        conn.on_authorize(|ctx| match ctx {
2106            AuthorizerContext::Read(_) => AuthorizerDecision::Ignore,
2107            _ => AuthorizerDecision::Allow,
2108        });
2109        let ignored = auth_replace_items::table
2110            .select(auth_replace_items::id.nullable())
2111            .load::<Option<i32>>(conn)
2112            .unwrap();
2113        assert_eq!(
2114            ignored,
2115            vec![None],
2116            "after replacing the authorizer the new decision takes effect"
2117        );
2118    }
2119
2120    #[diesel_test_helper::test]
2121    fn on_trace_reports_statement_and_profile() {
2122        use std::sync::Mutex;
2123
2124        let conn = &mut connection();
2125        crate::sql_query("CREATE TABLE t_trace (id INTEGER PRIMARY KEY)")
2126            .execute(conn)
2127            .unwrap();
2128
2129        // (sql, readonly) for Statement events, and the SQL of Profile events.
2130        let stmts: Arc<Mutex<Vec<(String, bool)>>> = Arc::new(Mutex::new(Vec::new()));
2131        let profiled: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
2132        let stmts2 = stmts.clone();
2133        let profiled2 = profiled.clone();
2134
2135        conn.on_trace(
2136            SqliteTraceFlags::STMT | SqliteTraceFlags::PROFILE,
2137            move |event| match event {
2138                SqliteTraceEvent::Statement { sql, readonly } => {
2139                    stmts2.lock().unwrap().push((sql.to_owned(), readonly));
2140                }
2141                SqliteTraceEvent::Profile { sql, .. } => {
2142                    profiled2.lock().unwrap().push(sql.to_owned());
2143                }
2144                _ => {}
2145            },
2146        );
2147
2148        crate::sql_query("SELECT id FROM t_trace")
2149            .execute(conn)
2150            .unwrap();
2151        crate::sql_query("INSERT INTO t_trace (id) VALUES (1)")
2152            .execute(conn)
2153            .unwrap();
2154
2155        let stmts = stmts.lock().unwrap();
2156        assert!(
2157            stmts
2158                .iter()
2159                .any(|(sql, ro)| sql.contains("SELECT id FROM t_trace") && *ro),
2160            "the SELECT should be traced and reported read-only"
2161        );
2162        assert!(
2163            stmts
2164                .iter()
2165                .any(|(sql, ro)| sql.contains("INSERT INTO t_trace") && !*ro),
2166            "the INSERT should be traced and reported not read-only"
2167        );
2168        assert!(
2169            !profiled.lock().unwrap().is_empty(),
2170            "at least one Profile event should have fired"
2171        );
2172    }
2173
2174    #[diesel_test_helper::test]
2175    fn remove_trace_stops_events() {
2176        use std::sync::atomic::AtomicUsize;
2177
2178        let conn = &mut connection();
2179        let count: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
2180        let count2 = count.clone();
2181
2182        conn.on_trace(SqliteTraceFlags::STMT, move |_event| {
2183            count2.fetch_add(1, Ordering::Relaxed);
2184        });
2185        crate::sql_query("SELECT 1").execute(conn).unwrap();
2186        let after_first = count.load(Ordering::Relaxed);
2187        assert!(after_first > 0, "trace should fire while registered");
2188
2189        conn.remove_trace();
2190        crate::sql_query("SELECT 1").execute(conn).unwrap();
2191        assert_eq!(
2192            count.load(Ordering::Relaxed),
2193            after_first,
2194            "no trace events should fire after remove_trace"
2195        );
2196    }
2197
2198    #[diesel_test_helper::test]
2199    fn on_collation_needed_registration_is_safe() {
2200        let conn = &mut connection();
2201
2202        conn.on_collation_needed(|_conn, _ctx| {});
2203        conn.remove_collation_needed_hook();
2204
2205        crate::sql_query("SELECT 1").execute(conn).unwrap();
2206    }
2207
2208    #[diesel_test_helper::test]
2209    fn replacing_collation_needed_hook_drops_old() {
2210        use std::sync::atomic::AtomicBool;
2211
2212        let conn = &mut connection();
2213
2214        let first_fired = Arc::new(AtomicBool::new(false));
2215        let first_fired2 = first_fired.clone();
2216        conn.on_collation_needed(move |_conn, _ctx| {
2217            first_fired2.store(true, Ordering::Relaxed);
2218        });
2219
2220        let second_fired = Arc::new(AtomicBool::new(false));
2221        let second_fired2 = second_fired.clone();
2222        conn.on_collation_needed(move |conn, ctx| {
2223            conn.register_collation(ctx.name, |a, b| a.cmp(b)).unwrap();
2224            second_fired2.store(true, Ordering::Relaxed);
2225        });
2226
2227        // `CREATE INDEX ... COLLATE FOO` forces SQLite to resolve FOO. A bare
2228        // `SELECT ... COLLATE FOO` does not.
2229        crate::sql_query("CREATE TABLE t_replace (x TEXT)")
2230            .execute(conn)
2231            .unwrap();
2232        crate::sql_query("CREATE INDEX i_replace ON t_replace (x COLLATE REPLACE_ME_COLL)")
2233            .execute(conn)
2234            .unwrap();
2235
2236        assert!(
2237            !first_fired.load(Ordering::Relaxed),
2238            "the replaced hook must not have been invoked"
2239        );
2240        assert!(
2241            second_fired.load(Ordering::Relaxed),
2242            "the current hook should have fired"
2243        );
2244    }
2245
2246    #[diesel_test_helper::test]
2247    fn collation_needed_fires_and_registers_collation() {
2248        use crate::sqlite::SqliteTextRep;
2249        use std::sync::atomic::AtomicBool;
2250
2251        let conn = &mut connection();
2252
2253        // The exact name is verified indirectly by `register_collation(ctx.name, ...)`.
2254        // If the callback saw the wrong name, the retry for MYCOLL would still fail.
2255        let fired = Arc::new(AtomicBool::new(false));
2256        let saw_name = Arc::new(AtomicBool::new(false));
2257        let saw_utf8 = Arc::new(AtomicBool::new(false));
2258        let fired2 = fired.clone();
2259        let saw_name2 = saw_name.clone();
2260        let saw_utf8_2 = saw_utf8.clone();
2261
2262        conn.on_collation_needed(move |conn, ctx| {
2263            fired2.store(true, Ordering::Relaxed);
2264            if ctx.name == "MYCOLL" {
2265                saw_name2.store(true, Ordering::Relaxed);
2266            }
2267            if ctx.text_rep == SqliteTextRep::Utf8 {
2268                saw_utf8_2.store(true, Ordering::Relaxed);
2269            }
2270            conn.register_collation(ctx.name, |a, b| a.cmp(b)).unwrap();
2271        });
2272
2273        // See `replacing_collation_needed_hook_drops_old` for why CREATE INDEX.
2274        crate::sql_query("CREATE TABLE t_fires (x TEXT)")
2275            .execute(conn)
2276            .unwrap();
2277        crate::sql_query("CREATE INDEX i_fires ON t_fires (x COLLATE MYCOLL)")
2278            .execute(conn)
2279            .unwrap();
2280
2281        assert!(
2282            fired.load(Ordering::Relaxed),
2283            "collation_needed callback should fire"
2284        );
2285        assert!(
2286            saw_name.load(Ordering::Relaxed),
2287            "callback should observe the exact missing collation name"
2288        );
2289        assert!(
2290            saw_utf8.load(Ordering::Relaxed),
2291            "SQLite should request the UTF-8 encoding for a plain TEXT column"
2292        );
2293    }
2294
2295    #[diesel_test_helper::test]
2296    fn remove_collation_needed_hook_without_registration_is_noop() {
2297        let conn = &mut connection();
2298
2299        conn.remove_collation_needed_hook();
2300        crate::sql_query("SELECT 1").execute(conn).unwrap();
2301    }
2302
2303    #[diesel_test_helper::test]
2304    fn callback_can_be_reentered_from_within_its_own_body() {
2305        use std::sync::atomic::AtomicBool;
2306
2307        let conn = &mut connection();
2308
2309        // Both flips must land: the outer callback fires for OUTER_COLL, and
2310        // then the SQL it executes internally triggers a second callback for
2311        // INNER_COLL while the outer callback frame is still on the stack.
2312        // This is the scenario the `Fn` (not `FnMut`) bound is designed to
2313        // support.
2314        let saw_outer = Arc::new(AtomicBool::new(false));
2315        let saw_inner = Arc::new(AtomicBool::new(false));
2316        let saw_outer2 = saw_outer.clone();
2317        let saw_inner2 = saw_inner.clone();
2318
2319        conn.on_collation_needed(move |conn, ctx| {
2320            if ctx.name.eq_ignore_ascii_case("OUTER_COLL") {
2321                saw_outer2.store(true, Ordering::Relaxed);
2322                conn.register_collation("OUTER_COLL", |a, b| a.cmp(b))
2323                    .unwrap();
2324                // From inside our own frame, drive SQL that needs INNER_COLL,
2325                // which is also unregistered. SQLite must be able to call the
2326                // trampoline re-entrantly to satisfy this.
2327                crate::sql_query("CREATE TABLE t_reent_inner (x TEXT)")
2328                    .execute(conn)
2329                    .unwrap();
2330                crate::sql_query(
2331                    "CREATE INDEX i_reent_inner ON t_reent_inner (x COLLATE INNER_COLL)",
2332                )
2333                .execute(conn)
2334                .unwrap();
2335            } else if ctx.name.eq_ignore_ascii_case("INNER_COLL") {
2336                saw_inner2.store(true, Ordering::Relaxed);
2337                conn.register_collation("INNER_COLL", |a, b| a.cmp(b))
2338                    .unwrap();
2339            } else {
2340                panic!("unexpected collation name: {}", ctx.name);
2341            }
2342        });
2343
2344        crate::sql_query("CREATE TABLE t_reent_outer (x TEXT)")
2345            .execute(conn)
2346            .unwrap();
2347        crate::sql_query("CREATE INDEX i_reent_outer ON t_reent_outer (x COLLATE OUTER_COLL)")
2348            .execute(conn)
2349            .unwrap();
2350
2351        assert!(
2352            saw_outer.load(Ordering::Relaxed),
2353            "outer callback should fire for OUTER_COLL"
2354        );
2355        assert!(
2356            saw_inner.load(Ordering::Relaxed),
2357            "inner callback should fire re-entrantly from within the outer body"
2358        );
2359    }
2360
2361    #[diesel_test_helper::test]
2362    fn remove_collation_needed_hook_stops_future_callbacks() {
2363        let conn = &mut connection();
2364        let calls: Arc<AtomicU32> = Arc::new(AtomicU32::new(0));
2365        let calls2 = calls.clone();
2366
2367        conn.on_collation_needed(move |conn, ctx| {
2368            calls2.fetch_add(1, Ordering::Relaxed);
2369            conn.register_collation(ctx.name, |a, b| a.cmp(b)).unwrap();
2370        });
2371
2372        // First trigger: callback fires and installs MYCOLL_STOP.
2373        crate::sql_query("CREATE TABLE t_stop (x TEXT)")
2374            .execute(conn)
2375            .unwrap();
2376        crate::sql_query("CREATE INDEX i_stop ON t_stop (x COLLATE MYCOLL_STOP)")
2377            .execute(conn)
2378            .unwrap();
2379        let after_first = calls.load(Ordering::Relaxed);
2380        assert!(after_first > 0, "callback should fire while registered");
2381
2382        conn.remove_collation_needed_hook();
2383
2384        // Second trigger with a fresh unregistered collation: SQL must fail
2385        // (nothing left to install YOURCOLL_STOP) and the counter must not
2386        // move. A regression that only drops the Rust box while leaving the
2387        // C-side pointer registered would call into freed memory here.
2388        let result = crate::sql_query("CREATE INDEX i_stop2 ON t_stop (x COLLATE YOURCOLL_STOP)")
2389            .execute(conn);
2390        assert!(
2391            result.is_err(),
2392            "SQL referencing an unregistered collation should fail after remove"
2393        );
2394        assert_eq!(
2395            calls.load(Ordering::Relaxed),
2396            after_first,
2397            "callback must not fire after remove_collation_needed_hook"
2398        );
2399    }
2400}