Skip to content

Commit 6984316

Browse files
fix: use sqlite3_value_text for REGEXP to match SQLite coercion (#4203)
Use sqlite3_value_text() instead of checking sqlite3_value_type() first, matching SQLite's documented text affinity coercion for the REGEXP operator. NULL values still return NULL. Closes #4190
1 parent 9d6bf66 commit 6984316

File tree

1 file changed

+63
-15
lines changed

1 file changed

+63
-15
lines changed

sqlx-sqlite/src/regexp.rs

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,11 @@ unsafe fn get_regex_from_arg(
136136
Some(regex)
137137
}
138138

139-
/// Get a text reference of the value of `arg`. If this value is not a string value, an error is printed and `None` is
140-
/// returned.
139+
/// Get a text reference of the value of `arg`. Returns `None` for NULL values.
140+
///
141+
/// For non-NULL values, `sqlite3_value_text()` is called directly, which lets SQLite
142+
/// coerce INTEGER, REAL, and BLOB values to their text representation. This matches
143+
/// the coercion behavior documented at <https://www.sqlite.org/c3ref/value_blob.html>.
141144
///
142145
/// The returned `&str` is valid for lifetime `'a` which can be determined by the caller. This lifetime should **not**
143146
/// outlive `ctx`.
@@ -146,20 +149,19 @@ unsafe fn get_text_from_arg<'a>(
146149
arg: *mut ffi::sqlite3_value,
147150
) -> Option<&'a str> {
148151
let ty = ffi::sqlite3_value_type(arg);
149-
if ty == ffi::SQLITE_TEXT {
150-
let ptr = ffi::sqlite3_value_text(arg);
151-
let len = ffi::sqlite3_value_bytes(arg);
152-
let slice = std::slice::from_raw_parts(ptr.cast(), len as usize);
153-
match std::str::from_utf8(slice) {
154-
Ok(result) => Some(result),
155-
Err(e) => {
156-
log::error!("Incoming text is not valid UTF8: {e:?}");
157-
ffi::sqlite3_result_error_code(ctx, ffi::SQLITE_CONSTRAINT_FUNCTION);
158-
None
159-
}
152+
if ty == ffi::SQLITE_NULL {
153+
return None;
154+
}
155+
let ptr = ffi::sqlite3_value_text(arg);
156+
let len = ffi::sqlite3_value_bytes(arg);
157+
let slice = std::slice::from_raw_parts(ptr.cast(), len as usize);
158+
match std::str::from_utf8(slice) {
159+
Ok(result) => Some(result),
160+
Err(e) => {
161+
log::error!("Incoming text is not valid UTF8: {e:?}");
162+
ffi::sqlite3_result_error_code(ctx, ffi::SQLITE_CONSTRAINT_FUNCTION);
163+
None
160164
}
161-
} else {
162-
None
163165
}
164166
}
165167

@@ -222,6 +224,52 @@ mod tests {
222224
assert!(result.is_empty());
223225
}
224226

227+
#[sqlx::test]
228+
async fn test_regexp_coerces_non_text_values() {
229+
let mut conn = crate::SqliteConnectOptions::from_str("sqlite://:memory:")
230+
.unwrap()
231+
.with_regexp()
232+
.connect()
233+
.await
234+
.unwrap();
235+
236+
// INTEGER coercion
237+
let result: Option<i32> = sqlx::query_scalar("SELECT 123 REGEXP '23'")
238+
.fetch_one(&mut conn)
239+
.await
240+
.unwrap();
241+
assert_eq!(result, Some(1));
242+
243+
// REAL coercion
244+
let result: Option<i32> = sqlx::query_scalar("SELECT 12.5 REGEXP '12\\.5'")
245+
.fetch_one(&mut conn)
246+
.await
247+
.unwrap();
248+
assert_eq!(result, Some(1));
249+
250+
// INTEGER column
251+
sqlx::query("CREATE TABLE int_test (x INTEGER NOT NULL)")
252+
.execute(&mut conn)
253+
.await
254+
.unwrap();
255+
sqlx::query("INSERT INTO int_test VALUES (123), (45)")
256+
.execute(&mut conn)
257+
.await
258+
.unwrap();
259+
let rows: Vec<i64> = sqlx::query_scalar("SELECT x FROM int_test WHERE x REGEXP '23'")
260+
.fetch_all(&mut conn)
261+
.await
262+
.unwrap();
263+
assert_eq!(rows, vec![123]);
264+
265+
// NULL should return NULL, not match
266+
let result: Option<i32> = sqlx::query_scalar("SELECT NULL REGEXP '.*'")
267+
.fetch_one(&mut conn)
268+
.await
269+
.unwrap();
270+
assert_eq!(result, None);
271+
}
272+
225273
#[sqlx::test]
226274
async fn test_invalid_regexp_should_fail() {
227275
let mut conn = test_db().await;

0 commit comments

Comments
 (0)