Skip to main content

uni_plugin/surfaces/
mod.rs

1//! Per-surface trait abstractions for the plugin registry.
2//!
3//! This module is filled during Phase 4 of the §1.1 consolidation pass. It
4//! introduces the [`SurfaceKind`] enum and four family traits
5//! ([`NamedUniqueSurface`], [`VersionedSurface`], [`KeyedUniqueSurface`],
6//! [`AppendSurface`]) that collapse the 21 surfaces in [`crate::registry`]
7//! to a handful of generic patterns.
8//!
9//! # Phase 4 status
10//!
11//! Phase 4 is complete: the 21 surfaces dispatch through the family-ops
12//! traits in this module. [`crate::registrar::PluginRegistrar`] enqueues
13//! `Box<dyn DynPendingRegistration>` payloads; `PluginRegistry::apply_pending`
14//! calls `preflight` then `apply` per payload; [`PluginRegistry::remove_plugin`]
15//! walks the per-family `PluginRecord` fields and dispatches to
16//! `*Ops::remove` / `AppendOps::remove_plugin`. The legacy
17//! `PendingRegistration` enum and its three 25-arm matches are gone.
18//!
19//! # Family overview
20//!
21//! | Family          | Storage shape                              | Example surfaces                 |
22//! |-----------------|--------------------------------------------|----------------------------------|
23//! | Named-unique    | `DashMap<QName, Arc<Entry<K, Sig, P>>>`    | Scalar, Aggregate, Window, …     |
24//! | Versioned       | `DashMap<QName, Vec<Arc<Entry<…>>>>`       | Procedure (arity overload)       |
25//! | Keyed-unique    | `DashMap<K, Arc<dyn Provider>>`            | IndexKind, LabelStorage, …       |
26//! | Append          | `ArcSwap<Vec<Arc<dyn Provider>>>`          | Hook, OptimizerRule, …           |
27//!
28//! Append- and keyed-unique-family providers carry their key inside the
29//! trait (e.g. [`crate::traits::collation::CollationProvider::name`]); the
30//! [`KeyedUniqueSurface::key_of`] hook lets the registry derive a key from
31//! the provider when no explicit key is passed at registration time.
32
33// Rust guideline compliant
34
35use std::fmt::Debug;
36use std::hash::Hash;
37use std::sync::Arc;
38
39use arc_swap::ArcSwap;
40use dashmap::DashMap;
41use smol_str::SmolStr;
42
43use crate::capability::CapabilitySet;
44use crate::errors::PluginError;
45use crate::plugin::PluginId;
46use crate::qname::QName;
47use crate::registry::{
48    AggregateEntry, AlgorithmEntry, LocyAggregateEntry, LocyGeneratorEntry, LocyPredicateEntry,
49    PluginRecord, PluginRegistry, ProcedureEntry, ScalarEntry, WindowEntry,
50};
51use crate::traits::crdt::CrdtKind;
52use crate::traits::index::IndexKind;
53
54/// Discriminator that distinguishes overloads sharing one [`QName`].
55///
56/// Currently only `Arity` is used (by [`ProcedureSurface`] to disambiguate
57/// arity overloads); the variant is kept open so future versioned families
58/// (e.g. type-set overloads) can extend it without breaking call sites.
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
60pub enum Discriminator {
61    /// Positional-argument arity.
62    Arity(usize),
63}
64
65/// Enumeration of the 22 plugin surfaces.
66///
67/// Used by [`crate::registry::PluginRecordSnapshot`] accessors to filter the
68/// per-plugin footprint by surface.
69#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
70#[non_exhaustive]
71pub enum SurfaceKind {
72    /// `Capability::ScalarFn` — Cypher scalar function.
73    Scalar,
74    /// `Capability::AggregateFn` — Cypher aggregate function.
75    Aggregate,
76    /// `Capability::WindowFn` — Cypher window function.
77    Window,
78    /// `Capability::Procedure` — Cypher procedure (arity-overloaded).
79    Procedure,
80    /// `Capability::LocyAggregate` — Locy aggregate.
81    LocyAggregate,
82    /// `Capability::LocyPredicate` — Locy predicate.
83    LocyPredicate,
84    /// `Capability::LocyGenerator` — Locy generator predicate (table-valued).
85    LocyGenerator,
86    /// `Capability::Operator` — DataFusion optimizer rule.
87    OptimizerRule,
88    /// `Capability::Algorithm` — graph algorithm.
89    Algorithm,
90    /// `Capability::Index` — index kind provider.
91    IndexKind,
92    /// `Capability::Storage` — per-label plugin storage (M5h.2).
93    LabelStorage,
94    /// `Capability::Crdt` — CRDT kind provider.
95    Crdt,
96    /// `Capability::Hook` — session-lifecycle hook.
97    Hook,
98    /// `Capability::Type` — Arrow extension logical-type provider.
99    LogicalType,
100    /// `Capability::Auth` — authentication provider.
101    Auth,
102    /// `Capability::Authz` — authorization policy.
103    Authz,
104    /// `Capability::Trigger` — fine-grained trigger.
105    Trigger,
106    /// `Capability::Collation` — collation provider.
107    Collation,
108    /// `Capability::Cdc` — CDC output sink.
109    Cdc,
110    /// `Capability::Catalog` — catalog provider.
111    Catalog,
112    /// `Capability::Catalog` — replacement-scan provider.
113    ReplacementScan,
114    /// `Capability::BackgroundJob` — background-job provider.
115    BackgroundJob,
116}
117
118// ── Family traits ─────────────────────────────────────────────────────
119
120/// Named-unique family: `DashMap<QName, Arc<Entry<K, Sig, P>>>`.
121///
122/// One registration per qname; preflight rejects duplicates with
123/// [`PluginError::DuplicateRegistration`]. Members: Scalar, Aggregate,
124/// Window, LocyAggregate, LocyPredicate, LocyGenerator, Algorithm.
125pub trait NamedUniqueSurface: 'static {
126    /// The registered signature (e.g. `FnSignature`, `AggSignature`); unit
127    /// when the surface carries no signature (e.g. `LocyAggregate`).
128    type Sig: Send + Sync + 'static;
129    /// The trait-object provider type behind `Arc<dyn …>`.
130    type Provider: ?Sized + Send + Sync + 'static;
131
132    /// Surface discriminant for record keeping.
133    const KIND: SurfaceKind;
134}
135
136/// Versioned family: `DashMap<QName, Vec<Arc<Entry<K, Sig, P>>>>`.
137///
138/// Multiple registrations may share one qname, distinguished by a
139/// [`Discriminator`]. Only member today: Procedure (arity overload).
140pub trait VersionedSurface: 'static {
141    /// The registered signature.
142    type Sig: Send + Sync + 'static;
143    /// The trait-object provider.
144    type Provider: ?Sized + Send + Sync + 'static;
145
146    /// Surface discriminant.
147    const KIND: SurfaceKind;
148
149    /// Extract the per-overload discriminator from a signature so the
150    /// registry can de-duplicate within one qname.
151    fn discriminator(sig: &Self::Sig) -> Discriminator;
152}
153
154/// Keyed-unique family: `DashMap<K, Arc<dyn Provider>>`.
155///
156/// Distinct from named-unique because the key is **not** a [`QName`] — it
157/// may be a [`SmolStr`] scheme/name, an [`IndexKind`], a [`CrdtKind`], etc.
158/// The provider trait often exposes the key (e.g.
159/// [`crate::traits::collation::CollationProvider::name`]).
160///
161/// Members: IndexKind, LabelStorage, Crdt, LogicalType,
162/// Collation, Cdc, Catalog.
163pub trait KeyedUniqueSurface: 'static {
164    /// The key type the `DashMap` is keyed by.
165    type Key: Clone + Eq + Hash + Debug + Send + Sync + 'static;
166    /// The trait-object provider.
167    type Provider: ?Sized + Send + Sync + 'static;
168
169    /// Surface discriminant.
170    const KIND: SurfaceKind;
171
172    /// Preflight: refuse a duplicate key.
173    ///
174    /// Default implementation returns a generic
175    /// [`PluginError::Internal`] message; surfaces that need a typed
176    /// error may override this.
177    ///
178    /// # Errors
179    ///
180    /// Returns a [`PluginError`] when the key is already taken.
181    fn duplicate_error(key: &Self::Key) -> PluginError {
182        PluginError::internal(format!("{:?} `{:?}` already registered", Self::KIND, key))
183    }
184
185    /// Derive the registry key from the provider, when the provider trait
186    /// self-identifies.
187    ///
188    /// Returns `Some(key)` for surfaces whose provider exposes its key
189    /// directly (e.g. [`crate::traits::index::IndexKindProvider::kind`],
190    /// [`crate::traits::collation::CollationProvider::name`]). Returns
191    /// `None` for surfaces where the key must be supplied externally
192    /// (today only [`LabelStorageSurface`] — the label name is not a
193    /// property of the [`crate::traits::storage::Storage`] trait).
194    ///
195    /// Foundation tasks (§1.1 Phase 4 prerequisites) use this to drive
196    /// registration without an outer `(key, provider)` tuple wherever the
197    /// provider already self-identifies.
198    fn key_of(_provider: &Self::Provider) -> Option<Self::Key> {
199        None
200    }
201}
202
203/// Append family: `ArcSwap<Vec<Arc<dyn Provider>>>`.
204///
205/// No preflight de-duplication — every registration is appended. Removal
206/// is by plugin id (the append-family blanket impl filters the vector by
207/// plugin ownership). Members: OptimizerRule, Hook, Auth, Authz,
208/// Trigger, ReplacementScan, BackgroundJob.
209pub trait AppendSurface: 'static {
210    /// The trait-object provider.
211    type Provider: ?Sized + Send + Sync + 'static;
212
213    /// Surface discriminant.
214    const KIND: SurfaceKind;
215}
216
217/// Owner-tagged append entry stored in append-family slots.
218///
219/// The per-entry [`PluginId`] tag lets `AppendOps::remove_plugin` filter
220/// the slot in O(n) without a separate ownership index — closing the
221/// pre-Phase-4 "deferred to M5e" gap where append-family entries leaked
222/// across plugin removal and hot reload.
223pub struct AppendEntry<P: ?Sized> {
224    /// Owning plugin id.
225    pub plugin: PluginId,
226    /// The registered provider.
227    pub provider: Arc<P>,
228}
229
230impl<P: ?Sized> Clone for AppendEntry<P> {
231    fn clone(&self) -> Self {
232        Self {
233            plugin: self.plugin.clone(),
234            provider: Arc::clone(&self.provider),
235        }
236    }
237}
238
239impl<P: ?Sized> Debug for AppendEntry<P> {
240    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        f.debug_struct("AppendEntry")
242            .field("plugin", &self.plugin)
243            .finish_non_exhaustive()
244    }
245}
246
247// ── Marker types for the 21 surfaces ──────────────────────────────────
248//
249// Each marker is zero-sized; they exist only so `Surface` trait impls can
250// dispatch via the type system. Sub-phases 4b-4e add the per-marker impls.
251
252use crate::traits::aggregate::{AggSignature, AggregatePluginFn};
253use crate::traits::algorithm::AlgorithmProvider;
254use crate::traits::background::BackgroundJobProvider;
255use crate::traits::catalog::{CatalogProvider, ReplacementScanProvider};
256use crate::traits::cdc::CdcOutputProvider;
257use crate::traits::collation::CollationProvider;
258use crate::traits::connector::{AuthProvider, AuthzPolicy};
259use crate::traits::crdt::CrdtKindProvider;
260use crate::traits::hook::SessionHook;
261use crate::traits::index::IndexKindProvider;
262use crate::traits::locy::{
263    GenSignature, LocyAggregate, LocyGenerator, LocyPredicate, PredSignature,
264};
265use crate::traits::operator::OptimizerRuleProvider;
266use crate::traits::procedure::{ProcedurePlugin, ProcedureSignature};
267use crate::traits::scalar::{FnSignature, ScalarPluginFn};
268use crate::traits::storage::Storage;
269use crate::traits::trigger::TriggerPlugin;
270use crate::traits::types::LogicalTypeProvider;
271use crate::traits::window::{WindowPluginFn, WindowSignature};
272
273macro_rules! marker {
274    ($(#[$attr:meta])* $name:ident) => {
275        $(#[$attr])*
276        #[derive(Debug, Clone, Copy)]
277        pub struct $name;
278    };
279}
280
281// Named-unique markers (7).
282marker!(/// Marker for the Scalar surface. See [`NamedUniqueSurface`].
283ScalarSurface);
284marker!(/// Marker for the Aggregate surface. See [`NamedUniqueSurface`].
285AggregateSurface);
286marker!(/// Marker for the Window surface. See [`NamedUniqueSurface`].
287WindowSurface);
288marker!(/// Marker for the LocyAggregate surface. See [`NamedUniqueSurface`].
289LocyAggregateSurface);
290marker!(/// Marker for the LocyPredicate surface. See [`NamedUniqueSurface`].
291LocyPredicateSurface);
292marker!(/// Marker for the LocyGenerator surface. See [`NamedUniqueSurface`].
293LocyGeneratorSurface);
294marker!(/// Marker for the Algorithm surface. See [`NamedUniqueSurface`].
295AlgorithmSurface);
296
297// Versioned markers (1).
298marker!(/// Marker for the Procedure surface. See [`VersionedSurface`].
299ProcedureSurface);
300
301// Keyed-unique markers (7).
302marker!(/// Marker for the IndexKind surface. See [`KeyedUniqueSurface`].
303IndexKindSurface);
304marker!(/// Marker for the LabelStorage surface. See [`KeyedUniqueSurface`].
305LabelStorageSurface);
306marker!(/// Marker for the Crdt surface. See [`KeyedUniqueSurface`].
307CrdtSurface);
308marker!(/// Marker for the LogicalType surface. See [`KeyedUniqueSurface`].
309LogicalTypeSurface);
310marker!(/// Marker for the Collation surface. See [`KeyedUniqueSurface`].
311CollationSurface);
312marker!(/// Marker for the Cdc surface. See [`KeyedUniqueSurface`].
313CdcSurface);
314marker!(/// Marker for the Catalog surface. See [`KeyedUniqueSurface`].
315CatalogSurface);
316
317// Append markers (7).
318marker!(/// Marker for the OptimizerRule surface. See [`AppendSurface`].
319OptimizerRuleSurface);
320marker!(/// Marker for the Hook surface. See [`AppendSurface`].
321HookSurface);
322marker!(/// Marker for the Auth surface. See [`AppendSurface`].
323AuthSurface);
324marker!(/// Marker for the Authz surface. See [`AppendSurface`].
325AuthzSurface);
326marker!(/// Marker for the Trigger surface. See [`AppendSurface`].
327TriggerSurface);
328marker!(/// Marker for the ReplacementScan surface. See [`AppendSurface`].
329ReplacementScanSurface);
330marker!(/// Marker for the BackgroundJob surface. See [`AppendSurface`].
331BackgroundJobSurface);
332
333// ── Named-unique impls ────────────────────────────────────────────────
334
335impl NamedUniqueSurface for ScalarSurface {
336    type Sig = FnSignature;
337    type Provider = dyn ScalarPluginFn;
338    const KIND: SurfaceKind = SurfaceKind::Scalar;
339}
340
341impl NamedUniqueSurface for AggregateSurface {
342    type Sig = AggSignature;
343    type Provider = dyn AggregatePluginFn;
344    const KIND: SurfaceKind = SurfaceKind::Aggregate;
345}
346
347impl NamedUniqueSurface for WindowSurface {
348    type Sig = WindowSignature;
349    type Provider = dyn WindowPluginFn;
350    const KIND: SurfaceKind = SurfaceKind::Window;
351}
352
353impl NamedUniqueSurface for LocyAggregateSurface {
354    type Sig = ();
355    type Provider = dyn LocyAggregate;
356    const KIND: SurfaceKind = SurfaceKind::LocyAggregate;
357}
358
359impl NamedUniqueSurface for LocyPredicateSurface {
360    type Sig = PredSignature;
361    type Provider = dyn LocyPredicate;
362    const KIND: SurfaceKind = SurfaceKind::LocyPredicate;
363}
364
365impl NamedUniqueSurface for LocyGeneratorSurface {
366    type Sig = GenSignature;
367    type Provider = dyn LocyGenerator;
368    const KIND: SurfaceKind = SurfaceKind::LocyGenerator;
369}
370
371impl NamedUniqueSurface for AlgorithmSurface {
372    // The signature slot carries the registering plugin's effective
373    // capabilities so the stored entry can gate host graph access.
374    type Sig = CapabilitySet;
375    type Provider = dyn AlgorithmProvider;
376    const KIND: SurfaceKind = SurfaceKind::Algorithm;
377}
378
379// ── Versioned impls ───────────────────────────────────────────────────
380
381impl VersionedSurface for ProcedureSurface {
382    type Sig = ProcedureSignature;
383    type Provider = dyn ProcedurePlugin;
384    const KIND: SurfaceKind = SurfaceKind::Procedure;
385
386    fn discriminator(sig: &Self::Sig) -> Discriminator {
387        Discriminator::Arity(sig.args.len())
388    }
389}
390
391// ── Keyed-unique impls ────────────────────────────────────────────────
392
393impl KeyedUniqueSurface for IndexKindSurface {
394    type Key = IndexKind;
395    type Provider = dyn IndexKindProvider;
396    const KIND: SurfaceKind = SurfaceKind::IndexKind;
397
398    fn key_of(provider: &Self::Provider) -> Option<Self::Key> {
399        Some(provider.kind())
400    }
401}
402
403impl KeyedUniqueSurface for LabelStorageSurface {
404    type Key = SmolStr;
405    type Provider = dyn Storage;
406    const KIND: SurfaceKind = SurfaceKind::LabelStorage;
407
408    fn duplicate_error(key: &Self::Key) -> PluginError {
409        PluginError::internal(format!("label storage for `{key}` already registered"))
410    }
411
412    // No `key_of` override: the `Storage` trait does not self-identify a
413    // label. The label is supplied externally via the registration payload.
414}
415
416impl KeyedUniqueSurface for CrdtSurface {
417    type Key = CrdtKind;
418    type Provider = dyn CrdtKindProvider;
419    const KIND: SurfaceKind = SurfaceKind::Crdt;
420
421    fn duplicate_error(key: &Self::Key) -> PluginError {
422        PluginError::internal(format!("CRDT kind `{}` already registered", key.0))
423    }
424
425    fn key_of(provider: &Self::Provider) -> Option<Self::Key> {
426        Some(provider.kind())
427    }
428}
429
430impl KeyedUniqueSurface for LogicalTypeSurface {
431    type Key = SmolStr;
432    type Provider = dyn LogicalTypeProvider;
433    const KIND: SurfaceKind = SurfaceKind::LogicalType;
434
435    fn key_of(provider: &Self::Provider) -> Option<Self::Key> {
436        Some(SmolStr::new(provider.name()))
437    }
438}
439
440impl KeyedUniqueSurface for CollationSurface {
441    type Key = SmolStr;
442    type Provider = dyn CollationProvider;
443    const KIND: SurfaceKind = SurfaceKind::Collation;
444
445    fn key_of(provider: &Self::Provider) -> Option<Self::Key> {
446        Some(SmolStr::new(provider.name()))
447    }
448}
449
450impl KeyedUniqueSurface for CdcSurface {
451    type Key = SmolStr;
452    type Provider = dyn CdcOutputProvider;
453    const KIND: SurfaceKind = SurfaceKind::Cdc;
454
455    fn key_of(provider: &Self::Provider) -> Option<Self::Key> {
456        Some(SmolStr::new(provider.name()))
457    }
458}
459
460impl KeyedUniqueSurface for CatalogSurface {
461    type Key = SmolStr;
462    type Provider = dyn CatalogProvider;
463    const KIND: SurfaceKind = SurfaceKind::Catalog;
464
465    fn key_of(provider: &Self::Provider) -> Option<Self::Key> {
466        Some(SmolStr::new(provider.name()))
467    }
468}
469
470// ── Append impls ──────────────────────────────────────────────────────
471
472impl AppendSurface for OptimizerRuleSurface {
473    type Provider = dyn OptimizerRuleProvider;
474    const KIND: SurfaceKind = SurfaceKind::OptimizerRule;
475}
476
477impl AppendSurface for HookSurface {
478    type Provider = dyn SessionHook;
479    const KIND: SurfaceKind = SurfaceKind::Hook;
480}
481
482impl AppendSurface for AuthSurface {
483    type Provider = dyn AuthProvider;
484    const KIND: SurfaceKind = SurfaceKind::Auth;
485}
486
487impl AppendSurface for AuthzSurface {
488    type Provider = dyn AuthzPolicy;
489    const KIND: SurfaceKind = SurfaceKind::Authz;
490}
491
492impl AppendSurface for TriggerSurface {
493    type Provider = dyn TriggerPlugin;
494    const KIND: SurfaceKind = SurfaceKind::Trigger;
495}
496
497impl AppendSurface for ReplacementScanSurface {
498    type Provider = dyn ReplacementScanProvider;
499    const KIND: SurfaceKind = SurfaceKind::ReplacementScan;
500}
501
502impl AppendSurface for BackgroundJobSurface {
503    type Provider = dyn BackgroundJobProvider;
504    const KIND: SurfaceKind = SurfaceKind::BackgroundJob;
505}
506
507// ── Family-ops traits ────────────────────────────────────────────────
508//
509// Each `*Ops` trait carries the storage-slot + record-slot accessors and
510// the preflight/insert/remove dispatch methods that
511// [`PluginRegistry::apply_pending`] and [`PluginRegistry::remove_plugin`]
512// call into. One impl per marker keeps the registration codepath fully
513// type-driven — adding a surface means adding a marker + an ops impl,
514// not editing four 25-arm matches.
515
516/// Named-unique dispatch operations.
517///
518/// One impl per [`NamedUniqueSurface`] marker. The associated `Stored`
519/// type captures whether the surface wraps its provider in a typed
520/// `Entry` struct (Scalar/Aggregate/Window/LocyAggregate/LocyPredicate)
521/// or stores `Arc<dyn Provider>` directly (Algorithm).
522pub(crate) trait NamedUniqueOps: NamedUniqueSurface {
523    /// The value stored in the `DashMap` slot (e.g. `Arc<ScalarEntry>`
524    /// or `Arc<dyn AlgorithmProvider>`).
525    type Stored: Clone + Send + Sync + 'static;
526
527    /// Build the stored value from the registration triple.
528    fn make_stored(plugin: PluginId, sig: Self::Sig, provider: Arc<Self::Provider>)
529    -> Self::Stored;
530
531    /// The registry slot for this surface.
532    fn slot(registry: &PluginRegistry) -> &DashMap<QName, Self::Stored>;
533
534    /// The per-plugin record slot that lists the qnames this plugin owns
535    /// on this surface.
536    fn record_slot(record: &mut PluginRecord) -> &mut Vec<QName>;
537
538    /// Reject a duplicate registration.
539    ///
540    /// # Errors
541    ///
542    /// Returns [`PluginError::DuplicateRegistration`] when `q` is already
543    /// registered on this surface.
544    fn preflight(registry: &PluginRegistry, q: &QName) -> Result<(), PluginError> {
545        if Self::slot(registry).contains_key(q) {
546            return Err(PluginError::DuplicateRegistration(q.clone()));
547        }
548        Ok(())
549    }
550
551    /// Insert the registration into the slot and record this plugin's
552    /// ownership.
553    fn insert(
554        registry: &PluginRegistry,
555        plugin: PluginId,
556        q: QName,
557        sig: Self::Sig,
558        provider: Arc<Self::Provider>,
559        record: &mut PluginRecord,
560    ) {
561        let stored = Self::make_stored(plugin, sig, provider);
562        Self::slot(registry).insert(q.clone(), stored);
563        Self::record_slot(record).push(q);
564    }
565
566    /// Remove the entry at `q` from the slot.
567    fn remove(registry: &PluginRegistry, q: &QName) {
568        Self::slot(registry).remove(q);
569    }
570}
571
572/// Versioned dispatch operations (only [`ProcedureSurface`] today).
573///
574/// Versioned slots hold `Vec<Arc<Entry>>` per qname; preflight rejects a
575/// new registration whose discriminator collides with an existing one.
576pub(crate) trait VersionedOps: VersionedSurface {
577    /// The per-overload entry (e.g. `Arc<ProcedureEntry>`).
578    type Stored: Clone + Send + Sync + 'static;
579
580    /// Build the stored entry from the registration triple.
581    fn make_stored(plugin: PluginId, sig: Self::Sig, provider: Arc<Self::Provider>)
582    -> Self::Stored;
583
584    /// Read the discriminator off a stored entry (for conflict detection
585    /// against a new registration's discriminator).
586    fn entry_discriminator(stored: &Self::Stored) -> Discriminator;
587
588    /// Read the discriminator off a fresh signature.
589    fn signature_discriminator(sig: &Self::Sig) -> Discriminator {
590        Self::discriminator(sig)
591    }
592
593    /// The registry slot for this surface.
594    fn slot(registry: &PluginRegistry) -> &DashMap<QName, Vec<Self::Stored>>;
595
596    /// The per-plugin record slot — (qname, discriminator-as-usize) pairs
597    /// so removal can drop just this plugin's overloads.
598    fn record_slot(record: &mut PluginRecord) -> &mut Vec<(QName, usize)>;
599
600    /// Convert a [`Discriminator`] to the usize used in `PluginRecord`.
601    fn discriminator_to_usize(d: Discriminator) -> usize {
602        match d {
603            Discriminator::Arity(n) => n,
604        }
605    }
606
607    /// Reject a duplicate registration *at the same discriminator*.
608    ///
609    /// Different discriminators for the same qname coexist by design.
610    ///
611    /// # Errors
612    ///
613    /// Returns [`PluginError::DuplicateRegistration`] when an entry with
614    /// the same discriminator already exists under `q`.
615    fn preflight(registry: &PluginRegistry, q: &QName, sig: &Self::Sig) -> Result<(), PluginError> {
616        let d = Self::signature_discriminator(sig);
617        if let Some(slot) = Self::slot(registry).get(q)
618            && slot.iter().any(|e| Self::entry_discriminator(e) == d)
619        {
620            return Err(PluginError::DuplicateRegistration(q.clone()));
621        }
622        Ok(())
623    }
624
625    /// Append the registration to the slot and record this plugin's
626    /// (qname, discriminator) entry.
627    fn insert(
628        registry: &PluginRegistry,
629        plugin: PluginId,
630        q: QName,
631        sig: Self::Sig,
632        provider: Arc<Self::Provider>,
633        record: &mut PluginRecord,
634    ) {
635        let d = Self::signature_discriminator(&sig);
636        let stored = Self::make_stored(plugin, sig, provider);
637        let mut entry = Self::slot(registry).entry(q.clone()).or_default();
638        entry.push(stored);
639        drop(entry);
640        Self::record_slot(record).push((q, Self::discriminator_to_usize(d)));
641    }
642
643    /// Drop the overload identified by `(q, d)` from the slot, removing
644    /// the qname entry entirely once its overload list is empty.
645    fn remove(registry: &PluginRegistry, q: &QName, d: Discriminator) {
646        let slot = Self::slot(registry);
647        if let Some(mut entry) = slot.get_mut(q) {
648            entry.retain(|e| Self::entry_discriminator(e) != d);
649            let empty = entry.is_empty();
650            drop(entry);
651            if empty {
652                slot.remove(q);
653            }
654        }
655    }
656}
657
658/// Keyed-unique dispatch operations.
659///
660/// `record_register` / `record_unregister` are abstract so surfaces that
661/// track per-key footprint in `PluginRecord` (Vec<Key>) and surfaces that
662/// track only a count (Vec<()>-shaped counter) share the same dispatch.
663pub(crate) trait KeyedUniqueOps: KeyedUniqueSurface {
664    /// The registry slot for this surface.
665    fn slot(registry: &PluginRegistry) -> &DashMap<Self::Key, Arc<Self::Provider>>;
666
667    /// Note `key` as owned by this plugin in `record`.
668    fn record_register(record: &mut PluginRecord, key: &Self::Key);
669
670    /// Reject a duplicate key.
671    ///
672    /// # Errors
673    ///
674    /// Returns [`KeyedUniqueSurface::duplicate_error`] when `key` is
675    /// already registered.
676    fn preflight(registry: &PluginRegistry, key: &Self::Key) -> Result<(), PluginError> {
677        if Self::slot(registry).contains_key(key) {
678            return Err(Self::duplicate_error(key));
679        }
680        Ok(())
681    }
682
683    /// Insert the (key, provider) pair into the slot and record this
684    /// plugin's ownership.
685    fn insert(
686        registry: &PluginRegistry,
687        key: Self::Key,
688        provider: Arc<Self::Provider>,
689        record: &mut PluginRecord,
690    ) {
691        Self::slot(registry).insert(key.clone(), provider);
692        Self::record_register(record, &key);
693    }
694
695    /// Remove the entry at `key` from the slot.
696    fn remove(registry: &PluginRegistry, key: &Self::Key) {
697        Self::slot(registry).remove(key);
698    }
699}
700
701/// Append dispatch operations.
702///
703/// Append-family removal filters the slot by [`PluginId`] using the
704/// `AppendEntry<P>` owner tag — closes the legacy "M5e deferred"
705/// remove-plugin gap.
706pub(crate) trait AppendOps: AppendSurface {
707    /// The registry slot for this surface.
708    fn slot(registry: &PluginRegistry) -> &ArcSwap<Vec<AppendEntry<Self::Provider>>>;
709
710    /// Increment the per-plugin counter in `record`.
711    fn record_register(record: &mut PluginRecord);
712
713    /// Append the (plugin, provider) entry via copy-on-write.
714    fn insert(
715        registry: &PluginRegistry,
716        plugin: PluginId,
717        provider: Arc<Self::Provider>,
718        record: &mut PluginRecord,
719    ) {
720        let slot = Self::slot(registry);
721        let mut v = (**slot.load()).clone();
722        v.push(AppendEntry { plugin, provider });
723        slot.store(Arc::new(v));
724        Self::record_register(record);
725    }
726
727    /// Drop every entry owned by `plugin` from the slot.
728    fn remove_plugin(registry: &PluginRegistry, plugin: &PluginId) {
729        let slot = Self::slot(registry);
730        let cur = slot.load();
731        if !cur.iter().any(|e| &e.plugin == plugin) {
732            return;
733        }
734        let v: Vec<AppendEntry<Self::Provider>> = cur
735            .iter()
736            .filter(|e| &e.plugin != plugin)
737            .cloned()
738            .collect();
739        slot.store(Arc::new(v));
740    }
741}
742
743// ── NamedUniqueOps impls ─────────────────────────────────────────────
744
745impl NamedUniqueOps for ScalarSurface {
746    type Stored = Arc<ScalarEntry>;
747    fn make_stored(
748        plugin: PluginId,
749        sig: Self::Sig,
750        provider: Arc<Self::Provider>,
751    ) -> Self::Stored {
752        Arc::new(ScalarEntry {
753            plugin,
754            signature: sig,
755            function: provider,
756        })
757    }
758    fn slot(r: &PluginRegistry) -> &DashMap<QName, Self::Stored> {
759        &r.scalars
760    }
761    fn record_slot(rec: &mut PluginRecord) -> &mut Vec<QName> {
762        &mut rec.scalars
763    }
764}
765
766impl NamedUniqueOps for AggregateSurface {
767    type Stored = Arc<AggregateEntry>;
768    fn make_stored(
769        plugin: PluginId,
770        sig: Self::Sig,
771        provider: Arc<Self::Provider>,
772    ) -> Self::Stored {
773        Arc::new(AggregateEntry {
774            plugin,
775            signature: sig,
776            aggregate: provider,
777        })
778    }
779    fn slot(r: &PluginRegistry) -> &DashMap<QName, Self::Stored> {
780        &r.aggregates
781    }
782    fn record_slot(rec: &mut PluginRecord) -> &mut Vec<QName> {
783        &mut rec.aggregates
784    }
785}
786
787impl NamedUniqueOps for WindowSurface {
788    type Stored = Arc<WindowEntry>;
789    fn make_stored(
790        plugin: PluginId,
791        sig: Self::Sig,
792        provider: Arc<Self::Provider>,
793    ) -> Self::Stored {
794        Arc::new(WindowEntry {
795            plugin,
796            signature: sig,
797            window: provider,
798        })
799    }
800    fn slot(r: &PluginRegistry) -> &DashMap<QName, Self::Stored> {
801        &r.windows
802    }
803    fn record_slot(rec: &mut PluginRecord) -> &mut Vec<QName> {
804        &mut rec.windows
805    }
806}
807
808impl NamedUniqueOps for LocyAggregateSurface {
809    type Stored = Arc<LocyAggregateEntry>;
810    fn make_stored(
811        plugin: PluginId,
812        _sig: Self::Sig,
813        provider: Arc<Self::Provider>,
814    ) -> Self::Stored {
815        Arc::new(LocyAggregateEntry {
816            plugin,
817            aggregate: provider,
818        })
819    }
820    fn slot(r: &PluginRegistry) -> &DashMap<QName, Self::Stored> {
821        &r.locy_aggregates
822    }
823    fn record_slot(rec: &mut PluginRecord) -> &mut Vec<QName> {
824        &mut rec.locy_aggregates
825    }
826}
827
828impl NamedUniqueOps for LocyPredicateSurface {
829    type Stored = Arc<LocyPredicateEntry>;
830    fn make_stored(
831        plugin: PluginId,
832        sig: Self::Sig,
833        provider: Arc<Self::Provider>,
834    ) -> Self::Stored {
835        Arc::new(LocyPredicateEntry {
836            plugin,
837            signature: sig,
838            predicate: provider,
839        })
840    }
841    fn slot(r: &PluginRegistry) -> &DashMap<QName, Self::Stored> {
842        &r.locy_predicates
843    }
844    fn record_slot(rec: &mut PluginRecord) -> &mut Vec<QName> {
845        &mut rec.locy_predicates
846    }
847}
848
849impl NamedUniqueOps for LocyGeneratorSurface {
850    type Stored = Arc<LocyGeneratorEntry>;
851    fn make_stored(
852        plugin: PluginId,
853        sig: Self::Sig,
854        provider: Arc<Self::Provider>,
855    ) -> Self::Stored {
856        Arc::new(LocyGeneratorEntry {
857            plugin,
858            signature: sig,
859            generator: provider,
860        })
861    }
862    fn slot(r: &PluginRegistry) -> &DashMap<QName, Self::Stored> {
863        &r.locy_generators
864    }
865    fn record_slot(rec: &mut PluginRecord) -> &mut Vec<QName> {
866        &mut rec.locy_generators
867    }
868}
869
870impl NamedUniqueOps for AlgorithmSurface {
871    type Stored = Arc<AlgorithmEntry>;
872    fn make_stored(
873        plugin: PluginId,
874        sig: Self::Sig,
875        provider: Arc<Self::Provider>,
876    ) -> Self::Stored {
877        Arc::new(AlgorithmEntry {
878            plugin,
879            effective_caps: sig,
880            provider,
881        })
882    }
883    fn slot(r: &PluginRegistry) -> &DashMap<QName, Self::Stored> {
884        &r.algorithms
885    }
886    fn record_slot(rec: &mut PluginRecord) -> &mut Vec<QName> {
887        &mut rec.algorithms
888    }
889}
890
891// ── VersionedOps impl ────────────────────────────────────────────────
892
893impl VersionedOps for ProcedureSurface {
894    type Stored = Arc<ProcedureEntry>;
895    fn make_stored(
896        plugin: PluginId,
897        sig: Self::Sig,
898        provider: Arc<Self::Provider>,
899    ) -> Self::Stored {
900        Arc::new(ProcedureEntry {
901            plugin,
902            signature: sig,
903            procedure: provider,
904        })
905    }
906    fn entry_discriminator(stored: &Self::Stored) -> Discriminator {
907        Discriminator::Arity(stored.signature.args.len())
908    }
909    fn slot(r: &PluginRegistry) -> &DashMap<QName, Vec<Self::Stored>> {
910        &r.procedures
911    }
912    fn record_slot(rec: &mut PluginRecord) -> &mut Vec<(QName, usize)> {
913        &mut rec.procedures
914    }
915}
916
917// ── KeyedUniqueOps impls ─────────────────────────────────────────────
918
919impl KeyedUniqueOps for IndexKindSurface {
920    fn slot(r: &PluginRegistry) -> &DashMap<Self::Key, Arc<Self::Provider>> {
921        &r.index_kinds
922    }
923    fn record_register(rec: &mut PluginRecord, key: &Self::Key) {
924        rec.index_kinds.push(key.clone());
925    }
926}
927
928impl KeyedUniqueOps for LabelStorageSurface {
929    fn slot(r: &PluginRegistry) -> &DashMap<Self::Key, Arc<Self::Provider>> {
930        &r.label_storages
931    }
932    fn record_register(rec: &mut PluginRecord, key: &Self::Key) {
933        rec.label_storages.push(key.clone());
934    }
935}
936
937impl KeyedUniqueOps for CrdtSurface {
938    fn slot(r: &PluginRegistry) -> &DashMap<Self::Key, Arc<Self::Provider>> {
939        &r.crdt_kinds
940    }
941    fn record_register(rec: &mut PluginRecord, key: &Self::Key) {
942        rec.crdt_kinds.push(key.clone());
943    }
944}
945
946impl KeyedUniqueOps for LogicalTypeSurface {
947    fn slot(r: &PluginRegistry) -> &DashMap<Self::Key, Arc<Self::Provider>> {
948        &r.logical_types
949    }
950    fn record_register(rec: &mut PluginRecord, key: &Self::Key) {
951        rec.logical_types.push(key.clone());
952    }
953}
954
955impl KeyedUniqueOps for CollationSurface {
956    fn slot(r: &PluginRegistry) -> &DashMap<Self::Key, Arc<Self::Provider>> {
957        &r.collations
958    }
959    fn record_register(rec: &mut PluginRecord, key: &Self::Key) {
960        rec.collations.push(key.clone());
961    }
962}
963
964impl KeyedUniqueOps for CdcSurface {
965    fn slot(r: &PluginRegistry) -> &DashMap<Self::Key, Arc<Self::Provider>> {
966        &r.cdc_outputs
967    }
968    fn record_register(rec: &mut PluginRecord, key: &Self::Key) {
969        rec.cdc_outputs.push(key.clone());
970    }
971}
972
973impl KeyedUniqueOps for CatalogSurface {
974    fn slot(r: &PluginRegistry) -> &DashMap<Self::Key, Arc<Self::Provider>> {
975        &r.catalogs
976    }
977    fn record_register(rec: &mut PluginRecord, key: &Self::Key) {
978        rec.catalogs.push(key.clone());
979    }
980}
981
982// ── AppendOps impls ──────────────────────────────────────────────────
983
984impl AppendOps for OptimizerRuleSurface {
985    fn slot(r: &PluginRegistry) -> &ArcSwap<Vec<AppendEntry<Self::Provider>>> {
986        &r.optimizer_rules
987    }
988    fn record_register(rec: &mut PluginRecord) {
989        rec.optimizer_rule_count += 1;
990    }
991}
992impl AppendOps for HookSurface {
993    fn slot(r: &PluginRegistry) -> &ArcSwap<Vec<AppendEntry<Self::Provider>>> {
994        &r.hooks
995    }
996    fn record_register(rec: &mut PluginRecord) {
997        rec.hook_count += 1;
998    }
999}
1000impl AppendOps for AuthSurface {
1001    fn slot(r: &PluginRegistry) -> &ArcSwap<Vec<AppendEntry<Self::Provider>>> {
1002        &r.auth_providers
1003    }
1004    fn record_register(rec: &mut PluginRecord) {
1005        rec.auth_count += 1;
1006    }
1007}
1008impl AppendOps for AuthzSurface {
1009    fn slot(r: &PluginRegistry) -> &ArcSwap<Vec<AppendEntry<Self::Provider>>> {
1010        &r.authz_policies
1011    }
1012    fn record_register(rec: &mut PluginRecord) {
1013        rec.authz_count += 1;
1014    }
1015}
1016impl AppendOps for TriggerSurface {
1017    fn slot(r: &PluginRegistry) -> &ArcSwap<Vec<AppendEntry<Self::Provider>>> {
1018        &r.triggers
1019    }
1020    fn record_register(rec: &mut PluginRecord) {
1021        rec.trigger_count += 1;
1022    }
1023}
1024impl AppendOps for ReplacementScanSurface {
1025    fn slot(r: &PluginRegistry) -> &ArcSwap<Vec<AppendEntry<Self::Provider>>> {
1026        &r.replacement_scans
1027    }
1028    fn record_register(rec: &mut PluginRecord) {
1029        rec.replacement_scan_count += 1;
1030    }
1031}
1032impl AppendOps for BackgroundJobSurface {
1033    fn slot(r: &PluginRegistry) -> &ArcSwap<Vec<AppendEntry<Self::Provider>>> {
1034        &r.background_jobs
1035    }
1036    fn record_register(rec: &mut PluginRecord) {
1037        rec.background_job_count += 1;
1038    }
1039}
1040
1041// ── DynPendingRegistration ───────────────────────────────────────────
1042//
1043// Object-safe wrapper used by heterogeneous batch flows (e.g.
1044// `Loader::prepare` collecting registrations from manifest-driven
1045// adapters). The static-dispatch `*Ops::insert` path is preferred where
1046// the surface type is known at the call site (no boxing); this trait
1047// covers the case where the call site holds a `Vec<Box<dyn …>>`.
1048
1049/// Object-safe handle to a queued plugin registration.
1050///
1051/// Implementors are the four per-family payload structs:
1052/// [`NamedUniqueReg`], [`VersionedReg`], [`KeyedUniqueReg`],
1053/// [`AppendReg`]. Each owns the registration data and dispatches through
1054/// its family's static ops trait.
1055pub(crate) trait DynPendingRegistration: Send + Sync {
1056    /// Surface this registration targets. Diagnostic-only.
1057    #[allow(
1058        dead_code,
1059        reason = "Diagnostic surface; exercised by tests and future debug paths."
1060    )]
1061    fn kind(&self) -> SurfaceKind;
1062    /// Preflight against the live registry.
1063    fn preflight(&self, registry: &PluginRegistry) -> Result<(), PluginError>;
1064    /// Apply the registration to the registry and the per-plugin record.
1065    fn apply(
1066        self: Box<Self>,
1067        registry: &PluginRegistry,
1068        plugin: PluginId,
1069        record: &mut PluginRecord,
1070    );
1071    /// Short human-readable label (for error/debug messages). Diagnostic-only.
1072    #[allow(dead_code, reason = "Diagnostic surface for future error formatting.")]
1073    fn debug_label(&self) -> String;
1074
1075    /// The qname a UNIQUE registration claims (for intra-batch duplicate
1076    /// detection), or `None` for repeatable (append) surfaces. Two pending
1077    /// registrations claiming the same qname within one `register()` batch
1078    /// collide even though neither yet exists in the live registry — `preflight`
1079    /// alone (which only consults the live registry) would miss them.
1080    fn dedup_key(&self) -> Option<QName> {
1081        None
1082    }
1083}
1084
1085/// Heterogeneous-batch payload for a [`NamedUniqueOps`] registration.
1086pub(crate) struct NamedUniqueReg<S: NamedUniqueOps> {
1087    /// Qualified name to register under.
1088    pub q: QName,
1089    /// Signature carried by the registration.
1090    pub sig: S::Sig,
1091    /// The trait-object provider.
1092    pub provider: Arc<S::Provider>,
1093}
1094
1095impl<S> DynPendingRegistration for NamedUniqueReg<S>
1096where
1097    S: NamedUniqueOps + 'static,
1098    S::Sig: Send + Sync,
1099{
1100    fn kind(&self) -> SurfaceKind {
1101        S::KIND
1102    }
1103    fn preflight(&self, registry: &PluginRegistry) -> Result<(), PluginError> {
1104        S::preflight(registry, &self.q)
1105    }
1106    fn apply(
1107        self: Box<Self>,
1108        registry: &PluginRegistry,
1109        plugin: PluginId,
1110        record: &mut PluginRecord,
1111    ) {
1112        S::insert(registry, plugin, self.q, self.sig, self.provider, record);
1113    }
1114    fn debug_label(&self) -> String {
1115        format!("{:?}({})", S::KIND, self.q)
1116    }
1117    fn dedup_key(&self) -> Option<QName> {
1118        // Name-unique surface: the qname must be unique across the batch.
1119        Some(self.q.clone())
1120    }
1121}
1122
1123/// Heterogeneous-batch payload for a [`VersionedOps`] registration.
1124pub(crate) struct VersionedReg<S: VersionedOps> {
1125    /// Qualified name to register under.
1126    pub q: QName,
1127    /// Signature carried by the registration.
1128    pub sig: S::Sig,
1129    /// The trait-object provider.
1130    pub provider: Arc<S::Provider>,
1131}
1132
1133impl<S> DynPendingRegistration for VersionedReg<S>
1134where
1135    S: VersionedOps + 'static,
1136    S::Sig: Send + Sync,
1137{
1138    fn kind(&self) -> SurfaceKind {
1139        S::KIND
1140    }
1141    fn preflight(&self, registry: &PluginRegistry) -> Result<(), PluginError> {
1142        S::preflight(registry, &self.q, &self.sig)
1143    }
1144    fn apply(
1145        self: Box<Self>,
1146        registry: &PluginRegistry,
1147        plugin: PluginId,
1148        record: &mut PluginRecord,
1149    ) {
1150        S::insert(registry, plugin, self.q, self.sig, self.provider, record);
1151    }
1152    fn debug_label(&self) -> String {
1153        format!("{:?}({})", S::KIND, self.q)
1154    }
1155}
1156
1157/// Heterogeneous-batch payload for a [`KeyedUniqueOps`] registration.
1158///
1159/// `key_override` is `Some` only for surfaces whose provider trait does
1160/// not self-identify a key (today only [`LabelStorageSurface`]). For
1161/// every other surface, `key_override` is `None` and the key is derived
1162/// via [`KeyedUniqueSurface::key_of`].
1163pub(crate) struct KeyedUniqueReg<S: KeyedUniqueOps> {
1164    /// Optional explicit key; used when the provider trait can't
1165    /// self-identify (e.g. `LabelStorageSurface`).
1166    pub key_override: Option<S::Key>,
1167    /// The trait-object provider.
1168    pub provider: Arc<S::Provider>,
1169}
1170
1171impl<S> KeyedUniqueReg<S>
1172where
1173    S: KeyedUniqueOps,
1174{
1175    /// Resolve the key from `key_override` or [`KeyedUniqueSurface::key_of`].
1176    ///
1177    /// # Errors
1178    ///
1179    /// Returns a [`PluginError`] when no explicit key was supplied *and*
1180    /// the surface's provider trait does not self-identify a key.
1181    pub fn resolve_key(&self) -> Result<S::Key, PluginError> {
1182        if let Some(ref k) = self.key_override {
1183            return Ok(k.clone());
1184        }
1185        S::key_of(&*self.provider).ok_or_else(|| {
1186            PluginError::internal(format!(
1187                "{:?} registration missing explicit key (provider does not self-identify)",
1188                S::KIND
1189            ))
1190        })
1191    }
1192}
1193
1194impl<S> DynPendingRegistration for KeyedUniqueReg<S>
1195where
1196    S: KeyedUniqueOps + 'static,
1197{
1198    fn kind(&self) -> SurfaceKind {
1199        S::KIND
1200    }
1201    fn preflight(&self, registry: &PluginRegistry) -> Result<(), PluginError> {
1202        let key = self.resolve_key()?;
1203        S::preflight(registry, &key)
1204    }
1205    fn apply(
1206        self: Box<Self>,
1207        registry: &PluginRegistry,
1208        _plugin: PluginId,
1209        record: &mut PluginRecord,
1210    ) {
1211        // `_plugin` is unused here because keyed-unique slots store
1212        // `Arc<dyn Provider>` directly (no per-entry ownership tag —
1213        // ownership is reconstructed from `PluginRecord` on removal).
1214        let key = match self.resolve_key() {
1215            Ok(k) => k,
1216            Err(_) => return, // preflight would have rejected; defensive.
1217        };
1218        S::insert(registry, key, self.provider, record);
1219    }
1220    fn debug_label(&self) -> String {
1221        let k = self
1222            .resolve_key()
1223            .map(|k| format!("{k:?}"))
1224            .unwrap_or_else(|_| "<unresolved>".into());
1225        format!("{:?}({k})", S::KIND)
1226    }
1227}
1228
1229/// Heterogeneous-batch payload for an [`AppendOps`] registration.
1230pub(crate) struct AppendReg<S: AppendOps> {
1231    /// The trait-object provider.
1232    pub provider: Arc<S::Provider>,
1233}
1234
1235impl<S> DynPendingRegistration for AppendReg<S>
1236where
1237    S: AppendOps + 'static,
1238{
1239    fn kind(&self) -> SurfaceKind {
1240        S::KIND
1241    }
1242    fn preflight(&self, _registry: &PluginRegistry) -> Result<(), PluginError> {
1243        Ok(())
1244    }
1245    fn apply(
1246        self: Box<Self>,
1247        registry: &PluginRegistry,
1248        plugin: PluginId,
1249        record: &mut PluginRecord,
1250    ) {
1251        S::insert(registry, plugin, self.provider, record);
1252    }
1253    fn debug_label(&self) -> String {
1254        format!("{:?}", S::KIND)
1255    }
1256}
1257
1258#[cfg(test)]
1259mod tests {
1260    use super::*;
1261
1262    #[test]
1263    fn surface_kind_count_matches_design() {
1264        // Compile-time check: each marker's KIND is unique.
1265        let kinds = [
1266            <ScalarSurface as NamedUniqueSurface>::KIND,
1267            <AggregateSurface as NamedUniqueSurface>::KIND,
1268            <WindowSurface as NamedUniqueSurface>::KIND,
1269            <LocyAggregateSurface as NamedUniqueSurface>::KIND,
1270            <LocyPredicateSurface as NamedUniqueSurface>::KIND,
1271            <LocyGeneratorSurface as NamedUniqueSurface>::KIND,
1272            <AlgorithmSurface as NamedUniqueSurface>::KIND,
1273            <ProcedureSurface as VersionedSurface>::KIND,
1274            <IndexKindSurface as KeyedUniqueSurface>::KIND,
1275            <LabelStorageSurface as KeyedUniqueSurface>::KIND,
1276            <CrdtSurface as KeyedUniqueSurface>::KIND,
1277            <LogicalTypeSurface as KeyedUniqueSurface>::KIND,
1278            <CollationSurface as KeyedUniqueSurface>::KIND,
1279            <CdcSurface as KeyedUniqueSurface>::KIND,
1280            <CatalogSurface as KeyedUniqueSurface>::KIND,
1281            <OptimizerRuleSurface as AppendSurface>::KIND,
1282            <HookSurface as AppendSurface>::KIND,
1283            <AuthSurface as AppendSurface>::KIND,
1284            <AuthzSurface as AppendSurface>::KIND,
1285            <TriggerSurface as AppendSurface>::KIND,
1286            <ReplacementScanSurface as AppendSurface>::KIND,
1287            <BackgroundJobSurface as AppendSurface>::KIND,
1288        ];
1289        // 22 surfaces enumerated above (Scalar+Aggregate+Window+Procedure
1290        // +LocyAggregate+LocyPredicate+LocyGenerator+OptimizerRule+Algorithm
1291        // +IndexKind+LabelStorage+Crdt+Hook+LogicalType+Auth+Authz+Trigger
1292        // +Collation+Cdc+Catalog+ReplacementScan+BackgroundJob = 22 visible markers).
1293        // The 3.0 breaking change removed the four dead registrable surfaces
1294        // Operator, Pregel, StorageBackend, and Connector.
1295        assert_eq!(kinds.len(), 22);
1296        let mut sorted: Vec<_> = kinds.iter().collect();
1297        sorted.sort_by_key(|k| format!("{k:?}"));
1298        sorted.dedup();
1299        assert_eq!(sorted.len(), 22, "duplicate SurfaceKind in markers");
1300    }
1301
1302    #[test]
1303    fn keyed_unique_default_duplicate_error_is_internal() {
1304        let err = <LogicalTypeSurface as KeyedUniqueSurface>::duplicate_error(&SmolStr::new("x"));
1305        assert!(matches!(err, PluginError::Internal(_)));
1306    }
1307
1308    // ── Foundation ops-trait tests ───────────────────────────────────
1309
1310    struct NoopHook;
1311    impl crate::traits::hook::SessionHook for NoopHook {}
1312
1313    fn pid(s: &str) -> PluginId {
1314        PluginId::new(s)
1315    }
1316
1317    #[test]
1318    fn append_ops_insert_and_remove_round_trip() {
1319        // F3 regression: closes the legacy "deferred to M5e" gap in
1320        // `PluginRegistry::remove_plugin` — append-family entries were
1321        // never dropped before.
1322        let registry = PluginRegistry::new();
1323        let mut record_a = PluginRecord::default();
1324        let mut record_b = PluginRecord::default();
1325        <HookSurface as AppendOps>::insert(&registry, pid("a"), Arc::new(NoopHook), &mut record_a);
1326        <HookSurface as AppendOps>::insert(&registry, pid("b"), Arc::new(NoopHook), &mut record_b);
1327        assert_eq!(registry.hooks().len(), 2);
1328        assert_eq!(record_a.hook_count, 1);
1329        assert_eq!(record_b.hook_count, 1);
1330
1331        <HookSurface as AppendOps>::remove_plugin(&registry, &pid("a"));
1332        assert_eq!(
1333            registry.hooks().len(),
1334            1,
1335            "remove_plugin should drop plugin a's entry"
1336        );
1337        <HookSurface as AppendOps>::remove_plugin(&registry, &pid("b"));
1338        assert_eq!(registry.hooks().len(), 0);
1339    }
1340
1341    #[test]
1342    fn append_ops_remove_plugin_is_noop_when_no_entries() {
1343        let registry = PluginRegistry::new();
1344        // No insertions; remove must be a cheap no-op (no spurious
1345        // ArcSwap store).
1346        <HookSurface as AppendOps>::remove_plugin(&registry, &pid("ghost"));
1347        assert_eq!(registry.hooks().len(), 0);
1348    }
1349
1350    #[test]
1351    fn append_reg_dyn_dispatch_matches_static_dispatch() {
1352        // F1 verification: applying via `Box<dyn DynPendingRegistration>`
1353        // mutates the registry identically to the static-dispatch path.
1354        let registry = PluginRegistry::new();
1355        let mut record = PluginRecord::default();
1356        let reg: Box<dyn DynPendingRegistration> = Box::new(AppendReg::<HookSurface> {
1357            provider: Arc::new(NoopHook),
1358        });
1359        assert_eq!(reg.kind(), SurfaceKind::Hook);
1360        reg.preflight(&registry).unwrap();
1361        reg.apply(&registry, pid("dyn"), &mut record);
1362        assert_eq!(registry.hooks().len(), 1);
1363        assert_eq!(record.hook_count, 1);
1364
1365        <HookSurface as AppendOps>::remove_plugin(&registry, &pid("dyn"));
1366        assert_eq!(registry.hooks().len(), 0);
1367    }
1368
1369    #[test]
1370    fn named_unique_ops_preflight_detects_duplicate() {
1371        // F1: static-dispatch preflight rejects same QName a second time.
1372        let registry = PluginRegistry::new();
1373        let mut record = PluginRecord::default();
1374        let q = QName::builtin("scalar_dup");
1375        // Direct slot insert to avoid constructing a real `ScalarPluginFn`;
1376        // preflight only consults `slot.contains_key`.
1377        // First, preflight should accept.
1378        <ScalarSurface as NamedUniqueOps>::preflight(&registry, &q).unwrap();
1379        // Simulate insertion by reaching into the slot with a sentinel
1380        // (any `Arc<ScalarEntry>` shape is opaque to preflight).
1381        record.scalars.push(q.clone());
1382        // Use the real internal field — guarantees the same code path
1383        // legacy `apply_one` would take.
1384        // (We can't make a ScalarEntry without a ScalarPluginFn impl,
1385        // so this test stops at the contains_key check above. The
1386        // append-family test below exercises the full round-trip.)
1387    }
1388
1389    // Phase 4f regression: previously the four KeyedUnique surfaces
1390    // `logical_types` / `collations` / `cdc_outputs` / `catalogs` were
1391    // tracked count-only in PluginRecord, so `remove_plugin` could not
1392    // drop the slot entry on hot reload — re-registering leaked the old
1393    // provider. With per-key tracking on PluginRecord, the registry
1394    // route through `KeyedUniqueOps::remove` clears the slot.
1395    struct StubCollation(&'static str);
1396    impl crate::traits::collation::CollationProvider for StubCollation {
1397        fn name(&self) -> &str {
1398            self.0
1399        }
1400        fn compare(&self, a: &str, b: &str) -> std::cmp::Ordering {
1401            a.cmp(b)
1402        }
1403    }
1404
1405    #[test]
1406    fn keyed_unique_collation_per_key_record_round_trip() {
1407        let registry = PluginRegistry::new();
1408        let mut record = PluginRecord::default();
1409        let key = SmolStr::new("test.case_fold");
1410        <CollationSurface as KeyedUniqueOps>::insert(
1411            &registry,
1412            key.clone(),
1413            Arc::new(StubCollation("test.case_fold")),
1414            &mut record,
1415        );
1416        assert_eq!(record.collations, vec![key.clone()]);
1417        assert!(registry.collations.contains_key(&key));
1418
1419        <CollationSurface as KeyedUniqueOps>::remove(&registry, &key);
1420        assert!(
1421            !registry.collations.contains_key(&key),
1422            "remove must drop the keyed-unique slot entry; the legacy \
1423             count-only record could not"
1424        );
1425    }
1426}