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