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::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    pub name: MaybeToken<&'a str>,
130    pub anchor: u32,
131    // True if the name is valid and unique for the extension kind, false if not.
132    pub unique: bool,
133}
134
135impl<'a> NamedAnchor<'a> {
136    /// Lookup an anchor in the extensions, and return a NamedAnchor. Errors will be pushed to the ErrorAccumulator along the way.
137    pub fn lookup<S: Scope>(ctx: &'a S, kind: ExtensionKind, anchor: u32) -> Self {
138        let ext = ctx.extensions().find_by_anchor(kind, anchor);
139        let (name, unique) = match ext {
140            Ok((_, n)) => match ctx.extensions().is_name_unique(kind, anchor, n) {
141                // Nothing wrong; may or may not be unique.
142                Ok(unique) => (MaybeToken(Ok(n)), unique),
143                Err(e) => (MaybeToken(Err(ctx.failure(e))), false),
144            },
145            Err(e) => (MaybeToken(Err(ctx.failure(e))), false),
146        };
147        Self {
148            name,
149            anchor,
150            unique,
151        }
152    }
153}
154
155impl<'a> Textify for NamedAnchor<'a> {
156    fn name() -> &'static str {
157        "NamedAnchor"
158    }
159
160    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
161        let anchor = Anchor::new(self.anchor, !self.unique);
162        write!(
163            w,
164            "{name}{anchor}",
165            name = self.name,
166            anchor = ctx.display(&anchor)
167        )
168    }
169}
170
171/// The type desciptor of the output of a function call.
172///
173/// This is optional, and if present, it must be the last argument in the
174/// function call.
175#[derive(Debug, Copy, Clone)]
176pub struct OutputType<T: Deref<Target = proto::Type>>(pub Option<T>);
177
178impl<T: Deref<Target = proto::Type>> Textify for OutputType<T> {
179    fn name() -> &'static str {
180        "OutputType"
181    }
182
183    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
184        match self.0 {
185            Some(ref t) => write!(w, ":{}", ctx.display(t.deref())),
186            None => Ok(()),
187        }
188    }
189}
190
191struct TypeVariation(u32);
192
193impl Textify for TypeVariation {
194    fn name() -> &'static str {
195        "TypeVariation"
196    }
197
198    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
199        let &TypeVariation(anchor) = self;
200        if anchor == 0 {
201            // This is the default, this doesn't count as a type variation
202            return Ok(());
203        }
204        let name_and_anchor = NamedAnchor::lookup(ctx, ExtensionKind::TypeVariation, anchor);
205
206        write!(
207            w,
208            "[{name_and_anchor}]",
209            name_and_anchor = ctx.display(&name_and_anchor)
210        )
211    }
212}
213
214// Textify a standard type with parameters.
215//
216// P will generally be the Parameter type, but it can be any type that
217// implements Textify.
218fn textify_type<S: Scope, W: fmt::Write>(
219    ctx: &S,
220    f: &mut W,
221    name: impl AsRef<str>,
222    nullability: ptype::Nullability,
223    variant: u32,
224    params: Parameters,
225) -> fmt::Result {
226    write!(
227        f,
228        "{name}{null}{var}{params}",
229        name = name.as_ref(),
230        null = ctx.display(&nullability),
231        var = ctx.display(&TypeVariation(variant)),
232        params = ctx.display(&params)
233    )
234}
235
236macro_rules! textify_kind {
237    ($ctx:expr, $f:expr, $kind:ident, $name:expr) => {
238        textify_type(
239            $ctx,
240            $f,
241            $name,
242            $kind.nullability(),
243            $kind.type_variation_reference,
244            Parameters(&[]),
245        )
246    };
247}
248
249impl Textify for Parameter {
250    fn name() -> &'static str {
251        "Parameter"
252    }
253
254    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
255        match self {
256            Parameter::Boolean(true) => write!(w, "true")?,
257            Parameter::Boolean(false) => write!(w, "false")?,
258            Parameter::DataType(t) => write!(w, "{}", ctx.display(t))?,
259            Parameter::Enum(e) => write!(w, "{e}")?,
260            Parameter::Integer(i) => write!(w, "{i}")?,
261            // TODO: Do we just put the string in directly?
262            Parameter::String(s) => write!(w, "{s}")?,
263            Parameter::Null(_) => write!(w, "null")?,
264        };
265
266        Ok(())
267    }
268}
269impl Textify for ptype::Parameter {
270    fn name() -> &'static str {
271        "Parameter"
272    }
273
274    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
275        write!(w, "{}", ctx.expect(self.parameter.as_ref()))
276    }
277}
278
279struct Parameters<'a>(&'a [Option<Parameter>]);
280
281impl<'a> Textify for Parameters<'a> {
282    fn name() -> &'static str {
283        "Parameters"
284    }
285
286    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
287        let mut first = true;
288        for param in self.0.iter() {
289            if first {
290                write!(w, "<")?;
291            } else {
292                write!(w, ", ")?;
293            }
294            write!(w, "{}", ctx.expect(param.as_ref()))?;
295            first = false;
296        }
297        if !first {
298            write!(w, ">")?;
299        }
300
301        Ok(())
302    }
303}
304
305impl Textify for ptype::UserDefined {
306    fn name() -> &'static str {
307        "UserDefined"
308    }
309
310    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
311        {
312            let name_and_anchor =
313                NamedAnchor::lookup(ctx, ExtensionKind::Type, self.type_reference);
314
315            let param_vec: Vec<Option<Parameter>> = self
316                .type_parameters
317                .iter()
318                .map(|t| t.parameter.clone())
319                .collect();
320            let params = Parameters(&param_vec);
321
322            write!(
323                w,
324                "{name_and_anchor}{null}{var}{params}",
325                name_and_anchor = ctx.display(&name_and_anchor),
326                null = ctx.display(&self.nullability()),
327                var = ctx.display(&TypeVariation(self.type_variation_reference)),
328                params = ctx.display(&params)
329            )
330        }
331    }
332}
333
334impl Textify for ptype::Kind {
335    fn name() -> &'static str {
336        "Kind"
337    }
338
339    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
340        match self {
341            // This is the expansion of:
342            //     textify_kind!(ctx, w, k, "boolean")
343            // Shown here for visibility
344            ptype::Kind::Bool(k) => textify_type(
345                ctx,
346                w,
347                "boolean",
348                k.nullability(),
349                k.type_variation_reference,
350                Parameters(&[]),
351            ),
352            ptype::Kind::I8(k) => textify_kind!(ctx, w, k, "i8"),
353            ptype::Kind::I16(k) => textify_kind!(ctx, w, k, "i16"),
354            ptype::Kind::I32(k) => textify_kind!(ctx, w, k, "i32"),
355            ptype::Kind::I64(k) => textify_kind!(ctx, w, k, "i64"),
356            ptype::Kind::Fp32(k) => textify_kind!(ctx, w, k, "fp32"),
357            ptype::Kind::Fp64(k) => textify_kind!(ctx, w, k, "fp64"),
358            ptype::Kind::String(k) => textify_kind!(ctx, w, k, "string"),
359            ptype::Kind::Binary(k) => textify_kind!(ctx, w, k, "binary"),
360            #[allow(deprecated)]
361            ptype::Kind::Timestamp(k) => textify_kind!(ctx, w, k, "timestamp"),
362            ptype::Kind::Date(k) => textify_kind!(ctx, w, k, "date"),
363            ptype::Kind::Time(k) => textify_kind!(ctx, w, k, "time"),
364            ptype::Kind::IntervalYear(i) => {
365                textify_kind!(ctx, w, i, "interval_year")
366            }
367            #[allow(deprecated)]
368            ptype::Kind::TimestampTz(ts) => {
369                textify_kind!(ctx, w, ts, "timestamp_tz")
370            }
371            ptype::Kind::Uuid(uuid) => textify_kind!(ctx, w, uuid, "uuid"),
372
373            ptype::Kind::IntervalDay(i) => textify_type(
374                ctx,
375                w,
376                "interval_day",
377                i.nullability(),
378                i.type_variation_reference,
379                // Precision defaults to 6 if unspecified
380                Parameters(&[Some(Parameter::Integer(i.precision.unwrap_or(6) as i64))]),
381            ),
382            ptype::Kind::IntervalCompound(i) => textify_type(
383                ctx,
384                w,
385                "interval_compound",
386                i.nullability(),
387                i.type_variation_reference,
388                Parameters(&[Some(Parameter::Integer(i.precision as i64))]),
389            ),
390            ptype::Kind::FixedChar(c) => textify_type(
391                ctx,
392                w,
393                "fixedchar",
394                c.nullability(),
395                c.type_variation_reference,
396                Parameters(&[Some(Parameter::Integer(c.length as i64))]),
397            ),
398            ptype::Kind::Varchar(c) => textify_type(
399                ctx,
400                w,
401                "varchar",
402                c.nullability(),
403                c.type_variation_reference,
404                Parameters(&[Some(Parameter::Integer(c.length as i64))]),
405            ),
406            ptype::Kind::FixedBinary(b) => textify_type(
407                ctx,
408                w,
409                "fixedbinary",
410                b.nullability(),
411                b.type_variation_reference,
412                Parameters(&[Some(Parameter::Integer(b.length as i64))]),
413            ),
414            ptype::Kind::Decimal(d) => {
415                let p = Parameter::Integer(d.precision as i64);
416                let s = Parameter::Integer(d.scale as i64);
417                textify_type(
418                    ctx,
419                    w,
420                    "decimal",
421                    d.nullability(),
422                    d.type_variation_reference,
423                    Parameters(&[Some(p), Some(s)]),
424                )
425            }
426            ptype::Kind::PrecisionTime(p) => textify_type(
427                ctx,
428                w,
429                "precisiontime",
430                p.nullability(),
431                p.type_variation_reference,
432                Parameters(&[Some(Parameter::Integer(p.precision as i64))]),
433            ),
434            ptype::Kind::PrecisionTimestamp(p) => textify_type(
435                ctx,
436                w,
437                "precisiontimestamp",
438                p.nullability(),
439                p.type_variation_reference,
440                Parameters(&[Some(Parameter::Integer(p.precision as i64))]),
441            ),
442            ptype::Kind::PrecisionTimestampTz(p) => textify_type(
443                ctx,
444                w,
445                "precisiontimestamptz",
446                p.nullability(),
447                p.type_variation_reference,
448                Parameters(&[Some(Parameter::Integer(p.precision as i64))]),
449            ),
450            ptype::Kind::Struct(s) => textify_type(
451                ctx,
452                w,
453                "struct",
454                s.nullability(),
455                s.type_variation_reference,
456                Parameters(
457                    &s.types
458                        .iter()
459                        .map(|t| Some(Parameter::DataType(t.clone())))
460                        .collect::<Vec<_>>(),
461                ),
462            ),
463            ptype::Kind::List(l) => {
464                let p = l
465                    .r#type
466                    .as_ref()
467                    .map(|t| Parameter::DataType((**t).to_owned()));
468                textify_type(
469                    ctx,
470                    w,
471                    "list",
472                    l.nullability(),
473                    l.type_variation_reference,
474                    Parameters(&[p]),
475                )
476            }
477            ptype::Kind::Map(m) => {
478                let k = m
479                    .key
480                    .as_ref()
481                    .map(|t| Parameter::DataType((**t).to_owned()));
482                let v = m
483                    .value
484                    .as_ref()
485                    .map(|t| Parameter::DataType((**t).to_owned()));
486                textify_type(
487                    ctx,
488                    w,
489                    "map",
490                    m.nullability(),
491                    m.type_variation_reference,
492                    Parameters(&[k, v]),
493                )
494            }
495            ptype::Kind::UserDefined(u) => u.textify(ctx, w),
496            #[allow(deprecated)]
497            ptype::Kind::UserDefinedTypeReference(r) => {
498                // Defer to the UserDefined definition, using defaults for
499                // variation, and non-nullable as suggested by the docs
500                let udf = ptype::UserDefined {
501                    type_reference: *r,
502                    type_variation_reference: 0,
503                    nullability: ptype::Nullability::Required as i32,
504                    type_parameters: vec![],
505                };
506                ptype::Kind::UserDefined(udf).textify(ctx, w)
507            }
508            ptype::Kind::Alias(_p) => {
509                write!(
510                    w,
511                    "{}",
512                    ctx.failure(PlanError::unimplemented(
513                        "AliasType",
514                        Some("Alias"),
515                        "TypeAliasReference textification not implemented",
516                    ))
517                )
518            }
519        }
520    }
521}
522
523impl Textify for proto::Type {
524    fn name() -> &'static str {
525        "Type"
526    }
527
528    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
529        write!(w, "{}", ctx.expect(self.kind.as_ref()))
530    }
531}
532
533// /// A schema is a named struct with a list of fields.
534// ///
535// /// This outputs the names and types of the fields in the struct,
536// /// comma-separated.
537// ///
538// /// Assumes that the struct is not nullable, that the type variation reference
539// /// is 0, and that the names and fields match up; otherwise, pushes errors.
540// ///
541// /// Names and fields are output without any bracketing; bring your own
542// /// bracketing.
543// pub struct Schema<'a>(pub &'a proto::NamedStruct);
544
545// impl<'a> Textify for Schema<'a> {
546//     fn name() -> &'static str {
547//         "Schema"
548//     }
549
550//     fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
551//         let mut fields = self
552//             .0
553//             .r#struct
554//             .as_ref()
555//             .map(|s| s.types.iter())
556//             .into_iter()
557//             .flatten();
558//         let mut names = self.0.names.iter();
559
560//         let field_count = self.0.r#struct.as_ref().map(|s| s.types.len()).unwrap_or(0);
561//         let name_count = self.0.names.len();
562
563//         if field_count != name_count {
564//             ctx.push_error(
565//                 TextifyError::invalid(
566//                     "Schema",
567//                     NONSPECIFIC,
568//                     format!(
569//                         "Field count ({}) does not match name count ({})",
570//                         field_count, name_count
571//                     ),
572//                 )
573//                 .into(),
574//             );
575//         }
576
577//         write!(w, "[")?;
578//         let mut first = true;
579//         loop {
580//             let field = fields.next();
581//             let name = names.next().map(|n| Name(n));
582//             if field.is_none() && name.is_none() {
583//                 break;
584//             }
585
586//             if first {
587//                 first = false;
588//             } else {
589//                 write!(w, ", ")?;
590//             }
591
592//             write!(w, "{}:{}", ctx.expect(name.as_ref()), ctx.expect(field))?;
593//         }
594//         write!(w, "]")?;
595
596//         let s = match &self.0.r#struct {
597//             None => return Ok(()),
598//             Some(s) => s,
599//         };
600
601//         if s.nullability() != Nullability::Required {
602//             ctx.push_error(
603//                 TextifyError::invalid(
604//                     "Schema",
605//                     Some("nullabilility"),
606//                     "Expected schema to be Nullability::Required",
607//                 )
608//                 .into(),
609//             );
610//             s.nullability().textify(ctx, w)?;
611//         }
612//         if s.type_variation_reference != 0 {
613//             ctx.push_error(
614//                 TextifyError::invalid(
615//                     "Schema",
616//                     Some("type_variation_reference"),
617//                     "Expected schema to have type_variation_reference 0",
618//                 )
619//                 .into(),
620//             );
621//             TypeVariation(s.type_variation_reference).textify(ctx, w)?;
622//         }
623
624//         Ok(())
625//     }
626// }
627
628#[cfg(test)]
629mod tests {
630
631    use super::*;
632    use crate::extensions::simple::{ExtensionKind, MissingReference};
633    use crate::fixtures::TestContext;
634    use crate::textify::foundation::FormatError;
635
636    #[test]
637    fn type_display() {
638        let ctx = TestContext::new()
639            .with_urn(1, "first")
640            .with_type_variation(1, 2, "u8");
641
642        let t = proto::Type {
643            kind: Some(ptype::Kind::Bool(ptype::Boolean {
644                type_variation_reference: 2,
645                nullability: ptype::Nullability::Nullable as i32,
646            })),
647        };
648
649        let s = ctx.textify_no_errors(&t);
650        assert_eq!(s, "boolean?[u8]");
651
652        let t = proto::Type {
653            kind: Some(ptype::Kind::I8(ptype::I8 {
654                type_variation_reference: 0,
655                nullability: ptype::Nullability::Required as i32,
656            })),
657        };
658        assert_eq!(ctx.textify_no_errors(&t), "i8");
659
660        let t = proto::Type {
661            kind: Some(ptype::Kind::PrecisionTimestamp(ptype::PrecisionTimestamp {
662                type_variation_reference: 0,
663                nullability: ptype::Nullability::Nullable as i32,
664                precision: 3,
665            })),
666        };
667        assert_eq!(ctx.textify_no_errors(&t), "precisiontimestamp?<3>");
668
669        let mut ctx = ctx.with_type_variation(1, 8, "int");
670        ctx.options.show_simple_extension_anchors = Visibility::Always;
671
672        let t = proto::Type {
673            kind: Some(ptype::Kind::PrecisionTime(ptype::PrecisionTime {
674                type_variation_reference: 8,
675                nullability: ptype::Nullability::Nullable as i32,
676                precision: 9,
677            })),
678        };
679        assert_eq!(ctx.textify_no_errors(&t), "precisiontime?[int#8]<9>");
680    }
681
682    #[test]
683    fn type_display_with_errors() {
684        let ctx = TestContext::new()
685            .with_urn(1, "first")
686            .with_type(1, 100, "cow");
687
688        let t = proto::Type {
689            kind: Some(ptype::Kind::Bool(ptype::Boolean {
690                type_variation_reference: 200,
691                nullability: ptype::Nullability::Nullable as i32,
692            })),
693        };
694        let (s, errs) = ctx.textify(&t);
695        assert_eq!(s, "boolean?[!{type_variation}#200]");
696        let err = errs.first();
697        let (&k, &a) = match err {
698            FormatError::Lookup(MissingReference::MissingAnchor(k, a)) => (k, a),
699            _ => panic!("Expected Lookup MissingAnchor: {err}"),
700        };
701
702        assert_eq!(k, ExtensionKind::TypeVariation);
703        assert_eq!(a, 200);
704
705        let t = proto::Type {
706            kind: Some(ptype::Kind::UserDefined(ptype::UserDefined {
707                type_variation_reference: 0,
708                nullability: ptype::Nullability::Required as i32,
709                type_reference: 100,
710                type_parameters: vec![],
711            })),
712        };
713
714        let (s, errs) = ctx.textify(&t);
715        assert!(errs.is_empty());
716        assert_eq!(s, "cow");
717
718        let t = proto::Type {
719            kind: Some(ptype::Kind::UserDefined(ptype::UserDefined {
720                type_variation_reference: 0,
721                nullability: ptype::Nullability::Required as i32,
722                type_reference: 12589,
723                type_parameters: vec![],
724            })),
725        };
726
727        let (s, errs) = ctx.textify(&t);
728        let err = errs.first();
729        let (&k, &a) = match err {
730            FormatError::Lookup(MissingReference::MissingAnchor(k, a)) => (k, a),
731            _ => panic!("Expected Lookup MissingAnchor: {err}"),
732        };
733        assert_eq!(k, ExtensionKind::Type);
734        assert_eq!(a, 12589);
735        assert_eq!(s, "!{type}#12589");
736    }
737
738    #[test]
739    fn struct_display() {
740        let ctx = TestContext::new();
741        let t = proto::Type {
742            kind: Some(ptype::Kind::Struct(ptype::Struct {
743                type_variation_reference: 0,
744                nullability: ptype::Nullability::Nullable as i32,
745                types: vec![
746                    proto::Type {
747                        kind: Some(ptype::Kind::String(ptype::String {
748                            type_variation_reference: 0,
749                            nullability: ptype::Nullability::Required as i32,
750                        })),
751                    },
752                    proto::Type {
753                        kind: Some(ptype::Kind::I8(ptype::I8 {
754                            type_variation_reference: 0,
755                            nullability: ptype::Nullability::Required as i32,
756                        })),
757                    },
758                    proto::Type {
759                        kind: Some(ptype::Kind::I32(ptype::I32 {
760                            type_variation_reference: 0,
761                            nullability: ptype::Nullability::Nullable as i32,
762                        })),
763                    },
764                    proto::Type {
765                        #[allow(deprecated)] // TimestampTz is deprecated
766                        kind: Some(ptype::Kind::TimestampTz(ptype::TimestampTz {
767                            type_variation_reference: 0,
768                            nullability: ptype::Nullability::Required as i32,
769                        })),
770                    },
771                ],
772            })),
773        };
774        assert_eq!(
775            ctx.textify_no_errors(&t),
776            "struct?<string, i8, i32?, timestamp_tz>"
777        );
778    }
779
780    #[test]
781    fn names_display() {
782        let ctx = TestContext::new();
783
784        let n = Name("name");
785        assert_eq!(ctx.textify_no_errors(&n), "name");
786
787        let n = Name("name with spaces");
788        assert_eq!(ctx.textify_no_errors(&n), "\"name with spaces\"");
789    }
790
791    // #[test]
792    // fn schema_display() {
793    //     let ctx = TestContext::new();
794
795    //     let s = ptype::Struct {
796    //         type_variation_reference: 0,
797    //         nullability: ptype::Nullability::Required as i32,
798    //         types: vec![
799    //             proto::Type {
800    //                 kind: Some(ptype::Kind::String(ptype::String {
801    //                     type_variation_reference: 0,
802    //                     nullability: ptype::Nullability::Required as i32,
803    //                 })),
804    //             },
805    //             proto::Type {
806    //                 kind: Some(ptype::Kind::I8(ptype::I8 {
807    //                     type_variation_reference: 0,
808    //                     nullability: ptype::Nullability::Required as i32,
809    //                 })),
810    //             },
811    //             proto::Type {
812    //                 kind: Some(ptype::Kind::I32(ptype::I32 {
813    //                     type_variation_reference: 0,
814    //                     nullability: ptype::Nullability::Nullable as i32,
815    //                 })),
816    //             },
817    //             proto::Type {
818    //                 kind: Some(ptype::Kind::TimestampTz(ptype::TimestampTz {
819    //                     type_variation_reference: 0,
820    //                     nullability: ptype::Nullability::Required as i32,
821    //                 })),
822    //             },
823    //         ],
824    //     };
825
826    //     let names = ["a", "b", "c", "d"].iter().map(|s| s.to_string()).collect();
827    //     let schema = proto::NamedStruct {
828    //         names,
829    //         r#struct: Some(s),
830    //     };
831
832    //     assert_eq!(
833    //         ctx.textify_no_errors(&Schema(&schema)),
834    //         "[a:string, b:i8, c:i32?, d:timestamp_tz]"
835    //     );
836    // }
837
838    // #[test]
839    // fn schema_display_with_errors() {
840    //     let ctx = TestContext::new();
841    //     let string = proto::Type {
842    //         kind: Some(ptype::Kind::String(ptype::String {
843    //             type_variation_reference: 0,
844    //             nullability: ptype::Nullability::Required as i32,
845    //         })),
846    //     };
847    //     let i64 = proto::Type {
848    //         kind: Some(ptype::Kind::I8(ptype::I8 {
849    //             type_variation_reference: 0,
850    //             nullability: ptype::Nullability::Nullable as i32,
851    //         })),
852    //     };
853    //     let fp64 = proto::Type {
854    //         kind: Some(ptype::Kind::Fp64(ptype::Fp64 {
855    //             type_variation_reference: 0,
856    //             nullability: ptype::Nullability::Nullable as i32,
857    //         })),
858    //     };
859
860    //     let s = ptype::Struct {
861    //         type_variation_reference: 0,
862    //         nullability: ptype::Nullability::Required as i32,
863    //         types: vec![string.clone(), i64, fp64, string],
864    //     };
865
866    //     let names = ["name", "id", "distance", "street address"]
867    //         .iter()
868    //         .map(|s| s.to_string())
869    //         .collect();
870    //     let schema = proto::NamedStruct {
871    //         names,
872    //         r#struct: Some(s),
873    //     };
874
875    //     let (s, errs) = ctx.textify(&Schema(&schema));
876    //     assert_eq!(
877    //         s,
878    //         "name:string, id:i8?, distance:fp64?, \"street address\":string"
879    //     );
880    //     assert!(errs.is_empty());
881    // }
882}