Skip to main content

uqa_sql/semantics/
view_mutation.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! View mutation target identity, declared columns, and rewrite-rule input analysis.
8use super::{
9    rules::analysis::RuleAnalysisContext,
10    view_rewrite::context::{stored_view_schema, ViewRewriteContext},
11};
12use crate::{
13    catalog::stored_view::StoredView,
14    plan::{DeletePlan, MergePlan, MergeWhenPlan, UpdatePlan},
15    ColumnType, SQLError, ScalarExpr,
16};
17use std::collections::BTreeSet;
18use uqa_core::Value;
19
20pub struct ViewMutationTarget {
21    pub canonical_name: String,
22    pub definition: StoredView,
23    pub columns: Vec<String>,
24    pub types: Vec<Option<ColumnType>>,
25}
26
27pub fn resolve_view_target(
28    context: ViewRewriteContext<'_>,
29    name: &str,
30) -> Result<ViewMutationTarget, SQLError> {
31    let canonical_name = context
32        .catalog
33        .try_resolve_view_name(name)
34        .map_err(|error| SQLError::Internal(format!("resolve DML view `{name}`: {error}")))?
35        .ok_or_else(|| SQLError::UnknownTable(name.to_string()))?;
36    let definition = context
37        .authorization
38        .view_definition(&canonical_name)?
39        .ok_or_else(|| SQLError::UnknownTable(name.to_string()))?;
40    if definition.kind != crate::catalog::view::StoredViewKind::View {
41        return Err(SQLError::Routine {
42            sqlstate: "42809".into(),
43            message: format!("relation \"{canonical_name}\" is not a view"),
44        });
45    }
46    let schema = stored_view_schema(context, &definition.rewrite_definition())?;
47    let columns = schema
48        .columns()
49        .iter()
50        .enumerate()
51        .map(|(position, column)| schema.public_name(position).unwrap_or(column).to_string())
52        .collect::<Vec<_>>();
53    let types = (0..columns.len())
54        .map(|position| schema.column_type(position).cloned())
55        .collect();
56    Ok(ViewMutationTarget {
57        canonical_name,
58        definition,
59        columns,
60        types,
61    })
62}
63
64fn collect_view_expression_columns(
65    expression: &ScalarExpr,
66    columns: &mut BTreeSet<String>,
67) -> bool {
68    expression.collect_columns(columns)
69}
70
71pub fn required_view_update_columns(
72    analysis: RuleAnalysisContext<'_>,
73    target: &ViewMutationTarget,
74    stmt: &UpdatePlan,
75) -> Result<Option<BTreeSet<String>>, SQLError> {
76    let Some(mut columns) = super::rules::analysis::relation_rule_row_columns(
77        analysis,
78        &target.canonical_name,
79        crate::ast::RuleEvent::Update,
80    )?
81    else {
82        return Ok(None);
83    };
84    columns.extend(
85        stmt.assignments
86            .iter()
87            .flat_map(|assignment| assignment.target.column_names())
88            .map(str::to_owned),
89    );
90    for expression in stmt
91        .assignments
92        .iter()
93        .flat_map(crate::plan::AssignmentPlan::expressions)
94    {
95        if !collect_view_expression_columns(expression, &mut columns) {
96            return Ok(None);
97        }
98    }
99    if let Some(predicate) = stmt.predicate.as_ref() {
100        if !collect_view_expression_columns(predicate, &mut columns) {
101            return Ok(None);
102        }
103    }
104    Ok(Some(columns))
105}
106
107pub fn required_view_delete_columns(
108    analysis: RuleAnalysisContext<'_>,
109    target: &ViewMutationTarget,
110    stmt: &DeletePlan,
111) -> Result<Option<BTreeSet<String>>, SQLError> {
112    let Some(mut columns) = super::rules::analysis::relation_rule_row_columns(
113        analysis,
114        &target.canonical_name,
115        crate::ast::RuleEvent::Delete,
116    )?
117    else {
118        return Ok(None);
119    };
120    if let Some(predicate) = stmt.predicate.as_ref() {
121        if !collect_view_expression_columns(predicate, &mut columns) {
122            return Ok(None);
123        }
124    }
125    Ok(Some(columns))
126}
127
128pub fn target_columns(
129    target: &ViewMutationTarget,
130    explicit: &[crate::ast::AssignmentTarget<ScalarExpr>],
131    operation: &str,
132) -> Result<Vec<crate::ast::AssignmentTarget<ScalarExpr>>, SQLError> {
133    let columns = if explicit.is_empty() {
134        target.columns.iter().cloned().map(Into::into).collect()
135    } else {
136        explicit.to_vec()
137    };
138    crate::assignment::targets::validate_repeated_targets(&columns, operation == "INSERT")?;
139    for column in &columns {
140        if !target.columns.contains(&column.column) {
141            return Err(SQLError::UnknownColumn(format!(
142                "{}.{}",
143                target.canonical_name, column.column
144            )));
145        }
146    }
147    if columns.is_empty() {
148        return Err(SQLError::Unsupported(format!(
149            "{operation} against a zero-column view is not supported"
150        )));
151    }
152    Ok(columns)
153}
154
155pub fn view_qualification_references_target(
156    target: &ViewMutationTarget,
157    target_qualifier: &str,
158    predicate: Option<&ScalarExpr>,
159) -> bool {
160    let Some(predicate) = predicate else {
161        return false;
162    };
163    if super::expr_contains_subquery(predicate) {
164        return true;
165    }
166    if super::expr_qualifiers(predicate).iter().any(|qualifier| {
167        qualifier.eq_ignore_ascii_case(target_qualifier)
168            || qualifier.eq_ignore_ascii_case(&target.canonical_name)
169    }) {
170        return true;
171    }
172    if !super::expr_has_unqualified_column(predicate) {
173        return false;
174    }
175    let mut columns = BTreeSet::new();
176    !predicate.collect_columns(&mut columns)
177        || columns.iter().any(|column| target.columns.contains(column))
178}
179
180pub fn coerce_view_value(
181    assignment: &dyn crate::assignment::AssignmentContext,
182    target: &ViewMutationTarget,
183    position: usize,
184    value: Value,
185) -> Result<Value, SQLError> {
186    match target.types[position].as_ref() {
187        Some(ty) => crate::assignment::conversion::convert_value_to_column_type_with_context(
188            assignment, value, ty,
189        ),
190        None => Ok(value),
191    }
192}
193
194pub fn validate_view_merge_targets(
195    target: &ViewMutationTarget,
196    plan: &MergePlan,
197) -> Result<(), SQLError> {
198    for clause in &plan.when_clauses {
199        match clause {
200            MergeWhenPlan::UpdateMatched { assignments, .. }
201            | MergeWhenPlan::UpdateNotMatchedBySource { assignments, .. } => {
202                let columns = assignments
203                    .iter()
204                    .flat_map(|assignment| assignment.target.targets().iter().cloned())
205                    .collect::<Vec<_>>();
206                let _ = target_columns(target, &columns, "UPDATE")?;
207            }
208            MergeWhenPlan::InsertNotMatched {
209                columns, values, ..
210            } => {
211                let implicit = columns.is_empty();
212                let columns = target_columns(target, columns, "INSERT")?;
213                if values.len() > columns.len() || (!implicit && values.len() != columns.len()) {
214                    return Err(SQLError::TypeMismatch(format!(
215                        "MERGE INSERT row width {} != column count {}",
216                        values.len(),
217                        columns.len()
218                    )));
219                }
220            }
221            _ => {}
222        }
223    }
224    Ok(())
225}
226
227pub fn validate_view_merge_contract(
228    rewrite: ViewRewriteContext<'_>,
229    target: &ViewMutationTarget,
230    plan: &MergePlan,
231    params: &[crate::SQLParam],
232    bindings: &crate::binding::context::BindingContext<'_>,
233) -> Result<(), SQLError> {
234    validate_view_merge_targets(target, plan)?;
235    let source = crate::binding::analyze_source_plan_schema(
236        rewrite.catalog,
237        &plan.source,
238        params,
239        bindings,
240        None,
241    )?;
242    super::view_rewrite::validate_public_merge_targets(rewrite, plan)?;
243    super::view_rewrite::validate_public_merge_contract(rewrite, plan, &source)?;
244    super::returning::validate_returning_alias_relations(
245        &plan.target_qualifier,
246        &plan.returning_aliases,
247        Some(&source),
248    )?;
249    let target_schema = crate::RowSchema::with_qualified_types(
250        &plan.target_qualifier,
251        target.columns.clone(),
252        target.types.clone(),
253    );
254    super::merge::validate_merge_action_scopes(
255        rewrite.catalog,
256        plan,
257        &target_schema,
258        &source,
259        params,
260        bindings,
261    )
262}