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#[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}