Skip to main content

radixdb_plugin/
call.rs

1use std::{marker::PhantomData, panic::AssertUnwindSafe};
2
3use radixdb_plugin_abi::{
4    RadixAbiBatchViewV1, RadixAbiCallContextV1, RadixAbiColumnViewV1, RadixAbiDiagnosticV1,
5    RadixAbiHashSinkV1, RadixAbiHeaderV1, RadixAbiResultBuilderV1, RadixAbiSliceV1,
6    RadixAbiStatusV1, RadixAbiValueV1, RADIX_COLUMN_LAYOUT_FIXED, RADIX_DIAGNOSTIC_CANCELLED,
7    RADIX_DIAGNOSTIC_DOMAIN, RADIX_DIAGNOSTIC_INTERNAL, RADIX_DIAGNOSTIC_INVALID_INPUT,
8    RADIX_DIAGNOSTIC_LIMIT, RADIX_DIAGNOSTIC_PLUGIN_PANIC, RADIX_HASH_COMPONENT_BYTES,
9    RADIX_HASH_COMPONENT_F64_BITS, RADIX_HASH_COMPONENT_I64, RADIX_HASH_COMPONENT_U64,
10    RADIX_RESULT_ITEM_FLAG_NULL, RADIX_STATUS_CANCELLED, RADIX_STATUS_CONTRACT_VIOLATION,
11    RADIX_STATUS_DOMAIN_ERROR, RADIX_STATUS_INTERNAL_ERROR, RADIX_STATUS_INVALID_ARGUMENT,
12    RADIX_STATUS_LIMIT_EXCEEDED, RADIX_STATUS_OK, RADIX_STATUS_PANIC, RADIX_TYPE_REF_EXTERNAL,
13};
14
15use crate::{value::AbiValue, PluginError, PluginErrorKind, PluginResult, RadixType, ValueType};
16
17pub struct CallContext<'a> {
18    raw: &'a RadixAbiCallContextV1,
19}
20
21impl CallContext<'_> {
22    pub fn check_cancelled(&self) -> PluginResult<()> {
23        let callback = self
24            .raw
25            .check_cancelled
26            .ok_or_else(|| PluginError::internal("missing cancellation callback"))?;
27        // SAFETY: callback and handle are supplied by the host for this call.
28        match unsafe { callback(self.raw.handle) } {
29            RADIX_STATUS_OK => Ok(()),
30            RADIX_STATUS_CANCELLED => Err(PluginError::cancelled()),
31            _ => Err(PluginError::internal(
32                "host cancellation callback violated its contract",
33            )),
34        }
35    }
36
37    pub fn charge_work(&self, units: u32) -> PluginResult<()> {
38        let callback = self
39            .raw
40            .charge_work
41            .ok_or_else(|| PluginError::internal("missing work-accounting callback"))?;
42        // SAFETY: callback and handle are supplied by the host for this call.
43        match unsafe { callback(self.raw.handle, units) } {
44            RADIX_STATUS_OK => Ok(()),
45            RADIX_STATUS_LIMIT_EXCEEDED => {
46                Err(PluginError::limit_exceeded("plugin work budget exhausted"))
47            }
48            RADIX_STATUS_CANCELLED => Err(PluginError::cancelled()),
49            _ => Err(PluginError::internal(
50                "host work callback violated its contract",
51            )),
52        }
53    }
54
55    pub fn deadline_unix_ns(&self) -> u64 {
56        self.raw.deadline_unix_ns
57    }
58
59    pub fn max_output_bytes(&self) -> u32 {
60        self.raw.max_output_bytes
61    }
62}
63
64pub struct HashSink<'a> {
65    backend: HashSinkBackend<'a>,
66}
67
68enum HashSinkBackend<'a> {
69    Abi(&'a RadixAbiHashSinkV1),
70    Test(&'a mut Vec<(u16, Vec<u8>)>),
71}
72
73impl HashSink<'_> {
74    pub(crate) fn for_testing(components: &mut Vec<(u16, Vec<u8>)>) -> HashSink<'_> {
75        HashSink {
76            backend: HashSinkBackend::Test(components),
77        }
78    }
79
80    pub fn bytes(&mut self, value: &[u8]) -> PluginResult<()> {
81        self.append(RADIX_HASH_COMPONENT_BYTES, value)
82    }
83
84    pub fn i64(&mut self, value: i64) -> PluginResult<()> {
85        self.append(RADIX_HASH_COMPONENT_I64, &value.to_le_bytes())
86    }
87
88    pub fn u64(&mut self, value: u64) -> PluginResult<()> {
89        self.append(RADIX_HASH_COMPONENT_U64, &value.to_le_bytes())
90    }
91
92    pub fn f64_bits(&mut self, value: f64) -> PluginResult<()> {
93        self.append(
94            RADIX_HASH_COMPONENT_F64_BITS,
95            &value.to_bits().to_le_bytes(),
96        )
97    }
98
99    fn append(&mut self, kind: u16, bytes: &[u8]) -> PluginResult<()> {
100        if bytes.len() > u32::MAX as usize {
101            return Err(PluginError::limit_exceeded("hash component is too large"));
102        }
103        match &mut self.backend {
104            HashSinkBackend::Abi(raw) => {
105                let callback = raw
106                    .append
107                    .ok_or_else(|| PluginError::internal("missing hash sink callback"))?;
108                // SAFETY: the slice remains live for the synchronous host callback.
109                let status = unsafe {
110                    callback(
111                        raw.handle,
112                        kind,
113                        0,
114                        RadixAbiSliceV1 {
115                            ptr: bytes.as_ptr(),
116                            len: bytes.len() as u32,
117                            reserved: 0,
118                        },
119                    )
120                };
121                status_result(status, "hash sink rejected component")
122            }
123            HashSinkBackend::Test(components) => {
124                if components.len() >= radixdb_plugin_abi::RADIX_MAX_HASH_COMPONENTS as usize
125                    || components
126                        .iter()
127                        .map(|(_, value)| value.len())
128                        .sum::<usize>()
129                        + bytes.len()
130                        > radixdb_plugin_abi::RADIX_MAX_HASH_BYTES as usize
131                {
132                    return Err(PluginError::limit_exceeded(
133                        "semantic hash components exceed SDK test bounds",
134                    ));
135                }
136                components.push((kind, bytes.to_vec()));
137                Ok(())
138            }
139        }
140    }
141}
142
143pub struct ResultBuilder<'a> {
144    raw: &'a RadixAbiResultBuilderV1,
145    items: u32,
146    bytes: u32,
147    finished: bool,
148}
149
150impl ResultBuilder<'_> {
151    pub fn push<T: ValueType>(&mut self, value: T) -> PluginResult<()> {
152        if value.is_null() {
153            return self.push_null();
154        }
155        let bytes = value.encode_abi()?;
156        self.write(0, &bytes)
157    }
158
159    pub fn push_null(&mut self) -> PluginResult<()> {
160        self.write(RADIX_RESULT_ITEM_FLAG_NULL, &[])
161    }
162
163    fn write(&mut self, flags: u32, bytes: &[u8]) -> PluginResult<()> {
164        if self.finished {
165            return Err(PluginError::internal("write after result finish"));
166        }
167        let next_items = self
168            .items
169            .checked_add(1)
170            .ok_or_else(|| PluginError::limit_exceeded("result item count overflow"))?;
171        let next_bytes = self
172            .bytes
173            .checked_add(bytes.len() as u32)
174            .ok_or_else(|| PluginError::limit_exceeded("result byte count overflow"))?;
175        if next_items > self.raw.max_items || next_bytes > self.raw.max_bytes {
176            return Err(PluginError::limit_exceeded(
177                "result exceeds host-owned builder bounds",
178            ));
179        }
180        let callback = self
181            .raw
182            .write
183            .ok_or_else(|| PluginError::internal("missing result writer"))?;
184        // SAFETY: bytes live through the synchronous host-owned copy.
185        let status = unsafe {
186            callback(
187                self.raw.handle,
188                flags,
189                0,
190                RadixAbiSliceV1 {
191                    ptr: bytes.as_ptr(),
192                    len: bytes.len() as u32,
193                    reserved: 0,
194                },
195            )
196        };
197        status_result(status, "result builder rejected output")?;
198        self.items = next_items;
199        self.bytes = next_bytes;
200        Ok(())
201    }
202
203    fn finish(&mut self) -> PluginResult<()> {
204        if self.finished {
205            return Err(PluginError::internal("result builder finished twice"));
206        }
207        let callback = self
208            .raw
209            .finish
210            .ok_or_else(|| PluginError::internal("missing result finisher"))?;
211        // SAFETY: callback and handle are host-owned for this call.
212        let status = unsafe { callback(self.raw.handle) };
213        status_result(status, "result builder finish failed")?;
214        self.finished = true;
215        Ok(())
216    }
217}
218
219pub struct ColumnBuilder<'borrow, 'host, T> {
220    output: &'borrow mut ResultBuilder<'host>,
221    marker: PhantomData<T>,
222}
223
224impl<T: ValueType> ColumnBuilder<'_, '_, T> {
225    pub fn push(&mut self, value: T) -> PluginResult<()> {
226        self.output.push(value)
227    }
228
229    pub fn push_null(&mut self) -> PluginResult<()> {
230        self.output.push_null()
231    }
232}
233
234#[derive(Clone, Copy)]
235pub struct ColumnView<'a, T> {
236    raw: &'a RadixAbiColumnViewV1,
237    row: u32,
238    marker: PhantomData<T>,
239}
240
241impl<'a, T: ValueType> Iterator for ColumnView<'a, T> {
242    type Item = PluginResult<T>;
243
244    fn next(&mut self) -> Option<Self::Item> {
245        if self.row >= self.raw.row_count {
246            return None;
247        }
248        let row = self.row;
249        self.row += 1;
250        Some(decode_column_value::<T>(self.raw, row))
251    }
252}
253
254#[derive(Debug, Clone, Copy)]
255pub struct PredicateView<'a> {
256    bytes: &'a [u8],
257}
258
259impl<'a> PredicateView<'a> {
260    pub fn as_bytes(&self) -> &'a [u8] {
261        self.bytes
262    }
263
264    pub fn target_function_id(&self) -> PluginResult<[u8; 16]> {
265        self.validate_header()?;
266        Ok(self.bytes[4..20].try_into().expect("validated fixed width"))
267    }
268
269    pub fn operator_class_id(&self) -> PluginResult<[u8; 16]> {
270        self.validate_header()?;
271        Ok(self.bytes[20..36]
272            .try_into()
273            .expect("validated fixed width"))
274    }
275
276    pub fn indexed_argument(&self) -> PluginResult<u16> {
277        self.validate_header()?;
278        Ok(u16::from_le_bytes(
279            self.bytes[36..38]
280                .try_into()
281                .expect("validated fixed width"),
282        ))
283    }
284
285    pub fn argument_count(&self) -> PluginResult<u16> {
286        self.validate_header()?;
287        Ok(u16::from_le_bytes(
288            self.bytes[38..40]
289                .try_into()
290                .expect("validated fixed width"),
291        ))
292    }
293
294    /// Decode one normalized constant. `None` is SQL NULL; the indexed-column
295    /// marker is not a constant and is rejected.
296    pub fn constant<T: ValueType>(&self, index: u16) -> PluginResult<Option<T>> {
297        let argument = self.argument(index)?;
298        if argument.kind != 2 {
299            return Err(PluginError::invalid_input(
300                "requested normalized argument is the indexed column",
301            ));
302        }
303        if argument.is_null {
304            return Ok(None);
305        }
306        let mut inline_bytes = [0; 16];
307        let variable = argument.type_ref.kind == RADIX_TYPE_REF_EXTERNAL
308            || matches!(
309                argument.type_ref.builtin_tag,
310                radixdb_plugin_abi::RADIX_BUILTIN_TEXT
311                    | radixdb_plugin_abi::RADIX_BUILTIN_JSON
312                    | radixdb_plugin_abi::RADIX_BUILTIN_VECTOR
313                    | radixdb_plugin_abi::RADIX_BUILTIN_DECIMAL
314                    | radixdb_plugin_abi::RADIX_BUILTIN_BYTES
315            );
316        let borrowed_bytes = if variable {
317            RadixAbiSliceV1 {
318                ptr: argument.bytes.as_ptr(),
319                len: argument.bytes.len() as u32,
320                reserved: 0,
321            }
322        } else {
323            if argument.bytes.len() > inline_bytes.len() {
324                return Err(PluginError::invalid_input(
325                    "normalized fixed value exceeds ABI width",
326                ));
327            }
328            inline_bytes[..argument.bytes.len()].copy_from_slice(argument.bytes);
329            RadixAbiSliceV1::EMPTY
330        };
331        let raw = RadixAbiValueV1 {
332            type_ref: argument.type_ref,
333            flags: 0,
334            reserved: 0,
335            inline_bytes,
336            borrowed_bytes,
337        };
338        // SAFETY: raw only borrows this immutable predicate for the decode.
339        let value = unsafe { AbiValue::new(&raw)? };
340        T::decode_abi(&value).map(Some)
341    }
342
343    fn validate_header(&self) -> PluginResult<()> {
344        if self.bytes.len() < 40 || self.bytes[..4] != *b"RPN1" {
345            return Err(PluginError::invalid_input(
346                "invalid normalized predicate header",
347            ));
348        }
349        Ok(())
350    }
351
352    fn argument(&self, requested: u16) -> PluginResult<NormalizedArgument<'a>> {
353        self.validate_header()?;
354        let count = self.argument_count()?;
355        if requested >= count {
356            return Err(PluginError::invalid_input(
357                "normalized predicate argument is out of bounds",
358            ));
359        }
360        let mut offset = 40usize;
361        for index in 0..count {
362            let header_end = offset
363                .checked_add(32)
364                .ok_or_else(|| PluginError::invalid_input("predicate length overflow"))?;
365            if header_end > self.bytes.len() {
366                return Err(PluginError::invalid_input(
367                    "truncated normalized predicate argument",
368                ));
369            }
370            let kind = self.bytes[offset];
371            let flags = self.bytes[offset + 1];
372            if !matches!(kind, 1 | 2)
373                || flags & !1 != 0
374                || self.bytes[offset + 2..offset + 4] != [0, 0]
375            {
376                return Err(PluginError::invalid_input(
377                    "invalid normalized predicate argument header",
378                ));
379            }
380            let type_ref = radixdb_plugin_abi::RadixAbiTypeRefV1 {
381                kind: u16::from_le_bytes(
382                    self.bytes[offset + 4..offset + 6]
383                        .try_into()
384                        .expect("fixed width"),
385                ),
386                builtin_tag: u16::from_le_bytes(
387                    self.bytes[offset + 6..offset + 8]
388                        .try_into()
389                        .expect("fixed width"),
390                ),
391                object_id: self.bytes[offset + 8..offset + 24]
392                    .try_into()
393                    .expect("fixed width"),
394                codec_version: u32::from_le_bytes(
395                    self.bytes[offset + 24..offset + 28]
396                        .try_into()
397                        .expect("fixed width"),
398                ),
399            };
400            abi_contract(radixdb_plugin_abi::validate_type_ref(&type_ref))?;
401            let len = u32::from_le_bytes(
402                self.bytes[offset + 28..header_end]
403                    .try_into()
404                    .expect("fixed width"),
405            ) as usize;
406            let end = header_end
407                .checked_add(len)
408                .ok_or_else(|| PluginError::invalid_input("predicate length overflow"))?;
409            if end > self.bytes.len()
410                || (kind == 1 && (len != 0 || flags != 0))
411                || (flags & 1 != 0 && len != 0)
412            {
413                return Err(PluginError::invalid_input(
414                    "invalid normalized predicate argument payload",
415                ));
416            }
417            if index == requested {
418                return Ok(NormalizedArgument {
419                    kind,
420                    is_null: flags & 1 != 0,
421                    type_ref,
422                    bytes: &self.bytes[header_end..end],
423                });
424            }
425            offset = end;
426        }
427        Err(PluginError::invalid_input(
428            "normalized predicate argument is missing",
429        ))
430    }
431}
432
433struct NormalizedArgument<'a> {
434    kind: u8,
435    is_null: bool,
436    type_ref: radixdb_plugin_abi::RadixAbiTypeRefV1,
437    bytes: &'a [u8],
438}
439
440#[derive(Debug, Clone, PartialEq, Eq)]
441pub struct CandidateSpan {
442    pub start: Vec<u8>,
443    pub end: Vec<u8>,
444}
445
446pub struct CandidatePlanBuilder<'borrow, 'host> {
447    output: &'borrow mut ResultBuilder<'host>,
448    context: &'borrow CallContext<'host>,
449}
450
451impl CandidatePlanBuilder<'_, '_> {
452    pub fn set_estimate(&mut self, estimated_rows: u64, cost_hint: u32) -> PluginResult<()> {
453        self.context.check_cancelled()?;
454        self.context.charge_work(1)?;
455        let mut bytes = Vec::with_capacity(20);
456        bytes.extend_from_slice(&[2, 0, 0, 0]);
457        bytes.extend_from_slice(&estimated_rows.to_le_bytes());
458        bytes.extend_from_slice(&cost_hint.to_le_bytes());
459        bytes.extend_from_slice(&0_u32.to_le_bytes());
460        self.output.write(0, &bytes)
461    }
462
463    pub fn push_span(&mut self, span: CandidateSpan) -> PluginResult<()> {
464        self.context.check_cancelled()?;
465        self.context.charge_work(1)?;
466        let start_len = u32::try_from(span.start.len())
467            .map_err(|_| PluginError::limit_exceeded("candidate start key is too large"))?;
468        let end_len = u32::try_from(span.end.len())
469            .map_err(|_| PluginError::limit_exceeded("candidate end key is too large"))?;
470        let mut bytes = Vec::with_capacity(12 + span.start.len() + span.end.len());
471        bytes.extend_from_slice(&[1, 0, 0, 0]);
472        bytes.extend_from_slice(&start_len.to_le_bytes());
473        bytes.extend_from_slice(&end_len.to_le_bytes());
474        bytes.extend_from_slice(&span.start);
475        bytes.extend_from_slice(&span.end);
476        self.output.write(0, &bytes)
477    }
478}
479
480fn decode_column_value<T: ValueType>(column: &RadixAbiColumnViewV1, row: u32) -> PluginResult<T> {
481    let is_null = if column.null_bitmap.len == 0 {
482        false
483    } else {
484        // SAFETY: validated host batch memory remains borrowed for this callback.
485        let bitmap = unsafe {
486            std::slice::from_raw_parts(column.null_bitmap.ptr, column.null_bitmap.len as usize)
487        };
488        bitmap[row as usize / 8] & (1 << (row % 8)) != 0
489    };
490    let (inline_bytes, borrowed_bytes) = if column.layout == RADIX_COLUMN_LAYOUT_FIXED {
491        let start = row as usize * column.stride as usize;
492        let width = column.element_width as usize;
493        // SAFETY: batch validation proves row_count * stride and readable data.
494        let data = unsafe { std::slice::from_raw_parts(column.data.ptr, column.data.len as usize) };
495        if column.type_ref.kind == RADIX_TYPE_REF_EXTERNAL {
496            ([0; 16], &data[start..start + width])
497        } else {
498            let mut inline = [0; 16];
499            if width > inline.len() {
500                return Err(PluginError::invalid_input(
501                    "fixed built-in column element exceeds inline ABI width",
502                ));
503            }
504            inline[..width].copy_from_slice(&data[start..start + width]);
505            (inline, &[][..])
506        }
507    } else {
508        // SAFETY: batch validation proves row_count + 1 aligned offsets.
509        let offsets =
510            unsafe { std::slice::from_raw_parts(column.offsets.ptr, column.offsets.len as usize) };
511        // SAFETY: batch validation proves terminal offset is within data.
512        let data = unsafe { std::slice::from_raw_parts(column.data.ptr, column.data.len as usize) };
513        let start = offsets[row as usize] as usize;
514        let end = offsets[row as usize + 1] as usize;
515        ([0; 16], &data[start..end])
516    };
517    let raw = RadixAbiValueV1 {
518        type_ref: column.type_ref,
519        flags: if is_null { 1 } else { 0 },
520        reserved: 0,
521        inline_bytes,
522        borrowed_bytes: RadixAbiSliceV1 {
523            ptr: borrowed_bytes.as_ptr(),
524            len: borrowed_bytes.len() as u32,
525            reserved: 0,
526        },
527    };
528    // SAFETY: raw references local inline data or the validated column range.
529    let value = unsafe { AbiValue::new(&raw)? };
530    T::decode_abi(&value)
531}
532
533pub(crate) fn status_result(status: u32, detail: &'static str) -> PluginResult<()> {
534    match status {
535        RADIX_STATUS_OK => Ok(()),
536        RADIX_STATUS_INVALID_ARGUMENT | RADIX_STATUS_CONTRACT_VIOLATION => {
537            Err(PluginError::invalid_input(detail))
538        }
539        RADIX_STATUS_DOMAIN_ERROR => Err(PluginError::domain(detail)),
540        RADIX_STATUS_LIMIT_EXCEEDED => Err(PluginError::limit_exceeded(detail)),
541        RADIX_STATUS_CANCELLED => Err(PluginError::cancelled()),
542        _ => Err(PluginError::internal(detail)),
543    }
544}
545
546fn abi_contract<T>(
547    result: Result<T, radixdb_plugin_abi::RadixAbiValidationError>,
548) -> PluginResult<T> {
549    result.map_err(|error| PluginError::invalid_input(format!("ABI contract violation: {error:?}")))
550}
551
552fn error_status(error: &PluginError) -> RadixAbiStatusV1 {
553    match error.kind() {
554        PluginErrorKind::InvalidInput => RADIX_STATUS_INVALID_ARGUMENT,
555        PluginErrorKind::Domain => RADIX_STATUS_DOMAIN_ERROR,
556        PluginErrorKind::LimitExceeded => RADIX_STATUS_LIMIT_EXCEEDED,
557        PluginErrorKind::Cancelled => RADIX_STATUS_CANCELLED,
558        PluginErrorKind::Internal => RADIX_STATUS_INTERNAL_ERROR,
559    }
560}
561
562fn report_error(context: Option<&RadixAbiCallContextV1>, error: &PluginError, panic: bool) {
563    let Some(context) = context else {
564        return;
565    };
566    // SAFETY: a non-null diagnostic sink is part of the validated call context.
567    let Some(sink) = (unsafe { context.diagnostics.as_ref() }) else {
568        return;
569    };
570    let Some(write) = sink.write else {
571        return;
572    };
573    let detail = error.detail().as_bytes();
574    let field = error.field().unwrap_or_default().as_bytes();
575    let category = if panic {
576        RADIX_DIAGNOSTIC_PLUGIN_PANIC
577    } else {
578        match error.kind() {
579            PluginErrorKind::InvalidInput => RADIX_DIAGNOSTIC_INVALID_INPUT,
580            PluginErrorKind::Domain => RADIX_DIAGNOSTIC_DOMAIN,
581            PluginErrorKind::LimitExceeded => RADIX_DIAGNOSTIC_LIMIT,
582            PluginErrorKind::Cancelled => RADIX_DIAGNOSTIC_CANCELLED,
583            PluginErrorKind::Internal => RADIX_DIAGNOSTIC_INTERNAL,
584        }
585    };
586    let diagnostic = RadixAbiDiagnosticV1 {
587        header: RadixAbiHeaderV1::new::<RadixAbiDiagnosticV1>(0),
588        category,
589        status: if panic {
590            RADIX_STATUS_PANIC
591        } else {
592            error_status(error)
593        },
594        detail: RadixAbiSliceV1 {
595            ptr: detail.as_ptr(),
596            len: detail.len() as u32,
597            reserved: 0,
598        },
599        field: RadixAbiSliceV1 {
600            ptr: field.as_ptr(),
601            len: field.len() as u32,
602            reserved: 0,
603        },
604    };
605    // SAFETY: diagnostic and its slices remain live for the synchronous write.
606    let _ = unsafe { write(sink.handle, &diagnostic) };
607}
608
609#[doc(hidden)]
610pub unsafe fn run_scalar<F>(
611    context: *const RadixAbiCallContextV1,
612    arguments: *const RadixAbiValueV1,
613    argument_count: u32,
614    output: *const RadixAbiResultBuilderV1,
615    expected_arguments: u32,
616    strict: bool,
617    callback: F,
618) -> RadixAbiStatusV1
619where
620    F: FnOnce(&CallContext<'_>, &[AbiValue<'_>], &mut ResultBuilder<'_>) -> PluginResult<()>,
621{
622    let context_ref = unsafe { context.as_ref() };
623    let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| -> PluginResult<()> {
624        let context = CallContext {
625            raw: context_ref.ok_or_else(|| PluginError::invalid_input("null call context"))?,
626        };
627        abi_contract(radixdb_plugin_abi::validate_call_context(context.raw))?;
628        let raw_output = unsafe { output.as_ref() }
629            .ok_or_else(|| PluginError::invalid_input("null result builder"))?;
630        abi_contract(radixdb_plugin_abi::validate_result_builder(raw_output))?;
631        if raw_output.max_bytes > context.max_output_bytes() {
632            return Err(PluginError::invalid_input(
633                "result builder exceeds call output budget",
634            ));
635        }
636        if argument_count != expected_arguments || (argument_count != 0 && arguments.is_null()) {
637            return Err(PluginError::invalid_input("scalar argument count mismatch"));
638        }
639        let raw_arguments = if argument_count == 0 {
640            &[]
641        } else {
642            // SAFETY: host validates the call-scoped argument table.
643            unsafe { std::slice::from_raw_parts(arguments, argument_count as usize) }
644        };
645        let values = raw_arguments
646            .iter()
647            .map(|raw| {
648                abi_contract(unsafe { radixdb_plugin_abi::validate_value_contents(raw) })?;
649                unsafe { AbiValue::new(raw) }
650            })
651            .collect::<PluginResult<Vec<_>>>()?;
652        let mut builder = ResultBuilder {
653            raw: raw_output,
654            items: 0,
655            bytes: 0,
656            finished: false,
657        };
658        if strict && values.iter().any(AbiValue::is_null) {
659            builder.push_null()?;
660        } else {
661            callback(&context, &values, &mut builder)?;
662        }
663        if builder.items != 1 {
664            return Err(PluginError::internal(
665                "scalar callback must emit exactly one item",
666            ));
667        }
668        builder.finish()
669    }));
670    finish_outcome(context_ref, outcome)
671}
672
673#[doc(hidden)]
674pub unsafe fn run_batch<F>(
675    context: *const RadixAbiCallContextV1,
676    input: *const RadixAbiBatchViewV1,
677    output: *const RadixAbiResultBuilderV1,
678    expected_columns: u32,
679    callback: F,
680) -> RadixAbiStatusV1
681where
682    F: FnOnce(&CallContext<'_>, &BatchInput<'_>, &mut ResultBuilder<'_>) -> PluginResult<()>,
683{
684    let context_ref = unsafe { context.as_ref() };
685    let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| -> PluginResult<()> {
686        let context = CallContext {
687            raw: context_ref.ok_or_else(|| PluginError::invalid_input("null call context"))?,
688        };
689        abi_contract(radixdb_plugin_abi::validate_call_context(context.raw))?;
690        let batch = unsafe { input.as_ref() }
691            .ok_or_else(|| PluginError::invalid_input("null batch input"))?;
692        abi_contract(unsafe { radixdb_plugin_abi::validate_batch_columns(batch) })?;
693        if batch.column_count != expected_columns
694            || (batch.column_count != 0 && batch.columns.is_null())
695        {
696            return Err(PluginError::invalid_input("batch column count mismatch"));
697        }
698        let columns = if batch.column_count == 0 {
699            &[]
700        } else {
701            // SAFETY: the host validates and owns the call-scoped table.
702            unsafe { std::slice::from_raw_parts(batch.columns, batch.column_count as usize) }
703        };
704        if columns
705            .iter()
706            .any(|column| column.row_count != batch.row_count)
707        {
708            return Err(PluginError::invalid_input("batch row count mismatch"));
709        }
710        let raw_output = unsafe { output.as_ref() }
711            .ok_or_else(|| PluginError::invalid_input("null result builder"))?;
712        abi_contract(radixdb_plugin_abi::validate_result_builder(raw_output))?;
713        if raw_output.max_bytes > context.max_output_bytes() {
714            return Err(PluginError::invalid_input(
715                "result builder exceeds call output budget",
716            ));
717        }
718        let input = BatchInput {
719            rows: batch.row_count,
720            columns,
721        };
722        let mut builder = ResultBuilder {
723            raw: raw_output,
724            items: 0,
725            bytes: 0,
726            finished: false,
727        };
728        callback(&context, &input, &mut builder)?;
729        if builder.items != batch.row_count {
730            return Err(PluginError::internal(
731                "batch callback output count differs from input rows",
732            ));
733        }
734        builder.finish()
735    }));
736    finish_outcome(context_ref, outcome)
737}
738
739pub struct BatchInput<'a> {
740    rows: u32,
741    columns: &'a [RadixAbiColumnViewV1],
742}
743
744impl<'a> BatchInput<'a> {
745    pub fn row_count(&self) -> u32 {
746        self.rows
747    }
748
749    pub fn column<T: ValueType>(&self, index: usize) -> PluginResult<ColumnView<'a, T>> {
750        let raw = self
751            .columns
752            .get(index)
753            .ok_or_else(|| PluginError::invalid_input("batch column is missing"))?;
754        Ok(ColumnView {
755            raw,
756            row: 0,
757            marker: PhantomData,
758        })
759    }
760}
761
762#[doc(hidden)]
763pub unsafe fn run_codec_encode<T: RadixType>(
764    context: *const RadixAbiCallContextV1,
765    input: *const RadixAbiValueV1,
766    output: *const RadixAbiResultBuilderV1,
767) -> RadixAbiStatusV1 {
768    unsafe {
769        run_scalar(
770            context,
771            input,
772            1,
773            output,
774            1,
775            true,
776            |_context, arguments, output| {
777                let value = T::decode_abi(&arguments[0])?;
778                output.push(value)
779            },
780        )
781    }
782}
783
784#[doc(hidden)]
785pub unsafe fn run_codec_decode<T: RadixType>(
786    context: *const RadixAbiCallContextV1,
787    input: RadixAbiSliceV1,
788    output: *const RadixAbiResultBuilderV1,
789) -> RadixAbiStatusV1 {
790    let context_ref = unsafe { context.as_ref() };
791    let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| -> PluginResult<()> {
792        let context = CallContext {
793            raw: context_ref.ok_or_else(|| PluginError::invalid_input("null call context"))?,
794        };
795        abi_contract(radixdb_plugin_abi::validate_call_context(context.raw))?;
796        if input.reserved != 0 || (input.len != 0 && input.ptr.is_null()) {
797            return Err(PluginError::invalid_input("invalid codec input slice"));
798        }
799        let bytes = if input.len == 0 {
800            &[]
801        } else {
802            // SAFETY: the host owns this call-scoped input range.
803            unsafe { std::slice::from_raw_parts(input.ptr, input.len as usize) }
804        };
805        let mut reader = crate::CodecReader::new(bytes);
806        let value = T::decode(&mut reader)?;
807        reader.finish()?;
808        let raw_output = unsafe { output.as_ref() }
809            .ok_or_else(|| PluginError::invalid_input("null result builder"))?;
810        abi_contract(radixdb_plugin_abi::validate_result_builder(raw_output))?;
811        if raw_output.max_bytes > context.max_output_bytes() {
812            return Err(PluginError::invalid_input(
813                "result builder exceeds call output budget",
814            ));
815        }
816        let mut builder = ResultBuilder {
817            raw: raw_output,
818            items: 0,
819            bytes: 0,
820            finished: false,
821        };
822        builder.push(value)?;
823        builder.finish()
824    }));
825    finish_outcome(context_ref, outcome)
826}
827
828#[doc(hidden)]
829pub unsafe fn run_equal<T: RadixType>(
830    context: *const RadixAbiCallContextV1,
831    left: *const RadixAbiValueV1,
832    right: *const RadixAbiValueV1,
833    output: *mut u8,
834) -> RadixAbiStatusV1 {
835    let callback = |left: T, right: T| {
836        let result = T::semantic_equal(&left, &right)
837            .ok_or_else(|| PluginError::internal("equality capability is not implemented"))?;
838        if output.is_null() {
839            return Err(PluginError::invalid_input("null equality output"));
840        }
841        // SAFETY: host provides a writable one-byte output for this callback.
842        unsafe { output.write(u8::from(result)) };
843        Ok(())
844    };
845    unsafe { run_binary_value(context, left, right, callback) }
846}
847
848#[doc(hidden)]
849pub unsafe fn run_compare<T: RadixType>(
850    context: *const RadixAbiCallContextV1,
851    left: *const RadixAbiValueV1,
852    right: *const RadixAbiValueV1,
853    output: *mut i8,
854) -> RadixAbiStatusV1 {
855    let callback = |left: T, right: T| {
856        let result = T::semantic_compare(&left, &right)
857            .ok_or_else(|| PluginError::internal("ordering capability is not implemented"))?;
858        if output.is_null() {
859            return Err(PluginError::invalid_input("null ordering output"));
860        }
861        let value = match result {
862            std::cmp::Ordering::Less => -1,
863            std::cmp::Ordering::Equal => 0,
864            std::cmp::Ordering::Greater => 1,
865        };
866        // SAFETY: host provides a writable one-byte output for this callback.
867        unsafe { output.write(value) };
868        Ok(())
869    };
870    unsafe { run_binary_value(context, left, right, callback) }
871}
872
873unsafe fn run_binary_value<T: RadixType, F>(
874    context: *const RadixAbiCallContextV1,
875    left: *const RadixAbiValueV1,
876    right: *const RadixAbiValueV1,
877    callback: F,
878) -> RadixAbiStatusV1
879where
880    F: FnOnce(T, T) -> PluginResult<()>,
881{
882    let context_ref = unsafe { context.as_ref() };
883    let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| -> PluginResult<()> {
884        let context = CallContext {
885            raw: context_ref.ok_or_else(|| PluginError::invalid_input("null call context"))?,
886        };
887        abi_contract(radixdb_plugin_abi::validate_call_context(context.raw))?;
888        let left = unsafe { left.as_ref() }
889            .ok_or_else(|| PluginError::invalid_input("null left value"))?;
890        let right = unsafe { right.as_ref() }
891            .ok_or_else(|| PluginError::invalid_input("null right value"))?;
892        abi_contract(unsafe { radixdb_plugin_abi::validate_value_contents(left) })?;
893        abi_contract(unsafe { radixdb_plugin_abi::validate_value_contents(right) })?;
894        let left = T::decode_abi(&unsafe { AbiValue::new(left)? })?;
895        let right = T::decode_abi(&unsafe { AbiValue::new(right)? })?;
896        callback(left, right)
897    }));
898    finish_outcome(context_ref, outcome)
899}
900
901#[doc(hidden)]
902pub unsafe fn run_hash<T: RadixType>(
903    context: *const RadixAbiCallContextV1,
904    value: *const RadixAbiValueV1,
905    sink: *const RadixAbiHashSinkV1,
906) -> RadixAbiStatusV1 {
907    let context_ref = unsafe { context.as_ref() };
908    let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| -> PluginResult<()> {
909        let context = context_ref.ok_or_else(|| PluginError::invalid_input("null call context"))?;
910        abi_contract(radixdb_plugin_abi::validate_call_context(context))?;
911        let value = unsafe { value.as_ref() }
912            .ok_or_else(|| PluginError::invalid_input("null hash value"))?;
913        abi_contract(unsafe { radixdb_plugin_abi::validate_value_contents(value) })?;
914        let value = T::decode_abi(&unsafe { AbiValue::new(value)? })?;
915        let raw_sink =
916            unsafe { sink.as_ref() }.ok_or_else(|| PluginError::invalid_input("null hash sink"))?;
917        abi_contract(radixdb_plugin_abi::validate_hash_sink(raw_sink))?;
918        let mut sink = HashSink {
919            backend: HashSinkBackend::Abi(raw_sink),
920        };
921        T::semantic_hash(&value, &mut sink)
922            .ok_or_else(|| PluginError::internal("hash capability is not implemented"))?
923    }));
924    finish_outcome(context_ref, outcome)
925}
926
927#[doc(hidden)]
928pub unsafe fn run_key_encoder<T: ValueType, K: ValueType>(
929    context: *const RadixAbiCallContextV1,
930    value: *const RadixAbiValueV1,
931    output: *const RadixAbiResultBuilderV1,
932    callback: fn(T) -> PluginResult<K>,
933) -> RadixAbiStatusV1 {
934    unsafe {
935        run_scalar(
936            context,
937            value,
938            1,
939            output,
940            1,
941            true,
942            |_context, arguments, output| output.push(callback(T::decode_abi(&arguments[0])?)?),
943        )
944    }
945}
946
947#[doc(hidden)]
948pub unsafe fn run_planner<F>(
949    context: *const RadixAbiCallContextV1,
950    predicate: RadixAbiSliceV1,
951    output: *const RadixAbiResultBuilderV1,
952    callback: F,
953) -> RadixAbiStatusV1
954where
955    F: FnOnce(PredicateView<'_>, &mut CandidatePlanBuilder<'_, '_>) -> PluginResult<()>,
956{
957    let context_ref = unsafe { context.as_ref() };
958    let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| -> PluginResult<()> {
959        let context = CallContext {
960            raw: context_ref.ok_or_else(|| PluginError::invalid_input("null call context"))?,
961        };
962        abi_contract(radixdb_plugin_abi::validate_call_context(context.raw))?;
963        if predicate.reserved != 0 || (predicate.len != 0 && predicate.ptr.is_null()) {
964            return Err(PluginError::invalid_input("invalid predicate input"));
965        }
966        let bytes = if predicate.len == 0 {
967            &[]
968        } else {
969            // SAFETY: the host owns this call-scoped predicate range.
970            unsafe { std::slice::from_raw_parts(predicate.ptr, predicate.len as usize) }
971        };
972        let raw_output = unsafe { output.as_ref() }
973            .ok_or_else(|| PluginError::invalid_input("null planner output"))?;
974        abi_contract(radixdb_plugin_abi::validate_result_builder(raw_output))?;
975        if raw_output.max_bytes > context.max_output_bytes() {
976            return Err(PluginError::invalid_input(
977                "result builder exceeds call output budget",
978            ));
979        }
980        let mut result = ResultBuilder {
981            raw: raw_output,
982            items: 0,
983            bytes: 0,
984            finished: false,
985        };
986        {
987            let mut builder = CandidatePlanBuilder {
988                output: &mut result,
989                context: &context,
990            };
991            callback(PredicateView { bytes }, &mut builder)?;
992        }
993        result.finish()
994    }));
995    finish_outcome(context_ref, outcome)
996}
997
998fn finish_outcome(
999    context: Option<&RadixAbiCallContextV1>,
1000    outcome: Result<PluginResult<()>, Box<dyn std::any::Any + Send>>,
1001) -> RadixAbiStatusV1 {
1002    match outcome {
1003        Ok(Ok(())) => RADIX_STATUS_OK,
1004        Ok(Err(error)) => {
1005            let status = error_status(&error);
1006            report_error(context, &error, false);
1007            status
1008        }
1009        Err(_) => {
1010            let error = PluginError::internal("plugin callback panicked");
1011            report_error(context, &error, true);
1012            RADIX_STATUS_PANIC
1013        }
1014    }
1015}
1016
1017#[doc(hidden)]
1018pub fn column_builder<'borrow, 'host, T>(
1019    output: &'borrow mut ResultBuilder<'host>,
1020) -> ColumnBuilder<'borrow, 'host, T> {
1021    ColumnBuilder {
1022        output,
1023        marker: PhantomData,
1024    }
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029    use super::*;
1030
1031    fn push_argument(
1032        frame: &mut Vec<u8>,
1033        kind: u8,
1034        flags: u8,
1035        type_ref: radixdb_plugin_abi::RadixAbiTypeRefV1,
1036        value: &[u8],
1037    ) {
1038        frame.extend_from_slice(&[kind, flags, 0, 0]);
1039        frame.extend_from_slice(&type_ref.kind.to_le_bytes());
1040        frame.extend_from_slice(&type_ref.builtin_tag.to_le_bytes());
1041        frame.extend_from_slice(&type_ref.object_id);
1042        frame.extend_from_slice(&type_ref.codec_version.to_le_bytes());
1043        frame.extend_from_slice(&(value.len() as u32).to_le_bytes());
1044        frame.extend_from_slice(value);
1045    }
1046
1047    #[test]
1048    fn normalized_predicate_view_decodes_typed_constants() {
1049        let integer = radixdb_plugin_abi::RadixAbiTypeRefV1::builtin(
1050            radixdb_plugin_abi::RADIX_BUILTIN_INTEGER,
1051        );
1052        let mut frame = Vec::new();
1053        frame.extend_from_slice(b"RPN1");
1054        frame.extend_from_slice(&[1; 16]);
1055        frame.extend_from_slice(&[2; 16]);
1056        frame.extend_from_slice(&0_u16.to_le_bytes());
1057        frame.extend_from_slice(&3_u16.to_le_bytes());
1058        push_argument(&mut frame, 1, 0, integer, &[]);
1059        push_argument(&mut frame, 2, 0, integer, &42_i64.to_le_bytes());
1060        push_argument(&mut frame, 2, 1, integer, &[]);
1061
1062        let predicate = PredicateView { bytes: &frame };
1063        assert_eq!(predicate.target_function_id().unwrap(), [1; 16]);
1064        assert_eq!(predicate.operator_class_id().unwrap(), [2; 16]);
1065        assert_eq!(predicate.indexed_argument().unwrap(), 0);
1066        assert_eq!(predicate.argument_count().unwrap(), 3);
1067        assert!(predicate.constant::<i64>(0).is_err());
1068        assert_eq!(predicate.constant::<i64>(1).unwrap(), Some(42));
1069        assert_eq!(predicate.constant::<i64>(2).unwrap(), None);
1070        assert!(predicate.constant::<i64>(3).is_err());
1071    }
1072
1073    #[test]
1074    fn normalized_predicate_view_rejects_malformed_frames() {
1075        let predicate = PredicateView { bytes: b"bad" };
1076        assert!(predicate.argument_count().is_err());
1077
1078        let mut truncated = Vec::new();
1079        truncated.extend_from_slice(b"RPN1");
1080        truncated.extend_from_slice(&[1; 32]);
1081        truncated.extend_from_slice(&0_u16.to_le_bytes());
1082        truncated.extend_from_slice(&1_u16.to_le_bytes());
1083        let predicate = PredicateView { bytes: &truncated };
1084        assert!(predicate.constant::<i64>(0).is_err());
1085    }
1086}