#[cfg(test)]
mod tests;
use super::{BindInfo, DataChunkHandle, InitInfo, LogicalTypeHandle, TableFunctionInfo, VTab};
use std::{
collections::HashMap,
sync::{
LazyLock, Mutex, MutexGuard, PoisonError,
atomic::{AtomicU64, AtomicUsize, Ordering},
},
};
use arrow::{
array::{ArrayData, StructArray},
datatypes::DataType,
error::ArrowError,
ffi::{FFI_ArrowArray, FFI_ArrowSchema},
record_batch::RecordBatch,
};
pub use crate::arrow_interop::*;
use crate::{ToSql, core::LogicalTypeId, types::ToSqlOutput};
pub struct ArrowBindData {
rb: RecordBatch,
}
pub struct ArrowInitData {
offset: AtomicUsize,
vector_size: usize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct RowSlice {
offset: usize,
len: usize,
}
impl ArrowInitData {
fn new(vector_size: usize) -> Self {
Self {
offset: AtomicUsize::new(0),
vector_size,
}
}
fn take_slice(&self, num_rows: usize) -> Option<RowSlice> {
let vector_size = self.vector_size;
let offset = self
.offset
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |offset| {
if offset >= num_rows {
None
} else {
Some(offset.saturating_add(vector_size).min(num_rows))
}
})
.ok()?;
Some(RowSlice {
offset,
len: (num_rows - offset).min(vector_size),
})
}
}
pub struct ArrowVTab;
static BATCHES: LazyLock<Mutex<HashMap<u64, RecordBatch>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
const ARROW_BATCH_TOKEN_START: u64 = 1 << 63;
static NEXT_TOKEN: AtomicU64 = AtomicU64::new(ARROW_BATCH_TOKEN_START);
fn lock_batches() -> MutexGuard<'static, HashMap<u64, RecordBatch>> {
BATCHES.lock().unwrap_or_else(PoisonError::into_inner)
}
impl VTab for ArrowVTab {
type BindData = ArrowBindData;
type InitData = ArrowInitData;
fn bind(bind: &BindInfo) -> Result<Self::BindData, Box<dyn std::error::Error>> {
let value = bind.get_parameter(0);
if value.is_null() {
return Err("ArrowVTab record batch token parameter must not be NULL".into());
}
let token = value.to_uint64();
let rb = lock_batches()
.get(&token)
.cloned()
.ok_or_else(|| format!("ArrowVTab record batch token {token} is not registered"))?;
for f in rb.schema().fields() {
let name = f.name();
let logical_type = to_duckdb_logical_type_for_field(f)?;
bind.add_result_column(name, logical_type);
}
Ok(ArrowBindData { rb })
}
fn init(_: &InitInfo) -> Result<Self::InitData, Box<dyn std::error::Error>> {
let vector_size = unsafe { crate::ffi::duckdb_vector_size() } as usize;
if vector_size == 0 {
return Err("DuckDB vector size must be greater than zero".into());
}
Ok(ArrowInitData::new(vector_size))
}
fn func(func: &TableFunctionInfo<Self>, output: &mut DataChunkHandle) -> Result<(), Box<dyn std::error::Error>> {
let init_info = func.get_init_data();
let rb = &func.get_bind_data().rb;
let num_rows = rb.num_rows();
let Some(slice) = init_info.take_slice(num_rows) else {
output.set_len(0);
return Ok(());
};
record_batch_to_duckdb_data_chunk(&rb.slice(slice.offset, slice.len), output)?;
Ok(())
}
fn parameters() -> Option<Vec<LogicalTypeHandle>> {
Some(vec![LogicalTypeHandle::from(LogicalTypeId::UBigint)])
}
}
#[derive(Debug)]
#[must_use = "dropping the registration releases its Arrow record batch"]
pub struct ArrowBatchRegistration {
token: u64,
}
impl ArrowBatchRegistration {
pub fn new(rb: RecordBatch) -> Self {
let token = NEXT_TOKEN.fetch_add(1, Ordering::Relaxed);
lock_batches().insert(token, rb);
Self { token }
}
pub fn from_array_data(data: ArrayData) -> Result<Self, ArrowError> {
if !matches!(data.data_type(), DataType::Struct(_)) {
return Err(ArrowError::InvalidArgumentError(format!(
"ArrowVTab registration requires a struct array, got {:?}",
data.data_type()
)));
}
if data.null_count() != 0 {
return Err(ArrowError::InvalidArgumentError(format!(
"ArrowVTab registration does not support {} top-level nulls",
data.null_count()
)));
}
let struct_array = StructArray::from(data);
Ok(Self::new(RecordBatch::from(struct_array)))
}
pub unsafe fn from_ffi(array: FFI_ArrowArray, schema: FFI_ArrowSchema) -> Result<Self, ArrowError> {
let data = unsafe { arrow::ffi::from_ffi(array, &schema) }?;
Self::from_array_data(data)
}
}
impl ToSql for &ArrowBatchRegistration {
fn to_sql(&self) -> crate::Result<ToSqlOutput<'_>> {
Ok(ToSqlOutput::from(self.token))
}
}
impl Drop for ArrowBatchRegistration {
fn drop(&mut self) {
let removed = {
let mut batches = lock_batches();
batches.remove(&self.token)
};
drop(removed);
}
}