Skip to main content

diesel/sqlite/connection/
collation_needed.rs

1//! Types used by [`SqliteConnection::on_collation_needed`](super::SqliteConnection::on_collation_needed).
2
3#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
4extern crate libsqlite3_sys as ffi;
5
6#[cfg(all(target_family = "wasm", target_os = "unknown"))]
7use sqlite_wasm_rs as ffi;
8
9use super::SqliteConnection;
10
11/// Text encoding SQLite requested for a missing collation.
12///
13/// [`register_collation`](super::SqliteConnection::register_collation) always
14/// installs `SQLITE_UTF8`, so most callbacks can ignore this field.
15#[derive(#[automatically_derived]
impl ::core::fmt::Debug for SqliteTextRep {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            Self::Utf8 => ::core::fmt::Formatter::write_str(f, "Utf8"),
            Self::Utf16Be => ::core::fmt::Formatter::write_str(f, "Utf16Be"),
            Self::Utf16Le => ::core::fmt::Formatter::write_str(f, "Utf16Le"),
            Self::Other(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Other",
                    &__self_0),
        }
    }
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for SqliteTextRep { }
#[automatically_derived]
impl ::core::clone::Clone for SqliteTextRep {
    #[inline]
    fn clone(&self) -> Self {
        let _: ::core::clone::AssertParamIsClone<i32>;
        *self
    }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for SqliteTextRep { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for SqliteTextRep { }
#[automatically_derived]
impl ::core::cmp::PartialEq for SqliteTextRep {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        ::core::intrinsics::discriminant_value(self) ==
                ::core::intrinsics::discriminant_value(other) &&
            match (self, other) {
                (Self::Other(__self_0), Self::Other(__arg1_0)) =>
                    __self_0 == __arg1_0,
                _ => true,
            }
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for SqliteTextRep {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<i32>;
    }
}Eq)]
16#[non_exhaustive]
17pub enum SqliteTextRep {
18    /// `SQLITE_UTF8`.
19    Utf8,
20    /// `SQLITE_UTF16BE`.
21    Utf16Be,
22    /// `SQLITE_UTF16LE`.
23    Utf16Le,
24    /// An encoding this release does not name. Preserves the raw
25    /// `eTextRep` for forward compatibility. Treat the inner integer as
26    /// opaque, and match a named variant if a future Diesel release adds one.
27    Other(i32),
28}
29
30impl SqliteTextRep {
31    pub(super) fn from_ffi(text_rep: i32) -> Self {
32        match text_rep {
33            ffi::SQLITE_UTF8 => SqliteTextRep::Utf8,
34            ffi::SQLITE_UTF16BE => SqliteTextRep::Utf16Be,
35            ffi::SQLITE_UTF16LE => SqliteTextRep::Utf16Le,
36            other => SqliteTextRep::Other(other),
37        }
38    }
39}
40
41/// Context passed to the collation-needed callback.
42///
43/// Added in SQLite 3.0.0.
44#[derive(#[automatically_derived]
impl<'a> ::core::fmt::Debug for CollationNeededContext<'a> {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f,
            "CollationNeededContext", "name", &self.name, "text_rep",
            &&self.text_rep)
    }
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl<'a> ::core::clone::TrivialClone for CollationNeededContext<'a> { }
#[automatically_derived]
impl<'a> ::core::clone::Clone for CollationNeededContext<'a> {
    #[inline]
    fn clone(&self) -> Self {
        let _: ::core::clone::AssertParamIsClone<&'a str>;
        let _: ::core::clone::AssertParamIsClone<SqliteTextRep>;
        *self
    }
}Clone, #[automatically_derived]
impl<'a> ::core::marker::Copy for CollationNeededContext<'a> { }Copy)]
45#[non_exhaustive]
46pub struct CollationNeededContext<'a> {
47    /// The name of the missing collation, as UTF-8.
48    pub name: &'a str,
49    /// Preferred text encoding for the collation.
50    pub text_rep: SqliteTextRep,
51}
52
53impl SqliteConnection {
54    /// Registers a callback fired when SQLite encounters an unknown collation.
55    ///
56    /// The callback receives a borrowed `&mut SqliteConnection` and a
57    /// [`CollationNeededContext`] naming the missing collation. It should
58    /// install the missing collation via
59    /// [`register_collation`](Self::register_collation) and return, after
60    /// which SQLite retries the lookup. The callback body may also execute
61    /// arbitrary SQL through `conn`, provided it leaves no open transaction.
62    ///
63    /// Only one callback is active at a time, and re-registering replaces it.
64    /// Panics in the callback abort the process.
65    ///
66    /// If the callback registers a collation that is itself missing, SQLite
67    /// re-enters the callback. Guard against unbounded recursion in that case.
68    ///
69    /// Added in SQLite 3.0.0.
70    ///
71    /// See: [`sqlite3_collation_needed`](https://www.sqlite.org/c3ref/collation_needed.html)
72    ///
73    /// # Example
74    ///
75    /// ```rust
76    /// # use diesel::prelude::*;
77    /// # use diesel::sqlite::SqliteConnection;
78    /// # let conn = &mut SqliteConnection::establish(":memory:").unwrap();
79    /// conn.on_collation_needed(|conn, ctx| {
80    ///     if ctx.name.eq_ignore_ascii_case("RUSTNOCASE") {
81    ///         let _ = conn.register_collation("RUSTNOCASE", |a, b| {
82    ///             a.to_lowercase().cmp(&b.to_lowercase())
83    ///         });
84    ///     }
85    /// });
86    ///
87    /// // Later: remove the callback
88    /// conn.remove_collation_needed_hook();
89    /// ```
90    pub fn on_collation_needed<F>(&mut self, hook: F)
91    where
92        F: Fn(&mut SqliteConnection, CollationNeededContext<'_>) + Send + 'static,
93    {
94        self.raw_connection.set_collation_needed_hook(hook);
95    }
96
97    /// Removes the collation-needed callback.
98    ///
99    /// See [`on_collation_needed`](Self::on_collation_needed) for usage
100    /// example.
101    pub fn remove_collation_needed_hook(&mut self) {
102        self.raw_connection.remove_collation_needed_hook();
103    }
104}
105
106// miri doesn't support callbacks yet
107#[cfg(all(test, not(miri)))]
108mod tests {
109    use super::*;
110    use crate::connection::Connection;
111    use crate::query_dsl::RunQueryDsl;
112    use std::sync::Arc;
113    use std::sync::atomic::{AtomicU32, Ordering};
114
115    #[diesel_test_helper::test]
116    fn from_ffi_maps_all_documented_variants() {
117        assert_eq!(
118            SqliteTextRep::from_ffi(ffi::SQLITE_UTF8),
119            SqliteTextRep::Utf8
120        );
121        assert_eq!(
122            SqliteTextRep::from_ffi(ffi::SQLITE_UTF16BE),
123            SqliteTextRep::Utf16Be,
124        );
125        assert_eq!(
126            SqliteTextRep::from_ffi(ffi::SQLITE_UTF16LE),
127            SqliteTextRep::Utf16Le,
128        );
129    }
130
131    #[diesel_test_helper::test]
132    fn from_ffi_preserves_unknown_encodings_in_other() {
133        // 999 is not any current SQLite eTextRep. If SQLite ever assigns it,
134        // this test starts failing and the enum learns a new named variant.
135        assert_eq!(SqliteTextRep::from_ffi(999), SqliteTextRep::Other(999));
136    }
137
138    fn connection() -> SqliteConnection {
139        SqliteConnection::establish(":memory:").unwrap()
140    }
141
142    #[diesel_test_helper::test]
143    fn on_collation_needed_registration_is_safe() {
144        let conn = &mut connection();
145
146        conn.on_collation_needed(|_conn, _ctx| {});
147        conn.remove_collation_needed_hook();
148
149        crate::sql_query("SELECT 1").execute(conn).unwrap();
150    }
151
152    #[diesel_test_helper::test]
153    fn replacing_collation_needed_hook_drops_old() {
154        use std::sync::atomic::AtomicBool;
155
156        let conn = &mut connection();
157
158        let first_fired = Arc::new(AtomicBool::new(false));
159        let first_fired2 = first_fired.clone();
160        conn.on_collation_needed(move |_conn, _ctx| {
161            first_fired2.store(true, Ordering::Relaxed);
162        });
163
164        let second_fired = Arc::new(AtomicBool::new(false));
165        let second_fired2 = second_fired.clone();
166        conn.on_collation_needed(move |conn, ctx| {
167            conn.register_collation(ctx.name, |a, b| a.cmp(b)).unwrap();
168            second_fired2.store(true, Ordering::Relaxed);
169        });
170
171        // `CREATE INDEX ... COLLATE FOO` forces SQLite to resolve FOO. A bare
172        // `SELECT ... COLLATE FOO` does not.
173        crate::sql_query("CREATE TABLE t_replace (x TEXT)")
174            .execute(conn)
175            .unwrap();
176        crate::sql_query("CREATE INDEX i_replace ON t_replace (x COLLATE REPLACE_ME_COLL)")
177            .execute(conn)
178            .unwrap();
179
180        assert!(
181            !first_fired.load(Ordering::Relaxed),
182            "the replaced hook must not have been invoked"
183        );
184        assert!(
185            second_fired.load(Ordering::Relaxed),
186            "the current hook should have fired"
187        );
188    }
189
190    #[diesel_test_helper::test]
191    fn collation_needed_fires_and_registers_collation() {
192        use crate::sqlite::SqliteTextRep;
193        use std::sync::atomic::AtomicBool;
194
195        let conn = &mut connection();
196
197        // The exact name is verified indirectly by `register_collation(ctx.name, ...)`.
198        // If the callback saw the wrong name, the retry for MYCOLL would still fail.
199        let fired = Arc::new(AtomicBool::new(false));
200        let saw_name = Arc::new(AtomicBool::new(false));
201        let saw_utf8 = Arc::new(AtomicBool::new(false));
202        let fired2 = fired.clone();
203        let saw_name2 = saw_name.clone();
204        let saw_utf8_2 = saw_utf8.clone();
205
206        conn.on_collation_needed(move |conn, ctx| {
207            fired2.store(true, Ordering::Relaxed);
208            if ctx.name == "MYCOLL" {
209                saw_name2.store(true, Ordering::Relaxed);
210            }
211            if ctx.text_rep == SqliteTextRep::Utf8 {
212                saw_utf8_2.store(true, Ordering::Relaxed);
213            }
214            conn.register_collation(ctx.name, |a, b| a.cmp(b)).unwrap();
215        });
216
217        // See `replacing_collation_needed_hook_drops_old` for why CREATE INDEX.
218        crate::sql_query("CREATE TABLE t_fires (x TEXT)")
219            .execute(conn)
220            .unwrap();
221        crate::sql_query("CREATE INDEX i_fires ON t_fires (x COLLATE MYCOLL)")
222            .execute(conn)
223            .unwrap();
224
225        assert!(
226            fired.load(Ordering::Relaxed),
227            "collation_needed callback should fire"
228        );
229        assert!(
230            saw_name.load(Ordering::Relaxed),
231            "callback should observe the exact missing collation name"
232        );
233        assert!(
234            saw_utf8.load(Ordering::Relaxed),
235            "SQLite should request the UTF-8 encoding for a plain TEXT column"
236        );
237    }
238
239    #[diesel_test_helper::test]
240    fn remove_collation_needed_hook_without_registration_is_noop() {
241        let conn = &mut connection();
242
243        conn.remove_collation_needed_hook();
244        crate::sql_query("SELECT 1").execute(conn).unwrap();
245    }
246
247    #[diesel_test_helper::test]
248    fn callback_can_be_reentered_from_within_its_own_body() {
249        use std::sync::atomic::AtomicBool;
250
251        let conn = &mut connection();
252
253        // Both flips must land: the outer callback fires for OUTER_COLL, and
254        // then the SQL it executes internally triggers a second callback for
255        // INNER_COLL while the outer callback frame is still on the stack.
256        // This is the scenario the `Fn` (not `FnMut`) bound is designed to
257        // support.
258        let saw_outer = Arc::new(AtomicBool::new(false));
259        let saw_inner = Arc::new(AtomicBool::new(false));
260        let saw_outer2 = saw_outer.clone();
261        let saw_inner2 = saw_inner.clone();
262
263        conn.on_collation_needed(move |conn, ctx| {
264            if ctx.name.eq_ignore_ascii_case("OUTER_COLL") {
265                saw_outer2.store(true, Ordering::Relaxed);
266                conn.register_collation("OUTER_COLL", |a, b| a.cmp(b))
267                    .unwrap();
268                // From inside our own frame, drive SQL that needs INNER_COLL,
269                // which is also unregistered. SQLite must be able to call the
270                // trampoline re-entrantly to satisfy this.
271                crate::sql_query("CREATE TABLE t_reent_inner (x TEXT)")
272                    .execute(conn)
273                    .unwrap();
274                crate::sql_query(
275                    "CREATE INDEX i_reent_inner ON t_reent_inner (x COLLATE INNER_COLL)",
276                )
277                .execute(conn)
278                .unwrap();
279            } else if ctx.name.eq_ignore_ascii_case("INNER_COLL") {
280                saw_inner2.store(true, Ordering::Relaxed);
281                conn.register_collation("INNER_COLL", |a, b| a.cmp(b))
282                    .unwrap();
283            } else {
284                panic!("unexpected collation name: {}", ctx.name);
285            }
286        });
287
288        crate::sql_query("CREATE TABLE t_reent_outer (x TEXT)")
289            .execute(conn)
290            .unwrap();
291        crate::sql_query("CREATE INDEX i_reent_outer ON t_reent_outer (x COLLATE OUTER_COLL)")
292            .execute(conn)
293            .unwrap();
294
295        assert!(
296            saw_outer.load(Ordering::Relaxed),
297            "outer callback should fire for OUTER_COLL"
298        );
299        assert!(
300            saw_inner.load(Ordering::Relaxed),
301            "inner callback should fire re-entrantly from within the outer body"
302        );
303    }
304
305    #[diesel_test_helper::test]
306    fn remove_collation_needed_hook_stops_future_callbacks() {
307        let conn = &mut connection();
308        let calls: Arc<AtomicU32> = Arc::new(AtomicU32::new(0));
309        let calls2 = calls.clone();
310
311        conn.on_collation_needed(move |conn, ctx| {
312            calls2.fetch_add(1, Ordering::Relaxed);
313            conn.register_collation(ctx.name, |a, b| a.cmp(b)).unwrap();
314        });
315
316        // First trigger: callback fires and installs MYCOLL_STOP.
317        crate::sql_query("CREATE TABLE t_stop (x TEXT)")
318            .execute(conn)
319            .unwrap();
320        crate::sql_query("CREATE INDEX i_stop ON t_stop (x COLLATE MYCOLL_STOP)")
321            .execute(conn)
322            .unwrap();
323        let after_first = calls.load(Ordering::Relaxed);
324        assert!(after_first > 0, "callback should fire while registered");
325
326        conn.remove_collation_needed_hook();
327
328        // Second trigger with a fresh unregistered collation: SQL must fail
329        // (nothing left to install YOURCOLL_STOP) and the counter must not
330        // move. A regression that only drops the Rust box while leaving the
331        // C-side pointer registered would call into freed memory here.
332        let result = crate::sql_query("CREATE INDEX i_stop2 ON t_stop (x COLLATE YOURCOLL_STOP)")
333            .execute(conn);
334        assert!(
335            result.is_err(),
336            "SQL referencing an unregistered collation should fail after remove"
337        );
338        assert_eq!(
339            calls.load(Ordering::Relaxed),
340            after_first,
341            "callback must not fire after remove_collation_needed_hook"
342        );
343    }
344}