Skip to main content

uqa_sql/semantics/
generated_values.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! `PostgreSQL` rules for the values a statement writes to the columns whose values a table generates: identity columns and generated columns.
8
9use crate::{
10    assignment::columns::AssignmentColumnCatalog,
11    ast::{AutoIncrementKind, OverridingKind},
12    SQLError,
13};
14
15/// How a table generates a column's value.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17enum Generation {
18    IdentityAlways,
19    IdentityByDefault,
20    Expression,
21}
22
23/// The identity and generated columns of one table, in column order, read once for a statement that writes it. `PostgreSQL` applies these rules when it rewrites the statement, column by column in table order, before it reads or writes a row, so a statement whose source yields no row is rejected as well.
24pub struct GeneratedValueColumns {
25    columns: Vec<(String, Generation)>,
26}
27
28impl GeneratedValueColumns {
29    /// The identity and generated columns of `table`; none for a relation the catalog does not describe as a table.
30    pub fn of(catalog: &dyn AssignmentColumnCatalog, table: &str) -> Result<Self, SQLError> {
31        let columns = catalog
32            .try_describe_table(table)
33            .map_err(|error| SQLError::Internal(format!("read generated columns: {error}")))?
34            .unwrap_or_default()
35            .into_iter()
36            .filter_map(|column| {
37                let generation = match column.auto_increment.as_ref() {
38                    Some(provenance) if provenance.kind == AutoIncrementKind::IdentityAlways => {
39                        Generation::IdentityAlways
40                    }
41                    Some(provenance) if provenance.is_identity() => Generation::IdentityByDefault,
42                    _ if column.generated.is_some() => Generation::Expression,
43                    _ => return None,
44                };
45                Some((column.name, generation))
46            })
47            .collect();
48        Ok(Self { columns })
49    }
50
51    /// Whether `column` is an identity column, whose value `OVERRIDING USER VALUE` leaves to its sequence.
52    #[must_use]
53    pub fn contains(&self, column: &str) -> bool {
54        self.columns.iter().any(|(name, generation)| {
55            name == column
56                && matches!(
57                    generation,
58                    Generation::IdentityAlways | Generation::IdentityByDefault
59                )
60        })
61    }
62
63    /// Reject a value an `INSERT` supplies for a generated column, and for a `GENERATED ALWAYS` identity column without an `OVERRIDING` clause, which a generated column ignores. `targets` names each target column with whether some row supplies a value other than `DEFAULT` for it.
64    pub fn validate_insert<'a>(
65        &self,
66        targets: impl IntoIterator<Item = (&'a str, bool)>,
67        overriding: Option<OverridingKind>,
68    ) -> Result<(), SQLError> {
69        let supplied = targets
70            .into_iter()
71            .filter_map(|(column, supplied)| supplied.then_some(column))
72            .collect::<std::collections::BTreeSet<_>>();
73        for (column, generation) in &self.columns {
74            if !supplied.contains(column.as_str()) {
75                continue;
76            }
77            match generation {
78                Generation::Expression => return Err(generated_column_insert_error(column)),
79                Generation::IdentityAlways if overriding.is_none() => {
80                    return Err(generated_always_insert_error(column));
81                }
82                Generation::IdentityAlways | Generation::IdentityByDefault => {}
83            }
84        }
85        Ok(())
86    }
87
88    /// Reject an assignment other than `DEFAULT` to a generated column or a `GENERATED ALWAYS` identity column by an `UPDATE`, an `ON CONFLICT DO UPDATE` or a `MERGE` update. `assignments` names each assigned column with whether its value is `DEFAULT`.
89    pub fn validate_update<'a>(
90        &self,
91        assignments: impl IntoIterator<Item = (&'a str, bool)>,
92    ) -> Result<(), SQLError> {
93        let assigned = assignments
94            .into_iter()
95            .filter_map(|(column, default)| (!default).then_some(column))
96            .collect::<std::collections::BTreeSet<_>>();
97        for (column, generation) in &self.columns {
98            if !assigned.contains(column.as_str()) {
99                continue;
100            }
101            match generation {
102                Generation::Expression => return Err(generated_column_update_error(column)),
103                Generation::IdentityAlways => return Err(generated_always_update_error(column)),
104                Generation::IdentityByDefault => {}
105            }
106        }
107        Ok(())
108    }
109}
110
111/// The `428C9` error of a value an `INSERT` supplies for a `GENERATED ALWAYS` identity column.
112#[must_use]
113pub fn generated_always_insert_error(column: &str) -> SQLError {
114    SQLError::Diagnostic {
115        sqlstate: "428C9".into(),
116        message: format!("cannot insert a non-DEFAULT value into column \"{column}\""),
117        detail: Some(format!(
118            "Column \"{column}\" is an identity column defined as GENERATED ALWAYS."
119        )),
120        hint: Some("Use OVERRIDING SYSTEM VALUE to override.".into()),
121    }
122}
123
124/// The `428C9` error of an assignment other than `DEFAULT` to a `GENERATED ALWAYS` identity column.
125#[must_use]
126pub fn generated_always_update_error(column: &str) -> SQLError {
127    SQLError::Diagnostic {
128        sqlstate: "428C9".into(),
129        message: format!("column \"{column}\" can only be updated to DEFAULT"),
130        detail: Some(format!(
131            "Column \"{column}\" is an identity column defined as GENERATED ALWAYS."
132        )),
133        hint: None,
134    }
135}
136
137/// The `428C9` error of a value an `INSERT` supplies for a generated column.
138#[must_use]
139pub fn generated_column_insert_error(column: &str) -> SQLError {
140    SQLError::Diagnostic {
141        sqlstate: "428C9".into(),
142        message: format!("cannot insert a non-DEFAULT value into column \"{column}\""),
143        detail: Some(format!("Column \"{column}\" is a generated column.")),
144        hint: None,
145    }
146}
147
148/// The `428C9` error of an assignment other than `DEFAULT` to a generated column.
149#[must_use]
150pub fn generated_column_update_error(column: &str) -> SQLError {
151    SQLError::Diagnostic {
152        sqlstate: "428C9".into(),
153        message: format!("column \"{column}\" can only be updated to DEFAULT"),
154        detail: Some(format!("Column \"{column}\" is a generated column.")),
155        hint: None,
156    }
157}