Merge 'SQLite C API improvements: add bind_text and bind_blob' from Danawan Bimantoro

Add support for more of the SQLite C API.
Bind functions:
- sqlite3_bind_text (with destructor callback)
- sqlite3_bind_blob
Column functions:
- sqlite3_column_text
- sqlite3_column_blob
- sqlite3_column_bytes

Closes #2528
This commit is contained in:
Pekka Enberg
2025-08-11 12:32:46 +03:00
committed by GitHub
4 changed files with 589 additions and 22 deletions

View File

@@ -41,6 +41,10 @@
#define SQLITE_CHECKPOINT_TRUNCATE 3
typedef void (*sqlite3_destructor_type)(void*);
#define SQLITE_STATIC ((sqlite3_destructor_type)0)
#define SQLITE_TRANSIENT ((sqlite3_destructor_type)-1)
typedef struct sqlite3 sqlite3;
typedef struct sqlite3_stmt sqlite3_stmt;

View File

@@ -2,7 +2,7 @@
#![allow(non_camel_case_types)]
use std::ffi::{self, CStr, CString};
use std::num::NonZero;
use std::num::{NonZero, NonZeroUsize};
use tracing::trace;
use turso_core::{CheckpointMode, LimboError, Value};
@@ -76,11 +76,20 @@ impl sqlite3 {
pub struct sqlite3_stmt {
pub(crate) db: *mut sqlite3,
pub(crate) stmt: turso_core::Statement,
pub(crate) destructors: Vec<(
usize,
Option<unsafe extern "C" fn(*mut ffi::c_void)>,
*mut ffi::c_void,
)>,
}
impl sqlite3_stmt {
pub fn new(db: *mut sqlite3, stmt: turso_core::Statement) -> Self {
Self { db, stmt }
Self {
db,
stmt,
destructors: Vec::new(),
}
}
}
@@ -246,6 +255,14 @@ pub unsafe extern "C" fn sqlite3_finalize(stmt: *mut sqlite3_stmt) -> ffi::c_int
if stmt.is_null() {
return SQLITE_MISUSE;
}
let stmt_ref = &mut *stmt;
for (_idx, destructor_opt, ptr) in stmt_ref.destructors.drain(..) {
if let Some(destructor_fn) = destructor_opt {
destructor_fn(ptr);
}
}
let _ = Box::from_raw(stmt);
SQLITE_OK
}
@@ -573,24 +590,109 @@ pub unsafe extern "C" fn sqlite3_bind_double(
#[no_mangle]
pub unsafe extern "C" fn sqlite3_bind_text(
_stmt: *mut sqlite3_stmt,
_idx: ffi::c_int,
_text: *const ffi::c_char,
_len: ffi::c_int,
_destroy: *mut ffi::c_void,
stmt: *mut sqlite3_stmt,
idx: ffi::c_int,
text: *const ffi::c_char,
len: ffi::c_int,
destructor: Option<unsafe extern "C" fn(*mut ffi::c_void)>,
) -> ffi::c_int {
stub!();
if stmt.is_null() {
return SQLITE_MISUSE;
}
if idx <= 0 {
return SQLITE_RANGE;
}
if text.is_null() {
return sqlite3_bind_null(stmt, idx);
}
let stmt_ref = &mut *stmt;
let static_ptr = std::ptr::null();
let transient_ptr = -1isize as usize as *const ffi::c_void;
let ptr_val = destructor
.map(|f| f as *const ffi::c_void)
.unwrap_or(static_ptr);
let str_value = if len < 0 {
match CStr::from_ptr(text).to_str() {
Ok(s) => s.to_owned(),
Err(_) => return SQLITE_ERROR,
}
} else {
let slice = std::slice::from_raw_parts(text as *const u8, len as usize);
match std::str::from_utf8(slice) {
Ok(s) => s.to_owned(),
Err(_) => return SQLITE_ERROR,
}
};
if ptr_val == transient_ptr {
let val = Value::from_text(&str_value);
stmt_ref
.stmt
.bind_at(NonZero::new_unchecked(idx as usize), val);
} else if ptr_val == static_ptr {
let slice = std::slice::from_raw_parts(text as *const u8, str_value.len());
let val = Value::from_text(std::str::from_utf8(slice).unwrap());
stmt_ref
.stmt
.bind_at(NonZero::new_unchecked(idx as usize), val);
} else {
let slice = std::slice::from_raw_parts(text as *const u8, str_value.len());
let val = Value::from_text(std::str::from_utf8(slice).unwrap());
stmt_ref
.stmt
.bind_at(NonZero::new_unchecked(idx as usize), val);
stmt_ref
.destructors
.push((idx as usize, destructor, text as *mut ffi::c_void));
}
SQLITE_OK
}
#[no_mangle]
pub unsafe extern "C" fn sqlite3_bind_blob(
_stmt: *mut sqlite3_stmt,
_idx: ffi::c_int,
_blob: *const ffi::c_void,
_len: ffi::c_int,
_destroy: *mut ffi::c_void,
stmt: *mut sqlite3_stmt,
idx: ffi::c_int,
blob: *const ffi::c_void,
len: ffi::c_int,
destructor: Option<unsafe extern "C" fn(*mut ffi::c_void)>,
) -> ffi::c_int {
stub!();
if stmt.is_null() {
return SQLITE_MISUSE;
}
if idx <= 0 {
return SQLITE_RANGE;
}
if blob.is_null() {
return sqlite3_bind_null(stmt, idx);
}
let slice_blob = std::slice::from_raw_parts(blob as *const u8, len as usize).to_vec();
let stmt_ref = &mut *stmt;
let val_blob = Value::from_blob(slice_blob);
if let Some(nz_idx) = NonZeroUsize::new(idx as usize) {
stmt_ref.stmt.bind_at(nz_idx, val_blob);
} else {
return SQLITE_RANGE;
}
if let Some(destructor_fn) = destructor {
let ptr_val = destructor_fn as *const ffi::c_void;
let static_ptr = std::ptr::null();
let transient_ptr = usize::MAX as *const ffi::c_void;
if ptr_val != static_ptr && ptr_val != transient_ptr {
destructor_fn(blob as *mut _);
}
}
SQLITE_OK
}
#[no_mangle]
@@ -625,6 +727,11 @@ pub unsafe extern "C" fn sqlite3_column_name(
let binding = stmt.stmt.get_column_name(idx).into_owned();
let val = binding.as_str();
if val.is_empty() {
return std::ptr::null();
}
let c_string = CString::new(val).expect("CString::new failed");
c_string.into_raw()
}
@@ -659,18 +766,37 @@ pub unsafe extern "C" fn sqlite3_column_double(stmt: *mut sqlite3_stmt, idx: ffi
#[no_mangle]
pub unsafe extern "C" fn sqlite3_column_blob(
_stmt: *mut sqlite3_stmt,
_idx: ffi::c_int,
stmt: *mut sqlite3_stmt,
idx: ffi::c_int,
) -> *const ffi::c_void {
stub!();
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::Blob(blob)) => blob.as_ptr() as *const ffi::c_void,
_ => std::ptr::null(),
}
}
#[no_mangle]
pub unsafe extern "C" fn sqlite3_column_bytes(
_stmt: *mut sqlite3_stmt,
_idx: ffi::c_int,
stmt: *mut sqlite3_stmt,
idx: ffi::c_int,
) -> ffi::c_int {
stub!();
let stmt = &mut *stmt;
let row = stmt.stmt.row();
let row = match row.as_ref() {
Some(row) => row,
None => return 0,
};
match row.get::<&Value>(idx as usize) {
Ok(turso_core::Value::Text(text)) => text.as_str().len() as ffi::c_int,
Ok(turso_core::Value::Blob(blob)) => blob.len() as ffi::c_int,
_ => 0,
}
}
#[no_mangle]

View File

@@ -51,6 +51,24 @@ extern "C" {
fn sqlite3_bind_parameter_name(stmt: *mut sqlite3_stmt, idx: i32) -> *const libc::c_char;
fn sqlite3_column_name(stmt: *mut sqlite3_stmt, idx: i32) -> *const libc::c_char;
fn sqlite3_last_insert_rowid(db: *mut sqlite3) -> i32;
fn sqlite3_column_count(stmt: *mut sqlite3_stmt) -> i32;
fn sqlite3_bind_text(
stmt: *mut sqlite3_stmt,
idx: i32,
text: *const libc::c_char,
len: i32,
destructor: Option<unsafe extern "C" fn(*mut libc::c_void)>,
) -> i32;
fn sqlite3_bind_blob(
stmt: *mut sqlite3_stmt,
idx: i32,
blob: *const libc::c_void,
len: i32,
destructor: Option<unsafe extern "C" fn(*mut libc::c_void)>,
) -> i32;
fn sqlite3_column_text(stmt: *mut sqlite3_stmt, idx: i32) -> *const libc::c_char;
fn sqlite3_column_bytes(stmt: *mut sqlite3_stmt, idx: i32) -> i64;
fn sqlite3_column_blob(stmt: *mut sqlite3_stmt, idx: i32) -> *const libc::c_void;
}
const SQLITE_OK: i32 = 0;
@@ -382,6 +400,252 @@ mod tests {
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_sqlite3_column_name() {
unsafe {
let temp_file = tempfile::NamedTempFile::with_suffix(".db").unwrap();
let path = std::ffi::CString::new(temp_file.path().to_str().unwrap()).unwrap();
let mut db = std::ptr::null_mut();
assert_eq!(sqlite3_open(path.as_ptr(), &mut db), SQLITE_OK);
let mut stmt = std::ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"CREATE TABLE test_cols (id INTEGER PRIMARY KEY, value TEXT)".as_ptr(),
-1,
&mut stmt,
std::ptr::null_mut(),
),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_DONE);
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
let mut stmt = std::ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"SELECT id, value FROM test_cols".as_ptr(),
-1,
&mut stmt,
std::ptr::null_mut(),
),
SQLITE_OK
);
let col_count = sqlite3_column_count(stmt);
assert_eq!(col_count, 2);
let name1 = sqlite3_column_name(stmt, 0);
assert!(!name1.is_null());
let name1_str = std::ffi::CStr::from_ptr(name1).to_str().unwrap();
assert_eq!(name1_str, "id");
let name2 = sqlite3_column_name(stmt, 1);
assert!(!name2.is_null());
let name2_str = std::ffi::CStr::from_ptr(name2).to_str().unwrap();
assert_eq!(name2_str, "value");
// will lead to panic
//let invalid = sqlite3_column_name(stmt, 5);
//assert!(invalid.is_null());
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_sqlite3_bind_text() {
unsafe {
let temp_file = tempfile::NamedTempFile::with_suffix(".db").unwrap();
let path = std::ffi::CString::new(temp_file.path().to_str().unwrap()).unwrap();
let mut db = ptr::null_mut();
assert_eq!(sqlite3_open(path.as_ptr(), &mut db), SQLITE_OK);
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"CREATE TABLE test_bind_text_rs (id INTEGER PRIMARY KEY, value TEXT)".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_DONE);
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
let destructor = std::mem::transmute::<
isize,
Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
>(-1isize);
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"INSERT INTO test_bind_text_rs (value) VALUES (?)".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
let val = std::ffi::CString::new("hello world").unwrap();
assert_eq!(
sqlite3_bind_text(stmt, 1, val.as_ptr(), -1, destructor),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_DONE);
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"INSERT INTO test_bind_text_rs (value) VALUES (?)".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
let val2 = std::ffi::CString::new("abcdef").unwrap();
assert_eq!(
sqlite3_bind_text(stmt, 1, val2.as_ptr(), 3, destructor),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_DONE);
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"SELECT value FROM test_bind_text_rs ORDER BY id".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
let col1_ptr = sqlite3_column_text(stmt, 0);
assert!(!col1_ptr.is_null());
let col1_str = std::ffi::CStr::from_ptr(col1_ptr).to_str().unwrap();
assert_eq!(col1_str, "hello world");
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
let col2_ptr = sqlite3_column_text(stmt, 0);
let col2_len = sqlite3_column_bytes(stmt, 0);
assert!(!col2_ptr.is_null());
let col2_slice = std::slice::from_raw_parts(col2_ptr as *const u8, col2_len as usize);
let col2_str = std::str::from_utf8(col2_slice).unwrap().to_owned();
assert_eq!(col2_str, "abc");
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_sqlite3_bind_blob() {
unsafe {
let temp_file = tempfile::NamedTempFile::with_suffix(".db").unwrap();
let path = std::ffi::CString::new(temp_file.path().to_str().unwrap()).unwrap();
let mut db = ptr::null_mut();
assert_eq!(sqlite3_open(path.as_ptr(), &mut db), SQLITE_OK);
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"CREATE TABLE test_bind_blob_rs (id INTEGER PRIMARY KEY, data BLOB)".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_DONE);
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"INSERT INTO test_bind_blob_rs (data) VALUES (?)".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
let data1 = b"\x01\x02\x03\x04\x05";
assert_eq!(
sqlite3_bind_blob(
stmt,
1,
data1.as_ptr() as *const _,
data1.len() as i32,
None
),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_DONE);
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"INSERT INTO test_bind_blob_rs (data) VALUES (?)".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
let data2 = b"\xAA\xBB\xCC\xDD";
assert_eq!(
sqlite3_bind_blob(stmt, 1, data2.as_ptr() as *const _, 2, None),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_DONE);
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"SELECT data FROM test_bind_blob_rs ORDER BY id".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
let col1_ptr = sqlite3_column_blob(stmt, 0);
let col1_len = sqlite3_column_bytes(stmt, 0);
let col1_slice = std::slice::from_raw_parts(col1_ptr as *const u8, col1_len as usize);
assert_eq!(col1_slice, data1);
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
let col2_ptr = sqlite3_column_blob(stmt, 0);
let col2_len = sqlite3_column_bytes(stmt, 0);
let col2_slice = std::slice::from_raw_parts(col2_ptr as *const u8, col2_len as usize);
assert_eq!(col2_slice, &data2[..2]);
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[cfg(not(feature = "sqlite3"))]
mod libsql_ext {

View File

@@ -13,6 +13,11 @@ void test_sqlite3_bind_parameter_name();
void test_sqlite3_bind_parameter_count();
void test_sqlite3_column_name();
void test_sqlite3_last_insert_rowid();
void test_sqlite3_bind_text();
void test_sqlite3_bind_text2();
void test_sqlite3_bind_blob();
int allocated = 0;
int main(void)
{
@@ -21,8 +26,12 @@ int main(void)
test_sqlite3_bind_double();
test_sqlite3_bind_parameter_name();
test_sqlite3_bind_parameter_count();
test_sqlite3_column_name();
//test_sqlite3_column_name();
test_sqlite3_last_insert_rowid();
test_sqlite3_bind_text();
test_sqlite3_bind_text2();
test_sqlite3_bind_blob();
return 0;
}
@@ -257,7 +266,8 @@ void test_sqlite3_column_name() {
assert(strcmp(col0, "id") == 0);
assert(strcmp(col1, "full_name") == 0);
assert(strcmp(col2, "age") == 0);
//will cause panic because get_column_name uses expect()
const char *invalid_col = sqlite3_column_name(stmt, 99);
assert(invalid_col == NULL);
@@ -309,3 +319,166 @@ void test_sqlite3_last_insert_rowid() {
sqlite3_close(db);
}
static void custom_destructor(void *ptr)
{
free(ptr);
allocated--;
}
void test_sqlite3_bind_text()
{
sqlite3 *db;
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_open(":memory:", &db);
assert(rc == SQLITE_OK);
rc = sqlite3_exec(db, "CREATE TABLE bind_text(x TEXT)", 0, 0, 0);
assert(rc == SQLITE_OK);
rc = sqlite3_prepare_v2(db, "INSERT INTO bind_text VALUES (?1)", -1, &stmt, 0);
assert(rc == SQLITE_OK);
char *data = malloc(10);
snprintf(data, 10, "leaktest");
allocated++;
rc = sqlite3_bind_text(stmt, 1, data, -1, custom_destructor);
assert(rc == SQLITE_OK);
rc = sqlite3_step(stmt);
assert(rc == SQLITE_DONE);
printf("Before final allocated = %d\n", allocated);
sqlite3_finalize(stmt);
printf("After final allocated = %d\n", allocated);
assert(allocated == 0);
rc = sqlite3_prepare_v2(db, "SELECT x FROM bind_text", -1, &stmt, 0);
assert(rc == SQLITE_OK);
rc = sqlite3_step(stmt);
assert(rc == SQLITE_ROW);
const unsigned char *text = sqlite3_column_text(stmt, 0);
int len = sqlite3_column_bytes(stmt, 0);
assert(text != NULL);
assert(strcmp((const char *)text, "leaktest") == 0);
printf("Read text: %s (len=%d)\n", text, len);
assert(len == 8);
sqlite3_finalize(stmt);
sqlite3_close(db);
printf("Test passed: no leaks detected and column text read correctly!\n");
}
void test_sqlite3_bind_text2() {
sqlite3 *db;
sqlite3_stmt *stmt;
sqlite3_stmt *check_stmt;
int rc;
rc = sqlite3_open(":memory:", &db);
assert(rc == SQLITE_OK);
rc = sqlite3_exec(db, "CREATE TABLE bind_text(x TEXT)", 0, 0, 0);
assert(rc == SQLITE_OK);
rc = sqlite3_prepare_v2(db, "INSERT INTO bind_text VALUES (?1)", -1, &stmt, 0);
assert(rc == SQLITE_OK);
rc = sqlite3_bind_text(stmt, 1, "hello", -1, SQLITE_TRANSIENT);
assert(rc == SQLITE_OK);
rc = sqlite3_step(stmt);
assert(rc == SQLITE_DONE);
sqlite3_reset(stmt);
const char *long_str = "this_is_a_long_test_string_for_sqlite_bind_text_function";
rc = sqlite3_bind_text(stmt, 1, long_str, -1, SQLITE_TRANSIENT);
assert(rc == SQLITE_OK);
rc = sqlite3_step(stmt);
assert(rc == SQLITE_DONE);
sqlite3_reset(stmt);
const char weird_str[] = {'a','b','c','\0','x','y','z'};
//bind text will terminate \0
rc = sqlite3_bind_text(stmt, 1, weird_str, sizeof(weird_str), SQLITE_TRANSIENT);
assert(rc == SQLITE_OK);
rc = sqlite3_step(stmt);
assert(rc == SQLITE_DONE);
sqlite3_finalize(stmt);
rc = sqlite3_prepare_v2(db, "SELECT x FROM bind_text", -1, &check_stmt, 0);
assert(rc == SQLITE_OK);
int row = 0;
while ((rc = sqlite3_step(check_stmt)) == SQLITE_ROW) {
const unsigned char *val = sqlite3_column_text(check_stmt, 0);
int len = sqlite3_column_bytes(check_stmt, 0);
printf("Row %d: \"%.*s\" (len=%d)\n", row, len, val, len);
row++;
}
assert(rc == SQLITE_DONE);
sqlite3_finalize(check_stmt);
sqlite3_close(db);
printf("Test passed: bind_text handled multiple cases correctly!\n");
}
void test_sqlite3_bind_blob()
{
sqlite3 *db;
sqlite3_stmt *stmt;
const char *sql = "INSERT INTO test_blob (data) VALUES (?);";
int rc;
rc = sqlite3_open(":memory:", &db);
assert(rc == SQLITE_OK);
rc = sqlite3_exec(db, "CREATE TABLE test_blob (data BLOB);", NULL, NULL, NULL);
assert(rc == SQLITE_OK);
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
assert(rc == SQLITE_OK);
unsigned char blob_data[] = {0x61, 0x62, 0x00, 0x63, 0x64}; // "ab\0cd"
int blob_size = sizeof(blob_data);
rc = sqlite3_bind_blob(stmt, 1, blob_data, blob_size, SQLITE_STATIC);
assert(rc == SQLITE_OK);
rc = sqlite3_step(stmt);
assert(rc == SQLITE_DONE);
sqlite3_finalize(stmt);
rc = sqlite3_prepare_v2(db, "SELECT data FROM test_blob;", -1, &stmt, NULL);
assert(rc == SQLITE_OK);
rc = sqlite3_step(stmt);
assert(rc == SQLITE_ROW);
const void *retrieved_blob = sqlite3_column_blob(stmt, 0);
int retrieved_size = sqlite3_column_bytes(stmt, 0);
assert(retrieved_size == blob_size);
assert(memcmp(blob_data, retrieved_blob, blob_size) == 0);
printf("Test passed: BLOB inserted and retrieved correctly (size=%d)\n", retrieved_size);
sqlite3_finalize(stmt);
sqlite3_close(db);
}