diff --git a/sqlite3/src/lib.rs b/sqlite3/src/lib.rs index 0083715ae..2cfa52bbd 100644 --- a/sqlite3/src/lib.rs +++ b/sqlite3/src/lib.rs @@ -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 = 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] diff --git a/sqlite3/tests/compat/mod.rs b/sqlite3/tests/compat/mod.rs index 0badf6051..a1f016ddc 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,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>); + 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_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>); + 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 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 {