1use 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 pub fn has_writable_column(&self) -> bool {
129 self.columns
130 .iter()
131 .any(|column| column.writable_source_column.is_some())
132 }
133
134 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
159pub 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
262pub 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
284fn 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
293fn 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
308fn 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
339fn 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 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 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}