diesel/sqlite/connection/
collation_needed.rs1#[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#[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 Utf8,
20 Utf16Be,
22 Utf16Le,
24 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#[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 pub name: &'a str,
49 pub text_rep: SqliteTextRep,
51}
52
53impl SqliteConnection {
54 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 pub fn remove_collation_needed_hook(&mut self) {
104 self.raw_connection.remove_collation_needed_hook();
105 }
106}
107
108#[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 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 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 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 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 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 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}