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