diff --git a/odbc-api/src/buffers/column_with_indicator.rs b/odbc-api/src/buffers/column_with_indicator.rs index bb77a3a9..b7219db0 100644 --- a/odbc-api/src/buffers/column_with_indicator.rs +++ b/odbc-api/src/buffers/column_with_indicator.rs @@ -93,6 +93,37 @@ impl<'a, T> NullableSlice<'a, T> { pub fn len(&self) -> usize { self.values.len() } + + /// Read access to the underlying raw value and indicator buffer. + /// + /// The number of elements in the buffer is equal to the number of rows returned in the current + /// result set. Yet the content of any value, those associated value in the indicator buffer is + /// [`crate::sys::NULL_DATA`] is undefined. + /// + /// This method is useful for writing performant bindings to datastructures with similar binary + /// layout, as it allows for using memcopy rather than iterating over individual values. + /// + /// # Example + /// + /// ``` + /// use odbc_api::{buffers::NullableSlice, sys::NULL_DATA}; + /// + /// // Memcopy the values out of the buffer, and make a mask of bools indicating the NULL + /// // values. + /// fn copy_values_and_make_mask(odbc_slice: NullableSlice) -> (Vec, Vec) { + /// let (values, indicators) = odbc_slice.raw_values(); + /// let values = values.to_vec(); + /// // Create array of bools indicating null values. + /// let mask: Vec = indicators + /// .iter() + /// .map(|&indicator| indicator != NULL_DATA) + /// .collect(); + /// (values, mask) + /// } + /// ``` + pub fn raw_values(&self) -> (&'a [T], &'a [isize]) { + (self.values, self.indicators) + } } impl<'a, T> Iterator for NullableSlice<'a, T> { diff --git a/odbc-api/tests/integration.rs b/odbc-api/tests/integration.rs index d1640c48..c07995d0 100644 --- a/odbc-api/tests/integration.rs +++ b/odbc-api/tests/integration.rs @@ -3046,6 +3046,60 @@ fn panic_in_drop_handlers_should_not_mask_original_error(profile: &Profile) { panic!("original error") } +/// Arrow uses the same binary format for the values of nullable slices, though null are represented +/// as bitmask. Make it possible for bindings to efficiently copy the values array out of a +/// nullable slice. +#[test_case(MSSQL; "Microsoft SQL Server")] +#[test_case(MARIADB; "Maria DB")] +#[test_case(SQLITE_3; "SQLite 3")] +fn memcopy_values_from_nullable_slice(profile: &Profile) { + // Given + let table_name = "MemcopyValuesFromNullableSlice"; + let conn = profile + .setup_empty_table(table_name, &["INTEGER"]) + .unwrap(); + conn.execute( + &format!("INSERT INTO {table_name} (a) VALUES (42), (NULL), (5);"), + (), + ) + .unwrap(); + + // When + let cursor = conn + .execute(&format!("SELECT a FROM {table_name}"), ()) + .unwrap() // Unwrap Result + .unwrap(); // Unwrap Option, we know a select statement to produce a cursor. + let buffer = buffer_from_description( + 3, + iter::once(BufferDescription { + kind: BufferKind::I32, + nullable: true, + }), + ); + let mut cursor = cursor.bind_buffer(buffer).unwrap(); + let batch = cursor.fetch().unwrap().unwrap(); + let nullable_slice = match batch.column(0) { + AnyColumnView::NullableI32(nullable_slice) => nullable_slice, + _ => panic!("Expected View type to be a nullable i32"), + }; + let (values, indicators) = nullable_slice.raw_values(); + // Memcopy values. + let values = values.to_vec(); + // Create array of bools indicating null values. + let nulls: Vec = indicators + .iter() + .map(|&indicator| indicator == sys::NULL_DATA) + .collect(); + + // Then + assert!(!nulls[0]); + assert_eq!(values[0], 42); + assert!(nulls[1]); + // We explicitly don't give any guarantees about the value of #values[1]`. + assert!(!nulls[2]); + assert_eq!(values[2], 5); +} + /// This test is inspired by a bug caused from a fetch statement generating a lot of diagnostic /// messages. #[test]