Skip to main content

icydb_core/db/query/builder/
scalar_projection.rs

1//! Module: query::builder::scalar_projection
2//! Responsibility: shared outward scalar-projection contracts and stable plan
3//! label rendering used by bounded projection helpers.
4//! Does not own: query planning, generic expression validation, or projection
5//! execution policy.
6//! Boundary: fluent helper projections share this contract so adapter surfaces
7//! can consume one stable projection-helper API.
8
9use crate::{
10    db::{
11        QueryError,
12        query::plan::expr::{Expr, FieldPath},
13    },
14    value::Value,
15};
16
17///
18/// ValueProjectionExpr
19///
20/// Shared bounded scalar projection helper contract used by fluent
21/// value-projection terminals.
22/// Implementors stay intentionally narrow and do not imply a generic
23/// expression-builder surface.
24///
25
26pub trait ValueProjectionExpr {
27    /// Borrow the single source field used by this bounded helper.
28    fn field(&self) -> &str;
29
30    /// Render the stable canonical output label for this projection.
31    fn projection_label(&self) -> String;
32
33    /// Apply this projection to one already-loaded source value.
34    fn apply_value(&self, value: Value) -> Result<Value, QueryError>;
35}
36
37/// Render one canonical bounded scalar projection expression back into a
38/// stable plan label.
39#[must_use]
40pub(in crate::db) fn render_scalar_projection_expr_plan_label(expr: &Expr) -> String {
41    render_scalar_projection_expr_plan_label_with_parent(expr, None, false)
42}
43
44fn render_scalar_projection_expr_plan_label_with_parent(
45    expr: &Expr,
46    parent_op: Option<crate::db::query::plan::expr::BinaryOp>,
47    is_right_child: bool,
48) -> String {
49    match expr {
50        Expr::Field(field) => field.as_str().to_string(),
51        Expr::FieldPath(path) => render_field_path_plan_label(path),
52        Expr::Literal(value) => render_scalar_projection_literal(value),
53        Expr::FunctionCall { function, args } => {
54            let rendered_args = args
55                .iter()
56                .map(|arg| render_scalar_projection_expr_plan_label_with_parent(arg, None, false))
57                .collect::<Vec<_>>()
58                .join(", ");
59
60            format!("{}({rendered_args})", function.canonical_label())
61        }
62        Expr::Case {
63            when_then_arms,
64            else_expr,
65        } => render_case_projection_expr_plan_label(when_then_arms, else_expr.as_ref()),
66        Expr::Binary { op, left, right } => {
67            let left = render_scalar_projection_expr_plan_label_with_parent(
68                left.as_ref(),
69                Some(*op),
70                false,
71            );
72            let right = render_scalar_projection_expr_plan_label_with_parent(
73                right.as_ref(),
74                Some(*op),
75                true,
76            );
77            let rendered = format!("{left} {} {right}", binary_op_symbol(*op));
78
79            if binary_expr_requires_parentheses(*op, parent_op, is_right_child) {
80                format!("({rendered})")
81            } else {
82                rendered
83            }
84        }
85        Expr::Aggregate(aggregate) => {
86            // Preserve full aggregate identity, including FILTER semantics, so
87            // alias-normalized grouped HAVING/ORDER BY terms round-trip back
88            // onto the same planner aggregate expression shape.
89            let kind = aggregate.kind().canonical_label();
90            let distinct = if aggregate.is_distinct() {
91                "DISTINCT "
92            } else {
93                ""
94            };
95            let filter = aggregate.filter_expr().map(|filter_expr| {
96                format!(
97                    " FILTER (WHERE {})",
98                    render_scalar_projection_expr_plan_label_with_parent(filter_expr, None, false,)
99                )
100            });
101
102            if let Some(input_expr) = aggregate.input_expr() {
103                let input =
104                    render_scalar_projection_expr_plan_label_with_parent(input_expr, None, false);
105
106                return format!("{kind}({distinct}{input}){}", filter.unwrap_or_default());
107            }
108
109            format!("{kind}({distinct}*){}", filter.unwrap_or_default())
110        }
111        #[cfg(test)]
112        Expr::Alias { expr, .. } => render_scalar_projection_expr_plan_label_with_parent(
113            expr.as_ref(),
114            parent_op,
115            is_right_child,
116        ),
117        Expr::Unary { op, expr } => {
118            let rendered =
119                render_scalar_projection_expr_plan_label_with_parent(expr.as_ref(), None, false);
120            match op {
121                crate::db::query::plan::expr::UnaryOp::Not => format!("NOT {rendered}"),
122            }
123        }
124    }
125}
126
127fn render_field_path_plan_label(path: &FieldPath) -> String {
128    let mut label = path.root().as_str().to_string();
129    for segment in path.segments() {
130        label.push('.');
131        label.push_str(segment);
132    }
133
134    label
135}
136
137fn render_case_projection_expr_plan_label(
138    when_then_arms: &[crate::db::query::plan::expr::CaseWhenArm],
139    else_expr: &Expr,
140) -> String {
141    let mut rendered = String::from("CASE");
142
143    for arm in when_then_arms {
144        rendered.push_str(" WHEN ");
145        rendered.push_str(
146            render_scalar_projection_expr_plan_label_with_parent(arm.condition(), None, false)
147                .as_str(),
148        );
149        rendered.push_str(" THEN ");
150        rendered.push_str(
151            render_scalar_projection_expr_plan_label_with_parent(arm.result(), None, false)
152                .as_str(),
153        );
154    }
155
156    rendered.push_str(" ELSE ");
157    rendered.push_str(
158        render_scalar_projection_expr_plan_label_with_parent(else_expr, None, false).as_str(),
159    );
160    rendered.push_str(" END");
161
162    rendered
163}
164
165const fn binary_expr_requires_parentheses(
166    op: crate::db::query::plan::expr::BinaryOp,
167    parent_op: Option<crate::db::query::plan::expr::BinaryOp>,
168    is_right_child: bool,
169) -> bool {
170    let Some(parent_op) = parent_op else {
171        return false;
172    };
173    let precedence = binary_op_precedence(op);
174    let parent_precedence = binary_op_precedence(parent_op);
175
176    precedence < parent_precedence || (is_right_child && precedence == parent_precedence)
177}
178
179const fn binary_op_precedence(op: crate::db::query::plan::expr::BinaryOp) -> u8 {
180    match op {
181        crate::db::query::plan::expr::BinaryOp::Or => 0,
182        crate::db::query::plan::expr::BinaryOp::And => 1,
183        crate::db::query::plan::expr::BinaryOp::Eq
184        | crate::db::query::plan::expr::BinaryOp::Ne
185        | crate::db::query::plan::expr::BinaryOp::Lt
186        | crate::db::query::plan::expr::BinaryOp::Lte
187        | crate::db::query::plan::expr::BinaryOp::Gt
188        | crate::db::query::plan::expr::BinaryOp::Gte => 2,
189        crate::db::query::plan::expr::BinaryOp::Add
190        | crate::db::query::plan::expr::BinaryOp::Sub => 3,
191        crate::db::query::plan::expr::BinaryOp::Mul
192        | crate::db::query::plan::expr::BinaryOp::Div => 4,
193    }
194}
195
196const fn binary_op_symbol(op: crate::db::query::plan::expr::BinaryOp) -> &'static str {
197    match op {
198        crate::db::query::plan::expr::BinaryOp::Or => "OR",
199        crate::db::query::plan::expr::BinaryOp::And => "AND",
200        crate::db::query::plan::expr::BinaryOp::Eq => "=",
201        crate::db::query::plan::expr::BinaryOp::Ne => "!=",
202        crate::db::query::plan::expr::BinaryOp::Lt => "<",
203        crate::db::query::plan::expr::BinaryOp::Lte => "<=",
204        crate::db::query::plan::expr::BinaryOp::Gt => ">",
205        crate::db::query::plan::expr::BinaryOp::Gte => ">=",
206        crate::db::query::plan::expr::BinaryOp::Add => "+",
207        crate::db::query::plan::expr::BinaryOp::Sub => "-",
208        crate::db::query::plan::expr::BinaryOp::Mul => "*",
209        crate::db::query::plan::expr::BinaryOp::Div => "/",
210    }
211}
212
213fn render_scalar_projection_literal(value: &Value) -> String {
214    match value {
215        Value::Null => "NULL".to_string(),
216        Value::Text(text) => format!("'{}'", text.replace('\'', "''")),
217        Value::Int(value) => value.to_string(),
218        Value::Int128(value) => value.to_string(),
219        Value::IntBig(value) => value.to_string(),
220        Value::Uint(value) => value.to_string(),
221        Value::Uint128(value) => value.to_string(),
222        Value::UintBig(value) => value.to_string(),
223        Value::Decimal(value) => value.to_string(),
224        Value::Float32(value) => value.to_string(),
225        Value::Float64(value) => value.to_string(),
226        Value::Bool(value) => value.to_string().to_uppercase(),
227        other => format!("{other:?}"),
228    }
229}