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