| #![deny(missing_docs, clippy::pedantic)] | |
| #![allow(clippy::cast_sign_loss)] // some lengths returned from sqlite3 are `i32`, but rust needs `usize` | |
| //! Here be dragons | |
| //! | |
| //! We need to register a custom REGEX implementation for sqlite | |
| //! some useful resources: | |
| //! - rusqlite has an example implementation: <https://docs.rs/rusqlite/0.28.0/rusqlite/functions/index.html> | |
| //! - sqlite supports registering custom C functions: <https://www.sqlite.org/c3ref/create_function.html> | |
| //! - sqlite also supports a `A REGEXP B` syntax, but ONLY if the user implements `regex(B, A)` | |
| //! - Note that A and B are indeed swapped: the regex comes first, the field comes second | |
| //! - <https://www.sqlite.org/lang_expr.html#regexp> | |
| //! - sqlx has a way to safely get a sqlite3 pointer: | |
| //! - <https://docs.rs/sqlx/0.6.2/sqlx/sqlite/struct.SqliteConnection.html#method.lock_handle> | |
| //! - <https://docs.rs/sqlx/0.6.2/sqlx/sqlite/struct.LockedSqliteHandle.html#method.as_raw_handle> | |
| use libsqlite3_sys as ffi; | |
| use log::error; | |
| use regex::Regex; | |
| use std::sync::Arc; | |
| /// The function name for sqlite3. This must be "regexp\0" | |
| static FN_NAME: &[u8] = b"regexp\0"; | |
| /// Register the regex function with sqlite. | |
| /// | |
| /// Returns the result code of `sqlite3_create_function_v2` | |
| pub fn register(sqlite3: *mut ffi::sqlite3) -> i32 { | |
| unsafe { | |
| ffi::sqlite3_create_function_v2( | |
| // the database connection | |
| sqlite3, | |
| // the function name. Must be up to 255 bytes, and 0-terminated | |
| FN_NAME.as_ptr().cast(), | |
| // the number of arguments this function accepts. We want 2 arguments: The regex and the field | |
| 2, | |
| // we want all our strings to be UTF8, and this function will return the same output with the same inputs | |
| ffi::SQLITE_UTF8 | ffi::SQLITE_DETERMINISTIC, | |
| // pointer to user data. We're not using user data | |
| std::ptr::null_mut(), | |
| // xFunc to be executed when we are invoked | |
| Some(sqlite3_regexp_func), | |
| // xStep, should be NULL for scalar functions | |
| None, | |
| // xFinal, should be NULL for scalar functions | |
| None, | |
| // xDestroy, called when this function is deregistered. Should be used to clean up our pointer to user-data | |
| None, | |
| ) | |
| } | |
| } | |
| /// A function to be called on each invocation of `regex(REGEX, FIELD)` from sqlite3 | |
| /// | |
| /// - `ctx`: a pointer to the current sqlite3 context | |
| /// - `n_arg`: The length of `args` | |
| /// - `args`: the arguments of this function call | |
| unsafe extern "C" fn sqlite3_regexp_func( | |
| ctx: *mut ffi::sqlite3_context, | |
| n_arg: i32, | |
| args: *mut *mut ffi::sqlite3_value, | |
| ) { | |
| // check the arg size. sqlite3 should already ensure this is only 2 args but we want to double check | |
| if n_arg != 2 { | |
| eprintln!("n_arg expected to be 2, is {n_arg}"); | |
| ffi::sqlite3_result_error_code(ctx, ffi::SQLITE_CONSTRAINT_FUNCTION); | |
| return; | |
| } | |
| // arg0: Regex | |
| let Some(regex) = get_regex_from_arg(ctx, *args, 0) else { | |
| return; | |
| }; | |
| // arg1: value | |
| let Some(value) = get_text_from_arg(ctx, *args.add(1)) else { | |
| return; | |
| }; | |
| // if the regex matches the value, set the result int as 1, else as 0 | |
| if regex.is_match(value) { | |
| ffi::sqlite3_result_int(ctx, 1); | |
| } else { | |
| ffi::sqlite3_result_int(ctx, 0); | |
| } | |
| } | |
| /// Get the regex from the given `arg` at the given `index`. | |
| /// | |
| /// First this will check to see if the value exists in sqlite's `auxdata`. If it does, that regex will be returned. | |
| /// sqlite is able to clean up this data at any point, but rust's [`Arc`] guarantees make sure things don't break. | |
| /// | |
| /// If this value does not exist in `auxdata`, [`try_load_value`] is called and a regex is created from this. If any of | |
| /// those fail, a message is printed and `None` is returned. | |
| /// | |
| /// After this regex is created it is stored in `auxdata` and loaded again. If it fails to load, this means that | |
| /// something inside of sqlite3 went wrong, and we return `None`. | |
| /// | |
| /// If this value is stored correctly, or if it already existed, the arc reference counter is increased and this value is returned. | |
| unsafe fn get_regex_from_arg( | |
| ctx: *mut ffi::sqlite3_context, | |
| arg: *mut ffi::sqlite3_value, | |
| index: i32, | |
| ) -> Option<Arc<Regex>> { | |
| // try to get the auxdata for this field | |
| let ptr = ffi::sqlite3_get_auxdata(ctx, index); | |
| if !ptr.is_null() { | |
| // if we have it, turn it into an Arc. | |
| // we need to make sure to call `increment_strong_count` because the returned `Arc` decrement this when it goes out of scope | |
| let ptr = ptr as *const Regex; | |
| Arc::increment_strong_count(ptr); | |
| return Some(Arc::from_raw(ptr)); | |
| } | |
| // get the text for this field | |
| let value = get_text_from_arg(ctx, arg)?; | |
| // try to compile it into a regex | |
| let regex = match Regex::new(value) { | |
| Ok(regex) => Arc::new(regex), | |
| Err(e) => { | |
| error!("Invalid regex {value:?}: {e:?}"); | |
| ffi::sqlite3_result_error_code(ctx, ffi::SQLITE_CONSTRAINT_FUNCTION); | |
| return None; | |
| } | |
| }; | |
| // set the regex as auxdata for the next time around | |
| ffi::sqlite3_set_auxdata( | |
| ctx, | |
| index, | |
| // make sure to call `Arc::clone` here, setting the strong count to 2. | |
| // this will be cleaned up at 2 points: | |
| // - when the returned arc goes out of scope | |
| // - when sqlite decides to clean it up an calls `cleanup_arc_regex_pointer` | |
| Arc::into_raw(Arc::clone(®ex)) as *mut _, | |
| Some(cleanup_arc_regex_pointer), | |
| ); | |
| Some(regex) | |
| } | |
| /// Get a text reference of the value of `arg`. Returns `None` for NULL values. | |
| /// | |
| /// For non-NULL values, `sqlite3_value_text()` is called directly, which lets SQLite | |
| /// coerce INTEGER, REAL, and BLOB values to their text representation. This matches | |
| /// the coercion behavior documented at <https://www.sqlite.org/c3ref/value_blob.html>. | |
| /// | |
| /// The returned `&str` is valid for lifetime `'a` which can be determined by the caller. This lifetime should **not** | |
| /// outlive `ctx`. | |
| unsafe fn get_text_from_arg<'a>( | |
| ctx: *mut ffi::sqlite3_context, | |
| arg: *mut ffi::sqlite3_value, | |
| ) -> Option<&'a str> { | |
| let ty = ffi::sqlite3_value_type(arg); | |
| if ty == ffi::SQLITE_NULL { | |
| return None; | |
| } | |
| let ptr = ffi::sqlite3_value_text(arg); | |
| let len = ffi::sqlite3_value_bytes(arg); | |
| let slice = std::slice::from_raw_parts(ptr.cast(), len as usize); | |
| match std::str::from_utf8(slice) { | |
| Ok(result) => Some(result), | |
| Err(e) => { | |
| log::error!("Incoming text is not valid UTF8: {e:?}"); | |
| ffi::sqlite3_result_error_code(ctx, ffi::SQLITE_CONSTRAINT_FUNCTION); | |
| None | |
| } | |
| } | |
| } | |
| /// Clean up the `Arc<Regex>` that is stored in the given `ptr`. | |
| unsafe extern "C" fn cleanup_arc_regex_pointer(ptr: *mut std::ffi::c_void) { | |
| Arc::decrement_strong_count(ptr.cast::<Regex>()); | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use sqlx::{ConnectOptions, Row}; | |
| use std::str::FromStr; | |
| async fn test_db() -> crate::SqliteConnection { | |
| let mut conn = crate::SqliteConnectOptions::from_str("sqlite://:memory:") | |
| .unwrap() | |
| .with_regexp() | |
| .connect() | |
| .await | |
| .unwrap(); | |
| sqlx::query("CREATE TABLE test (col TEXT NOT NULL)") | |
| .execute(&mut conn) | |
| .await | |
| .unwrap(); | |
| for i in 0..10 { | |
| sqlx::query("INSERT INTO test VALUES (?)") | |
| .bind(format!("value {i}")) | |
| .execute(&mut conn) | |
| .await | |
| .unwrap(); | |
| } | |
| conn | |
| } | |
| #[sqlx::test] | |
| async fn test_regexp_does_not_fail() { | |
| let mut conn = test_db().await; | |
| let result = sqlx::query("SELECT col FROM test WHERE col REGEXP 'foo.*bar'") | |
| .fetch_all(&mut conn) | |
| .await | |
| .expect("Could not execute query"); | |
| assert!(result.is_empty()); | |
| } | |
| #[sqlx::test] | |
| async fn test_regexp_filters_correctly() { | |
| let mut conn = test_db().await; | |
| let result = sqlx::query("SELECT col FROM test WHERE col REGEXP '.*2'") | |
| .fetch_all(&mut conn) | |
| .await | |
| .expect("Could not execute query"); | |
| assert_eq!(result.len(), 1); | |
| assert_eq!(result[0].get::<String, usize>(0), String::from("value 2")); | |
| let result = sqlx::query("SELECT col FROM test WHERE col REGEXP '^3'") | |
| .fetch_all(&mut conn) | |
| .await | |
| .expect("Could not execute query"); | |
| assert!(result.is_empty()); | |
| } | |
| #[sqlx::test] | |
| async fn test_regexp_coerces_non_text_values() { | |
| let mut conn = crate::SqliteConnectOptions::from_str("sqlite://:memory:") | |
| .unwrap() | |
| .with_regexp() | |
| .connect() | |
| .await | |
| .unwrap(); | |
| // INTEGER coercion | |
| let result: Option<i32> = sqlx::query_scalar("SELECT 123 REGEXP '23'") | |
| .fetch_one(&mut conn) | |
| .await | |
| .unwrap(); | |
| assert_eq!(result, Some(1)); | |
| // REAL coercion | |
| let result: Option<i32> = sqlx::query_scalar("SELECT 12.5 REGEXP '12\\.5'") | |
| .fetch_one(&mut conn) | |
| .await | |
| .unwrap(); | |
| assert_eq!(result, Some(1)); | |
| // INTEGER column | |
| sqlx::query("CREATE TABLE int_test (x INTEGER NOT NULL)") | |
| .execute(&mut conn) | |
| .await | |
| .unwrap(); | |
| sqlx::query("INSERT INTO int_test VALUES (123), (45)") | |
| .execute(&mut conn) | |
| .await | |
| .unwrap(); | |
| let rows: Vec<i64> = sqlx::query_scalar("SELECT x FROM int_test WHERE x REGEXP '23'") | |
| .fetch_all(&mut conn) | |
| .await | |
| .unwrap(); | |
| assert_eq!(rows, vec![123]); | |
| // NULL should return NULL, not match | |
| let result: Option<i32> = sqlx::query_scalar("SELECT NULL REGEXP '.*'") | |
| .fetch_one(&mut conn) | |
| .await | |
| .unwrap(); | |
| assert_eq!(result, None); | |
| } | |
| #[sqlx::test] | |
| async fn test_invalid_regexp_should_fail() { | |
| let mut conn = test_db().await; | |
| let result = sqlx::query("SELECT col from test WHERE col REGEXP '(?:?)'") | |
| .execute(&mut conn) | |
| .await; | |
| assert!(matches!(result, Err(sqlx::Error::Database(_)))); | |
| } | |
| } |