use std::sync::Arc;
use arrow_array::{ArrayRef, Float64Array, Int64Array, RecordBatch};
use arrow_schema::{DataType, Schema, SchemaRef};
use datafusion::error::DataFusionError;
use datafusion::execution::SendableRecordBatchStream;
use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
use uni_plugin::QName;
use uni_plugin::errors::FnError;
use uni_plugin::traits::algorithm::{AlgorithmContext, AlgorithmProvider, AlgorithmSignature};
use uni_plugin_builtin::algorithms::bridge::{
AlgorithmHostBridge, ProjectionPlan, await_projections, build_projections,
};
use uni_plugin_builtin::algorithms::graph_compute::handle::Handle;
use uni_plugin_builtin::algorithms::graph_compute::{
AlgoSession, Arena, DEFAULT_ARENA_MAX_HANDLES, SharedRegistry, WorkBudget, next_session_epoch,
};
use crate::adapter_common::{acquire, sanitize_qname};
use crate::pool::ExtismInstancePool;
#[must_use]
pub(crate) fn algo_invoke_export_name(qname: &QName) -> String {
format!("algo_{}_invoke", sanitize_qname(qname))
}
pub struct ExtismAlgorithm {
pool: Arc<ExtismInstancePool<extism::Plugin>>,
registry: SharedRegistry,
qname: QName,
invoke_export: String,
signature: AlgorithmSignature,
}
impl std::fmt::Debug for ExtismAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExtismAlgorithm")
.field("qname", &self.qname)
.finish_non_exhaustive()
}
}
impl ExtismAlgorithm {
#[must_use]
pub fn new(
pool: Arc<ExtismInstancePool<extism::Plugin>>,
registry: SharedRegistry,
qname: QName,
signature: AlgorithmSignature,
) -> Self {
let invoke_export = algo_invoke_export_name(&qname);
Self {
pool,
registry,
qname,
invoke_export,
signature,
}
}
}
fn to_i64(h: Handle) -> i64 {
#[expect(
clippy::cast_possible_wrap,
reason = "opaque handle round-trips bit-exact"
)]
let v = h.as_u64() as i64;
v
}
impl AlgorithmProvider for ExtismAlgorithm {
fn signature(&self) -> &AlgorithmSignature {
&self.signature
}
fn run(&self, ctx: AlgorithmContext<'_>) -> Result<SendableRecordBatchStream, FnError> {
let host = ctx
.host
.ok_or_else(|| FnError::new(0x800, "extism algorithm: host unbound"))?;
let bridge = host
.as_any()
.downcast_ref::<AlgorithmHostBridge>()
.ok_or_else(|| FnError::new(0x801, "extism algorithm: host is not the bridge"))?;
let mut json_args: Vec<serde_json::Value> = serde_json::from_str(ctx.config_json)
.map_err(|e| FnError::new(0x802, format!("extism algorithm: bad config json: {e}")))?;
let plan = ProjectionPlan::take_from_args(&mut json_args)?;
let projections = build_projections(bridge, &plan);
let (work_cap, arena_bytes) = bridge.graph_compute_caps();
let out_schema: SchemaRef = Arc::new(Schema::new(self.signature.output_fields.clone()));
let schema_for_batch = Arc::clone(&out_schema);
let registry = Arc::clone(&self.registry);
let pool = Arc::clone(&self.pool);
let invoke_export = self.invoke_export.clone();
let qname_str = self.qname.to_string();
let expected_cols = uni_plugin_builtin::algorithms::graph_compute::guest_emit_columns(
&self.signature.output_fields,
);
let stream = futures::stream::once(async move {
let bound = await_projections(projections)
.await
.map_err(|e| DataFusionError::Execution(format!("extism algorithm: {e}")))?;
let graph = Arc::clone(&bound.primary);
let budget = WorkBudget::resolve(work_cap, bound.total_vertices(), bound.total_edges());
let mut session = AlgoSession::new(
next_session_epoch()
.map_err(|e| DataFusionError::Execution(format!("extism algorithm: {e}")))?,
budget,
Arena::new(arena_bytes, DEFAULT_ARENA_MAX_HANDLES),
)
.with_expected_columns(expected_cols);
let g = to_i64(session.bind_graph(Arc::clone(&graph)));
let graphs: serde_json::Map<String, serde_json::Value> = bound
.named
.iter()
.map(|(name, proj)| {
(
name.clone(),
serde_json::Value::from(to_i64(session.bind_graph(Arc::clone(proj)))),
)
})
.collect();
let sid = registry.open(session);
let started = std::time::Instant::now();
let call_result: Result<(), DataFusionError> = (|| {
let input = serde_json::to_vec(&serde_json::json!({
"session": sid, "graph": g, "args": json_args, "graphs": graphs,
}))
.map_err(|e| DataFusionError::Execution(format!("extism algorithm input: {e}")))?;
let mut leased = acquire(&pool)
.map_err(|e| DataFusionError::Execution(format!("extism acquire: {e}")))?;
leased
.get_mut()
.call::<&[u8], &[u8]>(&invoke_export, &input)
.map(|_| ())
.map_err(|e| {
DataFusionError::Execution(format!("extism call `{invoke_export}`: {e}"))
})
})();
let closed = registry.close(sid);
if let Err(orig) = call_result {
let (spent, budget, crumbs) = closed.as_ref().map_or_else(
|| (0, 0, Vec::new()),
|s| {
(
s.work_spent_units(),
s.work_budget_units(),
s.trace_breadcrumbs(),
)
},
);
return Err(
uni_plugin_builtin::algorithms::graph_compute::error::incomplete_tag_after_guest(
&qname_str,
false,
spent,
budget,
started.elapsed().as_millis() as u64,
&crumbs,
)
.map_or(orig, DataFusionError::Execution),
);
}
let mut closed =
closed.ok_or_else(|| DataFusionError::Execution("session vanished".into()))?;
let emitted = closed
.finish_emitted()
.map_err(|e| DataFusionError::Execution(format!("extism algorithm emit: {e}")))?;
build_batch(&schema_for_batch, &graph, &emitted)
.map_err(|e| DataFusionError::Execution(format!("extism algorithm emit: {e}")))
});
Ok(Box::pin(RecordBatchStreamAdapter::new(out_schema, stream)))
}
}
fn build_batch(
schema: &SchemaRef,
graph: &uni_algo::algo::GraphProjection,
emitted: &[(String, Vec<f64>)],
) -> Result<RecordBatch, FnError> {
let n = graph.vertex_count();
let mut columns: Vec<ArrayRef> = Vec::with_capacity(schema.fields().len());
for field in schema.fields() {
if field.name() == "nodeId" {
#[expect(
clippy::cast_possible_wrap,
reason = "vids fit i64 in practice; Cypher integers are i64"
)]
let ids: Vec<i64> = (0..n as u32)
.map(|slot| graph.to_vid(slot).as_u64() as i64)
.collect();
columns.push(Arc::new(Int64Array::from(ids)));
continue;
}
let (_, values) = emitted
.iter()
.find(|(name, _)| name == field.name())
.ok_or_else(|| {
FnError::new(
0x869,
format!("guest did not emit declared column `{}`", field.name()),
)
})?;
match field.data_type() {
DataType::Float64 => columns.push(Arc::new(Float64Array::from(values.clone()))),
#[expect(
clippy::cast_possible_truncation,
reason = "int columns hold whole f64 values"
)]
DataType::Int64 => {
let ints: Vec<i64> = values.iter().map(|&v| v as i64).collect();
columns.push(Arc::new(Int64Array::from(ints)));
}
other => {
return Err(FnError::new(
0x862,
format!("unsupported emit column type {other:?}"),
));
}
}
}
RecordBatch::try_new(Arc::clone(schema), columns)
.map_err(|e| FnError::new(0x15, format!("extism algorithm batch: {e}")))
}