Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 52 additions & 10 deletions rust/driver_manager/tests/common/dummy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
// specific language governing permissions and limitations
// under the License.

use std::collections::HashMap;

use adbc_core::error::{Error, Result, Status};
use adbc_core::options::{
InfoCode, ObjectDepth, OptionConnection, OptionDatabase, OptionStatement, OptionValue,
Expand Down Expand Up @@ -51,29 +53,69 @@ impl Driver for DummyDriver {
}

#[derive(Default)]
pub struct DummyDatabase {}
pub struct DummyDatabase {
options: HashMap<OptionDatabase, OptionValue>,
}

impl DummyDatabase {
fn option(&self, key: OptionDatabase) -> Result<&OptionValue> {
// Let concurrent exported getters overlap while their driver borrows are live.
std::thread::yield_now();
self.options.get(&key).ok_or_else(|| {
Error::with_message_and_status(
format!("Option key not found: {key:?}"),
Status::NotFound,
)
})
}
}

impl Optionable for DummyDatabase {
type Option = OptionDatabase;

fn set_option(&mut self, _key: Self::Option, _value: OptionValue) -> Result<()> {
fn set_option(&mut self, key: Self::Option, value: OptionValue) -> Result<()> {
self.options.insert(key, value);
Ok(())
}

fn get_option_bytes(&self, _key: Self::Option) -> Result<Vec<u8>> {
todo!()
fn get_option_bytes(&self, key: Self::Option) -> Result<Vec<u8>> {
match self.option(key)? {
OptionValue::Bytes(value) => Ok(value.clone()),
_ => Err(Error::with_message_and_status(
"Expected bytes option",
Status::InvalidState,
)),
}
}

fn get_option_double(&self, _key: Self::Option) -> Result<f64> {
todo!()
fn get_option_double(&self, key: Self::Option) -> Result<f64> {
match self.option(key)? {
OptionValue::Double(value) => Ok(*value),
_ => Err(Error::with_message_and_status(
"Expected double option",
Status::InvalidState,
)),
}
}

fn get_option_int(&self, _key: Self::Option) -> Result<i64> {
todo!()
fn get_option_int(&self, key: Self::Option) -> Result<i64> {
match self.option(key)? {
OptionValue::Int(value) => Ok(*value),
_ => Err(Error::with_message_and_status(
"Expected integer option",
Status::InvalidState,
)),
}
}

fn get_option_string(&self, _key: Self::Option) -> Result<String> {
todo!()
fn get_option_string(&self, key: Self::Option) -> Result<String> {
match self.option(key)? {
OptionValue::String(value) => Ok(value.clone()),
_ => Err(Error::with_message_and_status(
"Expected string option",
Status::InvalidState,
)),
}
}
}

Expand Down
156 changes: 153 additions & 3 deletions rust/driver_manager/tests/exporter_cancellation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
// Test that the driver exporter and cancellation do not have undefined
// behavior by introducing overlapping mutable borrows. Run under Miri.

use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicPtr, Ordering};
use std::sync::{Arc, Barrier, Mutex};

use adbc_core::error::{Result, Status};
use adbc_core::constants::ADBC_STATUS_OK;
use adbc_core::error::{Error, Result, Status};
use adbc_core::options::AdbcVersion;
use adbc_core::{CancelHandle, Connection, Database, Driver, Statement};
use adbc_driver_manager::ManagedDriver;
use adbc_ffi::FFI_AdbcDriverInitFunc;
use adbc_ffi::{FFI_AdbcDatabase, FFI_AdbcDriverInitFunc, FFI_AdbcError, FFIDriver};
use arrow_array::{RecordBatch, RecordBatchReader};
use arrow_schema::{ArrowError, Schema, SchemaRef};

Expand Down Expand Up @@ -105,3 +107,151 @@ fn connection_cancellation() -> Result<()> {
guard.join().unwrap();
Ok(())
}

// Exercise database getters directly (avoiding driver manager's lock) to test
// for potential UB in the exporter under concurrent access.
fn concurrent_database_getters(initialized: bool) -> Result<()> {
let driver = common::dummy::DummyDriver::ffi_driver();
let new = driver.DatabaseNew.unwrap();
let init = driver.DatabaseInit.unwrap();
let release = driver.DatabaseRelease.unwrap();
let set_string = driver.DatabaseSetOption.unwrap();
let set_bytes = driver.DatabaseSetOptionBytes.unwrap();
let set_int = driver.DatabaseSetOptionInt.unwrap();
let set_double = driver.DatabaseSetOptionDouble.unwrap();
let get_string = driver.DatabaseGetOption.unwrap();
let get_bytes = driver.DatabaseGetOptionBytes.unwrap();
let get_int = driver.DatabaseGetOptionInt.unwrap();
let get_double = driver.DatabaseGetOptionDouble.unwrap();

let mut database = FFI_AdbcDatabase::default();
let mut error = FFI_AdbcError::default();
// Use the same raw pointer throughout: workers must not create &mut borrows
// of the shared handle, and passing through AtomicPtr preserves provenance.
let database_ptr = &raw mut database;
unsafe {
assert_eq!(new(database_ptr, &mut error), ADBC_STATUS_OK);
assert_eq!(
set_string(
database_ptr,
c"string".as_ptr(),
c"hello".as_ptr(),
&mut error
),
ADBC_STATUS_OK
);
assert_eq!(
set_bytes(
database_ptr,
c"bytes".as_ptr(),
b"hello".as_ptr(),
5,
&mut error
),
ADBC_STATUS_OK
);
assert_eq!(
set_int(database_ptr, c"int".as_ptr(), 42, &mut error),
ADBC_STATUS_OK
);
assert_eq!(
set_double(database_ptr, c"double".as_ptr(), 1.25, &mut error),
ADBC_STATUS_OK
);
if initialized {
assert_eq!(init(database_ptr, &mut error), ADBC_STATUS_OK);
// Also exercise the setter against the initialized database mutex.
assert_eq!(
set_int(database_ptr, c"int".as_ptr(), 42, &mut error),
ADBC_STATUS_OK
);
}
}

let shared = AtomicPtr::new(database_ptr);
let barrier = Barrier::new(2);
let result = std::thread::scope(|scope| -> Result<()> {
let mut workers = Vec::new();
for _ in 0..2 {
workers.push(scope.spawn(|| {
let database = shared.load(Ordering::Relaxed);
let mut error = FFI_AdbcError::default();
barrier.wait();
for _ in 0..4 {
let mut string = [0; 6];
let mut length = string.len();
unsafe {
assert_eq!(
get_string(
database,
c"string".as_ptr(),
string.as_mut_ptr().cast(),
&mut length,
&mut error
),
ADBC_STATUS_OK
);
}
assert_eq!(length, 6);
assert_eq!(&string, b"hello\0");

let mut bytes = [0; 5];
let mut length = bytes.len();
unsafe {
assert_eq!(
get_bytes(
database,
c"bytes".as_ptr(),
bytes.as_mut_ptr(),
&mut length,
&mut error
),
ADBC_STATUS_OK
);
}
assert_eq!(length, 5);
assert_eq!(&bytes, b"hello");

let mut int = 0;
unsafe {
assert_eq!(
get_int(database, c"int".as_ptr(), &mut int, &mut error),
ADBC_STATUS_OK
);
}
assert_eq!(int, 42);

let mut double = 0.0;
unsafe {
assert_eq!(
get_double(database, c"double".as_ptr(), &mut double, &mut error),
ADBC_STATUS_OK
);
}
assert_eq!(double, 1.25);
}
}));
}
for worker in workers {
worker.join().map_err(|_| {
Error::with_message_and_status("Database getter worker panicked", Status::Internal)
})?;
}
Ok(())
});

unsafe {
assert_eq!(release(database_ptr, &mut error), ADBC_STATUS_OK);
}
result
}

#[test]
fn database_getters_before_init() -> Result<()> {
concurrent_database_getters(false)
}

#[test]
fn database_getters_after_init() -> Result<()> {
concurrent_database_getters(true)
}
Loading
Loading