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    BackwardScanSupport, Batch, DefaultExpressionEvaluator, ExecResult, PhysicalOperator,
13    PhysicalScanDirection, RowSchema, SQLParam, ScalarExpr, SharedExpressionEvaluator,
14};
15use crate::batch::ProjectedSlot;
16
17/// Identity assigned to one projection result. SQL columns participate in
18/// ordinary name binding and wildcard expansion; internal attributes are
19/// executor-only `resjunk` slots addressed structurally.
20#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub enum ProjectionTarget {
22    Column(String),
23    Internal(uqa_sql::ast::InternalColumnRef),
24}
25
26impl From<String> for ProjectionTarget {
27    fn from(value: String) -> Self {
28        Self::Column(value)
29    }
30}
31
32impl From<&str> for ProjectionTarget {
33    fn from(value: &str) -> Self {
34        Self::Column(value.to_string())
35    }
36}
37
38/// Per-row scalar projection. Each `(alias, expr)` pair is evaluated
39/// against the input row and written under `alias` in the output. The
40/// child schema is replaced with the output aliases.
41pub struct Project<'a> {
42    child: Box<dyn PhysicalOperator + 'a>,
43    computed: Vec<ScalarExpr>,
44    evaluator: SharedExpressionEvaluator<'a>,
45    schema: RowSchema,
46    ordering: Vec<crate::PhysicalOrder>,
47}
48
49fn projection_layout(
50    input: &RowSchema,
51    projections: Vec<(ProjectionTarget, ScalarExpr)>,
52    evaluator: &SharedExpressionEvaluator<'_>,
53    pass_through: bool,
54) -> (RowSchema, Vec<ScalarExpr>) {
55    let mut projected = Vec::new();
56    let mut projected_internal = Vec::new();
57    let mut computed = Vec::new();
58    for (target, expression) in projections {
59        if let ScalarExpr::QualifiedStar(qualifier) = &expression {
60            let ProjectionTarget::Column(_) = target else {
61                unreachable!("an internal projection target cannot expand a qualified star");
62            };
63            if pass_through {
64                continue;
65            }
66            for (column, logical, _, ty) in input.qualified_star_position_layout(qualifier) {
67                if logical.is_some_and(|position| !evaluator.star_position_visible(input, position))
68                {
69                    continue;
70                }
71                let slot = logical
72                    .and_then(|logical| input.physical_slot(logical))
73                    .or_else(|| {
74                        input.physical_slot_for_identity(&crate::ColumnIdentity::qualified(
75                            qualifier, &column,
76                        ))
77                    });
78                projected.push((column, ty, ProjectedSlot::Input(slot)));
79            }
80            continue;
81        }
82        if matches!(expression, ScalarExpr::Star) {
83            let ProjectionTarget::Column(_) = target else {
84                unreachable!("an internal projection target cannot expand a star");
85            };
86            if pass_through {
87                continue;
88            }
89            for (logical, column) in input.iter().enumerate() {
90                if !evaluator.star_position_visible(input, logical) {
91                    continue;
92                }
93                projected.push((
94                    input.public_name(logical).unwrap_or(column).to_string(),
95                    input.column_type(logical).cloned(),
96                    ProjectedSlot::Input(input.physical_slot(logical)),
97                ));
98            }
99            continue;
100        }
101
102        let ty = evaluator.expression_type(&expression, input).ok().flatten();
103        let source = if let Some(logical) = crate::order_expression_position(input, &expression) {
104            ProjectedSlot::Input(input.physical_slot(logical))
105        } else {
106            let position = computed.len();
107            computed.push(expression);
108            ProjectedSlot::Computed(position)
109        };
110        match target {
111            ProjectionTarget::Column(name) => projected.push((name, ty, source)),
112            ProjectionTarget::Internal(column) => {
113                projected_internal.push((column, ty, source));
114            }
115        }
116    }
117    let computed_count = computed.len();
118    (
119        RowSchema::project_with_sources(
120            input,
121            projected,
122            projected_internal,
123            computed_count,
124            pass_through,
125        ),
126        computed,
127    )
128}
129
130impl Project<'static> {
131    pub fn new(
132        child: Box<dyn PhysicalOperator>,
133        projections: Vec<(String, ScalarExpr)>,
134        params: Vec<SQLParam>,
135    ) -> Self {
136        Self::with_evaluator(
137            child,
138            projections,
139            DefaultExpressionEvaluator::shared(params),
140        )
141    }
142
143    pub fn with_targets(
144        child: Box<dyn PhysicalOperator>,
145        projections: Vec<(ProjectionTarget, ScalarExpr)>,
146        params: Vec<SQLParam>,
147    ) -> Self {
148        Self::with_target_evaluator(
149            child,
150            projections,
151            DefaultExpressionEvaluator::shared(params),
152        )
153    }
154
155    /// Variant that keeps every input column in the output and appends
156    /// the projections at the end. Used by aggregate / window paths.
157    pub fn appending(
158        child: Box<dyn PhysicalOperator>,
159        projections: Vec<(String, ScalarExpr)>,
160        params: Vec<SQLParam>,
161    ) -> Self {
162        Self::appending_with_evaluator(
163            child,
164            projections,
165            DefaultExpressionEvaluator::shared(params),
166        )
167    }
168
169    pub fn appending_targets(
170        child: Box<dyn PhysicalOperator>,
171        projections: Vec<(ProjectionTarget, ScalarExpr)>,
172        params: Vec<SQLParam>,
173    ) -> Self {
174        Self::appending_target_evaluator(
175            child,
176            projections,
177            DefaultExpressionEvaluator::shared(params),
178        )
179    }
180}
181
182impl<'a> Project<'a> {
183    pub fn with_evaluator(
184        child: Box<dyn PhysicalOperator + 'a>,
185        projections: Vec<(String, ScalarExpr)>,
186        evaluator: SharedExpressionEvaluator<'a>,
187    ) -> Self {
188        Self::with_target_evaluator(
189            child,
190            projections
191                .into_iter()
192                .map(|(name, expression)| (ProjectionTarget::Column(name), expression))
193                .collect(),
194            evaluator,
195        )
196    }
197
198    pub fn with_target_evaluator(
199        child: Box<dyn PhysicalOperator + 'a>,
200        projections: Vec<(ProjectionTarget, ScalarExpr)>,
201        evaluator: SharedExpressionEvaluator<'a>,
202    ) -> Self {
203        let projections = projections
204            .into_iter()
205            .map(|(target, expression)| {
206                (
207                    target,
208                    evaluator.bind_type_introspection(expression, child.row_schema()),
209                )
210            })
211            .collect::<Vec<_>>();
212        let (schema, computed) =
213            projection_layout(child.row_schema(), projections, &evaluator, false);
214        Self {
215            child,
216            computed,
217            evaluator,
218            schema,
219            ordering: Vec::new(),
220        }
221    }
222
223    pub fn appending_with_evaluator(
224        child: Box<dyn PhysicalOperator + 'a>,
225        projections: Vec<(String, ScalarExpr)>,
226        evaluator: SharedExpressionEvaluator<'a>,
227    ) -> Self {
228        Self::appending_target_evaluator(
229            child,
230            projections
231                .into_iter()
232                .map(|(name, expression)| (ProjectionTarget::Column(name), expression))
233                .collect(),
234            evaluator,
235        )
236    }
237
238    pub fn appending_target_evaluator(
239        child: Box<dyn PhysicalOperator + 'a>,
240        projections: Vec<(ProjectionTarget, ScalarExpr)>,
241        evaluator: SharedExpressionEvaluator<'a>,
242    ) -> Self {
243        let projections = projections
244            .into_iter()
245            .map(|(target, expression)| {
246                (
247                    target,
248                    evaluator.bind_type_introspection(expression, child.row_schema()),
249                )
250            })
251            .collect::<Vec<_>>();
252        let ordering = child
253            .output_ordering()
254            .iter()
255            .take_while(|order| {
256                projections.iter().all(|(target, expression)| {
257                    let ProjectionTarget::Column(name) = target else {
258                        return true;
259                    };
260                    child
261                        .row_schema()
262                        .columns()
263                        .iter()
264                        .position(|column| column == name)
265                        != Some(order.position)
266                        || matches!(expression, ScalarExpr::Star | ScalarExpr::QualifiedStar(_))
267                        || crate::order_expression_position(child.row_schema(), expression)
268                            == Some(order.position)
269                })
270            })
271            .cloned()
272            .collect();
273        let (schema, computed) =
274            projection_layout(child.row_schema(), projections, &evaluator, true);
275        Self {
276            child,
277            computed,
278            evaluator,
279            schema,
280            ordering,
281        }
282    }
283
284    fn project_batch(&self, batch: Batch) -> ExecResult<Batch> {
285        if self.computed.is_empty() {
286            return Ok(Batch::from_physical_rows(self.schema.clone(), batch.rows));
287        }
288        let mut out = Vec::with_capacity(batch.rows.len());
289        for row in batch.rows {
290            let values = self
291                .computed
292                .iter()
293                .map(|expression| {
294                    self.evaluator
295                        .evaluate_physical(expression, &batch.schema, &row)
296                })
297                .collect::<ExecResult<Vec<Value>>>()?;
298            out.push(row.append_values(values));
299        }
300        Ok(Batch::from_physical_rows(self.schema.clone(), out))
301    }
302}
303
304impl PhysicalOperator for Project<'_> {
305    fn row_schema(&self) -> &RowSchema {
306        &self.schema
307    }
308
309    fn output_ordering(&self) -> &[crate::PhysicalOrder] {
310        &self.ordering
311    }
312
313    fn backward_scan_support(&self) -> BackwardScanSupport {
314        let child = self.child.backward_scan_support();
315        if child == BackwardScanSupport::Native || self.computed.is_empty() {
316            child
317        } else {
318            BackwardScanSupport::Unsupported
319        }
320    }
321
322    fn open(&mut self) -> ExecResult<()> {
323        self.child.open()
324    }
325
326    fn next(&mut self) -> ExecResult<Option<Batch>> {
327        let Some(batch) = self.child.next()? else {
328            return Ok(None);
329        };
330        self.project_batch(batch).map(Some)
331    }
332
333    fn next_direction(&mut self, direction: PhysicalScanDirection) -> ExecResult<Option<Batch>> {
334        let Some(batch) = self.child.next_direction(direction)? else {
335            return Ok(None);
336        };
337        self.project_batch(batch).map(Some)
338    }
339
340    fn rewind(&mut self) -> ExecResult<()> {
341        self.child.rewind()
342    }
343
344    fn close(&mut self) -> ExecResult<()> {
345        self.child.close()
346    }
347}
348
349// -------------------------------------------------------------------------
350// Sort
351// -------------------------------------------------------------------------