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