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