Skip to main content

uqa_sql/semantics/view_rewrite/
rewrite_update_delete.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7use super::{
8    add_check_option, bind_unqualified_source_positions, canonical_view_name,
9    combine_view_predicate, delete_ordinary_subquery_ids, dml_source_schema, dml_target_width,
10    duplicate_assignment, finalize_source_returning, instead_of_trigger_definition,
11    next_rewritten_layer, preserve_view_rule_returning, record_view_rule_relation,
12    returning_subquery_ids, rewritable_layer, rewrite_correlated_dml_context,
13    rewrite_existing_view_checks, rewrite_returning, rewrite_target_expression,
14    update_ordinary_subquery_ids, validate_delete_expressions, validate_direct_view_rule_path,
15    validate_mapped_columns, validate_public_delete_contract, validate_public_update_contract,
16    validate_public_view_targets, validate_update_expressions, validate_update_targets,
17    validate_writable_columns, view_not_updatable, writable_column, AssignmentPlan, BTreeSet,
18    ColumnWrite, CorrelatedDmlContext, DeletePlan, ExpressionScope, LayerPrivileges,
19    NotUpdatableReason, SQLError, TriggerEvent, UpdatePlan, ViewCommand, ViewRewriteContext,
20    ViewRuleUpdatePlan,
21};
22
23#[expect(
24    clippy::too_many_lines,
25    reason = "preserves view qualifier and row identity"
26)]
27pub fn rewrite_update_to_base(
28    services: ViewRewriteContext<'_>,
29    statement: &UpdatePlan,
30    params: &[crate::SQLParam],
31    inherited_ctes: Option<&super::CteScope>,
32) -> Result<UpdatePlan, SQLError> {
33    validate_public_view_targets(
34        services,
35        &statement.table,
36        statement
37            .assignments
38            .iter()
39            .flat_map(|assignment| assignment.target.column_names()),
40    )?;
41    let source_schema = dml_source_schema(
42        services,
43        statement.source.as_deref(),
44        &statement.ctes,
45        &statement.subqueries,
46        params,
47        inherited_ctes,
48    )?;
49    validate_public_update_contract(services, statement, source_schema.as_ref())?;
50    let view = canonical_view_name(services, &statement.table)?;
51    validate_direct_view_rule_path(
52        services,
53        &view,
54        crate::ast::RuleEvent::Update,
55        ViewCommand::Update,
56    )?;
57    let initial_layer = rewritable_layer(services, &view, ViewCommand::Update)?;
58    if !initial_layer.has_writable_column() {
59        return Err(view_not_updatable(
60            &view,
61            ViewCommand::Update,
62            NotUpdatableReason::NoUpdatableColumns,
63        ));
64    }
65    validate_update_targets(&initial_layer, statement)?;
66    let mut initial_layer = Some(initial_layer);
67    let mut plan = statement.clone();
68    let mut privileges = LayerPrivileges::new();
69    plan.target_privilege_subject = Some(privileges.check(
70        services.authorization,
71        &plan.table,
72        plan.target_privilege_subject.as_ref(),
73        || crate::semantics::view_privileges::ensure_update(services.authorization, &plan),
74    )?);
75    let mut cascaded = false;
76    let mut visited = BTreeSet::new();
77    let mut source_star_boundaries = Vec::new();
78    let mut rewrite_suppressed = false;
79    loop {
80        // An underlying view with an INSTEAD OF trigger ends the rewrite, since `RewriteQuery` rewrites only a view without one: the trigger performs the UPDATE on that view.
81        if !visited.is_empty()
82            && !rewrite_suppressed
83            && instead_of_trigger_definition(services, &plan.table, TriggerEvent::Update)?
84        {
85            break;
86        }
87        let Some(layer) = next_rewritten_layer(
88            services,
89            &plan.table,
90            &mut initial_layer,
91            rewrite_suppressed,
92            crate::ast::RuleEvent::Update,
93            ViewCommand::Update,
94        )?
95        else {
96            break;
97        };
98        if !visited.insert(layer.canonical_name.clone()) {
99            return Err(SQLError::Internal(format!(
100                "cycle while rewriting automatically updatable view `{}`",
101                layer.canonical_name
102            )));
103        }
104        let has_view_rules = if rewrite_suppressed {
105            false
106        } else {
107            record_view_rule_relation(
108                services,
109                &mut plan.view_rule_relations,
110                &layer,
111                crate::ast::RuleEvent::Update,
112            )?
113        };
114        let layer_suppresses = has_view_rules
115            && super::context::relation_suppresses_original_query(
116                services,
117                &layer.canonical_name,
118                crate::ast::RuleEvent::Update,
119            )?;
120        if visited.len() > 1 && !rewrite_suppressed && !layer_suppresses {
121            if !layer.has_writable_column() {
122                return Err(view_not_updatable(
123                    &layer.canonical_name,
124                    ViewCommand::Update,
125                    NotUpdatableReason::NoUpdatableColumns,
126                ));
127            }
128            plan.target_privilege_subject = Some(privileges.check(
129                services.authorization,
130                &plan.table,
131                plan.target_privilege_subject.as_ref(),
132                || crate::semantics::view_privileges::ensure_update(services.authorization, &plan),
133            )?);
134        }
135        if has_view_rules
136            && super::context::relation_has_returning_provider(
137                services,
138                &layer.canonical_name,
139                crate::ast::RuleEvent::Update,
140            )?
141        {
142            preserve_view_rule_returning(
143                &mut plan.view_rule_returning,
144                &layer.canonical_name,
145                &plan.target_qualifier,
146                &plan.returning,
147                &plan.returning_aliases,
148                &plan.subqueries,
149            );
150        }
151        if has_view_rules {
152            plan.view_rule_update_plans.push(ViewRuleUpdatePlan {
153                relation: layer.canonical_name.clone(),
154                assigned_columns: plan
155                    .assignments
156                    .iter()
157                    .flat_map(|assignment| assignment.target.column_names())
158                    .map(str::to_owned)
159                    .collect(),
160                input_columns: Vec::new(),
161            });
162        }
163        let target_qualifier = plan.target_qualifier.clone();
164        if visited.len() == 1 {
165            validate_update_expressions(
166                services,
167                &plan,
168                &layer,
169                source_schema.as_ref(),
170                params,
171                inherited_ctes,
172            )?;
173        }
174        let ordinary_subquery_ids = update_ordinary_subquery_ids(&plan);
175        rewrite_correlated_dml_context(
176            CorrelatedDmlContext {
177                inherited_ctes,
178                services,
179                layer: &layer,
180                target_qualifier: &target_qualifier,
181                source: source_schema.as_ref(),
182                returning_aliases: None,
183                include_excluded: false,
184                ctes: &plan.ctes,
185                ids: &ordinary_subquery_ids,
186                params,
187            },
188            &mut plan.subqueries,
189        )?;
190        let returning_subquery_ids = returning_subquery_ids(&plan.returning);
191        rewrite_correlated_dml_context(
192            CorrelatedDmlContext {
193                inherited_ctes,
194                services,
195                layer: &layer,
196                target_qualifier: &target_qualifier,
197                source: source_schema.as_ref(),
198                returning_aliases: Some(&plan.returning_aliases),
199                include_excluded: false,
200                ctes: &plan.ctes,
201                ids: &returning_subquery_ids,
202                params,
203            },
204            &mut plan.subqueries,
205        )?;
206        let ordinary_scope = ExpressionScope {
207            target_qualifier: &target_qualifier,
208            returning_aliases: None,
209            source: source_schema.as_ref(),
210            include_excluded: false,
211        };
212        if !layer_suppresses && !rewrite_suppressed {
213            validate_writable_columns(
214                &layer,
215                plan.assignments
216                    .iter()
217                    .flat_map(|assignment| assignment.target.column_names()),
218                ColumnWrite::Update,
219            )?;
220        }
221        for AssignmentPlan { target, value } in &mut plan.assignments {
222            for expression in target.expressions_mut() {
223                rewrite_target_expression(
224                    services,
225                    expression,
226                    &layer,
227                    ordinary_scope,
228                    &mut plan.subqueries,
229                )?;
230            }
231            rewrite_target_expression(
232                services,
233                value,
234                &layer,
235                ordinary_scope,
236                &mut plan.subqueries,
237            )?;
238            if !layer_suppresses && !rewrite_suppressed {
239                for target in target.targets_mut() {
240                    target.column = writable_column(&layer, &target.column, ColumnWrite::Update)?;
241                }
242            }
243        }
244        let mapped = plan
245            .assignments
246            .iter()
247            .flat_map(|assignment| assignment.target.targets().iter().cloned())
248            .collect::<Vec<_>>();
249        validate_mapped_columns(&mapped, duplicate_assignment)?;
250        if let Some(predicate) = &mut plan.predicate {
251            rewrite_target_expression(
252                services,
253                predicate,
254                &layer,
255                ordinary_scope,
256                &mut plan.subqueries,
257            )?;
258        }
259        rewrite_existing_view_checks(
260            services,
261            &mut plan.view_checks,
262            &layer,
263            &target_qualifier,
264            &mut plan.subqueries,
265        )?;
266        let (returning, boundaries) = rewrite_returning(
267            services,
268            plan.returning,
269            &layer,
270            &target_qualifier,
271            &plan.returning_aliases,
272            source_schema.as_ref(),
273            &mut plan.subqueries,
274        )?;
275        plan.returning = returning;
276        if visited.len() == 1 {
277            source_star_boundaries = boundaries;
278        }
279        plan.predicate = combine_view_predicate(
280            services,
281            plan.predicate,
282            &layer,
283            &target_qualifier,
284            &mut plan.subqueries,
285        )?;
286        add_check_option(
287            services,
288            &mut plan.view_checks,
289            &layer,
290            &target_qualifier,
291            &mut cascaded,
292            &mut plan.subqueries,
293        )?;
294        plan.table = layer.source_name;
295        plan.include_descendants = layer.source_include_descendants;
296        rewrite_suppressed |= layer_suppresses;
297        if !super::context::target_is_view(services, &plan.table)? {
298            break;
299        }
300    }
301    let input_columns = plan
302        .assignments
303        .iter()
304        .flat_map(|assignment| assignment.target.column_names())
305        .map(str::to_owned)
306        .collect::<Vec<_>>();
307    for update_plan in &mut plan.view_rule_update_plans {
308        update_plan.input_columns.clone_from(&input_columns);
309    }
310    if let Some(source) = source_schema.as_ref() {
311        let target_width = dml_target_width(services, &plan.table)?;
312        for assignment in &mut plan.assignments {
313            for expression in assignment.expressions_mut() {
314                bind_unqualified_source_positions(expression, source, target_width);
315            }
316        }
317        if let Some(predicate) = &mut plan.predicate {
318            bind_unqualified_source_positions(predicate, source, target_width);
319        }
320        for projection in &mut plan.returning {
321            bind_unqualified_source_positions(&mut projection.expr, source, target_width);
322        }
323    }
324    plan.returning = finalize_source_returning(
325        services,
326        &plan.table,
327        plan.returning,
328        source_schema.as_ref(),
329        &source_star_boundaries,
330    )?;
331    privileges.finish()?;
332    Ok(plan)
333}
334
335#[expect(
336    clippy::too_many_lines,
337    reason = "preserves view qualifier and row identity"
338)]
339pub fn rewrite_delete_to_base(
340    services: ViewRewriteContext<'_>,
341    statement: &DeletePlan,
342    params: &[crate::SQLParam],
343    inherited_ctes: Option<&super::CteScope>,
344) -> Result<DeletePlan, SQLError> {
345    let source_schema = dml_source_schema(
346        services,
347        statement.source.as_deref(),
348        &statement.ctes,
349        &statement.subqueries,
350        params,
351        inherited_ctes,
352    )?;
353    validate_public_delete_contract(services, statement, source_schema.as_ref())?;
354    let mut plan = statement.clone();
355    let mut privileges = LayerPrivileges::new();
356    plan.target_privilege_subject = Some(privileges.check(
357        services.authorization,
358        &plan.table,
359        plan.target_privilege_subject.as_ref(),
360        || crate::semantics::view_privileges::ensure_delete(services.authorization, &plan),
361    )?);
362    let mut initial_layer = None;
363    let mut visited = BTreeSet::new();
364    let mut source_star_boundaries = Vec::new();
365    let mut rewrite_suppressed = false;
366    loop {
367        // An underlying view with an INSTEAD OF trigger ends the rewrite, since `RewriteQuery` rewrites only a view without one: the trigger performs the DELETE on that view.
368        if !visited.is_empty()
369            && !rewrite_suppressed
370            && instead_of_trigger_definition(services, &plan.table, TriggerEvent::Delete)?
371        {
372            break;
373        }
374        let Some(layer) = next_rewritten_layer(
375            services,
376            &plan.table,
377            &mut initial_layer,
378            rewrite_suppressed,
379            crate::ast::RuleEvent::Delete,
380            ViewCommand::Delete,
381        )?
382        else {
383            break;
384        };
385        if !visited.insert(layer.canonical_name.clone()) {
386            return Err(SQLError::Internal(format!(
387                "cycle while rewriting automatically updatable view `{}`",
388                layer.canonical_name
389            )));
390        }
391        let has_view_rules = if rewrite_suppressed {
392            false
393        } else {
394            record_view_rule_relation(
395                services,
396                &mut plan.view_rule_relations,
397                &layer,
398                crate::ast::RuleEvent::Delete,
399            )?
400        };
401        let layer_suppresses = has_view_rules
402            && super::context::relation_suppresses_original_query(
403                services,
404                &layer.canonical_name,
405                crate::ast::RuleEvent::Delete,
406            )?;
407        if visited.len() > 1 && !rewrite_suppressed && !layer_suppresses {
408            plan.target_privilege_subject = Some(privileges.check(
409                services.authorization,
410                &plan.table,
411                plan.target_privilege_subject.as_ref(),
412                || crate::semantics::view_privileges::ensure_delete(services.authorization, &plan),
413            )?);
414        }
415        if has_view_rules
416            && super::context::relation_has_returning_provider(
417                services,
418                &layer.canonical_name,
419                crate::ast::RuleEvent::Delete,
420            )?
421        {
422            preserve_view_rule_returning(
423                &mut plan.view_rule_returning,
424                &layer.canonical_name,
425                &plan.target_qualifier,
426                &plan.returning,
427                &plan.returning_aliases,
428                &plan.subqueries,
429            );
430        }
431        let target_qualifier = plan.target_qualifier.clone();
432        if visited.len() == 1 {
433            validate_delete_expressions(
434                services,
435                &plan,
436                &layer,
437                source_schema.as_ref(),
438                params,
439                inherited_ctes,
440            )?;
441        }
442        let ordinary_subquery_ids = delete_ordinary_subquery_ids(&plan);
443        rewrite_correlated_dml_context(
444            CorrelatedDmlContext {
445                inherited_ctes,
446                services,
447                layer: &layer,
448                target_qualifier: &target_qualifier,
449                source: source_schema.as_ref(),
450                returning_aliases: None,
451                include_excluded: false,
452                ctes: &plan.ctes,
453                ids: &ordinary_subquery_ids,
454                params,
455            },
456            &mut plan.subqueries,
457        )?;
458        let returning_subquery_ids = returning_subquery_ids(&plan.returning);
459        rewrite_correlated_dml_context(
460            CorrelatedDmlContext {
461                inherited_ctes,
462                services,
463                layer: &layer,
464                target_qualifier: &target_qualifier,
465                source: source_schema.as_ref(),
466                returning_aliases: Some(&plan.returning_aliases),
467                include_excluded: false,
468                ctes: &plan.ctes,
469                ids: &returning_subquery_ids,
470                params,
471            },
472            &mut plan.subqueries,
473        )?;
474        let ordinary_scope = ExpressionScope {
475            target_qualifier: &target_qualifier,
476            returning_aliases: None,
477            source: source_schema.as_ref(),
478            include_excluded: false,
479        };
480        if let Some(predicate) = &mut plan.predicate {
481            rewrite_target_expression(
482                services,
483                predicate,
484                &layer,
485                ordinary_scope,
486                &mut plan.subqueries,
487            )?;
488        }
489        let (returning, boundaries) = rewrite_returning(
490            services,
491            plan.returning,
492            &layer,
493            &target_qualifier,
494            &plan.returning_aliases,
495            source_schema.as_ref(),
496            &mut plan.subqueries,
497        )?;
498        plan.returning = returning;
499        if visited.len() == 1 {
500            source_star_boundaries = boundaries;
501        }
502        plan.predicate = combine_view_predicate(
503            services,
504            plan.predicate,
505            &layer,
506            &target_qualifier,
507            &mut plan.subqueries,
508        )?;
509        plan.table = layer.source_name;
510        plan.include_descendants = layer.source_include_descendants;
511        rewrite_suppressed |= layer_suppresses;
512        if !super::context::target_is_view(services, &plan.table)? {
513            break;
514        }
515    }
516    if let Some(source) = source_schema.as_ref() {
517        let target_width = dml_target_width(services, &plan.table)?;
518        if let Some(predicate) = &mut plan.predicate {
519            bind_unqualified_source_positions(predicate, source, target_width);
520        }
521        for projection in &mut plan.returning {
522            bind_unqualified_source_positions(&mut projection.expr, source, target_width);
523        }
524    }
525    plan.returning = finalize_source_returning(
526        services,
527        &plan.table,
528        plan.returning,
529        source_schema.as_ref(),
530        &source_star_boundaries,
531    )?;
532    privileges.finish()?;
533    Ok(plan)
534}