Skip to main content

substrait_explain/textify/
expressions.rs

1use std::fmt;
2
3use chrono::{DateTime, NaiveDate, NaiveTime};
4use expr::RexType;
5use substrait::proto::expression::field_reference::{ReferenceType, RootReference, RootType};
6use substrait::proto::expression::literal::interval_day_to_second::PrecisionMode;
7use substrait::proto::expression::literal::{IntervalDayToSecond, LiteralType};
8use substrait::proto::expression::{
9    Cast, FieldReference, IfThen, ReferenceSegment, ScalarFunction, cast, reference_segment,
10};
11use substrait::proto::function_argument::ArgType;
12use substrait::proto::{
13    AggregateFunction, Expression, FunctionArgument, FunctionOption, expression as expr,
14};
15
16use super::{PlanError, Scope, Textify, Visibility};
17use crate::extensions::simple::ExtensionKind;
18use crate::precision::SupportedPrecision;
19use crate::textify::types::{Name, NamedAnchor, OutputType, escaped};
20
21// …(…) for function call
22// […] for variant
23// <…> for parameters
24// !{…} for missing value
25
26// $… for field reference
27// #… for anchor
28// @… for URN anchor
29// …::… for cast
30// …:… for specifying type
31// &… for enum
32
33pub fn textify_binary<S: Scope, W: fmt::Write>(items: &[u8], ctx: &S, w: &mut W) -> fmt::Result {
34    if ctx.options().show_literal_binaries {
35        write!(w, "0x")?;
36        for &n in items {
37            write!(w, "{n:02x}")?;
38        }
39    } else {
40        write!(w, "{{binary}}")?;
41    }
42    Ok(())
43}
44
45/// Write an error token for a literal type that hasn't been implemented yet.
46fn unimplemented_literal<S: Scope, W: fmt::Write>(
47    variant: &'static str,
48    ctx: &S,
49    w: &mut W,
50) -> fmt::Result {
51    write!(
52        w,
53        "{}",
54        ctx.failure(PlanError::unimplemented(
55            "LiteralType",
56            Some(variant),
57            format!("{variant} literal textification not implemented"),
58        ))
59    )
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63enum PrecisionFormatError {
64    /// `precision` isn't one of the precisions literals support (0, 3, 6, 9, 12).
65    UnsupportedPrecision,
66    /// `value` can't be represented at `precision`: it overflows the
67    /// representable range (timestamps) or falls outside a single day (times).
68    OutOfRange,
69}
70
71impl PrecisionFormatError {
72    fn into_plan_error(self, variant: &'static str, precision: i32) -> PlanError {
73        let message = match self {
74            PrecisionFormatError::UnsupportedPrecision => {
75                format!("unsupported precision {precision} for {variant}")
76            }
77            PrecisionFormatError::OutOfRange => {
78                format!("value is out of range for {variant} at precision {precision}")
79            }
80        };
81        PlanError::invalid("LiteralType", Some(variant), message)
82    }
83}
84
85/// Returns the diagnostic for truncating a picosecond value to nanoseconds.
86fn picosecond_truncation_warning(variant: &'static str) -> PlanError {
87    PlanError::invalid(
88        "LiteralType",
89        Some(variant),
90        "precision 12 (picoseconds) truncated to nanoseconds; sub-nanosecond precision lost",
91    )
92}
93
94fn write_precision_literal<S: Scope, W: fmt::Write>(
95    variant: &'static str,
96    precision: i32,
97    formatted: Result<String, PrecisionFormatError>,
98    ctx: &S,
99    w: &mut W,
100) -> fmt::Result {
101    match formatted {
102        Ok(s) => {
103            if precision == 12 {
104                ctx.push_error(picosecond_truncation_warning(variant).into());
105            }
106            write!(w, "'{}'", escaped(&s))
107        }
108        Err(e) => write!(w, "{}", ctx.failure(e.into_plan_error(variant, precision))),
109    }
110}
111
112/// Write an enum value. Enums are written as `&<identifier>`, if the string is
113/// a valid identifier; otherwise, they are written as `&'<escaped_string>'`.
114pub fn textify_enum<S: Scope, W: fmt::Write>(s: &str, _ctx: &S, w: &mut W) -> fmt::Result {
115    write!(w, "&{}", Name(s))
116}
117
118/// Convert days since Unix epoch to date string
119fn days_to_date_string(days: i32) -> String {
120    let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
121    let date = epoch + chrono::Duration::days(days as i64);
122    date.format("%Y-%m-%d").to_string()
123}
124
125/// Convert a value in `precision` units, to a `chrono::Duration`.
126/// Precision 12 (picoseconds) is truncated to nanoseconds: chrono can't represent sub-nanosecond resolution.
127fn duration_from_precision_units(
128    value: i64,
129    precision: i32,
130) -> Result<chrono::Duration, PrecisionFormatError> {
131    match precision {
132        0 => chrono::Duration::try_seconds(value).ok_or(PrecisionFormatError::OutOfRange),
133        3 => chrono::Duration::try_milliseconds(value).ok_or(PrecisionFormatError::OutOfRange),
134        6 => Ok(chrono::Duration::microseconds(value)),
135        9 => Ok(chrono::Duration::nanoseconds(value)),
136        12 => Ok(chrono::Duration::nanoseconds(value / 1000)),
137        _ => Err(PrecisionFormatError::UnsupportedPrecision),
138    }
139}
140
141/// The chrono fractional-seconds format specifier for a precision, or `""` for
142/// precision 0 (whole seconds, no fractional part). A fixed-width specifier is
143/// used so the rendered fractional digits match the declared precision exactly.
144/// Precision 12 renders at nanosecond width, matching the truncation in [`duration_from_precision_units`].
145fn fractional_spec(precision: i32) -> &'static str {
146    match precision {
147        3 => "%.3f",
148        6 => "%.6f",
149        9 | 12 => "%.9f",
150        _ => "", // precision 0 (or unsupported, already reported as an error)
151    }
152}
153
154/// Convert a value in precision units since the Unix epoch, to a timestamp string.
155/// Errors if `value` is out of chrono's representable date range.
156fn precision_timestamp_to_string(
157    value: i64,
158    precision: i32,
159) -> Result<String, PrecisionFormatError> {
160    let duration = duration_from_precision_units(value, precision)?;
161    let epoch = DateTime::from_timestamp(0, 0).unwrap().naive_utc();
162    let datetime = epoch
163        .checked_add_signed(duration)
164        .ok_or(PrecisionFormatError::OutOfRange)?;
165
166    let format = format!("%Y-%m-%dT%H:%M:%S{}", fractional_spec(precision));
167    Ok(datetime.format(&format).to_string())
168}
169
170/// Convert a value in precision units since midnight, to a time-of-day string.
171/// Errors if `value` falls outside a single day: `NaiveTime + Duration` wraps
172/// modulo 24 hours, which would otherwise silently misrepresent the value.
173///
174/// The sign check is on `value` itself, not the `chrono::Duration` derived from
175/// it: at precision 12, `duration_from_precision_units` truncates towards zero,
176/// so a small negative `value` (e.g. `-1`) would otherwise round to a
177/// zero/non-negative duration and be wrongly accepted.
178fn precision_time_to_string(value: i64, precision: i32) -> Result<String, PrecisionFormatError> {
179    if value < 0 {
180        return Err(PrecisionFormatError::OutOfRange);
181    }
182    let duration = duration_from_precision_units(value, precision)?;
183    if duration >= chrono::Duration::days(1) {
184        return Err(PrecisionFormatError::OutOfRange);
185    }
186    let midnight = NaiveTime::from_hms_opt(0, 0, 0).unwrap();
187    let time = midnight + duration;
188
189    let format = format!("%H:%M:%S{}", fractional_spec(precision));
190    Ok(time.format(&format).to_string())
191}
192
193/// The precision and sub-second count of an `IntervalDayToSecond`, as stored.
194///
195/// The deprecated `microseconds` precision mode is normalized to precision 6,
196/// since that field is microseconds by definition. An unset `precision_mode`
197/// also means microseconds: that is Substrait's documented meaning for interval
198/// values recorded before `precision_mode` existed, and it matches the default
199/// the type textifier uses for `Type.IntervalDay` with no precision.
200fn interval_day_precision_units(interval: &IntervalDayToSecond) -> (i32, i64) {
201    #[allow(deprecated)]
202    match interval.precision_mode {
203        Some(PrecisionMode::Precision(p)) => (p, interval.subseconds),
204        Some(PrecisionMode::Microseconds(us)) => (6, us as i64),
205        None => (6, interval.subseconds),
206    }
207}
208
209fn write_interval_day_to_second_literal<S: Scope, W: fmt::Write>(
210    interval: &IntervalDayToSecond,
211    ctx: &S,
212    w: &mut W,
213) -> fmt::Result {
214    // Substrait bounds interval_day to [-3,650,000..3,650,000] days, both for
215    // `days` alone and for `days` and `seconds` combined. Out-of-range values
216    // are still writable, so report them and keep going.
217    const MAX_DAYS: i64 = 3_650_000;
218
219    let (units, subseconds) = interval_day_precision_units(interval);
220    let Some(precision) = SupportedPrecision::from_units(units) else {
221        return write!(
222            w,
223            "{}",
224            ctx.failure(PlanError::invalid(
225                "LiteralType",
226                Some("IntervalDayToSecond.precision"),
227                format!(
228                    "IntervalDayToSecond precision {units} has no duration unit to write it in; expected one of 0, 3, 6, 9, or 12"
229                ),
230            ))
231        );
232    };
233
234    let total_seconds = interval.days as i64 * 86_400 + interval.seconds as i64;
235    if !(-MAX_DAYS..=MAX_DAYS).contains(&(interval.days as i64))
236        || !(-MAX_DAYS * 86_400..=MAX_DAYS * 86_400).contains(&total_seconds)
237    {
238        ctx.push_error(
239            PlanError::invalid(
240                "LiteralType",
241                Some("IntervalDayToSecond.days"),
242                format!(
243                    "IntervalDayToSecond days ({}) and seconds ({}) exceed the Substrait maximum of {MAX_DAYS} days",
244                    interval.days, interval.seconds
245                ),
246            )
247            .into(),
248        );
249    }
250
251    let mut parts = Vec::new();
252    if interval.days != 0 {
253        parts.push(format!("{}d", interval.days));
254    }
255    if interval.seconds != 0 {
256        parts.push(format!("{}s", interval.seconds));
257    }
258    if subseconds != 0 {
259        // Precision 0 has no sub-second component at all, so there is no term to
260        // write a non-zero `subseconds` as.
261        let Some(unit) = precision.subsecond_unit() else {
262            return write!(
263                w,
264                "{}",
265                ctx.failure(PlanError::invalid(
266                    "LiteralType",
267                    Some("IntervalDayToSecond.subseconds"),
268                    format!(
269                        "IntervalDayToSecond subseconds ({subseconds}) must be 0 at precision 0; there is no sub-second unit to write it in"
270                    ),
271                ))
272            );
273        };
274
275        // `subseconds` is the fraction of a second below `precision`, so whole
276        // seconds belong in the `seconds` field. Still writable, so report and
277        // keep going.
278        let subsecond_bound = 10_i64.pow(precision.units() as u32);
279        if subseconds <= -subsecond_bound || subseconds >= subsecond_bound {
280            ctx.push_error(
281                PlanError::invalid(
282                    "LiteralType",
283                    Some("IntervalDayToSecond.subseconds"),
284                    format!(
285                        "IntervalDayToSecond subseconds ({subseconds}) is a whole second or more at precision {precision}; whole seconds belong in the seconds field"
286                    ),
287                )
288                .into(),
289            );
290        }
291
292        parts.push(format!("{subseconds}{unit}"));
293    }
294
295    let duration = if parts.is_empty() {
296        "0s".to_string()
297    } else {
298        parts.join(" ")
299    };
300
301    write!(w, "'{}'", escaped(&duration))
302}
303
304/// Write just the value portion of a literal, with no type suffix or
305/// nullability marker.
306///
307/// For unimplemented types, writes an error token via `ctx.failure()`.
308fn write_literal_value<S: Scope, W: fmt::Write>(
309    lit: &LiteralType,
310    ctx: &S,
311    w: &mut W,
312) -> fmt::Result {
313    match lit {
314        LiteralType::Boolean(b) => write!(w, "{b}"),
315        LiteralType::I8(i) | LiteralType::I16(i) | LiteralType::I32(i) => write!(w, "{i}"),
316        LiteralType::I64(i) => write!(w, "{i}"),
317        LiteralType::Fp32(f) => write!(w, "{f}"),
318        LiteralType::Fp64(f) => write!(w, "{f}"),
319        LiteralType::String(s) => write!(w, "'{}'", s.escape_debug()),
320        LiteralType::Binary(items) => textify_binary(items, ctx, w),
321        LiteralType::Date(days) => {
322            write!(w, "'{}'", escaped(&days_to_date_string(*days)))
323        }
324        #[allow(deprecated)]
325        LiteralType::Time(microseconds) => write_precision_literal(
326            "Time",
327            6,
328            precision_time_to_string(*microseconds, 6),
329            ctx,
330            w,
331        ),
332        #[allow(deprecated)]
333        LiteralType::Timestamp(microseconds) => write_precision_literal(
334            "Timestamp",
335            6,
336            precision_timestamp_to_string(*microseconds, 6),
337            ctx,
338            w,
339        ),
340        LiteralType::IntervalYearToMonth(_) => unimplemented_literal("IntervalYearToMonth", ctx, w),
341        LiteralType::IntervalDayToSecond(interval) => {
342            write_interval_day_to_second_literal(interval, ctx, w)
343        }
344        LiteralType::IntervalCompound(_) => unimplemented_literal("IntervalCompound", ctx, w),
345        LiteralType::FixedChar(_) => unimplemented_literal("FixedChar", ctx, w),
346        LiteralType::VarChar(_) => unimplemented_literal("VarChar", ctx, w),
347        LiteralType::FixedBinary(_) => unimplemented_literal("FixedBinary", ctx, w),
348        LiteralType::Decimal(_) => unimplemented_literal("Decimal", ctx, w),
349        LiteralType::PrecisionTime(p) => write_precision_literal(
350            "PrecisionTime",
351            p.precision,
352            precision_time_to_string(p.value, p.precision),
353            ctx,
354            w,
355        ),
356        LiteralType::PrecisionTimestamp(p) => write_precision_literal(
357            "PrecisionTimestamp",
358            p.precision,
359            precision_timestamp_to_string(p.value, p.precision),
360            ctx,
361            w,
362        ),
363        LiteralType::PrecisionTimestampTz(p) => write_precision_literal(
364            "PrecisionTimestampTz",
365            p.precision,
366            precision_timestamp_to_string(p.value, p.precision),
367            ctx,
368            w,
369        ),
370        LiteralType::Struct(_) => unimplemented_literal("Struct", ctx, w),
371        LiteralType::Map(_) => unimplemented_literal("Map", ctx, w),
372        #[allow(deprecated)]
373        LiteralType::TimestampTz(_) => unimplemented_literal("TimestampTz", ctx, w),
374        LiteralType::Uuid(_) => unimplemented_literal("Uuid", ctx, w),
375        LiteralType::Null(_) => write!(w, "null"),
376        LiteralType::List(_) => unimplemented_literal("List", ctx, w),
377        LiteralType::EmptyList(_) => unimplemented_literal("EmptyList", ctx, w),
378        LiteralType::EmptyMap(_) => unimplemented_literal("EmptyMap", ctx, w),
379        LiteralType::UserDefined(_) => unimplemented_literal("UserDefined", ctx, w),
380    }
381}
382
383/// The type suffix for a literal, including its nullability marker (e.g.
384/// `"i32"`, `"fp64?"`, `"date"`, `"interval_day?<6>"`).
385///
386/// Nullability is part of the suffix because a parameterized type writes it
387/// before the parameter list (`interval_day?<6>`, not `interval_day<6>?`).
388/// Returns `None` for unimplemented types whose [`write_literal_value`] already
389/// emitted an error token.
390fn write_literal_type_suffix<W: fmt::Write>(
391    lit: &LiteralType,
392    nullable: bool,
393    w: &mut W,
394) -> fmt::Result {
395    // (type name, precision parameter for parameterized types).
396    let (name, precision): (&'static str, Option<i32>) = match lit {
397        LiteralType::Boolean(_) => ("boolean", None),
398        LiteralType::I8(_) => ("i8", None),
399        LiteralType::I16(_) => ("i16", None),
400        LiteralType::I32(_) => ("i32", None),
401        LiteralType::I64(_) => ("i64", None),
402        LiteralType::Fp32(_) => ("fp32", None),
403        LiteralType::Fp64(_) => ("fp64", None),
404        LiteralType::String(_) => ("string", None),
405        LiteralType::Binary(_) => ("binary", None),
406        LiteralType::Date(_) => ("date", None),
407        #[allow(deprecated)]
408        LiteralType::Time(_) => ("time", None),
409        #[allow(deprecated)]
410        LiteralType::Timestamp(_) => ("timestamp", None),
411        LiteralType::PrecisionTimestamp(p) => ("precisiontimestamp", Some(p.precision)),
412        LiteralType::PrecisionTimestampTz(p) => ("precisiontimestamptz", Some(p.precision)),
413        LiteralType::PrecisionTime(p) => ("precisiontime", Some(p.precision)),
414        // interval_day carries the value's sub-second precision, so the duration
415        // string doesn't have to encode it.
416        LiteralType::IntervalDayToSecond(interval) => (
417            "interval_day",
418            Some(interval_day_precision_units(interval).0),
419        ),
420        _ => return Ok(()),
421    };
422
423    write!(w, ":{name}")?;
424    if nullable {
425        write!(w, "?")?;
426    }
427    if let Some(p) = precision {
428        write!(w, "<{p}>")?;
429    }
430    Ok(())
431}
432
433/// Whether this type is the default interpretation for its value syntax.
434///
435/// Each literal value syntax has a default type that the parser assumes when
436/// no explicit type suffix is present:
437/// - `true`/`false` → `boolean`
438/// - bare integers (`42`) → `i64`
439/// - bare floats (`3.19`) → `fp64`
440/// - single-quoted strings (`'hello'`) → `string`
441/// - hex literals (`0x...`) → `binary`
442///
443/// Non-default types (e.g., `i32`, `fp32`, `date`) always need an explicit
444/// suffix to distinguish them from the default.
445fn is_default_for_syntax(lit: &LiteralType) -> bool {
446    matches!(
447        lit,
448        LiteralType::Boolean(_)
449            | LiteralType::String(_)
450            | LiteralType::Binary(_)
451            | LiteralType::I64(_)
452            | LiteralType::Fp64(_)
453    )
454}
455
456impl Textify for expr::Literal {
457    fn name() -> &'static str {
458        "Literal"
459    }
460
461    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
462        let Some(lit) = self.literal_type.as_ref() else {
463            return write!(
464                w,
465                "{}",
466                ctx.failure(PlanError::invalid(
467                    "Literal",
468                    Some("literal_type"),
469                    "missing literal_type",
470                ))
471            );
472        };
473        write_literal_value(lit, ctx, w)?;
474        let show_suffix = match ctx.options().literal_types {
475            Visibility::Never => false,
476            Visibility::Always => true,
477            Visibility::Required => self.nullable || !is_default_for_syntax(lit),
478        };
479        if let LiteralType::Null(typ) = lit {
480            write!(w, ":{}", ctx.expect(Some(typ)))?;
481            return Ok(());
482        }
483        if show_suffix {
484            write_literal_type_suffix(lit, self.nullable, w)?;
485        }
486        Ok(())
487    }
488}
489
490pub struct Reference(pub i32);
491
492impl fmt::Display for Reference {
493    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494        write!(f, "${}", self.0)
495    }
496}
497
498impl From<Reference> for Expression {
499    fn from(r: Reference) -> Self {
500        // XXX: Why is it so many layers to make a struct field reference? This is
501        // surprisingly complex
502        Expression {
503            rex_type: Some(RexType::Selection(Box::new(FieldReference {
504                reference_type: Some(ReferenceType::DirectReference(ReferenceSegment {
505                    reference_type: Some(reference_segment::ReferenceType::StructField(Box::new(
506                        reference_segment::StructField {
507                            field: r.0,
508                            child: None,
509                        },
510                    ))),
511                })),
512                root_type: Some(RootType::RootReference(RootReference {})),
513            }))),
514        }
515    }
516}
517
518impl Textify for Reference {
519    fn name() -> &'static str {
520        "Reference"
521    }
522
523    fn textify<S: Scope, W: fmt::Write>(&self, _ctx: &S, w: &mut W) -> fmt::Result {
524        write!(w, "{self}")
525    }
526}
527
528impl Textify for FieldReference {
529    fn name() -> &'static str {
530        "FieldReference"
531    }
532
533    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
534        match &self.root_type {
535            Some(RootType::RootReference(_)) => {}
536            None => {
537                return write!(
538                    w,
539                    "{}",
540                    ctx.failure(PlanError::invalid(
541                        "FieldReference",
542                        Some("root_type"),
543                        "Required field root_type is missing",
544                    ))
545                );
546            }
547            Some(RootType::Expression(_)) => {
548                return write!(
549                    w,
550                    "{}",
551                    ctx.failure(PlanError::unimplemented(
552                        "FieldReference",
553                        Some("root_type"),
554                        "FieldReference textification not implemented for Expression root_type",
555                    ))
556                );
557            }
558            Some(RootType::OuterReference(_)) => {
559                return write!(
560                    w,
561                    "{}",
562                    ctx.failure(PlanError::unimplemented(
563                        "FieldReference",
564                        Some("root_type"),
565                        "FieldReference textification not implemented for OuterReference root_type",
566                    ))
567                );
568            }
569            Some(RootType::LambdaParameterReference(_)) => {
570                return write!(
571                    w,
572                    "{}",
573                    ctx.failure(PlanError::unimplemented(
574                        "FieldReference",
575                        Some("root_type"),
576                        "FieldReference textification not implemented for LambdaParameterReference root_type",
577                    ))
578                );
579            }
580        }
581
582        let ref_type = match &self.reference_type {
583            None => {
584                return write!(
585                    w,
586                    "{}",
587                    ctx.failure(PlanError::invalid(
588                        "FieldReference",
589                        Some("reference_type"),
590                        "Required field reference_type is missing",
591                    ))
592                );
593            }
594            Some(ReferenceType::DirectReference(r)) => r,
595            _ => {
596                return write!(
597                    w,
598                    "{}",
599                    ctx.failure(PlanError::unimplemented(
600                        "FieldReference",
601                        Some("FieldReference"),
602                        "FieldReference textification implemented only for StructField",
603                    ))
604                );
605            }
606        };
607
608        match &ref_type.reference_type {
609            Some(reference_segment::ReferenceType::StructField(s)) => {
610                write!(w, "{}", Reference(s.field))
611            }
612            None => write!(
613                w,
614                "{}",
615                ctx.failure(PlanError::invalid(
616                    "ReferenceSegment",
617                    Some("reference_type"),
618                    "Required field reference_type is missing",
619                ))
620            ),
621            _ => write!(
622                w,
623                "{}",
624                ctx.failure(PlanError::unimplemented(
625                    "ReferenceSegment",
626                    Some("reference_type"),
627                    "ReferenceSegment textification implemented only for StructField",
628                ))
629            ),
630        }
631    }
632}
633
634/// The fields shared by every Substrait function call - `ScalarFunction`,
635/// `AggregateFunction`, and `WindowFunction` - that render as
636/// `name#anchor(args, options)`.
637///
638/// The remaining fields of each function message (the output type, and the
639/// aggregate/window-specific parts) are textified alongside this by the
640/// respective `Textify` implementations.
641#[derive(Debug, Clone, Copy)]
642pub struct FunctionInvocation<'a> {
643    pub function_reference: u32,
644    pub arguments: &'a [FunctionArgument],
645    pub options: &'a [FunctionOption],
646}
647
648impl<'a> From<&'a ScalarFunction> for FunctionInvocation<'a> {
649    fn from(f: &'a ScalarFunction) -> Self {
650        FunctionInvocation {
651            function_reference: f.function_reference,
652            arguments: &f.arguments,
653            options: &f.options,
654        }
655    }
656}
657
658impl<'a> From<&'a AggregateFunction> for FunctionInvocation<'a> {
659    fn from(f: &'a AggregateFunction) -> Self {
660        FunctionInvocation {
661            function_reference: f.function_reference,
662            arguments: &f.arguments,
663            options: &f.options,
664        }
665    }
666}
667
668impl Textify for FunctionInvocation<'_> {
669    fn name() -> &'static str {
670        "FunctionInvocation"
671    }
672
673    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
674        let name_and_anchor =
675            NamedAnchor::lookup(ctx, ExtensionKind::Function, self.function_reference);
676        let name_and_anchor = ctx.display(&name_and_anchor);
677
678        let args = ctx.separated(self.arguments, ", ");
679        let options = ctx.separated(self.options, ", ");
680        let between = if self.arguments.is_empty() || self.options.is_empty() {
681            ""
682        } else {
683            ", "
684        };
685
686        write!(w, "{name_and_anchor}({args}{between}{options})")
687    }
688}
689
690impl Textify for ScalarFunction {
691    fn name() -> &'static str {
692        "ScalarFunction"
693    }
694
695    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
696        let invocation = FunctionInvocation::from(self);
697        let output_type = OutputType(self.output_type.as_ref());
698
699        let invocation = ctx.display(&invocation);
700        let output_type = ctx.display(&output_type);
701
702        write!(w, "{invocation}{output_type}")
703    }
704}
705
706impl Textify for FunctionOption {
707    fn name() -> &'static str {
708        "FunctionOption"
709    }
710
711    fn textify<S: Scope, W: fmt::Write>(&self, _ctx: &S, w: &mut W) -> fmt::Result {
712        write!(w, "{}⇒[", self.name)?;
713        let mut first = true;
714        for pref in self.preference.iter() {
715            if !first {
716                write!(w, ", ")?;
717            } else {
718                first = false;
719            }
720            write!(w, "{pref}")?;
721        }
722        write!(w, "]")?;
723        Ok(())
724    }
725}
726
727impl Textify for FunctionArgument {
728    fn name() -> &'static str {
729        "FunctionArgument"
730    }
731
732    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
733        write!(w, "{}", ctx.expect(self.arg_type.as_ref()))
734    }
735}
736
737impl Textify for ArgType {
738    fn name() -> &'static str {
739        "ArgType"
740    }
741
742    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
743        match self {
744            ArgType::Type(t) => t.textify(ctx, w),
745            ArgType::Value(v) => v.textify(ctx, w),
746            ArgType::Enum(e) => textify_enum(e, ctx, w),
747        }
748    }
749}
750
751impl Textify for Cast {
752    fn name() -> &'static str {
753        "Cast"
754    }
755
756    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
757        let failure_err;
758        let fb: &dyn fmt::Display = match cast::FailureBehavior::try_from(self.failure_behavior) {
759            Ok(cast::FailureBehavior::Unspecified) => &"",
760            Ok(cast::FailureBehavior::ReturnNull) => &"?",
761            Ok(cast::FailureBehavior::ThrowException) => &"!",
762            Err(_) => {
763                failure_err = ctx.failure(PlanError::invalid(
764                    "Cast",
765                    Some("failure_behavior"),
766                    format!("Unknown failure_behavior value: {}", self.failure_behavior),
767                ));
768                &failure_err
769            }
770        };
771        let input = ctx.expect(self.input.as_deref());
772        let target_type = ctx.expect(self.r#type.as_ref());
773        write!(w, "({input})::{fb}{target_type}")
774    }
775}
776
777impl Textify for IfThen {
778    fn name() -> &'static str {
779        "IfThen"
780    }
781
782    // This method writes ifThen using the following convention of a comma separated sequence of 'if_clause -> then_clause, '
783    // followed by the final else clause denoted with '_'
784    // ex: true -> if_then(true || false -> true, _ -> false)
785    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
786        write!(w, "if_then(")?;
787        for clause in &self.ifs {
788            let if_expr = ctx.expect(clause.r#if.as_ref());
789            let then_expr = ctx.expect(clause.then.as_ref());
790            write!(w, "{if_expr} -> {then_expr}, ")?;
791        }
792        let else_expr = ctx.expect(self.r#else.as_deref());
793        write!(w, "_ -> {else_expr})")
794    }
795}
796
797impl Textify for RexType {
798    fn name() -> &'static str {
799        "RexType"
800    }
801
802    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
803        match self {
804            RexType::Literal(literal) => literal.textify(ctx, w),
805            RexType::Selection(f) => f.textify(ctx, w),
806            RexType::ScalarFunction(s) => s.textify(ctx, w),
807            RexType::WindowFunction(_f) => write!(
808                w,
809                "{}",
810                ctx.failure(PlanError::unimplemented(
811                    "RexType",
812                    Some("WindowFunction"),
813                    "WindowFunction textification not implemented",
814                ))
815            ),
816            RexType::IfThen(i) => i.textify(ctx, w),
817            RexType::SwitchExpression(_s) => write!(
818                w,
819                "{}",
820                ctx.failure(PlanError::unimplemented(
821                    "RexType",
822                    Some("SwitchExpression"),
823                    "SwitchExpression textification not implemented",
824                ))
825            ),
826            RexType::SingularOrList(_s) => write!(
827                w,
828                "{}",
829                ctx.failure(PlanError::unimplemented(
830                    "RexType",
831                    Some("SingularOrList"),
832                    "SingularOrList textification not implemented",
833                ))
834            ),
835            RexType::MultiOrList(_m) => write!(
836                w,
837                "{}",
838                ctx.failure(PlanError::unimplemented(
839                    "RexType",
840                    Some("MultiOrList"),
841                    "MultiOrList textification not implemented",
842                ))
843            ),
844            RexType::Cast(c) => c.textify(ctx, w),
845            RexType::Subquery(_s) => write!(
846                w,
847                "{}",
848                ctx.failure(PlanError::unimplemented(
849                    "RexType",
850                    Some("Subquery"),
851                    "Subquery textification not implemented",
852                ))
853            ),
854            RexType::Nested(_n) => write!(
855                w,
856                "{}",
857                ctx.failure(PlanError::unimplemented(
858                    "RexType",
859                    Some("Nested"),
860                    "Nested textification not implemented",
861                ))
862            ),
863            RexType::DynamicParameter(_d) => write!(
864                w,
865                "{}",
866                ctx.failure(PlanError::unimplemented(
867                    "RexType",
868                    Some("DynamicParameter"),
869                    "DynamicParameter textification not implemented",
870                ))
871            ),
872            #[allow(deprecated)]
873            RexType::Enum(_) => write!(
874                w,
875                "{}",
876                ctx.failure(PlanError::unimplemented(
877                    "RexType",
878                    Some("Enum"),
879                    "Enum textification not implemented",
880                ))
881            ),
882            RexType::Lambda(_) => write!(
883                w,
884                "{}",
885                ctx.failure(PlanError::unimplemented(
886                    "RexType",
887                    Some("Lambda"),
888                    "Lambda textification not implemented",
889                ))
890            ),
891            RexType::LambdaInvocation(_) => write!(
892                w,
893                "{}",
894                ctx.failure(PlanError::unimplemented(
895                    "RexType",
896                    Some("LambdaInvocation"),
897                    "LambdaInvocation textification not implemented",
898                ))
899            ),
900        }
901    }
902}
903
904impl Textify for Expression {
905    fn name() -> &'static str {
906        "Expression"
907    }
908
909    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
910        write!(w, "{}", ctx.expect(self.rex_type.as_ref()))
911    }
912}
913
914impl Textify for AggregateFunction {
915    fn name() -> &'static str {
916        "AggregateFunction"
917    }
918
919    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
920        let invocation = FunctionInvocation::from(self);
921        let output_type = OutputType(self.output_type.as_ref());
922
923        let invocation = ctx.display(&invocation);
924        let output_type = ctx.display(&output_type);
925
926        write!(w, "{invocation}{output_type}")
927    }
928}
929
930#[cfg(test)]
931mod tests {
932    use substrait::proto::Type;
933    use substrait::proto::expression::{cast, field_reference, if_then};
934    use substrait::proto::r#type::{Boolean, I16, I32, I64, Kind, Nullability, UserDefined};
935
936    use super::*;
937    use crate::extensions::simple::{ExtensionKind, MissingReference};
938    use crate::fixtures::TestContext;
939    use crate::textify::foundation::{FormatError, FormatErrorType};
940
941    fn literal_bool(value: bool) -> Expression {
942        Expression {
943            rex_type: Some(RexType::Literal(expr::Literal {
944                nullable: false,
945                type_variation_reference: 0,
946                literal_type: Some(expr::literal::LiteralType::Boolean(value)),
947            })),
948        }
949    }
950
951    fn non_nullable_literal(lit: expr::literal::LiteralType) -> expr::Literal {
952        expr::Literal {
953            nullable: false,
954            type_variation_reference: 0,
955            literal_type: Some(lit),
956        }
957    }
958
959    #[test]
960    fn test_literal_textify() {
961        let ctx = TestContext::new();
962
963        let literal = non_nullable_literal(LiteralType::Boolean(true));
964        assert_eq!(ctx.textify_no_errors(&literal), "true");
965    }
966
967    fn interval_day_literal(
968        days: i32,
969        seconds: i32,
970        subseconds: i64,
971        precision: i32,
972    ) -> expr::Literal {
973        non_nullable_literal(LiteralType::IntervalDayToSecond(
974            expr::literal::IntervalDayToSecond {
975                days,
976                seconds,
977                subseconds,
978                precision_mode: Some(PrecisionMode::Precision(precision)),
979            },
980        ))
981    }
982
983    #[test]
984    fn test_interval_day_literal_textify() {
985        let ctx = TestContext::new();
986
987        for (literal, expected) in [
988            // Only non-zero components are written: precision travels in the
989            // type suffix, so it survives without a zero sub-second term.
990            (interval_day_literal(5, 0, 0, 6), "'5d':interval_day<6>"),
991            (interval_day_literal(5, 0, 0, 0), "'5d':interval_day<0>"),
992            (interval_day_literal(4, 5, 0, 9), "'4d 5s':interval_day<9>"),
993            (
994                interval_day_literal(0, 0, 123, 3),
995                "'123ms':interval_day<3>",
996            ),
997            (
998                interval_day_literal(0, 0, 123_456, 6),
999                "'123456us':interval_day<6>",
1000            ),
1001            (
1002                interval_day_literal(5, 3, 123_456_789, 9),
1003                "'5d 3s 123456789ns':interval_day<9>",
1004            ),
1005            (
1006                interval_day_literal(0, 0, -500, 12),
1007                "'-500ps':interval_day<12>",
1008            ),
1009            // An all-zero interval still needs a term to be a valid duration.
1010            (interval_day_literal(0, 0, 0, 0), "'0s':interval_day<0>"),
1011            (interval_day_literal(0, 0, 0, 9), "'0s':interval_day<9>"),
1012        ] {
1013            assert_eq!(ctx.textify_no_errors(&literal), expected);
1014        }
1015    }
1016
1017    #[test]
1018    fn test_interval_day_literal_textify_nullable() {
1019        let ctx = TestContext::new();
1020        let literal = nullable_literal(LiteralType::IntervalDayToSecond(
1021            expr::literal::IntervalDayToSecond {
1022                days: 5,
1023                seconds: 0,
1024                subseconds: 0,
1025                precision_mode: Some(PrecisionMode::Precision(6)),
1026            },
1027        ));
1028        // Nullability goes before the parameter list, as it does for types.
1029        assert_eq!(ctx.textify_no_errors(&literal), "'5d':interval_day?<6>");
1030    }
1031
1032    #[test]
1033    fn test_interval_day_literal_textify_precision_modes() {
1034        let ctx = TestContext::new();
1035
1036        // The deprecated `microseconds` mode is microseconds by definition, and an
1037        // unset `precision_mode` means microseconds for compatibility - both are
1038        // written as precision 6 so protobuf -> text -> protobuf keeps precision.
1039        #[allow(deprecated)]
1040        let deprecated_microseconds = non_nullable_literal(LiteralType::IntervalDayToSecond(
1041            expr::literal::IntervalDayToSecond {
1042                days: 5,
1043                seconds: 0,
1044                subseconds: 0,
1045                precision_mode: Some(PrecisionMode::Microseconds(123)),
1046            },
1047        ));
1048        assert_eq!(
1049            ctx.textify_no_errors(&deprecated_microseconds),
1050            "'5d 123us':interval_day<6>"
1051        );
1052
1053        let unset = non_nullable_literal(LiteralType::IntervalDayToSecond(
1054            expr::literal::IntervalDayToSecond {
1055                days: 5,
1056                seconds: 0,
1057                subseconds: 123,
1058                precision_mode: None,
1059            },
1060        ));
1061        assert_eq!(ctx.textify_no_errors(&unset), "'5d 123us':interval_day<6>");
1062    }
1063
1064    #[test]
1065    fn test_interval_day_literal_textify_invalid_precision_is_error() {
1066        let ctx = TestContext::new();
1067        // A precision with no duration unit has no way to write a sub-second
1068        // value. Only the value becomes an error token; the suffix still reports
1069        // the precision that was actually stored.
1070        for precision in [4, 13, -1] {
1071            let literal = interval_day_literal(0, 0, 2, precision);
1072
1073            let (text, errors) = ctx.textify(&literal);
1074
1075            assert_eq!(text, format!("!{{LiteralType}}:interval_day<{precision}>"));
1076            match errors.first() {
1077                FormatError::Format(e) => {
1078                    assert_eq!(e.error_type, FormatErrorType::InvalidValue);
1079                    assert_eq!(e.lookup.as_deref(), Some("IntervalDayToSecond.precision"));
1080                }
1081                other => panic!("expected FormatError::Format, got: {other:?}"),
1082            }
1083        }
1084    }
1085
1086    #[test]
1087    fn test_interval_day_literal_textify_subseconds_at_precision_zero_is_error() {
1088        let ctx = TestContext::new();
1089        // Precision 0 has no sub-second component, so a non-zero `subseconds`
1090        // is invalid Substrait with no term to write it in.
1091        let literal = interval_day_literal(0, 0, 2, 0);
1092
1093        let (text, errors) = ctx.textify(&literal);
1094
1095        assert_eq!(text, "!{LiteralType}:interval_day<0>");
1096        match errors.first() {
1097            FormatError::Format(e) => {
1098                assert_eq!(e.error_type, FormatErrorType::InvalidValue);
1099                assert_eq!(e.lookup.as_deref(), Some("IntervalDayToSecond.subseconds"));
1100            }
1101            other => panic!("expected FormatError::Format, got: {other:?}"),
1102        }
1103    }
1104
1105    /// Values outside the Substrait ranges still have an unambiguous text form,
1106    /// so they are written and reported rather than replaced with an error token.
1107    #[test]
1108    fn test_interval_day_literal_textify_out_of_range_is_written_with_warning() {
1109        let ctx = TestContext::new();
1110
1111        for (literal, expected, field) in [
1112            // `days` alone exceeds the 3,650,000-day maximum.
1113            (
1114                interval_day_literal(3_650_001, 0, 0, 0),
1115                "'3650001d':interval_day<0>",
1116                "IntervalDayToSecond.days",
1117            ),
1118            // `days` and `seconds` are each in range, but combined they aren't.
1119            (
1120                interval_day_literal(3_649_999, 100_000, 0, 0),
1121                "'3649999d 100000s':interval_day<0>",
1122                "IntervalDayToSecond.days",
1123            ),
1124            // `subseconds` is a whole second or more at this precision.
1125            (
1126                interval_day_literal(0, 0, 1_000_000_000, 9),
1127                "'1000000000ns':interval_day<9>",
1128                "IntervalDayToSecond.subseconds",
1129            ),
1130        ] {
1131            let (text, errors) = ctx.textify(&literal);
1132
1133            assert_eq!(text, expected);
1134            match errors.first() {
1135                FormatError::Format(e) => {
1136                    assert_eq!(e.error_type, FormatErrorType::InvalidValue);
1137                    assert_eq!(e.lookup.as_deref(), Some(field));
1138                }
1139                other => panic!("expected FormatError::Format, got: {other:?}"),
1140            }
1141        }
1142    }
1143
1144    fn nullable_literal(lit: expr::literal::LiteralType) -> expr::Literal {
1145        expr::Literal {
1146            nullable: true,
1147            type_variation_reference: 0,
1148            literal_type: Some(lit),
1149        }
1150    }
1151
1152    #[test]
1153    fn test_nullable_boolean_literal_textify() {
1154        let ctx = TestContext::new();
1155        assert_eq!(
1156            ctx.textify_no_errors(&nullable_literal(expr::literal::LiteralType::Boolean(true))),
1157            "true:boolean?"
1158        );
1159        assert_eq!(
1160            ctx.textify_no_errors(&nullable_literal(expr::literal::LiteralType::Boolean(
1161                false
1162            ))),
1163            "false:boolean?"
1164        );
1165    }
1166
1167    #[test]
1168    fn test_nullable_integer_literal_textify() {
1169        let ctx = TestContext::new();
1170        assert_eq!(
1171            ctx.textify_no_errors(&nullable_literal(expr::literal::LiteralType::I32(78))),
1172            "78:i32?"
1173        );
1174        assert_eq!(
1175            ctx.textify_no_errors(&nullable_literal(expr::literal::LiteralType::I64(42))),
1176            "42:i64?"
1177        );
1178    }
1179
1180    #[test]
1181    fn test_nullable_float_literal_textify() {
1182        let ctx = TestContext::new();
1183        assert_eq!(
1184            ctx.textify_no_errors(&nullable_literal(expr::literal::LiteralType::Fp32(2.5))),
1185            "2.5:fp32?"
1186        );
1187        assert_eq!(
1188            ctx.textify_no_errors(&nullable_literal(expr::literal::LiteralType::Fp64(3.19))),
1189            "3.19:fp64?"
1190        );
1191    }
1192
1193    #[test]
1194    fn test_precision_timestamp_to_string() {
1195        assert_eq!(
1196            precision_timestamp_to_string(10, 0),
1197            Ok("1970-01-01T00:00:10".to_string())
1198        );
1199        assert_eq!(
1200            precision_timestamp_to_string(123_456_789, 9),
1201            Ok("1970-01-01T00:00:00.123456789".to_string())
1202        );
1203        // Precision 12 (picoseconds) is truncated to nanoseconds (best-effort):
1204        // the trailing 500 picoseconds below don't survive.
1205        assert_eq!(
1206            precision_timestamp_to_string(123_456_789_500, 12),
1207            Ok("1970-01-01T00:00:00.123456789".to_string())
1208        );
1209        assert_eq!(
1210            precision_timestamp_to_string(0, 13),
1211            Err(PrecisionFormatError::UnsupportedPrecision)
1212        );
1213    }
1214
1215    #[test]
1216    fn test_precision_time_to_string() {
1217        assert_eq!(precision_time_to_string(0, 0), Ok("00:00:00".to_string()));
1218        assert_eq!(
1219            // 01:01:01 = 3661 seconds, in microseconds. The fractional part is
1220            // rendered at the declared precision width (6 digits).
1221            precision_time_to_string(3_661_000_000, 6),
1222            Ok("01:01:01.000000".to_string())
1223        );
1224        assert_eq!(
1225            // 01:01:01 = 3661 seconds, in picoseconds, plus 500 ps that get
1226            // truncated away. Precision 12 renders at nanosecond width (9 digits).
1227            precision_time_to_string(3_661_000_000_000_500, 12),
1228            Ok("01:01:01.000000000".to_string())
1229        );
1230        assert_eq!(
1231            precision_time_to_string(0, 13),
1232            Err(PrecisionFormatError::UnsupportedPrecision)
1233        );
1234        // A negative value at precision 12 truncates towards zero when converted
1235        // to a `chrono::Duration` (-1 / 1000 == 0), so the sign must be checked
1236        // on the raw value, not the derived duration.
1237        assert_eq!(
1238            precision_time_to_string(-1, 12),
1239            Err(PrecisionFormatError::OutOfRange)
1240        );
1241    }
1242
1243    #[test]
1244    fn test_nullable_precision_timestamp_literal_textify() {
1245        let ctx = TestContext::new();
1246        assert_eq!(
1247            ctx.textify_no_errors(&nullable_literal(
1248                expr::literal::LiteralType::PrecisionTimestamp(expr::literal::PrecisionTimestamp {
1249                    precision: 6,
1250                    value: 1000,
1251                })
1252            )),
1253            "'1970-01-01T00:00:00.001000':precisiontimestamp?<6>"
1254        );
1255        assert_eq!(
1256            ctx.textify_no_errors(&nullable_literal(
1257                expr::literal::LiteralType::PrecisionTimestampTz(
1258                    expr::literal::PrecisionTimestamp {
1259                        precision: 3,
1260                        value: 5,
1261                    }
1262                )
1263            )),
1264            "'1970-01-01T00:00:00.005':precisiontimestamptz?<3>"
1265        );
1266        assert_eq!(
1267            ctx.textify_no_errors(&nullable_literal(
1268                expr::literal::LiteralType::PrecisionTime(expr::literal::PrecisionTime {
1269                    precision: 0,
1270                    value: 61,
1271                })
1272            )),
1273            "'00:01:01':precisiontime?<0>"
1274        );
1275    }
1276
1277    #[test]
1278    fn test_precision_time_literal_precision_12_best_effort() {
1279        let ctx = TestContext::new();
1280        let (s, errs) = ctx.textify(&non_nullable_literal(
1281            expr::literal::LiteralType::PrecisionTime(expr::literal::PrecisionTime {
1282                precision: 12,
1283                value: 3_661_000_000_000_500,
1284            }),
1285        ));
1286        // Value truncated to nanosecond resolution (9 fractional digits), but
1287        // the declared type `<12>` is preserved.
1288        assert_eq!(s, "'01:01:01.000000000':precisiontime<12>");
1289        assert_eq!(errs.0.len(), 1);
1290        assert!(errs.0[0].to_string().contains("truncated"));
1291    }
1292
1293    #[test]
1294    fn test_precision_timestamp_literal_supported_precision_no_warning() {
1295        let ctx = TestContext::new();
1296        // Precisions chrono can represent exactly shouldn't trigger the
1297        // precision-12 truncation warning.
1298        let (_, errs) = ctx.textify(&non_nullable_literal(
1299            expr::literal::LiteralType::PrecisionTimestamp(expr::literal::PrecisionTimestamp {
1300                precision: 9,
1301                value: 123_456_789,
1302            }),
1303        ));
1304        assert_eq!(errs.0.len(), 0);
1305    }
1306
1307    #[test]
1308    fn test_precision_timestamp_literal_unrecognized_precision_invalid() {
1309        let ctx = TestContext::new();
1310        let (s, errs) = ctx.textify(&non_nullable_literal(
1311            expr::literal::LiteralType::PrecisionTimestamp(expr::literal::PrecisionTimestamp {
1312                precision: 13,
1313                value: 0,
1314            }),
1315        ));
1316        // The value fails to render (unrecognized precision), but the suffix
1317        // is still written with the precision as-is.
1318        assert_eq!(s, "!{LiteralType}:precisiontimestamp<13>");
1319        assert_eq!(errs.0.len(), 1);
1320        assert!(errs.0[0].to_string().contains("PrecisionTimestamp"));
1321    }
1322
1323    #[test]
1324    fn test_precision_timestamp_literal_out_of_range_does_not_panic() {
1325        let ctx = TestContext::new();
1326        // 9e12 seconds since epoch is a valid i64 and a valid `chrono::Duration`,
1327        // but it's outside NaiveDateTime's representable range: adding it to the
1328        // epoch with `+` would panic. Textification should report a failure
1329        // token instead.
1330        let (s, errs) = ctx.textify(&non_nullable_literal(
1331            expr::literal::LiteralType::PrecisionTimestamp(expr::literal::PrecisionTimestamp {
1332                precision: 0,
1333                value: 9_000_000_000_000,
1334            }),
1335        ));
1336        assert_eq!(s, "!{LiteralType}:precisiontimestamp<0>");
1337        assert_eq!(errs.0.len(), 1);
1338    }
1339
1340    #[test]
1341    fn test_precision_time_literal_beyond_one_day_invalid() {
1342        let ctx = TestContext::new();
1343        // 86,460 seconds since midnight is more than a day; `NaiveTime + Duration`
1344        // wraps modulo 24 hours rather than erroring, which would otherwise
1345        // silently misrepresent the value as 00:01:00.
1346        let (s, errs) = ctx.textify(&non_nullable_literal(
1347            expr::literal::LiteralType::PrecisionTime(expr::literal::PrecisionTime {
1348                precision: 0,
1349                value: 86_460,
1350            }),
1351        ));
1352        assert_eq!(s, "!{LiteralType}:precisiontime<0>");
1353        assert_eq!(errs.0.len(), 1);
1354    }
1355
1356    #[test]
1357    fn test_precision_time_literal_unrecognized_precision_invalid() {
1358        let ctx = TestContext::new();
1359        let (s, errs) = ctx.textify(&non_nullable_literal(
1360            expr::literal::LiteralType::PrecisionTime(expr::literal::PrecisionTime {
1361                precision: 13,
1362                value: 0,
1363            }),
1364        ));
1365        assert_eq!(s, "!{LiteralType}:precisiontime<13>");
1366        assert_eq!(errs.0.len(), 1);
1367        assert!(errs.0[0].to_string().contains("PrecisionTime"));
1368    }
1369
1370    #[test]
1371    fn test_nullable_precision_timestamp_literal_precision_12_best_effort() {
1372        let ctx = TestContext::new();
1373        // Picoseconds aren't representable, the textify best-effort approach is to
1374        // truncate to nanoseconds. The lost precision is reported via the error accumulator.
1375        let (s, errs) = ctx.textify(&nullable_literal(
1376            expr::literal::LiteralType::PrecisionTimestamp(expr::literal::PrecisionTimestamp {
1377                precision: 12,
1378                value: 123_456_789_500,
1379            }),
1380        ));
1381        // Value truncated to nanosecond resolution, but the declared type
1382        // `<12>` is preserved rather than rewritten to `<9>`.
1383        assert_eq!(s, "'1970-01-01T00:00:00.123456789':precisiontimestamp?<12>");
1384        assert_eq!(errs.0.len(), 1);
1385        assert!(errs.0[0].to_string().contains("truncated"));
1386    }
1387
1388    #[test]
1389    fn test_expression_textify() {
1390        let ctx = TestContext::new();
1391
1392        // Test empty expression
1393        let expr_empty = Expression { rex_type: None }; // Renamed to avoid conflict
1394        let (s, errs) = ctx.textify(&expr_empty);
1395        assert!(!errs.is_empty());
1396        assert_eq!(s, "!{RexType}");
1397
1398        // Test literal expression
1399        let expr_lit = Expression {
1400            rex_type: Some(RexType::Literal(expr::Literal {
1401                nullable: false,
1402                type_variation_reference: 0,
1403                literal_type: Some(expr::literal::LiteralType::Boolean(true)),
1404            })),
1405        };
1406        assert_eq!(ctx.textify_no_errors(&expr_lit), "true");
1407    }
1408
1409    #[test]
1410    fn test_rextype_textify() {
1411        let ctx = TestContext::new();
1412
1413        let func = RexType::ScalarFunction(ScalarFunction {
1414            function_reference: 1000, // Does not exist
1415            arguments: vec![],
1416            options: vec![],
1417            output_type: Some(Type {
1418                kind: Some(Kind::I64(I64 {
1419                    nullability: Nullability::Required as i32,
1420                    type_variation_reference: 0,
1421                })),
1422            }),
1423            #[allow(deprecated)]
1424            args: vec![],
1425        });
1426        let (s, errq) = ctx.textify(&func);
1427        let errs: Vec<_> = errq.0;
1428        match errs[0] {
1429            FormatError::Lookup(MissingReference::MissingAnchor(k, a)) => {
1430                assert_eq!(k, ExtensionKind::Function);
1431                assert_eq!(a, 1000);
1432            }
1433            _ => panic!("Expected Lookup MissingAnchor: {}", errs[0]),
1434        }
1435        assert_eq!(s, "!{function}#1000():i64");
1436
1437        let ctx = ctx.with_urn(1, "first").with_function(1, 100, "first");
1438        let func = RexType::ScalarFunction(ScalarFunction {
1439            function_reference: 100,
1440            arguments: vec![],
1441            options: vec![],
1442            output_type: Some(Type {
1443                kind: Some(Kind::I64(I64 {
1444                    nullability: Nullability::Required as i32,
1445                    type_variation_reference: 0,
1446                })),
1447            }),
1448            #[allow(deprecated)]
1449            args: vec![],
1450        });
1451        let s = ctx.textify_no_errors(&func);
1452        assert_eq!(s, "first():i64");
1453
1454        // Test for duplicated function name requiring anchor
1455        let options_show_anchor = Default::default();
1456
1457        let ctx = TestContext::new()
1458            .with_options(options_show_anchor)
1459            .with_urn(1, "somewhere_on_the_internet")
1460            .with_urn(2, "somewhere_else")
1461            .with_function(1, 231, "duplicated")
1462            .with_function(2, 232, "duplicated");
1463
1464        let rex_dup = RexType::ScalarFunction(ScalarFunction {
1465            function_reference: 231,
1466            arguments: vec![FunctionArgument {
1467                arg_type: Some(ArgType::Value(Expression {
1468                    rex_type: Some(RexType::Literal(expr::Literal {
1469                        nullable: false,
1470                        type_variation_reference: 0,
1471                        literal_type: Some(expr::literal::LiteralType::Boolean(true)),
1472                    })),
1473                })),
1474            }],
1475            options: vec![],
1476            output_type: Some(Type {
1477                kind: Some(Kind::Bool(Boolean {
1478                    nullability: Nullability::Required as i32,
1479                    type_variation_reference: 0,
1480                })),
1481            }),
1482            #[allow(deprecated)]
1483            args: vec![],
1484        });
1485        let s = ctx.textify_no_errors(&rex_dup);
1486        assert_eq!(s, "duplicated#231(true):boolean");
1487    }
1488
1489    #[test]
1490    fn test_ifthen_textify() {
1491        let ctx = TestContext::new();
1492
1493        let if_then = IfThen {
1494            ifs: vec![
1495                if_then::IfClause {
1496                    r#if: Some(literal_bool(true)),
1497                    then: Some(literal_bool(false)),
1498                },
1499                if_then::IfClause {
1500                    r#if: Some(literal_bool(false)),
1501                    then: Some(literal_bool(true)),
1502                },
1503            ],
1504            r#else: Some(Box::new(literal_bool(true))),
1505        };
1506
1507        let s = ctx.textify_no_errors(&if_then);
1508        assert_eq!(s, "if_then(true -> false, false -> true, _ -> true)");
1509    }
1510
1511    #[test]
1512    fn test_ifthen_textify_missing_else() {
1513        let ctx = TestContext::new();
1514
1515        let if_then = IfThen {
1516            ifs: vec![if_then::IfClause {
1517                r#if: Some(literal_bool(true)),
1518                then: Some(literal_bool(false)),
1519            }],
1520            r#else: None,
1521        };
1522
1523        let (s, errs) = ctx.textify(&if_then);
1524        assert_eq!(s, "if_then(true -> false, _ -> !{Expression})");
1525        assert_eq!(errs.0.len(), 1);
1526    }
1527
1528    fn make_i32_type() -> Type {
1529        Type {
1530            kind: Some(Kind::I32(I32 {
1531                nullability: Nullability::Required as i32,
1532                type_variation_reference: 0,
1533            })),
1534        }
1535    }
1536
1537    fn make_i16_type() -> Type {
1538        Type {
1539            kind: Some(Kind::I16(I16 {
1540                nullability: Nullability::Required as i32,
1541                type_variation_reference: 0,
1542            })),
1543        }
1544    }
1545
1546    fn literal_i32(value: i32) -> Expression {
1547        Expression {
1548            rex_type: Some(RexType::Literal(expr::Literal {
1549                nullable: false,
1550                type_variation_reference: 0,
1551                literal_type: Some(expr::literal::LiteralType::I32(value)),
1552            })),
1553        }
1554    }
1555
1556    #[test]
1557    fn test_cast_textify() {
1558        let ctx = TestContext::new();
1559        let cast = Cast {
1560            r#type: Some(make_i16_type()),
1561            input: Some(Box::new(literal_i32(78))),
1562            failure_behavior: 0,
1563        };
1564        assert_eq!(ctx.textify_no_errors(&cast), "(78:i32)::i16");
1565    }
1566
1567    #[test]
1568    fn test_cast_textify_via_rextype() {
1569        let ctx = TestContext::new();
1570        let rex = RexType::Cast(Box::new(Cast {
1571            r#type: Some(make_i16_type()),
1572            input: Some(Box::new(literal_i32(78))),
1573            failure_behavior: 0,
1574        }));
1575        assert_eq!(ctx.textify_no_errors(&rex), "(78:i32)::i16");
1576    }
1577
1578    #[test]
1579    fn test_cast_textify_nested() {
1580        // ((78:i32)::i16)::i32 — cast of a cast
1581        let ctx = TestContext::new();
1582        let inner_cast = Expression {
1583            rex_type: Some(RexType::Cast(Box::new(Cast {
1584                r#type: Some(make_i16_type()),
1585                input: Some(Box::new(literal_i32(78))),
1586                failure_behavior: 0,
1587            }))),
1588        };
1589        let outer_cast = Cast {
1590            r#type: Some(make_i32_type()),
1591            input: Some(Box::new(inner_cast)),
1592            failure_behavior: 0,
1593        };
1594        assert_eq!(ctx.textify_no_errors(&outer_cast), "((78:i32)::i16)::i32");
1595    }
1596
1597    #[test]
1598    fn test_cast_textify_return_null() {
1599        let ctx = TestContext::new();
1600        let cast = Cast {
1601            r#type: Some(make_i16_type()),
1602            input: Some(Box::new(literal_i32(78))),
1603            failure_behavior: cast::FailureBehavior::ReturnNull as i32,
1604        };
1605        assert_eq!(ctx.textify_no_errors(&cast), "(78:i32)::?i16");
1606    }
1607
1608    #[test]
1609    fn test_cast_textify_throw_exception() {
1610        let ctx = TestContext::new();
1611        let cast = Cast {
1612            r#type: Some(make_i16_type()),
1613            input: Some(Box::new(literal_i32(78))),
1614            failure_behavior: cast::FailureBehavior::ThrowException as i32,
1615        };
1616        assert_eq!(ctx.textify_no_errors(&cast), "(78:i32)::!i16");
1617    }
1618
1619    #[test]
1620    fn test_cast_textify_missing_input() {
1621        let ctx = TestContext::new();
1622        let cast = Cast {
1623            r#type: Some(make_i16_type()),
1624            input: None,
1625            failure_behavior: 0,
1626        };
1627        let (s, errs) = ctx.textify(&cast);
1628        assert_eq!(s, "(!{Expression})::i16");
1629        match &errs.0[0] {
1630            FormatError::Format(e) => {
1631                assert_eq!(e.message, "Expression");
1632                assert_eq!(e.error_type, FormatErrorType::InvalidValue);
1633            }
1634            other => panic!("Expected Format(InvalidValue) for missing input, got: {other}"),
1635        }
1636    }
1637
1638    #[test]
1639    fn test_cast_textify_missing_type() {
1640        let ctx = TestContext::new();
1641        let cast = Cast {
1642            r#type: None,
1643            input: Some(Box::new(literal_i32(78))),
1644            failure_behavior: 0,
1645        };
1646        let (s, errs) = ctx.textify(&cast);
1647        assert_eq!(s, "(78:i32)::!{Type}");
1648        match &errs.0[0] {
1649            FormatError::Format(e) => {
1650                assert_eq!(e.message, "Type");
1651                assert_eq!(e.error_type, FormatErrorType::InvalidValue);
1652            }
1653            other => panic!("Expected Format(InvalidValue) for missing type, got: {other}"),
1654        }
1655    }
1656
1657    fn struct_field_reference(field: i32) -> FieldReference {
1658        FieldReference {
1659            reference_type: Some(ReferenceType::DirectReference(ReferenceSegment {
1660                reference_type: Some(reference_segment::ReferenceType::StructField(Box::new(
1661                    reference_segment::StructField { field, child: None },
1662                ))),
1663            })),
1664            root_type: Some(RootType::RootReference(RootReference {})),
1665        }
1666    }
1667
1668    #[test]
1669    fn test_field_reference_missing_root_type() {
1670        let ctx = TestContext::new();
1671        let mut fr = struct_field_reference(3);
1672        fr.root_type = None;
1673        let (s, errs) = ctx.textify(&fr);
1674        assert_eq!(s, "!{FieldReference}");
1675        match &errs.0[0] {
1676            FormatError::Format(e) => {
1677                assert_eq!(e.message, "FieldReference");
1678                assert_eq!(e.error_type, FormatErrorType::InvalidValue);
1679            }
1680            other => panic!("Expected Format(InvalidValue) for missing root_type, got: {other}"),
1681        }
1682    }
1683
1684    #[test]
1685    fn test_field_reference_root_reference() {
1686        let ctx = TestContext::new();
1687        let fr = struct_field_reference(3);
1688        assert_eq!(ctx.textify_no_errors(&fr), "$3");
1689    }
1690
1691    #[test]
1692    fn test_field_reference_outer_reference_unimplemented() {
1693        let ctx = TestContext::new();
1694        let mut fr = struct_field_reference(3);
1695        fr.root_type = Some(RootType::OuterReference(field_reference::OuterReference {
1696            steps_out: 1,
1697        }));
1698        let (s, errs) = ctx.textify(&fr);
1699        assert_eq!(s, "!{FieldReference}");
1700        match &errs.0[0] {
1701            FormatError::Format(e) => {
1702                assert_eq!(e.message, "FieldReference");
1703                assert_eq!(e.error_type, FormatErrorType::Unimplemented);
1704            }
1705            other => panic!("Expected Format(Unimplemented) for OuterReference, got: {other}"),
1706        }
1707    }
1708
1709    #[test]
1710    fn test_field_reference_expression_unimplemented() {
1711        let ctx = TestContext::new();
1712        let mut fr = struct_field_reference(3);
1713        fr.root_type = Some(RootType::Expression(Box::new(literal_bool(true))));
1714        let (s, errs) = ctx.textify(&fr);
1715        assert_eq!(s, "!{FieldReference}");
1716        match &errs.0[0] {
1717            FormatError::Format(e) => {
1718                assert_eq!(e.message, "FieldReference");
1719                assert_eq!(e.error_type, FormatErrorType::Unimplemented);
1720            }
1721            other => panic!("Expected Format(Unimplemented) for Expression, got: {other}"),
1722        }
1723    }
1724
1725    #[test]
1726    fn test_cast_textify_invalid_failure_behavior() {
1727        let ctx = TestContext::new();
1728        let cast = Cast {
1729            r#type: Some(make_i16_type()),
1730            input: Some(Box::new(literal_i32(78))),
1731            failure_behavior: 99,
1732        };
1733        let (s, errs) = ctx.textify(&cast);
1734        // Error token is embedded inline — input and type are still written
1735        assert_eq!(s, "(78:i32)::!{Cast}i16");
1736        match &errs.0[0] {
1737            FormatError::Format(e) => {
1738                assert_eq!(e.message, "Cast");
1739                assert_eq!(e.error_type, FormatErrorType::InvalidValue);
1740            }
1741            other => {
1742                panic!("Expected Format(InvalidValue) for invalid failure_behavior, got: {other}")
1743            }
1744        }
1745    }
1746
1747    #[test]
1748    fn test_cast_to_user_defined_type_textifies_without_u_prefix() {
1749        // A type stored as "u!json" normalizes to "json"; cast emits "::json".
1750        let ctx = TestContext::new()
1751            .with_urn(1, "urn:example:types")
1752            .with_type(1, 5, "u!json");
1753        let cast = Cast {
1754            r#type: Some(Type {
1755                kind: Some(Kind::UserDefined(UserDefined {
1756                    type_variation_reference: 0,
1757                    nullability: Nullability::Required as i32,
1758                    type_reference: 5,
1759                    type_parameters: vec![],
1760                })),
1761            }),
1762            input: Some(Box::new(literal_i32(1))),
1763            failure_behavior: 0,
1764        };
1765        assert_eq!(ctx.textify_no_errors(&cast), "(1:i32)::json");
1766    }
1767}