#![cfg(feature = "rhai-runtime")]
use std::sync::Arc;
use arrow_array::{Array, Float64Array, Int64Array, StringArray, builder::Float64Builder};
use parking_lot::Mutex;
use rhai::{Engine, EvalAltResult, Position};
macro_rules! immutable_column {
($(#[$meta:meta])* $name:ident, $array:ty, $to_dynamic:expr) => {
$(#[$meta])*
#[derive(Clone, Debug)]
pub struct $name {
inner: Arc<$array>,
}
impl $name {
#[must_use]
pub fn new(arr: Arc<$array>) -> Self {
Self { inner: arr }
}
#[must_use]
pub fn len(&mut self) -> i64 {
self.inner.len() as i64
}
#[must_use]
pub fn is_empty(&mut self) -> bool {
self.inner.is_empty()
}
pub fn get(&mut self, i: i64) -> rhai::Dynamic {
let idx = i as usize;
if idx >= self.inner.len() || self.inner.is_null(idx) {
return rhai::Dynamic::UNIT;
}
let cell = self.inner.value(idx);
$to_dynamic(cell)
}
}
};
}
immutable_column!(
Float64Column,
Float64Array,
rhai::Dynamic::from
);
immutable_column!(
Int64Column,
Int64Array,
rhai::Dynamic::from
);
immutable_column!(
Utf8Column,
StringArray,
|v: &str| rhai::Dynamic::from(v.to_owned())
);
#[derive(Clone, Debug)]
pub struct MutableFloat64Column {
values: Arc<Mutex<Vec<Option<f64>>>>,
}
impl MutableFloat64Column {
#[must_use]
pub fn with_capacity(n: i64) -> Self {
let n = n.max(0) as usize;
Self {
values: Arc::new(Mutex::new(vec![None; n])),
}
}
#[must_use]
pub fn len(&mut self) -> i64 {
self.values.lock().len() as i64
}
#[must_use]
pub fn is_empty(&mut self) -> bool {
self.values.lock().is_empty()
}
pub fn set(&mut self, i: i64, v: f64) -> Result<(), Box<EvalAltResult>> {
let idx = i as usize;
let mut values = self.values.lock();
if idx >= values.len() {
return Err(Box::new(EvalAltResult::ErrorIndexNotFound(
rhai::Dynamic::from(i),
Position::NONE,
)));
}
values[idx] = Some(v);
Ok(())
}
pub fn get(&mut self, i: i64) -> rhai::Dynamic {
let idx = i as usize;
let values = self.values.lock();
match values.get(idx).and_then(|cell| cell.as_ref()) {
Some(&v) => rhai::Dynamic::from(v),
None => rhai::Dynamic::UNIT,
}
}
#[must_use]
pub fn freeze(self) -> Arc<Float64Array> {
let mut values = self.values.lock();
let mut builder = Float64Builder::with_capacity(values.len());
for cell in values.drain(..) {
match cell {
Some(v) => builder.append_value(v),
None => builder.append_null(),
}
}
Arc::new(builder.finish())
}
}
pub fn register_column_types(engine: &mut Engine) {
engine
.register_type_with_name::<Float64Column>("Float64Column")
.register_fn("len", Float64Column::len)
.register_indexer_get(Float64Column::get);
engine
.register_type_with_name::<Int64Column>("Int64Column")
.register_fn("len", Int64Column::len)
.register_indexer_get(Int64Column::get);
engine
.register_type_with_name::<Utf8Column>("Utf8Column")
.register_fn("len", Utf8Column::len)
.register_indexer_get(Utf8Column::get);
engine
.register_type_with_name::<MutableFloat64Column>("MutableFloat64Column")
.register_fn("len", MutableFloat64Column::len)
.register_indexer_get(MutableFloat64Column::get)
.register_indexer_set(MutableFloat64Column::set);
engine.register_fn("uni_float_column", MutableFloat64Column::with_capacity);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::build_engine;
use crate::host_fns::RhaiHostFnRegistry;
use uni_plugin::CapabilitySet;
fn engine_with_columns() -> Engine {
let mut e = build_engine(&CapabilitySet::new(), &RhaiHostFnRegistry::new());
register_column_types(&mut e);
e
}
#[test]
fn float_column_indexer_round_trips() {
let arr = Arc::new(Float64Array::from(vec![Some(1.5), None, Some(3.0)]));
let col = Float64Column::new(arr);
let e = engine_with_columns();
let mut scope = rhai::Scope::new();
scope.push("col", col);
let v0: f64 = e.eval_with_scope(&mut scope, "col[0]").unwrap();
assert_eq!(v0, 1.5);
let v2: f64 = e.eval_with_scope(&mut scope, "col[2]").unwrap();
assert_eq!(v2, 3.0);
let v1: rhai::Dynamic = e.eval_with_scope(&mut scope, "col[1]").unwrap();
assert!(v1.is_unit());
}
#[test]
fn mutable_float_column_round_trips() {
let e = engine_with_columns();
let script = r#"
let out = uni_float_column(3);
out[0] = 1.5;
out[1] = 2.5;
out[2] = 3.5;
out
"#;
let out: MutableFloat64Column = e.eval(script).unwrap();
let arr = out.freeze();
assert_eq!(arr.value(0), 1.5);
assert_eq!(arr.value(1), 2.5);
assert_eq!(arr.value(2), 3.5);
}
#[test]
fn unset_slots_remain_null_after_freeze() {
let e = engine_with_columns();
let script = r#"
let out = uni_float_column(4);
out[0] = 10.0;
out[2] = 30.0;
out
"#;
let out: MutableFloat64Column = e.eval(script).unwrap();
let arr = out.freeze();
assert!(!arr.is_null(0));
assert_eq!(arr.value(0), 10.0);
assert!(arr.is_null(1), "unset slot must remain null");
assert!(!arr.is_null(2));
assert_eq!(arr.value(2), 30.0);
assert!(arr.is_null(3), "unset slot must remain null");
}
#[test]
fn set_out_of_range_returns_index_error() {
let e = engine_with_columns();
let script = r#"
let out = uni_float_column(2);
out[5] = 99.0;
out
"#;
let res = e.eval::<MutableFloat64Column>(script);
assert!(res.is_err(), "expected index error");
}
}