Merge 'sqlite3: Add multi-statement support for sqlite3_exec()' from Preston Thorpe

closes https://github.com/tursodatabase/turso/issues/3576

Closes #3809
This commit is contained in:
Pekka Enberg
2025-10-25 08:41:11 +03:00
committed by GitHub
2 changed files with 1012 additions and 11 deletions

View File

@@ -380,25 +380,272 @@ type exec_callback = Option<
pub unsafe extern "C" fn sqlite3_exec(
db: *mut sqlite3,
sql: *const ffi::c_char,
_callback: exec_callback,
_context: *mut ffi::c_void,
_err: *mut *mut ffi::c_char,
callback: exec_callback,
context: *mut ffi::c_void,
err: *mut *mut ffi::c_char,
) -> ffi::c_int {
if db.is_null() || sql.is_null() {
return SQLITE_MISUSE;
}
let db: &mut sqlite3 = &mut *db;
let db = db.inner.lock().unwrap();
let sql = CStr::from_ptr(sql);
let sql = match sql.to_str() {
let db_ref: &mut sqlite3 = &mut *db;
let sql_cstr = CStr::from_ptr(sql);
let sql_str = match sql_cstr.to_str() {
Ok(s) => s,
Err(_) => return SQLITE_MISUSE,
};
trace!("sqlite3_exec(sql={})", sql);
match db.conn.execute(sql) {
Ok(_) => SQLITE_OK,
Err(_) => SQLITE_ERROR,
trace!("sqlite3_exec(sql={})", sql_str);
if !err.is_null() {
*err = std::ptr::null_mut();
}
let statements = split_sql_statements(sql_str);
for stmt_sql in statements {
let trimmed = stmt_sql.trim();
if trimmed.is_empty() {
continue;
}
let is_dql = is_query_statement(trimmed);
if !is_dql {
// For DML/DDL, use normal execute path
let db_inner = db_ref.inner.lock().unwrap();
match db_inner.conn.execute(trimmed) {
Ok(_) => continue,
Err(e) => {
if !err.is_null() {
let err_msg = format!("SQL error: {e:?}");
*err = CString::new(err_msg).unwrap().into_raw();
}
return SQLITE_ERROR;
}
}
} else if callback.is_none() {
// DQL without callback provided, still execute but discard any result rows
let mut stmt_ptr: *mut sqlite3_stmt = std::ptr::null_mut();
let rc = sqlite3_prepare_v2(
db,
CString::new(trimmed).unwrap().as_ptr(),
-1,
&mut stmt_ptr,
std::ptr::null_mut(),
);
if rc != SQLITE_OK {
if !err.is_null() {
let err_msg = format!("Prepare failed: {rc}");
*err = CString::new(err_msg).unwrap().into_raw();
}
return rc;
}
loop {
let step_rc = sqlite3_step(stmt_ptr);
match step_rc {
SQLITE_ROW => continue,
SQLITE_DONE => break,
_ => {
sqlite3_finalize(stmt_ptr);
if !err.is_null() {
let err_msg = format!("Step failed: {step_rc}");
*err = CString::new(err_msg).unwrap().into_raw();
}
return step_rc;
}
}
}
sqlite3_finalize(stmt_ptr);
} else {
// DQL with callback
let rc = execute_query_with_callback(db, trimmed, callback, context, err);
if rc != SQLITE_OK {
return rc;
}
}
}
SQLITE_OK
}
/// Detect if a SQL statement is DQL
fn is_query_statement(sql: &str) -> bool {
let trimmed = sql.trim_start();
if trimmed.is_empty() {
return false;
}
let bytes = trimmed.as_bytes();
let starts_with_ignore_case = |keyword: &[u8]| -> bool {
if bytes.len() < keyword.len() {
return false;
}
// Check keyword matches
if !bytes[..keyword.len()].eq_ignore_ascii_case(keyword) {
return false;
}
// Ensure keyword is followed by whitespace or EOF
bytes.len() == keyword.len() || bytes[keyword.len()].is_ascii_whitespace()
};
// Check DQL keywords
if starts_with_ignore_case(b"SELECT")
|| starts_with_ignore_case(b"VALUES")
|| starts_with_ignore_case(b"WITH")
|| starts_with_ignore_case(b"PRAGMA")
|| starts_with_ignore_case(b"EXPLAIN")
{
return true;
}
// Look for RETURNING as a whole word, that's not part of another identifier
let mut i = 0;
while i < bytes.len() {
if i + 9 <= bytes.len() && bytes[i..i + 9].eq_ignore_ascii_case(b"RETURNING") {
// Check it's a word boundary before and after
let is_word_start =
i == 0 || !bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_';
let is_word_end = i + 9 == bytes.len()
|| !bytes[i + 9].is_ascii_alphanumeric() && bytes[i + 9] != b'_';
if is_word_start && is_word_end {
return true;
}
}
i += 1;
}
false
}
/// Execute a query statement with callback for each row
/// Only called when we know callback is Some
unsafe fn execute_query_with_callback(
db: *mut sqlite3,
sql: &str,
callback: exec_callback,
context: *mut ffi::c_void,
err: *mut *mut ffi::c_char,
) -> ffi::c_int {
let sql_cstring = match CString::new(sql) {
Ok(s) => s,
Err(_) => return SQLITE_MISUSE,
};
let mut stmt_ptr: *mut sqlite3_stmt = std::ptr::null_mut();
let rc = sqlite3_prepare_v2(
db,
sql_cstring.as_ptr(),
-1,
&mut stmt_ptr,
std::ptr::null_mut(),
);
if rc != SQLITE_OK {
if !err.is_null() {
let err_msg = format!("Prepare failed: {rc}");
*err = CString::new(err_msg).unwrap().into_raw();
}
return rc;
}
let stmt_ref = &*stmt_ptr;
let n_cols = stmt_ref.stmt.num_columns() as ffi::c_int;
let mut column_names: Vec<CString> = Vec::with_capacity(n_cols as usize);
for i in 0..n_cols {
let name = stmt_ref.stmt.get_column_name(i as usize);
column_names.push(CString::new(name.as_bytes()).unwrap());
}
loop {
let step_rc = sqlite3_step(stmt_ptr);
match step_rc {
SQLITE_ROW => {
// Safety: checked earlier
let callback = callback.unwrap();
let mut values: Vec<CString> = Vec::with_capacity(n_cols as usize);
let mut value_ptrs: Vec<*mut ffi::c_char> = Vec::with_capacity(n_cols as usize);
let mut col_ptrs: Vec<*mut ffi::c_char> = Vec::with_capacity(n_cols as usize);
for i in 0..n_cols {
let val = stmt_ref.stmt.row().unwrap().get_value(i as usize);
values.push(CString::new(val.to_string().as_bytes()).unwrap());
}
for value in &values {
value_ptrs.push(value.as_ptr() as *mut ffi::c_char);
}
for name in &column_names {
col_ptrs.push(name.as_ptr() as *mut ffi::c_char);
}
let cb_rc = callback(
context,
n_cols,
value_ptrs.as_mut_ptr(),
col_ptrs.as_mut_ptr(),
);
if cb_rc != 0 {
sqlite3_finalize(stmt_ptr);
return SQLITE_ABORT;
}
}
SQLITE_DONE => {
break;
}
_ => {
sqlite3_finalize(stmt_ptr);
if !err.is_null() {
let err_msg = format!("Step failed: {step_rc}");
*err = CString::new(err_msg).unwrap().into_raw();
}
return step_rc;
}
}
}
sqlite3_finalize(stmt_ptr)
}
/// Split SQL string into individual statements
/// Handles quoted strings properly and skips comments
fn split_sql_statements(sql: &str) -> Vec<&str> {
let mut statements = Vec::new();
let mut current_start = 0;
let mut in_single_quote = false;
let mut in_double_quote = false;
let bytes = sql.as_bytes();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
// Check for escaped quotes first
b'\'' if !in_double_quote => {
if i + 1 < bytes.len() && bytes[i + 1] == b'\'' {
i += 2;
continue;
}
in_single_quote = !in_single_quote;
}
b'"' if !in_single_quote => {
if i + 1 < bytes.len() && bytes[i + 1] == b'"' {
i += 2;
continue;
}
in_double_quote = !in_double_quote;
}
b';' if !in_single_quote && !in_double_quote => {
// we found the statement boundary
statements.push(&sql[current_start..i]);
current_start = i + 1;
}
_ => {}
}
i += 1;
}
if current_start < sql.len() {
statements.push(&sql[current_start..]);
}
statements
}
#[no_mangle]

View File

@@ -20,6 +20,21 @@ extern "C" {
fn sqlite3_close(db: *mut sqlite3) -> i32;
fn sqlite3_open(filename: *const libc::c_char, db: *mut *mut sqlite3) -> i32;
fn sqlite3_db_filename(db: *mut sqlite3, db_name: *const libc::c_char) -> *const libc::c_char;
fn sqlite3_exec(
db: *mut sqlite3,
sql: *const libc::c_char,
callback: Option<
unsafe extern "C" fn(
arg1: *mut libc::c_void,
arg2: libc::c_int,
arg3: *mut *mut libc::c_char,
arg4: *mut *mut libc::c_char,
) -> libc::c_int,
>,
arg: *mut libc::c_void,
errmsg: *mut *mut libc::c_char,
) -> i32;
fn sqlite3_free(ptr: *mut libc::c_void);
fn sqlite3_prepare_v2(
db: *mut sqlite3,
sql: *const libc::c_char,
@@ -106,6 +121,7 @@ const SQLITE_CHECKPOINT_RESTART: i32 = 2;
const SQLITE_CHECKPOINT_TRUNCATE: i32 = 3;
const SQLITE_INTEGER: i32 = 1;
const SQLITE_FLOAT: i32 = 2;
const SQLITE_ABORT: i32 = 4;
const SQLITE_TEXT: i32 = 3;
const SQLITE3_TEXT: i32 = 3;
const SQLITE_BLOB: i32 = 4;
@@ -762,6 +778,744 @@ mod tests {
}
}
#[test]
fn test_exec_multi_statement_dml() {
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);
// Multiple DML statements in one exec call
let rc = sqlite3_exec(
db,
c"CREATE TABLE bind_text(x TEXT);\
INSERT INTO bind_text(x) VALUES('TEXT1');\
INSERT INTO bind_text(x) VALUES('TEXT2');"
.as_ptr(),
None,
ptr::null_mut(),
ptr::null_mut(),
);
assert_eq!(rc, SQLITE_OK);
// Verify the data was inserted
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"SELECT COUNT(*) FROM bind_text".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
assert_eq!(sqlite3_column_int(stmt, 0), 2);
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_exec_multi_statement_with_semicolons_in_strings() {
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);
// Semicolons inside strings should not split statements
let rc = sqlite3_exec(
db,
c"CREATE TABLE test_semicolon(x TEXT);\
INSERT INTO test_semicolon(x) VALUES('value;with;semicolons');\
INSERT INTO test_semicolon(x) VALUES(\"another;value\");"
.as_ptr(),
None,
ptr::null_mut(),
ptr::null_mut(),
);
assert_eq!(rc, SQLITE_OK);
// Verify the values contain semicolons
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"SELECT x FROM test_semicolon ORDER BY rowid".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
let val1 = std::ffi::CStr::from_ptr(sqlite3_column_text(stmt, 0))
.to_str()
.unwrap();
assert_eq!(val1, "value;with;semicolons");
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
let val2 = std::ffi::CStr::from_ptr(sqlite3_column_text(stmt, 0))
.to_str()
.unwrap();
assert_eq!(val2, "another;value");
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_exec_multi_statement_with_escaped_quotes() {
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);
// Test escaped quotes
let rc = sqlite3_exec(
db,
c"CREATE TABLE test_quotes(x TEXT);\
INSERT INTO test_quotes(x) VALUES('it''s working');\
INSERT INTO test_quotes(x) VALUES(\"quote\"\"test\"\"\");"
.as_ptr(),
None,
ptr::null_mut(),
ptr::null_mut(),
);
assert_eq!(rc, SQLITE_OK);
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"SELECT x FROM test_quotes ORDER BY rowid".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
let val1 = std::ffi::CStr::from_ptr(sqlite3_column_text(stmt, 0))
.to_str()
.unwrap();
assert_eq!(val1, "it's working");
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
let val2 = std::ffi::CStr::from_ptr(sqlite3_column_text(stmt, 0))
.to_str()
.unwrap();
assert_eq!(val2, "quote\"test\"");
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_exec_with_select_callback() {
unsafe {
// Callback that collects results
unsafe extern "C" fn exec_callback(
context: *mut std::ffi::c_void,
n_cols: std::ffi::c_int,
values: *mut *mut std::ffi::c_char,
_cols: *mut *mut std::ffi::c_char,
) -> std::ffi::c_int {
let results = &mut *(context as *mut Vec<Vec<String>>);
let mut row = Vec::new();
for i in 0..n_cols as isize {
let value_ptr = *values.offset(i);
let value = if value_ptr.is_null() {
String::from("NULL")
} else {
std::ffi::CStr::from_ptr(value_ptr)
.to_str()
.unwrap()
.to_owned()
};
row.push(value);
}
results.push(row);
0 // Continue
}
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);
// Setup data
let rc = sqlite3_exec(
db,
c"CREATE TABLE test_select(id INTEGER, name TEXT);\
INSERT INTO test_select VALUES(1, 'Alice');\
INSERT INTO test_select VALUES(2, 'Bob');"
.as_ptr(),
None,
ptr::null_mut(),
ptr::null_mut(),
);
assert_eq!(rc, SQLITE_OK);
// Execute SELECT with callback
let mut results: Vec<Vec<String>> = Vec::new();
let rc = sqlite3_exec(
db,
c"SELECT id, name FROM test_select ORDER BY id".as_ptr(),
Some(exec_callback),
&mut results as *mut _ as *mut std::ffi::c_void,
ptr::null_mut(),
);
assert_eq!(rc, SQLITE_OK);
assert_eq!(results.len(), 2);
assert_eq!(results[0], vec!["1", "Alice"]);
assert_eq!(results[1], vec!["2", "Bob"]);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_exec_multi_statement_mixed_dml_select() {
unsafe {
// Callback that counts invocations
unsafe extern "C" fn count_callback(
context: *mut std::ffi::c_void,
_n_cols: std::ffi::c_int,
_values: *mut *mut std::ffi::c_char,
_cols: *mut *mut std::ffi::c_char,
) -> std::ffi::c_int {
let count = &mut *(context as *mut i32);
*count += 1;
0
}
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 callback_count = 0;
// Mix of DDL/DML/DQL
let rc = sqlite3_exec(
db,
c"CREATE TABLE mixed(x INTEGER);\
INSERT INTO mixed VALUES(1);\
INSERT INTO mixed VALUES(2);\
SELECT x FROM mixed;\
INSERT INTO mixed VALUES(3);\
SELECT COUNT(*) FROM mixed;"
.as_ptr(),
Some(count_callback),
&mut callback_count as *mut _ as *mut std::ffi::c_void,
ptr::null_mut(),
);
assert_eq!(rc, SQLITE_OK);
// Callback should be called 3 times total:
// 2 times for first SELECT (2 rows)
// 1 time for second SELECT (1 row with COUNT)
assert_eq!(callback_count, 3);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_exec_callback_abort() {
unsafe {
// Callback that aborts after first row
unsafe extern "C" fn abort_callback(
context: *mut std::ffi::c_void,
_n_cols: std::ffi::c_int,
_values: *mut *mut std::ffi::c_char,
_cols: *mut *mut std::ffi::c_char,
) -> std::ffi::c_int {
let count = &mut *(context as *mut i32);
*count += 1;
if *count >= 1 {
return 1; // Abort
}
0
}
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);
sqlite3_exec(
db,
c"CREATE TABLE test(x INTEGER);\
INSERT INTO test VALUES(1),(2),(3);"
.as_ptr(),
None,
ptr::null_mut(),
ptr::null_mut(),
);
let mut count = 0;
let rc = sqlite3_exec(
db,
c"SELECT x FROM test".as_ptr(),
Some(abort_callback),
&mut count as *mut _ as *mut std::ffi::c_void,
ptr::null_mut(),
);
assert_eq!(rc, SQLITE_ABORT);
assert_eq!(count, 1); // Only processed one row before aborting
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_exec_error_stops_execution() {
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 err_msg = ptr::null_mut();
// Second statement has error, third should not execute
let rc = sqlite3_exec(
db,
c"CREATE TABLE test(x INTEGER);\
INSERT INTO nonexistent VALUES(1);\
CREATE TABLE should_not_exist(y INTEGER);"
.as_ptr(),
None,
ptr::null_mut(),
&mut err_msg,
);
assert_eq!(rc, SQLITE_ERROR);
// Verify third statement didn't execute
let mut stmt = ptr::null_mut();
let check_rc = sqlite3_prepare_v2(
db,
c"SELECT name FROM sqlite_master WHERE type='table' AND name='should_not_exist'"
.as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
);
assert_eq!(check_rc, SQLITE_OK);
assert_eq!(sqlite3_step(stmt), SQLITE_DONE); // No rows = table doesn't exist
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
if !err_msg.is_null() {
sqlite3_free(err_msg as *mut std::ffi::c_void);
}
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_exec_empty_statements() {
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);
// Multiple semicolons and whitespace should be handled gracefully
let rc = sqlite3_exec(
db,
c"CREATE TABLE test(x INTEGER);;;\n\n;\t;INSERT INTO test VALUES(1);;;".as_ptr(),
None,
ptr::null_mut(),
ptr::null_mut(),
);
assert_eq!(rc, SQLITE_OK);
// Verify both statements executed
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"SELECT x FROM test".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
assert_eq!(sqlite3_column_int(stmt, 0), 1);
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_exec_with_comments() {
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);
// SQL comments shouldn't affect statement splitting
let rc = sqlite3_exec(
db,
c"-- This is a comment\n\
CREATE TABLE test(x INTEGER); -- inline comment\n\
INSERT INTO test VALUES(1); -- semicolon in comment ;\n\
INSERT INTO test VALUES(2) -- end with comment"
.as_ptr(),
None,
ptr::null_mut(),
ptr::null_mut(),
);
assert_eq!(rc, SQLITE_OK);
// Verify both inserts worked
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"SELECT COUNT(*) FROM test".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
assert_eq!(sqlite3_column_int(stmt, 0), 2);
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_exec_nested_quotes() {
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);
// Mix of quote types and nesting
let rc = sqlite3_exec(
db,
c"CREATE TABLE test(x TEXT);\
INSERT INTO test VALUES('single \"double\" inside');\
INSERT INTO test VALUES(\"double 'single' inside\");\
INSERT INTO test VALUES('mix;\"quote\";types');"
.as_ptr(),
None,
ptr::null_mut(),
ptr::null_mut(),
);
assert_eq!(rc, SQLITE_OK);
// Verify values
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"SELECT x FROM test ORDER BY rowid".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
let val1 = std::ffi::CStr::from_ptr(sqlite3_column_text(stmt, 0))
.to_str()
.unwrap();
assert_eq!(val1, "single \"double\" inside");
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
let val2 = std::ffi::CStr::from_ptr(sqlite3_column_text(stmt, 0))
.to_str()
.unwrap();
assert_eq!(val2, "double 'single' inside");
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
let val3 = std::ffi::CStr::from_ptr(sqlite3_column_text(stmt, 0))
.to_str()
.unwrap();
assert_eq!(val3, "mix;\"quote\";types");
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_exec_transaction_rollback() {
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);
// Test transaction rollback in multi-statement
let rc = sqlite3_exec(
db,
c"CREATE TABLE test(x INTEGER);\
BEGIN TRANSACTION;\
INSERT INTO test VALUES(1);\
INSERT INTO test VALUES(2);\
ROLLBACK;"
.as_ptr(),
None,
ptr::null_mut(),
ptr::null_mut(),
);
assert_eq!(rc, SQLITE_OK);
// Table should exist but be empty due to rollback
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"SELECT COUNT(*) FROM test".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
assert_eq!(sqlite3_column_int(stmt, 0), 0); // No rows due to rollback
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_exec_with_pragma() {
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);
// Callback to capture pragma results
unsafe extern "C" fn pragma_callback(
context: *mut std::ffi::c_void,
_n_cols: std::ffi::c_int,
_values: *mut *mut std::ffi::c_char,
_cols: *mut *mut std::ffi::c_char,
) -> std::ffi::c_int {
let count = &mut *(context as *mut i32);
*count += 1;
0
}
let mut callback_count = 0;
// PRAGMA should be treated as DQL when it returns results
let rc = sqlite3_exec(
db,
c"CREATE TABLE test(x INTEGER);\
PRAGMA table_info(test);"
.as_ptr(),
Some(pragma_callback),
&mut callback_count as *mut _ as *mut std::ffi::c_void,
ptr::null_mut(),
);
assert_eq!(rc, SQLITE_OK);
assert!(callback_count > 0); // PRAGMA should return at least one row
// PRAGMA without callback should discard row
let mut err_msg = ptr::null_mut();
let rc = sqlite3_exec(
db,
c"PRAGMA table_info(test)".as_ptr(),
None,
ptr::null_mut(),
&mut err_msg,
);
assert_eq!(rc, SQLITE_OK);
if !err_msg.is_null() {
sqlite3_free(err_msg as *mut std::ffi::c_void);
}
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_exec_with_cte() {
unsafe {
// Callback that collects results
unsafe extern "C" fn exec_callback(
context: *mut std::ffi::c_void,
n_cols: std::ffi::c_int,
values: *mut *mut std::ffi::c_char,
_cols: *mut *mut std::ffi::c_char,
) -> std::ffi::c_int {
let results = &mut *(context as *mut Vec<Vec<String>>);
let mut row = Vec::new();
for i in 0..n_cols as isize {
let value_ptr = *values.offset(i);
let value = if value_ptr.is_null() {
String::from("NULL")
} else {
std::ffi::CStr::from_ptr(value_ptr)
.to_str()
.unwrap()
.to_owned()
};
row.push(value);
}
results.push(row);
0
}
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);
// CTE should be recognized as DQL
let mut results: Vec<Vec<String>> = Vec::new();
let rc = sqlite3_exec(
db,
c"CREATE TABLE test(x INTEGER);\
INSERT INTO test VALUES(1),(2),(3);\
WITH cte AS (SELECT x FROM test WHERE x > 1) SELECT * FROM cte;"
.as_ptr(),
Some(exec_callback),
&mut results as *mut _ as *mut std::ffi::c_void,
ptr::null_mut(),
);
assert_eq!(rc, SQLITE_OK);
assert_eq!(results.len(), 2); // Should get 2 and 3
assert_eq!(results[0], vec!["2"]);
assert_eq!(results[1], vec!["3"]);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_exec_with_returning_clause() {
unsafe {
// Callback for RETURNING results
unsafe extern "C" fn exec_callback(
context: *mut std::ffi::c_void,
n_cols: std::ffi::c_int,
values: *mut *mut std::ffi::c_char,
_cols: *mut *mut std::ffi::c_char,
) -> std::ffi::c_int {
let results = &mut *(context as *mut Vec<Vec<String>>);
let mut row = Vec::new();
for i in 0..n_cols as isize {
let value_ptr = *values.offset(i);
let value = if value_ptr.is_null() {
String::from("NULL")
} else {
std::ffi::CStr::from_ptr(value_ptr)
.to_str()
.unwrap()
.to_owned()
};
row.push(value);
}
results.push(row);
0
}
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 results: Vec<Vec<String>> = Vec::new();
// INSERT...RETURNING with callback should capture the returned values
let rc = sqlite3_exec(
db,
c"CREATE TABLE test(id INTEGER PRIMARY KEY, x INTEGER);\
INSERT INTO test(x) VALUES(42) RETURNING id, x;"
.as_ptr(),
Some(exec_callback),
&mut results as *mut _ as *mut std::ffi::c_void,
ptr::null_mut(),
);
assert_eq!(rc, SQLITE_OK);
assert_eq!(results.len(), 1);
assert_eq!(results[0][1], "42"); // x value
// Add another row for testing
sqlite3_exec(
db,
c"INSERT INTO test(x) VALUES(99)".as_ptr(),
None,
ptr::null_mut(),
ptr::null_mut(),
);
// should still delete the row but discard the RETURNING results
let rc = sqlite3_exec(
db,
c"UPDATE test SET id = 3, x = 41 WHERE x=42 RETURNING id".as_ptr(),
None,
ptr::null_mut(),
ptr::null_mut(),
);
assert_eq!(rc, SQLITE_OK);
// Verify the row was actually updated
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(
db,
c"SELECT COUNT(*) FROM test WHERE x=42".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
assert_eq!(sqlite3_column_int(stmt, 0), 0); // Should be 0 rows with x=42
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
// Verify
assert_eq!(
sqlite3_prepare_v2(
db,
c"SELECT COUNT(*) FROM test".as_ptr(),
-1,
&mut stmt,
ptr::null_mut(),
),
SQLITE_OK
);
assert_eq!(sqlite3_step(stmt), SQLITE_ROW);
assert_eq!(sqlite3_column_int(stmt, 0), 2);
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[cfg(not(feature = "sqlite3"))]
mod libsql_ext {