Skip to main content

radixdb_plugin_host/
registry.rs

1use std::{collections::BTreeMap, sync::Arc};
2
3use radixdb_plugin_abi::{
4    RadixAbiBatchFnV1, RadixAbiCodecFnV1, RadixAbiCompareFnV1, RadixAbiEqualFnV1, RadixAbiHashFnV1,
5    RadixAbiKeyEncodeFnV1, RadixAbiParseFnV1, RadixAbiPlannerSupportFnV1, RadixAbiScalarFnV1,
6};
7use semver::Version;
8
9pub type ObjectId = [u8; 16];
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub enum ObjectKind {
13    ExternalType,
14    Function,
15    Operator,
16    OperatorClass,
17    PlannerSupport,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub enum RegisteredTypeRef {
22    Builtin(u16),
23    External {
24        object_id: ObjectId,
25        codec_version: u32,
26    },
27}
28
29#[derive(Clone)]
30pub struct RegisteredExternalType {
31    pub package_id: ObjectId,
32    pub object_id: ObjectId,
33    pub local_id: String,
34    pub display_name: String,
35    pub codec_version: u32,
36    pub semantic_revision: u32,
37    pub storage_kind: u16,
38    pub fixed_bytes: u32,
39    pub max_bytes: u32,
40    pub capabilities: u64,
41    pub codec_fingerprint: [u8; 32],
42    pub encode: RadixAbiCodecFnV1,
43    pub decode: RadixAbiParseFnV1,
44    pub equality: Option<RadixAbiEqualFnV1>,
45    pub hash: Option<RadixAbiHashFnV1>,
46    pub ordering: Option<RadixAbiCompareFnV1>,
47    pub text_input: Option<RadixAbiParseFnV1>,
48    pub text_output: Option<RadixAbiCodecFnV1>,
49    pub binary_input: Option<RadixAbiParseFnV1>,
50    pub binary_output: Option<RadixAbiCodecFnV1>,
51}
52
53#[derive(Clone)]
54pub struct RegisteredFunction {
55    pub package_id: ObjectId,
56    pub object_id: ObjectId,
57    pub local_id: String,
58    pub display_name: String,
59    pub semantic_revision: u32,
60    pub arguments: Vec<RegisteredTypeRef>,
61    pub result: RegisteredTypeRef,
62    pub volatility: u16,
63    pub cancellation: u16,
64    pub strict: bool,
65    pub parallel_safe: bool,
66    pub cost: u32,
67    pub max_output_bytes: u32,
68    pub scalar: RadixAbiScalarFnV1,
69    pub batch: Option<RadixAbiBatchFnV1>,
70}
71
72#[derive(Clone)]
73pub struct RegisteredOperator {
74    pub package_id: ObjectId,
75    pub object_id: ObjectId,
76    pub local_id: String,
77    pub symbol: String,
78    pub semantic_revision: u32,
79    pub left: Option<RegisteredTypeRef>,
80    pub right: RegisteredTypeRef,
81    pub result: RegisteredTypeRef,
82    pub function_id: ObjectId,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct RegisteredBinding {
87    pub slot: u16,
88    pub object_id: ObjectId,
89}
90
91#[derive(Clone)]
92pub struct RegisteredOperatorClass {
93    pub package_id: ObjectId,
94    pub object_id: ObjectId,
95    pub local_id: String,
96    pub semantic_revision: u32,
97    pub access_method: u16,
98    pub input_type: RegisteredTypeRef,
99    pub key_type: RegisteredTypeRef,
100    pub key_codec_revision: u32,
101    pub strategies: Vec<RegisteredBinding>,
102    pub supports: Vec<RegisteredBinding>,
103    pub fingerprint: [u8; 32],
104    pub encode_key: RadixAbiKeyEncodeFnV1,
105}
106
107#[derive(Clone)]
108pub struct RegisteredPlannerSupport {
109    pub package_id: ObjectId,
110    pub object_id: ObjectId,
111    pub local_id: String,
112    pub semantic_revision: u32,
113    pub max_spans: u32,
114    pub max_output_bytes: u32,
115    pub recheck_policy: u16,
116    pub target_function_id: Option<ObjectId>,
117    pub target_operator_class_id: Option<ObjectId>,
118    pub fingerprint: [u8; 32],
119    pub callback: RadixAbiPlannerSupportFnV1,
120}
121
122#[derive(Debug, Clone)]
123pub struct RegisteredPackage {
124    pub package_id: ObjectId,
125    pub name: String,
126    pub version: Version,
127    pub abi_major: u16,
128    pub abi_min_minor: u16,
129    pub abi_max_minor: u16,
130    pub abi_minor: u16,
131    pub descriptor_fingerprint: [u8; 32],
132}
133
134#[cfg(any(test, feature = "test-hooks"))]
135impl RegisteredPackage {
136    #[doc(hidden)]
137    pub fn for_test(
138        package_id: ObjectId,
139        name: impl Into<String>,
140        version: &str,
141        descriptor_fingerprint: [u8; 32],
142    ) -> Self {
143        Self {
144            package_id,
145            name: name.into(),
146            version: Version::parse(version).expect("test package version must be valid SemVer"),
147            abi_major: radixdb_plugin_abi::RADIX_ABI_MAJOR,
148            abi_min_minor: radixdb_plugin_abi::RADIX_ABI_MINOR,
149            abi_max_minor: radixdb_plugin_abi::RADIX_ABI_MINOR,
150            abi_minor: radixdb_plugin_abi::RADIX_ABI_MINOR,
151            descriptor_fingerprint,
152        }
153    }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub struct PluginRegistryStatus {
158    pub generation: u64,
159    pub packages: usize,
160    pub external_types: usize,
161    pub functions: usize,
162    pub operators: usize,
163    pub operator_classes: usize,
164    pub planner_support: usize,
165    pub shadowed_versions: usize,
166    pub loaded_library_bytes: u64,
167}
168
169/// A complete generation. All maps are built before this value is returned;
170/// consumers receive it through `Arc` and cannot mutate its contents.
171#[derive(Clone)]
172pub struct PluginRegistry {
173    pub(crate) generation: u64,
174    pub(crate) packages: BTreeMap<ObjectId, Arc<RegisteredPackage>>,
175    pub(crate) external_types: BTreeMap<ObjectId, Arc<RegisteredExternalType>>,
176    pub(crate) functions: BTreeMap<ObjectId, Arc<RegisteredFunction>>,
177    pub(crate) operators: BTreeMap<ObjectId, Arc<RegisteredOperator>>,
178    pub(crate) operator_classes: BTreeMap<ObjectId, Arc<RegisteredOperatorClass>>,
179    pub(crate) planner_support: BTreeMap<ObjectId, Arc<RegisteredPlannerSupport>>,
180    pub(crate) shadowed_versions: usize,
181    pub(crate) loaded_library_bytes: u64,
182}
183
184impl std::fmt::Debug for PluginRegistry {
185    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        formatter
187            .debug_struct("PluginRegistry")
188            .field("status", &self.status())
189            .finish_non_exhaustive()
190    }
191}
192
193impl Default for PluginRegistry {
194    fn default() -> Self {
195        Self::empty()
196    }
197}
198
199impl PluginRegistry {
200    pub fn empty() -> Self {
201        Self {
202            generation: 0,
203            packages: BTreeMap::new(),
204            external_types: BTreeMap::new(),
205            functions: BTreeMap::new(),
206            operators: BTreeMap::new(),
207            operator_classes: BTreeMap::new(),
208            planner_support: BTreeMap::new(),
209            shadowed_versions: 0,
210            loaded_library_bytes: 0,
211        }
212    }
213
214    #[cfg(any(test, feature = "test-hooks"))]
215    #[doc(hidden)]
216    pub fn from_test_packages(packages: impl IntoIterator<Item = RegisteredPackage>) -> Self {
217        let packages = packages
218            .into_iter()
219            .map(|package| (package.package_id, Arc::new(package)))
220            .collect();
221        Self {
222            generation: 1,
223            packages,
224            external_types: BTreeMap::new(),
225            functions: BTreeMap::new(),
226            operators: BTreeMap::new(),
227            operator_classes: BTreeMap::new(),
228            planner_support: BTreeMap::new(),
229            shadowed_versions: 0,
230            loaded_library_bytes: 0,
231        }
232    }
233
234    #[cfg(any(test, feature = "test-hooks"))]
235    #[doc(hidden)]
236    #[allow(clippy::too_many_arguments)]
237    pub fn from_test_objects(
238        packages: impl IntoIterator<Item = RegisteredPackage>,
239        external_types: impl IntoIterator<Item = RegisteredExternalType>,
240        functions: impl IntoIterator<Item = RegisteredFunction>,
241        operators: impl IntoIterator<Item = RegisteredOperator>,
242        operator_classes: impl IntoIterator<Item = RegisteredOperatorClass>,
243        planner_support: impl IntoIterator<Item = RegisteredPlannerSupport>,
244    ) -> Self {
245        Self {
246            generation: 1,
247            packages: packages
248                .into_iter()
249                .map(|value| (value.package_id, Arc::new(value)))
250                .collect(),
251            external_types: external_types
252                .into_iter()
253                .map(|value| (value.object_id, Arc::new(value)))
254                .collect(),
255            functions: functions
256                .into_iter()
257                .map(|value| (value.object_id, Arc::new(value)))
258                .collect(),
259            operators: operators
260                .into_iter()
261                .map(|value| (value.object_id, Arc::new(value)))
262                .collect(),
263            operator_classes: operator_classes
264                .into_iter()
265                .map(|value| (value.object_id, Arc::new(value)))
266                .collect(),
267            planner_support: planner_support
268                .into_iter()
269                .map(|value| (value.object_id, Arc::new(value)))
270                .collect(),
271            shadowed_versions: 0,
272            loaded_library_bytes: 0,
273        }
274    }
275
276    pub fn generation(&self) -> u64 {
277        self.generation
278    }
279
280    pub fn package(&self, id: &ObjectId) -> Option<&Arc<RegisteredPackage>> {
281        self.packages.get(id)
282    }
283
284    pub fn package_by_name_and_version(
285        &self,
286        name: &str,
287        version: &str,
288    ) -> Option<&Arc<RegisteredPackage>> {
289        self.packages
290            .values()
291            .find(|package| package.name == name && package.version.to_string() == version)
292    }
293
294    pub fn external_type(&self, id: &ObjectId) -> Option<&Arc<RegisteredExternalType>> {
295        self.external_types.get(id)
296    }
297
298    pub fn external_type_by_package_and_local_id(
299        &self,
300        package_id: &ObjectId,
301        local_id: &str,
302    ) -> Option<&Arc<RegisteredExternalType>> {
303        self.external_types.values().find(|external_type| {
304            &external_type.package_id == package_id && external_type.local_id == local_id
305        })
306    }
307
308    pub fn function(&self, id: &ObjectId) -> Option<&Arc<RegisteredFunction>> {
309        self.functions.get(id)
310    }
311
312    pub fn function_by_package_and_local_id(
313        &self,
314        package_id: &ObjectId,
315        local_id: &str,
316    ) -> Option<&Arc<RegisteredFunction>> {
317        self.functions
318            .values()
319            .find(|function| &function.package_id == package_id && function.local_id == local_id)
320    }
321
322    pub fn operator(&self, id: &ObjectId) -> Option<&Arc<RegisteredOperator>> {
323        self.operators.get(id)
324    }
325
326    pub fn operator_by_package_and_local_id(
327        &self,
328        package_id: &ObjectId,
329        local_id: &str,
330    ) -> Option<&Arc<RegisteredOperator>> {
331        self.operators
332            .values()
333            .find(|value| &value.package_id == package_id && value.local_id == local_id)
334    }
335
336    pub fn operator_class(&self, id: &ObjectId) -> Option<&Arc<RegisteredOperatorClass>> {
337        self.operator_classes.get(id)
338    }
339
340    pub fn operator_class_by_package_and_local_id(
341        &self,
342        package_id: &ObjectId,
343        local_id: &str,
344    ) -> Option<&Arc<RegisteredOperatorClass>> {
345        self.operator_classes
346            .values()
347            .find(|value| &value.package_id == package_id && value.local_id == local_id)
348    }
349
350    pub fn planner_support(&self, id: &ObjectId) -> Option<&Arc<RegisteredPlannerSupport>> {
351        self.planner_support.get(id)
352    }
353
354    pub fn planner_support_by_package_and_local_id(
355        &self,
356        package_id: &ObjectId,
357        local_id: &str,
358    ) -> Option<&Arc<RegisteredPlannerSupport>> {
359        self.planner_support
360            .values()
361            .find(|value| &value.package_id == package_id && value.local_id == local_id)
362    }
363
364    pub fn status(&self) -> PluginRegistryStatus {
365        PluginRegistryStatus {
366            generation: self.generation,
367            packages: self.packages.len(),
368            external_types: self.external_types.len(),
369            functions: self.functions.len(),
370            operators: self.operators.len(),
371            operator_classes: self.operator_classes.len(),
372            planner_support: self.planner_support.len(),
373            shadowed_versions: self.shadowed_versions,
374            loaded_library_bytes: self.loaded_library_bytes,
375        }
376    }
377
378    pub fn assess_requirements(
379        &self,
380        requirements: &[PackageRequirement],
381    ) -> DatabasePluginAdmission {
382        let mut issues = Vec::new();
383        for requirement in requirements {
384            let Some(package) = self.packages.get(&requirement.package_id) else {
385                issues.push(RequirementIssue::MissingPackage {
386                    package_id: requirement.package_id,
387                });
388                continue;
389            };
390            if package.version != requirement.version {
391                issues.push(RequirementIssue::PackageVersion {
392                    package_id: requirement.package_id,
393                    required: requirement.version.clone(),
394                    active: package.version.clone(),
395                });
396            }
397            if package.abi_major != requirement.abi_major
398                || package.abi_min_minor != requirement.abi_min_minor
399                || package.abi_max_minor != requirement.abi_max_minor
400            {
401                issues.push(RequirementIssue::PackageAbi {
402                    package_id: requirement.package_id,
403                    required_major: requirement.abi_major,
404                    required_min_minor: requirement.abi_min_minor,
405                    required_max_minor: requirement.abi_max_minor,
406                    active_major: package.abi_major,
407                    active_min_minor: package.abi_min_minor,
408                    active_max_minor: package.abi_max_minor,
409                });
410            }
411            if package.descriptor_fingerprint != requirement.descriptor_fingerprint {
412                issues.push(RequirementIssue::DescriptorFingerprint {
413                    package_id: requirement.package_id,
414                });
415            }
416            for object in &requirement.objects {
417                let present = match object.kind {
418                    ObjectKind::ExternalType => self
419                        .external_types
420                        .get(&object.object_id)
421                        .is_some_and(|value| {
422                            object
423                                .codec_version
424                                .is_none_or(|version| value.codec_version == version)
425                                && object
426                                    .semantic_revision
427                                    .is_none_or(|revision| value.semantic_revision == revision)
428                        }),
429                    ObjectKind::Function => {
430                        self.functions.get(&object.object_id).is_some_and(|v| {
431                            object
432                                .semantic_revision
433                                .is_none_or(|revision| v.semantic_revision == revision)
434                        })
435                    }
436                    ObjectKind::Operator => {
437                        self.operators.get(&object.object_id).is_some_and(|v| {
438                            object
439                                .semantic_revision
440                                .is_none_or(|revision| v.semantic_revision == revision)
441                        })
442                    }
443                    ObjectKind::OperatorClass => self
444                        .operator_classes
445                        .get(&object.object_id)
446                        .is_some_and(|v| {
447                            object
448                                .codec_version
449                                .is_none_or(|revision| v.key_codec_revision == revision)
450                                && object
451                                    .semantic_revision
452                                    .is_none_or(|revision| v.semantic_revision == revision)
453                        }),
454                    ObjectKind::PlannerSupport => self
455                        .planner_support
456                        .get(&object.object_id)
457                        .is_some_and(|v| {
458                            object
459                                .semantic_revision
460                                .is_none_or(|revision| v.semantic_revision == revision)
461                        }),
462                };
463                if !present {
464                    issues.push(RequirementIssue::MissingOrStaleObject {
465                        package_id: requirement.package_id,
466                        object_id: object.object_id,
467                        kind: object.kind,
468                    });
469                }
470            }
471        }
472        if issues.is_empty() {
473            DatabasePluginAdmission::Normal
474        } else {
475            DatabasePluginAdmission::Restricted { issues }
476        }
477    }
478}
479
480#[derive(Debug, Clone, PartialEq, Eq)]
481pub struct PackageRequirement {
482    pub package_id: ObjectId,
483    pub version: Version,
484    pub abi_major: u16,
485    pub abi_min_minor: u16,
486    pub abi_max_minor: u16,
487    pub descriptor_fingerprint: [u8; 32],
488    pub objects: Vec<ObjectRequirement>,
489}
490
491impl PackageRequirement {
492    #[allow(clippy::too_many_arguments)]
493    pub fn for_package_binding(
494        package_id: ObjectId,
495        version: &str,
496        abi_major: u16,
497        abi_min_minor: u16,
498        abi_max_minor: u16,
499        descriptor_fingerprint: [u8; 32],
500    ) -> Result<Self, String> {
501        let parsed = Version::parse(version).map_err(|error| error.to_string())?;
502        if parsed.to_string() != version || !parsed.build.is_empty() {
503            return Err(
504                "package binding version is not canonical SemVer without build metadata".to_owned(),
505            );
506        }
507        Ok(Self {
508            package_id,
509            version: parsed,
510            abi_major,
511            abi_min_minor,
512            abi_max_minor,
513            descriptor_fingerprint,
514            objects: Vec::new(),
515        })
516    }
517}
518
519#[derive(Debug, Clone, PartialEq, Eq)]
520pub struct ObjectRequirement {
521    pub object_id: ObjectId,
522    pub kind: ObjectKind,
523    pub codec_version: Option<u32>,
524    pub semantic_revision: Option<u32>,
525}
526
527#[derive(Debug, Clone, PartialEq, Eq)]
528pub enum RequirementIssue {
529    MissingPackage {
530        package_id: ObjectId,
531    },
532    PackageVersion {
533        package_id: ObjectId,
534        required: Version,
535        active: Version,
536    },
537    PackageAbi {
538        package_id: ObjectId,
539        required_major: u16,
540        required_min_minor: u16,
541        required_max_minor: u16,
542        active_major: u16,
543        active_min_minor: u16,
544        active_max_minor: u16,
545    },
546    DescriptorFingerprint {
547        package_id: ObjectId,
548    },
549    MissingOrStaleObject {
550        package_id: ObjectId,
551        object_id: ObjectId,
552        kind: ObjectKind,
553    },
554}
555
556#[derive(Debug, Clone, PartialEq, Eq)]
557pub enum DatabasePluginAdmission {
558    Normal,
559    Restricted { issues: Vec<RequirementIssue> },
560}
561
562pub fn derive_object_id(package_id: ObjectId, local_id: &str) -> Result<ObjectId, &'static str> {
563    radixdb_core::derive_plugin_object_identity_bytes(package_id, local_id)
564}
565
566#[cfg(test)]
567mod tests {
568    use super::*;
569
570    #[test]
571    fn identity_is_stable_and_sql_name_independent() {
572        let package = [7; 16];
573        assert_eq!(
574            derive_object_id(package, "point").unwrap(),
575            derive_object_id(package, "point").unwrap()
576        );
577        assert_ne!(
578            derive_object_id(package, "point").unwrap(),
579            derive_object_id(package, "point_v2").unwrap()
580        );
581    }
582
583    #[test]
584    fn empty_registry_is_compatibility_default() {
585        let registry = PluginRegistry::empty();
586        assert_eq!(registry.status().generation, 0);
587        assert_eq!(registry.status().packages, 0);
588        assert_eq!(
589            registry.assess_requirements(&[]),
590            DatabasePluginAdmission::Normal
591        );
592    }
593}