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            ptype::Kind::Time(k) => textify_kind!(ctx, w, k, "time"),
408            ptype::Kind::IntervalYear(i) => {
409                textify_kind!(ctx, w, i, "interval_year")
410            }
411            #[allow(deprecated)]
412            ptype::Kind::TimestampTz(ts) => {
413                textify_kind!(ctx, w, ts, "timestamp_tz")
414            }
415            ptype::Kind::Uuid(uuid) => textify_kind!(ctx, w, uuid, "uuid"),
416
417            ptype::Kind::IntervalDay(i) => textify_type(
418                ctx,
419                w,
420                "interval_day",
421                i.nullability(),
422                i.type_variation_reference,
423                // Precision defaults to 6 if unspecified
424                Parameters(&[Some(Parameter::Integer(i.precision.unwrap_or(6) as i64))]),
425            ),
426            ptype::Kind::IntervalCompound(i) => textify_type(
427                ctx,
428                w,
429                "interval_compound",
430                i.nullability(),
431                i.type_variation_reference,
432                Parameters(&[Some(Parameter::Integer(i.precision as i64))]),
433            ),
434            ptype::Kind::FixedChar(c) => textify_type(
435                ctx,
436                w,
437                "fixedchar",
438                c.nullability(),
439                c.type_variation_reference,
440                Parameters(&[Some(Parameter::Integer(c.length as i64))]),
441            ),
442            ptype::Kind::Varchar(c) => textify_type(
443                ctx,
444                w,
445                "varchar",
446                c.nullability(),
447                c.type_variation_reference,
448                Parameters(&[Some(Parameter::Integer(c.length as i64))]),
449            ),
450            ptype::Kind::FixedBinary(b) => textify_type(
451                ctx,
452                w,
453                "fixedbinary",
454                b.nullability(),
455                b.type_variation_reference,
456                Parameters(&[Some(Parameter::Integer(b.length as i64))]),
457            ),
458            ptype::Kind::Decimal(d) => {
459                let p = Parameter::Integer(d.precision as i64);
460                let s = Parameter::Integer(d.scale as i64);
461                textify_type(
462                    ctx,
463                    w,
464                    "decimal",
465                    d.nullability(),
466                    d.type_variation_reference,
467                    Parameters(&[Some(p), Some(s)]),
468                )
469            }
470            ptype::Kind::PrecisionTime(p) => textify_type(
471                ctx,
472                w,
473                "precisiontime",
474                p.nullability(),
475                p.type_variation_reference,
476                Parameters(&[Some(Parameter::Integer(p.precision as i64))]),
477            ),
478            ptype::Kind::PrecisionTimestamp(p) => textify_type(
479                ctx,
480                w,
481                "precisiontimestamp",
482                p.nullability(),
483                p.type_variation_reference,
484                Parameters(&[Some(Parameter::Integer(p.precision as i64))]),
485            ),
486            ptype::Kind::PrecisionTimestampTz(p) => textify_type(
487                ctx,
488                w,
489                "precisiontimestamptz",
490                p.nullability(),
491                p.type_variation_reference,
492                Parameters(&[Some(Parameter::Integer(p.precision as i64))]),
493            ),
494            ptype::Kind::Struct(s) => textify_type(
495                ctx,
496                w,
497                "struct",
498                s.nullability(),
499                s.type_variation_reference,
500                Parameters(
501                    &s.types
502                        .iter()
503                        .map(|t| Some(Parameter::DataType(t.clone())))
504                        .collect::<Vec<_>>(),
505                ),
506            ),
507            ptype::Kind::List(l) => {
508                let p = l
509                    .r#type
510                    .as_ref()
511                    .map(|t| Parameter::DataType((**t).to_owned()));
512                textify_type(
513                    ctx,
514                    w,
515                    "list",
516                    l.nullability(),
517                    l.type_variation_reference,
518                    Parameters(&[p]),
519                )
520            }
521            ptype::Kind::Map(m) => {
522                let k = m
523                    .key
524                    .as_ref()
525                    .map(|t| Parameter::DataType((**t).to_owned()));
526                let v = m
527                    .value
528                    .as_ref()
529                    .map(|t| Parameter::DataType((**t).to_owned()));
530                textify_type(
531                    ctx,
532                    w,
533                    "map",
534                    m.nullability(),
535                    m.type_variation_reference,
536                    Parameters(&[k, v]),
537                )
538            }
539            ptype::Kind::UserDefined(u) => u.textify(ctx, w),
540            #[allow(deprecated)]
541            ptype::Kind::UserDefinedTypeReference(r) => {
542                // Defer to the UserDefined definition, using defaults for
543                // variation, and non-nullable as suggested by the docs
544                let udf = ptype::UserDefined {
545                    type_reference: *r,
546                    type_variation_reference: 0,
547                    nullability: ptype::Nullability::Required as i32,
548                    type_parameters: vec![],
549                };
550                ptype::Kind::UserDefined(udf).textify(ctx, w)
551            }
552            ptype::Kind::Alias(_p) => {
553                write!(
554                    w,
555                    "{}",
556                    ctx.failure(PlanError::unimplemented(
557                        "AliasType",
558                        Some("Alias"),
559                        "TypeAliasReference textification not implemented",
560                    ))
561                )
562            }
563        }
564    }
565}
566
567impl Textify for proto::Type {
568    fn name() -> &'static str {
569        "Type"
570    }
571
572    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
573        write!(w, "{}", ctx.expect(self.kind.as_ref()))
574    }
575}
576
577// /// A schema is a named struct with a list of fields.
578// ///
579// /// This outputs the names and types of the fields in the struct,
580// /// comma-separated.
581// ///
582// /// Assumes that the struct is not nullable, that the type variation reference
583// /// is 0, and that the names and fields match up; otherwise, pushes errors.
584// ///
585// /// Names and fields are output without any bracketing; bring your own
586// /// bracketing.
587// pub struct Schema<'a>(pub &'a proto::NamedStruct);
588
589// impl<'a> Textify for Schema<'a> {
590//     fn name() -> &'static str {
591//         "Schema"
592//     }
593
594//     fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
595//         let mut fields = self
596//             .0
597//             .r#struct
598//             .as_ref()
599//             .map(|s| s.types.iter())
600//             .into_iter()
601//             .flatten();
602//         let mut names = self.0.names.iter();
603
604//         let field_count = self.0.r#struct.as_ref().map(|s| s.types.len()).unwrap_or(0);
605//         let name_count = self.0.names.len();
606
607//         if field_count != name_count {
608//             ctx.push_error(
609//                 TextifyError::invalid(
610//                     "Schema",
611//                     NONSPECIFIC,
612//                     format!(
613//                         "Field count ({}) does not match name count ({})",
614//                         field_count, name_count
615//                     ),
616//                 )
617//                 .into(),
618//             );
619//         }
620
621//         write!(w, "[")?;
622//         let mut first = true;
623//         loop {
624//             let field = fields.next();
625//             let name = names.next().map(|n| Name(n));
626//             if field.is_none() && name.is_none() {
627//                 break;
628//             }
629
630//             if first {
631//                 first = false;
632//             } else {
633//                 write!(w, ", ")?;
634//             }
635
636//             write!(w, "{}:{}", ctx.expect(name.as_ref()), ctx.expect(field))?;
637//         }
638//         write!(w, "]")?;
639
640//         let s = match &self.0.r#struct {
641//             None => return Ok(()),
642//             Some(s) => s,
643//         };
644
645//         if s.nullability() != Nullability::Required {
646//             ctx.push_error(
647//                 TextifyError::invalid(
648//                     "Schema",
649//                     Some("nullabilility"),
650//                     "Expected schema to be Nullability::Required",
651//                 )
652//                 .into(),
653//             );
654//             s.nullability().textify(ctx, w)?;
655//         }
656//         if s.type_variation_reference != 0 {
657//             ctx.push_error(
658//                 TextifyError::invalid(
659//                     "Schema",
660//                     Some("type_variation_reference"),
661//                     "Expected schema to have type_variation_reference 0",
662//                 )
663//                 .into(),
664//             );
665//             TypeVariation(s.type_variation_reference).textify(ctx, w)?;
666//         }
667
668//         Ok(())
669//     }
670// }
671
672#[cfg(test)]
673mod tests {
674
675    use super::*;
676    use crate::extensions::simple::{ExtensionKind, MissingReference};
677    use crate::fixtures::TestContext;
678    use crate::textify::foundation::FormatError;
679
680    #[test]
681    fn type_display() {
682        let ctx = TestContext::new()
683            .with_urn(1, "first")
684            .with_type_variation(1, 2, "u8");
685
686        let t = proto::Type {
687            kind: Some(ptype::Kind::Bool(ptype::Boolean {
688                type_variation_reference: 2,
689                nullability: ptype::Nullability::Nullable as i32,
690            })),
691        };
692
693        let s = ctx.textify_no_errors(&t);
694        assert_eq!(s, "boolean?[u8]");
695
696        let t = proto::Type {
697            kind: Some(ptype::Kind::I8(ptype::I8 {
698                type_variation_reference: 0,
699                nullability: ptype::Nullability::Required as i32,
700            })),
701        };
702        assert_eq!(ctx.textify_no_errors(&t), "i8");
703
704        let t = proto::Type {
705            kind: Some(ptype::Kind::PrecisionTimestamp(ptype::PrecisionTimestamp {
706                type_variation_reference: 0,
707                nullability: ptype::Nullability::Nullable as i32,
708                precision: 3,
709            })),
710        };
711        assert_eq!(ctx.textify_no_errors(&t), "precisiontimestamp?<3>");
712
713        let mut ctx = ctx.with_type_variation(1, 8, "int");
714        ctx.options.show_simple_extension_anchors = Visibility::Always;
715
716        let t = proto::Type {
717            kind: Some(ptype::Kind::PrecisionTime(ptype::PrecisionTime {
718                type_variation_reference: 8,
719                nullability: ptype::Nullability::Nullable as i32,
720                precision: 9,
721            })),
722        };
723        assert_eq!(ctx.textify_no_errors(&t), "precisiontime?[int#8]<9>");
724    }
725
726    #[test]
727    fn type_display_with_errors() {
728        let ctx = TestContext::new()
729            .with_urn(1, "first")
730            .with_type(1, 100, "cow");
731
732        let t = proto::Type {
733            kind: Some(ptype::Kind::Bool(ptype::Boolean {
734                type_variation_reference: 200,
735                nullability: ptype::Nullability::Nullable as i32,
736            })),
737        };
738        let (s, errs) = ctx.textify(&t);
739        assert_eq!(s, "boolean?[!{type_variation}#200]");
740        let err = errs.first();
741        let (&k, &a) = match err {
742            FormatError::Lookup(MissingReference::MissingAnchor(k, a)) => (k, a),
743            _ => panic!("Expected Lookup MissingAnchor: {err}"),
744        };
745
746        assert_eq!(k, ExtensionKind::TypeVariation);
747        assert_eq!(a, 200);
748
749        let t = proto::Type {
750            kind: Some(ptype::Kind::UserDefined(ptype::UserDefined {
751                type_variation_reference: 0,
752                nullability: ptype::Nullability::Required as i32,
753                type_reference: 100,
754                type_parameters: vec![],
755            })),
756        };
757
758        let (s, errs) = ctx.textify(&t);
759        assert!(errs.is_empty());
760        assert_eq!(s, "cow");
761
762        let t = proto::Type {
763            kind: Some(ptype::Kind::UserDefined(ptype::UserDefined {
764                type_variation_reference: 0,
765                nullability: ptype::Nullability::Required as i32,
766                type_reference: 12589,
767                type_parameters: vec![],
768            })),
769        };
770
771        let (s, errs) = ctx.textify(&t);
772        let err = errs.first();
773        let (&k, &a) = match err {
774            FormatError::Lookup(MissingReference::MissingAnchor(k, a)) => (k, a),
775            _ => panic!("Expected Lookup MissingAnchor: {err}"),
776        };
777        assert_eq!(k, ExtensionKind::Type);
778        assert_eq!(a, 12589);
779        assert_eq!(s, "!{type}#12589");
780    }
781
782    #[test]
783    fn struct_display() {
784        let ctx = TestContext::new();
785        let t = proto::Type {
786            kind: Some(ptype::Kind::Struct(ptype::Struct {
787                type_variation_reference: 0,
788                nullability: ptype::Nullability::Nullable as i32,
789                types: vec![
790                    proto::Type {
791                        kind: Some(ptype::Kind::String(ptype::String {
792                            type_variation_reference: 0,
793                            nullability: ptype::Nullability::Required as i32,
794                        })),
795                    },
796                    proto::Type {
797                        kind: Some(ptype::Kind::I8(ptype::I8 {
798                            type_variation_reference: 0,
799                            nullability: ptype::Nullability::Required as i32,
800                        })),
801                    },
802                    proto::Type {
803                        kind: Some(ptype::Kind::I32(ptype::I32 {
804                            type_variation_reference: 0,
805                            nullability: ptype::Nullability::Nullable as i32,
806                        })),
807                    },
808                    proto::Type {
809                        #[allow(deprecated)] // TimestampTz is deprecated
810                        kind: Some(ptype::Kind::TimestampTz(ptype::TimestampTz {
811                            type_variation_reference: 0,
812                            nullability: ptype::Nullability::Required as i32,
813                        })),
814                    },
815                ],
816            })),
817        };
818        assert_eq!(
819            ctx.textify_no_errors(&t),
820            "struct?<string, i8, i32?, timestamp_tz>"
821        );
822    }
823
824    #[test]
825    fn names_display() {
826        let ctx = TestContext::new();
827
828        let n = Name("name");
829        assert_eq!(ctx.textify_no_errors(&n), "name");
830
831        let n = Name("name with spaces");
832        assert_eq!(ctx.textify_no_errors(&n), "\"name with spaces\"");
833    }
834
835    // #[test]
836    // fn schema_display() {
837    //     let ctx = TestContext::new();
838
839    //     let s = ptype::Struct {
840    //         type_variation_reference: 0,
841    //         nullability: ptype::Nullability::Required as i32,
842    //         types: vec![
843    //             proto::Type {
844    //                 kind: Some(ptype::Kind::String(ptype::String {
845    //                     type_variation_reference: 0,
846    //                     nullability: ptype::Nullability::Required as i32,
847    //                 })),
848    //             },
849    //             proto::Type {
850    //                 kind: Some(ptype::Kind::I8(ptype::I8 {
851    //                     type_variation_reference: 0,
852    //                     nullability: ptype::Nullability::Required as i32,
853    //                 })),
854    //             },
855    //             proto::Type {
856    //                 kind: Some(ptype::Kind::I32(ptype::I32 {
857    //                     type_variation_reference: 0,
858    //                     nullability: ptype::Nullability::Nullable as i32,
859    //                 })),
860    //             },
861    //             proto::Type {
862    //                 kind: Some(ptype::Kind::TimestampTz(ptype::TimestampTz {
863    //                     type_variation_reference: 0,
864    //                     nullability: ptype::Nullability::Required as i32,
865    //                 })),
866    //             },
867    //         ],
868    //     };
869
870    //     let names = ["a", "b", "c", "d"].iter().map(|s| s.to_string()).collect();
871    //     let schema = proto::NamedStruct {
872    //         names,
873    //         r#struct: Some(s),
874    //     };
875
876    //     assert_eq!(
877    //         ctx.textify_no_errors(&Schema(&schema)),
878    //         "[a:string, b:i8, c:i32?, d:timestamp_tz]"
879    //     );
880    // }
881
882    // #[test]
883    // fn schema_display_with_errors() {
884    //     let ctx = TestContext::new();
885    //     let string = proto::Type {
886    //         kind: Some(ptype::Kind::String(ptype::String {
887    //             type_variation_reference: 0,
888    //             nullability: ptype::Nullability::Required as i32,
889    //         })),
890    //     };
891    //     let i64 = proto::Type {
892    //         kind: Some(ptype::Kind::I8(ptype::I8 {
893    //             type_variation_reference: 0,
894    //             nullability: ptype::Nullability::Nullable as i32,
895    //         })),
896    //     };
897    //     let fp64 = proto::Type {
898    //         kind: Some(ptype::Kind::Fp64(ptype::Fp64 {
899    //             type_variation_reference: 0,
900    //             nullability: ptype::Nullability::Nullable as i32,
901    //         })),
902    //     };
903
904    //     let s = ptype::Struct {
905    //         type_variation_reference: 0,
906    //         nullability: ptype::Nullability::Required as i32,
907    //         types: vec![string.clone(), i64, fp64, string],
908    //     };
909
910    //     let names = ["name", "id", "distance", "street address"]
911    //         .iter()
912    //         .map(|s| s.to_string())
913    //         .collect();
914    //     let schema = proto::NamedStruct {
915    //         names,
916    //         r#struct: Some(s),
917    //     };
918
919    //     let (s, errs) = ctx.textify(&Schema(&schema));
920    //     assert_eq!(
921    //         s,
922    //         "name:string, id:i8?, distance:fp64?, \"street address\":string"
923    //     );
924    //     assert!(errs.is_empty());
925    // }
926
927    // ---- Tests for NamedAnchor signature display ----
928
929    /// Build a TestContext with two overloaded functions (`equal:any_any` and
930    /// `equal:str_str`) sharing the same URN, plus a unique function (`add`).
931    fn overloaded_ctx() -> TestContext {
932        TestContext::new()
933            .with_urn(1, "substrait:functions_comparison")
934            .with_function(1, 1, "equal:any_any")
935            .with_function(1, 2, "equal:str_str")
936            .with_function(1, 3, "add:i64_i64")
937    }
938
939    #[test]
940    fn named_anchor_compact_unique_base_name_no_signature() {
941        // `add:i64_i64` is the only function with base name "add".
942        // Compact mode: show base name only, no anchor (compound name unique).
943        let ctx = overloaded_ctx();
944        let eq = crate::textify::ErrorQueue::default();
945        let scope = ctx.scope(&eq);
946        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 3);
947        assert!(na.base_name_unique, "add should have unique base name");
948        assert!(na.unique, "add:i64_i64 compound name should be unique");
949
950        let s = ctx.textify_no_errors(&na);
951        assert_eq!(s, "add");
952    }
953
954    #[test]
955    fn named_anchor_compact_overloaded_shows_signature() {
956        // `equal:any_any` shares base name "equal" with `equal:str_str`.
957        // Compact mode: base name not unique → show full compound name.
958        // Compound name is unique (only one URN) → no anchor.
959        let ctx = overloaded_ctx();
960        let eq = crate::textify::ErrorQueue::default();
961        let scope = ctx.scope(&eq);
962        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 1);
963        assert!(!na.base_name_unique, "equal base name should not be unique");
964        assert!(
965            na.unique,
966            "equal:any_any compound name should be unique across URNs"
967        );
968
969        let s = ctx.textify_no_errors(&na);
970        assert_eq!(s, "equal:any_any");
971    }
972
973    #[test]
974    fn named_anchor_verbose_unique_base_name_shows_signature_and_anchor() {
975        // Verbose mode: always show signature and always show anchor.
976        let mut ctx = overloaded_ctx();
977        ctx.options.show_simple_extension_anchors = Visibility::Always;
978
979        let eq = crate::textify::ErrorQueue::default();
980        let scope = ctx.scope(&eq);
981        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 3);
982        let s = ctx.textify_no_errors(&na);
983        assert_eq!(s, "add:i64_i64#3");
984    }
985
986    #[test]
987    fn named_anchor_verbose_overloaded_shows_signature_and_anchor() {
988        // Verbose mode: overloaded function shows full compound name + anchor.
989        let mut ctx = overloaded_ctx();
990        ctx.options.show_simple_extension_anchors = Visibility::Always;
991
992        let eq = crate::textify::ErrorQueue::default();
993        let scope = ctx.scope(&eq);
994        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 2);
995        let s = ctx.textify_no_errors(&na);
996        assert_eq!(s, "equal:str_str#2");
997    }
998
999    #[test]
1000    fn named_anchor_compact_same_compound_name_two_urns_shows_anchor() {
1001        // Same compound name `equal:any_any` registered in two different URNs.
1002        // Compact mode: compound name not unique → anchor required.
1003        let ctx = TestContext::new()
1004            .with_urn(1, "urn_a")
1005            .with_urn(2, "urn_b")
1006            .with_function(1, 1, "equal:any_any")
1007            .with_function(2, 2, "equal:any_any");
1008
1009        let eq = crate::textify::ErrorQueue::default();
1010        let scope = ctx.scope(&eq);
1011        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 1);
1012        assert!(!na.base_name_unique);
1013        assert!(!na.unique, "compound name not unique across two URNs");
1014
1015        let s = ctx.textify_no_errors(&na);
1016        assert_eq!(s, "equal:any_any#1");
1017    }
1018
1019    #[test]
1020    fn named_anchor_compact_plain_name_unique_no_signature_no_anchor() {
1021        let ctx = TestContext::new()
1022            .with_urn(1, "urn")
1023            .with_function(1, 10, "coalesce");
1024
1025        let eq = crate::textify::ErrorQueue::default();
1026        let scope = ctx.scope(&eq);
1027        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 10);
1028        assert!(na.base_name_unique);
1029        assert!(na.unique);
1030
1031        let s = ctx.textify_no_errors(&na);
1032        assert_eq!(s, "coalesce");
1033    }
1034
1035    #[test]
1036    fn named_anchor_compact_plain_name_non_unique_shows_anchor() {
1037        let ctx = TestContext::new()
1038            .with_urn(1, "urn1")
1039            .with_urn(2, "urn2")
1040            .with_function(1, 231, "duplicated")
1041            .with_function(2, 232, "duplicated");
1042
1043        let eq = crate::textify::ErrorQueue::default();
1044        let scope = ctx.scope(&eq);
1045        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 231);
1046        assert!(!na.base_name_unique);
1047        assert!(!na.unique);
1048
1049        let s = ctx.textify_no_errors(&na);
1050        assert_eq!(s, "duplicated#231");
1051    }
1052}