1use super::updatability::{
10 merge_into_materialized_view, merge_with_rules, mixed_merge_paths, non_writable_column,
11 view_not_updatable, ColumnWrite, NotUpdatableReason, ViewCommand,
12};
13use super::{
14 instead_of_trigger_definition, view_query_shape, AutomaticViewLayer, BTreeSet,
15 ConflictActionPlan, ConflictPlan, DeletePlan, InsertPlan, MergePlan, MergeWhenPlan,
16 ReturningAliases, RowSchema, SQLError, ScalarExpr, TriggerEvent, UpdatePlan, ViewColumn,
17 ViewQueryShape, ViewRewriteContext,
18};
19
20pub(super) fn layer_column<'a>(
21 layer: &'a AutomaticViewLayer,
22 name: &str,
23) -> Option<&'a ViewColumn> {
24 layer.columns.iter().find(|column| column.name == name)
25}
26
27fn unknown_view_column(layer: &AutomaticViewLayer, column: &str) -> SQLError {
28 SQLError::UnknownColumn(format!("{}.{column}", layer.canonical_name))
29}
30
31pub(super) fn duplicate_insert_column(column: &str) -> SQLError {
32 SQLError::Routine {
33 sqlstate: "42701".into(),
34 message: format!("column \"{column}\" specified more than once"),
35 }
36}
37
38pub(super) fn duplicate_assignment(column: &str) -> SQLError {
39 SQLError::Routine {
40 sqlstate: "42601".into(),
41 message: format!("multiple assignments to same column \"{column}\""),
42 }
43}
44
45fn validate_view_target_columns<'a>(
46 layer: &AutomaticViewLayer,
47 targets: impl IntoIterator<Item = &'a crate::ast::AssignmentTarget<ScalarExpr>>,
48 duplicate: fn(&str) -> SQLError,
49) -> Result<(), SQLError> {
50 let mut seen = std::collections::BTreeMap::new();
51 for target in targets {
52 if layer_column(layer, &target.column).is_none() {
53 return Err(unknown_view_column(layer, &target.column));
54 }
55 let whole = target.is_whole_column();
56 if seen
57 .insert(&target.column, whole)
58 .is_some_and(|previous| previous || whole)
59 {
60 return Err(duplicate(&target.column));
61 }
62 }
63 Ok(())
64}
65
66pub(super) fn validate_mapped_columns(
67 targets: &[crate::ast::AssignmentTarget<ScalarExpr>],
68 duplicate: fn(&str) -> SQLError,
69) -> Result<(), SQLError> {
70 let mut seen = std::collections::BTreeMap::new();
71 for target in targets {
72 let whole = target.is_whole_column();
73 if seen
74 .insert(&target.column, whole)
75 .is_some_and(|previous| previous || whole)
76 {
77 return Err(duplicate(&target.column));
78 }
79 }
80 Ok(())
81}
82
83pub(super) fn validate_insert_targets(
84 layer: &AutomaticViewLayer,
85 plan: &InsertPlan,
86) -> Result<(), SQLError> {
87 validate_view_target_columns(layer, plan.columns.iter(), duplicate_insert_column)?;
88 let Some(conflict) = &plan.on_conflict else {
89 return Ok(());
90 };
91 for column in &conflict.conflict_columns {
92 if layer_column(layer, column).is_none() {
93 return Err(unknown_view_column(layer, column));
94 }
95 }
96 if let ConflictActionPlan::Update { assignments, .. } = &conflict.action {
97 validate_view_target_columns(
98 layer,
99 assignments
100 .iter()
101 .flat_map(|assignment| assignment.target.targets()),
102 duplicate_assignment,
103 )?;
104 }
105 Ok(())
106}
107
108pub(super) fn validate_update_targets(
109 layer: &AutomaticViewLayer,
110 plan: &UpdatePlan,
111) -> Result<(), SQLError> {
112 validate_view_target_columns(
113 layer,
114 plan.assignments
115 .iter()
116 .flat_map(|assignment| assignment.target.targets()),
117 duplicate_assignment,
118 )
119}
120
121pub(super) fn writable_column(
122 layer: &AutomaticViewLayer,
123 name: &str,
124 write: ColumnWrite,
125) -> Result<String, SQLError> {
126 let column = layer_column(layer, name)
127 .ok_or_else(|| SQLError::UnknownColumn(format!("{}.{name}", layer.canonical_name)))?;
128 column.writable_source_column.clone().ok_or_else(|| {
129 non_writable_column(
130 &layer.canonical_name,
131 name,
132 write,
133 layer.column_restriction(column),
134 )
135 })
136}
137
138pub(super) fn validate_writable_columns<'a>(
140 layer: &AutomaticViewLayer,
141 modified: impl IntoIterator<Item = &'a str>,
142 write: ColumnWrite,
143) -> Result<(), SQLError> {
144 let modified = modified.into_iter().collect::<BTreeSet<_>>();
145 match layer.columns.iter().find(|column| {
146 column.writable_source_column.is_none() && modified.contains(column.name.as_str())
147 }) {
148 Some(column) => Err(non_writable_column(
149 &layer.canonical_name,
150 &column.name,
151 write,
152 layer.column_restriction(column),
153 )),
154 None => Ok(()),
155 }
156}
157
158fn ambiguous_column(column: &str) -> SQLError {
159 SQLError::Routine {
160 sqlstate: "42702".into(),
161 message: format!("column reference \"{column}\" is ambiguous"),
162 }
163}
164
165#[derive(Clone, Copy)]
166pub(super) struct ExpressionScope<'a> {
167 pub(super) target_qualifier: &'a str,
168 pub(super) returning_aliases: Option<&'a ReturningAliases>,
169 pub(super) source: Option<&'a RowSchema>,
170 pub(super) include_excluded: bool,
171}
172
173impl ExpressionScope<'_> {
174 pub(super) fn row_image_qualifier(self, qualifier: &str) -> bool {
175 self.returning_aliases.is_some_and(|aliases| {
176 [
177 (aliases.old.as_str(), aliases.old_explicit),
178 (aliases.new.as_str(), aliases.new_explicit),
179 ]
180 .into_iter()
181 .any(|(alias, explicit)| {
182 qualifier == alias
183 && (explicit
184 || !self
185 .source
186 .is_some_and(|source| source.has_qualifier(alias)))
187 })
188 })
189 }
190
191 pub(super) fn target_qualifier(self, qualifier: &str) -> bool {
192 qualifier == self.target_qualifier
193 || (self.include_excluded && qualifier == "excluded")
194 || self.row_image_qualifier(qualifier)
195 }
196}
197
198pub(super) fn validate_view_expression(
199 expression: &ScalarExpr,
200 layer: &AutomaticViewLayer,
201 scope: ExpressionScope<'_>,
202) -> Result<(), SQLError> {
203 let mut expression = expression.clone();
204 let mut error = None;
205 crate::plan::rewrite_scalar_expression(&mut expression, &mut |node| {
206 if error.is_some() {
207 return;
208 }
209 match node {
210 ScalarExpr::Column(column) => {
211 let target = layer_column(layer, column).is_some();
212 let source = scope
213 .source
214 .is_some_and(|source| source.has_unqualified_column(column));
215 if target && (source || scope.include_excluded) {
216 error = Some(ambiguous_column(column));
217 } else if !target && !source {
218 error = Some(SQLError::UnknownColumn(column.clone()));
219 }
220 }
221 ScalarExpr::QualifiedColumn { qualifier, column }
222 if scope.target_qualifier(qualifier) && layer_column(layer, column).is_none() =>
223 {
224 error = Some(SQLError::UnknownColumn(format!("{qualifier}.{column}")));
225 }
226 _ => {}
227 }
228 });
229 error.map_or(Ok(()), Err)
230}
231
232fn public_view_columns(
233 services: ViewRewriteContext<'_>,
234 view: &str,
235) -> Result<BTreeSet<String>, SQLError> {
236 let definition = services
237 .catalog
238 .view_definition(view)?
239 .ok_or_else(|| SQLError::UnknownTable(view.to_string()))?;
240 let schema = stored_view_schema(services, &definition)?;
241 Ok(schema
242 .columns()
243 .iter()
244 .enumerate()
245 .map(|(position, column)| schema.public_name(position).unwrap_or(column).to_string())
246 .collect())
247}
248
249fn validate_public_view_expression(
250 expression: &ScalarExpr,
251 public_columns: &BTreeSet<String>,
252 scope: ExpressionScope<'_>,
253) -> Result<(), SQLError> {
254 let mut expression = expression.clone();
255 let mut error = None;
256 crate::plan::rewrite_scalar_expression(&mut expression, &mut |node| {
257 if error.is_some() {
258 return;
259 }
260 match node {
261 ScalarExpr::Column(column) => {
262 let target = public_columns.contains(column);
263 let source = scope
264 .source
265 .is_some_and(|source| source.has_unqualified_column(column));
266 if target && (source || scope.include_excluded) {
267 error = Some(ambiguous_column(column));
268 } else if !target && !source {
269 error = Some(SQLError::UnknownColumn(column.clone()));
270 }
271 }
272 ScalarExpr::QualifiedColumn { qualifier, column }
273 if scope.target_qualifier(qualifier) && !public_columns.contains(column) =>
274 {
275 error = Some(SQLError::UnknownColumn(format!("{qualifier}.{column}")));
276 }
277 _ => {}
278 }
279 });
280 error.map_or(Ok(()), Err)
281}
282
283pub(super) fn validate_public_update_contract(
284 services: ViewRewriteContext<'_>,
285 plan: &UpdatePlan,
286 source: Option<&RowSchema>,
287) -> Result<(), SQLError> {
288 let columns = public_view_columns(services, &plan.table)?;
289 let ordinary_scope = ExpressionScope {
290 target_qualifier: &plan.target_qualifier,
291 returning_aliases: None,
292 source,
293 include_excluded: false,
294 };
295 for assignment in &plan.assignments {
296 for expression in assignment.expressions() {
297 validate_public_view_expression(expression, &columns, ordinary_scope)?;
298 }
299 }
300 if let Some(predicate) = plan.predicate.as_ref() {
301 validate_public_view_expression(predicate, &columns, ordinary_scope)?;
302 }
303 let returning_scope = ExpressionScope {
304 returning_aliases: Some(&plan.returning_aliases),
305 ..ordinary_scope
306 };
307 for projection in &plan.returning {
308 validate_public_view_expression(&projection.expr, &columns, returning_scope)?;
309 }
310 Ok(())
311}
312
313pub(super) fn validate_public_delete_contract(
314 services: ViewRewriteContext<'_>,
315 plan: &DeletePlan,
316 source: Option<&RowSchema>,
317) -> Result<(), SQLError> {
318 let columns = public_view_columns(services, &plan.table)?;
319 let ordinary_scope = ExpressionScope {
320 target_qualifier: &plan.target_qualifier,
321 returning_aliases: None,
322 source,
323 include_excluded: false,
324 };
325 if let Some(predicate) = plan.predicate.as_ref() {
326 validate_public_view_expression(predicate, &columns, ordinary_scope)?;
327 }
328 let returning_scope = ExpressionScope {
329 returning_aliases: Some(&plan.returning_aliases),
330 ..ordinary_scope
331 };
332 for projection in &plan.returning {
333 validate_public_view_expression(&projection.expr, &columns, returning_scope)?;
334 }
335 Ok(())
336}
337
338pub(super) fn validate_public_insert_contract(
339 services: ViewRewriteContext<'_>,
340 plan: &InsertPlan,
341) -> Result<(), SQLError> {
342 let columns = public_view_columns(services, &plan.table)?;
343 for predicate in plan.on_conflict.iter().flat_map(|conflict| {
344 conflict
345 .expressions
346 .iter()
347 .chain(conflict.predicate.iter().map(Box::as_ref))
348 }) {
349 validate_public_view_expression(
350 predicate,
351 &columns,
352 ExpressionScope {
353 target_qualifier: &plan.target_qualifier,
354 returning_aliases: None,
355 source: None,
356 include_excluded: false,
357 },
358 )?;
359 }
360 if let Some(ConflictPlan {
361 action:
362 ConflictActionPlan::Update {
363 assignments,
364 predicate,
365 },
366 ..
367 }) = &plan.on_conflict
368 {
369 let scope = ExpressionScope {
370 target_qualifier: &plan.target_qualifier,
371 returning_aliases: None,
372 source: None,
373 include_excluded: true,
374 };
375 for assignment in assignments {
376 for expression in assignment.expressions() {
377 validate_public_view_expression(expression, &columns, scope)?;
378 }
379 }
380 if let Some(predicate) = predicate {
381 validate_public_view_expression(predicate, &columns, scope)?;
382 }
383 }
384 let scope = ExpressionScope {
385 target_qualifier: &plan.target_qualifier,
386 returning_aliases: Some(&plan.returning_aliases),
387 source: None,
388 include_excluded: false,
389 };
390 for projection in &plan.returning {
391 validate_public_view_expression(&projection.expr, &columns, scope)?;
392 }
393 Ok(())
394}
395
396pub fn validate_public_merge_contract(
397 services: ViewRewriteContext<'_>,
398 plan: &MergePlan,
399 source: &RowSchema,
400) -> Result<(), SQLError> {
401 let columns = public_view_columns(services, &plan.target)?;
402 let matched_scope = ExpressionScope {
403 target_qualifier: &plan.target_qualifier,
404 returning_aliases: None,
405 source: Some(source),
406 include_excluded: false,
407 };
408 let target_only_scope = ExpressionScope {
409 source: None,
410 ..matched_scope
411 };
412 validate_public_view_expression(&plan.join_condition, &columns, matched_scope)?;
413 for clause in &plan.when_clauses {
414 match clause {
415 MergeWhenPlan::UpdateMatched {
416 condition,
417 assignments,
418 } => {
419 if let Some(condition) = condition {
420 validate_public_view_expression(condition, &columns, matched_scope)?;
421 }
422 for assignment in assignments {
423 for expression in assignment.expressions() {
424 validate_public_view_expression(expression, &columns, matched_scope)?;
425 }
426 }
427 }
428 MergeWhenPlan::DeleteMatched { condition }
429 | MergeWhenPlan::NothingMatched { condition } => {
430 if let Some(condition) = condition {
431 validate_public_view_expression(condition, &columns, matched_scope)?;
432 }
433 }
434 MergeWhenPlan::UpdateNotMatchedBySource {
435 condition,
436 assignments,
437 } => {
438 if let Some(condition) = condition {
439 validate_public_view_expression(condition, &columns, target_only_scope)?;
440 }
441 for assignment in assignments {
442 validate_public_view_expression(
443 &assignment.value,
444 &columns,
445 target_only_scope,
446 )?;
447 }
448 }
449 MergeWhenPlan::DeleteNotMatchedBySource { condition }
450 | MergeWhenPlan::NothingNotMatchedBySource { condition } => {
451 if let Some(condition) = condition {
452 validate_public_view_expression(condition, &columns, target_only_scope)?;
453 }
454 }
455 MergeWhenPlan::InsertNotMatched { .. } | MergeWhenPlan::NothingNotMatched { .. } => {}
456 }
457 }
458 let returning_scope = ExpressionScope {
459 returning_aliases: Some(&plan.returning_aliases),
460 ..matched_scope
461 };
462 for projection in &plan.returning {
463 validate_public_view_expression(&projection.expr, &columns, returning_scope)?;
464 }
465 Ok(())
466}
467
468pub(super) fn validate_merge_targets(
469 layer: &AutomaticViewLayer,
470 plan: &MergePlan,
471) -> Result<(), SQLError> {
472 for clause in &plan.when_clauses {
473 match clause {
474 MergeWhenPlan::UpdateMatched { assignments, .. }
475 | MergeWhenPlan::UpdateNotMatchedBySource { assignments, .. } => {
476 validate_view_target_columns(
477 layer,
478 assignments
479 .iter()
480 .flat_map(|assignment| assignment.target.targets()),
481 duplicate_assignment,
482 )?;
483 }
484 MergeWhenPlan::InsertNotMatched { columns, .. } if !columns.is_empty() => {
485 validate_view_target_columns(layer, columns.iter(), duplicate_insert_column)?;
486 }
487 _ => {}
488 }
489 }
490 Ok(())
491}
492
493pub fn validate_public_merge_targets(
494 services: ViewRewriteContext<'_>,
495 plan: &MergePlan,
496) -> Result<(), SQLError> {
497 for clause in &plan.when_clauses {
498 match clause {
499 MergeWhenPlan::UpdateMatched { assignments, .. }
500 | MergeWhenPlan::UpdateNotMatchedBySource { assignments, .. } => {
501 validate_public_view_targets(
502 services,
503 &plan.target,
504 assignments
505 .iter()
506 .flat_map(|assignment| assignment.target.column_names()),
507 )?;
508 validate_mapped_columns(
509 &assignments
510 .iter()
511 .flat_map(|assignment| assignment.target.targets().iter().cloned())
512 .collect::<Vec<_>>(),
513 duplicate_assignment,
514 )?;
515 }
516 MergeWhenPlan::InsertNotMatched { columns, .. } if !columns.is_empty() => {
517 validate_public_view_targets(
518 services,
519 &plan.target,
520 columns.iter().map(|target| target.column.as_str()),
521 )?;
522 validate_mapped_columns(columns, duplicate_insert_column)?;
523 }
524 _ => {}
525 }
526 }
527 Ok(())
528}
529
530fn validate_merge_rule_free(
531 services: ViewRewriteContext<'_>,
532 relation: &str,
533) -> Result<(), SQLError> {
534 let has_rules = [
535 crate::ast::RuleEvent::Insert,
536 crate::ast::RuleEvent::Update,
537 crate::ast::RuleEvent::Delete,
538 ]
539 .into_iter()
540 .map(|event| services.catalog.rules_for(relation, event))
541 .collect::<Result<Vec<_>, SQLError>>()?
542 .iter()
543 .any(|rules| !rules.is_empty());
544 if !has_rules {
545 return Ok(());
546 }
547 Err(merge_with_rules(relation))
548}
549
550#[derive(Debug, Clone, Copy, PartialEq, Eq)]
551pub enum MergeViewTargetPath {
552 AutomaticRewrite,
553 ViewTriggers,
554}
555
556pub fn merge_view_target_path(
558 services: ViewRewriteContext<'_>,
559 plan: &MergePlan,
560) -> Result<MergeViewTargetPath, SQLError> {
561 let canonical = services
562 .catalog
563 .try_resolve_view_name(&plan.target)
564 .map_err(|error| SQLError::Internal(format!("resolve MERGE view: {error}")))?
565 .ok_or_else(|| SQLError::UnknownTable(plan.target.clone()))?;
566 let definition = services
567 .catalog
568 .view_definition(&canonical)?
569 .ok_or_else(|| SQLError::UnknownTable(plan.target.clone()))?;
570 if definition.kind == crate::catalog::view::StoredViewKind::Materialized {
571 return Err(merge_into_materialized_view(&canonical));
572 }
573 validate_merge_rule_free(services, &canonical)?;
574 let mut untriggered = None;
575 let mut triggered = false;
576 for clause in &plan.when_clauses {
577 let Some((event, command)) = merge_clause_command(clause) else {
578 continue;
579 };
580 if instead_of_trigger_definition(services, &canonical, event)? {
581 triggered = true;
582 } else if untriggered.is_none() {
583 untriggered = Some(command);
584 }
585 }
586 let Some(untriggered) = untriggered else {
587 return Ok(MergeViewTargetPath::ViewTriggers);
588 };
589 let writes_columns = plan.when_clauses.iter().any(|clause| {
590 matches!(
591 clause,
592 MergeWhenPlan::InsertNotMatched { .. }
593 | MergeWhenPlan::UpdateMatched { .. }
594 | MergeWhenPlan::UpdateNotMatchedBySource { .. }
595 )
596 });
597 let reason = match view_query_shape(services, &canonical)? {
598 Some(ViewQueryShape::Updatable(layer)) => (writes_columns && !layer.has_writable_column())
599 .then_some(NotUpdatableReason::NoUpdatableColumns),
600 Some(ViewQueryShape::NotUpdatable(reason)) => Some(reason),
601 None => return Err(SQLError::UnknownTable(plan.target.clone())),
602 };
603 if let Some(reason) = reason {
604 return Err(view_not_updatable(&canonical, untriggered, reason));
605 }
606 if triggered {
607 return Err(mixed_merge_paths(&canonical));
608 }
609 Ok(MergeViewTargetPath::AutomaticRewrite)
610}
611
612fn merge_clause_command(clause: &MergeWhenPlan) -> Option<(TriggerEvent, ViewCommand)> {
614 match clause {
615 MergeWhenPlan::InsertNotMatched { .. } => {
616 Some((TriggerEvent::Insert, ViewCommand::MergeInsert))
617 }
618 MergeWhenPlan::UpdateMatched { .. } | MergeWhenPlan::UpdateNotMatchedBySource { .. } => {
619 Some((TriggerEvent::Update, ViewCommand::MergeUpdate))
620 }
621 MergeWhenPlan::DeleteMatched { .. } | MergeWhenPlan::DeleteNotMatchedBySource { .. } => {
622 Some((TriggerEvent::Delete, ViewCommand::MergeDelete))
623 }
624 MergeWhenPlan::NothingMatched { .. }
625 | MergeWhenPlan::NothingNotMatched { .. }
626 | MergeWhenPlan::NothingNotMatchedBySource { .. } => None,
627 }
628}
629
630pub(super) fn validate_public_view_targets<'a>(
631 services: ViewRewriteContext<'_>,
632 view: &str,
633 columns: impl IntoIterator<Item = &'a str>,
634) -> Result<(), SQLError> {
635 let definition = services
636 .catalog
637 .view_definition(view)?
638 .ok_or_else(|| SQLError::UnknownTable(view.to_string()))?;
639 let schema = stored_view_schema(services, &definition)?;
640 for column in columns {
641 if !schema.has_unqualified_column(column) {
642 return Err(SQLError::UnknownColumn(format!("{view}.{column}")));
643 }
644 }
645 Ok(())
646}
647
648pub(super) fn validate_direct_view_rule_path(
650 services: ViewRewriteContext<'_>,
651 view: &str,
652 event: crate::ast::RuleEvent,
653 command: ViewCommand,
654) -> Result<(), SQLError> {
655 let rules = services.catalog.rules_for(view, event)?;
656 let has_conditional_instead = rules
657 .iter()
658 .any(|rule| rule.definition.instead && rule.definition.condition.is_some());
659 let has_unconditional_instead = rules
660 .iter()
661 .any(|rule| rule.definition.instead && rule.definition.condition.is_none());
662 if has_conditional_instead && !has_unconditional_instead {
663 return Err(view_not_updatable(
664 view,
665 command,
666 NotUpdatableReason::ConditionalInsteadRule,
667 ));
668 }
669 Ok(())
670}
671
672use super::stored_view_schema;