#![deny(unsafe_op_in_unsafe_fn)]
#![warn(missing_docs)]
use std::cell::{Cell, RefCell};
use std::ffi::{c_char, c_int, c_void, CStr, CString};
use std::fmt;
use std::marker::PhantomData;
use std::ptr::NonNull;
use arrow::array::{make_array, Array as ArrowArrayTrait, ArrayRef};
use arrow::datatypes::DataType;
use arrow::ffi::{from_ffi, to_ffi, FFI_ArrowArray, FFI_ArrowSchema};
use arrowmetal_sys as sys;
use sys::ffi;
pub const LIB_DIR: &str = env!("ARROWMETAL_LINKED_LIB_DIR");
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Error {
message: String,
}
impl Error {
pub fn message(&self) -> &str {
&self.message
}
fn new(message: impl Into<String>) -> Self {
Self { message: message.into() }
}
fn last(context: &str) -> Self {
let raw = unsafe { ffi::am_last_error() };
let detail = if raw.is_null() {
String::from("(no message)")
} else {
unsafe { CStr::from_ptr(raw) }.to_string_lossy().into_owned()
};
Self::new(format!("{context}: {detail}"))
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for Error {}
impl From<arrow::error::ArrowError> for Error {
fn from(e: arrow::error::ArrowError) -> Self {
Self::new(format!("arrow: {e}"))
}
}
pub type Result<T> = std::result::Result<T, Error>;
fn check(code: c_int, context: &str) -> Result<()> {
if code == 0 {
Ok(())
} else {
Err(Error::last(context))
}
}
fn cstring(s: &str, what: &str) -> Result<CString> {
CString::new(s).map_err(|_| Error::new(format!("{what} contains an interior NUL byte")))
}
pub fn version() -> &'static str {
unsafe { CStr::from_ptr(ffi::am_version()) }.to_str().unwrap_or("unknown")
}
pub fn device_name() -> String {
unsafe { CStr::from_ptr(ffi::am_device_name()) }.to_string_lossy().into_owned()
}
mod sealed {
pub trait Sealed {}
}
pub trait NativeType: Copy + sealed::Sealed {
const FORMAT: &'static str;
}
macro_rules! native {
($($t:ty => $f:literal),* $(,)?) => {$(
impl sealed::Sealed for $t {}
impl NativeType for $t {
const FORMAT: &'static str = $f;
}
)*};
}
native! {
i8 => "c", u8 => "C",
i16 => "s", u16 => "S",
i32 => "i", u32 => "I",
i64 => "l", u64 => "L",
f32 => "f", f64 => "g",
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Scalar {
Int64(i64),
UInt64(u64),
Float64(f64),
}
impl Scalar {
pub fn as_f64(self) -> f64 {
match self {
Scalar::Int64(v) => v as f64,
Scalar::UInt64(v) => v as f64,
Scalar::Float64(v) => v,
}
}
pub fn as_i64(self) -> Option<i64> {
match self {
Scalar::Int64(v) => Some(v),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompareOp {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
impl CompareOp {
fn code(self) -> c_int {
match self {
CompareOp::Eq => 0,
CompareOp::Ne => 1,
CompareOp::Lt => 2,
CompareOp::Le => 3,
CompareOp::Gt => 4,
CompareOp::Ge => 5,
}
}
}
pub struct Array {
ptr: NonNull<sys::am_array>,
_not_send: PhantomData<*const ()>,
}
impl Drop for Array {
fn drop(&mut self) {
unsafe { ffi::am_release(self.ptr.as_ptr()) }
}
}
impl fmt::Debug for Array {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("arrowmetal::Array")
.field("len", &self.len())
.field("null_count", &self.null_count())
.field("format", &self.format())
.finish()
}
}
impl Array {
unsafe fn from_raw(ptr: *mut sys::am_array, context: &str) -> Result<Self> {
match NonNull::new(ptr) {
Some(ptr) => Ok(Self { ptr, _not_send: PhantomData }),
None => Err(Error::new(format!("{context}: succeeded but returned a null handle"))),
}
}
fn produce(context: &str, call: impl FnOnce(*mut *mut sys::am_array) -> c_int) -> Result<Self> {
let mut out: *mut sys::am_array = std::ptr::null_mut();
check(call(&mut out), context)?;
unsafe { Self::from_raw(out, context) }
}
fn as_ptr(&self) -> *mut sys::am_array {
self.ptr.as_ptr()
}
pub fn from_arrow(array: &dyn ArrowArrayTrait) -> Result<Self> {
if let DataType::Dictionary(key, value) = array.data_type() {
return Err(Error::new(format!(
"dictionary-encoded arrays are not accepted (this array is \
Dictionary({key}, {value})): ArrowMetal's am_format reports a dictionary's index \
type while its kernels compute on the value type, so a scalar operand cannot be \
type-checked against it. Decode first, e.g. \
`arrow::compute::cast(&array, &DataType::{value})`."
)));
}
let (ffi_array, ffi_schema) = to_ffi(&array.to_data())?;
let mut ffi_array = ffi_array;
Self::produce("am_import", |out| unsafe {
ffi::am_import(
(&ffi_schema as *const FFI_ArrowSchema).cast::<sys::ArrowSchema>(),
(&mut ffi_array as *mut FFI_ArrowArray).cast::<sys::ArrowArray>(),
out,
)
})
}
pub fn to_arrow(&self) -> Result<ArrayRef> {
let mut schema = FFI_ArrowSchema::empty();
let mut array = FFI_ArrowArray::empty();
check(
unsafe {
ffi::am_export(
self.as_ptr(),
(&mut schema as *mut FFI_ArrowSchema).cast::<sys::ArrowSchema>(),
(&mut array as *mut FFI_ArrowArray).cast::<sys::ArrowArray>(),
)
},
"am_export",
)?;
let data = unsafe { from_ffi(array, &schema) }?;
Ok(make_array(data))
}
pub fn len(&self) -> usize {
let n = unsafe { ffi::am_length(self.as_ptr()) };
if n < 0 {
0
} else {
n as usize
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn null_count(&self) -> usize {
let n = unsafe { ffi::am_null_count(self.as_ptr()) };
if n < 0 {
0
} else {
n as usize
}
}
pub fn format(&self) -> String {
let raw = unsafe { ffi::am_format(self.as_ptr()) };
if raw.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(raw) }.to_string_lossy().into_owned()
}
}
fn require_format<T: NativeType>(&self, what: &str) -> Result<()> {
let got = self.format();
if got == T::FORMAT {
Ok(())
} else {
Err(Error::new(format!(
"{what}: scalar type '{}' does not match the array's element type '{got}'",
T::FORMAT
)))
}
}
pub fn sum(&self) -> Result<Option<Scalar>> {
self.reduce(0, "am_reduce(sum)")
}
pub fn min(&self) -> Result<Option<Scalar>> {
self.reduce(1, "am_reduce(min)")
}
pub fn max(&self) -> Result<Option<Scalar>> {
self.reduce(2, "am_reduce(max)")
}
pub fn mean(&self) -> Result<Option<Scalar>> {
self.reduce(3, "am_reduce(mean)")
}
fn reduce(&self, op: c_int, context: &str) -> Result<Option<Scalar>> {
let mut out_i64: i64 = 0;
let mut out_f64: f64 = 0.0;
let mut kind: c_int = -1;
let mut is_null: c_int = 0;
check(
unsafe {
ffi::am_reduce(
self.as_ptr(),
op,
&mut out_i64,
&mut out_f64,
&mut kind,
&mut is_null,
)
},
context,
)?;
if is_null != 0 {
return Ok(None);
}
Ok(Some(match kind {
0 => Scalar::Int64(out_i64),
1 => Scalar::UInt64(out_i64 as u64),
2 => Scalar::Float64(out_f64),
other => {
return Err(Error::new(format!("{context}: unknown out_kind {other}")));
}
}))
}
pub fn compare_scalar<T: NativeType>(&self, op: CompareOp, scalar: T) -> Result<Array> {
self.require_format::<T>("compare_scalar")?;
let scalar = scalar;
Self::produce("am_compare_scalar", |out| unsafe {
ffi::am_compare_scalar(
self.as_ptr(),
op.code(),
(&scalar as *const T).cast::<c_void>(),
out,
)
})
}
pub fn compare(&self, op: CompareOp, other: &Array) -> Result<Array> {
Self::produce("am_compare_array", |out| unsafe {
ffi::am_compare_array(self.as_ptr(), op.code(), other.as_ptr(), out)
})
}
pub fn cast(&self, format: &str) -> Result<Array> {
let format = cstring(format, "cast format")?;
Self::produce("am_cast", |out| unsafe {
ffi::am_cast(self.as_ptr(), format.as_ptr(), out)
})
}
pub fn filter(&self, mask: &Array) -> Result<Array> {
Self::produce("am_filter", |out| unsafe {
ffi::am_filter(self.as_ptr(), mask.as_ptr(), out)
})
}
pub fn take(&self, indices: &Array) -> Result<Array> {
Self::produce("am_take", |out| unsafe {
ffi::am_take(self.as_ptr(), indices.as_ptr(), out)
})
}
pub fn slice(&self, offset: usize, length: usize) -> Result<Array> {
Self::produce("am_slice", |out| unsafe {
ffi::am_slice(self.as_ptr(), offset as i64, length as i64, out)
})
}
pub fn argsort(&self, descending: bool) -> Result<Array> {
Self::produce("am_argsort", |out| unsafe {
ffi::am_argsort(self.as_ptr(), descending as c_int, out)
})
}
pub fn sort(&self, descending: bool) -> Result<Array> {
Self::produce("am_sort", |out| unsafe {
ffi::am_sort(self.as_ptr(), descending as c_int, out)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Agg {
Sum,
CountAll,
Count,
Mean,
Min,
Max,
}
impl Agg {
fn code(self) -> c_int {
match self {
Agg::Sum => 0,
Agg::CountAll => 1,
Agg::Count => 2,
Agg::Mean => 3,
Agg::Min => 4,
Agg::Max => 5,
}
}
}
pub struct GroupBy {
ptr: NonNull<sys::am_groupby>,
n_keys: usize,
_not_send: PhantomData<*const ()>,
}
impl Drop for GroupBy {
fn drop(&mut self) {
unsafe { ffi::am_group_by_release(self.ptr.as_ptr()) }
}
}
impl fmt::Debug for GroupBy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("arrowmetal::GroupBy")
.field("groups", &self.group_count())
.field("key_columns", &self.n_keys)
.finish()
}
}
pub fn group_by(keys: &[&Array]) -> Result<GroupBy> {
if keys.is_empty() {
return Err(Error::new("group_by: needs at least one key column"));
}
let mut raw: Vec<*mut sys::am_array> = keys.iter().map(|k| k.as_ptr()).collect();
let mut out: *mut sys::am_groupby = std::ptr::null_mut();
check(
unsafe { ffi::am_group_by_keys(raw.as_mut_ptr(), raw.len() as i64, &mut out) },
"am_group_by_keys",
)?;
match NonNull::new(out) {
Some(ptr) => Ok(GroupBy { ptr, n_keys: keys.len(), _not_send: PhantomData }),
None => Err(Error::new("am_group_by_keys: succeeded but returned a null handle")),
}
}
impl GroupBy {
pub fn group_count(&self) -> usize {
let n = unsafe { ffi::am_group_by_group_count(self.ptr.as_ptr()) };
if n < 0 {
0
} else {
n as usize
}
}
pub fn key_column_count(&self) -> usize {
self.n_keys
}
pub fn keys(&self, i: usize) -> Result<Array> {
if i >= self.n_keys {
return Err(Error::new(format!(
"keys({i}): this grouping has {} key columns",
self.n_keys
)));
}
Array::produce("am_group_by_keys_result", |out| unsafe {
ffi::am_group_by_keys_result(self.ptr.as_ptr(), i as i64, out)
})
}
pub fn ids(&self) -> Result<Array> {
Array::produce("am_group_by_ids", |out| unsafe {
ffi::am_group_by_ids(self.ptr.as_ptr(), out)
})
}
pub fn agg(&self, op: Agg, values: Option<&Array>) -> Result<Array> {
if values.is_none() && op != Agg::CountAll {
return Err(Error::new(format!("{op:?}: needs a values column")));
}
self.agg_raw(op.code(), values, 0.0)
}
pub fn sum(&self, values: &Array) -> Result<Array> {
self.agg(Agg::Sum, Some(values))
}
pub fn agg_raw(&self, op: c_int, values: Option<&Array>, p1: f64) -> Result<Array> {
let values = values.map_or(std::ptr::null_mut(), |v| v.as_ptr());
Array::produce("am_group_agg_ex", |out| unsafe {
ffi::am_group_agg_ex(self.ptr.as_ptr(), values, op, p1, out)
})
}
}
pub struct Source {
ptr: NonNull<sys::am_plan_source>,
_columns: Vec<Array>,
_not_send: PhantomData<*const ()>,
}
impl Drop for Source {
fn drop(&mut self) {
unsafe { ffi::am_plan_source_release(self.ptr.as_ptr()) }
}
}
impl fmt::Debug for Source {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("arrowmetal::Source").field("columns", &self._columns.len()).finish()
}
}
impl Source {
pub fn new(name: &str, columns: Vec<(String, Array)>) -> Result<Self> {
if columns.is_empty() {
return Err(Error::new("Source::new: needs at least one column"));
}
let c_name = cstring(name, "source name")?;
let c_col_names: Vec<CString> = columns
.iter()
.map(|(n, _)| cstring(n, "column name"))
.collect::<Result<_>>()?;
let mut name_ptrs: Vec<*const c_char> = c_col_names.iter().map(|n| n.as_ptr()).collect();
let mut col_ptrs: Vec<*mut sys::am_array> = columns.iter().map(|(_, a)| a.as_ptr()).collect();
let mut out: *mut sys::am_plan_source = std::ptr::null_mut();
check(
unsafe {
ffi::am_plan_source_create(
c_name.as_ptr(),
col_ptrs.as_mut_ptr(),
name_ptrs.as_mut_ptr(),
col_ptrs.len() as i64,
&mut out,
)
},
"am_plan_source_create",
)?;
match NonNull::new(out) {
Some(ptr) => Ok(Source {
ptr,
_columns: columns.into_iter().map(|(_, a)| a).collect(),
_not_send: PhantomData,
}),
None => Err(Error::new("am_plan_source_create: returned a null handle")),
}
}
}
pub struct PlanResult {
ptr: NonNull<sys::am_plan_result>,
_not_send: PhantomData<*const ()>,
}
impl Drop for PlanResult {
fn drop(&mut self) {
unsafe { ffi::am_plan_result_release(self.ptr.as_ptr()) }
}
}
impl fmt::Debug for PlanResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("arrowmetal::PlanResult")
.field("columns", &self.column_count())
.field("rows", &self.row_count())
.finish()
}
}
impl PlanResult {
pub fn column_count(&self) -> usize {
let n = unsafe { ffi::am_plan_column_count(self.ptr.as_ptr()) };
if n < 0 {
0
} else {
n as usize
}
}
pub fn row_count(&self) -> usize {
let n = unsafe { ffi::am_plan_row_count(self.ptr.as_ptr()) };
if n < 0 {
0
} else {
n as usize
}
}
pub fn column_name(&self, i: usize) -> Result<String> {
if i >= self.column_count() {
return Err(Error::new(format!(
"column_name({i}): the result has {} columns",
self.column_count()
)));
}
let raw = unsafe { ffi::am_plan_column_name(self.ptr.as_ptr(), i as i64) };
if raw.is_null() {
Err(Error::new(format!("am_plan_column_name({i}) returned null")))
} else {
Ok(unsafe { CStr::from_ptr(raw) }.to_string_lossy().into_owned())
}
}
pub fn column(&self, i: usize) -> Result<Array> {
Array::produce("am_plan_column", |out| unsafe {
ffi::am_plan_column(self.ptr.as_ptr(), i as i64, out)
})
}
}
pub fn run_plan(plan_json: &str, sources: &[&Source], optimize: bool) -> Result<PlanResult> {
let plan = cstring(plan_json, "plan JSON")?;
let mut raw: Vec<*mut sys::am_plan_source> = sources.iter().map(|s| s.ptr.as_ptr()).collect();
let mut out: *mut sys::am_plan_result = std::ptr::null_mut();
check(
unsafe {
ffi::am_plan_run(
plan.as_ptr(),
raw.as_mut_ptr(),
raw.len() as i64,
optimize as c_int,
&mut out,
)
},
"am_plan_run",
)?;
match NonNull::new(out) {
Some(ptr) => Ok(PlanResult { ptr, _not_send: PhantomData }),
None => Err(Error::new("am_plan_run: succeeded but returned a null handle")),
}
}
pub fn explain_plan(plan_json: &str, sources: &[&Source], optimize: bool) -> Result<String> {
let plan = cstring(plan_json, "plan JSON")?;
let mut raw: Vec<*mut sys::am_plan_source> = sources.iter().map(|s| s.ptr.as_ptr()).collect();
let text = unsafe {
ffi::am_plan_explain(
plan.as_ptr(),
raw.as_mut_ptr(),
raw.len() as i64,
optimize as c_int,
)
};
if text.is_null() {
Err(Error::last("am_plan_explain"))
} else {
Ok(unsafe { CStr::from_ptr(text) }.to_string_lossy().into_owned())
}
}
thread_local! {
static BATCH_DEPTH: Cell<u32> = const { Cell::new(0) };
}
pub fn batch<T>(body: impl FnOnce() -> T) -> Result<T> {
let outermost = BATCH_DEPTH.with(|d| d.get() == 0);
if outermost {
check(unsafe { ffi::am_batch_begin() }, "am_batch_begin")?;
}
BATCH_DEPTH.with(|d| d.set(d.get() + 1));
struct Guard<'a> {
failure: &'a RefCell<Option<Error>>,
outermost: bool,
}
impl Drop for Guard<'_> {
fn drop(&mut self) {
BATCH_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
if !self.outermost {
return;
}
let rc = unsafe { ffi::am_batch_end() };
if rc != 0 {
*self.failure.borrow_mut() = Some(Error::last("am_batch_end"));
}
}
}
let failure: RefCell<Option<Error>> = RefCell::new(None);
let value = {
let _guard = Guard { failure: &failure, outermost };
body()
};
match failure.into_inner() {
Some(e) => Err(e),
None => Ok(value),
}
}