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