Skip to main content

uqa_sql/semantics/view_rewrite/
updatability.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Why a view cannot be rewritten onto its base relation, and the errors that report it as `RewriteQuery`, `rewriteTargetView` and `error_view_not_updatable` do.
8
9use super::{display_relation, SQLError};
10
11/// Why a view cannot be rewritten onto its base relation. `RewriteQuery` tests conditional `INSTEAD` rules first, and `view_query_is_auto_updatable` tests the rest in the order listed.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum NotUpdatableReason {
14    ConditionalInsteadRule,
15    Distinct,
16    GroupBy,
17    Having,
18    SetOperation,
19    With,
20    LimitOffset,
21    Aggregate,
22    Window,
23    SetReturning,
24    NotSingleRelation,
25    /// INSERT and UPDATE need an updatable column; DELETE does not.
26    NoUpdatableColumns,
27}
28
29impl NotUpdatableReason {
30    /// The DETAIL that reports the reason.
31    pub const fn detail(self) -> &'static str {
32        match self {
33            Self::ConditionalInsteadRule => {
34                "Views with conditional DO INSTEAD rules are not automatically updatable."
35            }
36            Self::Distinct => "Views containing DISTINCT are not automatically updatable.",
37            Self::GroupBy => "Views containing GROUP BY are not automatically updatable.",
38            Self::Having => "Views containing HAVING are not automatically updatable.",
39            Self::SetOperation => {
40                "Views containing UNION, INTERSECT, or EXCEPT are not automatically updatable."
41            }
42            Self::With => "Views containing WITH are not automatically updatable.",
43            Self::LimitOffset => "Views containing LIMIT or OFFSET are not automatically updatable.",
44            Self::Aggregate => {
45                "Views that return aggregate functions are not automatically updatable."
46            }
47            Self::Window => "Views that return window functions are not automatically updatable.",
48            Self::SetReturning => {
49                "Views that return set-returning functions are not automatically updatable."
50            }
51            Self::NotSingleRelation => {
52                "Views that do not select from a single table or view are not automatically updatable."
53            }
54            Self::NoUpdatableColumns => {
55                "Views that have no updatable columns are not automatically updatable."
56            }
57        }
58    }
59}
60
61/// Why a view column cannot be written, as `view_col_is_auto_updatable` reports it.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum ColumnRestriction {
64    /// The column is an expression, not a column of the base relation.
65    Computed,
66    /// The column refers to a system column of the base relation.
67    SystemColumn,
68}
69
70impl ColumnRestriction {
71    const fn detail(self) -> &'static str {
72        match self {
73            Self::Computed => {
74                "View columns that are not columns of their base relation are not updatable."
75            }
76            Self::SystemColumn => "View columns that refer to system columns are not updatable.",
77        }
78    }
79}
80
81/// The command a view must perform, by itself or as an action of a MERGE.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum ViewCommand {
84    Insert,
85    Update,
86    Delete,
87    MergeInsert,
88    MergeUpdate,
89    MergeDelete,
90}
91
92/// `error_view_not_updatable`: `55000` naming the command, the reason as DETAIL, and as HINT what would let the command through the view; MERGE supports no rules, so its hint names only a trigger.
93pub fn view_not_updatable(
94    view: &str,
95    command: ViewCommand,
96    reason: NotUpdatableReason,
97) -> SQLError {
98    let view = display_relation(view);
99    let (message, hint) = match command {
100        ViewCommand::Insert => (
101            format!("cannot insert into view \"{view}\""),
102            "To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.",
103        ),
104        ViewCommand::Update => (
105            format!("cannot update view \"{view}\""),
106            "To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.",
107        ),
108        ViewCommand::Delete => (
109            format!("cannot delete from view \"{view}\""),
110            "To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.",
111        ),
112        ViewCommand::MergeInsert => (
113            format!("cannot insert into view \"{view}\""),
114            "To enable inserting into the view using MERGE, provide an INSTEAD OF INSERT trigger.",
115        ),
116        ViewCommand::MergeUpdate => (
117            format!("cannot update view \"{view}\""),
118            "To enable updating the view using MERGE, provide an INSTEAD OF UPDATE trigger.",
119        ),
120        ViewCommand::MergeDelete => (
121            format!("cannot delete from view \"{view}\""),
122            "To enable deleting from the view using MERGE, provide an INSTEAD OF DELETE trigger.",
123        ),
124    };
125    SQLError::Diagnostic {
126        sqlstate: "55000".into(),
127        message,
128        detail: Some(reason.detail().into()),
129        hint: Some(hint.into()),
130    }
131}
132
133/// The statement whose write of a view column a view rejects.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub enum ColumnWrite {
136    Insert,
137    Update,
138    Merge,
139}
140
141/// `rewriteTargetView`'s `0A000` for a statement that writes a column the view cannot write, with the reason as DETAIL.
142pub fn non_writable_column(
143    view: &str,
144    column: &str,
145    write: ColumnWrite,
146    restriction: ColumnRestriction,
147) -> SQLError {
148    let verb = match write {
149        ColumnWrite::Insert => "insert into",
150        ColumnWrite::Update => "update",
151        ColumnWrite::Merge => "merge into",
152    };
153    SQLError::Diagnostic {
154        sqlstate: "0A000".into(),
155        message: format!(
156            "cannot {verb} column \"{column}\" of view \"{}\"",
157            display_relation(view)
158        ),
159        detail: Some(restriction.detail().into()),
160        hint: None,
161    }
162}
163
164/// `rewriteTargetView`'s `0A000` for a MERGE some of whose actions have an INSTEAD OF trigger on an automatically updatable view while others do not.
165pub fn mixed_merge_paths(view: &str) -> SQLError {
166    SQLError::Diagnostic {
167        sqlstate: "0A000".into(),
168        message: format!("cannot merge into view \"{}\"", display_relation(view)),
169        detail: Some(
170            "MERGE is not supported for views with INSTEAD OF triggers for some actions but not all."
171                .into(),
172        ),
173        hint: Some(
174            "To enable merging into the view, either provide a full set of INSTEAD OF triggers or drop the existing INSTEAD OF triggers."
175                .into(),
176        ),
177    }
178}
179
180/// The `0A000` of a MERGE whose target, or a view it is rewritten through, has rules, which `transformMergeStmt` and `RewriteQuery` reject.
181pub fn merge_with_rules(relation: &str) -> SQLError {
182    SQLError::Diagnostic {
183        sqlstate: "0A000".into(),
184        message: format!(
185            "cannot execute MERGE on relation \"{}\"",
186            display_relation(relation)
187        ),
188        detail: Some("MERGE is not supported for relations with rules.".into()),
189        hint: None,
190    }
191}
192
193/// `transformMergeStmt`'s `0A000` for a MERGE into a materialized view, with `errdetail_relkind_not_supported`'s DETAIL.
194pub fn merge_into_materialized_view(relation: &str) -> SQLError {
195    SQLError::Diagnostic {
196        sqlstate: "0A000".into(),
197        message: format!(
198            "cannot execute MERGE on relation \"{}\"",
199            display_relation(relation)
200        ),
201        detail: Some("This operation is not supported for materialized views.".into()),
202        hint: None,
203    }
204}