diff --git a/.travis.yml b/.travis.yml index 1cc4963..f0cc6fe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,7 @@ script: - cargo test - cargo test --features backup - cargo test --features blob + - cargo test --features collation - cargo test --features functions - cargo test --features hooks - cargo test --features limits @@ -44,7 +45,7 @@ script: - cargo test --features uuid - cargo test --features "unlock_notify bundled" - cargo test --features "array bundled csvtab vtab" - - cargo test --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace url uuid vtab" - - cargo test --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace url uuid vtab buildtime_bindgen" - - cargo test --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace url uuid vtab bundled" - - cargo test --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace url uuid vtab bundled buildtime_bindgen" + - cargo test --features "backup blob chrono collation csvtab functions hooks limits load_extension serde_json trace url uuid vtab" + - cargo test --features "backup blob chrono collation csvtab functions hooks limits load_extension serde_json trace url uuid vtab buildtime_bindgen" + - cargo test --features "backup blob chrono collation csvtab functions hooks limits load_extension serde_json trace url uuid vtab bundled" + - cargo test --features "backup blob chrono collation csvtab functions hooks limits load_extension serde_json trace url uuid vtab bundled buildtime_bindgen" diff --git a/Cargo.toml b/Cargo.toml index 40e2031..be6cdc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ load_extension = [] backup = ["libsqlite3-sys/min_sqlite_version_3_6_23"] # sqlite3_blob_reopen: 3.7.4 blob = ["libsqlite3-sys/min_sqlite_version_3_7_7"] +collation = [] # sqlite3_create_function_v2: 3.7.3 (2010-10-08) functions = ["libsqlite3-sys/min_sqlite_version_3_7_7"] # sqlite3_log: 3.6.23 (2010-03-09) @@ -69,6 +70,7 @@ tempdir = "0.3" lazy_static = "1.0" regex = "1.0" uuid = { version = "0.7", features = ["v4"] } +unicase = "2.4.0" [dependencies.libsqlite3-sys] path = "libsqlite3-sys" diff --git a/appveyor.yml b/appveyor.yml index 469e493..d260e38 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -33,7 +33,7 @@ build: false test_script: - cargo test --lib --verbose - cargo test --lib --verbose --features bundled - - cargo test --lib --features "backup blob chrono functions hooks limits load_extension serde_json trace" + - cargo test --lib --features "backup blob chrono collation functions hooks limits load_extension serde_json trace" - cargo test --lib --features "backup blob chrono functions hooks limits load_extension serde_json trace buildtime_bindgen" - cargo test --lib --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace vtab bundled" - cargo test --lib --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace vtab bundled buildtime_bindgen" diff --git a/src/collation.rs b/src/collation.rs new file mode 100644 index 0000000..03e9837 --- /dev/null +++ b/src/collation.rs @@ -0,0 +1,140 @@ +//! Add, remove, or modify a collation +use std::cmp::Ordering; +use std::os::raw::{c_int, c_void}; +use std::panic::{catch_unwind, UnwindSafe}; +use std::ptr; +use std::slice; + +use crate::ffi; +use crate::{str_to_cstring, Connection, InnerConnection, Result}; + +// TODO sqlite3_collation_needed https://sqlite.org/c3ref/collation_needed.html + +// FIXME copy/paste from function.rs +unsafe extern "C" fn free_boxed_value(p: *mut c_void) { + drop(Box::from_raw(p as *mut T)); +} + +impl Connection { + /// Add or modify a collation. + pub fn create_collation(&self, collation_name: &str, x_compare: C) -> Result<()> + where + C: Fn(&str, &str) -> Ordering + Send + UnwindSafe + 'static, + { + self.db + .borrow_mut() + .create_collation(collation_name, x_compare) + } + + /// Remove collation. + pub fn remove_collation(&self, collation_name: &str) -> Result<()> { + self.db.borrow_mut().remove_collation(collation_name) + } +} + +impl InnerConnection { + fn create_collation(&mut self, collation_name: &str, x_compare: C) -> Result<()> + where + C: Fn(&str, &str) -> Ordering + Send + UnwindSafe + 'static, + { + unsafe extern "C" fn call_boxed_closure( + arg1: *mut c_void, + arg2: c_int, + arg3: *const c_void, + arg4: c_int, + arg5: *const c_void, + ) -> c_int + where + F: Fn(&str, &str) -> Ordering, + { + use std::str; + + let r = catch_unwind(|| { + let boxed_f: *mut F = arg1 as *mut F; + assert!(!boxed_f.is_null(), "Internal error - null function pointer"); + let s1 = { + let c_slice = slice::from_raw_parts(arg3 as *const u8, arg2 as usize); + str::from_utf8_unchecked(c_slice) + }; + let s2 = { + let c_slice = slice::from_raw_parts(arg5 as *const u8, arg4 as usize); + str::from_utf8_unchecked(c_slice) + }; + (*boxed_f)(s1, s2) + }); + let t = match r { + Err(_) => { + return -1; // FIXME How ? + } + Ok(r) => r, + }; + + match t { + Ordering::Less => -1, + Ordering::Equal => 0, + Ordering::Greater => 1, + } + } + + let boxed_f: *mut C = Box::into_raw(Box::new(x_compare)); + let c_name = str_to_cstring(collation_name)?; + let flags = ffi::SQLITE_UTF8; + let r = unsafe { + ffi::sqlite3_create_collation_v2( + self.db(), + c_name.as_ptr(), + flags, + boxed_f as *mut c_void, + Some(call_boxed_closure::), + Some(free_boxed_value::), + ) + }; + self.decode_result(r) + } + + fn remove_collation(&mut self, collation_name: &str) -> Result<()> { + let c_name = str_to_cstring(collation_name)?; + let r = unsafe { + ffi::sqlite3_create_collation_v2( + self.db(), + c_name.as_ptr(), + ffi::SQLITE_UTF8, + ptr::null_mut(), + None, + None, + ) + }; + self.decode_result(r) + } +} + +#[cfg(test)] +mod test { + use crate::{Connection, NO_PARAMS}; + use fallible_streaming_iterator::FallibleStreamingIterator; + use std::cmp::Ordering; + use unicase::UniCase; + + fn unicase_compare(s1: &str, s2: &str) -> Ordering { + UniCase::new(s1).cmp(&UniCase::new(s2)) + } + + #[test] + fn test_unicase() { + let db = Connection::open_in_memory().unwrap(); + + db.create_collation("unicase", unicase_compare).unwrap(); + + db.execute_batch( + "CREATE TABLE foo (bar); + INSERT INTO foo (bar) VALUES ('Maße'); + INSERT INTO foo (bar) VALUES ('MASSE');", + ) + .unwrap(); + let mut stmt = db + .prepare("SELECT DISTINCT bar COLLATE unicase FROM foo ORDER BY 1") + .unwrap(); + let rows = stmt.query(NO_PARAMS).unwrap(); + assert_eq!(rows.count().unwrap(), 1); + } +} diff --git a/src/lib.rs b/src/lib.rs index 7fd98f3..9e38bdb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,6 +105,8 @@ pub mod backup; pub mod blob; mod busy; mod cache; +#[cfg(feature = "collation")] +mod collation; mod column; pub mod config; #[cfg(any(feature = "functions", feature = "vtab"))]