Skip to main content

uqa_sql/semantics/
catalog_mutation.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Mutation rules for SQL virtual catalog relations.
8
9use crate::catalog::{
10    analysis::AnalysisCatalog,
11    resolution::{RelationLookupMode, RelationNameResolution},
12    VirtualRelation,
13};
14use crate::plan::{CommandPlan, MergeWhenPlan};
15use crate::SQLError;
16
17/// Check spelling before requesting a catalog snapshot; an earlier user relation can still take precedence.
18pub 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}