From d0fd258ab58db23154cf33a8086083f1c2c6f16a Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Wed, 22 Oct 2025 15:02:24 -0400 Subject: [PATCH 1/7] Handle multiple statements via sqlite3_exec API --- sqlite3/src/lib.rs | 209 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 198 insertions(+), 11 deletions(-) diff --git a/sqlite3/src/lib.rs b/sqlite3/src/lib.rs index 0083715ae..32e20b0ca 100644 --- a/sqlite3/src/lib.rs +++ b/sqlite3/src/lib.rs @@ -380,25 +380,212 @@ 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); + let statements = split_sql_statements(sql_str); + + for stmt_sql in statements { + let trimmed = stmt_sql.trim(); + if trimmed.is_empty() { + continue; + } + + // check if this is a DQL statement, because we will only allow if there is a callback + let is_dql = is_query_statement(trimmed); + if is_dql && callback.is_none() { + if !err.is_null() { + let err_msg = + CString::new("queries return results, use callback or sqlite3_prepare") + .unwrap(); + *err = err_msg.into_raw(); + } + return SQLITE_MISUSE; + } + + // For DML/DDL, use normal execute path + if !is_dql { + 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 { + // Handle 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 sql_upper = sql.to_uppercase(); + let first_token = sql_upper.split_whitespace().next().unwrap_or(""); + + matches!( + first_token, + "SELECT" | "VALUES" | "WITH" | "PRAGMA" | "EXPLAIN" + ) || sql_upper.contains("RETURNING") +} + +/// 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 = 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 = 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] From 921f2e72bd1a5b66442800292b448faf76a3d8df Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Wed, 22 Oct 2025 15:02:49 -0400 Subject: [PATCH 2/7] Add integration tests for sqlite3_exec multi-statements --- sqlite3/tests/compat/mod.rs | 759 ++++++++++++++++++++++++++++++++++++ 1 file changed, 759 insertions(+) diff --git a/sqlite3/tests/compat/mod.rs b/sqlite3/tests/compat/mod.rs index 0badf6051..b3c911e66 100644 --- a/sqlite3/tests/compat/mod.rs +++ b/sqlite3/tests/compat/mod.rs @@ -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,749 @@ 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>); + 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::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_select_without_callback_fails() { + 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); + + sqlite3_exec( + db, + c"CREATE TABLE test(x INTEGER)".as_ptr(), + None, + ptr::null_mut(), + ptr::null_mut(), + ); + + // SELECT without callback should fail + let mut err_msg = ptr::null_mut(); + let rc = sqlite3_exec( + db, + c"SELECT * FROM test".as_ptr(), + None, + ptr::null_mut(), + &mut err_msg, + ); + assert_eq!(rc, SQLITE_MISUSE); + + if !err_msg.is_null() { + let msg = std::ffi::CStr::from_ptr(err_msg).to_str().unwrap(); + println!("Error message: {msg:?}"); + assert!(msg.contains("callback") || msg.contains("prepare")); + // Free the error message + sqlite3_free(err_msg as *mut std::ffi::c_void); + } + + let rc = sqlite3_close(db); + println!("RESULT: {rc}"); + assert_eq!(rc, 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 fail + 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_MISUSE); + 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>); + 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::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>); + 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::new(); + + // INSERT...RETURNING should be treated as DQL + 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 + + // RETURNING without callback should fail + let mut err_msg = ptr::null_mut(); + let rc = sqlite3_exec( + db, + c"DELETE FROM test WHERE x=42 RETURNING id".as_ptr(), + None, + ptr::null_mut(), + &mut err_msg, + ); + assert_eq!(rc, SQLITE_MISUSE); + if !err_msg.is_null() { + sqlite3_free(err_msg as *mut std::ffi::c_void); + } + + assert_eq!(sqlite3_close(db), SQLITE_OK); + } + } + #[cfg(not(feature = "sqlite3"))] mod libsql_ext { From fb26b72b1a1c4939d1f5483bbb57774ef39189af Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Wed, 22 Oct 2025 15:29:54 -0400 Subject: [PATCH 3/7] Add comment from sqlite3.h describing behavior of sqlite3_exec --- sqlite3/src/lib.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/sqlite3/src/lib.rs b/sqlite3/src/lib.rs index 32e20b0ca..15ca34bd7 100644 --- a/sqlite3/src/lib.rs +++ b/sqlite3/src/lib.rs @@ -376,6 +376,35 @@ type exec_callback = Option< ) -> ffi::c_int, >; +/* sqlite.h 365 +** The sqlite3_exec() interface is a convenience wrapper around +** [sqlite3_prepare_v2()], [sqlite3_step()], and [sqlite3_finalize()], +** that allows an application to run multiple statements of SQL +** without having to use a lot of C code. +** +** ^The sqlite3_exec() interface runs zero or more UTF-8 encoded, +** semicolon-separate SQL statements passed into its 2nd argument, +** in the context of the [database connection] passed in as its 1st +** argument. ^If the callback function of the 3rd argument to +** sqlite3_exec() is not NULL, then it is invoked for each result row +** coming out of the evaluated SQL statements. ^The 4th argument to +** sqlite3_exec() is relayed through to the 1st argument of each +** callback invocation. ^If the callback pointer to sqlite3_exec() +** is NULL, then no callback is ever invoked and result rows are +** ignored. +** +** ^If an error occurs while evaluating the SQL statements passed into +** sqlite3_exec(), then execution of the current statement stops and +** subsequent statements are skipped. ^If the 5th parameter to sqlite3_exec() +** is not NULL then any error message is written into memory obtained +** from [sqlite3_malloc()] and passed back through the 5th parameter. +** To avoid memory leaks, the application should invoke [sqlite3_free()] +** on error message strings returned through the 5th parameter of +** sqlite3_exec() after the error message string is no longer needed. +** ^If the 5th parameter to sqlite3_exec() is not NULL and no errors +** occur, then sqlite3_exec() sets the pointer in its 5th parameter to +** NULL before returning. +*/ #[no_mangle] pub unsafe extern "C" fn sqlite3_exec( db: *mut sqlite3, @@ -437,6 +466,14 @@ pub unsafe extern "C" fn sqlite3_exec( } } + /* ^If the 5th parameter to sqlite3_exec() is not NULL and no errors + ** occur, then sqlite3_exec() sets the pointer in its 5th parameter to + ** NULL before returning. + */ + if !err.is_null() { + *err = std::ptr::null_mut(); + } + SQLITE_OK } From ec30aad0155c23dc0d87b31629b2fa212781896d Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Wed, 22 Oct 2025 15:52:47 -0400 Subject: [PATCH 4/7] Replace inefficient is_query_statement fn in sqlite3 api --- sqlite3/src/lib.rs | 49 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/sqlite3/src/lib.rs b/sqlite3/src/lib.rs index 15ca34bd7..638b2683d 100644 --- a/sqlite3/src/lib.rs +++ b/sqlite3/src/lib.rs @@ -479,13 +479,50 @@ pub unsafe extern "C" fn sqlite3_exec( /// Detect if a SQL statement is DQL fn is_query_statement(sql: &str) -> bool { - let sql_upper = sql.to_uppercase(); - let first_token = sql_upper.split_whitespace().next().unwrap_or(""); + let trimmed = sql.trim_start(); + if trimmed.is_empty() { + return false; + } + let bytes = trimmed.as_bytes(); - matches!( - first_token, - "SELECT" | "VALUES" | "WITH" | "PRAGMA" | "EXPLAIN" - ) || sql_upper.contains("RETURNING") + 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 From 1204494068809e8e6747a57292f70eb9a2b714cf Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Wed, 22 Oct 2025 16:41:56 -0400 Subject: [PATCH 5/7] Fix sqlite_exec callback handling to discard rows when not provided --- sqlite3/src/lib.rs | 61 +++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/sqlite3/src/lib.rs b/sqlite3/src/lib.rs index 638b2683d..2a8a223e5 100644 --- a/sqlite3/src/lib.rs +++ b/sqlite3/src/lib.rs @@ -424,28 +424,19 @@ pub unsafe extern "C" fn sqlite3_exec( Err(_) => return SQLITE_MISUSE, }; 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; } - // check if this is a DQL statement, because we will only allow if there is a callback let is_dql = is_query_statement(trimmed); - if is_dql && callback.is_none() { - if !err.is_null() { - let err_msg = - CString::new("queries return results, use callback or sqlite3_prepare") - .unwrap(); - *err = err_msg.into_raw(); - } - return SQLITE_MISUSE; - } - - // For DML/DDL, use normal execute path 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, @@ -457,23 +448,47 @@ pub unsafe extern "C" fn sqlite3_exec( 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 { - // Handle DQL with callback + // DQL with callback let rc = execute_query_with_callback(db, trimmed, callback, context, err); if rc != SQLITE_OK { return rc; } } } - - /* ^If the 5th parameter to sqlite3_exec() is not NULL and no errors - ** occur, then sqlite3_exec() sets the pointer in its 5th parameter to - ** NULL before returning. - */ - if !err.is_null() { - *err = std::ptr::null_mut(); - } - SQLITE_OK } From 5318af16b5313eea3fb4317f4d95bf5b5040e30b Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Wed, 22 Oct 2025 16:42:20 -0400 Subject: [PATCH 6/7] Update tests in sqlite3 package to adapt to sqlite behavior --- sqlite3/tests/compat/mod.rs | 101 +++++++++++++++++------------------- 1 file changed, 48 insertions(+), 53 deletions(-) diff --git a/sqlite3/tests/compat/mod.rs b/sqlite3/tests/compat/mod.rs index b3c911e66..a1f016ddc 100644 --- a/sqlite3/tests/compat/mod.rs +++ b/sqlite3/tests/compat/mod.rs @@ -1033,47 +1033,6 @@ mod tests { } } - #[test] - fn test_exec_select_without_callback_fails() { - 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); - - sqlite3_exec( - db, - c"CREATE TABLE test(x INTEGER)".as_ptr(), - None, - ptr::null_mut(), - ptr::null_mut(), - ); - - // SELECT without callback should fail - let mut err_msg = ptr::null_mut(); - let rc = sqlite3_exec( - db, - c"SELECT * FROM test".as_ptr(), - None, - ptr::null_mut(), - &mut err_msg, - ); - assert_eq!(rc, SQLITE_MISUSE); - - if !err_msg.is_null() { - let msg = std::ffi::CStr::from_ptr(err_msg).to_str().unwrap(); - println!("Error message: {msg:?}"); - assert!(msg.contains("callback") || msg.contains("prepare")); - // Free the error message - sqlite3_free(err_msg as *mut std::ffi::c_void); - } - - let rc = sqlite3_close(db); - println!("RESULT: {rc}"); - assert_eq!(rc, SQLITE_OK); - } - } - #[test] fn test_exec_callback_abort() { unsafe { @@ -1382,7 +1341,7 @@ mod tests { assert_eq!(rc, SQLITE_OK); assert!(callback_count > 0); // PRAGMA should return at least one row - // PRAGMA without callback should fail + // PRAGMA without callback should discard row let mut err_msg = ptr::null_mut(); let rc = sqlite3_exec( db, @@ -1391,7 +1350,7 @@ mod tests { ptr::null_mut(), &mut err_msg, ); - assert_eq!(rc, SQLITE_MISUSE); + assert_eq!(rc, SQLITE_OK); if !err_msg.is_null() { sqlite3_free(err_msg as *mut std::ffi::c_void); } @@ -1489,7 +1448,7 @@ mod tests { let mut results: Vec> = Vec::new(); - // INSERT...RETURNING should be treated as DQL + // INSERT...RETURNING with callback should capture the returned values let rc = sqlite3_exec( db, c"CREATE TABLE test(id INTEGER PRIMARY KEY, x INTEGER);\ @@ -1503,19 +1462,55 @@ mod tests { assert_eq!(results.len(), 1); assert_eq!(results[0][1], "42"); // x value - // RETURNING without callback should fail - let mut err_msg = ptr::null_mut(); - let rc = sqlite3_exec( + // Add another row for testing + sqlite3_exec( db, - c"DELETE FROM test WHERE x=42 RETURNING id".as_ptr(), + c"INSERT INTO test(x) VALUES(99)".as_ptr(), None, ptr::null_mut(), - &mut err_msg, + ptr::null_mut(), ); - assert_eq!(rc, SQLITE_MISUSE); - if !err_msg.is_null() { - sqlite3_free(err_msg as *mut std::ffi::c_void); - } + + // 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); } From e9f1a451a28dbef58bc5ba7c43d7ae97464c9ce9 Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Fri, 24 Oct 2025 09:35:54 -0400 Subject: [PATCH 7/7] Remove sqlite comment from sqlite3_exec api --- sqlite3/src/lib.rs | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/sqlite3/src/lib.rs b/sqlite3/src/lib.rs index 2a8a223e5..2cfa52bbd 100644 --- a/sqlite3/src/lib.rs +++ b/sqlite3/src/lib.rs @@ -376,35 +376,6 @@ type exec_callback = Option< ) -> ffi::c_int, >; -/* sqlite.h 365 -** The sqlite3_exec() interface is a convenience wrapper around -** [sqlite3_prepare_v2()], [sqlite3_step()], and [sqlite3_finalize()], -** that allows an application to run multiple statements of SQL -** without having to use a lot of C code. -** -** ^The sqlite3_exec() interface runs zero or more UTF-8 encoded, -** semicolon-separate SQL statements passed into its 2nd argument, -** in the context of the [database connection] passed in as its 1st -** argument. ^If the callback function of the 3rd argument to -** sqlite3_exec() is not NULL, then it is invoked for each result row -** coming out of the evaluated SQL statements. ^The 4th argument to -** sqlite3_exec() is relayed through to the 1st argument of each -** callback invocation. ^If the callback pointer to sqlite3_exec() -** is NULL, then no callback is ever invoked and result rows are -** ignored. -** -** ^If an error occurs while evaluating the SQL statements passed into -** sqlite3_exec(), then execution of the current statement stops and -** subsequent statements are skipped. ^If the 5th parameter to sqlite3_exec() -** is not NULL then any error message is written into memory obtained -** from [sqlite3_malloc()] and passed back through the 5th parameter. -** To avoid memory leaks, the application should invoke [sqlite3_free()] -** on error message strings returned through the 5th parameter of -** sqlite3_exec() after the error message string is no longer needed. -** ^If the 5th parameter to sqlite3_exec() is not NULL and no errors -** occur, then sqlite3_exec() sets the pointer in its 5th parameter to -** NULL before returning. -*/ #[no_mangle] pub unsafe extern "C" fn sqlite3_exec( db: *mut sqlite3,