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            ptype::Kind::Timestamp(k) => textify_kind!(ctx, w, k, "timestamp"),
361            ptype::Kind::Date(k) => textify_kind!(ctx, w, k, "date"),
362            ptype::Kind::Time(k) => textify_kind!(ctx, w, k, "time"),
363            ptype::Kind::IntervalYear(i) => {
364                textify_kind!(ctx, w, i, "interval_year")
365            }
366
367            ptype::Kind::TimestampTz(ts) => {
368                textify_kind!(ctx, w, ts, "timestamp_tz")
369            }
370            ptype::Kind::Uuid(uuid) => textify_kind!(ctx, w, uuid, "uuid"),
371
372            ptype::Kind::IntervalDay(i) => textify_type(
373                ctx,
374                w,
375                "interval_day",
376                i.nullability(),
377                i.type_variation_reference,
378                // Precision defaults to 6 if unspecified
379                Parameters(&[Some(Parameter::Integer(i.precision.unwrap_or(6) as i64))]),
380            ),
381            ptype::Kind::IntervalCompound(i) => textify_type(
382                ctx,
383                w,
384                "interval_compound",
385                i.nullability(),
386                i.type_variation_reference,
387                Parameters(&[Some(Parameter::Integer(i.precision as i64))]),
388            ),
389            ptype::Kind::FixedChar(c) => textify_type(
390                ctx,
391                w,
392                "fixedchar",
393                c.nullability(),
394                c.type_variation_reference,
395                Parameters(&[Some(Parameter::Integer(c.length as i64))]),
396            ),
397            ptype::Kind::Varchar(_c) => todo!(),
398            ptype::Kind::FixedBinary(_b) => todo!(),
399            ptype::Kind::Decimal(_d) => todo!(),
400            ptype::Kind::PrecisionTime(p) => textify_type(
401                ctx,
402                w,
403                "precisiontime",
404                p.nullability(),
405                p.type_variation_reference,
406                Parameters(&[Some(Parameter::Integer(p.precision as i64))]),
407            ),
408            ptype::Kind::PrecisionTimestamp(p) => textify_type(
409                ctx,
410                w,
411                "precisiontimestamp",
412                p.nullability(),
413                p.type_variation_reference,
414                Parameters(&[Some(Parameter::Integer(p.precision as i64))]),
415            ),
416            ptype::Kind::PrecisionTimestampTz(_p) => todo!(),
417            ptype::Kind::Struct(s) => textify_type(
418                ctx,
419                w,
420                "struct",
421                s.nullability(),
422                s.type_variation_reference,
423                Parameters(
424                    &s.types
425                        .iter()
426                        .map(|t| Some(Parameter::DataType(t.clone())))
427                        .collect::<Vec<_>>(),
428                ),
429            ),
430            ptype::Kind::List(l) => {
431                let p = l
432                    .r#type
433                    .as_ref()
434                    .map(|t| Parameter::DataType((**t).to_owned()));
435                textify_type(
436                    ctx,
437                    w,
438                    "list",
439                    l.nullability(),
440                    l.type_variation_reference,
441                    Parameters(&[p]),
442                )
443            }
444            ptype::Kind::Map(m) => {
445                let k = m
446                    .key
447                    .as_ref()
448                    .map(|t| Parameter::DataType((**t).to_owned()));
449                let v = m
450                    .value
451                    .as_ref()
452                    .map(|t| Parameter::DataType((**t).to_owned()));
453                textify_type(
454                    ctx,
455                    w,
456                    "map",
457                    m.nullability(),
458                    m.type_variation_reference,
459                    Parameters(&[k, v]),
460                )
461            }
462            ptype::Kind::UserDefined(u) => u.textify(ctx, w),
463            ptype::Kind::UserDefinedTypeReference(r) => {
464                // Defer to the UserDefined definition, using defaults for
465                // variation, and non-nullable as suggested by the docs
466                let udf = ptype::UserDefined {
467                    type_reference: *r,
468                    type_variation_reference: 0,
469                    nullability: ptype::Nullability::Required as i32,
470                    type_parameters: vec![],
471                };
472                ptype::Kind::UserDefined(udf).textify(ctx, w)
473            }
474        }
475    }
476}
477
478impl Textify for proto::Type {
479    fn name() -> &'static str {
480        "Type"
481    }
482
483    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
484        write!(w, "{}", ctx.expect(self.kind.as_ref()))
485    }
486}
487
488// /// A schema is a named struct with a list of fields.
489// ///
490// /// This outputs the names and types of the fields in the struct,
491// /// comma-separated.
492// ///
493// /// Assumes that the struct is not nullable, that the type variation reference
494// /// is 0, and that the names and fields match up; otherwise, pushes errors.
495// ///
496// /// Names and fields are output without any bracketing; bring your own
497// /// bracketing.
498// pub struct Schema<'a>(pub &'a proto::NamedStruct);
499
500// impl<'a> Textify for Schema<'a> {
501//     fn name() -> &'static str {
502//         "Schema"
503//     }
504
505//     fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
506//         let mut fields = self
507//             .0
508//             .r#struct
509//             .as_ref()
510//             .map(|s| s.types.iter())
511//             .into_iter()
512//             .flatten();
513//         let mut names = self.0.names.iter();
514
515//         let field_count = self.0.r#struct.as_ref().map(|s| s.types.len()).unwrap_or(0);
516//         let name_count = self.0.names.len();
517
518//         if field_count != name_count {
519//             ctx.push_error(
520//                 TextifyError::invalid(
521//                     "Schema",
522//                     NONSPECIFIC,
523//                     format!(
524//                         "Field count ({}) does not match name count ({})",
525//                         field_count, name_count
526//                     ),
527//                 )
528//                 .into(),
529//             );
530//         }
531
532//         write!(w, "[")?;
533//         let mut first = true;
534//         loop {
535//             let field = fields.next();
536//             let name = names.next().map(|n| Name(n));
537//             if field.is_none() && name.is_none() {
538//                 break;
539//             }
540
541//             if first {
542//                 first = false;
543//             } else {
544//                 write!(w, ", ")?;
545//             }
546
547//             write!(w, "{}:{}", ctx.expect(name.as_ref()), ctx.expect(field))?;
548//         }
549//         write!(w, "]")?;
550
551//         let s = match &self.0.r#struct {
552//             None => return Ok(()),
553//             Some(s) => s,
554//         };
555
556//         if s.nullability() != Nullability::Required {
557//             ctx.push_error(
558//                 TextifyError::invalid(
559//                     "Schema",
560//                     Some("nullabilility"),
561//                     "Expected schema to be Nullability::Required",
562//                 )
563//                 .into(),
564//             );
565//             s.nullability().textify(ctx, w)?;
566//         }
567//         if s.type_variation_reference != 0 {
568//             ctx.push_error(
569//                 TextifyError::invalid(
570//                     "Schema",
571//                     Some("type_variation_reference"),
572//                     "Expected schema to have type_variation_reference 0",
573//                 )
574//                 .into(),
575//             );
576//             TypeVariation(s.type_variation_reference).textify(ctx, w)?;
577//         }
578
579//         Ok(())
580//     }
581// }
582
583#[cfg(test)]
584mod tests {
585
586    use super::*;
587    use crate::extensions::simple::{ExtensionKind, MissingReference};
588    use crate::fixtures::TestContext;
589    use crate::textify::foundation::FormatError;
590
591    #[test]
592    fn type_display() {
593        let ctx = TestContext::new()
594            .with_uri(1, "first")
595            .with_type_variation(1, 2, "u8");
596
597        let t = proto::Type {
598            kind: Some(ptype::Kind::Bool(ptype::Boolean {
599                type_variation_reference: 2,
600                nullability: ptype::Nullability::Nullable as i32,
601            })),
602        };
603
604        let s = ctx.textify_no_errors(&t);
605        assert_eq!(s, "boolean?[u8]");
606
607        let t = proto::Type {
608            kind: Some(ptype::Kind::I8(ptype::I8 {
609                type_variation_reference: 0,
610                nullability: ptype::Nullability::Required as i32,
611            })),
612        };
613        assert_eq!(ctx.textify_no_errors(&t), "i8");
614
615        let t = proto::Type {
616            kind: Some(ptype::Kind::PrecisionTimestamp(ptype::PrecisionTimestamp {
617                type_variation_reference: 0,
618                nullability: ptype::Nullability::Nullable as i32,
619                precision: 3,
620            })),
621        };
622        assert_eq!(ctx.textify_no_errors(&t), "precisiontimestamp?<3>");
623
624        let mut ctx = ctx.with_type_variation(1, 8, "int");
625        ctx.options.show_simple_extension_anchors = Visibility::Always;
626
627        let t = proto::Type {
628            kind: Some(ptype::Kind::PrecisionTime(ptype::PrecisionTime {
629                type_variation_reference: 8,
630                nullability: ptype::Nullability::Nullable as i32,
631                precision: 9,
632            })),
633        };
634        assert_eq!(ctx.textify_no_errors(&t), "precisiontime?[int#8]<9>");
635    }
636
637    #[test]
638    fn type_display_with_errors() {
639        let ctx = TestContext::new()
640            .with_uri(1, "first")
641            .with_type(1, 100, "cow");
642
643        let t = proto::Type {
644            kind: Some(ptype::Kind::Bool(ptype::Boolean {
645                type_variation_reference: 200,
646                nullability: ptype::Nullability::Nullable as i32,
647            })),
648        };
649        let (s, errs) = ctx.textify(&t);
650        assert_eq!(s, "boolean?[!{type_variation}#200]");
651        let err = errs.first();
652        let (&k, &a) = match err {
653            FormatError::Lookup(MissingReference::MissingAnchor(k, a)) => (k, a),
654            _ => panic!("Expected Lookup MissingAnchor: {err}"),
655        };
656
657        assert_eq!(k, ExtensionKind::TypeVariation);
658        assert_eq!(a, 200);
659
660        let t = proto::Type {
661            kind: Some(ptype::Kind::UserDefined(ptype::UserDefined {
662                type_variation_reference: 0,
663                nullability: ptype::Nullability::Required as i32,
664                type_reference: 100,
665                type_parameters: vec![],
666            })),
667        };
668
669        let (s, errs) = ctx.textify(&t);
670        assert!(errs.is_empty());
671        assert_eq!(s, "cow");
672
673        let t = proto::Type {
674            kind: Some(ptype::Kind::UserDefined(ptype::UserDefined {
675                type_variation_reference: 0,
676                nullability: ptype::Nullability::Required as i32,
677                type_reference: 12589,
678                type_parameters: vec![],
679            })),
680        };
681
682        let (s, errs) = ctx.textify(&t);
683        let err = errs.first();
684        let (&k, &a) = match err {
685            FormatError::Lookup(MissingReference::MissingAnchor(k, a)) => (k, a),
686            _ => panic!("Expected Lookup MissingAnchor: {err}"),
687        };
688        assert_eq!(k, ExtensionKind::Type);
689        assert_eq!(a, 12589);
690        assert_eq!(s, "!{type}#12589");
691    }
692
693    #[test]
694    fn struct_display() {
695        let ctx = TestContext::new();
696
697        let t = proto::Type {
698            kind: Some(ptype::Kind::Struct(ptype::Struct {
699                type_variation_reference: 0,
700                nullability: ptype::Nullability::Nullable as i32,
701                types: vec![
702                    proto::Type {
703                        kind: Some(ptype::Kind::String(ptype::String {
704                            type_variation_reference: 0,
705                            nullability: ptype::Nullability::Required as i32,
706                        })),
707                    },
708                    proto::Type {
709                        kind: Some(ptype::Kind::I8(ptype::I8 {
710                            type_variation_reference: 0,
711                            nullability: ptype::Nullability::Required as i32,
712                        })),
713                    },
714                    proto::Type {
715                        kind: Some(ptype::Kind::I32(ptype::I32 {
716                            type_variation_reference: 0,
717                            nullability: ptype::Nullability::Nullable as i32,
718                        })),
719                    },
720                    proto::Type {
721                        kind: Some(ptype::Kind::TimestampTz(ptype::TimestampTz {
722                            type_variation_reference: 0,
723                            nullability: ptype::Nullability::Required as i32,
724                        })),
725                    },
726                ],
727            })),
728        };
729        assert_eq!(
730            ctx.textify_no_errors(&t),
731            "struct?<string, i8, i32?, timestamp_tz>"
732        );
733    }
734
735    #[test]
736    fn names_display() {
737        let ctx = TestContext::new();
738
739        let n = Name("name");
740        assert_eq!(ctx.textify_no_errors(&n), "name");
741
742        let n = Name("name with spaces");
743        assert_eq!(ctx.textify_no_errors(&n), "\"name with spaces\"");
744    }
745
746    // #[test]
747    // fn schema_display() {
748    //     let ctx = TestContext::new();
749
750    //     let s = ptype::Struct {
751    //         type_variation_reference: 0,
752    //         nullability: ptype::Nullability::Required as i32,
753    //         types: vec![
754    //             proto::Type {
755    //                 kind: Some(ptype::Kind::String(ptype::String {
756    //                     type_variation_reference: 0,
757    //                     nullability: ptype::Nullability::Required as i32,
758    //                 })),
759    //             },
760    //             proto::Type {
761    //                 kind: Some(ptype::Kind::I8(ptype::I8 {
762    //                     type_variation_reference: 0,
763    //                     nullability: ptype::Nullability::Required as i32,
764    //                 })),
765    //             },
766    //             proto::Type {
767    //                 kind: Some(ptype::Kind::I32(ptype::I32 {
768    //                     type_variation_reference: 0,
769    //                     nullability: ptype::Nullability::Nullable as i32,
770    //                 })),
771    //             },
772    //             proto::Type {
773    //                 kind: Some(ptype::Kind::TimestampTz(ptype::TimestampTz {
774    //                     type_variation_reference: 0,
775    //                     nullability: ptype::Nullability::Required as i32,
776    //                 })),
777    //             },
778    //         ],
779    //     };
780
781    //     let names = ["a", "b", "c", "d"].iter().map(|s| s.to_string()).collect();
782    //     let schema = proto::NamedStruct {
783    //         names,
784    //         r#struct: Some(s),
785    //     };
786
787    //     assert_eq!(
788    //         ctx.textify_no_errors(&Schema(&schema)),
789    //         "[a:string, b:i8, c:i32?, d:timestamp_tz]"
790    //     );
791    // }
792
793    // #[test]
794    // fn schema_display_with_errors() {
795    //     let ctx = TestContext::new();
796    //     let string = proto::Type {
797    //         kind: Some(ptype::Kind::String(ptype::String {
798    //             type_variation_reference: 0,
799    //             nullability: ptype::Nullability::Required as i32,
800    //         })),
801    //     };
802    //     let i64 = proto::Type {
803    //         kind: Some(ptype::Kind::I8(ptype::I8 {
804    //             type_variation_reference: 0,
805    //             nullability: ptype::Nullability::Nullable as i32,
806    //         })),
807    //     };
808    //     let fp64 = proto::Type {
809    //         kind: Some(ptype::Kind::Fp64(ptype::Fp64 {
810    //             type_variation_reference: 0,
811    //             nullability: ptype::Nullability::Nullable as i32,
812    //         })),
813    //     };
814
815    //     let s = ptype::Struct {
816    //         type_variation_reference: 0,
817    //         nullability: ptype::Nullability::Required as i32,
818    //         types: vec![string.clone(), i64, fp64, string],
819    //     };
820
821    //     let names = ["name", "id", "distance", "street address"]
822    //         .iter()
823    //         .map(|s| s.to_string())
824    //         .collect();
825    //     let schema = proto::NamedStruct {
826    //         names,
827    //         r#struct: Some(s),
828    //     };
829
830    //     let (s, errs) = ctx.textify(&Schema(&schema));
831    //     assert_eq!(
832    //         s,
833    //         "name:string, id:i8?, distance:fp64?, \"street address\":string"
834    //     );
835    //     assert!(errs.is_empty());
836    // }
837}