Skip to main content

uqa_sql/binding/
commands.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Static RETURNING schemas for command CTEs.
8
9use super::{
10    projection_star_columns, ColumnType, RowSchema, SQLError, SQLParam, ScalarExpr, SchemaScope,
11    SourcePlan,
12};
13use crate::plan::{CommandPlan, CtePlanBody};
14use crate::routines::RoutineResolution;
15
16pub fn analyze_prepared_command_schema(
17    routines: &dyn RoutineResolution,
18    command: &CommandPlan,
19    params: &[SQLParam],
20    ctes: &super::BindingContext,
21) -> Result<Option<RowSchema>, SQLError> {
22    if command.mutation_target().is_none() {
23        return Ok(None);
24    }
25    let mut scope = SchemaScope::for_analysis(ctes)?;
26    scope.set_command_lookup_mode(command);
27    let previous = scope.bind_cte_schemas(routines, command.ctes(), params, None)?;
28    let result = (|| {
29        let (target, expression, _) =
30            scope.command_expression_schema(routines, command, params, None)?;
31        let excluded = RowSchema::with_relation_qualifier(&target, "excluded");
32        let conflict_input = RowSchema::join(&expression, &excluded, std::iter::empty::<String>());
33        let conflict_expressions = match command {
34            CommandPlan::Insert(insert) => insert
35                .on_conflict
36                .as_ref()
37                .map(|conflict| match &conflict.action {
38                    crate::plan::ConflictActionPlan::Update {
39                        assignments,
40                        predicate,
41                    } => assignments
42                        .iter()
43                        .flat_map(crate::plan::AssignmentPlan::expressions)
44                        .chain(predicate.as_deref())
45                        .collect::<Vec<_>>(),
46                    crate::plan::ConflictActionPlan::Nothing => Vec::new(),
47                })
48                .unwrap_or_default(),
49            _ => Vec::new(),
50        };
51        if let CommandPlan::Insert(insert) = command {
52            if let Some(source) = &insert.source {
53                scope.bind_query(routines, source, params, None)?;
54            }
55        }
56        for scalar in command.expressions() {
57            if matches!(command, CommandPlan::Merge(_)) {
58                continue;
59            }
60            scope.bind_expression_type(
61                routines,
62                scalar,
63                if conflict_expressions
64                    .iter()
65                    .any(|candidate| std::ptr::eq(*candidate, scalar))
66                {
67                    &conflict_input
68                } else {
69                    &expression
70                },
71                command.scalar_subqueries(),
72                params,
73            )?;
74        }
75        if let CommandPlan::Merge(merge) = command {
76            scope.bind_merge_expressions(routines, merge, params, &target, &expression)?;
77        }
78        let result = scope.bind_command_returning(routines, command, params)?;
79        Ok(command
80            .returning()
81            .filter(|returning| !returning.is_empty())
82            .map(|_| result))
83    })();
84    scope.restore_cte_schemas(previous);
85    result
86}
87
88impl SchemaScope {
89    pub(super) fn set_command_lookup_mode(&mut self, command: &CommandPlan) {
90        let bound = match command {
91            CommandPlan::Insert(plan) => plan.relations_bound,
92            CommandPlan::Update(plan) => plan.relations_bound,
93            CommandPlan::Delete(plan) => plan.relations_bound,
94            _ => false,
95        };
96        self.resolution.set_lookup_mode(if bound {
97            crate::catalog::resolution::RelationLookupMode::Bound
98        } else {
99            crate::catalog::resolution::RelationLookupMode::Dynamic
100        });
101    }
102
103    pub(super) fn bind_cte_body(
104        &mut self,
105        routines: &dyn RoutineResolution,
106        body: &CtePlanBody,
107        params: &[SQLParam],
108        outer: Option<&RowSchema>,
109    ) -> Result<RowSchema, SQLError> {
110        match body {
111            CtePlanBody::Query(query) => self.bind_query(routines, query, params, outer),
112            CtePlanBody::Command(command) => self.bind_command_returning(routines, command, params),
113        }
114    }
115
116    pub(super) fn bind_command_target(
117        &mut self,
118        routines: &dyn RoutineResolution,
119        command: &CommandPlan,
120        params: &[SQLParam],
121    ) -> Result<RowSchema, SQLError> {
122        let table = command
123            .mutation_target()
124            .ok_or_else(|| SQLError::Internal("non-DML command in a WITH definition".into()))?;
125        let source = SourcePlan::Table {
126            bound_columns: None,
127            name: table.to_string(),
128            qualifier: command.target_qualifier().unwrap_or(table).to_string(),
129            alias: None,
130            column_aliases: Vec::new(),
131            include_descendants: true,
132        };
133        // A mutation target always denotes a catalog relation, even when a CTE shadows its name.
134        let previous = std::mem::take(&mut self.ctes);
135        let deferred = std::mem::take(&mut self.deferred_ctes);
136        let bound = match command {
137            CommandPlan::Insert(plan) => plan.target_relation_bound,
138            CommandPlan::Update(plan) => plan.target_relation_bound,
139            CommandPlan::Delete(plan) => plan.target_relation_bound,
140            _ => false,
141        };
142        let lookup = bound.then(|| {
143            self.resolution
144                .set_lookup_mode(crate::catalog::resolution::RelationLookupMode::Bound)
145        });
146        let result = self.bind_source(routines, &source, &[], params, None);
147        if let Some(lookup) = lookup {
148            self.resolution.set_lookup_mode(lookup);
149        }
150        self.ctes = previous;
151        self.deferred_ctes = deferred;
152        let target = result?;
153        let columns = target
154            .columns()
155            .iter()
156            .enumerate()
157            .map(|(position, column)| target.public_name(position).unwrap_or(column).to_string())
158            .collect();
159        let schema = RowSchema::with_types(columns, target.column_types().to_vec())
160            .with_column_collations(
161                (0..target.len()).map(|position| target.column_collation(position)),
162            );
163        Ok(if target.columns_are_open(None) {
164            RowSchema::with_open_columns(&schema, None)
165        } else {
166            schema
167        })
168    }
169
170    pub(super) fn command_expression_schema(
171        &mut self,
172        routines: &dyn RoutineResolution,
173        command: &CommandPlan,
174        params: &[SQLParam],
175        outer: Option<&RowSchema>,
176    ) -> Result<(RowSchema, RowSchema, RowSchema), SQLError> {
177        let target = self.bind_command_target(routines, command, params)?;
178        let source = command
179            .source_input()
180            .map(|source| {
181                self.bind_source(routines, source, command.scalar_subqueries(), params, outer)
182            })
183            .transpose()?;
184        let expression = Self::command_input_schema(command, &target, source.as_ref())?;
185        let star = crate::semantics::returning::command_returning_star_schema(
186            command,
187            &target,
188            source.as_ref(),
189        )?;
190        Ok((target, expression, star))
191    }
192
193    pub(super) fn command_input_schema(
194        command: &CommandPlan,
195        target: &RowSchema,
196        source: Option<&RowSchema>,
197    ) -> Result<RowSchema, SQLError> {
198        let aliases = command
199            .returning_aliases()
200            .ok_or_else(|| SQLError::Internal("command CTE has no RETURNING namespace".into()))?;
201        let mut expression = crate::semantics::returning_expression_schema(
202            target,
203            command.target_qualifier().unwrap_or_default(),
204            aliases,
205            source,
206        );
207        if target.columns_are_open(None) {
208            expression = RowSchema::with_open_columns(&expression, command.target_qualifier());
209        }
210        Ok(expression)
211    }
212
213    pub(super) fn bind_command_returning(
214        &mut self,
215        routines: &dyn RoutineResolution,
216        command: &CommandPlan,
217        params: &[SQLParam],
218    ) -> Result<RowSchema, SQLError> {
219        let previous = self.bind_cte_schemas(routines, command.ctes(), params, None)?;
220        let result = (|| {
221            let (target, expression, star) =
222                self.command_expression_schema(routines, command, params, None)?;
223            if let CommandPlan::Insert(insert) = command {
224                if let Some(source) = &insert.source {
225                    self.bind_query(routines, source, params, None)?;
226                }
227            }
228            let returning =
229                crate::semantics::returning::command_returning_projections(command, &target)?;
230            let mut columns = Vec::new();
231            let mut types: Vec<Option<ColumnType>> = Vec::new();
232            let mut records = Vec::new();
233            let mut collations = super::projection::OutputCollations::default();
234            for projection in &returning {
235                let expansion = if matches!(projection.expr, ScalarExpr::QualifiedStar(_)) {
236                    &expression
237                } else {
238                    &star
239                };
240                if let Some(star) = projection_star_columns(&projection.expr, expansion)? {
241                    records.extend(
242                        super::record_fields::star_metadata(&projection.expr, expansion)
243                            .unwrap_or_default(),
244                    );
245                    for (column, ty, collation) in star {
246                        collations.push(types.len(), ty.as_ref(), collation);
247                        columns.push(column);
248                        types.push(ty);
249                    }
250                } else {
251                    columns.push(crate::semantics::projection_label_at(projection));
252                    let metadata = self.bind_projection_metadata(
253                        routines,
254                        &projection.expr,
255                        &expression,
256                        command.scalar_subqueries(),
257                        params,
258                    )?;
259                    collations.push(types.len(), metadata.ty.as_ref(), metadata.collation);
260                    types.push(metadata.ty);
261                    records.push(metadata.record);
262                }
263            }
264            crate::semantics::returning::validate_returning_width(
265                command.returning().unwrap_or_default(),
266                columns.len(),
267            )?;
268            if let Some(error) = crate::semantics::virtual_relation_mutation_error(
269                self.catalog.as_ref(),
270                &self.resolution,
271                command,
272            )? {
273                return Err(error);
274            }
275            for projection in &returning {
276                self.collations.validate_expression(&projection.expr)?;
277            }
278            Ok(collations
279                .apply(RowSchema::with_types(columns, types))
280                .with_record_metadata(
281                    records
282                        .into_iter()
283                        .enumerate()
284                        .filter_map(|(index, record)| record.map(|record| (index, record))),
285                ))
286        })();
287        self.restore_cte_schemas(previous);
288        result
289    }
290}