Skip to main content

uqa_sql/semantics/
view_rewrite.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! `PostgreSQL` 18 automatically updatable view analysis and DML rewriting.
8
9use std::collections::{BTreeMap, BTreeSet};
10
11use crate::ast::{ReturningAliases, TriggerEvent, TriggerTiming};
12use crate::plan::{
13    AssignmentPlan, ComputePlan, ConflictActionPlan, ConflictPlan, DeletePlan, InsertPlan,
14    MergePlan, MergeWhenPlan, ProjectionPlan, QueryPlan, RelationalPlan, SourcePlan, UpdatePlan,
15    ViewCheckPlan, ViewRuleInsertPlan, ViewRuleReturningPlan, ViewRuleUpdatePlan,
16};
17use crate::SQLError;
18use crate::{ColumnIdentity, RowSchema, ScalarExpr};
19
20use crate::{
21    catalog::view::{StoredViewKind, ViewRewriteDefinition as StoredView},
22    RelationIdentity,
23};
24
25use crate::binding::snapshot::BindingSnapshot as CteScope;
26pub mod context;
27use context::{stored_view_schema, ViewRewriteContext};
28
29mod correlation;
30mod returning;
31mod rewrite_insert;
32mod rewrite_merge;
33mod rewrite_update_delete;
34pub mod rule_inputs;
35mod validation;
36
37use correlation::{
38    collect_expression_subquery_ids, delete_ordinary_subquery_ids, dml_analysis_scope,
39    dml_source_schema, insert_conflict_subquery_ids, insert_input_width,
40    merge_matched_subquery_ids, merge_target_only_subquery_ids, returning_subquery_ids,
41    rewrite_correlated_dml_context, schema_public_columns, update_ordinary_subquery_ids,
42    validate_delete_expressions, validate_insert_expressions, validate_merge_expressions,
43    validate_update_expressions, CorrelatedDmlContext,
44};
45use returning::{
46    add_check_option, bind_unqualified_source_positions, combine_view_predicate, dml_target_width,
47    finalize_source_returning, instead_of_trigger_definition, preserve_view_rule_returning,
48    record_view_rule_relation, retarget_source_expression, rewrite_existing_view_checks,
49    rewrite_merge_returning, rewrite_returning, rewrite_target_expression,
50};
51pub use rewrite_insert::rewrite_insert_to_base;
52pub use rewrite_merge::rewrite_merge_to_base;
53pub use rewrite_update_delete::{rewrite_delete_to_base, rewrite_update_to_base};
54pub use rule_inputs::rule_input_requirements;
55use validation::{
56    duplicate_assignment, duplicate_insert_column, layer_column, merge_action_capability_error,
57    validate_direct_view_rule_path, validate_insert_targets, validate_mapped_columns,
58    validate_merge_targets, validate_public_delete_contract, validate_public_insert_contract,
59    validate_public_update_contract, validate_public_view_targets, validate_update_targets,
60    validate_view_expression, writable_column, ExpressionScope,
61};
62pub use validation::{merge_view_target_path, MergeViewTargetPath};
63pub use validation::{validate_public_merge_contract, validate_public_merge_targets};
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum ViewCheckOption {
67    None,
68    Local,
69    Cascaded,
70}
71
72impl ViewCheckOption {
73    fn from_options(options: &[(String, String)]) -> Self {
74        options
75            .iter()
76            .rev()
77            .find(|(name, _)| name == "check_option")
78            .map_or(Self::None, |(_, value)| match value.as_str() {
79                "local" => Self::Local,
80                "cascaded" => Self::Cascaded,
81                _ => Self::None,
82            })
83    }
84
85    const fn catalog_value(self) -> &'static str {
86        match self {
87            Self::None => "NONE",
88            Self::Local => "LOCAL",
89            Self::Cascaded => "CASCADED",
90        }
91    }
92}
93
94#[derive(Debug, Clone)]
95pub struct ViewColumn {
96    pub name: String,
97    pub expression: ScalarExpr,
98    pub writable_source_column: Option<String>,
99}
100
101#[derive(Debug, Clone)]
102pub struct AutomaticViewLayer {
103    pub canonical_name: String,
104    pub source_name: String,
105    pub source_qualifier: String,
106    pub source_column_map: BTreeMap<String, String>,
107    pub source_include_descendants: bool,
108    pub source_schema: RowSchema,
109    pub columns: Vec<ViewColumn>,
110    pub predicate: Option<ScalarExpr>,
111    pub subqueries: Vec<QueryPlan>,
112    pub check_option: ViewCheckOption,
113}
114
115impl AutomaticViewLayer {
116    pub fn physical_source_column<'a>(&'a self, column: &'a str) -> &'a str {
117        self.source_column_map
118            .get(column)
119            .map_or(column, String::as_str)
120    }
121}
122
123pub use crate::catalog::view::ViewMutationCapabilities;
124
125#[derive(Debug, Clone)]
126pub struct ViewUpdatability {
127    pub automatic: ViewMutationCapabilities,
128    runtime: ViewMutationCapabilities,
129    runtime_insert_columns: Vec<bool>,
130    runtime_columns: Vec<bool>,
131    pub catalog: ViewMutationCapabilities,
132    catalog_insert_columns: Vec<bool>,
133    pub catalog_columns: Vec<bool>,
134    pub check_option: String,
135}
136
137fn display_relation(name: &str) -> String {
138    RelationIdentity::from_legacy_name(name)
139        .map_or_else(|_| name.to_string(), |relation| relation.name)
140}
141
142fn not_automatically_updatable(view: &str, operation: &str) -> SQLError {
143    SQLError::Routine {
144        sqlstate: "55000".into(),
145        message: format!(
146            "cannot {} view \"{}\": the view is not automatically updatable",
147            operation.to_ascii_lowercase(),
148            display_relation(view)
149        ),
150    }
151}
152
153fn non_writable_column(view: &str, column: &str, operation: &str) -> SQLError {
154    SQLError::Routine {
155        sqlstate: "0A000".into(),
156        message: format!(
157            "cannot {} column \"{column}\" of view \"{}\"",
158            operation.to_ascii_lowercase(),
159            display_relation(view)
160        ),
161    }
162}
163
164pub fn relation_columns(
165    services: ViewRewriteContext<'_>,
166    relation: &str,
167) -> Result<Vec<String>, SQLError> {
168    if let Some(view) = services.catalog.view_definition(relation)? {
169        let schema = stored_view_schema(services, &view)?;
170        return Ok(schema
171            .columns()
172            .iter()
173            .enumerate()
174            .map(|(position, column)| {
175                schema
176                    .public_name(position)
177                    .map_or_else(|| column.clone(), str::to_string)
178            })
179            .collect());
180    }
181    let definitions = services
182        .catalog
183        .try_describe_table(relation)
184        .map_err(|error| SQLError::Internal(format!("describe view source `{relation}`: {error}")))?
185        .ok_or_else(|| SQLError::UnknownTable(relation.to_string()))?;
186    if definitions.is_empty() {
187        return services
188            .catalog
189            .try_table_columns(relation)
190            .map_err(|error| {
191                SQLError::Internal(format!("describe view source `{relation}`: {error}"))
192            });
193    }
194    Ok(definitions
195        .into_iter()
196        .map(|definition| definition.name)
197        .collect())
198}
199
200fn source_qualifier_matches(qualifier: &str, source_qualifier: &str, source_name: &str) -> bool {
201    qualifier == source_qualifier
202        || qualifier == source_name
203        || RelationIdentity::from_legacy_name(source_name)
204            .is_ok_and(|identity| qualifier == identity.name)
205}
206
207fn direct_source_column(
208    expression: &ScalarExpr,
209    source_qualifier: &str,
210    source_name: &str,
211    source_columns: &BTreeMap<String, String>,
212) -> Option<String> {
213    let column = match expression {
214        ScalarExpr::Column(column) => Some(column.clone()),
215        ScalarExpr::QualifiedColumn { qualifier, column }
216            if source_qualifier_matches(qualifier, source_qualifier, source_name) =>
217        {
218            Some(column.clone())
219        }
220        _ => None,
221    }?;
222    source_columns.get(&column).cloned()
223}
224
225pub fn automatic_view_layer(
226    services: ViewRewriteContext<'_>,
227    name: &str,
228) -> Result<Option<AutomaticViewLayer>, SQLError> {
229    let Some(canonical_name) = services
230        .catalog
231        .try_resolve_view_name(name)
232        .map_err(|error| SQLError::Internal(format!("resolve DML view `{name}`: {error}")))?
233    else {
234        return Ok(None);
235    };
236    let definition = services
237        .catalog
238        .view_definition(&canonical_name)?
239        .ok_or_else(|| SQLError::UnknownTable(name.to_string()))?;
240    automatic_view_layer_from_definition(services, &canonical_name, &definition)
241}
242
243#[expect(
244    clippy::too_many_lines,
245    reason = "preserves view qualifier and row identity"
246)]
247fn automatic_view_layer_from_definition(
248    services: ViewRewriteContext<'_>,
249    canonical_name: &str,
250    definition: &StoredView,
251) -> Result<Option<AutomaticViewLayer>, SQLError> {
252    if definition.kind != StoredViewKind::View || !definition.query.ctes.is_empty() {
253        return Ok(None);
254    }
255    let RelationalPlan::QueryBlock(block) = &definition.query.root else {
256        return Ok(None);
257    };
258    if !matches!(block.compute, ComputePlan::Project)
259        || !block.group_by.is_empty()
260        || !block.grouping_sets.is_empty()
261        || block.group_distinct
262        || block.having.is_some()
263        || block.limit.is_some()
264        || block.with_ties
265        || block.offset.is_some()
266        || block.distinct
267        || !block.distinct_on.is_empty()
268        || !block.locking.is_empty()
269    {
270        return Ok(None);
271    }
272    let Some(
273        source_plan @ SourcePlan::Table {
274            name: source_name,
275            qualifier,
276            alias,
277            column_aliases,
278            include_descendants,
279            ..
280        },
281    ) = block.from.as_ref()
282    else {
283        return Ok(None);
284    };
285    let analysis_scope = services.catalog.binding_scope()?;
286    let source_schema = crate::semantics::view_rewrite::context::analyze_source_plan_schema(
287        services,
288        source_plan,
289        &[],
290        &analysis_scope,
291        None,
292    )?;
293    let resolver = crate::binding::scoped_types::BindingTypeResolver {
294        routines: services.catalog,
295        scope: &analysis_scope,
296    };
297    for projection in &block.projections {
298        if crate::semantics::sets::validation::expression_may_return_set(
299            services.catalog,
300            &resolver,
301            &projection.expr,
302            &source_schema,
303            &[],
304        )? {
305            return Ok(None);
306        }
307    }
308    let source_qualifier = alias.as_deref().unwrap_or(qualifier).to_string();
309    let source_columns = relation_columns(services, source_name)?;
310    let mut visible_source_columns = source_columns.clone();
311    for (column, alias) in visible_source_columns.iter_mut().zip(column_aliases) {
312        column.clone_from(alias);
313    }
314    let source_column_map = visible_source_columns
315        .iter()
316        .cloned()
317        .zip(source_columns.iter().cloned())
318        .collect::<BTreeMap<_, _>>();
319    let mut expressions = Vec::new();
320    for projection in &block.projections {
321        match &projection.expr {
322            ScalarExpr::Star => {
323                expressions.extend(
324                    visible_source_columns
325                        .iter()
326                        .cloned()
327                        .map(ScalarExpr::Column),
328                );
329            }
330            ScalarExpr::QualifiedStar(star_qualifier)
331                if source_qualifier_matches(star_qualifier, &source_qualifier, source_name) =>
332            {
333                expressions.extend(visible_source_columns.iter().cloned().map(|column| {
334                    ScalarExpr::QualifiedColumn {
335                        qualifier: source_qualifier.clone(),
336                        column,
337                    }
338                }));
339            }
340            expression => expressions.push(expression.clone()),
341        }
342    }
343    let schema = stored_view_schema(services, definition)?;
344    let output_columns = schema
345        .columns()
346        .iter()
347        .enumerate()
348        .map(|(position, column)| schema.public_name(position).unwrap_or(column).to_string())
349        .collect::<Vec<_>>();
350    if expressions.len() != output_columns.len() {
351        return Err(SQLError::Internal(format!(
352            "view `{canonical_name}` has {} stored projections for {} output columns",
353            expressions.len(),
354            output_columns.len()
355        )));
356    }
357    let columns = output_columns
358        .into_iter()
359        .zip(expressions)
360        .map(|(name, expression)| ViewColumn {
361            writable_source_column: direct_source_column(
362                &expression,
363                &source_qualifier,
364                source_name,
365                &source_column_map,
366            ),
367            name,
368            expression,
369        })
370        .collect();
371    Ok(Some(AutomaticViewLayer {
372        canonical_name: canonical_name.to_string(),
373        source_name: source_name.clone(),
374        source_qualifier,
375        source_column_map,
376        source_include_descendants: *include_descendants,
377        source_schema,
378        columns,
379        predicate: block.r#where.clone(),
380        subqueries: block.subqueries.clone(),
381        check_option: ViewCheckOption::from_options(&definition.options),
382    }))
383}
384
385mod layer_rewrite;
386use layer_rewrite::embed_layer_expression;
387
388pub fn has_instead_of_trigger(
389    services: ViewRewriteContext<'_>,
390    view: &str,
391    event: TriggerEvent,
392) -> Result<bool, SQLError> {
393    let Some(canonical) = services
394        .catalog
395        .try_resolve_view_name(view)
396        .map_err(|error| SQLError::Internal(format!("resolve DML view `{view}`: {error}")))?
397    else {
398        return Ok(false);
399    };
400    instead_of_trigger_definition(services, &canonical, event)
401}
402
403#[expect(
404    clippy::too_many_lines,
405    reason = "preserves view qualifier and row identity"
406)]
407fn view_updatability_inner(
408    services: ViewRewriteContext<'_>,
409    name: &str,
410    visited: &mut BTreeSet<String>,
411) -> Result<ViewUpdatability, SQLError> {
412    let definition = services
413        .catalog
414        .view_definition(name)?
415        .ok_or_else(|| SQLError::UnknownTable(name.to_string()))?;
416    let schema = stored_view_schema(services, &definition)?;
417    let width = schema.len();
418    let check_option = ViewCheckOption::from_options(&definition.options)
419        .catalog_value()
420        .to_string();
421    let mut automatic = ViewMutationCapabilities::default();
422    let mut automatic_insert_columns = vec![false; width];
423    let mut automatic_columns = vec![false; width];
424    let mut projected_catalog = ViewMutationCapabilities::default();
425    let mut projected_catalog_insert_columns = vec![false; width];
426    let mut projected_catalog_columns = vec![false; width];
427    if let Some(layer) = automatic_view_layer(services, name)? {
428        if visited.insert(layer.canonical_name.clone()) {
429            if services
430                .catalog
431                .view_definition(&layer.source_name)?
432                .is_some()
433            {
434                let source = view_updatability_inner(services, &layer.source_name, visited)?;
435                let source_columns = relation_columns(services, &layer.source_name)?;
436                let mapped = |source_capabilities: &[bool]| {
437                    layer
438                        .columns
439                        .iter()
440                        .map(|column| {
441                            column.writable_source_column.as_ref().is_some_and(|name| {
442                                source_columns
443                                    .iter()
444                                    .position(|candidate| candidate == name)
445                                    .is_some_and(|position| {
446                                        source_capabilities.get(position) == Some(&true)
447                                    })
448                            })
449                        })
450                        .collect::<Vec<_>>()
451                };
452                automatic_insert_columns = mapped(&source.runtime_insert_columns);
453                automatic_columns = mapped(&source.runtime_columns);
454                automatic = ViewMutationCapabilities {
455                    insertable: source.runtime.insertable
456                        && automatic_insert_columns.iter().any(|value| *value),
457                    updatable: source.runtime.updatable
458                        && automatic_columns.iter().any(|value| *value),
459                    deletable: source.runtime.deletable,
460                };
461                projected_catalog_insert_columns = mapped(&source.catalog_insert_columns);
462                projected_catalog_columns = mapped(&source.catalog_columns);
463                projected_catalog = ViewMutationCapabilities {
464                    insertable: source.catalog.insertable
465                        && projected_catalog_insert_columns.iter().any(|value| *value),
466                    updatable: source.catalog.updatable
467                        && projected_catalog_columns.iter().any(|value| *value),
468                    deletable: source.catalog.deletable,
469                };
470            } else {
471                automatic_insert_columns = layer
472                    .columns
473                    .iter()
474                    .map(|column| column.writable_source_column.is_some())
475                    .collect();
476                automatic_columns.clone_from(&automatic_insert_columns);
477                automatic = ViewMutationCapabilities {
478                    insertable: automatic_insert_columns.iter().any(|value| *value),
479                    updatable: automatic_columns.iter().any(|value| *value),
480                    deletable: true,
481                };
482                projected_catalog = automatic;
483                projected_catalog_insert_columns.clone_from(&automatic_insert_columns);
484                projected_catalog_columns.clone_from(&automatic_columns);
485            }
486            visited.remove(&layer.canonical_name);
487        }
488    }
489    let active_insert =
490        active_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Insert)?;
491    let active_update =
492        active_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Update)?;
493    let active_delete =
494        active_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Delete)?;
495    let runtime = ViewMutationCapabilities {
496        insertable: automatic.insertable || active_insert,
497        updatable: automatic.updatable || active_update,
498        deletable: automatic.deletable || active_delete,
499    };
500    let runtime_insert_columns = automatic_insert_columns
501        .iter()
502        .map(|column| *column || active_insert)
503        .collect();
504    let runtime_columns = automatic_columns
505        .iter()
506        .map(|column| *column || active_update)
507        .collect();
508    let rule_insertable =
509        has_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Insert)?;
510    let rule_updatable =
511        has_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Update)?;
512    let rule_deletable =
513        has_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Delete)?;
514    let catalog = ViewMutationCapabilities {
515        insertable: projected_catalog.insertable || rule_insertable,
516        updatable: projected_catalog.updatable || rule_updatable,
517        deletable: projected_catalog.deletable || rule_deletable,
518    };
519    let catalog_insert_columns = projected_catalog_insert_columns
520        .iter()
521        .map(|column| *column || rule_insertable)
522        .collect();
523    let catalog_columns = if catalog.fully_updatable() {
524        projected_catalog_columns
525            .iter()
526            .map(|column| *column || rule_updatable)
527            .collect()
528    } else {
529        vec![false; width]
530    };
531    Ok(ViewUpdatability {
532        automatic,
533        runtime,
534        runtime_insert_columns,
535        runtime_columns,
536        catalog,
537        catalog_insert_columns,
538        catalog_columns,
539        check_option,
540    })
541}
542
543pub fn view_updatability(
544    services: ViewRewriteContext<'_>,
545    name: &str,
546) -> Result<ViewUpdatability, SQLError> {
547    view_updatability_inner(services, name, &mut BTreeSet::new())
548}
549
550fn active_unconditional_instead_rule(
551    services: ViewRewriteContext<'_>,
552    relation: &str,
553    event: crate::ast::RuleEvent,
554) -> Result<bool, SQLError> {
555    Ok(services
556        .catalog
557        .rules_for(relation, event)?
558        .iter()
559        .any(|rule| rule.definition.instead && rule.definition.condition.is_none()))
560}
561
562fn has_unconditional_instead_rule(
563    services: ViewRewriteContext<'_>,
564    relation: &str,
565    event: crate::ast::RuleEvent,
566) -> Result<bool, SQLError> {
567    Ok(services
568        .catalog
569        .rule_definitions_for(relation, event)?
570        .iter()
571        .any(|rule| rule.definition.instead && rule.definition.condition.is_none()))
572}
573
574pub fn validate_view_definition_check_option(
575    services: ViewRewriteContext<'_>,
576    name: &str,
577    definition: &StoredView,
578) -> Result<(), SQLError> {
579    if ViewCheckOption::from_options(&definition.options) == ViewCheckOption::None {
580        return Ok(());
581    }
582    let updatable =
583        automatic_view_layer_from_definition(services, name, definition)?.is_some_and(|layer| {
584            layer
585                .columns
586                .iter()
587                .any(|column| column.writable_source_column.is_some())
588        });
589    if updatable {
590        return Ok(());
591    }
592    Err(SQLError::Routine {
593        sqlstate: "0A000".into(),
594        message: "WITH CHECK OPTION is supported only on automatically updatable views".into(),
595    })
596}