uqa_sql/semantics/
catalog_mutation.rs1use crate::catalog::{
10 analysis::AnalysisCatalog,
11 resolution::{RelationLookupMode, RelationNameResolution},
12 VirtualRelation,
13};
14use crate::plan::{CommandPlan, MergeWhenPlan};
15use crate::SQLError;
16
17pub fn virtual_relation_mutation_candidate(command: &CommandPlan) -> bool {
19 command
20 .mutation_target()
21 .is_some_and(|target| session_metadata_relation(target).is_some())
22}
23
24fn session_metadata_relation(target: &str) -> Option<VirtualRelation> {
25 crate::catalog::resolve_virtual_relation(&[], target).filter(|relation| {
26 matches!(
27 relation,
28 VirtualRelation::PgPreparedStatements | VirtualRelation::PgCursors
29 )
30 })
31}
32
33pub fn virtual_relation_mutation_error(
34 catalog: &dyn AnalysisCatalog,
35 resolution: &RelationNameResolution,
36 command: &CommandPlan,
37) -> Result<Option<SQLError>, SQLError> {
38 let Some(target) = command.mutation_target() else {
39 return Ok(None);
40 };
41 let Some(relation) = session_metadata_relation(target) else {
42 return Ok(None);
43 };
44 let bound = match command {
45 CommandPlan::Insert(plan) => plan.target_relation_bound,
46 CommandPlan::Update(plan) => plan.target_relation_bound,
47 CommandPlan::Delete(plan) => plan.target_relation_bound,
48 _ => false,
49 };
50 let mut resolution = resolution.clone();
51 if bound {
52 resolution.set_lookup_mode(RelationLookupMode::Bound);
53 }
54 if catalog
55 .virtual_relation_schema(&resolution, target)?
56 .is_none()
57 {
58 return Ok(None);
59 }
60 Ok(mutation_error(command, relation))
61}
62
63fn mutation_error(command: &CommandPlan, relation: VirtualRelation) -> Option<SQLError> {
64 let action = match command {
65 CommandPlan::Insert(_) => "insert into",
66 CommandPlan::Update(_) => "update",
67 CommandPlan::Delete(_) => "delete from",
68 CommandPlan::Merge(merge) => merge.when_clauses.iter().find_map(|clause| match clause {
69 MergeWhenPlan::InsertNotMatched { .. } => Some("insert into"),
70 MergeWhenPlan::UpdateMatched { .. }
71 | MergeWhenPlan::UpdateNotMatchedBySource { .. } => Some("update"),
72 MergeWhenPlan::DeleteMatched { .. }
73 | MergeWhenPlan::DeleteNotMatchedBySource { .. } => Some("delete from"),
74 _ => None,
75 })?,
76 _ => return None,
77 };
78 Some(SQLError::Routine {
79 sqlstate: "55000".into(),
80 message: format!("cannot {action} view \"{}\"", relation.name()),
81 })
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn session_catalog_mutations_retain_each_views_name_and_operation() {
90 for relation in [
91 VirtualRelation::PgCursors,
92 VirtualRelation::PgPreparedStatements,
93 ] {
94 for (sql, action) in [
95 (
96 format!("INSERT INTO {} (name) VALUES ('x')", relation.name()),
97 "insert into",
98 ),
99 (
100 format!("UPDATE {} SET name = 'x'", relation.name()),
101 "update",
102 ),
103 (format!("DELETE FROM {}", relation.name()), "delete from"),
104 ] {
105 let crate::plan::UnifiedPlan::Command(command) =
106 crate::plan::UnifiedPlan::lower(crate::compile(&sql).unwrap().remove(0))
107 else {
108 panic!("mutation fixture produced a query");
109 };
110 assert!(virtual_relation_mutation_candidate(&command));
111 let error = mutation_error(&command, relation).unwrap();
112 assert_eq!(error.sqlstate(), Some("55000"));
113 assert_eq!(
114 error.to_string(),
115 format!("cannot {action} view \"{}\"", relation.name())
116 );
117 }
118 }
119 assert_eq!(session_metadata_relation("public.pg_cursors"), None);
120 assert_eq!(session_metadata_relation("pg_catalog.\"PG_CURSORS\""), None);
121 }
122}