Skip to main content

uqa_execution/relational/
project.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Scalar projection and star expansion.
8
9use uqa_core::Value;
10
11use super::{
12    Batch, DefaultExpressionEvaluator, ExecResult, PhysicalOperator, RowSchema, SQLParam,
13    ScalarExpr, SharedExpressionEvaluator,
14};
15use crate::batch::ProjectedSlot;
16
17/// Per-row scalar projection. Each `(alias, expr)` pair is evaluated
18/// against the input row and written under `alias` in the output. The
19/// child schema is replaced with the output aliases.
20pub struct Project<'a> {
21    child: Box<dyn PhysicalOperator + 'a>,
22    computed: Vec<ScalarExpr>,
23    evaluator: SharedExpressionEvaluator<'a>,
24    schema: RowSchema,
25    ordering: Vec<crate::PhysicalOrder>,
26}
27
28fn projection_layout(
29    input: &RowSchema,
30    projections: Vec<(String, ScalarExpr)>,
31    evaluator: &SharedExpressionEvaluator<'_>,
32    pass_through: bool,
33) -> (RowSchema, Vec<ScalarExpr>) {
34    let mut projected = Vec::new();
35    let mut computed = Vec::new();
36    for (name, expression) in projections {
37        if let ScalarExpr::QualifiedStar(qualifier) = &expression {
38            if pass_through {
39                continue;
40            }
41            for (column, logical, _, ty) in input.qualified_star_position_layout(qualifier) {
42                if !evaluator.star_column_visible(&column) {
43                    continue;
44                }
45                let slot = logical
46                    .and_then(|logical| input.physical_slot(logical))
47                    .or_else(|| {
48                        input.physical_slot_for_identity(&crate::ColumnIdentity::qualified(
49                            qualifier, &column,
50                        ))
51                    });
52                projected.push((column, ty, ProjectedSlot::Input(slot)));
53            }
54            continue;
55        }
56        if matches!(expression, ScalarExpr::Star) {
57            if pass_through {
58                continue;
59            }
60            for (logical, column) in input.iter().enumerate() {
61                if !evaluator.star_column_visible(column) {
62                    continue;
63                }
64                projected.push((
65                    input.public_name(logical).unwrap_or(column).to_string(),
66                    input.column_type(logical).cloned(),
67                    ProjectedSlot::Input(input.physical_slot(logical)),
68                ));
69            }
70            continue;
71        }
72
73        let ty = evaluator.expression_type(&expression, input).ok().flatten();
74        if let Some(logical) = crate::order_expression_position(input, &expression) {
75            projected.push((name, ty, ProjectedSlot::Input(input.physical_slot(logical))));
76        } else {
77            projected.push((name, ty, ProjectedSlot::Computed));
78            computed.push(expression);
79        }
80    }
81    (
82        RowSchema::project_with_sources(input, projected, pass_through),
83        computed,
84    )
85}
86
87impl Project<'static> {
88    pub fn new(
89        child: Box<dyn PhysicalOperator>,
90        projections: Vec<(String, ScalarExpr)>,
91        params: Vec<SQLParam>,
92    ) -> Self {
93        Self::with_evaluator(
94            child,
95            projections,
96            DefaultExpressionEvaluator::shared(params),
97        )
98    }
99
100    /// Variant that keeps every input column in the output and appends
101    /// the projections at the end. Used by aggregate / window paths.
102    pub fn appending(
103        child: Box<dyn PhysicalOperator>,
104        projections: Vec<(String, ScalarExpr)>,
105        params: Vec<SQLParam>,
106    ) -> Self {
107        Self::appending_with_evaluator(
108            child,
109            projections,
110            DefaultExpressionEvaluator::shared(params),
111        )
112    }
113}
114
115impl<'a> Project<'a> {
116    pub fn with_evaluator(
117        child: Box<dyn PhysicalOperator + 'a>,
118        projections: Vec<(String, ScalarExpr)>,
119        evaluator: SharedExpressionEvaluator<'a>,
120    ) -> Self {
121        let params = evaluator.parameters();
122        let projections = projections
123            .into_iter()
124            .map(|(name, expression)| {
125                (
126                    name,
127                    crate::bind_type_introspection(expression, child.row_schema(), params),
128                )
129            })
130            .collect::<Vec<_>>();
131        let (schema, computed) =
132            projection_layout(child.row_schema(), projections, &evaluator, false);
133        Self {
134            child,
135            computed,
136            evaluator,
137            schema,
138            ordering: Vec::new(),
139        }
140    }
141
142    pub fn appending_with_evaluator(
143        child: Box<dyn PhysicalOperator + 'a>,
144        projections: Vec<(String, ScalarExpr)>,
145        evaluator: SharedExpressionEvaluator<'a>,
146    ) -> Self {
147        let params = evaluator.parameters();
148        let projections = projections
149            .into_iter()
150            .map(|(name, expression)| {
151                (
152                    name,
153                    crate::bind_type_introspection(expression, child.row_schema(), params),
154                )
155            })
156            .collect::<Vec<_>>();
157        let ordering = child
158            .output_ordering()
159            .iter()
160            .take_while(|order| {
161                projections.iter().all(|(name, expression)| {
162                    child
163                        .row_schema()
164                        .columns()
165                        .iter()
166                        .position(|column| column == name)
167                        != Some(order.position)
168                        || matches!(expression, ScalarExpr::Star | ScalarExpr::QualifiedStar(_))
169                        || crate::order_expression_position(child.row_schema(), expression)
170                            == Some(order.position)
171                })
172            })
173            .cloned()
174            .collect();
175        let (schema, computed) =
176            projection_layout(child.row_schema(), projections, &evaluator, true);
177        Self {
178            child,
179            computed,
180            evaluator,
181            schema,
182            ordering,
183        }
184    }
185}
186
187impl PhysicalOperator for Project<'_> {
188    fn row_schema(&self) -> &RowSchema {
189        &self.schema
190    }
191
192    fn output_ordering(&self) -> &[crate::PhysicalOrder] {
193        &self.ordering
194    }
195
196    fn open(&mut self) -> ExecResult<()> {
197        self.child.open()
198    }
199
200    fn next(&mut self) -> ExecResult<Option<Batch>> {
201        let Some(batch) = self.child.next()? else {
202            return Ok(None);
203        };
204        if self.computed.is_empty() {
205            return Ok(Some(Batch::from_physical_rows(
206                self.schema.clone(),
207                batch.rows,
208            )));
209        }
210        let mut out = Vec::with_capacity(batch.rows.len());
211        for row in batch.rows {
212            let values = self
213                .computed
214                .iter()
215                .map(|expression| {
216                    self.evaluator
217                        .evaluate_physical(expression, &batch.schema, &row)
218                })
219                .collect::<ExecResult<Vec<Value>>>()?;
220            out.push(row.append_values(values));
221        }
222        Ok(Some(Batch::from_physical_rows(self.schema.clone(), out)))
223    }
224
225    fn close(&mut self) -> ExecResult<()> {
226        self.child.close()
227    }
228}
229
230// -------------------------------------------------------------------------
231// Sort
232// -------------------------------------------------------------------------