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")))]
4use libsqlite3_sys as ffi;
5#[cfg(all(target_family = "wasm", target_os = "unknown"))]
6use sqlite_wasm_rs as ffi;
7
8use super::SqliteConnection;
9use alloc::boxed::Box;
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]
    #[rustc_trivial_field_reads]
    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_struct_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]
    #[rustc_trivial_field_reads]
    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]
    #[rustc_trivial_field_reads]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f,
            "CollationNeededContext name text_rep", &self.name,
            &&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]
    #[rustc_trivial_field_reads]
    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 [`CollationNeededContext`] naming the missing
57    /// collation and returns its comparator, which Diesel registers under that
58    /// name before SQLite retries the lookup. Returning `None` leaves the
59    /// collation missing, so the statement fails.
60    ///
61    /// Only one callback is active at a time, and re-registering replaces it.
62    /// Panics in the callback abort the process.
63    ///
64    /// Added in SQLite 3.0.0.
65    ///
66    /// See: [`sqlite3_collation_needed`](https://www.sqlite.org/c3ref/collation_needed.html)
67    ///
68    /// # Example
69    ///
70    /// ```rust
71    /// # use diesel::prelude::*;
72    /// # use diesel::sqlite::SqliteConnection;
73    /// # let conn = &mut SqliteConnection::establish(":memory:").unwrap();
74    /// conn.on_collation_needed(|ctx| {
75    ///     if ctx.name.eq_ignore_ascii_case("RUSTNOCASE") {
76    ///         Some(Box::new(|a: &str, b: &str| {
77    ///             a.to_lowercase().cmp(&b.to_lowercase())
78    ///         }))
79    ///     } else {
80    ///         None
81    ///     }
82    /// });
83    ///
84    /// // Later: remove the callback
85    /// conn.remove_collation_needed_hook();
86    /// ```
87    pub fn on_collation_needed<F>(&mut self, hook: F)
88    where
89        F: Fn(
90                CollationNeededContext<'_>,
91            ) -> Option<
92                Box<dyn Fn(&str, &str) -> core::cmp::Ordering + Send + core::panic::UnwindSafe>,
93            > + Send
94            + 'static,
95    {
96        self.raw_connection.set_collation_needed_hook(hook);
97    }
98
99    /// Removes the collation-needed callback.
100    ///
101    /// See [`on_collation_needed`](Self::on_collation_needed) for usage
102    /// example.
103    pub fn remove_collation_needed_hook(&mut self) {
104        self.raw_connection.remove_collation_needed_hook();
105    }
106}
107
108// miri doesn't support callbacks yet
109#[cfg(all(test, not(miri)))]
110mod tests {
111    use super::*;
112    use crate::connection::Connection;
113    use crate::query_dsl::RunQueryDsl;
114    use std::sync::Arc;
115    use std::sync::atomic::{AtomicU32, Ordering};
116
117    #[diesel_test_helper::test]
118    fn from_ffi_maps_all_documented_variants() {
119        assert_eq!(
120            SqliteTextRep::from_ffi(ffi::SQLITE_UTF8),
121            SqliteTextRep::Utf8
122        );
123        assert_eq!(
124            SqliteTextRep::from_ffi(ffi::SQLITE_UTF16BE),
125            SqliteTextRep::Utf16Be,
126        );
127        assert_eq!(
128            SqliteTextRep::from_ffi(ffi::SQLITE_UTF16LE),
129            SqliteTextRep::Utf16Le,
130        );
131    }
132
133    #[diesel_test_helper::test]
134    fn from_ffi_preserves_unknown_encodings_in_other() {
135        // 999 is not any current SQLite eTextRep. If SQLite ever assigns it,
136        // this test starts failing and the enum learns a new named variant.
137        assert_eq!(SqliteTextRep::from_ffi(999), SqliteTextRep::Other(999));
138    }
139
140    fn connection() -> SqliteConnection {
141        SqliteConnection::establish(":memory:").unwrap()
142    }
143
144    fn binary_order()
145    -> Option<Box<dyn Fn(&str, &str) -> core::cmp::Ordering + Send + core::panic::UnwindSafe>> {
146        Some(Box::new(|a: &str, b: &str| a.cmp(b)))
147    }
148
149    #[diesel_test_helper::test]
150    fn on_collation_needed_registration_is_safe() {
151        let conn = &mut connection();
152
153        conn.on_collation_needed(|_ctx| None);
154        conn.remove_collation_needed_hook();
155
156        crate::sql_query("SELECT 1").execute(conn).unwrap();
157    }
158
159    #[diesel_test_helper::test]
160    fn replacing_collation_needed_hook_drops_old() {
161        use std::sync::atomic::AtomicBool;
162
163        let conn = &mut connection();
164
165        let first_fired = Arc::new(AtomicBool::new(false));
166        let first_fired2 = first_fired.clone();
167        conn.on_collation_needed(move |_ctx| {
168            first_fired2.store(true, Ordering::Relaxed);
169            None
170        });
171
172        let second_fired = Arc::new(AtomicBool::new(false));
173        let second_fired2 = second_fired.clone();
174        conn.on_collation_needed(move |_ctx| {
175            second_fired2.store(true, Ordering::Relaxed);
176            binary_order()
177        });
178
179        // `CREATE INDEX ... COLLATE FOO` forces SQLite to resolve FOO. A bare
180        // `SELECT ... COLLATE FOO` does not.
181        crate::sql_query("CREATE TABLE t_replace (x TEXT)")
182            .execute(conn)
183            .unwrap();
184        crate::sql_query("CREATE INDEX i_replace ON t_replace (x COLLATE REPLACE_ME_COLL)")
185            .execute(conn)
186            .unwrap();
187
188        assert!(
189            !first_fired.load(Ordering::Relaxed),
190            "the replaced hook must not have been invoked"
191        );
192        assert!(
193            second_fired.load(Ordering::Relaxed),
194            "the current hook should have fired"
195        );
196    }
197
198    #[diesel_test_helper::test]
199    fn collation_needed_fires_and_registers_collation() {
200        use crate::sqlite::SqliteTextRep;
201        use std::sync::atomic::AtomicBool;
202
203        let conn = &mut connection();
204
205        // Diesel registers the comparator under `ctx.name`, so a wrong name would still fail the retry for MYCOLL.
206        let fired = Arc::new(AtomicBool::new(false));
207        let saw_name = Arc::new(AtomicBool::new(false));
208        let saw_utf8 = Arc::new(AtomicBool::new(false));
209        let fired2 = fired.clone();
210        let saw_name2 = saw_name.clone();
211        let saw_utf8_2 = saw_utf8.clone();
212
213        conn.on_collation_needed(move |ctx| {
214            fired2.store(true, Ordering::Relaxed);
215            if ctx.name == "MYCOLL" {
216                saw_name2.store(true, Ordering::Relaxed);
217            }
218            if ctx.text_rep == SqliteTextRep::Utf8 {
219                saw_utf8_2.store(true, Ordering::Relaxed);
220            }
221            binary_order()
222        });
223
224        // See `replacing_collation_needed_hook_drops_old` for why CREATE INDEX.
225        crate::sql_query("CREATE TABLE t_fires (x TEXT)")
226            .execute(conn)
227            .unwrap();
228        crate::sql_query("CREATE INDEX i_fires ON t_fires (x COLLATE MYCOLL)")
229            .execute(conn)
230            .unwrap();
231
232        assert!(
233            fired.load(Ordering::Relaxed),
234            "collation_needed callback should fire"
235        );
236        assert!(
237            saw_name.load(Ordering::Relaxed),
238            "callback should observe the exact missing collation name"
239        );
240        assert!(
241            saw_utf8.load(Ordering::Relaxed),
242            "SQLite should request the UTF-8 encoding for a plain TEXT column"
243        );
244    }
245
246    #[diesel_test_helper::test]
247    fn remove_collation_needed_hook_without_registration_is_noop() {
248        let conn = &mut connection();
249
250        conn.remove_collation_needed_hook();
251        crate::sql_query("SELECT 1").execute(conn).unwrap();
252    }
253
254    #[diesel_test_helper::test]
255    fn returned_comparator_serves_the_requested_collation_and_none_leaves_it_missing() {
256        use crate::collation::Custom;
257        use crate::prelude::*;
258
259        crate::table! {
260            words (id) {
261                id -> Integer,
262                word -> Text,
263            }
264        }
265
266        let conn = &mut connection();
267        let calls = Arc::new(AtomicU32::new(0));
268        let calls2 = calls.clone();
269        conn.on_collation_needed(move |ctx| {
270            calls2.fetch_add(1, Ordering::Relaxed);
271            (ctx.name == "MYNOCASE").then(|| {
272                Box::new(|a: &str, b: &str| a.to_lowercase().cmp(&b.to_lowercase()))
273                    as Box<
274                        dyn Fn(&str, &str) -> core::cmp::Ordering + Send + core::panic::UnwindSafe,
275                    >
276            })
277        });
278        crate::sql_query("CREATE TABLE words (id INTEGER PRIMARY KEY, word TEXT NOT NULL)")
279            .execute(conn)
280            .unwrap();
281        crate::insert_into(words::table)
282            .values([words::word.eq("a"), words::word.eq("b")])
283            .execute(conn)
284            .unwrap();
285
286        let matching = |collation| {
287            words::table
288                .select(words::word)
289                .filter(words::word.collate(Custom(collation)).eq("A"))
290        };
291        assert_eq!(matching("MYNOCASE").load::<String>(conn).unwrap(), ["a"]);
292        assert_eq!(matching("MYNOCASE").load::<String>(conn).unwrap(), ["a"]);
293        assert_eq!(calls.load(Ordering::Relaxed), 1);
294        assert!(matching("UNKNOWN").load::<String>(conn).is_err());
295    }
296
297    #[diesel_test_helper::test]
298    fn remove_collation_needed_hook_stops_future_callbacks() {
299        let conn = &mut connection();
300        let calls: Arc<AtomicU32> = Arc::new(AtomicU32::new(0));
301        let calls2 = calls.clone();
302
303        conn.on_collation_needed(move |_ctx| {
304            calls2.fetch_add(1, Ordering::Relaxed);
305            binary_order()
306        });
307
308        // First trigger: callback fires and installs MYCOLL_STOP.
309        crate::sql_query("CREATE TABLE t_stop (x TEXT)")
310            .execute(conn)
311            .unwrap();
312        crate::sql_query("CREATE INDEX i_stop ON t_stop (x COLLATE MYCOLL_STOP)")
313            .execute(conn)
314            .unwrap();
315        let after_first = calls.load(Ordering::Relaxed);
316        assert!(after_first > 0, "callback should fire while registered");
317
318        conn.remove_collation_needed_hook();
319
320        // Second trigger with a fresh unregistered collation: SQL must fail
321        // (nothing left to install YOURCOLL_STOP) and the counter must not
322        // move. A regression that only drops the Rust box while leaving the
323        // C-side pointer registered would call into freed memory here.
324        let result = crate::sql_query("CREATE INDEX i_stop2 ON t_stop (x COLLATE YOURCOLL_STOP)")
325            .execute(conn);
326        assert!(
327            result.is_err(),
328            "SQL referencing an unregistered collation should fail after remove"
329        );
330        assert_eq!(
331            calls.load(Ordering::Relaxed),
332            after_first,
333            "callback must not fire after remove_collation_needed_hook"
334        );
335    }
336}