1use std::sync::Arc;
20
21use arrow_array::{ArrayRef, Float64Array, Int64Array, RecordBatch};
22use arrow_schema::{DataType, Schema, SchemaRef};
23use datafusion::error::DataFusionError;
24use datafusion::execution::SendableRecordBatchStream;
25use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
26use uni_plugin::QName;
27use uni_plugin::errors::FnError;
28use uni_plugin::traits::algorithm::{
29 AlgorithmContext, AlgorithmProvider, AlgorithmSignature, GraphProjectionSpec,
30};
31use uni_plugin_builtin::algorithms::bridge::AlgorithmHostBridge;
32use uni_plugin_builtin::algorithms::graph_compute::handle::Handle;
33use uni_plugin_builtin::algorithms::graph_compute::{
34 AlgoSession, Arena, DEFAULT_ARENA_MAX_HANDLES, SharedRegistry, WorkBudget, next_session_epoch,
35};
36
37use crate::adapter_common::{acquire, sanitize_qname};
38use crate::pool::ExtismInstancePool;
39
40#[must_use]
42pub(crate) fn algo_invoke_export_name(qname: &QName) -> String {
43 format!("algo_{}_invoke", sanitize_qname(qname))
44}
45
46pub struct ExtismAlgorithm {
48 pool: Arc<ExtismInstancePool<extism::Plugin>>,
49 registry: SharedRegistry,
50 qname: QName,
51 invoke_export: String,
52 signature: AlgorithmSignature,
53}
54
55impl std::fmt::Debug for ExtismAlgorithm {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 f.debug_struct("ExtismAlgorithm")
58 .field("qname", &self.qname)
59 .finish_non_exhaustive()
60 }
61}
62
63impl ExtismAlgorithm {
64 #[must_use]
66 pub fn new(
67 pool: Arc<ExtismInstancePool<extism::Plugin>>,
68 registry: SharedRegistry,
69 qname: QName,
70 signature: AlgorithmSignature,
71 ) -> Self {
72 let invoke_export = algo_invoke_export_name(&qname);
73 Self {
74 pool,
75 registry,
76 qname,
77 invoke_export,
78 signature,
79 }
80 }
81}
82
83fn to_i64(h: Handle) -> i64 {
84 #[expect(
85 clippy::cast_possible_wrap,
86 reason = "opaque handle round-trips bit-exact"
87 )]
88 let v = h.as_u64() as i64;
89 v
90}
91
92impl AlgorithmProvider for ExtismAlgorithm {
93 fn signature(&self) -> &AlgorithmSignature {
94 &self.signature
95 }
96
97 fn run(&self, ctx: AlgorithmContext<'_>) -> Result<SendableRecordBatchStream, FnError> {
98 let host = ctx
99 .host
100 .ok_or_else(|| FnError::new(0x800, "extism algorithm: host unbound"))?;
101 let bridge = host
102 .as_any()
103 .downcast_ref::<AlgorithmHostBridge>()
104 .ok_or_else(|| FnError::new(0x801, "extism algorithm: host is not the bridge"))?;
105
106 let json_args: Vec<serde_json::Value> = serde_json::from_str(ctx.config_json)
107 .map_err(|e| FnError::new(0x802, format!("extism algorithm: bad config json: {e}")))?;
108
109 let spec = GraphProjectionSpec {
110 include_reverse: true, ..GraphProjectionSpec::default()
112 };
113 let projection = bridge.project_for_graph_compute(&spec);
114 let (work_cap, arena_bytes) = bridge.graph_compute_caps();
115
116 let out_schema: SchemaRef = Arc::new(Schema::new(self.signature.output_fields.clone()));
117 let schema_for_batch = Arc::clone(&out_schema);
118 let registry = Arc::clone(&self.registry);
119 let pool = Arc::clone(&self.pool);
120 let invoke_export = self.invoke_export.clone();
121 let qname_str = self.qname.to_string();
122 let expected_cols = uni_plugin_builtin::algorithms::graph_compute::guest_emit_columns(
123 &self.signature.output_fields,
124 );
125
126 let stream = futures::stream::once(async move {
127 let graph = projection
128 .await
129 .map_err(|e| DataFusionError::Execution(format!("extism algorithm: {e}")))?;
130
131 let budget = WorkBudget::resolve(
134 work_cap,
135 graph.vertex_count() as u64,
136 graph.edge_count() as u64,
137 );
138 let mut session = AlgoSession::new(
139 next_session_epoch(),
140 budget,
141 Arena::new(arena_bytes, DEFAULT_ARENA_MAX_HANDLES),
142 )
143 .with_expected_columns(expected_cols);
144 let g = to_i64(session.bind_graph(Arc::clone(&graph)));
145 let sid = registry.open(session);
146
147 let started = std::time::Instant::now();
151 let call_result: Result<(), DataFusionError> = (|| {
152 let input = serde_json::to_vec(&serde_json::json!({
153 "session": sid, "graph": g, "args": json_args,
154 }))
155 .map_err(|e| DataFusionError::Execution(format!("extism algorithm input: {e}")))?;
156 let mut leased = acquire(&pool)
157 .map_err(|e| DataFusionError::Execution(format!("extism acquire: {e}")))?;
158 leased
159 .get_mut()
160 .call::<&[u8], &[u8]>(&invoke_export, &input)
161 .map(|_| ())
162 .map_err(|e| {
163 DataFusionError::Execution(format!("extism call `{invoke_export}`: {e}"))
164 })
165 })();
166
167 let closed = registry.close(sid);
169 if let Err(orig) = call_result {
170 let (spent, budget) = closed
174 .as_ref()
175 .map_or((0, 0), |s| (s.work_spent(), s.work_budget()));
176 return Err(
177 uni_plugin_builtin::algorithms::graph_compute::error::incomplete_tag_after_guest(
178 &qname_str,
179 false,
180 spent,
181 budget,
182 started.elapsed().as_millis() as u64,
183 )
184 .map_or(orig, DataFusionError::Execution),
185 );
186 }
187 let mut closed =
188 closed.ok_or_else(|| DataFusionError::Execution("session vanished".into()))?;
189 let emitted = closed.take_emitted();
190
191 build_batch(&schema_for_batch, &graph, &emitted)
192 .map_err(|e| DataFusionError::Execution(format!("extism algorithm emit: {e}")))
193 });
194
195 Ok(Box::pin(RecordBatchStreamAdapter::new(out_schema, stream)))
196 }
197}
198
199fn build_batch(
201 schema: &SchemaRef,
202 graph: &uni_algo::algo::GraphProjection,
203 emitted: &[(String, Vec<f64>)],
204) -> Result<RecordBatch, FnError> {
205 let n = graph.vertex_count();
206 let mut columns: Vec<ArrayRef> = Vec::with_capacity(schema.fields().len());
207 for field in schema.fields() {
208 if field.name() == "nodeId" {
209 #[expect(
210 clippy::cast_possible_wrap,
211 reason = "vids fit i64 in practice; Cypher integers are i64"
212 )]
213 let ids: Vec<i64> = (0..n as u32)
214 .map(|slot| graph.to_vid(slot).as_u64() as i64)
215 .collect();
216 columns.push(Arc::new(Int64Array::from(ids)));
217 continue;
218 }
219 let (_, values) = emitted
220 .iter()
221 .find(|(name, _)| name == field.name())
222 .ok_or_else(|| {
223 FnError::new(
224 0x869,
225 format!("guest did not emit declared column `{}`", field.name()),
226 )
227 })?;
228 match field.data_type() {
229 DataType::Float64 => columns.push(Arc::new(Float64Array::from(values.clone()))),
230 #[expect(
231 clippy::cast_possible_truncation,
232 reason = "int columns hold whole f64 values"
233 )]
234 DataType::Int64 => {
235 let ints: Vec<i64> = values.iter().map(|&v| v as i64).collect();
236 columns.push(Arc::new(Int64Array::from(ints)));
237 }
238 other => {
239 return Err(FnError::new(
240 0x862,
241 format!("unsupported emit column type {other:?}"),
242 ));
243 }
244 }
245 }
246 RecordBatch::try_new(Arc::clone(schema), columns)
247 .map_err(|e| FnError::new(0x15, format!("extism algorithm batch: {e}")))
248}