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