Skip to main content

reddb_engine/query/
labels.rs

1//! Canonical output column labels shared by execution hosts.
2
3use reddb_rql::ast::{FieldRef, Projection};
4
5pub fn projection_name(projection: &Projection) -> String {
6    match projection {
7        Projection::All => "*".to_string(),
8        Projection::Column(column) => column.clone(),
9        Projection::Alias(_, alias) => alias.clone(),
10        // Scalar function projections encode `FUNC(...) AS alias`
11        // as `"FUNC:alias"` in the first tuple field. If an alias
12        // is present, expose it as the output column name; otherwise
13        // fall back to the function name.
14        Projection::Function(name, args) => {
15            if let Some((_, alias)) = name.split_once(':') {
16                alias.to_string()
17            } else {
18                // #1370 — an unaliased function / operator projection is labeled
19                // with its source-text form (`UPPER(name)`, `id * 2`,
20                // `name || '!'`), reconstructed from the lowered projection.
21                render_projection_label(name, args)
22            }
23        }
24        Projection::Expression(_, alias) => alias.clone().unwrap_or_else(|| "expr".to_string()),
25        Projection::Field(field, alias) => alias.clone().unwrap_or_else(|| field_ref_name(field)),
26        Projection::Window { name, alias, .. } => alias.clone().unwrap_or_else(|| name.clone()),
27    }
28}
29
30/// SQL infix symbol for an arithmetic / concat operator that
31/// `sql_lowering::projection_binop_name` lowered to a function name.
32fn projection_operator_symbol(name: &str) -> Option<&'static str> {
33    match name {
34        "ADD" => Some("+"),
35        "SUB" => Some("-"),
36        "MUL" => Some("*"),
37        "DIV" => Some("/"),
38        "MOD" => Some("%"),
39        "CONCAT" => Some("||"),
40        _ => None,
41    }
42}
43
44/// Render one argument of an unaliased function/operator projection back to
45/// its source-text label.
46fn projection_arg_label(projection: &Projection) -> String {
47    match projection {
48        Projection::Column(column) => match column.strip_prefix("LIT:") {
49            // Numbers / bool / null render bare; string literals are re-quoted
50            // to match the source text the user wrote (#1370).
51            Some(lit)
52                if lit.is_empty()
53                    || lit.parse::<f64>().is_ok()
54                    || lit.eq_ignore_ascii_case("true")
55                    || lit.eq_ignore_ascii_case("false")
56                    || lit.eq_ignore_ascii_case("null") =>
57            {
58                lit.to_string()
59            }
60            Some(lit) => format!("'{lit}'"),
61            None => column.clone(),
62        },
63        Projection::Function(name, args) => {
64            let base = name.split_once(':').map(|(b, _)| b).unwrap_or(name);
65            render_projection_label(base, args)
66        }
67        Projection::Field(field, _) => field_ref_name(field),
68        Projection::Alias(_, alias) => alias.clone(),
69        other => projection_name(other),
70    }
71}
72
73/// Reconstruct the source-text label of an unaliased function / operator
74/// projection: operators render infix (`id * 2`), other functions render as
75/// calls (`UPPER(name)`, `COALESCE(name, 'fb')`).
76fn render_projection_label(name: &str, args: &[Projection]) -> String {
77    if let Some(symbol) = projection_operator_symbol(name) {
78        args.iter()
79            .map(projection_arg_label)
80            .collect::<Vec<_>>()
81            .join(&format!(" {symbol} "))
82    } else {
83        format!(
84            "{}({})",
85            name,
86            args.iter()
87                .map(projection_arg_label)
88                .collect::<Vec<_>>()
89                .join(", ")
90        )
91    }
92}
93
94pub fn field_ref_name(field: &FieldRef) -> String {
95    match field {
96        FieldRef::TableColumn { table, column } => {
97            if table.is_empty() {
98                column.clone()
99            } else {
100                format!("{table}.{column}")
101            }
102        }
103        FieldRef::NodeProperty { alias, property } => format!("{alias}.{property}"),
104        FieldRef::EdgeProperty { alias, property } => format!("{alias}.{property}"),
105        FieldRef::NodeId { alias } => format!("{alias}.id"),
106    }
107}