Skip to main content

substrait/parse/text/simple_extensions/
types.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Concrete type system for function validation in the registry.
4//!
5//! This module provides a clean, type-safe wrapper around Substrait extension types,
6//! separating function signature patterns from concrete argument types.
7
8use super::TypeExpr;
9use super::argument::{
10    EnumOptions as ParsedEnumOptions, EnumOptionsError as ParsedEnumOptionsError,
11};
12use super::extensions::TypeContext;
13use super::type_ast::TypeExprParam;
14use crate::parse::Parse;
15use crate::parse::text::simple_extensions::type_ast::TypeParseError;
16use crate::text::simple_extensions::{
17    EnumOptions as RawEnumOptions, SimpleExtensionsTypesItem, Type as RawType, TypeParamDefs,
18    TypeParamDefsItem, TypeParamDefsItemType,
19};
20use indexmap::IndexMap;
21use serde_json::Value;
22use std::convert::TryFrom;
23use std::fmt;
24use std::ops::RangeInclusive;
25use thiserror::Error;
26
27/// Write a sequence of items separated by a separator, with a start and end
28/// delimiter.
29///
30/// Start and end are only included in the output if there is at least one item.
31fn write_separated<I, T>(
32    f: &mut fmt::Formatter<'_>,
33    iter: I,
34    start: &str,
35    end: &str,
36    sep: &str,
37) -> fmt::Result
38where
39    I: IntoIterator<Item = T>,
40    T: fmt::Display,
41{
42    let mut it = iter.into_iter();
43    if let Some(first) = it.next() {
44        f.write_str(start)?;
45        write!(f, "{first}")?;
46        for item in it {
47            f.write_str(sep)?;
48            write!(f, "{item}")?;
49        }
50        f.write_str(end)
51    } else {
52        Ok(())
53    }
54}
55
56/// A pair of a key and a value, separated by a separator. For display purposes.
57struct KeyValueDisplay<K, V>(K, V, &'static str);
58
59impl<K, V> fmt::Display for KeyValueDisplay<K, V>
60where
61    K: fmt::Display,
62    V: fmt::Display,
63{
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "{}{}{}", self.0, self.2, self.1)
66    }
67}
68
69/// Non-recursive, built-in Substrait types: types with no parameters (primitive
70/// types), or simple with only primitive / literal parameters.
71#[derive(Clone, Debug, PartialEq)]
72pub enum BasicBuiltinType {
73    /// Boolean type - `bool`
74    Boolean,
75    /// 8-bit signed integer - `i8`
76    I8,
77    /// 16-bit signed integer - `i16`
78    I16,
79    /// 32-bit signed integer - `i32`
80    I32,
81    /// 64-bit signed integer - `i64`
82    I64,
83    /// 32-bit floating point - `fp32`
84    Fp32,
85    /// 64-bit floating point - `fp64`
86    Fp64,
87    /// Variable-length string - `string`
88    String,
89    /// Variable-length binary data - `binary`
90    Binary,
91    /// Calendar date - `date`
92    Date,
93    /// Year-month interval - `interval_year`
94    IntervalYear,
95    /// 128-bit UUID - `uuid`
96    Uuid,
97    /// Fixed-length character string: `FIXEDCHAR<L>`
98    FixedChar {
99        /// Length (number of characters), must be >= 1
100        length: i32,
101    },
102    /// Variable-length character string: `VARCHAR<L>`
103    VarChar {
104        /// Maximum length (number of characters), must be >= 1
105        length: i32,
106    },
107    /// Fixed-length binary data: `FIXEDBINARY<L>`
108    FixedBinary {
109        /// Length (number of bytes), must be >= 1
110        length: i32,
111    },
112    /// Fixed-point decimal: `DECIMAL<P, S>`
113    Decimal {
114        /// Precision (total digits), <= 38
115        precision: i32,
116        /// Scale (digits after decimal point), 0 <= S <= P
117        scale: i32,
118    },
119    /// Time with sub-second precision: `PRECISIONTIME<P>`
120    PrecisionTime {
121        /// Sub-second precision digits (0-12: seconds to picoseconds)
122        precision: i32,
123    },
124    /// Timestamp with sub-second precision: `PRECISIONTIMESTAMP<P>`
125    PrecisionTimestamp {
126        /// Sub-second precision digits (0-12: seconds to picoseconds)
127        precision: i32,
128    },
129    /// Timezone-aware timestamp with precision: `PRECISIONTIMESTAMPTZ<P>`
130    PrecisionTimestampTz {
131        /// Sub-second precision digits (0-12: seconds to picoseconds)
132        precision: i32,
133    },
134    /// Day-time interval: `INTERVAL_DAY<P>`
135    IntervalDay {
136        /// Sub-second precision digits (0-9: seconds to nanoseconds)
137        precision: i32,
138    },
139    /// Compound interval: `INTERVAL_COMPOUND<P>`
140    IntervalCompound {
141        /// Sub-second precision digits
142        precision: i32,
143    },
144}
145
146impl BasicBuiltinType {
147    /// Check if a string is a valid name for a builtin scalar type
148    pub fn is_name(name: &str) -> bool {
149        let lower = name.to_ascii_lowercase();
150        primitive_builtin(&lower).is_some()
151            || matches!(
152                lower.as_str(),
153                "fixedchar"
154                    | "varchar"
155                    | "fixedbinary"
156                    | "decimal"
157                    | "precisiontime"
158                    | "precision_time"
159                    | "precision_timestamp"
160                    | "precision_timestamp_tz"
161                    | "interval_day"
162                    | "interval_compound"
163            )
164    }
165}
166
167impl fmt::Display for BasicBuiltinType {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        match self {
170            BasicBuiltinType::Boolean => f.write_str("bool"),
171            BasicBuiltinType::I8 => f.write_str("i8"),
172            BasicBuiltinType::I16 => f.write_str("i16"),
173            BasicBuiltinType::I32 => f.write_str("i32"),
174            BasicBuiltinType::I64 => f.write_str("i64"),
175            BasicBuiltinType::Fp32 => f.write_str("fp32"),
176            BasicBuiltinType::Fp64 => f.write_str("fp64"),
177            BasicBuiltinType::String => f.write_str("string"),
178            BasicBuiltinType::Binary => f.write_str("binary"),
179            BasicBuiltinType::Date => f.write_str("date"),
180            BasicBuiltinType::IntervalYear => f.write_str("interval_year"),
181            BasicBuiltinType::Uuid => f.write_str("uuid"),
182            BasicBuiltinType::FixedChar { length } => write!(f, "FIXEDCHAR<{length}>"),
183            BasicBuiltinType::VarChar { length } => write!(f, "VARCHAR<{length}>"),
184            BasicBuiltinType::FixedBinary { length } => write!(f, "FIXEDBINARY<{length}>"),
185            BasicBuiltinType::Decimal { precision, scale } => {
186                write!(f, "DECIMAL<{precision}, {scale}>")
187            }
188            BasicBuiltinType::PrecisionTime { precision } => {
189                write!(f, "PRECISIONTIME<{precision}>")
190            }
191            BasicBuiltinType::PrecisionTimestamp { precision } => {
192                write!(f, "PRECISIONTIMESTAMP<{precision}>")
193            }
194            BasicBuiltinType::PrecisionTimestampTz { precision } => {
195                write!(f, "PRECISIONTIMESTAMPTZ<{precision}>")
196            }
197            BasicBuiltinType::IntervalDay { precision } => write!(f, "INTERVAL_DAY<{precision}>"),
198            BasicBuiltinType::IntervalCompound { precision } => {
199                write!(f, "INTERVAL_COMPOUND<{precision}>")
200            }
201        }
202    }
203}
204
205/// A parameter, used in parameterized types
206#[derive(Clone, Debug, PartialEq)]
207pub enum TypeParameter {
208    /// Integer parameter (e.g., precision, scale)
209    Integer(i64),
210    /// Type parameter (nested type)
211    Type(ConcreteType),
212    // TODO: Add support for other type parameters, as described in
213    // https://github.com/substrait-io/substrait/blob/35101020d961eda48f8dd1aafbc794c9e5cac077/proto/substrait/type.proto#L250-L265
214}
215
216impl fmt::Display for TypeParameter {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        match self {
219            TypeParameter::Integer(i) => write!(f, "{i}"),
220            TypeParameter::Type(t) => write!(f, "{t}"),
221        }
222    }
223}
224
225/// Parse a primitive (no type parameters) builtin type name
226fn primitive_builtin(lower_name: &str) -> Option<BasicBuiltinType> {
227    match lower_name {
228        "bool" | "boolean" => Some(BasicBuiltinType::Boolean),
229        "i8" => Some(BasicBuiltinType::I8),
230        "i16" => Some(BasicBuiltinType::I16),
231        "i32" => Some(BasicBuiltinType::I32),
232        "i64" => Some(BasicBuiltinType::I64),
233        "fp32" => Some(BasicBuiltinType::Fp32),
234        "fp64" => Some(BasicBuiltinType::Fp64),
235        "string" => Some(BasicBuiltinType::String),
236        "binary" => Some(BasicBuiltinType::Binary),
237        "date" => Some(BasicBuiltinType::Date),
238        "interval_year" => Some(BasicBuiltinType::IntervalYear),
239        "uuid" => Some(BasicBuiltinType::Uuid),
240        _ => None,
241    }
242}
243
244/// Parameter type information for type definitions
245#[derive(Clone, Debug, PartialEq)]
246pub enum ParameterConstraint {
247    /// Data type parameter
248    DataType,
249    /// Integer parameter with range constraints
250    Integer {
251        /// Minimum value (inclusive), if specified
252        min: Option<i64>,
253        /// Maximum value (inclusive), if specified
254        max: Option<i64>,
255    },
256    /// Enumeration parameter
257    Enumeration {
258        /// Valid enumeration values (validated, deduplicated)
259        options: ParsedEnumOptions,
260    },
261    /// Boolean parameter
262    Boolean,
263    /// String parameter
264    String,
265}
266
267impl ParameterConstraint {
268    /// Convert back to raw TypeParamDefsItemType
269    fn raw_type(&self) -> TypeParamDefsItemType {
270        match self {
271            ParameterConstraint::DataType => TypeParamDefsItemType::DataType,
272            ParameterConstraint::Boolean => TypeParamDefsItemType::Boolean,
273            ParameterConstraint::Integer { .. } => TypeParamDefsItemType::Integer,
274            ParameterConstraint::Enumeration { .. } => TypeParamDefsItemType::Enumeration,
275            ParameterConstraint::String => TypeParamDefsItemType::String,
276        }
277    }
278
279    /// Extract raw bounds for integer parameters (min, max)
280    fn raw_bounds(&self) -> (Option<f64>, Option<f64>) {
281        match self {
282            ParameterConstraint::Integer { min, max } => {
283                (min.map(|i| i as f64), max.map(|i| i as f64))
284            }
285            _ => (None, None),
286        }
287    }
288
289    /// Extract raw enum options for enumeration parameters
290    fn raw_options(&self) -> Option<RawEnumOptions> {
291        match self {
292            ParameterConstraint::Enumeration { options } => Some(options.clone().into()),
293            _ => None,
294        }
295    }
296
297    /// Check if a parameter value is valid for this parameter type
298    pub fn is_valid_value(&self, value: &Value) -> bool {
299        match (self, value) {
300            (ParameterConstraint::DataType, Value::String(_)) => true,
301            (ParameterConstraint::Integer { min, max }, Value::Number(n)) => {
302                if let Some(i) = n.as_i64() {
303                    min.is_none_or(|min_val| i >= min_val) && max.is_none_or(|max_val| i <= max_val)
304                } else {
305                    false
306                }
307            }
308            (ParameterConstraint::Enumeration { options }, Value::String(s)) => options.contains(s),
309            (ParameterConstraint::Boolean, Value::Bool(_)) => true,
310            (ParameterConstraint::String, Value::String(_)) => true,
311            _ => false,
312        }
313    }
314
315    fn from_raw(
316        t: TypeParamDefsItemType,
317        opts: Option<RawEnumOptions>,
318        min: Option<f64>,
319        max: Option<f64>,
320    ) -> Result<Self, TypeParamError> {
321        Ok(match t {
322            TypeParamDefsItemType::DataType => Self::DataType,
323            TypeParamDefsItemType::Boolean => Self::Boolean,
324            TypeParamDefsItemType::Integer => {
325                match (min, max) {
326                    (Some(min_f), _) if min_f.fract() != 0.0 => {
327                        return Err(TypeParamError::InvalidIntegerBounds { min, max });
328                    }
329                    (_, Some(max_f)) if max_f.fract() != 0.0 => {
330                        return Err(TypeParamError::InvalidIntegerBounds { min, max });
331                    }
332                    _ => (),
333                }
334
335                let min_i = min.map(|v| v as i64);
336                let max_i = max.map(|v| v as i64);
337                Self::Integer {
338                    min: min_i,
339                    max: max_i,
340                }
341            }
342            TypeParamDefsItemType::Enumeration => {
343                let options: ParsedEnumOptions =
344                    opts.ok_or(TypeParamError::MissingEnumOptions)?.try_into()?;
345                Self::Enumeration { options }
346            }
347            TypeParamDefsItemType::String => Self::String,
348        })
349    }
350}
351
352/// A validated type parameter with name and constraints
353#[derive(Clone, Debug, PartialEq)]
354pub struct TypeParam {
355    /// Parameter name (e.g., "K" for a type variable)
356    pub name: String,
357    /// Parameter type constraints
358    pub param_type: ParameterConstraint,
359    /// Human-readable description
360    pub description: Option<String>,
361}
362
363impl TypeParam {
364    /// Create a new type parameter
365    pub fn new(name: String, param_type: ParameterConstraint, description: Option<String>) -> Self {
366        Self {
367            name,
368            param_type,
369            description,
370        }
371    }
372
373    /// Check if a parameter value is valid
374    pub fn is_valid_value(&self, value: &Value) -> bool {
375        self.param_type.is_valid_value(value)
376    }
377}
378
379impl TryFrom<TypeParamDefsItem> for TypeParam {
380    type Error = TypeParamError;
381
382    fn try_from(item: TypeParamDefsItem) -> Result<Self, Self::Error> {
383        let name = item.name.ok_or(TypeParamError::MissingName)?;
384        let param_type =
385            ParameterConstraint::from_raw(item.type_, item.options, item.min, item.max)?;
386
387        Ok(Self {
388            name,
389            param_type,
390            description: item.description,
391        })
392    }
393}
394
395/// Error types for extension type validation
396#[derive(Debug, Error, PartialEq)]
397pub enum ExtensionTypeError {
398    /// Extension type name is invalid
399    #[error("{0}")]
400    InvalidName(#[from] InvalidTypeName),
401    /// Any type variable is invalid for concrete types
402    #[error("Any type variable is invalid for concrete types: any{}{}", id, nullability.then_some("?").unwrap_or(""))]
403    InvalidAnyTypeVariable {
404        /// The type variable name
405        id: u32,
406        /// Whether the type variable is nullable
407        nullability: bool,
408    },
409    /// Unknown type name (not a builtin, missing u! prefix for extension types)
410    #[error(
411        "Unknown type name: '{}'. Extension types must use the u! prefix (e.g., u!{})",
412        name,
413        name
414    )]
415    UnknownTypeName {
416        /// The unknown type name
417        name: String,
418    },
419    /// Parameter validation failed
420    #[error("Invalid parameter: {0}")]
421    InvalidParameter(#[from] TypeParamError),
422    /// Field type is invalid
423    #[error("Invalid structure field type: {0}")]
424    InvalidFieldType(String),
425    /// Duplicate struct field name
426    #[error("Duplicate struct field '{field_name}'")]
427    DuplicateFieldName {
428        /// The duplicated field name
429        field_name: String,
430    },
431    /// Type parameter count is invalid for the given type name
432    #[error("Type '{type_name}' expects {expected} parameters, got {actual}")]
433    InvalidParameterCount {
434        /// The type name being validated
435        type_name: String,
436        /// Expected number of parameters
437        expected: usize,
438        /// The actual number of parameters provided
439        actual: usize,
440    },
441    /// Type parameter is of the wrong kind for the given position
442    #[error("Type '{type_name}' parameter {index} must be {expected}")]
443    InvalidParameterKind {
444        /// The type name being validated
445        type_name: String,
446        /// Zero-based index of the offending parameter
447        index: usize,
448        /// Expected parameter kind (e.g., integer, type)
449        expected: &'static str,
450    },
451    /// Provided parameter value does not fit within the expected bounds
452    #[error("Type '{type_name}' parameter {index} value {value} is not within {expected}")]
453    InvalidParameterValue {
454        /// The type name being validated
455        type_name: String,
456        /// Zero-based index of the offending parameter
457        index: usize,
458        /// Provided parameter value
459        value: i64,
460        /// Description of the expected range or type
461        expected: &'static str,
462    },
463    /// Provided parameter value does not fit within the expected bounds
464    #[error("Type '{type_name}' parameter {index} value {value} is out of range {expected:?}")]
465    InvalidParameterRange {
466        /// The type name being validated
467        type_name: String,
468        /// Zero-based index of the offending parameter
469        index: usize,
470        /// Provided parameter value
471        value: i64,
472        /// Description of the expected range or type
473        expected: RangeInclusive<i32>,
474    },
475    /// Structure representation cannot be nullable
476    #[error("Structure representation cannot be nullable: {type_string}")]
477    StructureCannotBeNullable {
478        /// The type string that was nullable
479        type_string: String,
480    },
481    /// Error parsing type
482    #[error("Error parsing type: {0}")]
483    ParseType(#[from] TypeParseError),
484}
485
486/// Error types for TypeParam validation
487#[derive(Debug, Error, PartialEq)]
488pub enum TypeParamError {
489    /// Parameter name is missing
490    #[error("Parameter name is required")]
491    MissingName,
492    /// Integer parameter has non-integer min/max values
493    #[error("Integer parameter has invalid min/max values: min={min:?}, max={max:?}")]
494    InvalidIntegerBounds {
495        /// The invalid minimum value
496        min: Option<f64>,
497        /// The invalid maximum value
498        max: Option<f64>,
499    },
500    /// Enumeration parameter is missing options
501    #[error("Enumeration parameter is missing options")]
502    MissingEnumOptions,
503    /// Enumeration parameter has invalid options
504    #[error("Enumeration parameter has invalid options: {0}")]
505    InvalidEnumOptions(#[from] ParsedEnumOptionsError),
506}
507
508/// A validated Simple Extension type definition
509#[derive(Clone, Debug, PartialEq)]
510pub struct CustomType {
511    /// Type name
512    pub name: String,
513    /// Type parameters (e.g., for generic types)
514    pub parameters: Vec<TypeParam>,
515    /// Concrete structure definition, if any
516    pub structure: Option<ConcreteType>,
517    /// Whether this type can have variadic parameters
518    pub variadic: Option<bool>,
519    /// Human-readable description
520    pub description: Option<String>,
521}
522
523impl CustomType {
524    /// Check if this type name is valid according to Substrait naming rules
525    /// (see the `Identifier` rule in `substrait/grammar/SubstraitLexer.g4`).
526    /// Identifiers are case-insensitive and must start with a an ASCII letter,
527    /// `_`, or `$`, followed by ASCII letters, digits, `_`, or `$`.
528    //
529    // Note: I'm not sure if `$` is actually something we want to allow, or if
530    // `_` is, but it's in the grammar so I'm allowing it here.
531    pub fn validate_name(name: &str) -> Result<(), InvalidTypeName> {
532        let mut chars = name.chars();
533        let first = chars
534            .next()
535            .ok_or_else(|| InvalidTypeName(name.to_string()))?;
536        if !(first.is_ascii_alphabetic() || first == '_' || first == '$') {
537            return Err(InvalidTypeName(name.to_string()));
538        }
539
540        if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$') {
541            return Err(InvalidTypeName(name.to_string()));
542        }
543
544        Ok(())
545    }
546
547    /// Create a new custom type with validation
548    pub fn new(
549        name: String,
550        parameters: Vec<TypeParam>,
551        structure: Option<ConcreteType>,
552        variadic: Option<bool>,
553        description: Option<String>,
554    ) -> Result<Self, ExtensionTypeError> {
555        Self::validate_name(&name)?;
556
557        Ok(Self {
558            name,
559            parameters,
560            structure,
561            variadic,
562            description,
563        })
564    }
565}
566
567impl From<CustomType> for SimpleExtensionsTypesItem {
568    fn from(value: CustomType) -> Self {
569        // Convert parameters back to TypeParamDefs if any
570        let parameters = if value.parameters.is_empty() {
571            None
572        } else {
573            Some(TypeParamDefs(
574                value
575                    .parameters
576                    .into_iter()
577                    .map(|param| {
578                        let (min, max) = param.param_type.raw_bounds();
579                        TypeParamDefsItem {
580                            name: Some(param.name),
581                            description: param.description,
582                            type_: param.param_type.raw_type(),
583                            min,
584                            max,
585                            options: param.param_type.raw_options(),
586                            // TODO: add this to TypeParamDefsItem parsing, and
587                            // follow it through here. I'm not entirely sure
588                            // when/if it is used.
589                            optional: None,
590                        }
591                    })
592                    .collect(),
593            ))
594        };
595
596        // Convert structure back to Type if any
597        let structure = value.structure.map(Into::into);
598
599        SimpleExtensionsTypesItem {
600            name: value.name,
601            description: value.description,
602            metadata: Default::default(),
603            parameters,
604            structure,
605            variadic: value.variadic,
606            // Not tracked by `CustomType`; default to no deprecation.
607            deprecated: None,
608        }
609    }
610}
611
612impl Parse<TypeContext> for SimpleExtensionsTypesItem {
613    type Parsed = CustomType;
614    type Error = ExtensionTypeError;
615
616    fn parse(self, ctx: &mut TypeContext) -> Result<Self::Parsed, Self::Error> {
617        let name = self.name;
618        CustomType::validate_name(&name)?;
619
620        // Register this type as found
621        ctx.found(&name);
622
623        let parameters = if let Some(param_defs) = self.parameters {
624            param_defs
625                .0
626                .into_iter()
627                .map(TypeParam::try_from)
628                .collect::<Result<Vec<_>, _>>()?
629        } else {
630            Vec::new()
631        };
632
633        // Parse structure with context, so referenced extension types are recorded as linked
634        let structure = match self.structure {
635            Some(structure_data) => {
636                let parsed = Parse::parse(structure_data, ctx)?;
637                // TODO: check that the structure is valid. The `Type::Object`
638                // form of `structure_data` is by definition a non-nullable `NSTRUCT`; however,
639                // what types allowed under the `Type::String` form is less clear in the spec:
640                // See https://github.com/substrait-io/substrait/issues/920.
641                Some(parsed)
642            }
643            None => None,
644        };
645
646        Ok(CustomType {
647            name,
648            parameters,
649            structure,
650            variadic: self.variadic,
651            description: self.description,
652        })
653    }
654}
655
656impl Parse<TypeContext> for RawType {
657    type Parsed = ConcreteType;
658    type Error = ExtensionTypeError;
659
660    fn parse(self, ctx: &mut TypeContext) -> Result<Self::Parsed, Self::Error> {
661        match self {
662            RawType::String(type_string) => {
663                let parsed_type = TypeExpr::parse(&type_string)?;
664                let mut link = |name: &str| ctx.linked(name);
665                parsed_type.visit_references(&mut link);
666                let concrete = ConcreteType::try_from(parsed_type)?;
667                Ok(concrete)
668            }
669            RawType::Object(field_map) => {
670                // Type structure in Substrait must preserve field order (see
671                // substrait-io/substrait#915). The typify generation uses
672                // IndexMap to retain the YAML order so that the order of the
673                // fields in the structure matches that of the extensions file.
674                let mut fields = IndexMap::new();
675
676                for (field_name, field_type_value) in field_map {
677                    let type_string = match field_type_value {
678                        serde_json::Value::String(s) => s,
679                        _ => {
680                            return Err(ExtensionTypeError::InvalidFieldType(
681                                "Struct field types must be strings".to_string(),
682                            ));
683                        }
684                    };
685
686                    let parsed_field_type = TypeExpr::parse(&type_string)?;
687                    let mut link = |name: &str| ctx.linked(name);
688                    parsed_field_type.visit_references(&mut link);
689                    let field_concrete_type = ConcreteType::try_from(parsed_field_type)?;
690
691                    if fields
692                        .insert(field_name.clone(), field_concrete_type)
693                        .is_some()
694                    {
695                        return Err(ExtensionTypeError::DuplicateFieldName { field_name });
696                    }
697                }
698
699                Ok(ConcreteType {
700                    kind: ConcreteTypeKind::NamedStruct { fields },
701                    nullable: false,
702                })
703            }
704        }
705    }
706}
707
708/// Invalid type name error
709#[derive(Debug, Error, PartialEq)]
710#[error("invalid type name `{0}`")]
711pub struct InvalidTypeName(String);
712
713/// The structural kind of a Substrait type (builtin, list, map, etc).
714///
715/// This is almost a complete type, but is missing nullability information. It must be
716/// wrapped in a [`ConcreteType`] to form a complete type with nullable/non-nullable annotation.
717///
718/// Note that this is a recursive type - other than the [BuiltinType]s, the other variants can
719/// have type parameters that are themselves [ConcreteType]s.
720#[derive(Clone, Debug, PartialEq)]
721pub enum ConcreteTypeKind {
722    /// Built-in Substrait type (primitive or parameterized)
723    Builtin(BasicBuiltinType),
724    /// Extension type with optional parameters
725    Extension {
726        /// Extension type name
727        name: String,
728        /// Type parameters
729        parameters: Vec<TypeParameter>,
730    },
731    /// List type with element type
732    List(Box<ConcreteType>),
733    /// Map type with key and value types
734    Map {
735        /// Key type
736        key: Box<ConcreteType>,
737        /// Value type
738        value: Box<ConcreteType>,
739    },
740    /// Struct type (ordered fields without names)
741    Struct(Vec<ConcreteType>),
742    /// Named struct type (nstruct - ordered fields with names)
743    NamedStruct {
744        /// Ordered field names and types. They are in the order they should
745        /// appear in the struct - hence the use of [`IndexMap`].
746        fields: IndexMap<String, ConcreteType>,
747    },
748}
749
750impl fmt::Display for ConcreteTypeKind {
751    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
752        match self {
753            ConcreteTypeKind::Builtin(b) => write!(f, "{b}"),
754            ConcreteTypeKind::Extension { name, parameters } => {
755                write!(f, "{name}")?;
756                write_separated(f, parameters.iter(), "<", ">", ", ")
757            }
758            ConcreteTypeKind::List(elem) => write!(f, "list<{elem}>"),
759            ConcreteTypeKind::Map { key, value } => write!(f, "map<{key}, {value}>"),
760            ConcreteTypeKind::Struct(types) => {
761                write_separated(f, types.iter(), "struct<", ">", ", ")
762            }
763            ConcreteTypeKind::NamedStruct { fields } => {
764                let kvs = fields.iter().map(|(k, v)| KeyValueDisplay(k, v, ": "));
765
766                write_separated(f, kvs, "{", "}", ", ")
767            }
768        }
769    }
770}
771
772/// A concrete, fully-resolved type instance with nullability.
773#[derive(Clone, Debug, PartialEq)]
774pub struct ConcreteType {
775    /// The resolved type shape
776    pub kind: ConcreteTypeKind,
777    /// Whether this type is nullable
778    pub nullable: bool,
779}
780
781impl ConcreteType {
782    /// Create a new builtin scalar type
783    pub fn builtin(builtin_type: BasicBuiltinType, nullable: bool) -> ConcreteType {
784        ConcreteType {
785            kind: ConcreteTypeKind::Builtin(builtin_type),
786            nullable,
787        }
788    }
789
790    /// Create a new extension type reference (without parameters)
791    pub fn extension(name: String, nullable: bool) -> ConcreteType {
792        ConcreteType {
793            kind: ConcreteTypeKind::Extension {
794                name,
795                parameters: Vec::new(),
796            },
797            nullable,
798        }
799    }
800
801    /// Create a new parameterized extension type
802    pub fn extension_with_params(
803        name: String,
804        parameters: Vec<TypeParameter>,
805        nullable: bool,
806    ) -> ConcreteType {
807        ConcreteType {
808            kind: ConcreteTypeKind::Extension { name, parameters },
809            nullable,
810        }
811    }
812
813    /// Create a new list type
814    pub fn list(element_type: ConcreteType, nullable: bool) -> ConcreteType {
815        ConcreteType {
816            kind: ConcreteTypeKind::List(Box::new(element_type)),
817            nullable,
818        }
819    }
820
821    /// Create a new struct type (ordered fields without names)
822    pub fn r#struct(field_types: Vec<ConcreteType>, nullable: bool) -> ConcreteType {
823        ConcreteType {
824            kind: ConcreteTypeKind::Struct(field_types),
825            nullable,
826        }
827    }
828
829    /// Create a new map type
830    pub fn map(key_type: ConcreteType, value_type: ConcreteType, nullable: bool) -> ConcreteType {
831        ConcreteType {
832            kind: ConcreteTypeKind::Map {
833                key: Box::new(key_type),
834                value: Box::new(value_type),
835            },
836            nullable,
837        }
838    }
839
840    /// Create a new named struct type (nstruct - ordered fields with names)
841    pub fn named_struct(fields: IndexMap<String, ConcreteType>, nullable: bool) -> ConcreteType {
842        ConcreteType {
843            kind: ConcreteTypeKind::NamedStruct { fields },
844            nullable,
845        }
846    }
847
848    /// Check if this type (as a function argument) is compatible with another
849    /// type (as an input).
850    ///
851    /// Mainly checks nullability:
852    ///   - `i64?` is compatible with `i64` and `i64?` - both can be passed as
853    ///     arguments
854    ///   - `i64` is compatible with `i64` but NOT `i64?` - you can't pass a
855    ///     nullable type to a function that only accepts non-nullable arguments
856    pub fn is_compatible_with(&self, other: &ConcreteType) -> bool {
857        // Types must match exactly, but nullable types can accept non-nullable values
858        self.kind == other.kind && (self.nullable || !other.nullable)
859    }
860}
861
862impl fmt::Display for ConcreteType {
863    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
864        write!(f, "{}", self.kind)?;
865        if self.nullable {
866            write!(f, "?")?;
867        }
868        Ok(())
869    }
870}
871
872impl From<ConcreteType> for RawType {
873    fn from(val: ConcreteType) -> Self {
874        match val.kind {
875            ConcreteTypeKind::NamedStruct { fields } => RawType::Object(
876                fields
877                    .into_iter()
878                    .map(|(name, ty)| (name, serde_json::Value::String(ty.to_string())))
879                    .collect(),
880            ),
881            _ => RawType::String(val.to_string()),
882        }
883    }
884}
885
886/// Extract and validate an integer parameter for a built-in type.
887///
888/// For `DECIMAL<10,2>`, this validates that `10` (index 0) and `2` (index 1)
889/// are integers within their required ranges (precision 1-38, scale
890/// 0-precision).
891///
892/// - `type_name`: Type being validated (for error messages, e.g., "DECIMAL")
893/// - `index`: Parameter position (0-based, e.g., 0 for precision, 1 for scale);
894///   needed for error messages
895/// - `param`: The parameter to validate
896/// - `range`: Optional bounds to enforce (e.g., `Some(1..=38)` for precision)
897fn expect_integer_param(
898    type_name: &str,
899    index: usize,
900    param: &TypeExprParam<'_>,
901    range: Option<RangeInclusive<i32>>,
902) -> Result<i32, ExtensionTypeError> {
903    let value = match param {
904        TypeExprParam::Integer(value) => {
905            i32::try_from(*value).map_err(|_| ExtensionTypeError::InvalidParameterValue {
906                type_name: type_name.to_string(),
907                index,
908                value: *value,
909                expected: "an i32",
910            })
911        }
912        _ => Err(ExtensionTypeError::InvalidParameterKind {
913            type_name: type_name.to_string(),
914            index,
915            expected: "an integer",
916        }),
917    }?;
918
919    if let Some(range) = range {
920        if range.contains(&value) {
921            return Ok(value);
922        }
923        return Err(ExtensionTypeError::InvalidParameterRange {
924            type_name: type_name.to_string(),
925            index,
926            value: i64::from(value),
927            expected: range,
928        });
929    }
930
931    Ok(value)
932}
933
934/// Helper function - checks that param length matches expectations, returns
935/// error if not. Assumes a fixed number of expected parameters.
936fn expect_param_len(
937    type_name: &str,
938    params: &[TypeExprParam<'_>],
939    expected: usize,
940) -> Result<(), ExtensionTypeError> {
941    if params.len() != expected {
942        return Err(ExtensionTypeError::InvalidParameterCount {
943            type_name: type_name.to_string(),
944            expected,
945            actual: params.len(),
946        });
947    }
948    Ok(())
949}
950
951/// Helper function - expect a type parameter, and return the [ConcreteType] if it is a [TypeExpr]
952/// or an error if not.
953fn expect_type_argument<'a>(
954    type_name: &str,
955    index: usize,
956    param: TypeExprParam<'a>,
957) -> Result<ConcreteType, ExtensionTypeError> {
958    match param {
959        TypeExprParam::Type(t) => ConcreteType::try_from(t),
960        TypeExprParam::Integer(_) => Err(ExtensionTypeError::InvalidParameterKind {
961            type_name: type_name.to_string(),
962            index,
963            expected: "a type",
964        }),
965    }
966}
967
968impl<'a> TryFrom<TypeExprParam<'a>> for TypeParameter {
969    type Error = ExtensionTypeError;
970
971    fn try_from(param: TypeExprParam<'a>) -> Result<Self, Self::Error> {
972        Ok(match param {
973            TypeExprParam::Integer(v) => TypeParameter::Integer(v),
974            TypeExprParam::Type(t) => TypeParameter::Type(ConcreteType::try_from(t)?),
975        })
976    }
977}
978
979/// Parse a builtin type. Returns an `ExtensionTypeError` if the type name is
980/// matched, but parameters are incorrect; returns `Some(None)` if the type is
981/// not known.
982fn parse_builtin<'a>(
983    display_name: &str,
984    lower_name: &str,
985    params: &[TypeExprParam<'a>],
986) -> Result<Option<BasicBuiltinType>, ExtensionTypeError> {
987    if let Some(builtin) = primitive_builtin(lower_name) {
988        expect_param_len(display_name, params, 0)?;
989        return Ok(Some(builtin));
990    }
991
992    match lower_name {
993        // Parameterized builtins
994        "fixedchar" => {
995            expect_param_len(display_name, params, 1)?;
996            let length = expect_integer_param(display_name, 0, &params[0], None)?;
997            Ok(Some(BasicBuiltinType::FixedChar { length }))
998        }
999        "varchar" => {
1000            expect_param_len(display_name, params, 1)?;
1001            let length = expect_integer_param(display_name, 0, &params[0], None)?;
1002            Ok(Some(BasicBuiltinType::VarChar { length }))
1003        }
1004        "fixedbinary" => {
1005            expect_param_len(display_name, params, 1)?;
1006            let length = expect_integer_param(display_name, 0, &params[0], None)?;
1007            Ok(Some(BasicBuiltinType::FixedBinary { length }))
1008        }
1009        "decimal" => {
1010            expect_param_len(display_name, params, 2)?;
1011            let precision = expect_integer_param(display_name, 0, &params[0], Some(1..=38))?;
1012            let scale = expect_integer_param(display_name, 1, &params[1], Some(0..=precision))?;
1013            Ok(Some(BasicBuiltinType::Decimal { precision, scale }))
1014        }
1015        "precisiontime" | "precision_time" => {
1016            expect_param_len(display_name, params, 1)?;
1017            let precision = expect_integer_param(display_name, 0, &params[0], Some(0..=12))?;
1018            Ok(Some(BasicBuiltinType::PrecisionTime { precision }))
1019        }
1020        "precision_timestamp" => {
1021            expect_param_len(display_name, params, 1)?;
1022            let precision = expect_integer_param(display_name, 0, &params[0], Some(0..=12))?;
1023            Ok(Some(BasicBuiltinType::PrecisionTimestamp { precision }))
1024        }
1025        "precision_timestamp_tz" => {
1026            expect_param_len(display_name, params, 1)?;
1027            let precision = expect_integer_param(display_name, 0, &params[0], Some(0..=12))?;
1028            Ok(Some(BasicBuiltinType::PrecisionTimestampTz { precision }))
1029        }
1030        "interval_day" => {
1031            expect_param_len(display_name, params, 1)?;
1032            let precision = expect_integer_param(display_name, 0, &params[0], Some(0..=9))?;
1033            Ok(Some(BasicBuiltinType::IntervalDay { precision }))
1034        }
1035        "interval_compound" => {
1036            expect_param_len(display_name, params, 1)?;
1037            let precision = expect_integer_param(display_name, 0, &params[0], None)?;
1038            Ok(Some(BasicBuiltinType::IntervalCompound { precision }))
1039        }
1040        _ => Ok(None),
1041    }
1042}
1043
1044impl<'a> TryFrom<TypeExpr<'a>> for ConcreteType {
1045    type Error = ExtensionTypeError;
1046
1047    fn try_from(parsed_type: TypeExpr<'a>) -> Result<Self, Self::Error> {
1048        match parsed_type {
1049            TypeExpr::Simple(name, params, nullable) => {
1050                let lower = name.to_ascii_lowercase();
1051
1052                match lower.as_str() {
1053                    "list" => {
1054                        expect_param_len(name, &params, 1)?;
1055                        let element =
1056                            expect_type_argument(name, 0, params.into_iter().next().unwrap())?;
1057                        return Ok(ConcreteType::list(element, nullable));
1058                    }
1059                    "map" => {
1060                        expect_param_len(name, &params, 2)?;
1061                        let mut iter = params.into_iter();
1062                        let key = expect_type_argument(name, 0, iter.next().unwrap())?;
1063                        let value = expect_type_argument(name, 1, iter.next().unwrap())?;
1064                        return Ok(ConcreteType::map(key, value, nullable));
1065                    }
1066                    "struct" => {
1067                        let field_types = params
1068                            .into_iter()
1069                            .enumerate()
1070                            .map(|(idx, param)| expect_type_argument(name, idx, param))
1071                            .collect::<Result<Vec<_>, _>>()?;
1072                        return Ok(ConcreteType::r#struct(field_types, nullable));
1073                    }
1074                    _ => {}
1075                }
1076
1077                if let Some(builtin) = parse_builtin(name, lower.as_str(), &params)? {
1078                    return Ok(ConcreteType::builtin(builtin, nullable));
1079                }
1080
1081                // Simple types that aren't builtins are unknown
1082                // Extension types MUST use the u! prefix
1083                Err(ExtensionTypeError::UnknownTypeName {
1084                    name: name.to_string(),
1085                })
1086            }
1087            TypeExpr::UserDefined(name, params, nullable) => {
1088                let parameters = params
1089                    .into_iter()
1090                    .map(TypeParameter::try_from)
1091                    .collect::<Result<Vec<_>, _>>()?;
1092                Ok(ConcreteType::extension_with_params(
1093                    name.to_string(),
1094                    parameters,
1095                    nullable,
1096                ))
1097            }
1098            TypeExpr::TypeVariable(id, nullability) => {
1099                Err(ExtensionTypeError::InvalidAnyTypeVariable { id, nullability })
1100            }
1101        }
1102    }
1103}
1104
1105#[cfg(test)]
1106mod tests {
1107    use super::super::extensions::TypeContext;
1108    use super::*;
1109    use crate::parse::text::simple_extensions::TypeExpr;
1110    use crate::parse::text::simple_extensions::argument::EnumOptions as ParsedEnumOptions;
1111    use crate::text::simple_extensions;
1112    use std::iter::FromIterator;
1113
1114    /// Create a [ConcreteType] from a [BuiltinType]
1115    fn concretize(builtin: BasicBuiltinType) -> ConcreteType {
1116        ConcreteType::builtin(builtin, false)
1117    }
1118
1119    /// Parse a string into a [ConcreteType]
1120    fn parse_type(expr: &str) -> ConcreteType {
1121        let parsed = TypeExpr::parse(expr).unwrap();
1122        ConcreteType::try_from(parsed).unwrap()
1123    }
1124
1125    /// Parse a string into a [ConcreteType], returning the result
1126    fn parse_type_result(expr: &str) -> Result<ConcreteType, ExtensionTypeError> {
1127        let parsed = TypeExpr::parse(expr).unwrap();
1128        ConcreteType::try_from(parsed)
1129    }
1130
1131    /// Parse a string into a builtin [ConcreteType], with no unresolved
1132    /// extension references
1133    fn parse_simple(s: &str) -> ConcreteType {
1134        let parsed = TypeExpr::parse(s).unwrap();
1135
1136        let mut refs = Vec::new();
1137        parsed.visit_references(&mut |name| refs.push(name.to_string()));
1138        assert!(refs.is_empty(), "{s} should not add an extension reference");
1139
1140        ConcreteType::try_from(parsed).unwrap()
1141    }
1142
1143    /// Create a type parameter from a type expression string
1144    fn type_param(expr: &str) -> TypeParameter {
1145        TypeParameter::Type(parse_type(expr))
1146    }
1147
1148    /// Create an extension type
1149    fn extension(name: &str, parameters: Vec<TypeParameter>, nullable: bool) -> ConcreteType {
1150        ConcreteType::extension_with_params(name.to_string(), parameters, nullable)
1151    }
1152
1153    /// Convert a custom type to raw and back, ensuring round-trip consistency
1154    fn round_trip(custom: &CustomType) {
1155        let item: simple_extensions::SimpleExtensionsTypesItem = custom.clone().into();
1156        let mut ctx = TypeContext::default();
1157        let parsed = Parse::parse(item, &mut ctx).unwrap();
1158        assert_eq!(&parsed, custom);
1159    }
1160
1161    /// Create a raw named struct (e.g. straight from YAML) from field name and
1162    /// type pairs
1163    fn raw_named_struct(fields: &[(&str, &str)]) -> RawType {
1164        RawType::Object(
1165            fields
1166                .iter()
1167                .map(|(name, ty)| ((*name).into(), serde_json::Value::String((*ty).into())))
1168                .collect(),
1169        )
1170    }
1171
1172    #[test]
1173    fn test_builtin_scalar_parsing() {
1174        let cases = vec![
1175            ("bool", Some(BasicBuiltinType::Boolean)),
1176            ("i8", Some(BasicBuiltinType::I8)),
1177            ("i16", Some(BasicBuiltinType::I16)),
1178            ("i32", Some(BasicBuiltinType::I32)),
1179            ("i64", Some(BasicBuiltinType::I64)),
1180            ("fp32", Some(BasicBuiltinType::Fp32)),
1181            ("fp64", Some(BasicBuiltinType::Fp64)),
1182            ("STRING", Some(BasicBuiltinType::String)),
1183            ("binary", Some(BasicBuiltinType::Binary)),
1184            ("uuid", Some(BasicBuiltinType::Uuid)),
1185            ("date", Some(BasicBuiltinType::Date)),
1186            ("interval_year", Some(BasicBuiltinType::IntervalYear)),
1187            ("invalid", None),
1188        ];
1189
1190        for (input, expected) in cases {
1191            let result = parse_builtin(input, input.to_ascii_lowercase().as_str(), &[]).unwrap();
1192            match expected {
1193                Some(expected_type) => {
1194                    assert_eq!(
1195                        result,
1196                        Some(expected_type),
1197                        "expected builtin type for {input}"
1198                    );
1199                }
1200                None => {
1201                    assert!(result.is_none(), "expected parsing {input} to fail");
1202                }
1203            }
1204        }
1205    }
1206
1207    #[test]
1208    fn test_parameterized_builtin_types() {
1209        let cases = vec![
1210            (
1211                "precisiontime<2>",
1212                concretize(BasicBuiltinType::PrecisionTime { precision: 2 }),
1213            ),
1214            (
1215                "precision_timestamp<1>",
1216                concretize(BasicBuiltinType::PrecisionTimestamp { precision: 1 }),
1217            ),
1218            (
1219                "precision_timestamp_tz<5>",
1220                concretize(BasicBuiltinType::PrecisionTimestampTz { precision: 5 }),
1221            ),
1222            (
1223                "DECIMAL<10,2>",
1224                concretize(BasicBuiltinType::Decimal {
1225                    precision: 10,
1226                    scale: 2,
1227                }),
1228            ),
1229            (
1230                "fixedchar<12>",
1231                concretize(BasicBuiltinType::FixedChar { length: 12 }),
1232            ),
1233            (
1234                "VarChar<42>",
1235                concretize(BasicBuiltinType::VarChar { length: 42 }),
1236            ),
1237            (
1238                "fixedbinary<8>",
1239                concretize(BasicBuiltinType::FixedBinary { length: 8 }),
1240            ),
1241            (
1242                "interval_day<7>",
1243                concretize(BasicBuiltinType::IntervalDay { precision: 7 }),
1244            ),
1245            (
1246                "interval_compound<6>",
1247                concretize(BasicBuiltinType::IntervalCompound { precision: 6 }),
1248            ),
1249        ];
1250
1251        for (expr, expected) in cases {
1252            let found = parse_simple(expr);
1253            assert_eq!(found, expected, "unexpected type for {expr}");
1254        }
1255    }
1256
1257    #[test]
1258    fn test_parameterized_builtin_range_errors() {
1259        use ExtensionTypeError::InvalidParameterRange;
1260
1261        let cases = vec![
1262            ("precisiontime<13>", "precisiontime", 0, 13, 0..=12),
1263            ("precisiontime<-1>", "precisiontime", 0, -1, 0..=12),
1264            (
1265                "precision_timestamp<13>",
1266                "precision_timestamp",
1267                0,
1268                13,
1269                0..=12,
1270            ),
1271            (
1272                "precision_timestamp<-1>",
1273                "precision_timestamp",
1274                0,
1275                -1,
1276                0..=12,
1277            ),
1278            (
1279                "precision_timestamp_tz<20>",
1280                "precision_timestamp_tz",
1281                0,
1282                20,
1283                0..=12,
1284            ),
1285            ("interval_day<10>", "interval_day", 0, 10, 0..=9),
1286            ("DECIMAL<39,0>", "DECIMAL", 0, 39, 1..=38),
1287            ("DECIMAL<0,0>", "DECIMAL", 0, 0, 1..=38),
1288            ("DECIMAL<10,-1>", "DECIMAL", 1, -1, 0..=10),
1289            ("DECIMAL<10,12>", "DECIMAL", 1, 12, 0..=10),
1290        ];
1291
1292        for (expr, expected_type, expected_index, expected_value, expected_range) in cases {
1293            match parse_type_result(expr) {
1294                Ok(value) => panic!("expected error parsing {expr}, got {value:?}"),
1295                Err(InvalidParameterRange {
1296                    type_name,
1297                    index,
1298                    value,
1299                    expected,
1300                }) => {
1301                    assert_eq!(type_name, expected_type, "unexpected type for {expr}");
1302                    assert_eq!(index, expected_index, "unexpected index for {expr}");
1303                    assert_eq!(
1304                        value,
1305                        i64::from(expected_value),
1306                        "unexpected value for {expr}"
1307                    );
1308                    assert_eq!(expected, expected_range, "unexpected range for {expr}");
1309                }
1310                Err(other) => panic!("expected InvalidParameterRange for {expr}, got {other:?}"),
1311            }
1312        }
1313    }
1314
1315    #[test]
1316    fn test_container_types() {
1317        let cases = vec![
1318            (
1319                "List<i32>",
1320                ConcreteType::list(ConcreteType::builtin(BasicBuiltinType::I32, false), false),
1321            ),
1322            (
1323                "List<fp64?>",
1324                ConcreteType::list(ConcreteType::builtin(BasicBuiltinType::Fp64, true), false),
1325            ),
1326            (
1327                "Map?<i64, string?>",
1328                ConcreteType::map(
1329                    ConcreteType::builtin(BasicBuiltinType::I64, false),
1330                    ConcreteType::builtin(BasicBuiltinType::String, true),
1331                    true,
1332                ),
1333            ),
1334            (
1335                "Struct?<i8, string?>",
1336                ConcreteType::r#struct(
1337                    vec![
1338                        ConcreteType::builtin(BasicBuiltinType::I8, false),
1339                        ConcreteType::builtin(BasicBuiltinType::String, true),
1340                    ],
1341                    true,
1342                ),
1343            ),
1344        ];
1345
1346        for (expr, expected) in cases {
1347            assert_eq!(parse_type(expr), expected, "unexpected parse for {expr}");
1348        }
1349    }
1350
1351    #[test]
1352    fn test_extension_types() {
1353        let cases = vec![
1354            (
1355                "u!geo<List<i32>, 10>",
1356                extension(
1357                    "geo",
1358                    vec![type_param("List<i32>"), TypeParameter::Integer(10)],
1359                    false,
1360                ),
1361            ),
1362            (
1363                "u!Geo?<List<i32?>>",
1364                extension("Geo", vec![type_param("List<i32?>")], true),
1365            ),
1366            (
1367                "u!Custom<string?, bool>",
1368                extension(
1369                    "Custom",
1370                    vec![
1371                        type_param("string?"),
1372                        TypeParameter::Type(ConcreteType::builtin(
1373                            BasicBuiltinType::Boolean,
1374                            false,
1375                        )),
1376                    ],
1377                    false,
1378                ),
1379            ),
1380        ];
1381
1382        for (expr, expected) in cases {
1383            assert_eq!(
1384                parse_type(expr),
1385                expected,
1386                "unexpected extension for {expr}"
1387            );
1388        }
1389    }
1390
1391    #[test]
1392    fn test_parameter_type_validation() {
1393        let int_param = ParameterConstraint::Integer {
1394            min: Some(1),
1395            max: Some(10),
1396        };
1397        let enum_param = ParameterConstraint::Enumeration {
1398            options: ParsedEnumOptions::try_from(simple_extensions::EnumOptions(vec![
1399                "OVERFLOW".to_string(),
1400                "ERROR".to_string(),
1401            ]))
1402            .unwrap(),
1403        };
1404
1405        let cases = vec![
1406            (&int_param, Value::Number(5.into()), true),
1407            (&int_param, Value::Number(0.into()), false),
1408            (&int_param, Value::Number(11.into()), false),
1409            (&int_param, Value::String("not a number".into()), false),
1410            (&enum_param, Value::String("OVERFLOW".into()), true),
1411            (&enum_param, Value::String("INVALID".into()), false),
1412        ];
1413
1414        for (param, value, expected) in cases {
1415            assert_eq!(
1416                param.is_valid_value(&value),
1417                expected,
1418                "unexpected validation result for {value:?}"
1419            );
1420        }
1421    }
1422
1423    #[test]
1424    fn test_type_round_trip_display() {
1425        // (example, canonical form)
1426        let cases = vec![
1427            ("i32", "i32"),
1428            ("I64?", "i64?"),
1429            ("list<string>", "list<string>"),
1430            ("List<STRING?>", "list<string?>"),
1431            ("map<i32, list<string>>", "map<i32, list<string>>"),
1432            ("struct<i8, string?>", "struct<i8, string?>"),
1433            (
1434                "Struct<List<i32>, Map<string, list<i64?>>>",
1435                "struct<list<i32>, map<string, list<i64?>>>",
1436            ),
1437            (
1438                "Map<List<I32?>, Struct<string, list<i64?>>>",
1439                "map<list<i32?>, struct<string, list<i64?>>>",
1440            ),
1441            ("u!custom<i32>", "custom<i32>"),
1442        ];
1443
1444        for (input, expected) in cases {
1445            let parsed = TypeExpr::parse(input).unwrap();
1446            let concrete = ConcreteType::try_from(parsed).unwrap();
1447            let actual = concrete.to_string();
1448
1449            assert_eq!(actual, expected, "unexpected display for {input}");
1450        }
1451    }
1452
1453    /// Test that named struct field order preserves the structure order when
1454    /// round-tripping through RawType (Substrait #915).
1455    #[test]
1456    fn test_named_struct_field_order_stability() -> Result<(), ExtensionTypeError> {
1457        let raw = RawType::Object(
1458            [("beta", "i32"), ("alpha", "string?")]
1459                .into_iter()
1460                .map(|(name, ty)| (name.to_string(), Value::String(ty.to_string())))
1461                .collect(),
1462        );
1463        let mut ctx = TypeContext::default();
1464        let concrete = Parse::parse(raw, &mut ctx)?;
1465
1466        let round_tripped: RawType = concrete.into();
1467        match round_tripped {
1468            RawType::Object(result_map) => {
1469                let keys: Vec<_> = result_map.keys().collect();
1470                assert_eq!(
1471                    keys,
1472                    vec!["beta", "alpha"],
1473                    "field order should be preserved"
1474                );
1475            }
1476            other => panic!("expected Object, got {other:?}"),
1477        }
1478
1479        Ok(())
1480    }
1481
1482    #[test]
1483    fn test_integer_param_bounds_round_trip() {
1484        let cases = vec![
1485            (
1486                "bounded",
1487                simple_extensions::TypeParamDefsItem {
1488                    name: Some("K".to_string()),
1489                    description: None,
1490                    type_: simple_extensions::TypeParamDefsItemType::Integer,
1491                    min: Some(1.0),
1492                    max: Some(10.0),
1493                    options: None,
1494                    optional: None,
1495                },
1496                Ok((Some(1), Some(10))),
1497            ),
1498            (
1499                "fractional_min",
1500                simple_extensions::TypeParamDefsItem {
1501                    name: Some("K".to_string()),
1502                    description: None,
1503                    type_: simple_extensions::TypeParamDefsItemType::Integer,
1504                    min: Some(1.5),
1505                    max: None,
1506                    options: None,
1507                    optional: None,
1508                },
1509                Err(TypeParamError::InvalidIntegerBounds {
1510                    min: Some(1.5),
1511                    max: None,
1512                }),
1513            ),
1514            (
1515                "fractional_max",
1516                simple_extensions::TypeParamDefsItem {
1517                    name: Some("K".to_string()),
1518                    description: None,
1519                    type_: simple_extensions::TypeParamDefsItemType::Integer,
1520                    min: None,
1521                    max: Some(9.9),
1522                    options: None,
1523                    optional: None,
1524                },
1525                Err(TypeParamError::InvalidIntegerBounds {
1526                    min: None,
1527                    max: Some(9.9),
1528                }),
1529            ),
1530        ];
1531
1532        for (label, item, expected) in cases {
1533            match (TypeParam::try_from(item), expected) {
1534                (Ok(tp), Ok((expected_min, expected_max))) => match tp.param_type {
1535                    ParameterConstraint::Integer { min, max } => {
1536                        assert_eq!(min, expected_min, "min mismatch for {label}");
1537                        assert_eq!(max, expected_max, "max mismatch for {label}");
1538                    }
1539                    _ => panic!("expected integer param type for {label}"),
1540                },
1541                (Err(actual_err), Err(expected_err)) => {
1542                    assert_eq!(actual_err, expected_err, "unexpected error for {label}");
1543                }
1544                (result, expected) => {
1545                    panic!("unexpected result for {label}: got {result:?}, expected {expected:?}")
1546                }
1547            }
1548        }
1549    }
1550
1551    #[test]
1552    fn test_custom_type_round_trip() -> Result<(), ExtensionTypeError> {
1553        let fields = IndexMap::from_iter([
1554            (
1555                "x".to_string(),
1556                ConcreteType::builtin(BasicBuiltinType::Fp64, false),
1557            ),
1558            (
1559                "y".to_string(),
1560                ConcreteType::builtin(BasicBuiltinType::Fp64, false),
1561            ),
1562        ]);
1563
1564        let cases = vec![
1565            CustomType::new(
1566                "AliasType".to_string(),
1567                vec![],
1568                Some(ConcreteType::builtin(BasicBuiltinType::I32, false)),
1569                None,
1570                Some("a test alias type".to_string()),
1571            )?,
1572            CustomType::new(
1573                "Point".to_string(),
1574                vec![TypeParam::new(
1575                    "T".to_string(),
1576                    ParameterConstraint::DataType,
1577                    Some("a numeric value".to_string()),
1578                )],
1579                Some(ConcreteType::named_struct(fields, false)),
1580                None,
1581                None,
1582            )?,
1583        ];
1584
1585        for custom in cases {
1586            round_trip(&custom);
1587        }
1588
1589        Ok(())
1590    }
1591
1592    #[test]
1593    fn test_invalid_type_names() {
1594        let cases = vec![
1595            ("", false),
1596            ("bad name", false),
1597            ("9bad", false),
1598            ("bad-name", false),
1599            ("bad.name", false),
1600            ("GoodName", true),
1601            ("also_good", true),
1602            ("_underscore", true),
1603            ("$dollar", true),
1604            ("CamelCase123", true),
1605        ];
1606
1607        for (name, expected_ok) in cases {
1608            let result = CustomType::validate_name(name);
1609            assert_eq!(
1610                result.is_ok(),
1611                expected_ok,
1612                "unexpected validation for {name}"
1613            );
1614        }
1615    }
1616
1617    #[test]
1618    fn test_ext_type_to_concrete_type() -> Result<(), ExtensionTypeError> {
1619        let cases = vec![
1620            (
1621                "alias",
1622                RawType::String("i32".to_string()),
1623                ConcreteType::builtin(BasicBuiltinType::I32, false),
1624            ),
1625            (
1626                "named_struct",
1627                raw_named_struct(&[("field1", "fp64"), ("field2", "i32?")]),
1628                ConcreteType::named_struct(
1629                    IndexMap::from_iter([
1630                        (
1631                            "field1".to_string(),
1632                            ConcreteType::builtin(BasicBuiltinType::Fp64, false),
1633                        ),
1634                        (
1635                            "field2".to_string(),
1636                            ConcreteType::builtin(BasicBuiltinType::I32, true),
1637                        ),
1638                    ]),
1639                    false,
1640                ),
1641            ),
1642        ];
1643
1644        for (label, raw, expected) in cases {
1645            let mut ctx = TypeContext::default();
1646            let parsed = Parse::parse(raw, &mut ctx)?;
1647            assert_eq!(parsed, expected, "unexpected type for {label}");
1648        }
1649
1650        Ok(())
1651    }
1652
1653    #[test]
1654    fn test_custom_type_parsing() -> Result<(), ExtensionTypeError> {
1655        let cases = vec![
1656            (
1657                "alias",
1658                simple_extensions::SimpleExtensionsTypesItem {
1659                    deprecated: None,
1660                    name: "Alias".to_string(),
1661                    description: Some("Alias type".to_string()),
1662                    metadata: Default::default(),
1663                    parameters: None,
1664                    structure: Some(RawType::String("BINARY".to_string())),
1665                    variadic: None,
1666                },
1667                "Alias",
1668                Some("Alias type"),
1669                Some(ConcreteType::builtin(BasicBuiltinType::Binary, false)),
1670            ),
1671            (
1672                "named_struct",
1673                simple_extensions::SimpleExtensionsTypesItem {
1674                    deprecated: None,
1675                    name: "Point".to_string(),
1676                    description: Some("A 2D point".to_string()),
1677                    metadata: Default::default(),
1678                    parameters: None,
1679                    structure: Some(raw_named_struct(&[("x", "fp64"), ("y", "fp64?")])),
1680                    variadic: None,
1681                },
1682                "Point",
1683                Some("A 2D point"),
1684                Some(ConcreteType::named_struct(
1685                    IndexMap::from_iter([
1686                        (
1687                            "x".to_string(),
1688                            ConcreteType::builtin(BasicBuiltinType::Fp64, false),
1689                        ),
1690                        (
1691                            "y".to_string(),
1692                            ConcreteType::builtin(BasicBuiltinType::Fp64, true),
1693                        ),
1694                    ]),
1695                    false,
1696                )),
1697            ),
1698            (
1699                "no_structure",
1700                simple_extensions::SimpleExtensionsTypesItem {
1701                    deprecated: None,
1702                    name: "Opaque".to_string(),
1703                    description: None,
1704                    metadata: Default::default(),
1705                    parameters: None,
1706                    structure: None,
1707                    variadic: Some(true),
1708                },
1709                "Opaque",
1710                None,
1711                None,
1712            ),
1713        ];
1714
1715        for (label, item, expected_name, expected_description, expected_structure) in cases {
1716            let mut ctx = TypeContext::default();
1717            let parsed = Parse::parse(item, &mut ctx)?;
1718            assert_eq!(parsed.name, expected_name);
1719            assert_eq!(
1720                parsed.description.as_deref(),
1721                expected_description,
1722                "description mismatch for {label}"
1723            );
1724            assert_eq!(
1725                parsed.structure, expected_structure,
1726                "structure mismatch for {label}"
1727            );
1728        }
1729
1730        Ok(())
1731    }
1732}