From 23cddbcad9eef4e38b9e5985365ee5ac68c1cbb6 Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Thu, 23 Oct 2025 10:11:55 -0400 Subject: [PATCH 1/2] Return null terminated strings from sqlite3_column_text --- sqlite3/src/lib.rs | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/sqlite3/src/lib.rs b/sqlite3/src/lib.rs index 037e5015b..0083715ae 100644 --- a/sqlite3/src/lib.rs +++ b/sqlite3/src/lib.rs @@ -94,15 +94,25 @@ pub struct sqlite3_stmt { *mut ffi::c_void, )>, pub(crate) next: *mut sqlite3_stmt, + pub(crate) text_cache: Vec>, } impl sqlite3_stmt { pub fn new(db: *mut sqlite3, stmt: turso_core::Statement) -> Self { + let n_cols = stmt.num_columns(); Self { db, stmt, destructors: Vec::new(), next: std::ptr::null_mut(), + text_cache: vec![vec![]; n_cols], + } + } + #[inline] + fn clear_text_cache(&mut self) { + // Drop per-column buffers for the previous row + for r in &mut self.text_cache { + r.clear(); } } } @@ -323,7 +333,7 @@ pub unsafe extern "C" fn sqlite3_finalize(stmt: *mut sqlite3_stmt) -> ffi::c_int destructor_fn(ptr); } } - + stmt_ref.clear_text_cache(); let _ = Box::from_raw(stmt); SQLITE_OK } @@ -340,9 +350,15 @@ pub unsafe extern "C" fn sqlite3_step(stmt: *mut sqlite3_stmt) -> ffi::c_int { stmt.stmt.run_once().unwrap(); continue; } - turso_core::StepResult::Done => return SQLITE_DONE, + turso_core::StepResult::Done => { + stmt.clear_text_cache(); + return SQLITE_DONE; + } turso_core::StepResult::Interrupt => return SQLITE_INTERRUPT, - turso_core::StepResult::Row => return SQLITE_ROW, + turso_core::StepResult::Row => { + stmt.clear_text_cache(); + return SQLITE_ROW; + } turso_core::StepResult::Busy => return SQLITE_BUSY, } } else { @@ -389,6 +405,7 @@ pub unsafe extern "C" fn sqlite3_exec( pub unsafe extern "C" fn sqlite3_reset(stmt: *mut sqlite3_stmt) -> ffi::c_int { let stmt = &mut *stmt; stmt.stmt.reset(); + stmt.clear_text_cache(); SQLITE_OK } @@ -1048,14 +1065,30 @@ pub unsafe extern "C" fn sqlite3_column_text( stmt: *mut sqlite3_stmt, idx: ffi::c_int, ) -> *const ffi::c_uchar { + if stmt.is_null() || idx < 0 { + return std::ptr::null(); + } let stmt = &mut *stmt; let row = stmt.stmt.row(); let row = match row.as_ref() { Some(row) => row, None => return std::ptr::null(), }; - match row.get::<&Value>(idx as usize) { - Ok(turso_core::Value::Text(text)) => text.as_str().as_ptr(), + let i = idx as usize; + if i >= stmt.text_cache.len() { + return std::ptr::null(); + } + if !stmt.text_cache[i].is_empty() { + // we have already cached this value + return stmt.text_cache[i].as_ptr() as *const ffi::c_uchar; + } + match row.get::<&Value>(i) { + Ok(turso_core::Value::Text(text)) => { + let buf = &mut stmt.text_cache[i]; + buf.extend(text.as_str().as_bytes()); + buf.push(0); + buf.as_ptr() as *const ffi::c_uchar + } _ => std::ptr::null(), } } From 8ed4e7cac19e8884b866ca4011b464c32bfa2d45 Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Thu, 23 Oct 2025 10:54:19 -0400 Subject: [PATCH 2/2] Add test for null terminated string from sqlite3_column_text --- sqlite3/tests/compat/mod.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/sqlite3/tests/compat/mod.rs b/sqlite3/tests/compat/mod.rs index d04b933e8..0badf6051 100644 --- a/sqlite3/tests/compat/mod.rs +++ b/sqlite3/tests/compat/mod.rs @@ -412,6 +412,42 @@ mod tests { } } + #[test] + #[cfg(not(target_os = "windows"))] + fn column_text_is_nul_terminated_and_bytes_match() { + unsafe { + let mut db = std::ptr::null_mut(); + assert_eq!( + sqlite3_open(c"../testing/testing.db".as_ptr(), &mut db), + SQLITE_OK + ); + let mut stmt = std::ptr::null_mut(); + assert_eq!( + sqlite3_prepare_v2( + db, + c"SELECT first_name FROM users ORDER BY rowid ASC LIMIT 1;".as_ptr(), + -1, + &mut stmt, + std::ptr::null_mut() + ), + SQLITE_OK + ); + assert_eq!(sqlite3_step(stmt), SQLITE_ROW); + let p = sqlite3_column_text(stmt, 0); + assert!(!p.is_null()); + let bytes = sqlite3_column_bytes(stmt, 0) as usize; + // NUL at [bytes], and no extra counted + let slice = std::slice::from_raw_parts(p, bytes + 1); + assert_eq!(slice[bytes], 0); + assert_eq!(libc::strlen(p), bytes); + + let s = std::ffi::CStr::from_ptr(p).to_str().unwrap(); + assert_eq!(s, "Jamie"); + assert_eq!(sqlite3_finalize(stmt), SQLITE_OK); + assert_eq!(sqlite3_close(db), SQLITE_OK); + } + } + #[test] fn test_sqlite3_bind_text() { unsafe {