Skip to main content

substrait_explain/extensions/
registry.rs

1//! Registry for custom Substrait advanced extension payloads.
2//!
3//! This module lets users register handlers for advanced extensions that carry
4//! `google.protobuf.Any` detail payloads: custom relation types, relation
5//! enhancements, and optimization hints.
6//!
7//! # Overview
8//!
9//! The extension registry allows users to:
10//! - Register custom extension handlers in relation, enhancement, or
11//!   optimization namespaces
12//! - Parse extension arguments/named arguments into `google.protobuf.Any`
13//!   detail fields
14//! - Textify extension detail fields back into readable text format
15//! - Keep each registered payload's protobuf type URL associated with its
16//!   canonical text-format name
17//!
18//! # Architecture
19//!
20//! The system is built around several key traits:
21//! - `AnyConvertible`: For converting types to/from protobuf Any messages
22//! - `Explainable`: For converting types to/from ExtensionArgs
23//! - `ExtensionRegistry`: Registry for managing extension types
24//!
25//! # Example Usage
26//!
27//! ```rust
28//! use substrait_explain::extensions::{
29//!     Any, AnyConvertible, AnyRef, Explainable, ExtensionArgs, ExtensionContext, ExtensionError,
30//!     ExtensionRegistry,
31//! };
32//!
33//! // Define a custom extension type
34//! struct CustomScanConfig {
35//!     path: String,
36//! }
37//!
38//! // Implement AnyConvertible for protobuf serialization
39//! impl AnyConvertible for CustomScanConfig {
40//!     fn to_any(&self) -> Result<Any, ExtensionError> {
41//!         // For this example, we'll create a simple Any (protobuf details field) with the path
42//!         Ok(Any::new(Self::type_url(), self.path.as_bytes().to_vec()))
43//!     }
44//!
45//!     fn from_any<'a>(any: AnyRef<'a>) -> Result<Self, ExtensionError> {
46//!         // Deserialize from Any
47//!         let path = String::from_utf8(any.value.to_vec())
48//!             .map_err(|e| ExtensionError::Custom(format!("Invalid UTF-8: {}", e)))?;
49//!         Ok(CustomScanConfig { path })
50//!     }
51//!
52//!     fn type_url() -> String {
53//!         "type.googleapis.com/example.CustomScanConfig".to_string()
54//!     }
55//! }
56//!
57//! // Implement Explainable for text format conversion
58//! impl Explainable for CustomScanConfig {
59//!     fn name() -> &'static str {
60//!         "ParquetScan"
61//!     }
62//!
63//!     fn from_args(args: &ExtensionArgs) -> Result<Self, ExtensionError> {
64//!         let mut extractor = args.extractor();
65//!         let path: &str = extractor.expect_named_arg("path")?;
66//!         extractor.check_exhausted()?;
67//!         Ok(CustomScanConfig {
68//!             path: path.to_string(),
69//!         })
70//!     }
71//!
72//!     fn to_args(
73//!         &self,
74//!         _context: &ExtensionContext<'_>,
75//!     ) -> Result<ExtensionArgs, ExtensionError> {
76//!         let mut args = ExtensionArgs::default();
77//!         args.insert("path", self.path.clone());
78//!         Ok(args)
79//!     }
80//! }
81//!
82//! // Register the extension type
83//! let mut registry = ExtensionRegistry::new();
84//! registry.register_relation::<CustomScanConfig>().unwrap();
85//! ```
86
87use std::collections::HashMap;
88use std::fmt;
89use std::marker::PhantomData;
90use std::sync::Arc;
91
92use substrait::proto::NamedStruct;
93use substrait::proto::r#type::{Nullability, Struct};
94use thiserror::Error;
95
96use crate::extensions::any::{Any, AnyRef};
97use crate::extensions::args::{ExtensionArgs, ExtensionColumn, ExtensionValueKind};
98
99/// Type of extension in the registry, used for namespace separation.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101pub enum ExtensionType {
102    /// Relation extension (e.g., ExtensionLeaf, ExtensionSingle, ExtensionMulti)
103    Relation,
104    /// ExtensionTable detail attached to a ReadRel (uses `+ Ext:` prefix in text format)
105    ExtensionTable,
106    /// Enhancement attached to a relation (uses `+ Enh:` prefix in text format)
107    Enhancement,
108    /// Optimization attached to a relation (uses `+ Opt:` prefix in text format)
109    Optimization,
110}
111
112/// Information about one input to an extension relation.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct ExtensionInput {
115    emitted_column_count: usize,
116}
117
118impl ExtensionInput {
119    pub(crate) fn new(emitted_column_count: usize) -> Self {
120        Self {
121            emitted_column_count,
122        }
123    }
124
125    /// Number of columns emitted by this input after applying its output mapping.
126    pub fn emitted_column_count(&self) -> usize {
127        self.emitted_column_count
128    }
129}
130
131/// Context available while converting an extension payload to text arguments.
132///
133/// Relation extensions receive one entry per available input, in relation
134/// order. Other extension namespaces, and context-free registry decoding,
135/// receive an empty input slice.
136#[derive(Debug, Clone, Copy, Default)]
137pub struct ExtensionContext<'a> {
138    inputs: &'a [ExtensionInput],
139}
140
141impl<'a> ExtensionContext<'a> {
142    pub(crate) fn new(inputs: &'a [ExtensionInput]) -> Self {
143        Self { inputs }
144    }
145
146    /// Inputs to the extension relation, in relation order.
147    pub fn inputs(&self) -> &'a [ExtensionInput] {
148        self.inputs
149    }
150}
151
152/// Errors during extension registration (setup phase)
153#[derive(Debug, Error, Clone)]
154pub enum RegistrationError {
155    #[error("{ext_type:?} extension '{name}' already registered")]
156    DuplicateName {
157        ext_type: ExtensionType,
158        name: String,
159    },
160
161    #[error("Type URL '{type_url}' already registered to {ext_type:?} extension '{existing_name}'")]
162    ConflictingTypeUrl {
163        type_url: String,
164        ext_type: ExtensionType,
165        existing_name: String,
166    },
167}
168
169/// Errors during extension parsing, formatting, and argument extraction (runtime)
170#[derive(Debug, Error, Clone)]
171pub enum ExtensionError {
172    /// Extension not found in registry during lookup
173    #[error("Extension '{name}' not found in registry")]
174    NotFound { name: String },
175
176    /// Required argument not present (from ArgsExtractor)
177    #[error("Missing required argument: {name}")]
178    MissingArgument { name: String },
179
180    /// Invalid argument type found while extracting an extension argument.
181    #[error("Invalid argument: expected {expected}, got {actual}")]
182    InvalidArgumentType {
183        expected: ExtensionValueKind,
184        actual: ExtensionValueKind,
185    },
186
187    /// Invalid argument value with a custom diagnostic.
188    ///
189    /// Prefer structured variants for common mechanical validation failures.
190    /// Use this for domain-specific validation from `Explainable`
191    /// implementations.
192    #[error("Invalid argument: {0}")]
193    InvalidArgument(String),
194
195    /// Type URL mismatch during protobuf Any decode
196    #[error("Type URL mismatch: expected {expected}, got {actual}")]
197    TypeUrlMismatch { expected: String, actual: String },
198
199    /// Protobuf message decode failure
200    #[error("Failed to decode protobuf message")]
201    DecodeFailed(#[source] prost::DecodeError),
202
203    /// Protobuf message encode failure
204    #[error("Failed to encode protobuf message")]
205    EncodeFailed(#[source] prost::EncodeError),
206
207    /// Extension detail field is missing from the relation
208    #[error("Extension detail is missing")]
209    MissingDetail,
210
211    /// Error from a custom AnyConvertible implementation
212    #[error("{0}")]
213    Custom(String),
214}
215
216/// Trait for types that can be converted to/from protobuf Any messages. Note
217/// that this is already implemented for all prost::Message types. For custom
218/// types, implement this trait.
219pub trait AnyConvertible: Sized {
220    /// Convert this type to a protobuf Any message
221    fn to_any(&self) -> Result<Any, ExtensionError>;
222
223    /// Convert from a protobuf Any message to this type
224    fn from_any<'a>(any: AnyRef<'a>) -> Result<Self, ExtensionError>;
225
226    /// Get the protobuf type URL for this type.
227    /// For prost::Message types, this is provided automatically via blanket impl.
228    /// Custom types must implement this method.
229    fn type_url() -> String;
230}
231
232// Blanket implementation for all prost::Message types
233impl<T> AnyConvertible for T
234where
235    T: prost::Message + prost::Name + Default,
236{
237    fn to_any(&self) -> Result<Any, ExtensionError> {
238        Any::encode(self)
239    }
240
241    fn from_any<'a>(any: AnyRef<'a>) -> Result<Self, ExtensionError> {
242        any.decode()
243    }
244
245    fn type_url() -> String {
246        T::type_url()
247    }
248}
249
250/// Conversion between extension arguments and Substrait protobuf values.
251///
252/// Extension arguments are the structured values exposed to [`Explainable`]
253/// implementations, such as [`ExtensionArgs`],
254/// [`ExtensionValue`](crate::extensions::ExtensionValue), and
255/// [`ExtensionColumn`]. This trait adapts those values to and from protobuf
256/// types without going through text.
257///
258/// Implementations may convert in either direction; the target type `T`
259/// determines the direction.
260pub trait ExtensionProtoConvert<T> {
261    /// Convert this value into `T`.
262    fn convert(&self) -> Result<T, ExtensionError>;
263}
264
265impl ExtensionProtoConvert<NamedStruct> for [ExtensionColumn] {
266    fn convert(&self) -> Result<NamedStruct, ExtensionError> {
267        let mut names = Vec::with_capacity(self.len());
268        let mut types = Vec::with_capacity(self.len());
269        for col in self {
270            match col {
271                ExtensionColumn::Named { name, r#type: ty } => {
272                    names.push(name.clone());
273                    types.push(ty.clone());
274                }
275                other => {
276                    return Err(ExtensionError::InvalidArgument(format!(
277                        "Expected named column, got {other:?}"
278                    )));
279                }
280            }
281        }
282        Ok(NamedStruct {
283            names,
284            r#struct: Some(Struct {
285                types,
286                type_variation_reference: 0,
287                // In Substrait, the schema of a type is defined as
288                // non-nullable; you can have an empty schema (no columns), but
289                // not a null schema.
290                nullability: Nullability::Required as i32,
291            }),
292        })
293    }
294}
295
296impl ExtensionProtoConvert<Vec<ExtensionColumn>> for NamedStruct {
297    fn convert(&self) -> Result<Vec<ExtensionColumn>, ExtensionError> {
298        let types = self
299            .r#struct
300            .as_ref()
301            .map(|s| s.types.as_slice())
302            .unwrap_or_default();
303        if self.names.len() != types.len() {
304            return Err(ExtensionError::InvalidArgument(format!(
305                "NamedStruct has {} names but {} types",
306                self.names.len(),
307                types.len()
308            )));
309        }
310        Ok(self
311            .names
312            .iter()
313            .zip(types.iter())
314            .map(|(name, ty)| ExtensionColumn::Named {
315                name: name.clone(),
316                r#type: ty.clone(),
317            })
318            .collect())
319    }
320}
321
322/// Trait for types that participate in text explanations.
323pub trait Explainable: Sized {
324    /// Canonical textual name for this extension. This is what appears in
325    /// Substrait text plans and how the registry identifies the type.
326    fn name() -> &'static str;
327
328    /// Parse extension arguments into this type
329    fn from_args(args: &ExtensionArgs) -> Result<Self, ExtensionError>;
330
331    /// Convert this type to extension arguments
332    fn to_args(&self, context: &ExtensionContext<'_>) -> Result<ExtensionArgs, ExtensionError>;
333}
334
335/// Internal trait that converts between ExtensionArgs and protobuf Any messages.
336///
337/// This trait exists because we need to store handlers for different extension types
338/// in a single HashMap. Since Rust doesn't allow trait objects with multiple traits
339/// (like `Box<dyn AnyConvertible + Explainable>`), we need a single trait that
340/// combines both operations.
341///
342/// The ExtensionConverter acts as a bridge between:
343/// - The text format representation (ExtensionArgs) used by the parser/formatter
344/// - The protobuf Any messages stored in Substrait advanced extension payloads
345///
346/// This design allows the registry to work with any type while maintaining type safety
347/// through the AnyConvertible and Explainable traits that users implement.
348trait ExtensionConverter: Send + Sync {
349    fn parse_detail(&self, args: &ExtensionArgs) -> Result<Any, ExtensionError>;
350
351    fn textify_detail(
352        &self,
353        detail: AnyRef<'_>,
354        context: &ExtensionContext<'_>,
355    ) -> Result<ExtensionArgs, ExtensionError>;
356}
357
358/// Type adapter that implements ExtensionConverter for any type T that implements
359/// both AnyConvertible and Explainable.
360///
361/// This struct exists to solve Rust's "trait object problem": we can't store
362/// `Box<dyn AnyConvertible + Explainable>` because that's two traits, not one.
363/// Instead, we store `Box<dyn ExtensionConverter>` and use this adapter to bridge
364/// from the two user-facing traits to our single internal trait.
365///
366/// The adapter pattern allows us to:
367/// 1. Keep a clean API where users only implement AnyConvertible and Explainable
368/// 2. Store different types in the same HashMap through type erasure
369/// 3. Maintain type safety - the concrete type T is known at registration time
370/// 4. Avoid any runtime type checking or unsafe code
371///
372/// The PhantomData is necessary because we don't actually store a T, but we need
373/// the type information to call T's static methods (from_args, from_any).
374struct ExtensionAdapter<T>(PhantomData<T>);
375
376impl<T: AnyConvertible + Explainable + Send + Sync> ExtensionConverter for ExtensionAdapter<T> {
377    fn parse_detail(&self, args: &ExtensionArgs) -> Result<Any, ExtensionError> {
378        T::from_args(args)?.to_any()
379    }
380
381    fn textify_detail(
382        &self,
383        detail: AnyRef<'_>,
384        context: &ExtensionContext<'_>,
385    ) -> Result<ExtensionArgs, ExtensionError> {
386        let owned_any = Any::new(detail.type_url.to_string(), detail.value.to_vec());
387        T::from_any(owned_any.as_ref())?.to_args(context)
388    }
389}
390
391pub trait Extension: AnyConvertible + Explainable + Send + Sync + 'static {}
392
393impl<T> Extension for T where T: AnyConvertible + Explainable + Send + Sync + 'static {}
394
395/// Registry for extension handlers
396#[derive(Default, Clone)]
397pub struct ExtensionRegistry {
398    // Composite key: (ExtensionType, name) -> handler
399    handlers: HashMap<(ExtensionType, String), Arc<dyn ExtensionConverter>>,
400    // Composite key: (ExtensionType, type_url) -> name
401    type_urls: HashMap<(ExtensionType, String), String>,
402    // Compiled proto FileDescriptorSet blobs for extension types.
403    // Used by the JSON parser to resolve google.protobuf.Any type URLs in Go
404    // protojson input. Register these alongside the Rust handler so that a
405    // single registry carries all extension knowledge for both formatting and
406    // JSON parsing.
407    descriptors: Vec<Vec<u8>>,
408}
409
410impl ExtensionRegistry {
411    /// Create a new empty extension registry
412    pub fn new() -> Self {
413        Self {
414            handlers: HashMap::new(),
415            type_urls: HashMap::new(),
416            descriptors: Vec::new(),
417        }
418    }
419
420    /// Register a compiled proto `FileDescriptorSet` blob for extension types.
421    ///
422    /// Required when parsing extensions for plans that contain
423    /// `google.protobuf.Any` fields that use standard JSON encoding (with
424    /// `@type` for the type_url) whose types are not part of the Substrait core
425    /// schema. Pass the bytes of a compiled `.bin` descriptor, e.g.
426    /// `include_bytes!("my_extensions.bin")`.
427    pub fn add_descriptor(&mut self, bytes: Vec<u8>) {
428        self.descriptors.push(bytes);
429    }
430
431    /// Returns slices of all registered descriptor blobs.
432    pub fn descriptors(&self) -> Vec<&[u8]> {
433        self.descriptors.iter().map(|b| b.as_slice()).collect()
434    }
435
436    /// Register an extension type with a specific ExtensionType
437    fn register<T>(&mut self, ext_type: ExtensionType) -> Result<(), RegistrationError>
438    where
439        T: Extension,
440    {
441        let canonical_name = T::name();
442        let type_url = T::type_url();
443        let handler: Arc<dyn ExtensionConverter> = Arc::new(ExtensionAdapter::<T>(PhantomData));
444
445        let key = (ext_type, canonical_name.to_string());
446        if self.handlers.contains_key(&key) {
447            return Err(RegistrationError::DuplicateName {
448                ext_type,
449                name: canonical_name.to_string(),
450            });
451        }
452
453        // Check for type URL conflicts before mutating any state
454        let type_url_key = (ext_type, type_url.clone());
455        if let Some(existing) = self.type_urls.get(&type_url_key)
456            && existing != canonical_name
457        {
458            return Err(RegistrationError::ConflictingTypeUrl {
459                type_url,
460                ext_type,
461                existing_name: existing.clone(),
462            });
463        }
464
465        // All checks passed — safe to mutate
466        self.handlers.insert(key, Arc::clone(&handler));
467        self.type_urls
468            .insert(type_url_key, canonical_name.to_string());
469        Ok(())
470    }
471
472    /// Register a relation extension type that implements both AnyConvertible and Explainable
473    ///
474    /// The canonical textual name comes from `T::name()`.
475    pub fn register_relation<T>(&mut self) -> Result<(), RegistrationError>
476    where
477        T: Extension,
478    {
479        self.register::<T>(ExtensionType::Relation)
480    }
481
482    /// Register an ExtensionTable detail type that implements both AnyConvertible and Explainable
483    ///
484    /// ExtensionTable details are registered in a separate namespace from
485    /// extension relations, allowing the same type URL to exist in both namespaces
486    /// without conflict.
487    ///
488    /// The canonical textual name comes from `T::name()`.
489    pub fn register_extension_table<T>(&mut self) -> Result<(), RegistrationError>
490    where
491        T: Extension,
492    {
493        self.register::<T>(ExtensionType::ExtensionTable)
494    }
495
496    /// Register an enhancement type that implements both AnyConvertible and Explainable
497    ///
498    /// Enhancements are registered in a separate namespace from relation extensions,
499    /// allowing the same type URL to exist in both namespaces without conflict.
500    ///
501    /// The canonical textual name comes from `T::name()`.
502    pub fn register_enhancement<T>(&mut self) -> Result<(), RegistrationError>
503    where
504        T: Extension,
505    {
506        self.register::<T>(ExtensionType::Enhancement)
507    }
508
509    /// Register an optimization type that implements both AnyConvertible and Explainable
510    ///
511    /// Optimizations are registered in a separate namespace from relation extensions,
512    /// allowing the same type URL to exist in both namespaces without conflict.
513    ///
514    /// The canonical textual name comes from `T::name()`.
515    pub fn register_optimization<T>(&mut self) -> Result<(), RegistrationError>
516    where
517        T: Extension,
518    {
519        self.register::<T>(ExtensionType::Optimization)
520    }
521
522    /// Parse extension arguments into a protobuf Any message
523    pub fn parse_extension(
524        &self,
525        extension_name: &str,
526        args: &ExtensionArgs,
527    ) -> Result<Any, ExtensionError> {
528        self.parse_with_type(ExtensionType::Relation, extension_name, args)
529    }
530
531    /// Parse ExtensionTable arguments into a protobuf Any message
532    ///
533    /// Looks up the ExtensionTable detail handler in the ExtensionTable namespace
534    /// and parses the arguments into a protobuf Any message.
535    pub fn parse_extension_table(
536        &self,
537        extension_table_name: &str,
538        args: &ExtensionArgs,
539    ) -> Result<Any, ExtensionError> {
540        self.parse_with_type(ExtensionType::ExtensionTable, extension_table_name, args)
541    }
542
543    /// Parse enhancement arguments into a protobuf Any message
544    ///
545    /// Looks up the enhancement handler in the enhancement namespace and parses
546    /// the arguments into a protobuf Any message.
547    pub fn parse_enhancement(
548        &self,
549        enhancement_name: &str,
550        args: &ExtensionArgs,
551    ) -> Result<Any, ExtensionError> {
552        self.parse_with_type(ExtensionType::Enhancement, enhancement_name, args)
553    }
554
555    /// Parse optimization arguments into a protobuf Any message
556    ///
557    /// Looks up the optimization handler in the optimization namespace and parses
558    /// the arguments into a protobuf Any message.
559    pub fn parse_optimization(
560        &self,
561        optimization_name: &str,
562        args: &ExtensionArgs,
563    ) -> Result<Any, ExtensionError> {
564        self.parse_with_type(ExtensionType::Optimization, optimization_name, args)
565    }
566
567    /// Internal method to parse extension arguments with a specific ExtensionType
568    fn parse_with_type(
569        &self,
570        ext_type: ExtensionType,
571        name: &str,
572        args: &ExtensionArgs,
573    ) -> Result<Any, ExtensionError> {
574        let key = (ext_type, name.to_string());
575        let handler = self
576            .handlers
577            .get(&key)
578            .ok_or_else(|| ExtensionError::NotFound {
579                name: name.to_string(),
580            })?;
581        handler.parse_detail(args)
582    }
583
584    /// Decode extension detail to extension name and ExtensionArgs
585    /// This is the primary method for textification - given an AnyRef with extension detail,
586    /// decode it to the extension name and appropriate ExtensionArgs for display
587    pub fn decode(&self, detail: AnyRef<'_>) -> Result<(String, ExtensionArgs), ExtensionError> {
588        self.decode_with_type(ExtensionType::Relation, detail)
589    }
590
591    /// Decode relation extension detail using information about its inputs.
592    pub(crate) fn decode_with_context(
593        &self,
594        detail: AnyRef<'_>,
595        context: &ExtensionContext<'_>,
596    ) -> Result<(String, ExtensionArgs), ExtensionError> {
597        self.decode_with_type_and_context(ExtensionType::Relation, detail, context)
598    }
599
600    /// Decode ExtensionTable detail to extension name and ExtensionArgs
601    ///
602    /// This is the primary method for textification of ExtensionTable reads -
603    /// given an AnyRef with ExtensionTable detail, decode it to the extension
604    /// name and appropriate ExtensionArgs for display.
605    pub fn decode_extension_table(
606        &self,
607        detail: AnyRef<'_>,
608    ) -> Result<(String, ExtensionArgs), ExtensionError> {
609        self.decode_with_type(ExtensionType::ExtensionTable, detail)
610    }
611
612    /// Decode enhancement detail to enhancement name and ExtensionArgs
613    ///
614    /// This is the primary method for textification of enhancements - given an AnyRef
615    /// with enhancement detail, decode it to the enhancement name and appropriate
616    /// ExtensionArgs for display.
617    ///
618    /// Looks up the enhancement handler in the enhancement namespace by type URL.
619    pub fn decode_enhancement(
620        &self,
621        detail: AnyRef<'_>,
622    ) -> Result<(String, ExtensionArgs), ExtensionError> {
623        self.decode_with_type(ExtensionType::Enhancement, detail)
624    }
625
626    /// Decode optimization detail to optimization name and ExtensionArgs
627    ///
628    /// This is the primary method for textification of optimizations - given an AnyRef
629    /// with optimization detail, decode it to the optimization name and appropriate
630    /// ExtensionArgs for display.
631    ///
632    /// Looks up the optimization handler in the optimization namespace by type URL.
633    pub fn decode_optimization(
634        &self,
635        detail: AnyRef<'_>,
636    ) -> Result<(String, ExtensionArgs), ExtensionError> {
637        self.decode_with_type(ExtensionType::Optimization, detail)
638    }
639
640    /// Internal method to decode extension detail with a specific ExtensionType
641    fn decode_with_type(
642        &self,
643        ext_type: ExtensionType,
644        detail: AnyRef<'_>,
645    ) -> Result<(String, ExtensionArgs), ExtensionError> {
646        self.decode_with_type_and_context(ext_type, detail, &ExtensionContext::default())
647    }
648
649    fn decode_with_type_and_context(
650        &self,
651        ext_type: ExtensionType,
652        detail: AnyRef<'_>,
653        context: &ExtensionContext<'_>,
654    ) -> Result<(String, ExtensionArgs), ExtensionError> {
655        // Find extension name by type URL in the specified namespace
656        let type_url_key = (ext_type, detail.type_url.to_string());
657        let extension_name =
658            self.type_urls
659                .get(&type_url_key)
660                .ok_or_else(|| ExtensionError::NotFound {
661                    name: detail.type_url.to_string(),
662                })?;
663
664        // Get handler and textify the detail
665        let name_key = (ext_type, extension_name.clone());
666        let handler = self
667            .handlers
668            .get(&name_key)
669            .ok_or_else(|| ExtensionError::NotFound {
670                name: extension_name.clone(),
671            })?;
672
673        let args = handler.textify_detail(detail, context)?;
674
675        Ok((extension_name.clone(), args))
676    }
677
678    /// Get all registered extension names for a specific ExtensionType
679    pub fn extension_names(&self, ext_type: ExtensionType) -> Vec<&str> {
680        let mut names: Vec<&str> = self
681            .type_urls
682            .iter()
683            .filter_map(|((t, _), name)| {
684                if *t == ext_type {
685                    Some(name.as_str())
686                } else {
687                    None
688                }
689            })
690            .collect();
691        names.sort_unstable();
692        names.dedup();
693        names
694    }
695
696    /// Check if an extension is registered for a specific ExtensionType
697    pub fn has_extension(&self, ext_type: ExtensionType, name: &str) -> bool {
698        self.handlers.contains_key(&(ext_type, name.to_string()))
699    }
700}
701
702impl fmt::Debug for ExtensionRegistry {
703    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
704        let mut keys: Vec<_> = self
705            .handlers
706            .keys()
707            .map(|(t, n)| (format!("{t:?}"), n.as_str()))
708            .collect();
709        keys.sort();
710        f.debug_struct("ExtensionRegistry")
711            .field("handlers", &keys)
712            .finish()
713    }
714}
715
716#[cfg(test)]
717mod tests {
718    use super::*;
719    use crate::extensions::ExtensionColumn;
720    use crate::fixtures::parse_type;
721    use crate::textify::expressions::Reference;
722
723    // Mock type for testing
724    struct TestExtension {
725        path: String,
726        batch_size: i64,
727    }
728
729    // Manual implementation of AnyConvertible for testing (without prost)
730    impl AnyConvertible for TestExtension {
731        fn to_any(&self) -> Result<Any, ExtensionError> {
732            // Simple test implementation - create Any with JSON-like bytes
733            let json_str = format!(
734                r#"{{"path":"{}","batch_size":{}}}"#,
735                self.path, self.batch_size
736            );
737            Ok(Any::new(Self::type_url(), json_str.into_bytes()))
738        }
739
740        fn type_url() -> String {
741            "test.TestExtension".to_string()
742        }
743
744        fn from_any<'a>(any: AnyRef<'a>) -> Result<Self, ExtensionError> {
745            // Simple test implementation - parse from JSON-like bytes
746            let json_str = String::from_utf8(any.value.to_vec())
747                .map_err(|e| ExtensionError::Custom(format!("Invalid UTF-8: {e}")))?;
748
749            // Simple manual parsing for test
750            if json_str.contains("path") && json_str.contains("batch_size") {
751                Ok(TestExtension {
752                    path: "test.parquet".to_string(),
753                    batch_size: 1024,
754                })
755            } else {
756                Err(ExtensionError::Custom("Missing fields".to_string()))
757            }
758        }
759    }
760
761    impl Explainable for TestExtension {
762        fn name() -> &'static str {
763            "TestExtension"
764        }
765
766        fn from_args(args: &ExtensionArgs) -> Result<Self, ExtensionError> {
767            let mut extractor = args.extractor();
768            let path: String = extractor.expect_named_arg::<&str>("path")?.to_string();
769            let batch_size: i64 = extractor.expect_named_arg("batch_size")?;
770            extractor.check_exhausted()?;
771
772            Ok(TestExtension {
773                path: path.to_string(),
774                batch_size,
775            })
776        }
777
778        fn to_args(&self, context: &ExtensionContext<'_>) -> Result<ExtensionArgs, ExtensionError> {
779            let mut args = ExtensionArgs::default();
780            args.insert("path", self.path.clone());
781            args.insert("batch_size", self.batch_size);
782            if !context.inputs().is_empty() {
783                args.insert("input_count", context.inputs().len() as i64);
784            }
785            Ok(args)
786        }
787    }
788
789    #[test]
790    fn test_extension_registry_basic() {
791        let mut registry = ExtensionRegistry::new();
792
793        // Initially empty
794        assert_eq!(registry.extension_names(ExtensionType::Relation).len(), 0);
795        assert_eq!(
796            registry
797                .extension_names(ExtensionType::ExtensionTable)
798                .len(),
799            0
800        );
801        assert!(!registry.has_extension(ExtensionType::Relation, "TestExtension"));
802
803        // Register extension type
804        registry.register_relation::<TestExtension>().unwrap();
805
806        // Now has extension
807        assert_eq!(registry.extension_names(ExtensionType::Relation).len(), 1);
808        assert!(registry.has_extension(ExtensionType::Relation, "TestExtension"));
809
810        // Test parse and textify
811        let mut args = ExtensionArgs::default();
812        args.insert("path", "data.parquet");
813        args.insert("batch_size", 2048_i64);
814
815        let any = registry.parse_extension("TestExtension", &args).unwrap();
816        assert_eq!(any.type_url, "test.TestExtension");
817
818        let any_ref = any.as_ref();
819        let result = registry.decode(any_ref).unwrap();
820        assert_eq!(result.0, "TestExtension");
821        assert_eq!(
822            <&str>::try_from(result.1.named.get("path").unwrap()).unwrap(),
823            "test.parquet"
824        );
825        assert!(!result.1.named.contains_key("input_count"));
826    }
827
828    #[test]
829    fn test_extension_registry_decode_with_context() {
830        let mut registry = ExtensionRegistry::new();
831        registry.register_relation::<TestExtension>().unwrap();
832
833        let mut args = ExtensionArgs::default();
834        args.insert("path", "data.parquet");
835        args.insert("batch_size", 2048_i64);
836        let any = registry.parse_extension("TestExtension", &args).unwrap();
837        let inputs = [ExtensionInput::new(4)];
838        let context = ExtensionContext::new(&inputs);
839
840        let (_, decoded_args) = registry
841            .decode_with_context(any.as_ref(), &context)
842            .unwrap();
843        assert_eq!(
844            i64::try_from(decoded_args.named.get("input_count").unwrap()).unwrap(),
845            1
846        );
847    }
848
849    #[test]
850    fn test_extension_table_registry_basic() {
851        let mut registry = ExtensionRegistry::new();
852
853        registry
854            .register_extension_table::<TestExtension>()
855            .unwrap();
856
857        assert_eq!(
858            registry.extension_names(ExtensionType::ExtensionTable),
859            vec!["TestExtension"]
860        );
861        assert!(registry.has_extension(ExtensionType::ExtensionTable, "TestExtension"));
862
863        let mut args = ExtensionArgs::default();
864        args.insert("path", "data.parquet");
865        args.insert("batch_size", 2048_i64);
866
867        let any = registry
868            .parse_extension_table("TestExtension", &args)
869            .unwrap();
870        assert_eq!(any.type_url, "test.TestExtension");
871
872        let (name, decoded_args) = registry.decode_extension_table(any.as_ref()).unwrap();
873        assert_eq!(name, "TestExtension");
874        assert_eq!(
875            <&str>::try_from(decoded_args.named.get("path").unwrap()).unwrap(),
876            "test.parquet"
877        );
878        assert!(!decoded_args.named.contains_key("input_count"));
879    }
880
881    #[test]
882    fn test_extension_args() {
883        let mut args = ExtensionArgs::default();
884
885        // Add named args
886        args.insert("path", "data/*.parquet");
887        args.insert("batch_size", 1024_i64);
888
889        // Add positional args
890        args.push(Reference(0));
891
892        // Add output columns
893        args.output_columns.push(ExtensionColumn::Named {
894            name: "col1".to_string(),
895            r#type: parse_type("i32"),
896        });
897
898        // Test retrieval - use extractor
899        let mut extractor = args.extractor();
900
901        let path = extractor.get_named_arg("path").unwrap();
902        assert_eq!(<&str>::try_from(path).unwrap(), "data/*.parquet");
903
904        let batch_size = extractor.get_named_arg("batch_size").unwrap();
905        assert_eq!(i64::try_from(batch_size).unwrap(), 1024);
906
907        // Verify they were consumed
908        assert!(extractor.check_exhausted().is_ok());
909
910        assert_eq!(args.positional.len(), 1);
911        assert_eq!(args.output_columns.len(), 1);
912    }
913
914    #[test]
915    fn test_extension_error_cases() {
916        let registry = ExtensionRegistry::new();
917
918        // Extension not found
919        let args = ExtensionArgs::default();
920        let result = registry.parse_extension("NonExistent", &args);
921        assert!(matches!(result, Err(ExtensionError::NotFound { .. })));
922
923        // Missing argument
924        let args = ExtensionArgs::default();
925        let mut extractor = args.extractor();
926        let result = extractor.get_named_arg("missing");
927        assert!(result.is_none());
928        assert!(extractor.check_exhausted().is_ok());
929
930        // Type check example
931        let mut args = ExtensionArgs::default();
932        args.insert("test", 42_i64);
933        let mut extractor = args.extractor();
934        let result = extractor.get_named_arg("test");
935        assert_eq!(i64::try_from(result.unwrap()).unwrap(), 42);
936        assert!(extractor.check_exhausted().is_ok());
937    }
938
939    // Mock enhancement type for testing namespace separation
940    struct TestEnhancement {
941        hint: String,
942    }
943
944    impl AnyConvertible for TestEnhancement {
945        fn to_any(&self) -> Result<Any, ExtensionError> {
946            let json_str = format!(r#"{{"hint":"{}"}}"#, self.hint);
947            Ok(Any::new(Self::type_url(), json_str.into_bytes()))
948        }
949
950        fn type_url() -> String {
951            // Same type URL as TestExtension to test namespace separation
952            "test.TestExtension".to_string()
953        }
954
955        fn from_any<'a>(any: AnyRef<'a>) -> Result<Self, ExtensionError> {
956            let json_str = String::from_utf8(any.value.to_vec())
957                .map_err(|e| ExtensionError::Custom(format!("Invalid UTF-8: {e}")))?;
958            if json_str.contains("hint") {
959                Ok(TestEnhancement {
960                    hint: "test_hint".to_string(),
961                })
962            } else {
963                Err(ExtensionError::Custom("Missing hint field".to_string()))
964            }
965        }
966    }
967
968    impl Explainable for TestEnhancement {
969        fn name() -> &'static str {
970            "TestEnhancement"
971        }
972
973        fn from_args(args: &ExtensionArgs) -> Result<Self, ExtensionError> {
974            let mut extractor = args.extractor();
975            let hint: String = extractor.expect_named_arg::<&str>("hint")?.to_string();
976            extractor.check_exhausted()?;
977            Ok(TestEnhancement { hint })
978        }
979
980        fn to_args(
981            &self,
982            _context: &ExtensionContext<'_>,
983        ) -> Result<ExtensionArgs, ExtensionError> {
984            let mut args = ExtensionArgs::default();
985            args.insert("hint", self.hint.clone());
986            Ok(args)
987        }
988    }
989
990    #[test]
991    fn test_namespace_separation() {
992        let mut registry = ExtensionRegistry::new();
993
994        // Register same type URL in multiple namespaces - should not conflict
995        registry.register_relation::<TestExtension>().unwrap();
996        registry
997            .register_extension_table::<TestExtension>()
998            .unwrap();
999        registry.register_enhancement::<TestEnhancement>().unwrap();
1000
1001        // Verify all are registered
1002        assert!(registry.has_extension(ExtensionType::Relation, "TestExtension"));
1003        assert!(registry.has_extension(ExtensionType::ExtensionTable, "TestExtension"));
1004        assert!(registry.has_extension(ExtensionType::Enhancement, "TestEnhancement"));
1005        assert_eq!(registry.extension_names(ExtensionType::Relation).len(), 1);
1006        assert_eq!(
1007            registry
1008                .extension_names(ExtensionType::ExtensionTable)
1009                .len(),
1010            1
1011        );
1012        assert_eq!(
1013            registry.extension_names(ExtensionType::Enhancement).len(),
1014            1
1015        );
1016
1017        // Test that extension namespace works
1018        let mut ext_args = ExtensionArgs::default();
1019        ext_args.insert("path", "data.parquet");
1020        ext_args.insert("batch_size", 2048_i64);
1021
1022        let ext_any = registry
1023            .parse_extension("TestExtension", &ext_args)
1024            .unwrap();
1025        assert_eq!(ext_any.type_url, "test.TestExtension");
1026
1027        // Test that ExtensionTable namespace works independently
1028        let table_any = registry
1029            .parse_extension_table("TestExtension", &ext_args)
1030            .unwrap();
1031        assert_eq!(table_any.type_url, "test.TestExtension");
1032
1033        // Test that enhancement namespace works
1034        let mut enh_args = ExtensionArgs::default();
1035        enh_args.insert("hint", "optimize");
1036
1037        let enh_any = registry
1038            .parse_enhancement("TestEnhancement", &enh_args)
1039            .unwrap();
1040        assert_eq!(enh_any.type_url, "test.TestExtension"); // Same type URL!
1041
1042        // Test decode_enhancement
1043        let enh_ref = enh_any.as_ref();
1044        let (name, args) = registry.decode_enhancement(enh_ref).unwrap();
1045        assert_eq!(name, "TestEnhancement");
1046        assert_eq!(
1047            <&str>::try_from(args.named.get("hint").unwrap()).unwrap(),
1048            "test_hint"
1049        );
1050    }
1051
1052    #[test]
1053    fn test_enhancement_duplicate_registration_returns_error() {
1054        let mut registry = ExtensionRegistry::new();
1055        registry.register_enhancement::<TestEnhancement>().unwrap();
1056        let result = registry.register_enhancement::<TestEnhancement>();
1057        assert!(matches!(
1058            result,
1059            Err(RegistrationError::DuplicateName { .. })
1060        ));
1061    }
1062
1063    #[test]
1064    fn test_extension_table_duplicate_registration_returns_error() {
1065        let mut registry = ExtensionRegistry::new();
1066        registry
1067            .register_extension_table::<TestExtension>()
1068            .unwrap();
1069        let result = registry.register_extension_table::<TestExtension>();
1070        assert!(matches!(
1071            result,
1072            Err(RegistrationError::DuplicateName { .. })
1073        ));
1074    }
1075
1076    #[test]
1077    fn test_extension_table_not_found_error() {
1078        let registry = ExtensionRegistry::new();
1079        let args = ExtensionArgs::default();
1080        let result = registry.parse_extension_table("NonExistentExtensionTable", &args);
1081        assert!(matches!(result, Err(ExtensionError::NotFound { .. })));
1082    }
1083
1084    #[test]
1085    fn test_enhancement_not_found_error() {
1086        let registry = ExtensionRegistry::new();
1087        let args = ExtensionArgs::default();
1088        let result = registry.parse_enhancement("NonExistentEnhancement", &args);
1089        assert!(matches!(result, Err(ExtensionError::NotFound { .. })));
1090    }
1091
1092    // Extension with same type URL as TestExtension but different name,
1093    // used to test that conflicting type URLs don't leave stale state.
1094    struct ConflictingExtension;
1095
1096    impl AnyConvertible for ConflictingExtension {
1097        fn to_any(&self) -> Result<Any, ExtensionError> {
1098            Ok(Any::new(Self::type_url(), vec![]))
1099        }
1100
1101        fn type_url() -> String {
1102            // Same type URL as TestExtension — will conflict in the same namespace
1103            "test.TestExtension".to_string()
1104        }
1105
1106        fn from_any<'a>(_any: AnyRef<'a>) -> Result<Self, ExtensionError> {
1107            Ok(ConflictingExtension)
1108        }
1109    }
1110
1111    impl Explainable for ConflictingExtension {
1112        fn name() -> &'static str {
1113            "ConflictingExtension"
1114        }
1115
1116        fn from_args(_args: &ExtensionArgs) -> Result<Self, ExtensionError> {
1117            Ok(ConflictingExtension)
1118        }
1119
1120        fn to_args(
1121            &self,
1122            _context: &ExtensionContext<'_>,
1123        ) -> Result<ExtensionArgs, ExtensionError> {
1124            Ok(ExtensionArgs::default())
1125        }
1126    }
1127
1128    #[test]
1129    fn test_conflicting_type_url_leaves_registry_unchanged() {
1130        let mut registry = ExtensionRegistry::new();
1131        registry.register_relation::<TestExtension>().unwrap();
1132
1133        // Attempt to register a different extension with the same type URL
1134        let result = registry.register_relation::<ConflictingExtension>();
1135        assert!(matches!(
1136            result,
1137            Err(RegistrationError::ConflictingTypeUrl { .. })
1138        ));
1139
1140        // Registry should still only know about the original extension
1141        assert!(registry.has_extension(ExtensionType::Relation, "TestExtension"));
1142        assert!(!registry.has_extension(ExtensionType::Relation, "ConflictingExtension"));
1143        assert_eq!(
1144            registry.extension_names(ExtensionType::Relation),
1145            vec!["TestExtension"]
1146        );
1147    }
1148
1149    #[test]
1150    fn test_extension_table_conflicting_type_url_leaves_registry_unchanged() {
1151        let mut registry = ExtensionRegistry::new();
1152        registry
1153            .register_extension_table::<TestExtension>()
1154            .unwrap();
1155
1156        // Attempt to register a different extension table with the same type URL
1157        let result = registry.register_extension_table::<ConflictingExtension>();
1158        assert!(matches!(
1159            result,
1160            Err(RegistrationError::ConflictingTypeUrl { .. })
1161        ));
1162
1163        // Registry should still only know about the original extension table
1164        assert!(registry.has_extension(ExtensionType::ExtensionTable, "TestExtension"));
1165        assert!(!registry.has_extension(ExtensionType::ExtensionTable, "ConflictingExtension"));
1166        assert_eq!(
1167            registry.extension_names(ExtensionType::ExtensionTable),
1168            vec!["TestExtension"]
1169        );
1170    }
1171}