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 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 pub fn has_writable_column(&self) -> bool {
130 self.columns
131 .iter()
132 .any(|column| column.writable_source_column.is_some())
133 }
134
135 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
160pub 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
263pub 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
285fn 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
294fn 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
309fn 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
340fn 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 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 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}