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 `"count:"`.
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        // Show the anchor when:
211        // - The compound name is not unique (another URN uses the same name), OR
212        // - The name is simple (no colon, e.g. "add") and the base name is not
213        //   unique — without an anchor, "add" would be parsed as a base-name
214        //   lookup and fail as ambiguous.
215        let needs_anchor = !self.unique
216            || match &self.name.0 {
217                Ok(n) if !n.full().contains(':') => !self.base_name_unique,
218                _ => false,
219            };
220        let anchor = Anchor::new(self.anchor, needs_anchor);
221        write!(w, "{}", ctx.display(&anchor))
222    }
223}
224
225/// The output type of a function call, rendered as `:type` after the argument list.
226#[derive(Debug, Copy, Clone)]
227pub struct OutputType<T: Deref<Target = proto::Type>>(pub Option<T>);
228
229impl<T: Deref<Target = proto::Type>> Textify for OutputType<T> {
230    fn name() -> &'static str {
231        "OutputType"
232    }
233
234    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
235        match self.0 {
236            Some(ref t) => write!(w, ":{}", ctx.display(t.deref())),
237            None => write!(
238                w,
239                "{}",
240                ctx.failure(PlanError::invalid(
241                    "OutputType",
242                    None::<&str>,
243                    "function output_type must be set",
244                ))
245            ),
246        }
247    }
248}
249
250struct TypeVariation(u32);
251
252impl Textify for TypeVariation {
253    fn name() -> &'static str {
254        "TypeVariation"
255    }
256
257    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
258        let &TypeVariation(anchor) = self;
259        if anchor == 0 {
260            // This is the default, this doesn't count as a type variation
261            return Ok(());
262        }
263        let name_and_anchor = NamedAnchor::lookup(ctx, ExtensionKind::TypeVariation, anchor);
264
265        write!(
266            w,
267            "[{name_and_anchor}]",
268            name_and_anchor = ctx.display(&name_and_anchor)
269        )
270    }
271}
272
273// Textify a standard type with parameters.
274//
275// P will generally be the Parameter type, but it can be any type that
276// implements Textify.
277fn textify_type<S: Scope, W: fmt::Write>(
278    ctx: &S,
279    f: &mut W,
280    name: impl AsRef<str>,
281    nullability: ptype::Nullability,
282    variant: u32,
283    params: Parameters,
284) -> fmt::Result {
285    write!(
286        f,
287        "{name}{null}{var}{params}",
288        name = name.as_ref(),
289        null = ctx.display(&nullability),
290        var = ctx.display(&TypeVariation(variant)),
291        params = ctx.display(&params)
292    )
293}
294
295macro_rules! textify_kind {
296    ($ctx:expr, $f:expr, $kind:ident, $name:expr) => {
297        textify_type(
298            $ctx,
299            $f,
300            $name,
301            $kind.nullability(),
302            $kind.type_variation_reference,
303            Parameters(&[]),
304        )
305    };
306}
307
308impl Textify for Parameter {
309    fn name() -> &'static str {
310        "Parameter"
311    }
312
313    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
314        match self {
315            Parameter::Boolean(true) => write!(w, "true")?,
316            Parameter::Boolean(false) => write!(w, "false")?,
317            Parameter::DataType(t) => write!(w, "{}", ctx.display(t))?,
318            Parameter::Enum(e) => write!(w, "{e}")?,
319            Parameter::Integer(i) => write!(w, "{i}")?,
320            // TODO: Do we just put the string in directly?
321            Parameter::String(s) => write!(w, "{s}")?,
322            Parameter::Null(_) => write!(w, "null")?,
323        };
324
325        Ok(())
326    }
327}
328impl Textify for ptype::Parameter {
329    fn name() -> &'static str {
330        "Parameter"
331    }
332
333    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
334        write!(w, "{}", ctx.expect(self.parameter.as_ref()))
335    }
336}
337
338struct Parameters<'a>(&'a [Option<Parameter>]);
339
340impl<'a> Textify for Parameters<'a> {
341    fn name() -> &'static str {
342        "Parameters"
343    }
344
345    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
346        let mut first = true;
347        for param in self.0.iter() {
348            if first {
349                write!(w, "<")?;
350            } else {
351                write!(w, ", ")?;
352            }
353            write!(w, "{}", ctx.expect(param.as_ref()))?;
354            first = false;
355        }
356        if !first {
357            write!(w, ">")?;
358        }
359
360        Ok(())
361    }
362}
363
364impl Textify for ptype::UserDefined {
365    fn name() -> &'static str {
366        "UserDefined"
367    }
368
369    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
370        {
371            let name_and_anchor =
372                NamedAnchor::lookup(ctx, ExtensionKind::Type, self.type_reference);
373
374            let param_vec: Vec<Option<Parameter>> = self
375                .type_parameters
376                .iter()
377                .map(|t| t.parameter.clone())
378                .collect();
379            let params = Parameters(&param_vec);
380
381            write!(
382                w,
383                "{name_and_anchor}{null}{var}{params}",
384                name_and_anchor = ctx.display(&name_and_anchor),
385                null = ctx.display(&self.nullability()),
386                var = ctx.display(&TypeVariation(self.type_variation_reference)),
387                params = ctx.display(&params)
388            )
389        }
390    }
391}
392
393impl Textify for ptype::Kind {
394    fn name() -> &'static str {
395        "Kind"
396    }
397
398    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
399        match self {
400            // This is the expansion of:
401            //     textify_kind!(ctx, w, k, "boolean")
402            // Shown here for visibility
403            ptype::Kind::Bool(k) => textify_type(
404                ctx,
405                w,
406                "boolean",
407                k.nullability(),
408                k.type_variation_reference,
409                Parameters(&[]),
410            ),
411            ptype::Kind::I8(k) => textify_kind!(ctx, w, k, "i8"),
412            ptype::Kind::I16(k) => textify_kind!(ctx, w, k, "i16"),
413            ptype::Kind::I32(k) => textify_kind!(ctx, w, k, "i32"),
414            ptype::Kind::I64(k) => textify_kind!(ctx, w, k, "i64"),
415            ptype::Kind::Fp32(k) => textify_kind!(ctx, w, k, "fp32"),
416            ptype::Kind::Fp64(k) => textify_kind!(ctx, w, k, "fp64"),
417            ptype::Kind::String(k) => textify_kind!(ctx, w, k, "string"),
418            ptype::Kind::Binary(k) => textify_kind!(ctx, w, k, "binary"),
419            #[allow(deprecated)]
420            ptype::Kind::Timestamp(k) => textify_kind!(ctx, w, k, "timestamp"),
421            ptype::Kind::Date(k) => textify_kind!(ctx, w, k, "date"),
422            #[allow(deprecated)]
423            ptype::Kind::Time(k) => textify_kind!(ctx, w, k, "time"),
424            ptype::Kind::IntervalYear(i) => {
425                textify_kind!(ctx, w, i, "interval_year")
426            }
427            #[allow(deprecated)]
428            ptype::Kind::TimestampTz(ts) => {
429                textify_kind!(ctx, w, ts, "timestamp_tz")
430            }
431            ptype::Kind::Uuid(uuid) => textify_kind!(ctx, w, uuid, "uuid"),
432
433            ptype::Kind::IntervalDay(i) => textify_type(
434                ctx,
435                w,
436                "interval_day",
437                i.nullability(),
438                i.type_variation_reference,
439                // Precision defaults to 6 if unspecified
440                Parameters(&[Some(Parameter::Integer(i.precision.unwrap_or(6) as i64))]),
441            ),
442            ptype::Kind::IntervalCompound(i) => textify_type(
443                ctx,
444                w,
445                "interval_compound",
446                i.nullability(),
447                i.type_variation_reference,
448                Parameters(&[Some(Parameter::Integer(i.precision as i64))]),
449            ),
450            ptype::Kind::FixedChar(c) => textify_type(
451                ctx,
452                w,
453                "fixedchar",
454                c.nullability(),
455                c.type_variation_reference,
456                Parameters(&[Some(Parameter::Integer(c.length as i64))]),
457            ),
458            ptype::Kind::Varchar(c) => textify_type(
459                ctx,
460                w,
461                "varchar",
462                c.nullability(),
463                c.type_variation_reference,
464                Parameters(&[Some(Parameter::Integer(c.length as i64))]),
465            ),
466            ptype::Kind::FixedBinary(b) => textify_type(
467                ctx,
468                w,
469                "fixedbinary",
470                b.nullability(),
471                b.type_variation_reference,
472                Parameters(&[Some(Parameter::Integer(b.length as i64))]),
473            ),
474            ptype::Kind::Decimal(d) => {
475                let p = Parameter::Integer(d.precision as i64);
476                let s = Parameter::Integer(d.scale as i64);
477                textify_type(
478                    ctx,
479                    w,
480                    "decimal",
481                    d.nullability(),
482                    d.type_variation_reference,
483                    Parameters(&[Some(p), Some(s)]),
484                )
485            }
486            ptype::Kind::PrecisionTime(p) => textify_type(
487                ctx,
488                w,
489                "precisiontime",
490                p.nullability(),
491                p.type_variation_reference,
492                Parameters(&[Some(Parameter::Integer(p.precision as i64))]),
493            ),
494            ptype::Kind::PrecisionTimestamp(p) => textify_type(
495                ctx,
496                w,
497                "precisiontimestamp",
498                p.nullability(),
499                p.type_variation_reference,
500                Parameters(&[Some(Parameter::Integer(p.precision as i64))]),
501            ),
502            ptype::Kind::PrecisionTimestampTz(p) => textify_type(
503                ctx,
504                w,
505                "precisiontimestamptz",
506                p.nullability(),
507                p.type_variation_reference,
508                Parameters(&[Some(Parameter::Integer(p.precision as i64))]),
509            ),
510            ptype::Kind::Struct(s) => textify_type(
511                ctx,
512                w,
513                "struct",
514                s.nullability(),
515                s.type_variation_reference,
516                Parameters(
517                    &s.types
518                        .iter()
519                        .map(|t| Some(Parameter::DataType(t.clone())))
520                        .collect::<Vec<_>>(),
521                ),
522            ),
523            ptype::Kind::List(l) => {
524                let p = l
525                    .r#type
526                    .as_ref()
527                    .map(|t| Parameter::DataType((**t).to_owned()));
528                textify_type(
529                    ctx,
530                    w,
531                    "list",
532                    l.nullability(),
533                    l.type_variation_reference,
534                    Parameters(&[p]),
535                )
536            }
537            ptype::Kind::Map(m) => {
538                let k = m
539                    .key
540                    .as_ref()
541                    .map(|t| Parameter::DataType((**t).to_owned()));
542                let v = m
543                    .value
544                    .as_ref()
545                    .map(|t| Parameter::DataType((**t).to_owned()));
546                textify_type(
547                    ctx,
548                    w,
549                    "map",
550                    m.nullability(),
551                    m.type_variation_reference,
552                    Parameters(&[k, v]),
553                )
554            }
555            ptype::Kind::UserDefined(u) => u.textify(ctx, w),
556            #[allow(deprecated)]
557            ptype::Kind::UserDefinedTypeReference(r) => {
558                // Defer to the UserDefined definition, using defaults for
559                // variation, and non-nullable as suggested by the docs
560                let udf = ptype::UserDefined {
561                    type_reference: *r,
562                    type_variation_reference: 0,
563                    nullability: ptype::Nullability::Required as i32,
564                    type_parameters: vec![],
565                };
566                ptype::Kind::UserDefined(udf).textify(ctx, w)
567            }
568            ptype::Kind::Func(_f) => {
569                write!(
570                    w,
571                    "{}",
572                    ctx.failure(PlanError::unimplemented(
573                        "FuncType",
574                        Some("Func"),
575                        "Function type textification not implemented",
576                    ))
577                )
578            }
579            ptype::Kind::Alias(_p) => {
580                write!(
581                    w,
582                    "{}",
583                    ctx.failure(PlanError::unimplemented(
584                        "AliasType",
585                        Some("Alias"),
586                        "TypeAliasReference textification not implemented",
587                    ))
588                )
589            }
590        }
591    }
592}
593
594impl Textify for proto::Type {
595    fn name() -> &'static str {
596        "Type"
597    }
598
599    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
600        write!(w, "{}", ctx.expect(self.kind.as_ref()))
601    }
602}
603
604// /// A schema is a named struct with a list of fields.
605// ///
606// /// This outputs the names and types of the fields in the struct,
607// /// comma-separated.
608// ///
609// /// Assumes that the struct is not nullable, that the type variation reference
610// /// is 0, and that the names and fields match up; otherwise, pushes errors.
611// ///
612// /// Names and fields are output without any bracketing; bring your own
613// /// bracketing.
614// pub struct Schema<'a>(pub &'a proto::NamedStruct);
615
616// impl<'a> Textify for Schema<'a> {
617//     fn name() -> &'static str {
618//         "Schema"
619//     }
620
621//     fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
622//         let mut fields = self
623//             .0
624//             .r#struct
625//             .as_ref()
626//             .map(|s| s.types.iter())
627//             .into_iter()
628//             .flatten();
629//         let mut names = self.0.names.iter();
630
631//         let field_count = self.0.r#struct.as_ref().map(|s| s.types.len()).unwrap_or(0);
632//         let name_count = self.0.names.len();
633
634//         if field_count != name_count {
635//             ctx.push_error(
636//                 TextifyError::invalid(
637//                     "Schema",
638//                     NONSPECIFIC,
639//                     format!(
640//                         "Field count ({}) does not match name count ({})",
641//                         field_count, name_count
642//                     ),
643//                 )
644//                 .into(),
645//             );
646//         }
647
648//         write!(w, "[")?;
649//         let mut first = true;
650//         loop {
651//             let field = fields.next();
652//             let name = names.next().map(|n| Name(n));
653//             if field.is_none() && name.is_none() {
654//                 break;
655//             }
656
657//             if first {
658//                 first = false;
659//             } else {
660//                 write!(w, ", ")?;
661//             }
662
663//             write!(w, "{}:{}", ctx.expect(name.as_ref()), ctx.expect(field))?;
664//         }
665//         write!(w, "]")?;
666
667//         let s = match &self.0.r#struct {
668//             None => return Ok(()),
669//             Some(s) => s,
670//         };
671
672//         if s.nullability() != Nullability::Required {
673//             ctx.push_error(
674//                 TextifyError::invalid(
675//                     "Schema",
676//                     Some("nullabilility"),
677//                     "Expected schema to be Nullability::Required",
678//                 )
679//                 .into(),
680//             );
681//             s.nullability().textify(ctx, w)?;
682//         }
683//         if s.type_variation_reference != 0 {
684//             ctx.push_error(
685//                 TextifyError::invalid(
686//                     "Schema",
687//                     Some("type_variation_reference"),
688//                     "Expected schema to have type_variation_reference 0",
689//                 )
690//                 .into(),
691//             );
692//             TypeVariation(s.type_variation_reference).textify(ctx, w)?;
693//         }
694
695//         Ok(())
696//     }
697// }
698
699#[cfg(test)]
700mod tests {
701
702    use super::*;
703    use crate::extensions::simple::{ExtensionKind, MissingReference};
704    use crate::fixtures::TestContext;
705    use crate::textify::foundation::FormatError;
706
707    #[test]
708    fn type_display() {
709        let ctx = TestContext::new()
710            .with_urn(1, "first")
711            .with_type_variation(1, 2, "u8");
712
713        let t = proto::Type {
714            kind: Some(ptype::Kind::Bool(ptype::Boolean {
715                type_variation_reference: 2,
716                nullability: ptype::Nullability::Nullable as i32,
717            })),
718        };
719
720        let s = ctx.textify_no_errors(&t);
721        assert_eq!(s, "boolean?[u8]");
722
723        let t = proto::Type {
724            kind: Some(ptype::Kind::I8(ptype::I8 {
725                type_variation_reference: 0,
726                nullability: ptype::Nullability::Required as i32,
727            })),
728        };
729        assert_eq!(ctx.textify_no_errors(&t), "i8");
730
731        let t = proto::Type {
732            kind: Some(ptype::Kind::PrecisionTimestamp(ptype::PrecisionTimestamp {
733                type_variation_reference: 0,
734                nullability: ptype::Nullability::Nullable as i32,
735                precision: 3,
736            })),
737        };
738        assert_eq!(ctx.textify_no_errors(&t), "precisiontimestamp?<3>");
739
740        let mut ctx = ctx.with_type_variation(1, 8, "int");
741        ctx.options.show_simple_extension_anchors = Visibility::Always;
742
743        let t = proto::Type {
744            kind: Some(ptype::Kind::PrecisionTime(ptype::PrecisionTime {
745                type_variation_reference: 8,
746                nullability: ptype::Nullability::Nullable as i32,
747                precision: 9,
748            })),
749        };
750        assert_eq!(ctx.textify_no_errors(&t), "precisiontime?[int#8]<9>");
751    }
752
753    #[test]
754    fn type_display_with_errors() {
755        let ctx = TestContext::new()
756            .with_urn(1, "first")
757            .with_type(1, 100, "cow");
758
759        let t = proto::Type {
760            kind: Some(ptype::Kind::Bool(ptype::Boolean {
761                type_variation_reference: 200,
762                nullability: ptype::Nullability::Nullable as i32,
763            })),
764        };
765        let (s, errs) = ctx.textify(&t);
766        assert_eq!(s, "boolean?[!{type_variation}#200]");
767        let err = errs.first();
768        let (&k, &a) = match err {
769            FormatError::Lookup(MissingReference::MissingAnchor(k, a)) => (k, a),
770            _ => panic!("Expected Lookup MissingAnchor: {err}"),
771        };
772
773        assert_eq!(k, ExtensionKind::TypeVariation);
774        assert_eq!(a, 200);
775
776        let t = proto::Type {
777            kind: Some(ptype::Kind::UserDefined(ptype::UserDefined {
778                type_variation_reference: 0,
779                nullability: ptype::Nullability::Required as i32,
780                type_reference: 100,
781                type_parameters: vec![],
782            })),
783        };
784
785        let (s, errs) = ctx.textify(&t);
786        assert!(errs.is_empty());
787        assert_eq!(s, "cow");
788
789        let t = proto::Type {
790            kind: Some(ptype::Kind::UserDefined(ptype::UserDefined {
791                type_variation_reference: 0,
792                nullability: ptype::Nullability::Required as i32,
793                type_reference: 12589,
794                type_parameters: vec![],
795            })),
796        };
797
798        let (s, errs) = ctx.textify(&t);
799        let err = errs.first();
800        let (&k, &a) = match err {
801            FormatError::Lookup(MissingReference::MissingAnchor(k, a)) => (k, a),
802            _ => panic!("Expected Lookup MissingAnchor: {err}"),
803        };
804        assert_eq!(k, ExtensionKind::Type);
805        assert_eq!(a, 12589);
806        assert_eq!(s, "!{type}#12589");
807    }
808
809    #[test]
810    fn struct_display() {
811        let ctx = TestContext::new();
812        let t = proto::Type {
813            kind: Some(ptype::Kind::Struct(ptype::Struct {
814                type_variation_reference: 0,
815                nullability: ptype::Nullability::Nullable as i32,
816                types: vec![
817                    proto::Type {
818                        kind: Some(ptype::Kind::String(ptype::String {
819                            type_variation_reference: 0,
820                            nullability: ptype::Nullability::Required as i32,
821                        })),
822                    },
823                    proto::Type {
824                        kind: Some(ptype::Kind::I8(ptype::I8 {
825                            type_variation_reference: 0,
826                            nullability: ptype::Nullability::Required as i32,
827                        })),
828                    },
829                    proto::Type {
830                        kind: Some(ptype::Kind::I32(ptype::I32 {
831                            type_variation_reference: 0,
832                            nullability: ptype::Nullability::Nullable as i32,
833                        })),
834                    },
835                    proto::Type {
836                        #[allow(deprecated)] // TimestampTz is deprecated
837                        kind: Some(ptype::Kind::TimestampTz(ptype::TimestampTz {
838                            type_variation_reference: 0,
839                            nullability: ptype::Nullability::Required as i32,
840                        })),
841                    },
842                ],
843            })),
844        };
845        assert_eq!(
846            ctx.textify_no_errors(&t),
847            "struct?<string, i8, i32?, timestamp_tz>"
848        );
849    }
850
851    #[test]
852    fn names_display() {
853        let ctx = TestContext::new();
854
855        let n = Name("name");
856        assert_eq!(ctx.textify_no_errors(&n), "name");
857
858        let n = Name("name with spaces");
859        assert_eq!(ctx.textify_no_errors(&n), "\"name with spaces\"");
860    }
861
862    // #[test]
863    // fn schema_display() {
864    //     let ctx = TestContext::new();
865
866    //     let s = ptype::Struct {
867    //         type_variation_reference: 0,
868    //         nullability: ptype::Nullability::Required as i32,
869    //         types: vec![
870    //             proto::Type {
871    //                 kind: Some(ptype::Kind::String(ptype::String {
872    //                     type_variation_reference: 0,
873    //                     nullability: ptype::Nullability::Required as i32,
874    //                 })),
875    //             },
876    //             proto::Type {
877    //                 kind: Some(ptype::Kind::I8(ptype::I8 {
878    //                     type_variation_reference: 0,
879    //                     nullability: ptype::Nullability::Required as i32,
880    //                 })),
881    //             },
882    //             proto::Type {
883    //                 kind: Some(ptype::Kind::I32(ptype::I32 {
884    //                     type_variation_reference: 0,
885    //                     nullability: ptype::Nullability::Nullable as i32,
886    //                 })),
887    //             },
888    //             proto::Type {
889    //                 kind: Some(ptype::Kind::TimestampTz(ptype::TimestampTz {
890    //                     type_variation_reference: 0,
891    //                     nullability: ptype::Nullability::Required as i32,
892    //                 })),
893    //             },
894    //         ],
895    //     };
896
897    //     let names = ["a", "b", "c", "d"].iter().map(|s| s.to_string()).collect();
898    //     let schema = proto::NamedStruct {
899    //         names,
900    //         r#struct: Some(s),
901    //     };
902
903    //     assert_eq!(
904    //         ctx.textify_no_errors(&Schema(&schema)),
905    //         "[a:string, b:i8, c:i32?, d:timestamp_tz]"
906    //     );
907    // }
908
909    // #[test]
910    // fn schema_display_with_errors() {
911    //     let ctx = TestContext::new();
912    //     let string = proto::Type {
913    //         kind: Some(ptype::Kind::String(ptype::String {
914    //             type_variation_reference: 0,
915    //             nullability: ptype::Nullability::Required as i32,
916    //         })),
917    //     };
918    //     let i64 = proto::Type {
919    //         kind: Some(ptype::Kind::I8(ptype::I8 {
920    //             type_variation_reference: 0,
921    //             nullability: ptype::Nullability::Nullable as i32,
922    //         })),
923    //     };
924    //     let fp64 = proto::Type {
925    //         kind: Some(ptype::Kind::Fp64(ptype::Fp64 {
926    //             type_variation_reference: 0,
927    //             nullability: ptype::Nullability::Nullable as i32,
928    //         })),
929    //     };
930
931    //     let s = ptype::Struct {
932    //         type_variation_reference: 0,
933    //         nullability: ptype::Nullability::Required as i32,
934    //         types: vec![string.clone(), i64, fp64, string],
935    //     };
936
937    //     let names = ["name", "id", "distance", "street address"]
938    //         .iter()
939    //         .map(|s| s.to_string())
940    //         .collect();
941    //     let schema = proto::NamedStruct {
942    //         names,
943    //         r#struct: Some(s),
944    //     };
945
946    //     let (s, errs) = ctx.textify(&Schema(&schema));
947    //     assert_eq!(
948    //         s,
949    //         "name:string, id:i8?, distance:fp64?, \"street address\":string"
950    //     );
951    //     assert!(errs.is_empty());
952    // }
953
954    // ---- Tests for NamedAnchor signature display ----
955
956    /// Build a TestContext with two overloaded functions (`equal:any_any` and
957    /// `equal:str_str`) sharing the same URN, plus a unique function (`add`).
958    fn overloaded_ctx() -> TestContext {
959        TestContext::new()
960            .with_urn(1, "substrait:functions_comparison")
961            .with_function(1, 1, "equal:any_any")
962            .with_function(1, 2, "equal:str_str")
963            .with_function(1, 3, "add:i64_i64")
964    }
965
966    #[test]
967    fn named_anchor_compact_unique_base_name_no_signature() {
968        // `add:i64_i64` is the only function with base name "add".
969        // Compact mode: show base name only, no anchor (compound name unique).
970        let ctx = overloaded_ctx();
971        let eq = crate::textify::ErrorQueue::default();
972        let scope = ctx.scope(&eq);
973        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 3);
974        assert!(na.base_name_unique, "add should have unique base name");
975        assert!(na.unique, "add:i64_i64 compound name should be unique");
976
977        let s = ctx.textify_no_errors(&na);
978        assert_eq!(s, "add");
979    }
980
981    #[test]
982    fn named_anchor_compact_overloaded_shows_signature() {
983        // `equal:any_any` shares base name "equal" with `equal:str_str`.
984        // Compact mode: base name not unique → show full compound name.
985        // Compound name is unique (only one URN) → no anchor.
986        let ctx = overloaded_ctx();
987        let eq = crate::textify::ErrorQueue::default();
988        let scope = ctx.scope(&eq);
989        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 1);
990        assert!(!na.base_name_unique, "equal base name should not be unique");
991        assert!(
992            na.unique,
993            "equal:any_any compound name should be unique across URNs"
994        );
995
996        let s = ctx.textify_no_errors(&na);
997        assert_eq!(s, "equal:any_any");
998    }
999
1000    #[test]
1001    fn named_anchor_verbose_unique_base_name_shows_signature_and_anchor() {
1002        // Verbose mode: always show signature and always show anchor.
1003        let mut ctx = overloaded_ctx();
1004        ctx.options.show_simple_extension_anchors = Visibility::Always;
1005
1006        let eq = crate::textify::ErrorQueue::default();
1007        let scope = ctx.scope(&eq);
1008        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 3);
1009        let s = ctx.textify_no_errors(&na);
1010        assert_eq!(s, "add:i64_i64#3");
1011    }
1012
1013    #[test]
1014    fn named_anchor_verbose_overloaded_shows_signature_and_anchor() {
1015        // Verbose mode: overloaded function shows full compound name + anchor.
1016        let mut ctx = overloaded_ctx();
1017        ctx.options.show_simple_extension_anchors = Visibility::Always;
1018
1019        let eq = crate::textify::ErrorQueue::default();
1020        let scope = ctx.scope(&eq);
1021        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 2);
1022        let s = ctx.textify_no_errors(&na);
1023        assert_eq!(s, "equal:str_str#2");
1024    }
1025
1026    #[test]
1027    fn named_anchor_compact_same_compound_name_two_urns_shows_anchor() {
1028        // Same compound name `equal:any_any` registered in two different URNs.
1029        // Compact mode: compound name not unique → anchor required.
1030        let ctx = TestContext::new()
1031            .with_urn(1, "urn_a")
1032            .with_urn(2, "urn_b")
1033            .with_function(1, 1, "equal:any_any")
1034            .with_function(2, 2, "equal:any_any");
1035
1036        let eq = crate::textify::ErrorQueue::default();
1037        let scope = ctx.scope(&eq);
1038        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 1);
1039        assert!(!na.base_name_unique);
1040        assert!(!na.unique, "compound name not unique across two URNs");
1041
1042        let s = ctx.textify_no_errors(&na);
1043        assert_eq!(s, "equal:any_any#1");
1044    }
1045
1046    #[test]
1047    fn named_anchor_compact_unique_full_name_zero_arg_type_signature() {
1048        // "count:" is the only function with base "count".
1049        // Compact mode: base name unique → write base name only, no anchor.
1050        // "count:" and "count:i64" are not conceptually different;
1051        // both write just the base name when unambiguous.
1052        let ctx = TestContext::new()
1053            .with_urn(1, "urn")
1054            .with_function(1, 5, "count:");
1055
1056        let eq = crate::textify::ErrorQueue::default();
1057        let scope = ctx.scope(&eq);
1058        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 5);
1059        assert!(na.base_name_unique);
1060        assert!(na.unique);
1061
1062        let s = ctx.textify_no_errors(&na);
1063        assert_eq!(s, "count");
1064    }
1065
1066    #[test]
1067    fn named_anchor_verbose_full_name_shows_signature_and_anchor() {
1068        // Verbose mode: always writes full() + anchor.
1069        let mut ctx = TestContext::new()
1070            .with_urn(1, "urn")
1071            .with_function(1, 5, "count:");
1072        ctx.options.show_simple_extension_anchors = Visibility::Always;
1073
1074        let eq = crate::textify::ErrorQueue::default();
1075        let scope = ctx.scope(&eq);
1076        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 5);
1077
1078        let s = ctx.textify_no_errors(&na);
1079        assert_eq!(s, "count:#5");
1080    }
1081
1082    #[test]
1083    fn named_anchor_compact_plain_name_unique_no_signature_no_anchor() {
1084        let ctx = TestContext::new()
1085            .with_urn(1, "urn")
1086            .with_function(1, 10, "coalesce");
1087
1088        let eq = crate::textify::ErrorQueue::default();
1089        let scope = ctx.scope(&eq);
1090        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 10);
1091        assert!(na.base_name_unique);
1092        assert!(na.unique);
1093
1094        let s = ctx.textify_no_errors(&na);
1095        assert_eq!(s, "coalesce");
1096    }
1097
1098    #[test]
1099    fn named_anchor_compact_plain_name_non_unique_shows_anchor() {
1100        let ctx = TestContext::new()
1101            .with_urn(1, "urn1")
1102            .with_urn(2, "urn2")
1103            .with_function(1, 231, "duplicated")
1104            .with_function(2, 232, "duplicated");
1105
1106        let eq = crate::textify::ErrorQueue::default();
1107        let scope = ctx.scope(&eq);
1108        let na = NamedAnchor::lookup(&scope, ExtensionKind::Function, 231);
1109        assert!(!na.base_name_unique);
1110        assert!(!na.unique);
1111
1112        let s = ctx.textify_no_errors(&na);
1113        assert_eq!(s, "duplicated#231");
1114    }
1115}