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(relation) = crate::catalog::VirtualRelation::from_qualified_name(relation) {
169        return Ok(relation
170            .schema()
171            .into_iter()
172            .map(|(name, _)| name)
173            .collect());
174    }
175    if let Some(view) = services.catalog.view_definition(relation)? {
176        let schema = stored_view_schema(services, &view)?;
177        return Ok(schema
178            .columns()
179            .iter()
180            .enumerate()
181            .map(|(position, column)| {
182                schema
183                    .public_name(position)
184                    .map_or_else(|| column.clone(), str::to_string)
185            })
186            .collect());
187    }
188    let definitions = services
189        .catalog
190        .try_describe_table(relation)
191        .map_err(|error| SQLError::Internal(format!("describe view source `{relation}`: {error}")))?
192        .ok_or_else(|| SQLError::UnknownTable(relation.to_string()))?;
193    if definitions.is_empty() {
194        return services
195            .catalog
196            .try_table_columns(relation)
197            .map_err(|error| {
198                SQLError::Internal(format!("describe view source `{relation}`: {error}"))
199            });
200    }
201    Ok(definitions
202        .into_iter()
203        .map(|definition| definition.name)
204        .collect())
205}
206
207fn source_qualifier_matches(qualifier: &str, source_qualifier: &str, source_name: &str) -> bool {
208    qualifier == source_qualifier
209        || qualifier == source_name
210        || RelationIdentity::from_legacy_name(source_name)
211            .is_ok_and(|identity| qualifier == identity.name)
212}
213
214fn direct_source_column(
215    expression: &ScalarExpr,
216    source_qualifier: &str,
217    source_name: &str,
218    source_columns: &BTreeMap<String, String>,
219) -> Option<String> {
220    let column = match expression {
221        ScalarExpr::Column(column) => Some(column.clone()),
222        ScalarExpr::QualifiedColumn { qualifier, column }
223            if source_qualifier_matches(qualifier, source_qualifier, source_name) =>
224        {
225            Some(column.clone())
226        }
227        _ => None,
228    }?;
229    source_columns.get(&column).cloned()
230}
231
232pub fn automatic_view_layer(
233    services: ViewRewriteContext<'_>,
234    name: &str,
235) -> Result<Option<AutomaticViewLayer>, SQLError> {
236    let Some(canonical_name) = services
237        .catalog
238        .try_resolve_view_name(name)
239        .map_err(|error| SQLError::Internal(format!("resolve DML view `{name}`: {error}")))?
240    else {
241        return Ok(None);
242    };
243    let definition = services
244        .catalog
245        .view_definition(&canonical_name)?
246        .ok_or_else(|| SQLError::UnknownTable(name.to_string()))?;
247    automatic_view_layer_from_definition(services, &canonical_name, &definition)
248}
249
250#[expect(
251    clippy::too_many_lines,
252    reason = "preserves view qualifier and row identity"
253)]
254fn automatic_view_layer_from_definition(
255    services: ViewRewriteContext<'_>,
256    canonical_name: &str,
257    definition: &StoredView,
258) -> Result<Option<AutomaticViewLayer>, SQLError> {
259    if definition.kind != StoredViewKind::View || !definition.query.ctes.is_empty() {
260        return Ok(None);
261    }
262    let RelationalPlan::QueryBlock(block) = &definition.query.root else {
263        return Ok(None);
264    };
265    if !matches!(block.compute, ComputePlan::Project)
266        || !block.group_by.is_empty()
267        || !block.grouping_sets.is_empty()
268        || block.group_distinct
269        || block.having.is_some()
270        || block.limit.is_some()
271        || block.with_ties
272        || block.offset.is_some()
273        || block.distinct
274        || !block.distinct_on.is_empty()
275        || !block.locking.is_empty()
276    {
277        return Ok(None);
278    }
279    let Some(
280        source_plan @ SourcePlan::Table {
281            name: source_name,
282            qualifier,
283            alias,
284            column_aliases,
285            include_descendants,
286            ..
287        },
288    ) = block.from.as_ref()
289    else {
290        return Ok(None);
291    };
292    let analysis_scope = services.catalog.binding_scope()?;
293    let source_schema = crate::semantics::view_rewrite::context::analyze_source_plan_schema(
294        services,
295        source_plan,
296        &[],
297        &analysis_scope,
298        None,
299    )?;
300    let resolver = crate::binding::scoped_types::BindingTypeResolver {
301        routines: services.catalog,
302        scope: &analysis_scope,
303    };
304    for projection in &block.projections {
305        if crate::semantics::sets::validation::expression_may_return_set(
306            services.catalog,
307            &resolver,
308            &projection.expr,
309            &source_schema,
310            &[],
311        )? {
312            return Ok(None);
313        }
314    }
315    let source_qualifier = alias.as_deref().unwrap_or(qualifier).to_string();
316    let source_columns = relation_columns(services, source_name)?;
317    let mut visible_source_columns = source_columns.clone();
318    for (column, alias) in visible_source_columns.iter_mut().zip(column_aliases) {
319        column.clone_from(alias);
320    }
321    let source_column_map = visible_source_columns
322        .iter()
323        .cloned()
324        .zip(source_columns.iter().cloned())
325        .collect::<BTreeMap<_, _>>();
326    let mut expressions = Vec::new();
327    for projection in &block.projections {
328        match &projection.expr {
329            ScalarExpr::Star => {
330                expressions.extend(
331                    visible_source_columns
332                        .iter()
333                        .cloned()
334                        .map(ScalarExpr::Column),
335                );
336            }
337            ScalarExpr::QualifiedStar(star_qualifier)
338                if source_qualifier_matches(star_qualifier, &source_qualifier, source_name) =>
339            {
340                expressions.extend(visible_source_columns.iter().cloned().map(|column| {
341                    ScalarExpr::QualifiedColumn {
342                        qualifier: source_qualifier.clone(),
343                        column,
344                    }
345                }));
346            }
347            expression => expressions.push(expression.clone()),
348        }
349    }
350    let schema = stored_view_schema(services, definition)?;
351    let output_columns = schema
352        .columns()
353        .iter()
354        .enumerate()
355        .map(|(position, column)| schema.public_name(position).unwrap_or(column).to_string())
356        .collect::<Vec<_>>();
357    if expressions.len() != output_columns.len() {
358        return Err(SQLError::Internal(format!(
359            "view `{canonical_name}` has {} stored projections for {} output columns",
360            expressions.len(),
361            output_columns.len()
362        )));
363    }
364    let columns = output_columns
365        .into_iter()
366        .zip(expressions)
367        .map(|(name, expression)| ViewColumn {
368            writable_source_column: direct_source_column(
369                &expression,
370                &source_qualifier,
371                source_name,
372                &source_column_map,
373            ),
374            name,
375            expression,
376        })
377        .collect();
378    Ok(Some(AutomaticViewLayer {
379        canonical_name: canonical_name.to_string(),
380        source_name: source_name.clone(),
381        source_qualifier,
382        source_column_map,
383        source_include_descendants: *include_descendants,
384        source_schema,
385        columns,
386        predicate: block.r#where.clone(),
387        subqueries: block.subqueries.clone(),
388        check_option: ViewCheckOption::from_options(&definition.options),
389    }))
390}
391
392mod layer_rewrite;
393use layer_rewrite::embed_layer_expression;
394
395pub fn has_instead_of_trigger(
396    services: ViewRewriteContext<'_>,
397    view: &str,
398    event: TriggerEvent,
399) -> Result<bool, SQLError> {
400    let Some(canonical) = services
401        .catalog
402        .try_resolve_view_name(view)
403        .map_err(|error| SQLError::Internal(format!("resolve DML view `{view}`: {error}")))?
404    else {
405        return Ok(false);
406    };
407    instead_of_trigger_definition(services, &canonical, event)
408}
409
410#[expect(
411    clippy::too_many_lines,
412    reason = "preserves view qualifier and row identity"
413)]
414fn view_updatability_inner(
415    services: ViewRewriteContext<'_>,
416    name: &str,
417    visited: &mut BTreeSet<String>,
418) -> Result<ViewUpdatability, SQLError> {
419    let definition = services
420        .catalog
421        .view_definition(name)?
422        .ok_or_else(|| SQLError::UnknownTable(name.to_string()))?;
423    let schema = stored_view_schema(services, &definition)?;
424    let width = schema.len();
425    let check_option = ViewCheckOption::from_options(&definition.options)
426        .catalog_value()
427        .to_string();
428    let mut automatic = ViewMutationCapabilities::default();
429    let mut automatic_insert_columns = vec![false; width];
430    let mut automatic_columns = vec![false; width];
431    let mut projected_catalog = ViewMutationCapabilities::default();
432    let mut projected_catalog_insert_columns = vec![false; width];
433    let mut projected_catalog_columns = vec![false; width];
434    if let Some(layer) = automatic_view_layer(services, name)? {
435        if visited.insert(layer.canonical_name.clone()) {
436            if services
437                .catalog
438                .view_definition(&layer.source_name)?
439                .is_some()
440            {
441                let source = view_updatability_inner(services, &layer.source_name, visited)?;
442                let source_columns = relation_columns(services, &layer.source_name)?;
443                let mapped = |source_capabilities: &[bool]| {
444                    layer
445                        .columns
446                        .iter()
447                        .map(|column| {
448                            column.writable_source_column.as_ref().is_some_and(|name| {
449                                source_columns
450                                    .iter()
451                                    .position(|candidate| candidate == name)
452                                    .is_some_and(|position| {
453                                        source_capabilities.get(position) == Some(&true)
454                                    })
455                            })
456                        })
457                        .collect::<Vec<_>>()
458                };
459                automatic_insert_columns = mapped(&source.runtime_insert_columns);
460                automatic_columns = mapped(&source.runtime_columns);
461                automatic = ViewMutationCapabilities {
462                    insertable: source.runtime.insertable
463                        && automatic_insert_columns.iter().any(|value| *value),
464                    updatable: source.runtime.updatable
465                        && automatic_columns.iter().any(|value| *value),
466                    deletable: source.runtime.deletable,
467                };
468                projected_catalog_insert_columns = mapped(&source.catalog_insert_columns);
469                projected_catalog_columns = mapped(&source.catalog_columns);
470                projected_catalog = ViewMutationCapabilities {
471                    insertable: source.catalog.insertable
472                        && projected_catalog_insert_columns.iter().any(|value| *value),
473                    updatable: source.catalog.updatable
474                        && projected_catalog_columns.iter().any(|value| *value),
475                    deletable: source.catalog.deletable,
476                };
477            } else {
478                automatic_insert_columns = layer
479                    .columns
480                    .iter()
481                    .map(|column| column.writable_source_column.is_some())
482                    .collect();
483                automatic_columns.clone_from(&automatic_insert_columns);
484                automatic = ViewMutationCapabilities {
485                    insertable: automatic_insert_columns.iter().any(|value| *value),
486                    updatable: automatic_columns.iter().any(|value| *value),
487                    deletable: true,
488                };
489                projected_catalog = automatic;
490                projected_catalog_insert_columns.clone_from(&automatic_insert_columns);
491                projected_catalog_columns.clone_from(&automatic_columns);
492            }
493            visited.remove(&layer.canonical_name);
494        }
495    }
496    let active_insert =
497        active_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Insert)?;
498    let active_update =
499        active_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Update)?;
500    let active_delete =
501        active_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Delete)?;
502    let runtime = ViewMutationCapabilities {
503        insertable: automatic.insertable || active_insert,
504        updatable: automatic.updatable || active_update,
505        deletable: automatic.deletable || active_delete,
506    };
507    let runtime_insert_columns = automatic_insert_columns
508        .iter()
509        .map(|column| *column || active_insert)
510        .collect();
511    let runtime_columns = automatic_columns
512        .iter()
513        .map(|column| *column || active_update)
514        .collect();
515    let rule_insertable =
516        has_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Insert)?;
517    let rule_updatable =
518        has_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Update)?;
519    let rule_deletable =
520        has_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Delete)?;
521    let catalog = ViewMutationCapabilities {
522        insertable: projected_catalog.insertable || rule_insertable,
523        updatable: projected_catalog.updatable || rule_updatable,
524        deletable: projected_catalog.deletable || rule_deletable,
525    };
526    let catalog_insert_columns = projected_catalog_insert_columns
527        .iter()
528        .map(|column| *column || rule_insertable)
529        .collect();
530    let catalog_columns = if catalog.fully_updatable() {
531        projected_catalog_columns
532            .iter()
533            .map(|column| *column || rule_updatable)
534            .collect()
535    } else {
536        vec![false; width]
537    };
538    Ok(ViewUpdatability {
539        automatic,
540        runtime,
541        runtime_insert_columns,
542        runtime_columns,
543        catalog,
544        catalog_insert_columns,
545        catalog_columns,
546        check_option,
547    })
548}
549
550pub fn view_updatability(
551    services: ViewRewriteContext<'_>,
552    name: &str,
553) -> Result<ViewUpdatability, SQLError> {
554    view_updatability_inner(services, name, &mut BTreeSet::new())
555}
556
557fn active_unconditional_instead_rule(
558    services: ViewRewriteContext<'_>,
559    relation: &str,
560    event: crate::ast::RuleEvent,
561) -> Result<bool, SQLError> {
562    Ok(services
563        .catalog
564        .rules_for(relation, event)?
565        .iter()
566        .any(|rule| rule.definition.instead && rule.definition.condition.is_none()))
567}
568
569fn has_unconditional_instead_rule(
570    services: ViewRewriteContext<'_>,
571    relation: &str,
572    event: crate::ast::RuleEvent,
573) -> Result<bool, SQLError> {
574    Ok(services
575        .catalog
576        .rule_definitions_for(relation, event)?
577        .iter()
578        .any(|rule| rule.definition.instead && rule.definition.condition.is_none()))
579}
580
581pub fn validate_view_definition_check_option(
582    services: ViewRewriteContext<'_>,
583    name: &str,
584    definition: &StoredView,
585) -> Result<(), SQLError> {
586    if ViewCheckOption::from_options(&definition.options) == ViewCheckOption::None {
587        return Ok(());
588    }
589    let updatable =
590        automatic_view_layer_from_definition(services, name, definition)?.is_some_and(|layer| {
591            layer
592                .columns
593                .iter()
594                .any(|column| column.writable_source_column.is_some())
595        });
596    if updatable {
597        return Ok(());
598    }
599    Err(SQLError::Routine {
600        sqlstate: "0A000".into(),
601        message: "WITH CHECK OPTION is supported only on automatically updatable views".into(),
602    })
603}