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 layer_privileges;
31mod returning;
32mod rewrite_insert;
33mod rewrite_merge;
34mod rewrite_update_delete;
35pub mod rule_inputs;
36mod updatability;
37mod validation;
38
39use correlation::{
40    collect_expression_subquery_ids, delete_ordinary_subquery_ids, dml_analysis_scope,
41    dml_source_schema, insert_conflict_subquery_ids, insert_input_width,
42    merge_matched_subquery_ids, merge_target_only_subquery_ids, returning_subquery_ids,
43    rewrite_correlated_dml_context, schema_public_columns, update_ordinary_subquery_ids,
44    validate_delete_expressions, validate_insert_expressions, validate_merge_expressions,
45    validate_update_expressions, CorrelatedDmlContext,
46};
47use layer_privileges::LayerPrivileges;
48use returning::{
49    add_check_option, bind_unqualified_source_positions, combine_view_predicate, dml_target_width,
50    finalize_source_returning, instead_of_trigger_definition, preserve_view_rule_returning,
51    record_view_rule_relation, retarget_source_expression, rewrite_existing_view_checks,
52    rewrite_merge_returning, rewrite_returning, rewrite_target_expression,
53};
54pub use rewrite_insert::rewrite_insert_to_base;
55pub use rewrite_merge::rewrite_merge_to_base;
56pub use rewrite_update_delete::{rewrite_delete_to_base, rewrite_update_to_base};
57pub use rule_inputs::{rule_input_requirements, validate_grouped_rule_inputs};
58pub use updatability::NotUpdatableReason;
59use updatability::{view_not_updatable, ColumnRestriction, ColumnWrite, ViewCommand};
60use validation::{
61    duplicate_assignment, duplicate_insert_column, layer_column, validate_direct_view_rule_path,
62    validate_insert_targets, validate_mapped_columns, validate_merge_targets,
63    validate_public_delete_contract, validate_public_insert_contract,
64    validate_public_update_contract, validate_public_view_targets, validate_update_targets,
65    validate_view_expression, validate_writable_columns, writable_column, ExpressionScope,
66};
67pub use validation::{merge_view_target_path, MergeViewTargetPath};
68pub use validation::{validate_public_merge_contract, validate_public_merge_targets};
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum ViewCheckOption {
72    None,
73    Local,
74    Cascaded,
75}
76
77impl ViewCheckOption {
78    fn from_options(options: &[(String, String)]) -> Self {
79        options
80            .iter()
81            .rev()
82            .find(|(name, _)| name == "check_option")
83            .map_or(Self::None, |(_, value)| match value.as_str() {
84                "local" => Self::Local,
85                "cascaded" => Self::Cascaded,
86                _ => Self::None,
87            })
88    }
89
90    const fn catalog_value(self) -> &'static str {
91        match self {
92            Self::None => "NONE",
93            Self::Local => "LOCAL",
94            Self::Cascaded => "CASCADED",
95        }
96    }
97}
98
99#[derive(Debug, Clone)]
100pub struct ViewColumn {
101    pub name: String,
102    pub collation_oid: Option<u32>,
103    pub expression: ScalarExpr,
104    pub writable_source_column: Option<String>,
105}
106
107#[derive(Debug, Clone)]
108pub struct AutomaticViewLayer {
109    pub canonical_name: String,
110    pub source_name: String,
111    pub source_qualifier: String,
112    pub source_column_map: BTreeMap<String, String>,
113    pub source_include_descendants: bool,
114    pub source_schema: RowSchema,
115    pub columns: Vec<ViewColumn>,
116    pub predicate: Option<ScalarExpr>,
117    pub subqueries: Vec<QueryPlan>,
118    pub check_option: ViewCheckOption,
119}
120
121impl AutomaticViewLayer {
122    pub fn physical_source_column<'a>(&'a self, column: &'a str) -> &'a str {
123        self.source_column_map
124            .get(column)
125            .map_or(column, String::as_str)
126    }
127
128    /// Whether a column of the view is a column of its base relation, which INSERT and UPDATE need and DELETE does not.
129    pub fn has_writable_column(&self) -> bool {
130        self.columns
131            .iter()
132            .any(|column| column.writable_source_column.is_some())
133    }
134
135    /// Why `column`, which is not a column of the base relation, cannot be written, as `view_col_is_auto_updatable` reports it.
136    fn column_restriction(&self, column: &ViewColumn) -> ColumnRestriction {
137        let name = match &column.expression {
138            ScalarExpr::Column(name) => Some(name),
139            ScalarExpr::QualifiedColumn { qualifier, column }
140                if source_qualifier_matches(
141                    qualifier,
142                    &self.source_qualifier,
143                    &self.source_name,
144                ) =>
145            {
146                Some(column)
147            }
148            _ => None,
149        };
150        if name.is_some_and(|name| {
151            crate::schema::columns::POSTGRES_SYSTEM_COLUMNS.contains(&name.as_str())
152        }) {
153            ColumnRestriction::SystemColumn
154        } else {
155            ColumnRestriction::Computed
156        }
157    }
158}
159
160/// What `view_query_is_auto_updatable` finds in a view's own query, short of its test for an updatable column, which depends on the command.
161pub enum ViewQueryShape {
162    Updatable(Box<AutomaticViewLayer>),
163    NotUpdatable(NotUpdatableReason),
164}
165
166pub use crate::catalog::view::ViewMutationCapabilities;
167
168#[derive(Debug, Clone)]
169pub struct ViewUpdatability {
170    pub automatic: ViewMutationCapabilities,
171    runtime: ViewMutationCapabilities,
172    runtime_insert_columns: Vec<bool>,
173    runtime_columns: Vec<bool>,
174    pub catalog: ViewMutationCapabilities,
175    catalog_insert_columns: Vec<bool>,
176    pub catalog_columns: Vec<bool>,
177    pub check_option: String,
178}
179
180fn display_relation(name: &str) -> String {
181    RelationIdentity::from_legacy_name(name)
182        .map_or_else(|_| name.to_string(), |relation| relation.name)
183}
184
185pub fn relation_columns(
186    services: ViewRewriteContext<'_>,
187    relation: &str,
188) -> Result<Vec<String>, SQLError> {
189    if let Some(relation) = crate::catalog::VirtualRelation::from_qualified_name(relation) {
190        return Ok(relation
191            .schema()
192            .into_iter()
193            .map(|(name, _)| name)
194            .collect());
195    }
196    if let Some(view) = services.catalog.view_definition(relation)? {
197        let schema = stored_view_schema(services, &view)?;
198        return Ok(schema
199            .columns()
200            .iter()
201            .enumerate()
202            .map(|(position, column)| {
203                schema
204                    .public_name(position)
205                    .map_or_else(|| column.clone(), str::to_string)
206            })
207            .collect());
208    }
209    let definitions = services
210        .catalog
211        .try_describe_table(relation)
212        .map_err(|error| SQLError::Internal(format!("describe view source `{relation}`: {error}")))?
213        .ok_or_else(|| SQLError::UnknownTable(relation.to_string()))?;
214    if definitions.is_empty() {
215        return services
216            .catalog
217            .try_table_columns(relation)
218            .map_err(|error| {
219                SQLError::Internal(format!("describe view source `{relation}`: {error}"))
220            });
221    }
222    Ok(definitions
223        .into_iter()
224        .map(|definition| definition.name)
225        .collect())
226}
227
228fn source_qualifier_matches(qualifier: &str, source_qualifier: &str, source_name: &str) -> bool {
229    qualifier == source_qualifier
230        || qualifier == source_name
231        || RelationIdentity::from_legacy_name(source_name)
232            .is_ok_and(|identity| qualifier == identity.name)
233}
234
235fn direct_source_column(
236    expression: &ScalarExpr,
237    source_qualifier: &str,
238    source_name: &str,
239    source_columns: &BTreeMap<String, String>,
240) -> Option<String> {
241    let column = match expression {
242        ScalarExpr::Column(column) => Some(column.clone()),
243        ScalarExpr::QualifiedColumn { qualifier, column }
244            if source_qualifier_matches(qualifier, source_qualifier, source_name) =>
245        {
246            Some(column.clone())
247        }
248        _ => None,
249    }?;
250    source_columns.get(&column).cloned()
251}
252
253pub fn automatic_view_layer(
254    services: ViewRewriteContext<'_>,
255    name: &str,
256) -> Result<Option<AutomaticViewLayer>, SQLError> {
257    Ok(match view_query_shape(services, name)? {
258        Some(ViewQueryShape::Updatable(layer)) => Some(*layer),
259        Some(ViewQueryShape::NotUpdatable(_)) | None => None,
260    })
261}
262
263/// The shape of the query of view `name`, or `None` when `name` names no regular view.
264pub fn view_query_shape(
265    services: ViewRewriteContext<'_>,
266    name: &str,
267) -> Result<Option<ViewQueryShape>, SQLError> {
268    let Some(canonical_name) = services
269        .catalog
270        .try_resolve_view_name(name)
271        .map_err(|error| SQLError::Internal(format!("resolve DML view `{name}`: {error}")))?
272    else {
273        return Ok(None);
274    };
275    let definition = services
276        .catalog
277        .view_definition(&canonical_name)?
278        .ok_or_else(|| SQLError::UnknownTable(name.to_string()))?;
279    if definition.kind != StoredViewKind::View {
280        return Ok(None);
281    }
282    view_query_shape_from_definition(services, &canonical_name, &definition).map(Some)
283}
284
285/// The catalog name of view `name`.
286fn canonical_view_name(services: ViewRewriteContext<'_>, name: &str) -> Result<String, SQLError> {
287    services
288        .catalog
289        .try_resolve_view_name(name)
290        .map_err(|error| SQLError::Internal(format!("resolve DML view `{name}`: {error}")))?
291        .ok_or_else(|| SQLError::UnknownTable(name.to_string()))
292}
293
294/// The layer of view `view` that `command` is rewritten through, or the error `rewriteTargetView` reports when the view's query does not allow the rewrite.
295fn rewritable_layer(
296    services: ViewRewriteContext<'_>,
297    view: &str,
298    command: ViewCommand,
299) -> Result<AutomaticViewLayer, SQLError> {
300    match view_query_shape(services, view)? {
301        Some(ViewQueryShape::Updatable(layer)) => Ok(*layer),
302        Some(ViewQueryShape::NotUpdatable(reason)) => {
303            Err(view_not_updatable(view, command, reason))
304        }
305        None => Err(SQLError::UnknownTable(view.to_string())),
306    }
307}
308
309/// The next layer the rewrite of `command` passes through at view `view`: the first layer, checked already, while `initial` holds it; `None` where an unconditional INSTEAD rule of a view that cannot be rewritten handles the command. As `RewriteQuery` does, conditional INSTEAD rules without an unconditional one are rejected before the view's query is examined.
310fn next_rewritten_layer(
311    services: ViewRewriteContext<'_>,
312    view: &str,
313    initial: &mut Option<AutomaticViewLayer>,
314    rewrite_suppressed: bool,
315    event: crate::ast::RuleEvent,
316    command: ViewCommand,
317) -> Result<Option<AutomaticViewLayer>, SQLError> {
318    if let Some(layer) = initial.take() {
319        return Ok(Some(layer));
320    }
321    if !rewrite_suppressed {
322        validate_direct_view_rule_path(services, view, event, command)?;
323    }
324    match view_query_shape(services, view)? {
325        Some(ViewQueryShape::Updatable(layer)) => Ok(Some(*layer)),
326        shape => {
327            if active_unconditional_instead_rule(services, view, event)? {
328                return Ok(None);
329            }
330            Err(match shape {
331                Some(ViewQueryShape::NotUpdatable(reason)) => {
332                    view_not_updatable(view, command, reason)
333                }
334                _ => SQLError::UnknownTable(view.to_string()),
335            })
336        }
337    }
338}
339
340/// The reason `view_query_is_auto_updatable` finds first in the clauses of a query block, before it looks at the block's target list and source.
341fn query_block_restriction(
342    services: ViewRewriteContext<'_>,
343    query: &QueryPlan,
344    block: &crate::plan::QueryBlockPlan,
345) -> Option<NotUpdatableReason> {
346    if block.distinct || !block.distinct_on.is_empty() {
347        Some(NotUpdatableReason::Distinct)
348    } else if !block.group_by.is_empty() || !block.grouping_sets.is_empty() || block.group_distinct
349    {
350        Some(NotUpdatableReason::GroupBy)
351    } else if block.having.is_some() {
352        Some(NotUpdatableReason::Having)
353    } else if !query.ctes.is_empty() {
354        Some(NotUpdatableReason::With)
355    } else if block.limit.is_some() || block.offset.is_some() || block.with_ties {
356        Some(NotUpdatableReason::LimitOffset)
357    } else if matches!(block.compute, ComputePlan::Aggregate)
358        || (matches!(block.compute, ComputePlan::Window)
359            && crate::semantics::aggregates::has_aggregate(
360                &|name: &str| services.routines.is_registered_aggregate(name),
361                &block.projections,
362            ))
363    {
364        Some(NotUpdatableReason::Aggregate)
365    } else if matches!(block.compute, ComputePlan::Window) {
366        Some(NotUpdatableReason::Window)
367    } else {
368        None
369    }
370}
371
372#[expect(
373    clippy::too_many_lines,
374    reason = "preserves view qualifier and row identity"
375)]
376fn view_query_shape_from_definition(
377    services: ViewRewriteContext<'_>,
378    canonical_name: &str,
379    definition: &StoredView,
380) -> Result<ViewQueryShape, SQLError> {
381    let block = match &definition.query.root {
382        RelationalPlan::QueryBlock(block) => block,
383        RelationalPlan::SetOp { .. } => {
384            return Ok(ViewQueryShape::NotUpdatable(
385                NotUpdatableReason::SetOperation,
386            ))
387        }
388        RelationalPlan::Values { .. } => {
389            return Ok(ViewQueryShape::NotUpdatable(
390                NotUpdatableReason::NotSingleRelation,
391            ))
392        }
393    };
394    if let Some(reason) = query_block_restriction(services, &definition.query, block) {
395        return Ok(ViewQueryShape::NotUpdatable(reason));
396    }
397    let analysis_scope = services.catalog.binding_scope()?;
398    let source_schema = match block.from.as_ref() {
399        Some(source) => crate::semantics::view_rewrite::context::analyze_source_plan_schema(
400            services,
401            source,
402            &[],
403            &analysis_scope,
404            None,
405        )?,
406        None => RowSchema::default(),
407    };
408    let resolver = crate::binding::scoped_types::BindingTypeResolver {
409        routines: services.routines,
410        scope: &analysis_scope,
411    };
412    for projection in &block.projections {
413        if crate::semantics::sets::validation::expression_may_return_set(
414            services.routines,
415            &resolver,
416            &projection.expr,
417            &source_schema,
418            &[],
419        )? {
420            return Ok(ViewQueryShape::NotUpdatable(
421                NotUpdatableReason::SetReturning,
422            ));
423        }
424    }
425    // The base relation must be one table or view; a materialized view is neither.
426    let Some(SourcePlan::Table {
427        name: source_name,
428        qualifier,
429        alias,
430        column_aliases,
431        include_descendants,
432        ..
433    }) = block.from.as_ref()
434    else {
435        return Ok(ViewQueryShape::NotUpdatable(
436            NotUpdatableReason::NotSingleRelation,
437        ));
438    };
439    if services.catalog.target_view_kind(source_name)? == Some(StoredViewKind::Materialized) {
440        return Ok(ViewQueryShape::NotUpdatable(
441            NotUpdatableReason::NotSingleRelation,
442        ));
443    }
444    let source_qualifier = alias.as_deref().unwrap_or(qualifier).to_string();
445    let source_columns = relation_columns(services, source_name)?;
446    let mut visible_source_columns = source_columns.clone();
447    for (column, alias) in visible_source_columns.iter_mut().zip(column_aliases) {
448        column.clone_from(alias);
449    }
450    let source_column_map = visible_source_columns
451        .iter()
452        .cloned()
453        .zip(source_columns.iter().cloned())
454        .collect::<BTreeMap<_, _>>();
455    let mut expressions = Vec::new();
456    for projection in &block.projections {
457        match &projection.expr {
458            ScalarExpr::Star => {
459                expressions.extend(
460                    visible_source_columns
461                        .iter()
462                        .cloned()
463                        .map(ScalarExpr::Column),
464                );
465            }
466            ScalarExpr::QualifiedStar(star_qualifier)
467                if source_qualifier_matches(star_qualifier, &source_qualifier, source_name) =>
468            {
469                expressions.extend(visible_source_columns.iter().cloned().map(|column| {
470                    ScalarExpr::QualifiedColumn {
471                        qualifier: source_qualifier.clone(),
472                        column,
473                    }
474                }));
475            }
476            expression => expressions.push(expression.clone()),
477        }
478    }
479    let schema = stored_view_schema(services, definition)?;
480    let output_columns = schema
481        .columns()
482        .iter()
483        .enumerate()
484        .map(|(position, column)| schema.public_name(position).unwrap_or(column).to_string())
485        .collect::<Vec<_>>();
486    if expressions.len() != output_columns.len() {
487        return Err(SQLError::Internal(format!(
488            "view `{canonical_name}` has {} stored projections for {} output columns",
489            expressions.len(),
490            output_columns.len()
491        )));
492    }
493    let columns = output_columns
494        .into_iter()
495        .zip(expressions)
496        .enumerate()
497        .map(|(position, (name, expression))| ViewColumn {
498            collation_oid: schema.column_collation(position),
499            writable_source_column: direct_source_column(
500                &expression,
501                &source_qualifier,
502                source_name,
503                &source_column_map,
504            ),
505            name,
506            expression,
507        })
508        .collect();
509    Ok(ViewQueryShape::Updatable(Box::new(AutomaticViewLayer {
510        canonical_name: canonical_name.to_string(),
511        source_name: source_name.clone(),
512        source_qualifier,
513        source_column_map,
514        source_include_descendants: *include_descendants,
515        source_schema,
516        columns,
517        predicate: block.r#where.clone(),
518        subqueries: block.subqueries.clone(),
519        check_option: ViewCheckOption::from_options(&definition.options),
520    })))
521}
522
523mod layer_rewrite;
524use layer_rewrite::embed_layer_expression;
525
526pub fn has_instead_of_trigger(
527    services: ViewRewriteContext<'_>,
528    view: &str,
529    event: TriggerEvent,
530) -> Result<bool, SQLError> {
531    let Some(canonical) = services
532        .catalog
533        .try_resolve_view_name(view)
534        .map_err(|error| SQLError::Internal(format!("resolve DML view `{view}`: {error}")))?
535    else {
536        return Ok(false);
537    };
538    instead_of_trigger_definition(services, &canonical, event)
539}
540
541#[expect(
542    clippy::too_many_lines,
543    reason = "preserves view qualifier and row identity"
544)]
545fn view_updatability_inner(
546    services: ViewRewriteContext<'_>,
547    name: &str,
548    visited: &mut BTreeSet<String>,
549) -> Result<ViewUpdatability, SQLError> {
550    let definition = services
551        .catalog
552        .view_definition(name)?
553        .ok_or_else(|| SQLError::UnknownTable(name.to_string()))?;
554    let schema = stored_view_schema(services, &definition)?;
555    let width = schema.len();
556    let check_option = ViewCheckOption::from_options(&definition.options)
557        .catalog_value()
558        .to_string();
559    let mut automatic = ViewMutationCapabilities::default();
560    let mut automatic_insert_columns = vec![false; width];
561    let mut automatic_columns = vec![false; width];
562    let mut projected_catalog = ViewMutationCapabilities::default();
563    let mut projected_catalog_insert_columns = vec![false; width];
564    let mut projected_catalog_columns = vec![false; width];
565    if let Some(layer) = automatic_view_layer(services, name)? {
566        if visited.insert(layer.canonical_name.clone()) {
567            if services
568                .catalog
569                .view_definition(&layer.source_name)?
570                .is_some()
571            {
572                let source = view_updatability_inner(services, &layer.source_name, visited)?;
573                let source_columns = relation_columns(services, &layer.source_name)?;
574                let mapped = |source_capabilities: &[bool]| {
575                    layer
576                        .columns
577                        .iter()
578                        .map(|column| {
579                            column.writable_source_column.as_ref().is_some_and(|name| {
580                                source_columns
581                                    .iter()
582                                    .position(|candidate| candidate == name)
583                                    .is_some_and(|position| {
584                                        source_capabilities.get(position) == Some(&true)
585                                    })
586                            })
587                        })
588                        .collect::<Vec<_>>()
589                };
590                automatic_insert_columns = mapped(&source.runtime_insert_columns);
591                automatic_columns = mapped(&source.runtime_columns);
592                automatic = ViewMutationCapabilities {
593                    insertable: source.runtime.insertable
594                        && automatic_insert_columns.iter().any(|value| *value),
595                    updatable: source.runtime.updatable
596                        && automatic_columns.iter().any(|value| *value),
597                    deletable: source.runtime.deletable,
598                };
599                projected_catalog_insert_columns = mapped(&source.catalog_insert_columns);
600                projected_catalog_columns = mapped(&source.catalog_columns);
601                projected_catalog = ViewMutationCapabilities {
602                    insertable: source.catalog.insertable
603                        && projected_catalog_insert_columns.iter().any(|value| *value),
604                    updatable: source.catalog.updatable
605                        && projected_catalog_columns.iter().any(|value| *value),
606                    deletable: source.catalog.deletable,
607                };
608            } else {
609                automatic_insert_columns = layer
610                    .columns
611                    .iter()
612                    .map(|column| column.writable_source_column.is_some())
613                    .collect();
614                automatic_columns.clone_from(&automatic_insert_columns);
615                automatic = ViewMutationCapabilities {
616                    insertable: automatic_insert_columns.iter().any(|value| *value),
617                    updatable: automatic_columns.iter().any(|value| *value),
618                    deletable: true,
619                };
620                projected_catalog = automatic;
621                projected_catalog_insert_columns.clone_from(&automatic_insert_columns);
622                projected_catalog_columns.clone_from(&automatic_columns);
623            }
624            visited.remove(&layer.canonical_name);
625        }
626    }
627    let active_insert =
628        active_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Insert)?;
629    let active_update =
630        active_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Update)?;
631    let active_delete =
632        active_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Delete)?;
633    let runtime = ViewMutationCapabilities {
634        insertable: automatic.insertable || active_insert,
635        updatable: automatic.updatable || active_update,
636        deletable: automatic.deletable || active_delete,
637    };
638    let runtime_insert_columns = automatic_insert_columns
639        .iter()
640        .map(|column| *column || active_insert)
641        .collect();
642    let runtime_columns = automatic_columns
643        .iter()
644        .map(|column| *column || active_update)
645        .collect();
646    let rule_insertable =
647        has_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Insert)?;
648    let rule_updatable =
649        has_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Update)?;
650    let rule_deletable =
651        has_unconditional_instead_rule(services, name, crate::ast::RuleEvent::Delete)?;
652    let catalog = ViewMutationCapabilities {
653        insertable: projected_catalog.insertable || rule_insertable,
654        updatable: projected_catalog.updatable || rule_updatable,
655        deletable: projected_catalog.deletable || rule_deletable,
656    };
657    let catalog_insert_columns = projected_catalog_insert_columns
658        .iter()
659        .map(|column| *column || rule_insertable)
660        .collect();
661    let catalog_columns = if catalog.fully_updatable() {
662        projected_catalog_columns
663            .iter()
664            .map(|column| *column || rule_updatable)
665            .collect()
666    } else {
667        vec![false; width]
668    };
669    Ok(ViewUpdatability {
670        automatic,
671        runtime,
672        runtime_insert_columns,
673        runtime_columns,
674        catalog,
675        catalog_insert_columns,
676        catalog_columns,
677        check_option,
678    })
679}
680
681pub fn view_updatability(
682    services: ViewRewriteContext<'_>,
683    name: &str,
684) -> Result<ViewUpdatability, SQLError> {
685    view_updatability_inner(services, name, &mut BTreeSet::new())
686}
687
688fn active_unconditional_instead_rule(
689    services: ViewRewriteContext<'_>,
690    relation: &str,
691    event: crate::ast::RuleEvent,
692) -> Result<bool, SQLError> {
693    Ok(services
694        .catalog
695        .rules_for(relation, event)?
696        .iter()
697        .any(|rule| rule.definition.instead && rule.definition.condition.is_none()))
698}
699
700fn has_unconditional_instead_rule(
701    services: ViewRewriteContext<'_>,
702    relation: &str,
703    event: crate::ast::RuleEvent,
704) -> Result<bool, SQLError> {
705    Ok(services
706        .catalog
707        .rule_definitions_for(relation, event)?
708        .iter()
709        .any(|rule| rule.definition.instead && rule.definition.condition.is_none()))
710}
711
712pub fn validate_view_definition_check_option(
713    services: ViewRewriteContext<'_>,
714    name: &str,
715    definition: &StoredView,
716) -> Result<(), SQLError> {
717    if ViewCheckOption::from_options(&definition.options) == ViewCheckOption::None {
718        return Ok(());
719    }
720    // `DefineView` reports why the view is not automatically updatable as the hint.
721    let reason = match view_query_shape_from_definition(services, name, definition)? {
722        ViewQueryShape::Updatable(layer) if layer.has_writable_column() => return Ok(()),
723        ViewQueryShape::Updatable(_) => NotUpdatableReason::NoUpdatableColumns,
724        ViewQueryShape::NotUpdatable(reason) => reason,
725    };
726    Err(SQLError::Diagnostic {
727        sqlstate: "0A000".into(),
728        message: "WITH CHECK OPTION is supported only on automatically updatable views".into(),
729        detail: None,
730        hint: Some(reason.detail().into()),
731    })
732}