Merge 'Return null terminated strings from sqlite3_column_text' from Preston Thorpe

closes #3811
adds `text_cache` which owns the null terminated bytes, which get cached
if a subsequent call to `sqlite3_column_text` is made.
#3809 depends on this fix

Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #3817
This commit is contained in:
Preston Thorpe
2025-10-23 13:21:12 -04:00
committed by GitHub
2 changed files with 74 additions and 5 deletions

View File

@@ -94,15 +94,25 @@ pub struct sqlite3_stmt {
*mut ffi::c_void,
)>,
pub(crate) next: *mut sqlite3_stmt,
pub(crate) text_cache: Vec<Vec<u8>>,
}
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(),
}
}

View File

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