Skip to main content

uni_plugin/
registry.rs

1//! The [`PluginRegistry`] — per-surface trait-object tables.
2//!
3//! All registrations land here. Reads are wait-free via `arc-swap`; writes
4//! are CAS-style. Hot reload swaps a per-plugin entry; queries holding an
5//! `Arc::clone()` of the old entry continue against the old version until
6//! their reference is dropped.
7
8use std::collections::HashMap;
9use std::sync::Arc;
10
11use arc_swap::ArcSwap;
12use dashmap::DashMap;
13use parking_lot::{Mutex, RwLock};
14use smol_str::SmolStr;
15
16use crate::capability::CapabilitySet;
17use crate::errors::PluginError;
18use crate::plugin::PluginId;
19use crate::qname::QName;
20use crate::traits::aggregate::{AggSignature, AggregatePluginFn};
21use crate::traits::algorithm::AlgorithmProvider;
22use crate::traits::background::BackgroundJobProvider;
23use crate::traits::catalog::{CatalogProvider, ReplacementScanProvider};
24use crate::traits::cdc::CdcOutputProvider;
25use crate::traits::collation::CollationProvider;
26use crate::traits::connector::{AuthProvider, AuthzPolicy};
27use crate::traits::crdt::{CrdtKind, CrdtKindProvider};
28use crate::traits::hook::SessionHook;
29use crate::traits::index::{IndexHandle, IndexKind, IndexKindProvider};
30use crate::traits::locy::{
31    GenSignature, LocyAggregate, LocyGenerator, LocyPredicate, PredSignature,
32};
33use crate::traits::operator::OptimizerRuleProvider;
34use crate::traits::procedure::{ProcedurePlugin, ProcedureSignature};
35use crate::traits::scalar::{FnSignature, ScalarPluginFn};
36use crate::traits::trigger::TriggerPlugin;
37use crate::traits::types::LogicalTypeProvider;
38use crate::traits::window::{WindowPluginFn, WindowSignature};
39
40/// A single scalar-fn registry entry.
41pub struct ScalarEntry {
42    /// Owning plugin id.
43    pub plugin: PluginId,
44    /// Function signature.
45    pub signature: FnSignature,
46    /// The registered function.
47    pub function: Arc<dyn ScalarPluginFn>,
48}
49
50impl std::fmt::Debug for ScalarEntry {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        f.debug_struct("ScalarEntry")
53            .field("plugin", &self.plugin)
54            .field("signature", &self.signature)
55            .finish_non_exhaustive()
56    }
57}
58
59/// A single aggregate-fn registry entry.
60pub struct AggregateEntry {
61    /// Owning plugin id.
62    pub plugin: PluginId,
63    /// Aggregate signature.
64    pub signature: AggSignature,
65    /// The registered aggregate.
66    pub aggregate: Arc<dyn AggregatePluginFn>,
67}
68
69impl std::fmt::Debug for AggregateEntry {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.debug_struct("AggregateEntry")
72            .field("plugin", &self.plugin)
73            .field("signature", &self.signature)
74            .finish_non_exhaustive()
75    }
76}
77
78/// A single window-fn registry entry.
79pub struct WindowEntry {
80    /// Owning plugin id.
81    pub plugin: PluginId,
82    /// Window signature.
83    pub signature: WindowSignature,
84    /// The registered window function.
85    pub window: Arc<dyn WindowPluginFn>,
86}
87
88impl std::fmt::Debug for WindowEntry {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        f.debug_struct("WindowEntry")
91            .field("plugin", &self.plugin)
92            .field("signature", &self.signature)
93            .finish_non_exhaustive()
94    }
95}
96
97/// A single graph-algorithm registry entry.
98///
99/// Carries the owning plugin's effective capability set so the CALL
100/// dispatcher can enforce host-access grants (e.g. `HostQuery`) when
101/// building the algorithm host at invocation time.
102pub struct AlgorithmEntry {
103    /// Owning plugin id.
104    pub plugin: PluginId,
105    /// Effective capabilities granted to the owning plugin.
106    pub effective_caps: CapabilitySet,
107    /// The registered algorithm provider.
108    pub provider: Arc<dyn AlgorithmProvider>,
109}
110
111impl std::fmt::Debug for AlgorithmEntry {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        f.debug_struct("AlgorithmEntry")
114            .field("plugin", &self.plugin)
115            .field("effective_caps", &self.effective_caps)
116            .finish_non_exhaustive()
117    }
118}
119
120/// A single procedure registry entry.
121pub struct ProcedureEntry {
122    /// Owning plugin id.
123    pub plugin: PluginId,
124    /// Procedure signature.
125    pub signature: ProcedureSignature,
126    /// The registered procedure.
127    pub procedure: Arc<dyn ProcedurePlugin>,
128}
129
130impl std::fmt::Debug for ProcedureEntry {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        f.debug_struct("ProcedureEntry")
133            .field("plugin", &self.plugin)
134            .field("signature", &self.signature)
135            .finish_non_exhaustive()
136    }
137}
138
139/// A Locy aggregate entry.
140pub struct LocyAggregateEntry {
141    /// Owning plugin id.
142    pub plugin: PluginId,
143    /// The registered aggregate.
144    pub aggregate: Arc<dyn LocyAggregate>,
145}
146
147impl std::fmt::Debug for LocyAggregateEntry {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.debug_struct("LocyAggregateEntry")
150            .field("plugin", &self.plugin)
151            .finish_non_exhaustive()
152    }
153}
154
155/// A Locy predicate entry.
156pub struct LocyPredicateEntry {
157    /// Owning plugin id.
158    pub plugin: PluginId,
159    /// Predicate signature.
160    pub signature: PredSignature,
161    /// The registered predicate.
162    pub predicate: Arc<dyn LocyPredicate>,
163}
164
165impl std::fmt::Debug for LocyPredicateEntry {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        f.debug_struct("LocyPredicateEntry")
168            .field("plugin", &self.plugin)
169            .field("signature", &self.signature)
170            .finish_non_exhaustive()
171    }
172}
173
174/// A Locy generator-predicate entry.
175pub struct LocyGeneratorEntry {
176    /// Owning plugin id.
177    pub plugin: PluginId,
178    /// Generator signature.
179    pub signature: GenSignature,
180    /// The registered generator.
181    pub generator: Arc<dyn LocyGenerator>,
182}
183
184impl std::fmt::Debug for LocyGeneratorEntry {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        f.debug_struct("LocyGeneratorEntry")
187            .field("plugin", &self.plugin)
188            .field("signature", &self.signature)
189            .finish_non_exhaustive()
190    }
191}
192
193/// A live index handle keyed by index *name* (e.g., `"vec_idx_embedding"`).
194///
195/// Unlike `IndexKindProvider`, which is plugin-registered via the
196/// `PluginRegistrar` and describes a *kind* of index, an `IndexHandleEntry`
197/// represents a *specific* live index — the runtime object produced by
198/// `IndexKindProvider::build().finalize()` (or `IndexKindProvider::open()`).
199/// Handles are inserted by the host (not by the plugin's `register()` call)
200/// because their lifetime tracks the storage layer rather than plugin
201/// metadata.
202///
203/// The planner consults this table by index name when dispatching a vector
204/// KNN query (see `plan_vector_knn`). When `Some`, the planner routes the
205/// probe through the plugin handle; when `None`, the native storage path
206/// runs (preserving the "no behavior change for built-ins" invariant).
207#[derive(Clone)]
208pub struct IndexHandleEntry {
209    /// Kind that produced this handle (informational; matches the
210    /// `IndexKindProvider::kind` that built it).
211    pub kind: IndexKind,
212    /// The live handle.
213    pub handle: Arc<dyn IndexHandle>,
214}
215
216impl std::fmt::Debug for IndexHandleEntry {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        f.debug_struct("IndexHandleEntry")
219            .field("kind", &self.kind)
220            .finish_non_exhaustive()
221    }
222}
223
224/// One slot in the virtual label / edge-type allocation table — bundles
225/// the name the planner saw with the `CatalogTable` that owns its rows.
226///
227/// Used by [`PluginRegistry::register_virtual_label`] / `_edge_type`.
228/// Lookups by ID (via `virtual_label_by_id`) return a cheap clone of
229/// this entry so the planner's physical-scan layer can route directly
230/// to `table.scan(...)` without re-consulting the providers.
231#[derive(Clone)]
232pub struct VirtualEntry {
233    /// The user-typed name (e.g. `"External"`).
234    pub name: SmolStr,
235    /// The catalog table that owns the rows for this virtual identifier.
236    pub table: Arc<dyn crate::traits::catalog::CatalogTable>,
237}
238
239impl std::fmt::Debug for VirtualEntry {
240    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        f.debug_struct("VirtualEntry")
242            .field("name", &self.name)
243            .finish_non_exhaustive()
244    }
245}
246
247/// A virtual identifier type (label `u16` or edge-type `u32`) that the
248/// allocator can hand out. Captures the per-type `START`/`SENTINEL`
249/// bounds and the saturating increment so the allocator body can be
250/// written once, generically.
251trait VirtualId:
252    Copy + Eq + Ord + std::hash::Hash + std::fmt::Debug + std::fmt::LowerHex + 'static
253{
254    /// First ID handed out (inclusive lower bound of the virtual range).
255    const START: Self;
256    /// Reserved upper bound (exclusive); reaching it means the space is
257    /// exhausted.
258    const SENTINEL: Self;
259    /// Human-facing label for the kind of identifier, used in the
260    /// exhaustion error message (e.g. `"label"`, `"edge-type"`).
261    const KIND_LABEL: &'static str;
262
263    /// Increment without overflow (the allocator never relies on the
264    /// wrapped value because it bails at `SENTINEL` first).
265    fn next(self) -> Self;
266}
267
268impl VirtualId for u16 {
269    const START: Self = uni_common::core::schema::VIRTUAL_LABEL_ID_START;
270    const SENTINEL: Self = uni_common::core::schema::VIRTUAL_LABEL_ID_SENTINEL;
271    const KIND_LABEL: &'static str = "label";
272
273    fn next(self) -> Self {
274        self.saturating_add(1)
275    }
276}
277
278impl VirtualId for u32 {
279    const START: Self = uni_common::core::edge_type::VIRTUAL_EDGE_TYPE_ID_START;
280    const SENTINEL: Self = uni_common::core::edge_type::VIRTUAL_EDGE_TYPE_ID_SENTINEL;
281    const KIND_LABEL: &'static str = "edge-type";
282
283    fn next(self) -> Self {
284        self.saturating_add(1)
285    }
286}
287
288/// Inner mutable state for a virtual-ID allocator (labels use `u16`,
289/// edge-types use `u32`). Held behind a `parking_lot::Mutex` because
290/// allocations are rare (one per first reference to a previously-unseen
291/// name) and the contention surface is tiny.
292#[derive(Debug)]
293struct VirtualIdSpace<Id: VirtualId> {
294    name_to_id: HashMap<SmolStr, Id>,
295    id_to_entry: HashMap<Id, VirtualEntry>,
296    next_id: Id,
297}
298
299impl<Id: VirtualId> Default for VirtualIdSpace<Id> {
300    fn default() -> Self {
301        Self {
302            name_to_id: HashMap::new(),
303            id_to_entry: HashMap::new(),
304            next_id: Id::START,
305        }
306    }
307}
308
309impl<Id: VirtualId> VirtualIdSpace<Id> {
310    /// Allocate (or look up) an ID for `name`, replacing the stored
311    /// table on re-registration. Returns `Err` when the virtual range is
312    /// exhausted.
313    fn register(
314        &mut self,
315        name: SmolStr,
316        table: Arc<dyn crate::traits::catalog::CatalogTable>,
317    ) -> Result<Id, PluginError> {
318        if let Some(&id) = self.name_to_id.get(&name) {
319            self.id_to_entry.insert(
320                id,
321                VirtualEntry {
322                    name: name.clone(),
323                    table,
324                },
325            );
326            return Ok(id);
327        }
328        if self.next_id >= Id::SENTINEL {
329            return Err(PluginError::Internal(format!(
330                "virtual {}-ID space exhausted ({} slots taken; sentinel {:#x})",
331                Id::KIND_LABEL,
332                self.id_to_entry.len(),
333                Id::SENTINEL,
334            )));
335        }
336        let id = self.next_id;
337        self.next_id = self.next_id.next();
338        self.name_to_id.insert(name.clone(), id);
339        self.id_to_entry.insert(id, VirtualEntry { name, table });
340        Ok(id)
341    }
342}
343
344/// Per-plugin record of *what* this plugin registered, for unregister /
345/// hot-reload.
346///
347/// `pub(crate)` (with `pub(crate)` fields) so the family-ops traits in
348/// [`crate::surfaces`] can update the record without an accessor for each
349/// surface during the Phase 4 migration.
350#[derive(Default, Debug)]
351pub(crate) struct PluginRecord {
352    pub(crate) scalars: Vec<QName>,
353    pub(crate) aggregates: Vec<QName>,
354    pub(crate) windows: Vec<QName>,
355    /// Procedures are arity-overloaded: a given `QName` may be registered
356    /// multiple times with different arities (see `procedure_with_arity`).
357    /// The `usize` is the procedure's positional argument count, used by
358    /// `remove_plugin` to drop the exact overload this plugin owns.
359    pub(crate) procedures: Vec<(QName, usize)>,
360    pub(crate) locy_aggregates: Vec<QName>,
361    pub(crate) locy_predicates: Vec<QName>,
362    pub(crate) locy_generators: Vec<QName>,
363    pub(crate) algorithms: Vec<QName>,
364    pub(crate) index_kinds: Vec<IndexKind>,
365    pub(crate) label_storages: Vec<SmolStr>,
366    pub(crate) crdt_kinds: Vec<CrdtKind>,
367    /// Logical-type extension names this plugin registered. Tracked
368    /// per-key (not count-only) so `remove_plugin` can drop the entries
369    /// on hot reload.
370    pub(crate) logical_types: Vec<SmolStr>,
371    /// Collation names this plugin registered.
372    pub(crate) collations: Vec<SmolStr>,
373    /// CDC output sink names this plugin registered.
374    pub(crate) cdc_outputs: Vec<SmolStr>,
375    /// Catalog names this plugin registered.
376    pub(crate) catalogs: Vec<SmolStr>,
377    pub(crate) hook_count: usize,
378    pub(crate) auth_count: usize,
379    pub(crate) authz_count: usize,
380    pub(crate) trigger_count: usize,
381    pub(crate) replacement_scan_count: usize,
382    pub(crate) optimizer_rule_count: usize,
383    pub(crate) background_job_count: usize,
384}
385
386impl PluginRecord {
387    /// Merge another record's surfaces into this one: append every owned-key
388    /// vector and sum the count-only tallies.
389    ///
390    /// `apply_pending` must merge, not overwrite, when a plugin id commits more
391    /// than once (e.g. two `declareFunction` calls that each run their own
392    /// registrar under the same namespace id). Overwriting the record drops the
393    /// earlier commit's surfaces from the ownership map, so `remove_plugin` later
394    /// leaks them (they stay live in the registry slots but are untracked).
395    fn merge(&mut self, other: PluginRecord) {
396        self.scalars.extend(other.scalars);
397        self.aggregates.extend(other.aggregates);
398        self.windows.extend(other.windows);
399        self.procedures.extend(other.procedures);
400        self.locy_aggregates.extend(other.locy_aggregates);
401        self.locy_predicates.extend(other.locy_predicates);
402        self.locy_generators.extend(other.locy_generators);
403        self.algorithms.extend(other.algorithms);
404        self.index_kinds.extend(other.index_kinds);
405        self.label_storages.extend(other.label_storages);
406        self.crdt_kinds.extend(other.crdt_kinds);
407        self.logical_types.extend(other.logical_types);
408        self.collations.extend(other.collations);
409        self.cdc_outputs.extend(other.cdc_outputs);
410        self.catalogs.extend(other.catalogs);
411        self.hook_count += other.hook_count;
412        self.auth_count += other.auth_count;
413        self.authz_count += other.authz_count;
414        self.trigger_count += other.trigger_count;
415        self.replacement_scan_count += other.replacement_scan_count;
416        self.optimizer_rule_count += other.optimizer_rule_count;
417        self.background_job_count += other.background_job_count;
418    }
419}
420
421/// A deep-clone snapshot of one plugin's registry footprint.
422///
423/// Produced by [`PluginRegistry::iter_for_plugin`] and consumed by
424/// [`crate::reload::ReloadDispatcher`]. The snapshot is **not** kept
425/// in sync with the live registry; it represents the surfaces a
426/// plugin owned at the moment the snapshot was taken.
427#[derive(Clone, Debug, Default)]
428pub struct PluginRecordSnapshot {
429    /// Scalar fns this plugin registered.
430    pub scalars: Vec<QName>,
431    /// Aggregate fns this plugin registered.
432    pub aggregates: Vec<QName>,
433    /// Window fns this plugin registered.
434    pub windows: Vec<QName>,
435    /// Procedures (qname + arity) this plugin registered.
436    pub procedures: Vec<(QName, usize)>,
437    /// Locy aggregates this plugin registered.
438    pub locy_aggregates: Vec<QName>,
439    /// Locy predicates this plugin registered.
440    pub locy_predicates: Vec<QName>,
441    /// Locy generator predicates this plugin registered.
442    pub locy_generators: Vec<QName>,
443    /// Algorithms this plugin registered.
444    pub algorithms: Vec<QName>,
445    /// Index kinds this plugin registered.
446    pub index_kinds: Vec<IndexKind>,
447    /// Label storages this plugin registered.
448    pub label_storages: Vec<SmolStr>,
449    /// CRDT kinds this plugin registered.
450    pub crdt_kinds: Vec<CrdtKind>,
451    /// Logical-type extension names this plugin registered.
452    pub logical_types: Vec<SmolStr>,
453    /// Collation names this plugin registered.
454    pub collations: Vec<SmolStr>,
455    /// CDC output sink names this plugin registered.
456    pub cdc_outputs: Vec<SmolStr>,
457    /// Catalog names this plugin registered.
458    pub catalogs: Vec<SmolStr>,
459    /// Number of `SessionHook`s this plugin registered.
460    pub hook_count: usize,
461    /// Number of `AuthProvider`s this plugin registered.
462    pub auth_count: usize,
463    /// Number of `AuthzPolicy`s this plugin registered.
464    pub authz_count: usize,
465    /// Number of `TriggerPlugin`s this plugin registered.
466    pub trigger_count: usize,
467    /// Number of `ReplacementScanProvider`s this plugin registered.
468    pub replacement_scan_count: usize,
469    /// Number of `OptimizerRuleProvider`s this plugin registered.
470    pub optimizer_rule_count: usize,
471    /// Number of `BackgroundJobProvider`s this plugin registered.
472    pub background_job_count: usize,
473}
474
475impl From<&PluginRecord> for PluginRecordSnapshot {
476    /// Deep-clone a live `PluginRecord` into a standalone snapshot. The
477    /// field list lives only on the two struct definitions; this clones
478    /// each (`Vec`s deep-clone their elements, counts are `Copy`).
479    fn from(r: &PluginRecord) -> Self {
480        Self {
481            scalars: r.scalars.clone(),
482            aggregates: r.aggregates.clone(),
483            windows: r.windows.clone(),
484            procedures: r.procedures.clone(),
485            locy_aggregates: r.locy_aggregates.clone(),
486            locy_predicates: r.locy_predicates.clone(),
487            locy_generators: r.locy_generators.clone(),
488            algorithms: r.algorithms.clone(),
489            index_kinds: r.index_kinds.clone(),
490            label_storages: r.label_storages.clone(),
491            crdt_kinds: r.crdt_kinds.clone(),
492            logical_types: r.logical_types.clone(),
493            collations: r.collations.clone(),
494            cdc_outputs: r.cdc_outputs.clone(),
495            catalogs: r.catalogs.clone(),
496            hook_count: r.hook_count,
497            auth_count: r.auth_count,
498            authz_count: r.authz_count,
499            trigger_count: r.trigger_count,
500            replacement_scan_count: r.replacement_scan_count,
501            optimizer_rule_count: r.optimizer_rule_count,
502            background_job_count: r.background_job_count,
503        }
504    }
505}
506
507/// All-surfaces plugin registry.
508///
509/// Per-surface tables wrapped in `arc-swap` for wait-free reads. The
510/// registry tracks per-plugin ownership so `remove_plugin` can clean up
511/// all of a plugin's registrations in one pass.
512#[derive(Default)]
513pub struct PluginRegistry {
514    pub(crate) scalars: DashMap<QName, Arc<ScalarEntry>>,
515    pub(crate) aggregates: DashMap<QName, Arc<AggregateEntry>>,
516    pub(crate) windows: DashMap<QName, Arc<WindowEntry>>,
517    /// Procedures keyed by qname. Each qname may carry multiple overload
518    /// entries discriminated by `entry.signature.args.len()` so callers can
519    /// register two registrations under the same name with different
520    /// arities (M5c.2: legacy 5-arg + new 2-arg algorithm signatures).
521    /// `procedure(&q)` returns the first registration; arity-aware callers
522    /// use `procedure_with_arity(&q, arity)`.
523    pub(crate) procedures: DashMap<QName, Vec<Arc<ProcedureEntry>>>,
524    pub(crate) locy_aggregates: DashMap<QName, Arc<LocyAggregateEntry>>,
525    pub(crate) locy_predicates: DashMap<QName, Arc<LocyPredicateEntry>>,
526    pub(crate) locy_generators: DashMap<QName, Arc<LocyGeneratorEntry>>,
527    pub(crate) optimizer_rules:
528        ArcSwap<Vec<crate::surfaces::AppendEntry<dyn OptimizerRuleProvider>>>,
529    pub(crate) algorithms: DashMap<QName, Arc<AlgorithmEntry>>,
530    pub(crate) index_kinds: DashMap<IndexKind, Arc<dyn IndexKindProvider>>,
531    index_handles: DashMap<SmolStr, IndexHandleEntry>,
532    /// Per-label plugin storage (M5h.2). Keyed by *label name* and
533    /// resolves to an already-open `Storage`. The host's
534    /// `StorageManager::scan_vertex_table` consults this map before
535    /// falling through to the native backend so a third-party plugin
536    /// can serve a native-schema label from its own storage.
537    pub(crate) label_storages: DashMap<SmolStr, Arc<dyn crate::traits::storage::Storage>>,
538    pub(crate) crdt_kinds: DashMap<CrdtKind, Arc<dyn CrdtKindProvider>>,
539    pub(crate) hooks: ArcSwap<Vec<crate::surfaces::AppendEntry<dyn SessionHook>>>,
540    pub(crate) logical_types: DashMap<SmolStr, Arc<dyn LogicalTypeProvider>>,
541    pub(crate) auth_providers: ArcSwap<Vec<crate::surfaces::AppendEntry<dyn AuthProvider>>>,
542    pub(crate) authz_policies: ArcSwap<Vec<crate::surfaces::AppendEntry<dyn AuthzPolicy>>>,
543    pub(crate) triggers: ArcSwap<Vec<crate::surfaces::AppendEntry<dyn TriggerPlugin>>>,
544    pub(crate) collations: DashMap<SmolStr, Arc<dyn CollationProvider>>,
545    pub(crate) cdc_outputs: DashMap<SmolStr, Arc<dyn CdcOutputProvider>>,
546    pub(crate) catalogs: DashMap<SmolStr, Arc<dyn CatalogProvider>>,
547    pub(crate) replacement_scans:
548        ArcSwap<Vec<crate::surfaces::AppendEntry<dyn ReplacementScanProvider>>>,
549    pub(crate) background_jobs:
550        ArcSwap<Vec<crate::surfaces::AppendEntry<dyn BackgroundJobProvider>>>,
551    /// Virtual label-ID allocator. Allocates IDs in the schema's reserved
552    /// virtual range (`uni_common::core::schema::VIRTUAL_LABEL_ID_START..
553    /// VIRTUAL_LABEL_ID_SENTINEL`) on first observation of an unknown label
554    /// name that a `CatalogProvider` or `ReplacementScanProvider` claims.
555    /// See [`Self::register_virtual_label`] / [`Self::virtual_label_by_id`].
556    virtual_labels: Mutex<VirtualIdSpace<u16>>,
557    /// Virtual edge-type allocator. Allocates IDs in
558    /// `uni_common::core::edge_type::VIRTUAL_EDGE_TYPE_ID_START..
559    /// VIRTUAL_EDGE_TYPE_ID_SENTINEL`. Same first-observation semantics.
560    virtual_edge_types: Mutex<VirtualIdSpace<u32>>,
561    per_plugin: RwLock<dashmap::DashMap<PluginId, PluginRecord>>,
562}
563
564impl std::fmt::Debug for PluginRegistry {
565    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566        f.debug_struct("PluginRegistry")
567            .field("scalar_fns", &self.scalars.len())
568            .field("aggregates", &self.aggregates.len())
569            .field("procedures", &self.procedures.len())
570            .field("locy_aggregates", &self.locy_aggregates.len())
571            .field("algorithms", &self.algorithms.len())
572            .field("index_kinds", &self.index_kinds.len())
573            .field("hooks", &self.hooks.load().len())
574            .field("plugins", &self.per_plugin.read().len())
575            .finish()
576    }
577}
578
579impl PluginRegistry {
580    /// Construct an empty registry.
581    #[must_use]
582    pub fn new() -> Self {
583        Self::default()
584    }
585
586    /// Look up a registered scalar function by qname.
587    #[must_use]
588    pub fn scalar_fn(&self, q: &QName) -> Option<Arc<ScalarEntry>> {
589        self.scalars.get(q).map(|e| Arc::clone(e.value()))
590    }
591
592    /// Iterate every registered scalar function — `(QName, ScalarEntry)`.
593    ///
594    /// Collects into a `Vec` so the iteration does not hold a long-lived
595    /// reference to the underlying `DashMap` (avoids subtle aliasing
596    /// hazards when callers register or remove plugins mid-iteration).
597    ///
598    /// # Examples
599    ///
600    /// ```ignore
601    /// for (qname, entry) in registry.iter_scalars() {
602    ///     ctx.register_udf(ScalarUDF::new_from_impl(adapt(qname, entry)));
603    /// }
604    /// ```
605    #[must_use]
606    pub fn iter_scalars(&self) -> Vec<(QName, Arc<ScalarEntry>)> {
607        self.scalars
608            .iter()
609            .map(|kv| (kv.key().clone(), Arc::clone(kv.value())))
610            .collect()
611    }
612
613    /// Iterate every registered procedure — `(QName, ProcedureEntry)`.
614    ///
615    /// Arity-overloaded names yield one tuple per registered overload.
616    #[must_use]
617    pub fn iter_procedures(&self) -> Vec<(QName, Arc<ProcedureEntry>)> {
618        self.procedures
619            .iter()
620            .flat_map(|kv| {
621                let q = kv.key().clone();
622                kv.value()
623                    .iter()
624                    .map(move |e| (q.clone(), Arc::clone(e)))
625                    .collect::<Vec<_>>()
626            })
627            .collect()
628    }
629
630    /// Iterate every registered Locy aggregate — `(QName, LocyAggregateEntry)`.
631    #[must_use]
632    pub fn iter_locy_aggregates(&self) -> Vec<(QName, Arc<LocyAggregateEntry>)> {
633        self.locy_aggregates
634            .iter()
635            .map(|kv| (kv.key().clone(), Arc::clone(kv.value())))
636            .collect()
637    }
638
639    /// Iterate every registered Locy predicate — `(QName, LocyPredicateEntry)`.
640    #[must_use]
641    pub fn iter_locy_predicates(&self) -> Vec<(QName, Arc<LocyPredicateEntry>)> {
642        self.locy_predicates
643            .iter()
644            .map(|kv| (kv.key().clone(), Arc::clone(kv.value())))
645            .collect()
646    }
647
648    /// Iterate every registered Locy generator — `(QName, LocyGeneratorEntry)`.
649    #[must_use]
650    pub fn iter_locy_generators(&self) -> Vec<(QName, Arc<LocyGeneratorEntry>)> {
651        self.locy_generators
652            .iter()
653            .map(|kv| (kv.key().clone(), Arc::clone(kv.value())))
654            .collect()
655    }
656
657    /// Iterate every registered algorithm — `(QName, AlgorithmProvider)`.
658    #[must_use]
659    pub fn iter_algorithms(&self) -> Vec<(QName, Arc<dyn AlgorithmProvider>)> {
660        self.algorithms
661            .iter()
662            .map(|kv| (kv.key().clone(), Arc::clone(&kv.value().provider)))
663            .collect()
664    }
665
666    /// Iterate every registered index kind — `(IndexKind, IndexKindProvider)`.
667    #[must_use]
668    pub fn iter_index_kinds(&self) -> Vec<(IndexKind, Arc<dyn IndexKindProvider>)> {
669        self.index_kinds
670            .iter()
671            .map(|kv| (kv.key().clone(), Arc::clone(kv.value())))
672            .collect()
673    }
674
675    /// Snapshot the registered catalog providers.
676    ///
677    /// Returns a `Vec` so the iteration does not hold a long-lived reference
678    /// to the underlying `DashMap`.
679    #[must_use]
680    pub fn catalogs(&self) -> Vec<Arc<dyn CatalogProvider>> {
681        self.catalogs
682            .iter()
683            .map(|kv| Arc::clone(kv.value()))
684            .collect()
685    }
686
687    /// Look up a registered aggregate by qname.
688    #[must_use]
689    pub fn aggregate(&self, q: &QName) -> Option<Arc<AggregateEntry>> {
690        self.aggregates.get(q).map(|e| Arc::clone(e.value()))
691    }
692
693    /// Look up a registered window function by qname.
694    #[must_use]
695    pub fn window(&self, q: &QName) -> Option<Arc<WindowEntry>> {
696        self.windows.get(q).map(|e| Arc::clone(e.value()))
697    }
698
699    /// Look up a registered procedure by qname.
700    ///
701    /// If the qname carries multiple arity overloads (M5c.2), this returns
702    /// the *first* registered entry, which preserves the legacy
703    /// single-arity lookup contract. Arity-aware callers should use
704    /// [`Self::procedure_with_arity`] instead.
705    #[must_use]
706    pub fn procedure(&self, q: &QName) -> Option<Arc<ProcedureEntry>> {
707        self.procedures
708            .get(q)
709            .and_then(|e| e.value().first().map(Arc::clone))
710    }
711
712    /// Look up a registered procedure by qname *and* positional argument
713    /// count. Returns the entry whose signature has exactly `arity`
714    /// arguments, or `None` if no overload matches.
715    ///
716    /// Procedures may be registered with the same qname under multiple
717    /// arities (e.g. an algorithm's legacy 5-arg form alongside the new
718    /// `(graphRef, config)` 2-arg form). Resolution sites that know the
719    /// call's argument count should prefer this method; the bare
720    /// [`Self::procedure`] is preserved for callers that only need the
721    /// first registration.
722    #[must_use]
723    pub fn procedure_with_arity(&self, q: &QName, arity: usize) -> Option<Arc<ProcedureEntry>> {
724        self.procedures.get(q).and_then(|e| {
725            e.value()
726                .iter()
727                .find(|entry| entry.signature.args.len() == arity)
728                .map(Arc::clone)
729        })
730    }
731
732    /// Return all arity overloads registered under `q`.
733    ///
734    /// The returned `Vec` is empty when nothing is registered. Useful for
735    /// diagnostic surfaces (e.g. `EXPLAIN` of an ambiguous call) and for
736    /// listing API.
737    #[must_use]
738    pub fn procedure_overloads(&self, q: &QName) -> Vec<Arc<ProcedureEntry>> {
739        self.procedures
740            .get(q)
741            .map(|e| e.value().iter().map(Arc::clone).collect())
742            .unwrap_or_default()
743    }
744
745    /// Look up a registered Locy aggregate by qname.
746    #[must_use]
747    pub fn locy_aggregate(&self, q: &QName) -> Option<Arc<LocyAggregateEntry>> {
748        self.locy_aggregates.get(q).map(|e| Arc::clone(e.value()))
749    }
750
751    /// Look up a registered Locy predicate by qname.
752    #[must_use]
753    pub fn locy_predicate(&self, q: &QName) -> Option<Arc<LocyPredicateEntry>> {
754        self.locy_predicates.get(q).map(|e| Arc::clone(e.value()))
755    }
756
757    /// Look up a registered Locy generator by qname.
758    #[must_use]
759    pub fn locy_generator(&self, q: &QName) -> Option<Arc<LocyGeneratorEntry>> {
760        self.locy_generators.get(q).map(|e| Arc::clone(e.value()))
761    }
762
763    /// Look up the plugin `Storage` (if any) registered to serve the
764    /// given native label name (M5h.2). Consulted by the host's
765    /// `StorageManager::scan_vertex_table` before the native backend
766    /// fallback — when this returns `Some`, the planner's graph-scan
767    /// path is routed through plugin storage instead of Lance.
768    #[must_use]
769    pub fn lookup_label_storage(
770        &self,
771        label: &str,
772    ) -> Option<Arc<dyn crate::traits::storage::Storage>> {
773        self.label_storages
774            .get(&SmolStr::new(label))
775            .map(|e| Arc::clone(e.value()))
776    }
777
778    /// Look up a registered index-kind by kind.
779    #[must_use]
780    pub fn index_kind(&self, k: &IndexKind) -> Option<Arc<dyn IndexKindProvider>> {
781        self.index_kinds.get(k).map(|e| Arc::clone(e.value()))
782    }
783
784    /// Register a live `IndexHandle` under an index name.
785    ///
786    /// The host calls this after building a handle from a custom
787    /// `IndexKindProvider` (or after `open()` from persisted bytes). The
788    /// planner consults this table from `plan_vector_knn` to route probes
789    /// through the plugin handle instead of the native storage path.
790    ///
791    /// If an entry already exists under the same name, it is replaced.
792    pub fn register_index_handle(
793        &self,
794        name: impl Into<SmolStr>,
795        kind: IndexKind,
796        handle: Arc<dyn IndexHandle>,
797    ) {
798        self.index_handles
799            .insert(name.into(), IndexHandleEntry { kind, handle });
800    }
801
802    /// Look up a live `IndexHandle` by index name. Returns a cheap clone
803    /// (the inner handle is `Arc`-wrapped).
804    #[must_use]
805    pub fn index_handle(&self, name: &str) -> Option<IndexHandleEntry> {
806        self.index_handles
807            .get(&SmolStr::new(name))
808            .map(|e| e.value().clone())
809    }
810
811    /// Remove a live `IndexHandle`. Returns the removed entry if one
812    /// existed.
813    pub fn deregister_index_handle(&self, name: &str) -> Option<IndexHandleEntry> {
814        self.index_handles
815            .remove(&SmolStr::new(name))
816            .map(|(_, v)| v)
817    }
818
819    /// Allocate (or look up) a virtual label ID for `name`, owned by
820    /// `table`. The host's `QueryPlanner` calls this when an unknown
821    /// label name is claimed by a `CatalogProvider` or
822    /// `ReplacementScanProvider`; subsequent references to the same name
823    /// return the cached ID without re-running discovery.
824    ///
825    /// Idempotent: a second call with the same name returns the
826    /// previously-allocated ID and *replaces* the stored `CatalogTable`
827    /// (so cached `LogicalPlan`s naturally pick up the latest table on
828    /// next execute). Returns `Err` if the virtual range is exhausted
829    /// (255 slots, see `uni_common::core::schema`).
830    pub fn register_virtual_label(
831        &self,
832        name: impl Into<SmolStr>,
833        table: Arc<dyn crate::traits::catalog::CatalogTable>,
834    ) -> Result<u16, PluginError> {
835        self.virtual_labels.lock().register(name.into(), table)
836    }
837
838    /// Look up a virtual label by name. Returns `None` if no provider
839    /// has claimed it yet (the caller hasn't called
840    /// `register_virtual_label`).
841    #[must_use]
842    pub fn virtual_label_by_name(&self, name: &str) -> Option<u16> {
843        let inner = self.virtual_labels.lock();
844        inner.name_to_id.get(&SmolStr::new(name)).copied()
845    }
846
847    /// Look up the catalog table behind a virtual label ID. Returns the
848    /// entry cheaply cloned (inner `Arc<dyn CatalogTable>`).
849    #[must_use]
850    pub fn virtual_label_by_id(&self, id: u16) -> Option<VirtualEntry> {
851        self.virtual_labels.lock().id_to_entry.get(&id).cloned()
852    }
853
854    /// Allocate (or look up) a virtual edge-type ID for `name`. Same
855    /// semantics as [`Self::register_virtual_label`] but for the
856    /// `u32` edge-type ID space.
857    pub fn register_virtual_edge_type(
858        &self,
859        name: impl Into<SmolStr>,
860        table: Arc<dyn crate::traits::catalog::CatalogTable>,
861    ) -> Result<u32, PluginError> {
862        self.virtual_edge_types.lock().register(name.into(), table)
863    }
864
865    /// Look up a virtual edge type by name.
866    #[must_use]
867    pub fn virtual_edge_type_by_name(&self, name: &str) -> Option<u32> {
868        let inner = self.virtual_edge_types.lock();
869        inner.name_to_id.get(&SmolStr::new(name)).copied()
870    }
871
872    /// Look up the catalog table behind a virtual edge-type ID.
873    #[must_use]
874    pub fn virtual_edge_type_by_id(&self, id: u32) -> Option<VirtualEntry> {
875        self.virtual_edge_types.lock().id_to_entry.get(&id).cloned()
876    }
877
878    /// Look up a registered algorithm provider by qname.
879    #[must_use]
880    pub fn algorithm(&self, q: &QName) -> Option<Arc<dyn AlgorithmProvider>> {
881        self.algorithms
882            .get(q)
883            .map(|e| Arc::clone(&e.value().provider))
884    }
885
886    /// Look up a registered algorithm's full entry by qname.
887    ///
888    /// Unlike [`Self::algorithm`], the returned [`AlgorithmEntry`] also
889    /// carries the owning plugin's effective capabilities, which the CALL
890    /// dispatcher needs to gate host graph access.
891    #[must_use]
892    pub fn algorithm_entry(&self, q: &QName) -> Option<Arc<AlgorithmEntry>> {
893        self.algorithms.get(q).map(|e| Arc::clone(e.value()))
894    }
895
896    /// Look up a registered CRDT kind.
897    #[must_use]
898    pub fn crdt_kind(&self, k: &CrdtKind) -> Option<Arc<dyn CrdtKindProvider>> {
899        self.crdt_kinds.get(k).map(|e| Arc::clone(e.value()))
900    }
901
902    /// Look up a registered logical type by its Arrow extension name.
903    #[must_use]
904    pub fn logical_type(&self, name: &SmolStr) -> Option<Arc<dyn LogicalTypeProvider>> {
905        self.logical_types.get(name).map(|e| Arc::clone(e.value()))
906    }
907
908    /// Snapshot the registered hook chain.
909    #[must_use]
910    pub fn hooks(&self) -> Arc<Vec<Arc<dyn SessionHook>>> {
911        Self::project_append(&self.hooks)
912    }
913
914    /// Snapshot the registered optimizer-rule providers (M5h).
915    #[must_use]
916    pub fn optimizer_rules(&self) -> Arc<Vec<Arc<dyn OptimizerRuleProvider>>> {
917        Self::project_append(&self.optimizer_rules)
918    }
919
920    /// Snapshot the registered trigger chain.
921    #[must_use]
922    pub fn triggers(&self) -> Arc<Vec<Arc<dyn TriggerPlugin>>> {
923        Self::project_append(&self.triggers)
924    }
925
926    /// Snapshot every registered [`CdcOutputProvider`] keyed by name (FU-4).
927    ///
928    /// Used by `Uni::build` to start a CDC stream per provider before
929    /// the commit broadcaster begins pushing `CdcBatch`es.
930    #[must_use]
931    pub fn cdc_outputs_snapshot(&self) -> Vec<(SmolStr, Arc<dyn CdcOutputProvider>)> {
932        self.cdc_outputs
933            .iter()
934            .map(|e| (e.key().clone(), Arc::clone(e.value())))
935            .collect()
936    }
937
938    /// `true` when no [`CdcOutputProvider`] is registered.
939    ///
940    /// Used by the commit hot-path to skip mutation-row materialization
941    /// when there are no CDC subscribers — preserves the empty-registry
942    /// fast path.
943    #[must_use]
944    pub fn cdc_outputs_is_empty(&self) -> bool {
945        self.cdc_outputs.is_empty()
946    }
947
948    /// Snapshot the registered authentication providers (M5i).
949    #[must_use]
950    pub fn auth_providers(&self) -> Arc<Vec<Arc<dyn AuthProvider>>> {
951        Self::project_append(&self.auth_providers)
952    }
953
954    /// Snapshot the registered authorization policies (M5i).
955    #[must_use]
956    pub fn authz_policies(&self) -> Arc<Vec<Arc<dyn AuthzPolicy>>> {
957        Self::project_append(&self.authz_policies)
958    }
959
960    /// Snapshot the registered replacement-scan providers.
961    #[must_use]
962    pub fn replacement_scans(&self) -> Arc<Vec<Arc<dyn ReplacementScanProvider>>> {
963        Self::project_append(&self.replacement_scans)
964    }
965
966    /// Apply a batch of pending registrations atomically.
967    ///
968    /// Preflights every entry against the live registry first, then
969    /// applies them in order. Dispatch is per-family (see
970    /// [`crate::surfaces`]): static-typed `*Ops` impls handle storage and
971    /// per-plugin record-keeping; the `DynPendingRegistration` boxes
972    /// erase the family type so a heterogeneous batch can be queued.
973    ///
974    /// # Errors
975    ///
976    /// Returns the first preflight failure (e.g.
977    /// [`PluginError::DuplicateRegistration`] or
978    /// [`PluginError::StorageSchemeConflict`]); nothing in the batch is
979    /// applied in that case.
980    pub(crate) fn apply_pending(
981        &self,
982        plugin_id: &PluginId,
983        pending: Vec<Box<dyn crate::surfaces::DynPendingRegistration>>,
984    ) -> Result<(), PluginError> {
985        // Preflight against the live registry, and — because that only sees the
986        // live registry, not the rest of this batch — also reject duplicate
987        // unique keys WITHIN the batch (two entries for the same qname in one
988        // register() call would otherwise both pass and silently last-write-win).
989        let mut seen: std::collections::HashSet<QName> = std::collections::HashSet::new();
990        for reg in &pending {
991            reg.preflight(self)?;
992            if let Some(qname) = reg.dedup_key()
993                && !seen.insert(qname.clone())
994            {
995                return Err(PluginError::DuplicateRegistration(qname));
996            }
997        }
998
999        let mut record = PluginRecord::default();
1000        for reg in pending {
1001            reg.apply(self, plugin_id.clone(), &mut record);
1002        }
1003
1004        // Merge (do NOT overwrite) so a second commit under the same plugin id
1005        // keeps the surfaces the earlier commit registered.
1006        self.per_plugin
1007            .read()
1008            .entry(plugin_id.clone())
1009            .or_default()
1010            .merge(record);
1011
1012        Ok(())
1013    }
1014
1015    /// Snapshot the registered background jobs.
1016    #[must_use]
1017    pub fn background_jobs(&self) -> Arc<Vec<Arc<dyn BackgroundJobProvider>>> {
1018        Self::project_append(&self.background_jobs)
1019    }
1020
1021    /// Materialize an `Arc<Vec<Arc<dyn P>>>` view of an append-family slot,
1022    /// stripping the per-entry `AppendEntry` ownership tag.
1023    ///
1024    /// The legacy public read-accessor signature returns `Arc<Vec<Arc<dyn
1025    /// P>>>` for wait-free callers (`hooks()`, `triggers()`, …). The
1026    /// owner-tagged storage required for proper `remove_plugin`
1027    /// implementation (closes the M5e gap; see [`crate::surfaces`]
1028    /// foundation work) carries the plugin id inline, so projecting back to
1029    /// the legacy shape costs one allocation + N `Arc` clones per call.
1030    /// Phase 4f will retire this helper in favour of returning the typed
1031    /// `AppendEntry` slice directly.
1032    fn project_append<P: ?Sized>(
1033        slot: &ArcSwap<Vec<crate::surfaces::AppendEntry<P>>>,
1034    ) -> Arc<Vec<Arc<P>>> {
1035        let snap = slot.load();
1036        let v: Vec<Arc<P>> = snap.iter().map(|e| Arc::clone(&e.provider)).collect();
1037        Arc::new(v)
1038    }
1039
1040    /// Snapshot the surfaces a plugin currently owns.
1041    ///
1042    /// Returns `None` when the plugin has never registered anything (or
1043    /// has already been removed). Used by
1044    /// [`crate::reload::ReloadDispatcher`] to determine which per-kind
1045    /// reload protocols to invoke for the old plugin.
1046    ///
1047    /// The snapshot is a deep clone of the registry's internal
1048    /// `PluginRecord`; mutating the registry afterward does not affect
1049    /// the snapshot.
1050    #[must_use]
1051    pub fn iter_for_plugin(&self, plugin: &PluginId) -> Option<PluginRecordSnapshot> {
1052        let guard = self.per_plugin.read();
1053        guard.get(plugin).map(|r| PluginRecordSnapshot::from(&*r))
1054    }
1055
1056    /// Remove a single named-unique surface (scalar or aggregate) that `plugin`
1057    /// registered under `qname`, leaving the plugin's other surfaces intact.
1058    ///
1059    /// [`Self::remove_plugin`] drops an entire plugin id at once; declared-function
1060    /// stores pack many functions under one namespace id (e.g. `mycorp.f1`,
1061    /// `mycorp.f2` both under `mycorp`), so dropping one must not unregister its
1062    /// siblings. It is also used to drop the prior entry when a declared qname is
1063    /// re-declared, so re-registration is not mistaken for shadowing a native fn.
1064    ///
1065    /// Returns whether anything was removed.
1066    pub fn remove_named_unique(&self, plugin: &PluginId, qname: &QName) -> bool {
1067        use crate::surfaces::{AggregateSurface, NamedUniqueOps, ScalarSurface};
1068        let mut removed = false;
1069        if let Some(mut rec) = self.per_plugin.read().get_mut(plugin) {
1070            if let Some(pos) = rec.scalars.iter().position(|q| q == qname) {
1071                rec.scalars.remove(pos);
1072                <ScalarSurface as NamedUniqueOps>::remove(self, qname);
1073                removed = true;
1074            }
1075            if let Some(pos) = rec.aggregates.iter().position(|q| q == qname) {
1076                rec.aggregates.remove(pos);
1077                <AggregateSurface as NamedUniqueOps>::remove(self, qname);
1078                removed = true;
1079            }
1080        }
1081        removed
1082    }
1083
1084    /// Remove all registrations for the given plugin.
1085    ///
1086    /// Used by `Uni::remove_plugin` and as part of hot reload's drain step.
1087    /// Dispatches per family via the `*Ops` traits in [`crate::surfaces`];
1088    /// the label-storage / logical-type / collation / cdc / catalog
1089    /// surfaces are dropped here too (the per-key tracking lifts the old
1090    /// count-only gap where hot reload leaked entries on those slots).
1091    pub fn remove_plugin(&self, plugin: &PluginId) {
1092        use crate::surfaces::{
1093            AggregateSurface, AlgorithmSurface, AppendOps, AuthSurface, AuthzSurface,
1094            BackgroundJobSurface, CatalogSurface, CdcSurface, CollationSurface, CrdtSurface,
1095            Discriminator, HookSurface, IndexKindSurface, KeyedUniqueOps, LabelStorageSurface,
1096            LocyAggregateSurface, LocyGeneratorSurface, LocyPredicateSurface, LogicalTypeSurface,
1097            NamedUniqueOps, OptimizerRuleSurface, ProcedureSurface, ReplacementScanSurface,
1098            ScalarSurface, TriggerSurface, VersionedOps, WindowSurface,
1099        };
1100
1101        let record = self.per_plugin.read().remove(plugin).map(|(_, r)| r);
1102        let Some(record) = record else { return };
1103
1104        for q in record.scalars {
1105            <ScalarSurface as NamedUniqueOps>::remove(self, &q);
1106        }
1107        for q in record.aggregates {
1108            <AggregateSurface as NamedUniqueOps>::remove(self, &q);
1109        }
1110        for q in record.windows {
1111            <WindowSurface as NamedUniqueOps>::remove(self, &q);
1112        }
1113        for (q, arity) in record.procedures {
1114            <ProcedureSurface as VersionedOps>::remove(self, &q, Discriminator::Arity(arity));
1115        }
1116        for q in record.locy_aggregates {
1117            <LocyAggregateSurface as NamedUniqueOps>::remove(self, &q);
1118        }
1119        for q in record.locy_predicates {
1120            <LocyPredicateSurface as NamedUniqueOps>::remove(self, &q);
1121        }
1122        for q in record.locy_generators {
1123            <LocyGeneratorSurface as NamedUniqueOps>::remove(self, &q);
1124        }
1125        for q in record.algorithms {
1126            <AlgorithmSurface as NamedUniqueOps>::remove(self, &q);
1127        }
1128        for k in record.index_kinds {
1129            <IndexKindSurface as KeyedUniqueOps>::remove(self, &k);
1130        }
1131        for l in record.label_storages {
1132            <LabelStorageSurface as KeyedUniqueOps>::remove(self, &l);
1133        }
1134        for k in record.crdt_kinds {
1135            <CrdtSurface as KeyedUniqueOps>::remove(self, &k);
1136        }
1137        for k in record.logical_types {
1138            <LogicalTypeSurface as KeyedUniqueOps>::remove(self, &k);
1139        }
1140        for k in record.collations {
1141            <CollationSurface as KeyedUniqueOps>::remove(self, &k);
1142        }
1143        for k in record.cdc_outputs {
1144            <CdcSurface as KeyedUniqueOps>::remove(self, &k);
1145        }
1146        for k in record.catalogs {
1147            <CatalogSurface as KeyedUniqueOps>::remove(self, &k);
1148        }
1149
1150        <OptimizerRuleSurface as AppendOps>::remove_plugin(self, plugin);
1151        <HookSurface as AppendOps>::remove_plugin(self, plugin);
1152        <AuthSurface as AppendOps>::remove_plugin(self, plugin);
1153        <AuthzSurface as AppendOps>::remove_plugin(self, plugin);
1154        <TriggerSurface as AppendOps>::remove_plugin(self, plugin);
1155        <ReplacementScanSurface as AppendOps>::remove_plugin(self, plugin);
1156        <BackgroundJobSurface as AppendOps>::remove_plugin(self, plugin);
1157    }
1158}
1159
1160#[cfg(test)]
1161mod tests {
1162    use super::*;
1163
1164    #[test]
1165    fn registry_default_is_empty() {
1166        let r = PluginRegistry::new();
1167        assert!(r.scalar_fn(&QName::builtin("anything")).is_none());
1168        assert!(r.procedure(&QName::builtin("anything")).is_none());
1169        assert_eq!(r.hooks().len(), 0);
1170    }
1171
1172    #[test]
1173    fn debug_smoke() {
1174        let r = PluginRegistry::new();
1175        let s = format!("{r:?}");
1176        assert!(s.contains("PluginRegistry"));
1177    }
1178}