Skip to main content

sochdb_query/executor/
project.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3//! Project operator (column selection and expression evaluation).
4
5use crate::sql::ast::Expr;
6use super::eval::eval_expr;
7use super::node::PlanNode;
8use super::types::{Row, Schema, ColumnMeta};
9use sochdb_core::Result;
10
11/// Projection expression: an expression and its output alias.
12pub struct ProjectExpr {
13    pub expr: Expr,
14    pub alias: String,
15}
16
17/// Project operator: evaluates expressions to produce a new row shape.
18///
19/// ```text
20/// Project(exprs=[a, b+1 AS c])
21///   └── input
22/// ```
23pub struct ProjectNode {
24    input: Box<dyn PlanNode>,
25    exprs: Vec<ProjectExpr>,
26    output_schema: Schema,
27}
28
29impl ProjectNode {
30    pub fn new(input: Box<dyn PlanNode>, exprs: Vec<ProjectExpr>) -> Self {
31        let output_schema = Schema::new(
32            exprs.iter().map(|e| ColumnMeta::new(e.alias.clone())).collect(),
33        );
34        Self { input, exprs, output_schema }
35    }
36
37    /// Create a simple column-selection projection (no expressions).
38    pub fn columns(input: Box<dyn PlanNode>, columns: Vec<String>) -> Self {
39        let input_schema = input.schema().clone();
40        let exprs: Vec<ProjectExpr> = columns
41            .iter()
42            .map(|c| {
43                ProjectExpr {
44                    expr: Expr::Column(crate::sql::ast::ColumnRef::new(c.clone())),
45                    alias: c.clone(),
46                }
47            })
48            .collect();
49        let output_schema = Schema::new(
50            columns.iter().map(|c| {
51                // Preserve table qualification from input schema
52                input_schema
53                    .columns
54                    .iter()
55                    .find(|cm| cm.name == *c)
56                    .cloned()
57                    .unwrap_or_else(|| ColumnMeta::new(c.clone()))
58            }).collect()
59        );
60        Self { input, exprs, output_schema }
61    }
62}
63
64impl PlanNode for ProjectNode {
65    fn schema(&self) -> &Schema {
66        &self.output_schema
67    }
68
69    fn next(&mut self) -> Result<Option<Row>> {
70        match self.input.next()? {
71            Some(row) => {
72                let input_schema = self.input.schema();
73                let mut output = Vec::with_capacity(self.exprs.len());
74                for pe in &self.exprs {
75                    let val = eval_expr(&pe.expr, &row, input_schema)?;
76                    output.push(val);
77                }
78                Ok(Some(output))
79            }
80            None => Ok(None),
81        }
82    }
83
84    fn reset(&mut self) -> Result<()> {
85        self.input.reset()
86    }
87}
88
89/// Wildcard expansion: pass-through all columns.
90pub struct PassThroughNode {
91    input: Box<dyn PlanNode>,
92}
93
94impl PassThroughNode {
95    pub fn new(input: Box<dyn PlanNode>) -> Self {
96        Self { input }
97    }
98}
99
100impl PlanNode for PassThroughNode {
101    fn schema(&self) -> &Schema {
102        self.input.schema()
103    }
104
105    fn next(&mut self) -> Result<Option<Row>> {
106        self.input.next()
107    }
108
109    fn reset(&mut self) -> Result<()> {
110        self.input.reset()
111    }
112}