substrait_explain/textify/
expressions.rs

1use std::fmt::{self};
2
3use chrono::{DateTime, NaiveDate};
4use expr::RexType;
5use substrait::proto::expression::field_reference::ReferenceType;
6use substrait::proto::expression::literal::LiteralType;
7use substrait::proto::expression::{
8    Cast, FieldReference, IfThen, ReferenceSegment, ScalarFunction, cast, reference_segment,
9};
10use substrait::proto::function_argument::ArgType;
11use substrait::proto::{
12    AggregateFunction, Expression, FunctionArgument, FunctionOption, expression as expr,
13};
14
15use super::{PlanError, Scope, Textify, Visibility};
16use crate::extensions::simple::ExtensionKind;
17use crate::textify::types::{Name, NamedAnchor, OutputType, escaped};
18
19// …(…) for function call
20// […] for variant
21// <…> for parameters
22// !{…} for missing value
23
24// $… for field reference
25// #… for anchor
26// @… for URN anchor
27// …::… for cast
28// …:… for specifying type
29// &… for enum
30
31pub fn textify_binary<S: Scope, W: fmt::Write>(items: &[u8], ctx: &S, w: &mut W) -> fmt::Result {
32    if ctx.options().show_literal_binaries {
33        write!(w, "0x")?;
34        for &n in items {
35            write!(w, "{n:02x}")?;
36        }
37    } else {
38        write!(w, "{{binary}}")?;
39    }
40    Ok(())
41}
42
43/// Write an error token for a literal type that hasn't been implemented yet.
44fn unimplemented_literal<S: Scope, W: fmt::Write>(
45    variant: &'static str,
46    ctx: &S,
47    w: &mut W,
48) -> fmt::Result {
49    write!(
50        w,
51        "{}",
52        ctx.failure(PlanError::unimplemented(
53            "LiteralType",
54            Some(variant),
55            format!("{variant} literal textification not implemented"),
56        ))
57    )
58}
59
60/// Write an enum value. Enums are written as `&<identifier>`, if the string is
61/// a valid identifier; otherwise, they are written as `&'<escaped_string>'`.
62pub fn textify_enum<S: Scope, W: fmt::Write>(s: &str, _ctx: &S, w: &mut W) -> fmt::Result {
63    write!(w, "&{}", Name(s))
64}
65
66pub fn timestamp_to_string(t: i64) -> String {
67    let ts = chrono::DateTime::from_timestamp_nanos(t);
68    ts.to_rfc3339()
69}
70
71/// Convert days since Unix epoch to date string
72fn days_to_date_string(days: i32) -> String {
73    let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
74    let date = epoch + chrono::Duration::days(days as i64);
75    date.format("%Y-%m-%d").to_string()
76}
77
78/// Convert microseconds since midnight to time string
79fn microseconds_to_time_string(microseconds: i64) -> String {
80    let total_seconds = microseconds / 1_000_000;
81    let remaining_microseconds = microseconds % 1_000_000;
82
83    let hours = total_seconds / 3600;
84    let minutes = (total_seconds % 3600) / 60;
85    let seconds = total_seconds % 60;
86
87    if remaining_microseconds == 0 {
88        format!("{hours:02}:{minutes:02}:{seconds:02}")
89    } else {
90        // Convert microseconds to fractional seconds
91        let fractional = remaining_microseconds as f64 / 1_000_000.0;
92        format!("{hours:02}:{minutes:02}:{seconds:02}{fractional:.6}")
93            .trim_end_matches('0')
94            .trim_end_matches('.')
95            .to_string()
96    }
97}
98
99/// Convert microseconds since Unix epoch to timestamp string
100fn microseconds_to_timestamp_string(microseconds: i64) -> String {
101    let epoch = DateTime::from_timestamp(0, 0).unwrap().naive_utc();
102    let duration = chrono::Duration::microseconds(microseconds);
103    let datetime = epoch + duration;
104
105    // Format with fractional seconds, then clean up trailing zeros
106    let formatted = datetime.format("%Y-%m-%dT%H:%M:%S%.f").to_string();
107
108    // If there are fractional seconds, trim trailing zeros and dot if needed
109    if formatted.contains('.') {
110        formatted
111            .trim_end_matches('0')
112            .trim_end_matches('.')
113            .to_string()
114    } else {
115        formatted
116    }
117}
118
119/// Write just the value portion of a literal, with no type suffix or
120/// nullability marker.
121///
122/// For unimplemented types, writes an error token via `ctx.failure()`.
123fn write_literal_value<S: Scope, W: fmt::Write>(
124    lit: &LiteralType,
125    ctx: &S,
126    w: &mut W,
127) -> fmt::Result {
128    match lit {
129        LiteralType::Boolean(b) => write!(w, "{b}"),
130        LiteralType::I8(i) | LiteralType::I16(i) | LiteralType::I32(i) => write!(w, "{i}"),
131        LiteralType::I64(i) => write!(w, "{i}"),
132        LiteralType::Fp32(f) => write!(w, "{f}"),
133        LiteralType::Fp64(f) => write!(w, "{f}"),
134        LiteralType::String(s) => write!(w, "'{}'", s.escape_debug()),
135        LiteralType::Binary(items) => textify_binary(items, ctx, w),
136        LiteralType::Date(days) => {
137            write!(w, "'{}'", escaped(&days_to_date_string(*days)))
138        }
139        LiteralType::Time(microseconds) => {
140            write!(
141                w,
142                "'{}'",
143                escaped(&microseconds_to_time_string(*microseconds))
144            )
145        }
146        #[allow(deprecated)]
147        LiteralType::Timestamp(microseconds) => {
148            write!(
149                w,
150                "'{}'",
151                escaped(&microseconds_to_timestamp_string(*microseconds))
152            )
153        }
154        LiteralType::IntervalYearToMonth(_) => unimplemented_literal("IntervalYearToMonth", ctx, w),
155        LiteralType::IntervalDayToSecond(_) => unimplemented_literal("IntervalDayToSecond", ctx, w),
156        LiteralType::IntervalCompound(_) => unimplemented_literal("IntervalCompound", ctx, w),
157        LiteralType::FixedChar(_) => unimplemented_literal("FixedChar", ctx, w),
158        LiteralType::VarChar(_) => unimplemented_literal("VarChar", ctx, w),
159        LiteralType::FixedBinary(_) => unimplemented_literal("FixedBinary", ctx, w),
160        LiteralType::Decimal(_) => unimplemented_literal("Decimal", ctx, w),
161        LiteralType::PrecisionTime(_) => unimplemented_literal("PrecisionTime", ctx, w),
162        LiteralType::PrecisionTimestamp(_) => unimplemented_literal("PrecisionTimestamp", ctx, w),
163        LiteralType::PrecisionTimestampTz(_) => {
164            unimplemented_literal("PrecisionTimestampTz", ctx, w)
165        }
166        LiteralType::Struct(_) => unimplemented_literal("Struct", ctx, w),
167        LiteralType::Map(_) => unimplemented_literal("Map", ctx, w),
168        #[allow(deprecated)]
169        LiteralType::TimestampTz(_) => unimplemented_literal("TimestampTz", ctx, w),
170        LiteralType::Uuid(_) => unimplemented_literal("Uuid", ctx, w),
171        LiteralType::Null(_) => unimplemented_literal("Null", ctx, w),
172        LiteralType::List(_) => unimplemented_literal("List", ctx, w),
173        LiteralType::EmptyList(_) => unimplemented_literal("EmptyList", ctx, w),
174        LiteralType::EmptyMap(_) => unimplemented_literal("EmptyMap", ctx, w),
175        LiteralType::UserDefined(_) => unimplemented_literal("UserDefined", ctx, w),
176    }
177}
178
179/// The type suffix for a literal (e.g., `"i32"`, `"fp64"`, `"date"`).
180///
181/// Returns `None` for unimplemented types whose [`write_literal_value`] already
182/// emitted an error token.
183fn literal_type_suffix(lit: &LiteralType) -> Option<&'static str> {
184    match lit {
185        LiteralType::Boolean(_) => Some("boolean"),
186        LiteralType::I8(_) => Some("i8"),
187        LiteralType::I16(_) => Some("i16"),
188        LiteralType::I32(_) => Some("i32"),
189        LiteralType::I64(_) => Some("i64"),
190        LiteralType::Fp32(_) => Some("fp32"),
191        LiteralType::Fp64(_) => Some("fp64"),
192        LiteralType::String(_) => Some("string"),
193        LiteralType::Binary(_) => Some("binary"),
194        LiteralType::Date(_) => Some("date"),
195        LiteralType::Time(_) => Some("time"),
196        #[allow(deprecated)]
197        LiteralType::Timestamp(_) => Some("timestamp"),
198        _ => None,
199    }
200}
201
202/// Whether this type is the default interpretation for its value syntax.
203///
204/// Each literal value syntax has a default type that the parser assumes when
205/// no explicit type suffix is present:
206/// - `true`/`false` → `boolean`
207/// - bare integers (`42`) → `i64`
208/// - bare floats (`3.19`) → `fp64`
209/// - single-quoted strings (`'hello'`) → `string`
210/// - hex literals (`0x...`) → `binary`
211///
212/// Non-default types (e.g., `i32`, `fp32`, `date`) always need an explicit
213/// suffix to distinguish them from the default.
214fn is_default_for_syntax(lit: &LiteralType) -> bool {
215    matches!(
216        lit,
217        LiteralType::Boolean(_)
218            | LiteralType::String(_)
219            | LiteralType::Binary(_)
220            | LiteralType::I64(_)
221            | LiteralType::Fp64(_)
222    )
223}
224
225impl Textify for expr::Literal {
226    fn name() -> &'static str {
227        "Literal"
228    }
229
230    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
231        let Some(lit) = self.literal_type.as_ref() else {
232            return write!(
233                w,
234                "{}",
235                ctx.failure(PlanError::invalid(
236                    "Literal",
237                    Some("literal_type"),
238                    "missing literal_type",
239                ))
240            );
241        };
242        write_literal_value(lit, ctx, w)?;
243        let show_suffix = match ctx.options().literal_types {
244            Visibility::Never => false,
245            Visibility::Always => true,
246            Visibility::Required => self.nullable || !is_default_for_syntax(lit),
247        };
248        if show_suffix {
249            if let Some(suffix) = literal_type_suffix(lit) {
250                write!(w, ":{suffix}")?;
251            }
252            if self.nullable {
253                write!(w, "?")?;
254            }
255        }
256        Ok(())
257    }
258}
259
260pub struct Reference(pub i32);
261
262impl fmt::Display for Reference {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        write!(f, "${}", self.0)
265    }
266}
267
268impl From<Reference> for Expression {
269    fn from(r: Reference) -> Self {
270        // XXX: Why is it so many layers to make a struct field reference? This is
271        // surprisingly complex
272        Expression {
273            rex_type: Some(RexType::Selection(Box::new(FieldReference {
274                reference_type: Some(ReferenceType::DirectReference(ReferenceSegment {
275                    reference_type: Some(reference_segment::ReferenceType::StructField(Box::new(
276                        reference_segment::StructField {
277                            field: r.0,
278                            child: None,
279                        },
280                    ))),
281                })),
282                root_type: None,
283            }))),
284        }
285    }
286}
287
288impl Textify for Reference {
289    fn name() -> &'static str {
290        "Reference"
291    }
292
293    fn textify<S: Scope, W: fmt::Write>(&self, _ctx: &S, w: &mut W) -> fmt::Result {
294        write!(w, "{self}")
295    }
296}
297
298impl Textify for FieldReference {
299    fn name() -> &'static str {
300        "FieldReference"
301    }
302
303    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
304        let ref_type = match &self.reference_type {
305            None => {
306                return write!(
307                    w,
308                    "{}",
309                    ctx.failure(PlanError::invalid(
310                        "FieldReference",
311                        Some("reference_type"),
312                        "Required field reference_type is missing",
313                    ))
314                );
315            }
316            Some(ReferenceType::DirectReference(r)) => r,
317            _ => {
318                return write!(
319                    w,
320                    "{}",
321                    ctx.failure(PlanError::unimplemented(
322                        "FieldReference",
323                        Some("FieldReference"),
324                        "FieldReference textification implemented only for StructField",
325                    ))
326                );
327            }
328        };
329
330        match &ref_type.reference_type {
331            Some(reference_segment::ReferenceType::StructField(s)) => {
332                write!(w, "{}", Reference(s.field))
333            }
334            None => write!(
335                w,
336                "{}",
337                ctx.failure(PlanError::invalid(
338                    "ReferenceSegment",
339                    Some("reference_type"),
340                    "Required field reference_type is missing",
341                ))
342            ),
343            _ => write!(
344                w,
345                "{}",
346                ctx.failure(PlanError::unimplemented(
347                    "ReferenceSegment",
348                    Some("reference_type"),
349                    "ReferenceSegment textification implemented only for StructField",
350                ))
351            ),
352        }
353    }
354}
355
356impl Textify for ScalarFunction {
357    fn name() -> &'static str {
358        "ScalarFunction"
359    }
360
361    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
362        let name_and_anchor =
363            NamedAnchor::lookup(ctx, ExtensionKind::Function, self.function_reference);
364        let name_and_anchor = ctx.display(&name_and_anchor);
365
366        let args = ctx.separated(&self.arguments, ", ");
367        let options = ctx.separated(&self.options, ", ");
368        let between = if self.arguments.is_empty() || self.options.is_empty() {
369            ""
370        } else {
371            ", "
372        };
373
374        let output = OutputType(self.output_type.as_ref());
375        let output_type = ctx.optional(&output, ctx.options().fn_types);
376
377        write!(
378            w,
379            "{name_and_anchor}({args}{between}{options}){output_type}"
380        )?;
381        Ok(())
382    }
383}
384
385impl Textify for FunctionOption {
386    fn name() -> &'static str {
387        "FunctionOption"
388    }
389
390    fn textify<S: Scope, W: fmt::Write>(&self, _ctx: &S, w: &mut W) -> fmt::Result {
391        write!(w, "{}⇒[", self.name)?;
392        let mut first = true;
393        for pref in self.preference.iter() {
394            if !first {
395                write!(w, ", ")?;
396            } else {
397                first = false;
398            }
399            write!(w, "{pref}")?;
400        }
401        write!(w, "]")?;
402        Ok(())
403    }
404}
405
406impl Textify for FunctionArgument {
407    fn name() -> &'static str {
408        "FunctionArgument"
409    }
410
411    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
412        write!(w, "{}", ctx.expect(self.arg_type.as_ref()))
413    }
414}
415
416impl Textify for ArgType {
417    fn name() -> &'static str {
418        "ArgType"
419    }
420
421    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
422        match self {
423            ArgType::Type(t) => t.textify(ctx, w),
424            ArgType::Value(v) => v.textify(ctx, w),
425            ArgType::Enum(e) => textify_enum(e, ctx, w),
426        }
427    }
428}
429
430impl Textify for Cast {
431    fn name() -> &'static str {
432        "Cast"
433    }
434
435    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
436        let failure_err;
437        let fb: &dyn fmt::Display = match cast::FailureBehavior::try_from(self.failure_behavior) {
438            Ok(cast::FailureBehavior::Unspecified) => &"",
439            Ok(cast::FailureBehavior::ReturnNull) => &"?",
440            Ok(cast::FailureBehavior::ThrowException) => &"!",
441            Err(_) => {
442                failure_err = ctx.failure(PlanError::invalid(
443                    "Cast",
444                    Some("failure_behavior"),
445                    format!("Unknown failure_behavior value: {}", self.failure_behavior),
446                ));
447                &failure_err
448            }
449        };
450        let input = ctx.expect(self.input.as_deref());
451        let target_type = ctx.expect(self.r#type.as_ref());
452        write!(w, "({input})::{fb}{target_type}")
453    }
454}
455
456impl Textify for IfThen {
457    fn name() -> &'static str {
458        "IfThen"
459    }
460
461    // This method writes ifThen using the following convention of a comma separated sequence of 'if_clause -> then_clause, '
462    // followed by the final else clause denoted with '_'
463    // ex: true -> if_then(true || false -> true, _ -> false)
464    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
465        write!(w, "if_then(")?;
466        for clause in &self.ifs {
467            let if_expr = ctx.expect(clause.r#if.as_ref());
468            let then_expr = ctx.expect(clause.then.as_ref());
469            write!(w, "{if_expr} -> {then_expr}, ")?;
470        }
471        let else_expr = ctx.expect(self.r#else.as_deref());
472        write!(w, "_ -> {else_expr})")
473    }
474}
475
476impl Textify for RexType {
477    fn name() -> &'static str {
478        "RexType"
479    }
480
481    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
482        match self {
483            RexType::Literal(literal) => literal.textify(ctx, w),
484            RexType::Selection(f) => f.textify(ctx, w),
485            RexType::ScalarFunction(s) => s.textify(ctx, w),
486            RexType::WindowFunction(_w) => write!(
487                w,
488                "{}",
489                ctx.failure(PlanError::unimplemented(
490                    "RexType",
491                    Some("WindowFunction"),
492                    "WindowFunction textification not implemented",
493                ))
494            ),
495            RexType::IfThen(i) => i.textify(ctx, w),
496            RexType::SwitchExpression(_s) => write!(
497                w,
498                "{}",
499                ctx.failure(PlanError::unimplemented(
500                    "RexType",
501                    Some("SwitchExpression"),
502                    "SwitchExpression textification not implemented",
503                ))
504            ),
505            RexType::SingularOrList(_s) => write!(
506                w,
507                "{}",
508                ctx.failure(PlanError::unimplemented(
509                    "RexType",
510                    Some("SingularOrList"),
511                    "SingularOrList textification not implemented",
512                ))
513            ),
514            RexType::MultiOrList(_m) => write!(
515                w,
516                "{}",
517                ctx.failure(PlanError::unimplemented(
518                    "RexType",
519                    Some("MultiOrList"),
520                    "MultiOrList textification not implemented",
521                ))
522            ),
523            RexType::Cast(c) => c.textify(ctx, w),
524            RexType::Subquery(_s) => write!(
525                w,
526                "{}",
527                ctx.failure(PlanError::unimplemented(
528                    "RexType",
529                    Some("Subquery"),
530                    "Subquery textification not implemented",
531                ))
532            ),
533            RexType::Nested(_n) => write!(
534                w,
535                "{}",
536                ctx.failure(PlanError::unimplemented(
537                    "RexType",
538                    Some("Nested"),
539                    "Nested textification not implemented",
540                ))
541            ),
542            RexType::DynamicParameter(_d) => write!(
543                w,
544                "{}",
545                ctx.failure(PlanError::unimplemented(
546                    "RexType",
547                    Some("DynamicParameter"),
548                    "DynamicParameter textification not implemented",
549                ))
550            ),
551            #[allow(deprecated)]
552            RexType::Enum(_) => write!(
553                w,
554                "{}",
555                ctx.failure(PlanError::unimplemented(
556                    "RexType",
557                    Some("Enum"),
558                    "Enum textification not implemented",
559                ))
560            ),
561        }
562    }
563}
564
565impl Textify for Expression {
566    fn name() -> &'static str {
567        "Expression"
568    }
569
570    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
571        write!(w, "{}", ctx.expect(self.rex_type.as_ref()))
572    }
573}
574
575impl Textify for AggregateFunction {
576    fn name() -> &'static str {
577        "AggregateFunction"
578    }
579
580    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
581        // Similar to ScalarFunction textification
582        let name_and_anchor =
583            NamedAnchor::lookup(ctx, ExtensionKind::Function, self.function_reference);
584        let name_and_anchor = ctx.display(&name_and_anchor);
585
586        let args = ctx.separated(&self.arguments, ", ");
587        let options = ctx.separated(&self.options, ", ");
588        let between = if self.arguments.is_empty() || self.options.is_empty() {
589            ""
590        } else {
591            ", "
592        };
593
594        let output = OutputType(self.output_type.as_ref());
595        let output_type = ctx.optional(&output, ctx.options().fn_types);
596
597        write!(
598            w,
599            "{name_and_anchor}({args}{between}{options}){output_type}"
600        )
601    }
602}
603
604#[cfg(test)]
605mod tests {
606    use substrait::proto::Type;
607    use substrait::proto::expression::{cast, if_then};
608    use substrait::proto::r#type::{I16, I32, Kind, Nullability};
609
610    use super::*;
611    use crate::extensions::simple::{ExtensionKind, MissingReference};
612    use crate::fixtures::TestContext;
613    use crate::textify::foundation::{FormatError, FormatErrorType};
614
615    fn literal_bool(value: bool) -> Expression {
616        Expression {
617            rex_type: Some(RexType::Literal(expr::Literal {
618                nullable: false,
619                type_variation_reference: 0,
620                literal_type: Some(expr::literal::LiteralType::Boolean(value)),
621            })),
622        }
623    }
624
625    fn non_nullable_literal(lit: expr::literal::LiteralType) -> expr::Literal {
626        expr::Literal {
627            nullable: false,
628            type_variation_reference: 0,
629            literal_type: Some(lit),
630        }
631    }
632
633    #[test]
634    fn test_literal_textify() {
635        let ctx = TestContext::new();
636
637        let literal = non_nullable_literal(LiteralType::Boolean(true));
638        assert_eq!(ctx.textify_no_errors(&literal), "true");
639    }
640
641    fn nullable_literal(lit: expr::literal::LiteralType) -> expr::Literal {
642        expr::Literal {
643            nullable: true,
644            type_variation_reference: 0,
645            literal_type: Some(lit),
646        }
647    }
648
649    #[test]
650    fn test_nullable_boolean_literal_textify() {
651        let ctx = TestContext::new();
652        assert_eq!(
653            ctx.textify_no_errors(&nullable_literal(expr::literal::LiteralType::Boolean(true))),
654            "true:boolean?"
655        );
656        assert_eq!(
657            ctx.textify_no_errors(&nullable_literal(expr::literal::LiteralType::Boolean(
658                false
659            ))),
660            "false:boolean?"
661        );
662    }
663
664    #[test]
665    fn test_nullable_integer_literal_textify() {
666        let ctx = TestContext::new();
667        assert_eq!(
668            ctx.textify_no_errors(&nullable_literal(expr::literal::LiteralType::I32(78))),
669            "78:i32?"
670        );
671        assert_eq!(
672            ctx.textify_no_errors(&nullable_literal(expr::literal::LiteralType::I64(42))),
673            "42:i64?"
674        );
675    }
676
677    #[test]
678    fn test_nullable_float_literal_textify() {
679        let ctx = TestContext::new();
680        assert_eq!(
681            ctx.textify_no_errors(&nullable_literal(expr::literal::LiteralType::Fp32(2.5))),
682            "2.5:fp32?"
683        );
684        assert_eq!(
685            ctx.textify_no_errors(&nullable_literal(expr::literal::LiteralType::Fp64(3.19))),
686            "3.19:fp64?"
687        );
688    }
689
690    #[test]
691    fn test_expression_textify() {
692        let ctx = TestContext::new();
693
694        // Test empty expression
695        let expr_empty = Expression { rex_type: None }; // Renamed to avoid conflict
696        let (s, errs) = ctx.textify(&expr_empty);
697        assert!(!errs.is_empty());
698        assert_eq!(s, "!{RexType}");
699
700        // Test literal expression
701        let expr_lit = Expression {
702            rex_type: Some(RexType::Literal(expr::Literal {
703                nullable: false,
704                type_variation_reference: 0,
705                literal_type: Some(expr::literal::LiteralType::Boolean(true)),
706            })),
707        };
708        assert_eq!(ctx.textify_no_errors(&expr_lit), "true");
709    }
710
711    #[test]
712    fn test_rextype_textify() {
713        let ctx = TestContext::new();
714
715        let func = RexType::ScalarFunction(ScalarFunction {
716            function_reference: 1000, // Does not exist
717            arguments: vec![],
718            options: vec![],
719            output_type: None,
720            #[allow(deprecated)]
721            args: vec![],
722        });
723        // With strict=false (default in OutputOptions), it should format as unknown
724        // If strict=true, it would be an error.
725        // Assuming default OutputOptions has strict = false.
726        // ScopedContext.options() has strict. Default OutputOptions has strict: false.
727        let (s, errq) = ctx.textify(&func);
728        let errs: Vec<_> = errq.0;
729        match errs[0] {
730            FormatError::Lookup(MissingReference::MissingAnchor(k, a)) => {
731                assert_eq!(k, ExtensionKind::Function);
732                assert_eq!(a, 1000);
733            }
734            _ => panic!("Expected Lookup MissingAnchor: {}", errs[0]),
735        }
736        assert_eq!(s, "!{function}#1000()");
737
738        let ctx = ctx.with_urn(1, "first").with_function(1, 100, "first");
739        let func = RexType::ScalarFunction(ScalarFunction {
740            function_reference: 100,
741            arguments: vec![],
742            options: vec![],
743            output_type: None,
744            #[allow(deprecated)]
745            args: vec![],
746        });
747        let s = ctx.textify_no_errors(&func);
748        assert_eq!(s, "first()");
749
750        // Test for duplicated function name requiring anchor
751        let options_show_anchor = Default::default();
752
753        let ctx = TestContext::new()
754            .with_options(options_show_anchor)
755            .with_urn(1, "somewhere_on_the_internet")
756            .with_urn(2, "somewhere_else")
757            .with_function(1, 231, "duplicated")
758            .with_function(2, 232, "duplicated");
759
760        let rex_dup = RexType::ScalarFunction(ScalarFunction {
761            function_reference: 231,
762            arguments: vec![FunctionArgument {
763                arg_type: Some(ArgType::Value(Expression {
764                    rex_type: Some(RexType::Literal(expr::Literal {
765                        nullable: false,
766                        type_variation_reference: 0,
767                        literal_type: Some(expr::literal::LiteralType::Boolean(true)),
768                    })),
769                })),
770            }],
771            options: vec![],
772            output_type: None,
773            #[allow(deprecated)]
774            args: vec![],
775        });
776        let s = ctx.textify_no_errors(&rex_dup);
777        assert_eq!(s, "duplicated#231(true)");
778    }
779
780    #[test]
781    fn test_ifthen_textify() {
782        let ctx = TestContext::new();
783
784        let if_then = IfThen {
785            ifs: vec![
786                if_then::IfClause {
787                    r#if: Some(literal_bool(true)),
788                    then: Some(literal_bool(false)),
789                },
790                if_then::IfClause {
791                    r#if: Some(literal_bool(false)),
792                    then: Some(literal_bool(true)),
793                },
794            ],
795            r#else: Some(Box::new(literal_bool(true))),
796        };
797
798        let s = ctx.textify_no_errors(&if_then);
799        assert_eq!(s, "if_then(true -> false, false -> true, _ -> true)");
800    }
801
802    #[test]
803    fn test_ifthen_textify_missing_else() {
804        let ctx = TestContext::new();
805
806        let if_then = IfThen {
807            ifs: vec![if_then::IfClause {
808                r#if: Some(literal_bool(true)),
809                then: Some(literal_bool(false)),
810            }],
811            r#else: None,
812        };
813
814        let (s, errs) = ctx.textify(&if_then);
815        assert_eq!(s, "if_then(true -> false, _ -> !{Expression})");
816        assert_eq!(errs.0.len(), 1);
817    }
818
819    fn make_i32_type() -> Type {
820        Type {
821            kind: Some(Kind::I32(I32 {
822                nullability: Nullability::Required as i32,
823                type_variation_reference: 0,
824            })),
825        }
826    }
827
828    fn make_i16_type() -> Type {
829        Type {
830            kind: Some(Kind::I16(I16 {
831                nullability: Nullability::Required as i32,
832                type_variation_reference: 0,
833            })),
834        }
835    }
836
837    fn literal_i32(value: i32) -> Expression {
838        Expression {
839            rex_type: Some(RexType::Literal(expr::Literal {
840                nullable: false,
841                type_variation_reference: 0,
842                literal_type: Some(expr::literal::LiteralType::I32(value)),
843            })),
844        }
845    }
846
847    #[test]
848    fn test_cast_textify() {
849        let ctx = TestContext::new();
850        let cast = Cast {
851            r#type: Some(make_i16_type()),
852            input: Some(Box::new(literal_i32(78))),
853            failure_behavior: 0,
854        };
855        assert_eq!(ctx.textify_no_errors(&cast), "(78:i32)::i16");
856    }
857
858    #[test]
859    fn test_cast_textify_via_rextype() {
860        let ctx = TestContext::new();
861        let rex = RexType::Cast(Box::new(Cast {
862            r#type: Some(make_i16_type()),
863            input: Some(Box::new(literal_i32(78))),
864            failure_behavior: 0,
865        }));
866        assert_eq!(ctx.textify_no_errors(&rex), "(78:i32)::i16");
867    }
868
869    #[test]
870    fn test_cast_textify_nested() {
871        // ((78:i32)::i16)::i32 — cast of a cast
872        let ctx = TestContext::new();
873        let inner_cast = Expression {
874            rex_type: Some(RexType::Cast(Box::new(Cast {
875                r#type: Some(make_i16_type()),
876                input: Some(Box::new(literal_i32(78))),
877                failure_behavior: 0,
878            }))),
879        };
880        let outer_cast = Cast {
881            r#type: Some(make_i32_type()),
882            input: Some(Box::new(inner_cast)),
883            failure_behavior: 0,
884        };
885        assert_eq!(ctx.textify_no_errors(&outer_cast), "((78:i32)::i16)::i32");
886    }
887
888    #[test]
889    fn test_cast_textify_return_null() {
890        let ctx = TestContext::new();
891        let cast = Cast {
892            r#type: Some(make_i16_type()),
893            input: Some(Box::new(literal_i32(78))),
894            failure_behavior: cast::FailureBehavior::ReturnNull as i32,
895        };
896        assert_eq!(ctx.textify_no_errors(&cast), "(78:i32)::?i16");
897    }
898
899    #[test]
900    fn test_cast_textify_throw_exception() {
901        let ctx = TestContext::new();
902        let cast = Cast {
903            r#type: Some(make_i16_type()),
904            input: Some(Box::new(literal_i32(78))),
905            failure_behavior: cast::FailureBehavior::ThrowException as i32,
906        };
907        assert_eq!(ctx.textify_no_errors(&cast), "(78:i32)::!i16");
908    }
909
910    #[test]
911    fn test_cast_textify_missing_input() {
912        let ctx = TestContext::new();
913        let cast = Cast {
914            r#type: Some(make_i16_type()),
915            input: None,
916            failure_behavior: 0,
917        };
918        let (s, errs) = ctx.textify(&cast);
919        assert_eq!(s, "(!{Expression})::i16");
920        match &errs.0[0] {
921            FormatError::Format(e) => {
922                assert_eq!(e.message, "Expression");
923                assert_eq!(e.error_type, FormatErrorType::InvalidValue);
924            }
925            other => panic!("Expected Format(InvalidValue) for missing input, got: {other}"),
926        }
927    }
928
929    #[test]
930    fn test_cast_textify_missing_type() {
931        let ctx = TestContext::new();
932        let cast = Cast {
933            r#type: None,
934            input: Some(Box::new(literal_i32(78))),
935            failure_behavior: 0,
936        };
937        let (s, errs) = ctx.textify(&cast);
938        assert_eq!(s, "(78:i32)::!{Type}");
939        match &errs.0[0] {
940            FormatError::Format(e) => {
941                assert_eq!(e.message, "Type");
942                assert_eq!(e.error_type, FormatErrorType::InvalidValue);
943            }
944            other => panic!("Expected Format(InvalidValue) for missing type, got: {other}"),
945        }
946    }
947
948    #[test]
949    fn test_cast_textify_invalid_failure_behavior() {
950        let ctx = TestContext::new();
951        let cast = Cast {
952            r#type: Some(make_i16_type()),
953            input: Some(Box::new(literal_i32(78))),
954            failure_behavior: 99,
955        };
956        let (s, errs) = ctx.textify(&cast);
957        // Error token is embedded inline — input and type are still written
958        assert_eq!(s, "(78:i32)::!{Cast}i16");
959        match &errs.0[0] {
960            FormatError::Format(e) => {
961                assert_eq!(e.message, "Cast");
962                assert_eq!(e.error_type, FormatErrorType::InvalidValue);
963            }
964            other => {
965                panic!("Expected Format(InvalidValue) for invalid failure_behavior, got: {other}")
966            }
967        }
968    }
969}