Skip to main content

substrait_explain/textify/
types.rs

1use std::fmt;
2use std::ops::Deref;
3
4use ptype::parameter::Parameter;
5use substrait::proto;
6use substrait::proto::r#type::{self as ptype};
7
8use super::foundation::{NONSPECIFIC, Scope};
9use super::{PlanError, Textify};
10use crate::extensions::simple::{CompoundName, ExtensionKind};
11use crate::textify::foundation::{MaybeToken, Visibility};
12
13const NULLABILITY_UNSPECIFIED: &str = "⁉";
14
15impl Textify for ptype::Nullability {
16    fn name() -> &'static str {
17        "Nullability"
18    }
19
20    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
21        match self {
22            ptype::Nullability::Unspecified => {
23                ctx.push_error(
24                    PlanError::invalid("Nullability", NONSPECIFIC, "Nullability left Unspecified")
25                        .into(),
26                );
27
28                // TODO: what should unspecified Nullabilitylook like?
29                w.write_str(NULLABILITY_UNSPECIFIED)?;
30            }
31            ptype::Nullability::Nullable => write!(w, "?")?,
32            ptype::Nullability::Required => {}
33        };
34        Ok(())
35    }
36}
37
38/// A valid identifier is a sequence of ASCII letters, digits, and underscores,
39/// starting with a letter.
40///
41/// We could expand this at some point to include any valid Unicode identifier
42/// (see <https://docs.rs/unicode-ident/latest/unicode_ident/>), but that seems
43/// overboard for now.
44pub fn is_identifer(s: &str) -> bool {
45    let mut chars = s.chars();
46    let first = match chars.next() {
47        Some(c) => c,
48        None => return false,
49    };
50
51    if !first.is_ascii_alphabetic() {
52        return false;
53    }
54
55    for c in chars {
56        if !c.is_ascii_alphanumeric() && c != '_' {
57            return false;
58        }
59    }
60
61    true
62}
63
64/// Escape a string for use in a literal or quoted identifier.
65pub fn escaped(s: &str) -> impl fmt::Display + fmt::Debug {
66    s.escape_debug()
67}
68
69/// The name of a something to be represented. It will be displayed on its own
70/// if the string is a proper identifier, or in double quotes if it is not.
71#[derive(Debug, Copy, Clone)]
72pub struct Name<'a>(pub &'a str);
73
74impl<'a> fmt::Display for Name<'a> {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        if is_identifer(self.0) {
77            write!(f, "{}", self.0)
78        } else {
79            write!(f, "\"{}\"", escaped(self.0))
80        }
81    }
82}
83
84impl<'a> Textify for Name<'a> {
85    fn name() -> &'static str {
86        "Name"
87    }
88
89    fn textify<S: Scope, W: fmt::Write>(&self, _ctx: &S, w: &mut W) -> fmt::Result {
90        write!(w, "{self}")
91    }
92}
93
94#[derive(Debug, Copy, Clone)]
95pub struct Anchor {
96    reference: u32,
97    required: bool,
98}
99
100impl Anchor {
101    pub fn new(reference: u32, required: bool) -> Self {
102        Self {
103            reference,
104            required,
105        }
106    }
107}
108
109impl Textify for Anchor {
110    fn name() -> &'static str {
111        "Anchor"
112    }
113
114    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
115        match ctx.options().show_simple_extension_anchors {
116            Visibility::Never => return Ok(()),
117            Visibility::Required if !self.required => {
118                return Ok(());
119            }
120            Visibility::Required => {}
121            Visibility::Always => {}
122        }
123        write!(w, "#{}", self.reference)
124    }
125}
126
127#[derive(Debug, Copy, Clone)]
128pub struct NamedAnchor<'a> {
129    /// The full stored compound name, e.g. `"equal:any_any"` or `"add"`.
130    pub name: MaybeToken<&'a CompoundName>,
131    pub anchor: u32,
132    /// True if the compound name is unique across all URNs for this extension
133    /// kind (i.e. no other URN registers the same full compound name).
134    /// anchor shown when `false`.
135    pub unique: bool,
136    /// True if the base name (part before the first `:`) is unique for this
137    /// extension kind. signature shown when `false`.
138    pub base_name_unique: bool,
139}
140
141impl<'a> NamedAnchor<'a> {
142    /// Lookup an anchor in the extensions, and return a NamedAnchor.
143    pub fn lookup<S: Scope>(ctx: &'a S, kind: ExtensionKind, anchor: u32) -> Self {
144        if kind == ExtensionKind::Function {
145            return match ctx.extensions().lookup_function(anchor) {
146                Ok(r) => Self {
147                    name: MaybeToken(Ok(r.name)),
148                    anchor,
149                    unique: r.name_unique,
150                    base_name_unique: r.base_name_unique,
151                },
152                Err(e) => Self {
153                    name: MaybeToken(Err(ctx.failure(e))),
154                    anchor,
155                    unique: false,
156                    base_name_unique: false,
157                },
158            };
159        }
160        // For non-function kinds, use find_by_anchor + is_name_unique.
161        // base_name_unique defaults to true since non-function names don't use
162        // signature suffixes.
163        let ext = ctx.extensions().find_by_anchor(kind, anchor);
164        let (name, unique, base_name_unique) = match ext {
165            Ok((_, n)) => {
166                let unique = match ctx.extensions().is_name_unique(kind, anchor, n.full()) {
167                    Ok(u) => u,
168                    Err(e) => {
169                        ctx.push_error(e.into());
170                        false
171                    }
172                };
173                (MaybeToken(Ok(n)), unique, true)
174            }
175            Err(e) => (MaybeToken(Err(ctx.failure(e))), false, false),
176        };
177        Self {
178            name,
179            anchor,
180            unique,
181            base_name_unique,
182        }
183    }
184}
185
186impl<'a> Textify for NamedAnchor<'a> {
187    fn name() -> &'static str {
188        "NamedAnchor"
189    }
190
191    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
192        // Decide whether to show the full compound name or just the base name.
193        let show_signature = match ctx.options().show_simple_extension_anchors {
194            Visibility::Always => true,
195            Visibility::Required => !self.base_name_unique,
196            Visibility::Never => false,
197        };
198
199        match &self.name.0 {
200            Ok(n) => {
201                if show_signature {
202                    write!(w, "{}", n.full())?;
203                } else {
204                    write!(w, "{}", n.base())?;
205                }
206            }
207            Err(e) => write!(w, "{e}")?,
208        }
209
210        let anchor = Anchor::new(self.anchor, !self.unique);
211        write!(w, "{}", ctx.display(&anchor))
212    }
213}
214
215/// The type desciptor of the output of a function call.
216///
217/// This is optional, and if present, it must be the last argument in the
218/// function call.
219#[derive(Debug, Copy, Clone)]
220pub struct OutputType<T: Deref<Target = proto::Type>>(pub Option<T>);
221
222impl<T: Deref<Target = proto::Type>> Textify for OutputType<T> {
223    fn name() -> &'static str {
224        "OutputType"
225    }
226
227    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
228        match self.0 {
229            Some(ref t) => write!(w, ":{}", ctx.display(t.deref())),
230            None => Ok(()),
231        }
232    }
233}
234
235struct TypeVariation(u32);
236
237impl Textify for TypeVariation {
238    fn name() -> &'static str {
239        "TypeVariation"
240    }
241
242    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
243        let &TypeVariation(anchor) = self;
244        if anchor == 0 {
245            // This is the default, this doesn't count as a type variation
246            return Ok(());
247        }
248        let name_and_anchor = NamedAnchor::lookup(ctx, ExtensionKind::TypeVariation, anchor);
249
250        write!(
251            w,
252            "[{name_and_anchor}]",
253            name_and_anchor = ctx.display(&name_and_anchor)
254        )
255    }
256}
257
258// Textify a standard type with parameters.
259//
260// P will generally be the Parameter type, but it can be any type that
261// implements Textify.
262fn textify_type<S: Scope, W: fmt::Write>(
263    ctx: &S,
264    f: &mut W,
265    name: impl AsRef<str>,
266    nullability: ptype::Nullability,
267    variant: u32,
268    params: Parameters,
269) -> fmt::Result {
270    write!(
271        f,
272        "{name}{null}{var}{params}",
273        name = name.as_ref(),
274        null = ctx.display(&nullability),
275        var = ctx.display(&TypeVariation(variant)),
276        params = ctx.display(&params)
277    )
278}
279
280macro_rules! textify_kind {
281    ($ctx:expr, $f:expr, $kind:ident, $name:expr) => {
282        textify_type(
283            $ctx,
284            $f,
285            $name,
286            $kind.nullability(),
287            $kind.type_variation_reference,
288            Parameters(&[]),
289        )
290    };
291}
292
293impl Textify for Parameter {
294    fn name() -> &'static str {
295        "Parameter"
296    }
297
298    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
299        match self {
300            Parameter::Boolean(true) => write!(w, "true")?,
301            Parameter::Boolean(false) => write!(w, "false")?,
302            Parameter::DataType(t) => write!(w, "{}", ctx.display(t))?,
303            Parameter::Enum(e) => write!(w, "{e}")?,
304            Parameter::Integer(i) => write!(w, "{i}")?,
305            // TODO: Do we just put the string in directly?
306            Parameter::String(s) => write!(w, "{s}")?,
307            Parameter::Null(_) => write!(w, "null")?,
308        };
309
310        Ok(())
311    }
312}
313impl Textify for ptype::Parameter {
314    fn name() -> &'static str {
315        "Parameter"
316    }
317
318    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
319        write!(w, "{}", ctx.expect(self.parameter.as_ref()))
320    }
321}
322
323struct Parameters<'a>(&'a [Option<Parameter>]);
324
325impl<'a> Textify for Parameters<'a> {
326    fn name() -> &'static str {
327        "Parameters"
328    }
329
330    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
331        let mut first = true;
332        for param in self.0.iter() {
333            if first {
334                write!(w, "<")?;
335            } else {
336                write!(w, ", ")?;
337            }
338            write!(w, "{}", ctx.expect(param.as_ref()))?;
339            first = false;
340        }
341        if !first {
342            write!(w, ">")?;
343        }
344
345        Ok(())
346    }
347}
348
349impl Textify for ptype::UserDefined {
350    fn name() -> &'static str {
351        "UserDefined"
352    }
353
354    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
355        {
356            let name_and_anchor =
357                NamedAnchor::lookup(ctx, ExtensionKind::Type, self.type_reference);
358
359            let param_vec: Vec<Option<Parameter>> = self
360                .type_parameters
361                .iter()
362                .map(|t| t.parameter.clone())
363                .collect();
364            let params = Parameters(&param_vec);
365
366            write!(
367                w,
368                "{name_and_anchor}{null}{var}{params}",
369                name_and_anchor = ctx.display(&name_and_anchor),
370                null = ctx.display(&self.nullability()),
371                var = ctx.display(&TypeVariation(self.type_variation_reference)),
372                params = ctx.display(&params)
373            )
374        }
375    }
376}
377
378impl Textify for ptype::Kind {
379    fn name() -> &'static str {
380        "Kind"
381    }
382
383    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
384        match self {
385            // This is the expansion of:
386            //     textify_kind!(ctx, w, k, "boolean")
387            // Shown here for visibility
388            ptype::Kind::Bool(k) => textify_type(
389                ctx,
390                w,
391                "boolean",
392                k.nullability(),
393                k.type_variation_reference,
394                Parameters(&[]),
395            ),
396            ptype::Kind::I8(k) => textify_kind!(ctx, w, k, "i8"),
397            ptype::Kind::I16(k) => textify_kind!(ctx, w, k, "i16"),
398            ptype::Kind::I32(k) => textify_kind!(ctx, w, k, "i32"),
399            ptype::Kind::I64(k) => textify_kind!(ctx, w, k, "i64"),
400            ptype::Kind::Fp32(k) => textify_kind!(ctx, w, k, "fp32"),
401            ptype::Kind::Fp64(k) => textify_kind!(ctx, w, k, "fp64"),
402            ptype::Kind::String(k) => textify_kind!(ctx, w, k, "string"),
403            ptype::Kind::Binary(k) => textify_kind!(ctx, w, k, "binary"),
404            #[allow(deprecated)]
405            ptype::Kind::Timestamp(k) => textify_kind!(ctx, w, k, "timestamp"),
406            ptype::Kind::Date(k) => textify_kind!(ctx, w, k, "date"),
407            #[allow(deprecated)]
408            ptype::Kind::Time(k) => textify_kind!(ctx, w, k, "time"),
409            ptype::Kind::IntervalYear(i) => {
410                textify_kind!(ctx, w, i, "interval_year")
411            }
412            #[allow(deprecated)]
413            ptype::Kind::TimestampTz(ts) => {
414                textify_kind!(ctx, w, ts, "timestamp_tz")
415            }
416            ptype::Kind::Uuid(uuid) => textify_kind!(ctx, w, uuid, "uuid"),
417
418            ptype::Kind::IntervalDay(i) => textify_type(
419                ctx,
420                w,
421                "interval_day",
422                i.nullability(),
423                i.type_variation_reference,
424                // Precision defaults to 6 if unspecified
425                Parameters(&[Some(Parameter::Integer(i.precision.unwrap_or(6) as i64))]),
426            ),
427            ptype::Kind::IntervalCompound(i) => textify_type(
428                ctx,
429                w,
430                "interval_compound",
431                i.nullability(),
432                i.type_variation_reference,
433                Parameters(&[Some(Parameter::Integer(i.precision as i64))]),
434            ),
435            ptype::Kind::FixedChar(c) => textify_type(
436                ctx,
437                w,
438                "fixedchar",
439                c.nullability(),
440                c.type_variation_reference,
441                Parameters(&[Some(Parameter::Integer(c.length as i64))]),
442            ),
443            ptype::Kind::Varchar(c) => textify_type(
444                ctx,
445                w,
446                "varchar",
447                c.nullability(),
448                c.type_variation_reference,
449                Parameters(&[Some(Parameter::Integer(c.length as i64))]),
450            ),
451            ptype::Kind::FixedBinary(b) => textify_type(
452                ctx,
453                w,
454                "fixedbinary",
455                b.nullability(),
456                b.type_variation_reference,
457                Parameters(&[Some(Parameter::Integer(b.length as i64))]),
458            ),
459            ptype::Kind::Decimal(d) => {
460                let p = Parameter::Integer(d.precision as i64);
461                let s = Parameter::Integer(d.scale as i64);
462                textify_type(
463                    ctx,
464                    w,
465                    "decimal",
466                    d.nullability(),
467                    d.type_variation_reference,
468                    Parameters(&[Some(p), Some(s)]),
469                )
470            }
471            ptype::Kind::PrecisionTime(p) => textify_type(
472                ctx,
473                w,
474                "precisiontime",
475                p.nullability(),
476                p.type_variation_reference,
477                Parameters(&[Some(Parameter::Integer(p.precision as i64))]),
478            ),
479            ptype::Kind::PrecisionTimestamp(p) => textify_type(
480                ctx,
481                w,
482                "precisiontimestamp",
483                p.nullability(),
484                p.type_variation_reference,
485                Parameters(&[Some(Parameter::Integer(p.precision as i64))]),
486            ),
487            ptype::Kind::PrecisionTimestampTz(p) => textify_type(
488                ctx,
489                w,
490                "precisiontimestamptz",
491                p.nullability(),
492                p.type_variation_reference,
493                Parameters(&[Some(Parameter::Integer(p.precision as i64))]),
494            ),
495            ptype::Kind::Struct(s) => textify_type(
496                ctx,
497                w,
498                "struct",
499                s.nullability(),
500                s.type_variation_reference,
501                Parameters(
502                    &s.types
503                        .iter()
504                        .map(|t| Some(Parameter::DataType(t.clone())))
505                        .collect::<Vec<_>>(),
506                ),
507            ),
508            ptype::Kind::List(l) => {
509                let p = l
510                    .r#type
511                    .as_ref()
512                    .map(|t| Parameter::DataType((**t).to_owned()));
513                textify_type(
514                    ctx,
515                    w,
516                    "list",
517                    l.nullability(),
518                    l.type_variation_reference,
519                    Parameters(&[p]),
520                )
521            }
522            ptype::Kind::Map(m) => {
523                let k = m
524                    .key
525                    .as_ref()
526                    .map(|t| Parameter::DataType((**t).to_owned()));
527                let v = m
528                    .value
529                    .as_ref()
530                    .map(|t| Parameter::DataType((**t).to_owned()));
531                textify_type(
532                    ctx,
533                    w,
534                    "map",
535                    m.nullability(),
536                    m.type_variation_reference,
537                    Parameters(&[k, v]),
538                )
539            }
540            ptype::Kind::UserDefined(u) => u.textify(ctx, w),
541            #[allow(deprecated)]
542            ptype::Kind::UserDefinedTypeReference(r) => {
543                // Defer to the UserDefined definition, using defaults for
544                // variation, and non-nullable as suggested by the docs
545                let udf = ptype::UserDefined {
546                    type_reference: *r,
547                    type_variation_reference: 0,
548                    nullability: ptype::Nullability::Required as i32,
549                    type_parameters: vec![],
550                };
551                ptype::Kind::UserDefined(udf).textify(ctx, w)
552            }
553            ptype::Kind::Func(_f) => {
554                write!(
555                    w,
556                    "{}",
557                    ctx.failure(PlanError::unimplemented(
558                        "FuncType",
559                        Some("Func"),
560                        "Function type textification not implemented",
561                    ))
562                )
563            }
564            ptype::Kind::Alias(_p) => {
565                write!(
566                    w,
567                    "{}",
568                    ctx.failure(PlanError::unimplemented(
569                        "AliasType",
570                        Some("Alias"),
571                        "TypeAliasReference textification not implemented",
572                    ))
573                )
574            }
575        }
576    }
577}
578
579impl Textify for proto::Type {
580    fn name() -> &'static str {
581        "Type"
582    }
583
584    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
585        write!(w, "{}", ctx.expect(self.kind.as_ref()))
586    }
587}
588
589// /// A schema is a named struct with a list of fields.
590// ///
591// /// This outputs the names and types of the fields in the struct,
592// /// comma-separated.
593// ///
594// /// Assumes that the struct is not nullable, that the type variation reference
595// /// is 0, and that the names and fields match up; otherwise, pushes errors.
596// ///
597// /// Names and fields are output without any bracketing; bring your own
598// /// bracketing.
599// pub struct Schema<'a>(pub &'a proto::NamedStruct);
600
601// impl<'a> Textify for Schema<'a> {
602//     fn name() -> &'static str {
603//         "Schema"
604//     }
605
606//     fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
607//         let mut fields = self
608//             .0
609//             .r#struct
610//             .as_ref()
611//             .map(|s| s.types.iter())
612//             .into_iter()
613//             .flatten();
614//         let mut names = self.0.names.iter();
615
616//         let field_count = self.0.r#struct.as_ref().map(|s| s.types.len()).unwrap_or(0);
617//         let name_count = self.0.names.len();
618
619//         if field_count != name_count {
620//             ctx.push_error(
621//                 TextifyError::invalid(
622//                     "Schema",
623//                     NONSPECIFIC,
624//                     format!(
625//                         "Field count ({}) does not match name count ({})",
626//                         field_count, name_count
627//                     ),
628//                 )
629//                 .into(),
630//             );
631//         }
632
633//         write!(w, "[")?;
634//         let mut first = true;
635//         loop {
636//             let field = fields.next();
637//             let name = names.next().map(|n| Name(n));
638//             if field.is_none() && name.is_none() {
639//                 break;
640//             }
641
642//             if first {
643//                 first = false;
644//             } else {
645//                 write!(w, ", ")?;
646//             }
647
648//             write!(w, "{}:{}", ctx.expect(name.as_ref()), ctx.expect(field))?;
649//         }
650//         write!(w, "]")?;
651
652//         let s = match &self.0.r#struct {
653//             None => return Ok(()),
654//             Some(s) => s,
655//         };
656
657//         if s.nullability() != Nullability::Required {
658//             ctx.push_error(
659//                 TextifyError::invalid(
660//                     "Schema",
661//                     Some("nullabilility"),
662//                     "Expected schema to be Nullability::Required",
663//                 )
664//                 .into(),
665//             );
666//             s.nullability().textify(ctx, w)?;
667//         }
668//         if s.type_variation_reference != 0 {
669//             ctx.push_error(
670//                 TextifyError::invalid(
671//                     "Schema",
672//                     Some("type_variation_reference"),
673//                     "Expected schema to have type_variation_reference 0",
674//                 )
675//                 .into(),
676//             );
677//             TypeVariation(s.type_variation_reference).textify(ctx, w)?;
678//         }
679
680//         Ok(())
681//     }
682// }
683
684#[cfg(test)]
685mod tests {
686
687    use super::*;
688    use crate::extensions::simple::{ExtensionKind, MissingReference};
689    use crate::fixtures::TestContext;
690    use crate::textify::foundation::FormatError;
691
692    #[test]
693    fn type_display() {
694        let ctx = TestContext::new()
695            .with_urn(1, "first")
696            .with_type_variation(1, 2, "u8");
697
698        let t = proto::Type {
699            kind: Some(ptype::Kind::Bool(ptype::Boolean {
700                type_variation_reference: 2,
701                nullability: ptype::Nullability::Nullable as i32,
702            })),
703        };
704
705        let s = ctx.textify_no_errors(&t);
706        assert_eq!(s, "boolean?[u8]");
707
708        let t = proto::Type {
709            kind: Some(ptype::Kind::I8(ptype::I8 {
710                type_variation_reference: 0,
711                nullability: ptype::Nullability::Required as i32,
712            })),
713        };
714        assert_eq!(ctx.textify_no_errors(&t), "i8");
715
716        let t = proto::Type {
717            kind: Some(ptype::Kind::PrecisionTimestamp(ptype::PrecisionTimestamp {
718                type_variation_reference: 0,
719                nullability: ptype::Nullability::Nullable as i32,
720                precision: 3,
721            })),
722        };
723        assert_eq!(ctx.textify_no_errors(&t), "precisiontimestamp?<3>");
724
725        let mut ctx = ctx.with_type_variation(1, 8, "int");
726        ctx.options.show_simple_extension_anchors = Visibility::Always;
727
728        let t = proto::Type {
729            kind: Some(ptype::Kind::PrecisionTime(ptype::PrecisionTime {
730                type_variation_reference: 8,
731                nullability: ptype::Nullability::Nullable as i32,
732                precision: 9,
733            })),
734        };
735        assert_eq!(ctx.textify_no_errors(&t), "precisiontime?[int#8]<9>");
736    }
737
738    #[test]
739    fn type_display_with_errors() {
740        let ctx = TestContext::new()
741            .with_urn(1, "first")
742            .with_type(1, 100, "cow");
743
744        let t = proto::Type {
745            kind: Some(ptype::Kind::Bool(ptype::Boolean {
746                type_variation_reference: 200,
747                nullability: ptype::Nullability::Nullable as i32,
748            })),
749        };
750        let (s, errs) = ctx.textify(&t);
751        assert_eq!(s, "boolean?[!{type_variation}#200]");
752        let err = errs.first();
753        let (&k, &a) = match err {
754            FormatError::Lookup(MissingReference::MissingAnchor(k, a)) => (k, a),
755            _ => panic!("Expected Lookup MissingAnchor: {err}"),
756        };
757
758        assert_eq!(k, ExtensionKind::TypeVariation);
759        assert_eq!(a, 200);
760
761        let t = proto::Type {
762            kind: Some(ptype::Kind::UserDefined(ptype::UserDefined {
763                type_variation_reference: 0,
764                nullability: ptype::Nullability::Required as i32,
765                type_reference: 100,
766                type_parameters: vec![],
767            })),
768        };
769
770        let (s, errs) = ctx.textify(&t);
771        assert!(errs.is_empty());
772        assert_eq!(s, "cow");
773
774        let t = proto::Type {
775            kind: Some(ptype::Kind::UserDefined(ptype::UserDefined {
776                type_variation_reference: 0,
777                nullability: ptype::Nullability::Required as i32,
778                type_reference: 12589,
779                type_parameters: vec![],
780            })),
781        };
782
783        let (s, errs) = ctx.textify(&t);
784        let err = errs.first();
785        let (&k, &a) = match err {
786            FormatError::Lookup(MissingReference::MissingAnchor(k, a)) => (k, a),
787            _ => panic!("Expected Lookup MissingAnchor: {err}"),
788        };
789        assert_eq!(k, ExtensionKind::Type);
790        assert_eq!(a, 12589);
791        assert_eq!(s, "!{type}#12589");
792    }
793
794    #[test]
795    fn struct_display() {
796        let ctx = TestContext::new();
797        let t = proto::Type {
798            kind: Some(ptype::Kind::Struct(ptype::Struct {
799                type_variation_reference: 0,
800                nullability: ptype::Nullability::Nullable as i32,
801                types: vec![
802                    proto::Type {
803                        kind: Some(ptype::Kind::String(ptype::String {
804                            type_variation_reference: 0,
805                            nullability: ptype::Nullability::Required as i32,
806                        })),
807                    },
808                    proto::Type {
809                        kind: Some(ptype::Kind::I8(ptype::I8 {
810                            type_variation_reference: 0,
811                            nullability: ptype::Nullability::Required as i32,
812                        })),
813                    },
814                    proto::Type {
815                        kind: Some(ptype::Kind::I32(ptype::I32 {
816                            type_variation_reference: 0,
817                            nullability: ptype::Nullability::Nullable as i32,
818                        })),
819                    },
820                    proto::Type {
821                        #[allow(deprecated)] // TimestampTz is deprecated
822                        kind: Some(ptype::Kind::TimestampTz(ptype::TimestampTz {
823                            type_variation_reference: 0,
824                            nullability: ptype::Nullability::Required as i32,
825                        })),
826                    },
827                ],
828            })),
829        };
830        assert_eq!(
831            ctx.textify_no_errors(&t),
832            "struct?<string, i8, i32?, timestamp_tz>"
833        );
834    }
835
836    #[test]
837    fn names_display() {
838        let ctx = TestContext::new();
839
840        let n = Name("name");
841        assert_eq!(ctx.textify_no_errors(&n), "name");
842
843        let n = Name("name with spaces");
844        assert_eq!(ctx.textify_no_errors(&n), "\"name with spaces\"");
845    }
846
847    // #[test]
848    // fn schema_display() {
849    //     let ctx = TestContext::new();
850
851    //     let s = ptype::Struct {
852    //         type_variation_reference: 0,
853    //         nullability: ptype::Nullability::Required as i32,
854    //         types: vec![
855    //             proto::Type {
856    //                 kind: Some(ptype::Kind::String(ptype::String {
857    //                     type_variation_reference: 0,
858    //                     nullability: ptype::Nullability::Required as i32,
859    //                 })),
860    //             },
861    //             proto::Type {
862    //                 kind: Some(ptype::Kind::I8(ptype::I8 {
863    //                     type_variation_reference: 0,
864    //                     nullability: ptype::Nullability::Required as i32,
865    //                 })),
866    //             },
867    //             proto::Type {
868    //                 kind: Some(ptype::Kind::I32(ptype::I32 {
869    //                     type_variation_reference: 0,
870    //                     nullability: ptype::Nullability::Nullable as i32,
871    //                 })),
872    //             },
873    //             proto::Type {
874    //                 kind: Some(ptype::Kind::TimestampTz(ptype::TimestampTz {
875    //                     type_variation_reference: 0,
876    //                     nullability: ptype::Nullability::Required as i32,
877    //                 })),
878    //             },
879    //         ],
880    //     };
881
882    //     let names = ["a", "b", "c", "d"].iter().map(|s| s.to_string()).collect();
883    //     let schema = proto::NamedStruct {
884    //         names,
885    //         r#struct: Some(s),
886    //     };
887
888    //     assert_eq!(
889    //         ctx.textify_no_errors(&Schema(&schema)),
890    //         "[a:string, b:i8, c:i32?, d:timestamp_tz]"
891    //     );
892    // }
893
894    // #[test]
895    // fn schema_display_with_errors() {
896    //     let ctx = TestContext::new();
897    //     let string = proto::Type {
898    //         kind: Some(ptype::Kind::String(ptype::String {
899    //             type_variation_reference: 0,
900    //             nullability: ptype::Nullability::Required as i32,
901    //         })),
902    //     };
903    //     let i64 = proto::Type {
904    //         kind: Some(ptype::Kind::I8(ptype::I8 {
905    //             type_variation_reference: 0,
906    //             nullability: ptype::Nullability::Nullable as i32,
907    //         })),
908    //     };
909    //     let fp64 = proto::Type {
910    //         kind: Some(ptype::Kind::Fp64(ptype::Fp64 {
911    //             type_variation_reference: 0,
912    //             nullability: ptype::Nullability::Nullable as i32,
913    //         })),
914    //     };
915
916    //     let s = ptype::Struct {
917    //         type_variation_reference: 0,
918    //         nullability: ptype::Nullability::Required as i32,
919    //         types: vec![string.clone(), i64, fp64, string],
920    //     };
921
922    //     let names = ["name", "id", "distance", "street address"]
923    //         .iter()
924    //         .map(|s| s.to_string())
925    //         .collect();
926    //     let schema = proto::NamedStruct {
927    //         names,
928    //         r#struct: Some(s),
929    //     };
930
931    //     let (s, errs) = ctx.textify(&Schema(&schema));
932    //     assert_eq!(
933    //         s,
934    //         "name:string, id:i8?, distance:fp64?, \"street address\":string"
935    //     );
936    //     assert!(errs.is_empty());
937    // }
938
939    // ---- Tests for NamedAnchor signature display ----
940
941    /// Build a TestContext with two overloaded functions (`equal:any_any` and
942    /// `equal:str_str`) sharing the same URN, plus a unique function (`add`).
943    fn overloaded_ctx() -> TestContext {
944        TestContext::new()
945            .with_urn(1, "substrait:functions_comparison")
946            .with_function(1, 1, "equal:any_any")
947            .with_function(1, 2, "equal:str_str")
948            .with_function(1, 3, "add:i64_i64")
949    }
950
951    #[test]
952    fn named_anchor_compact_unique_base_name_no_signature() {
953        // `add:i64_i64` is the only function with base name "add".
954        // Compact mode: show base name only, no anchor (compound name unique).
955        let ctx = overloaded_ctx();
956        let eq = crate::textify::ErrorQueue::default();
957        let scope = ctx.scope(&eq);
958        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 3);
959        assert!(na.base_name_unique, "add should have unique base name");
960        assert!(na.unique, "add:i64_i64 compound name should be unique");
961
962        let s = ctx.textify_no_errors(&na);
963        assert_eq!(s, "add");
964    }
965
966    #[test]
967    fn named_anchor_compact_overloaded_shows_signature() {
968        // `equal:any_any` shares base name "equal" with `equal:str_str`.
969        // Compact mode: base name not unique → show full compound name.
970        // Compound name is unique (only one URN) → no anchor.
971        let ctx = overloaded_ctx();
972        let eq = crate::textify::ErrorQueue::default();
973        let scope = ctx.scope(&eq);
974        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 1);
975        assert!(!na.base_name_unique, "equal base name should not be unique");
976        assert!(
977            na.unique,
978            "equal:any_any compound name should be unique across URNs"
979        );
980
981        let s = ctx.textify_no_errors(&na);
982        assert_eq!(s, "equal:any_any");
983    }
984
985    #[test]
986    fn named_anchor_verbose_unique_base_name_shows_signature_and_anchor() {
987        // Verbose mode: always show signature and always show anchor.
988        let mut ctx = overloaded_ctx();
989        ctx.options.show_simple_extension_anchors = Visibility::Always;
990
991        let eq = crate::textify::ErrorQueue::default();
992        let scope = ctx.scope(&eq);
993        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 3);
994        let s = ctx.textify_no_errors(&na);
995        assert_eq!(s, "add:i64_i64#3");
996    }
997
998    #[test]
999    fn named_anchor_verbose_overloaded_shows_signature_and_anchor() {
1000        // Verbose mode: overloaded function shows full compound name + anchor.
1001        let mut ctx = overloaded_ctx();
1002        ctx.options.show_simple_extension_anchors = Visibility::Always;
1003
1004        let eq = crate::textify::ErrorQueue::default();
1005        let scope = ctx.scope(&eq);
1006        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 2);
1007        let s = ctx.textify_no_errors(&na);
1008        assert_eq!(s, "equal:str_str#2");
1009    }
1010
1011    #[test]
1012    fn named_anchor_compact_same_compound_name_two_urns_shows_anchor() {
1013        // Same compound name `equal:any_any` registered in two different URNs.
1014        // Compact mode: compound name not unique → anchor required.
1015        let ctx = TestContext::new()
1016            .with_urn(1, "urn_a")
1017            .with_urn(2, "urn_b")
1018            .with_function(1, 1, "equal:any_any")
1019            .with_function(2, 2, "equal:any_any");
1020
1021        let eq = crate::textify::ErrorQueue::default();
1022        let scope = ctx.scope(&eq);
1023        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 1);
1024        assert!(!na.base_name_unique);
1025        assert!(!na.unique, "compound name not unique across two URNs");
1026
1027        let s = ctx.textify_no_errors(&na);
1028        assert_eq!(s, "equal:any_any#1");
1029    }
1030
1031    #[test]
1032    fn named_anchor_compact_plain_name_unique_no_signature_no_anchor() {
1033        let ctx = TestContext::new()
1034            .with_urn(1, "urn")
1035            .with_function(1, 10, "coalesce");
1036
1037        let eq = crate::textify::ErrorQueue::default();
1038        let scope = ctx.scope(&eq);
1039        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 10);
1040        assert!(na.base_name_unique);
1041        assert!(na.unique);
1042
1043        let s = ctx.textify_no_errors(&na);
1044        assert_eq!(s, "coalesce");
1045    }
1046
1047    #[test]
1048    fn named_anchor_compact_plain_name_non_unique_shows_anchor() {
1049        let ctx = TestContext::new()
1050            .with_urn(1, "urn1")
1051            .with_urn(2, "urn2")
1052            .with_function(1, 231, "duplicated")
1053            .with_function(2, 232, "duplicated");
1054
1055        let eq = crate::textify::ErrorQueue::default();
1056        let scope = ctx.scope(&eq);
1057        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 231);
1058        assert!(!na.base_name_unique);
1059        assert!(!na.unique);
1060
1061        let s = ctx.textify_no_errors(&na);
1062        assert_eq!(s, "duplicated#231");
1063    }
1064}