Skip to main content

uqa_sql/semantics/
projection.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Projection labels, star expansion, and DML row-image schemas.
8
9use super::{DOC_ID_COLUMN, TABLE_OID_COLUMN};
10use crate::ast::ReturningAliases;
11use crate::plan::ProjectionPlan;
12use crate::{ColumnIdentity, RowSchema, SQLError, ScalarExpr};
13
14pub fn projection_columns(projections: &[ProjectionPlan]) -> Vec<String> {
15    projections.iter().map(projection_label_at).collect()
16}
17
18/// Compute a projection's `PostgreSQL` output column name. Standalone expressions use `?column?`; repeated labels remain repeated until the final named-map compatibility boundary.
19pub fn projection_label_at(proj: &ProjectionPlan) -> String {
20    if let Some(a) = &proj.alias {
21        return a.clone();
22    }
23    match &proj.expr {
24        ScalarExpr::Column(c) => c.clone(),
25        ScalarExpr::QualifiedColumn { column, .. } => column.clone(),
26        ScalarExpr::Star | ScalarExpr::QualifiedStar(_) => "*".into(),
27        ScalarExpr::Func { name, binding, .. } => function_projection_label(name, binding.as_ref()),
28        _ => "?column?".into(),
29    }
30}
31
32pub(crate) fn function_projection_label(
33    name: &str,
34    binding: Option<&crate::ast::FunctionBinding>,
35) -> String {
36    if matches!(
37        binding.and_then(|binding| binding.dispatch),
38        Some(crate::ast::FunctionDispatch::NumericOperator(_))
39    ) {
40        return "?column?".into();
41    }
42    crate::parse_regobject_name(name)
43        .and_then(|mut names| names.pop())
44        .unwrap_or_else(|| name.to_string())
45}
46
47pub fn expand_from_star_columns(
48    columns: Vec<String>,
49    projections: &[ProjectionPlan],
50    source_schema: &RowSchema,
51) -> Result<Vec<String>, SQLError> {
52    let mut output = Vec::new();
53    for (position, projection) in projections.iter().enumerate() {
54        match &projection.expr {
55            ScalarExpr::Star => {
56                output.extend(
57                    source_schema
58                        .columns()
59                        .iter()
60                        .enumerate()
61                        .filter(|(position, _)| {
62                            visible_projection_source_position(source_schema, *position)
63                        })
64                        .map(|(source_position, column)| {
65                            source_schema
66                                .public_name(source_position)
67                                .unwrap_or(column)
68                                .to_string()
69                        }),
70                );
71            }
72            ScalarExpr::QualifiedStar(qualifier) => {
73                let qualified_columns = source_schema
74                    .qualified_star_position_layout(qualifier)
75                    .into_iter()
76                    .filter(|(_, logical, _, _)| {
77                        logical.is_none_or(|position| {
78                            visible_projection_source_position(source_schema, position)
79                        })
80                    })
81                    .map(|(column, _, _, _)| column)
82                    .collect::<Vec<_>>();
83                if qualified_columns.is_empty() {
84                    return Err(SQLError::UnknownTable(qualifier.clone()));
85                }
86                output.extend(qualified_columns);
87            }
88            _ => output.push(columns[position].clone()),
89        }
90    }
91    Ok(output)
92}
93
94pub fn bound_projection_expression(schema: &RowSchema, position: usize) -> ScalarExpr {
95    let Some(identity) = schema.identity(position) else {
96        return ScalarExpr::Position(position);
97    };
98    if let Some(qualifier) = identity.qualifier() {
99        if schema.qualified_position(qualifier, identity.column()) == Some(position) {
100            return ScalarExpr::qualified_column(qualifier, identity.column());
101        }
102    } else if schema.unqualified_position(identity.column()) == Some(position) {
103        return ScalarExpr::Column(identity.column().to_string());
104    }
105    ScalarExpr::Position(position)
106}
107
108pub fn expand_bound_projection_stars(
109    projections: &[ProjectionPlan],
110    schema: &RowSchema,
111) -> Result<Vec<ProjectionPlan>, SQLError> {
112    let mut expanded = Vec::new();
113    for projection in projections {
114        match &projection.expr {
115            ScalarExpr::Star => {
116                for (position, column) in schema.columns().iter().enumerate() {
117                    if !visible_projection_source_position(schema, position) {
118                        continue;
119                    }
120                    expanded.push(ProjectionPlan {
121                        expr: bound_projection_expression(schema, position),
122                        alias: Some(schema.public_name(position).unwrap_or(column).to_string()),
123                    });
124                }
125            }
126            ScalarExpr::QualifiedStar(qualifier) => {
127                let layout = schema.qualified_star_position_layout(qualifier);
128                if layout.is_empty() {
129                    return Err(SQLError::UnknownTable(qualifier.clone()));
130                }
131                for (column, logical, _, _) in layout {
132                    if logical.is_some_and(|position| {
133                        !visible_projection_source_position(schema, position)
134                    }) {
135                        continue;
136                    }
137                    expanded.push(ProjectionPlan {
138                        expr: logical.map_or_else(
139                            || ScalarExpr::qualified_column(qualifier, &column),
140                            |position| bound_projection_expression(schema, position),
141                        ),
142                        alias: Some(column),
143                    });
144                }
145            }
146            _ => expanded.push(projection.clone()),
147        }
148    }
149    Ok(expanded)
150}
151
152pub fn visible_projection_source_position(schema: &RowSchema, position: usize) -> bool {
153    schema.wildcard_position_visible(position)
154}
155
156pub fn returning_context_schema(
157    columns: &[String],
158    types: &[Option<crate::ast::ColumnType>],
159    composite_width: usize,
160    target_qualifier: &str,
161    aliases: &ReturningAliases,
162) -> RowSchema {
163    let target =
164        RowSchema::with_qualified_types(target_qualifier, columns.to_vec(), types.to_vec());
165    let target = RowSchema::with_wildcard_hidden_positions(&target, composite_width..columns.len());
166    let hidden_types = types
167        .iter()
168        .cloned()
169        .chain(types.iter().cloned())
170        .collect::<Vec<_>>();
171    let schema = RowSchema::append_hidden_typed(&target, &hidden_types);
172    let width = columns.len();
173    let identity_aliases = columns
174        .iter()
175        .enumerate()
176        .flat_map(|(position, column)| {
177            [
178                (
179                    ColumnIdentity::qualified(&aliases.old, column),
180                    width + position,
181                    types[position].clone(),
182                ),
183                (
184                    ColumnIdentity::qualified(&aliases.new, column),
185                    width * 2 + position,
186                    types[position].clone(),
187                ),
188            ]
189        })
190        .collect::<Vec<_>>();
191    RowSchema::with_physical_identity_aliases(&schema, &identity_aliases)
192}
193
194pub fn returning_expression_schema(
195    target: &RowSchema,
196    target_qualifier: &str,
197    aliases: &ReturningAliases,
198    supplemental: Option<&RowSchema>,
199) -> RowSchema {
200    let composite_width = target.len();
201    let mut columns = target.columns().to_vec();
202    let mut types = target.column_types().to_vec();
203    if !columns.iter().any(|column| column == DOC_ID_COLUMN) {
204        columns.push(DOC_ID_COLUMN.into());
205        types.push(Some(crate::ast::ColumnType::BigInteger));
206    }
207    columns.push(TABLE_OID_COLUMN.into());
208    types.push(Some(crate::ast::ColumnType::Oid));
209    columns.push(crate::semantics::XMIN_COLUMN.into());
210    types.push(Some(crate::ast::ColumnType::Xid));
211    let target =
212        returning_context_schema(&columns, &types, composite_width, target_qualifier, aliases);
213    supplemental.map_or(target.clone(), |source| {
214        RowSchema::join(&target, source, std::iter::empty())
215    })
216}
217
218pub fn query_plan_output_columns(plan: &crate::plan::QueryPlan) -> Option<Vec<String>> {
219    match &plan.root {
220        crate::plan::RelationalPlan::QueryBlock(block) => {
221            Some(projection_columns(&block.projections))
222        }
223        crate::plan::RelationalPlan::SetOp { left, .. } => query_plan_output_columns(left),
224        crate::plan::RelationalPlan::Values { rows, .. } => rows.first().map(|row| {
225            (1..=row.len())
226                .map(|index| format!("column{index}"))
227                .collect()
228        }),
229    }
230}
231
232pub fn should_defer_distinct_limit(stmt: &crate::plan::QueryBlockPlan) -> bool {
233    stmt.distinct && (stmt.limit.is_some() || stmt.offset.is_some())
234}
235
236pub fn select_execution_stmt(
237    stmt: &crate::plan::QueryBlockPlan,
238    defer_distinct_limit: bool,
239) -> crate::plan::QueryBlockPlan {
240    if !defer_distinct_limit {
241        return stmt.clone();
242    }
243    let mut exec_stmt = stmt.clone();
244    exec_stmt.limit = None;
245    exec_stmt.offset = None;
246    exec_stmt
247}