@@ -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