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    SQLError, ScalarExpr,
16};
17use std::collections::BTreeSet;
18use uqa_core::Value;
19
20pub struct ViewMutationTarget {
21    pub schema: crate::RowSchema,
22    pub canonical_name: String,
23    pub definition: StoredView,
24    pub columns: Vec<String>,
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    Ok(ViewMutationTarget {
54        schema,
55        canonical_name,
56        definition,
57        columns,
58    })
59}
60
61fn collect_view_expression_columns(
62    expression: &ScalarExpr,
63    columns: &mut BTreeSet<String>,
64) -> bool {
65    expression.collect_columns(columns)
66}
67
68pub fn required_view_update_columns(
69    analysis: RuleAnalysisContext<'_>,
70    target: &ViewMutationTarget,
71    stmt: &UpdatePlan,
72) -> Result<Option<BTreeSet<String>>, SQLError> {
73    let Some(mut columns) = super::rules::analysis::relation_rule_row_columns(
74        analysis,
75        &target.canonical_name,
76        crate::ast::RuleEvent::Update,
77    )?
78    else {
79        return Ok(None);
80    };
81    columns.extend(
82        stmt.assignments
83            .iter()
84            .flat_map(|assignment| assignment.target.column_names())
85            .map(str::to_owned),
86    );
87    for expression in stmt
88        .assignments
89        .iter()
90        .flat_map(crate::plan::AssignmentPlan::expressions)
91    {
92        if !collect_view_expression_columns(expression, &mut columns) {
93            return Ok(None);
94        }
95    }
96    if let Some(predicate) = stmt.predicate.as_ref() {
97        if !collect_view_expression_columns(predicate, &mut columns) {
98            return Ok(None);
99        }
100    }
101    Ok(Some(columns))
102}
103
104pub fn required_view_delete_columns(
105    analysis: RuleAnalysisContext<'_>,
106    target: &ViewMutationTarget,
107    stmt: &DeletePlan,
108) -> Result<Option<BTreeSet<String>>, SQLError> {
109    let Some(mut columns) = super::rules::analysis::relation_rule_row_columns(
110        analysis,
111        &target.canonical_name,
112        crate::ast::RuleEvent::Delete,
113    )?
114    else {
115        return Ok(None);
116    };
117    if let Some(predicate) = stmt.predicate.as_ref() {
118        if !collect_view_expression_columns(predicate, &mut columns) {
119            return Ok(None);
120        }
121    }
122    Ok(Some(columns))
123}
124
125pub fn target_columns(
126    target: &ViewMutationTarget,
127    explicit: &[crate::ast::AssignmentTarget<ScalarExpr>],
128    operation: &str,
129) -> Result<Vec<crate::ast::AssignmentTarget<ScalarExpr>>, SQLError> {
130    let columns = if explicit.is_empty() {
131        target.columns.iter().cloned().map(Into::into).collect()
132    } else {
133        explicit.to_vec()
134    };
135    crate::assignment::targets::validate_repeated_targets(&columns, operation == "INSERT")?;
136    for column in &columns {
137        if !target.columns.contains(&column.column) {
138            return Err(SQLError::UnknownColumn(format!(
139                "{}.{}",
140                target.canonical_name, column.column
141            )));
142        }
143    }
144    if columns.is_empty() {
145        return Err(SQLError::Unsupported(format!(
146            "{operation} against a zero-column view is not supported"
147        )));
148    }
149    Ok(columns)
150}
151
152pub fn view_qualification_references_target(
153    target: &ViewMutationTarget,
154    target_qualifier: &str,
155    predicate: Option<&ScalarExpr>,
156) -> bool {
157    let Some(predicate) = predicate else {
158        return false;
159    };
160    if super::expr_contains_subquery(predicate) {
161        return true;
162    }
163    if super::expr_qualifiers(predicate).iter().any(|qualifier| {
164        qualifier.eq_ignore_ascii_case(target_qualifier)
165            || qualifier.eq_ignore_ascii_case(&target.canonical_name)
166    }) {
167        return true;
168    }
169    if !super::expr_has_unqualified_column(predicate) {
170        return false;
171    }
172    let mut columns = BTreeSet::new();
173    !predicate.collect_columns(&mut columns)
174        || columns.iter().any(|column| target.columns.contains(column))
175}
176
177pub fn coerce_view_value(
178    assignment: &dyn crate::assignment::AssignmentContext,
179    target: &ViewMutationTarget,
180    position: usize,
181    value: Value,
182) -> Result<Value, SQLError> {
183    match target.schema.column_type(position) {
184        Some(ty) => crate::assignment::conversion::convert_value_to_column_type_with_context(
185            assignment, value, ty,
186        ),
187        None => Ok(value),
188    }
189}
190
191pub fn validate_view_merge_targets(
192    target: &ViewMutationTarget,
193    plan: &MergePlan,
194) -> Result<(), SQLError> {
195    for clause in &plan.when_clauses {
196        match clause {
197            MergeWhenPlan::UpdateMatched { assignments, .. }
198            | MergeWhenPlan::UpdateNotMatchedBySource { assignments, .. } => {
199                let columns = assignments
200                    .iter()
201                    .flat_map(|assignment| assignment.target.targets().iter().cloned())
202                    .collect::<Vec<_>>();
203                let _ = target_columns(target, &columns, "UPDATE")?;
204            }
205            MergeWhenPlan::InsertNotMatched {
206                columns, values, ..
207            } => {
208                let implicit = columns.is_empty();
209                let columns = target_columns(target, columns, "INSERT")?;
210                if values.len() > columns.len() || (!implicit && values.len() != columns.len()) {
211                    return Err(SQLError::TypeMismatch(format!(
212                        "MERGE INSERT row width {} != column count {}",
213                        values.len(),
214                        columns.len()
215                    )));
216                }
217            }
218            _ => {}
219        }
220    }
221    Ok(())
222}
223
224pub fn validate_view_merge_contract(
225    rewrite: ViewRewriteContext<'_>,
226    target: &ViewMutationTarget,
227    plan: &MergePlan,
228    params: &[crate::SQLParam],
229    bindings: &crate::binding::context::BindingContext<'_>,
230) -> Result<(), SQLError> {
231    validate_view_merge_targets(target, plan)?;
232    let source = crate::binding::analyze_source_plan_schema(
233        rewrite.routines,
234        &plan.source,
235        params,
236        bindings,
237        None,
238    )?;
239    super::view_rewrite::validate_public_merge_targets(rewrite, plan)?;
240    super::view_rewrite::validate_public_merge_contract(rewrite, plan, &source)?;
241    super::returning::validate_returning_alias_relations(
242        &plan.target_qualifier,
243        &plan.returning_aliases,
244        Some(&source),
245    )?;
246    let target_schema =
247        crate::RowSchema::with_relation_qualifier(&target.schema, &plan.target_qualifier);
248    super::merge::validate_merge_action_scopes(
249        rewrite.routines,
250        plan,
251        &target_schema,
252        &source,
253        params,
254        bindings,
255    )
256}