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/// Resolve an SQL92 output ordinal against the expanded SELECT list before using its type or physical expression.
19pub fn query_output_position(position: i64, width: usize, clause: &str) -> Result<usize, SQLError> {
20    usize::try_from(position)
21        .ok()
22        .and_then(|position| position.checked_sub(1))
23        .filter(|position| *position < width)
24        .ok_or_else(|| output_position_error(clause, position))
25}
26
27pub fn output_position_error(clause: &str, position: i64) -> SQLError {
28    SQLError::Routine {
29        sqlstate: "42P10".into(),
30        message: format!("{clause} position {position} is not in the select list"),
31    }
32}
33
34/// Compute a projection's `PostgreSQL` output column name. Standalone expressions use `?column?`; repeated labels remain repeated until the final named-map compatibility boundary.
35pub fn projection_label_at(proj: &ProjectionPlan) -> String {
36    if let Some(a) = &proj.alias {
37        return a.clone();
38    }
39    match &proj.expr {
40        ScalarExpr::Column(c) => c.clone(),
41        ScalarExpr::QualifiedColumn { column, .. } => column.clone(),
42        ScalarExpr::Star | ScalarExpr::QualifiedStar(_) => "*".into(),
43        ScalarExpr::Func {
44            name,
45            binding,
46            args,
47            ..
48        } => field_selection_label(binding.as_ref(), args.get(1))
49            .unwrap_or_else(|| function_projection_label(name, binding.as_ref())),
50        ScalarExpr::WindowCall { name, .. } => function_projection_label(name, None),
51        _ => "?column?".into(),
52    }
53}
54
55/// `FigureColname` names a field selection by its field.
56pub(crate) fn field_selection_label<E: FieldNameLiteral>(
57    binding: Option<&crate::ast::FunctionBinding>,
58    field: Option<&E>,
59) -> Option<String> {
60    (binding.and_then(|binding| binding.dispatch)
61        == Some(crate::ast::FunctionDispatch::FieldSelect))
62    .then(|| field.and_then(FieldNameLiteral::field_name))
63    .flatten()
64    .map(str::to_owned)
65}
66
67/// The field name literal of a field selection in parsed or planned expressions.
68pub(crate) trait FieldNameLiteral {
69    fn field_name(&self) -> Option<&str>;
70}
71
72impl FieldNameLiteral for ScalarExpr {
73    fn field_name(&self) -> Option<&str> {
74        match self {
75            ScalarExpr::Literal(uqa_core::Value::Str(field)) => Some(field),
76            _ => None,
77        }
78    }
79}
80
81impl FieldNameLiteral for crate::ast::Expr {
82    fn field_name(&self) -> Option<&str> {
83        match self {
84            crate::ast::Expr::Literal(uqa_core::Value::Str(field)) => Some(field),
85            _ => None,
86        }
87    }
88}
89
90pub fn function_projection_label(
91    name: &str,
92    binding: Option<&crate::ast::FunctionBinding>,
93) -> String {
94    // Operators and other syntax that the parser lowers to calls have no name of their own.
95    if matches!(
96        binding.and_then(|binding| binding.dispatch),
97        Some(
98            crate::ast::FunctionDispatch::NumericOperator(_)
99                | crate::ast::FunctionDispatch::ArrayConcat
100                | crate::ast::FunctionDispatch::ArrayAppend
101                | crate::ast::FunctionDispatch::ArrayPrepend
102                | crate::ast::FunctionDispatch::IsDistinct
103                | crate::ast::FunctionDispatch::AnyOperator
104                | crate::ast::FunctionDispatch::AllOperator
105                | crate::ast::FunctionDispatch::BetweenSymmetric
106                | crate::ast::FunctionDispatch::JsonExtract { .. }
107        )
108    ) {
109        return "?column?".into();
110    }
111    let name = crate::parse_regobject_name(name)
112        .and_then(|mut names| names.pop())
113        .unwrap_or_else(|| name.to_string());
114    if operator_call(&name) {
115        return "?column?".into();
116    }
117    name
118}
119
120/// The calls the parser lowers operator syntax to under names no `PostgreSQL` function has: `||`, `@@`, `@?`, `@>`, `<@`, `&&`, `?`, `?|`, `?&`, `#-`, `~`, `~*`, `LIKE`, `ILIKE` and `SIMILAR TO`.
121fn operator_call(name: &str) -> bool {
122    matches!(
123        name,
124        "concat_op"
125            | "fts_match"
126            | "jsonpath_exists"
127            | "contains_op"
128            | "contained_by_op"
129            | "array_overlap"
130            | "json_has_key"
131            | "json_has_any_key"
132            | "json_has_all_keys"
133            | "json_delete_path"
134            | "like"
135            | "ilike"
136            | "similar_to"
137            | "regex_match_op"
138            | "regex_imatch_op"
139    )
140}
141
142pub fn expand_from_star_columns(
143    columns: Vec<String>,
144    projections: &[ProjectionPlan],
145    source_schema: &RowSchema,
146) -> Result<Vec<String>, SQLError> {
147    let mut output = Vec::new();
148    for (position, projection) in projections.iter().enumerate() {
149        match &projection.expr {
150            ScalarExpr::Star => {
151                output.extend(
152                    source_schema
153                        .columns()
154                        .iter()
155                        .enumerate()
156                        .filter(|(position, _)| {
157                            visible_projection_source_position(source_schema, *position)
158                        })
159                        .map(|(source_position, column)| {
160                            source_schema
161                                .public_name(source_position)
162                                .unwrap_or(column)
163                                .to_string()
164                        }),
165                );
166            }
167            ScalarExpr::QualifiedStar(qualifier) => {
168                let qualified_columns = source_schema
169                    .qualified_star_position_layout(qualifier)
170                    .into_iter()
171                    .filter(|(_, logical, _, _)| {
172                        logical.is_none_or(|position| {
173                            visible_projection_source_position(source_schema, position)
174                        })
175                    })
176                    .map(|(column, _, _, _)| column)
177                    .collect::<Vec<_>>();
178                if qualified_columns.is_empty() {
179                    return Err(SQLError::UnknownTable(qualifier.clone()));
180                }
181                output.extend(qualified_columns);
182            }
183            _ => output.push(columns[position].clone()),
184        }
185    }
186    Ok(output)
187}
188
189pub fn bound_projection_expression(schema: &RowSchema, position: usize) -> ScalarExpr {
190    let Some(identity) = schema.identity(position) else {
191        return ScalarExpr::Position(position);
192    };
193    if let Some(qualifier) = identity.qualifier() {
194        if schema.qualified_position(qualifier, identity.column()) == Some(position) {
195            return ScalarExpr::qualified_column(qualifier, identity.column());
196        }
197    } else if schema.unqualified_position(identity.column()) == Some(position) {
198        return ScalarExpr::Column(identity.column().to_string());
199    }
200    ScalarExpr::Position(position)
201}
202
203pub fn expand_bound_projection_stars(
204    projections: &[ProjectionPlan],
205    schema: &RowSchema,
206) -> Result<Vec<ProjectionPlan>, SQLError> {
207    let mut expanded = Vec::new();
208    for projection in projections {
209        match &projection.expr {
210            ScalarExpr::Star => {
211                for (position, column) in schema.columns().iter().enumerate() {
212                    if !visible_projection_source_position(schema, position) {
213                        continue;
214                    }
215                    expanded.push(ProjectionPlan {
216                        expr: bound_projection_expression(schema, position),
217                        alias: Some(schema.public_name(position).unwrap_or(column).to_string()),
218                    });
219                }
220            }
221            ScalarExpr::QualifiedStar(qualifier) => {
222                let layout = schema.qualified_star_position_layout(qualifier);
223                if layout.is_empty() {
224                    return Err(SQLError::UnknownTable(qualifier.clone()));
225                }
226                for (column, logical, _, _) in layout {
227                    if logical.is_some_and(|position| {
228                        !visible_projection_source_position(schema, position)
229                    }) {
230                        continue;
231                    }
232                    expanded.push(ProjectionPlan {
233                        expr: logical.map_or_else(
234                            || ScalarExpr::qualified_column(qualifier, &column),
235                            |position| bound_projection_expression(schema, position),
236                        ),
237                        alias: Some(column),
238                    });
239                }
240            }
241            _ => expanded.push(projection.clone()),
242        }
243    }
244    Ok(expanded)
245}
246
247pub fn visible_projection_source_position(schema: &RowSchema, position: usize) -> bool {
248    schema.wildcard_position_visible(position)
249}
250
251pub fn returning_context_schema(
252    columns: &[String],
253    types: &[Option<crate::ast::ColumnType>],
254    composite_width: usize,
255    target_qualifier: &str,
256    aliases: &ReturningAliases,
257) -> RowSchema {
258    let target =
259        RowSchema::with_qualified_types(target_qualifier, columns.to_vec(), types.to_vec());
260    let target = RowSchema::with_wildcard_hidden_positions(&target, composite_width..columns.len());
261    let hidden_types = types
262        .iter()
263        .cloned()
264        .chain(types.iter().cloned())
265        .collect::<Vec<_>>();
266    let schema = RowSchema::append_hidden_typed(&target, &hidden_types);
267    let width = columns.len();
268    let identity_aliases = columns
269        .iter()
270        .enumerate()
271        .flat_map(|(position, column)| {
272            [
273                (
274                    ColumnIdentity::qualified(&aliases.old, column),
275                    width + position,
276                    types[position].clone(),
277                ),
278                (
279                    ColumnIdentity::qualified(&aliases.new, column),
280                    width * 2 + position,
281                    types[position].clone(),
282                ),
283            ]
284        })
285        .collect::<Vec<_>>();
286    RowSchema::with_physical_identity_aliases(&schema, &identity_aliases)
287}
288
289pub fn returning_expression_schema(
290    target: &RowSchema,
291    target_qualifier: &str,
292    aliases: &ReturningAliases,
293    supplemental: Option<&RowSchema>,
294) -> RowSchema {
295    let composite_width = target.len();
296    let (columns, types) = target_with_system_columns(target);
297    let target =
298        returning_context_schema(&columns, &types, composite_width, target_qualifier, aliases);
299    supplemental.map_or(target.clone(), |source| {
300        RowSchema::join(&target, source, std::iter::empty())
301    })
302}
303
304/// The row scope of the clauses of a data-modifying statement other than `RETURNING`: the target, named by its name or alias, with its system columns, and the statement's other sources. The `old` and `new` aliases of the target belong to `RETURNING` alone.
305#[must_use]
306pub fn mutation_clause_schema(
307    target: &RowSchema,
308    target_qualifier: &str,
309    supplemental: Option<&RowSchema>,
310) -> RowSchema {
311    let composite_width = target.len();
312    let (columns, types) = target_with_system_columns(target);
313    let width = columns.len();
314    let target = RowSchema::with_wildcard_hidden_positions(
315        &RowSchema::with_qualified_types(target_qualifier, columns, types),
316        composite_width..width,
317    );
318    supplemental.map_or(target.clone(), |source| {
319        RowSchema::join(&target, source, std::iter::empty())
320    })
321}
322
323/// The target's columns followed by the system columns a statement can name on it.
324fn target_with_system_columns(
325    target: &RowSchema,
326) -> (Vec<String>, Vec<Option<crate::ast::ColumnType>>) {
327    let mut columns = target.columns().to_vec();
328    let mut types = target.column_types().to_vec();
329    if !columns.iter().any(|column| column == DOC_ID_COLUMN) {
330        columns.push(DOC_ID_COLUMN.into());
331        types.push(Some(crate::ast::ColumnType::BigInteger));
332    }
333    columns.push(TABLE_OID_COLUMN.into());
334    types.push(Some(crate::ast::ColumnType::Oid));
335    columns.push(crate::semantics::XMIN_COLUMN.into());
336    types.push(Some(crate::ast::ColumnType::Xid));
337    (columns, types)
338}
339
340pub fn query_plan_output_columns(plan: &crate::plan::QueryPlan) -> Option<Vec<String>> {
341    match &plan.root {
342        crate::plan::RelationalPlan::QueryBlock(block) => {
343            Some(projection_columns(&block.projections))
344        }
345        crate::plan::RelationalPlan::SetOp { left, .. } => query_plan_output_columns(left),
346        crate::plan::RelationalPlan::Values { rows, .. } => rows.first().map(|row| {
347            (1..=row.len())
348                .map(|index| format!("column{index}"))
349                .collect()
350        }),
351    }
352}
353
354pub fn should_defer_distinct_limit(stmt: &crate::plan::QueryBlockPlan) -> bool {
355    stmt.distinct && (stmt.limit.is_some() || stmt.offset.is_some())
356}
357
358pub fn select_execution_stmt(
359    stmt: &crate::plan::QueryBlockPlan,
360    defer_distinct_limit: bool,
361) -> crate::plan::QueryBlockPlan {
362    if !defer_distinct_limit {
363        return stmt.clone();
364    }
365    let mut exec_stmt = stmt.clone();
366    exec_stmt.limit = None;
367    exec_stmt.offset = None;
368    exec_stmt
369}