Skip to main content

uqa_sql/schema/constraint_changes/
not_null_removal.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Protect primary keys and identity columns when removing NOT NULL constraints.
8
9use super::constraint_error;
10use crate::{
11    ast::{ColumnDef, TableConstraintSet, TableKeyConstraintKind},
12    SQLError,
13};
14
15pub fn validate_constraint_removal(
16    table: &str,
17    column: &ColumnDef,
18    constraints: &TableConstraintSet,
19) -> Result<(), SQLError> {
20    if column.primary_key
21        || constraints.key_constraints.iter().any(|key| {
22            key.kind == TableKeyConstraintKind::PrimaryKey && key.columns.contains(&column.name)
23        })
24    {
25        return Err(constraint_error(
26            "42P16",
27            format!("column \"{}\" is in a primary key", column.name),
28        ));
29    }
30    reject_identity(table, column, "55000")
31}
32
33/// `ATExecDropNotNull`'s checks of a NOT NULL column: an identity column must stay NOT NULL, and so must a partition's column while its parent's is.
34pub fn validate_column_removal(
35    table: &str,
36    column: &ColumnDef,
37    parent_not_null: bool,
38) -> Result<(), SQLError> {
39    if !column.not_null {
40        return Ok(());
41    }
42    reject_identity(table, column, "42601")?;
43    if parent_not_null {
44        return Err(constraint_error(
45            "42P16",
46            format!(
47                "column \"{}\" is marked NOT NULL in parent table",
48                column.name
49            ),
50        ));
51    }
52    Ok(())
53}
54
55fn reject_identity(table: &str, column: &ColumnDef, sqlstate: &str) -> Result<(), SQLError> {
56    if !column
57        .auto_increment
58        .as_ref()
59        .is_some_and(|definition| definition.is_identity())
60    {
61        return Ok(());
62    }
63    let relation =
64        uqa_core::RelationIdentity::from_legacy_name(table).map_err(SQLError::Internal)?;
65    Err(constraint_error(
66        sqlstate,
67        format!(
68            "column \"{}\" of relation \"{}\" is an identity column",
69            column.name, relation.name
70        ),
71    ))
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn identity_not_null_removal_distinguishes_column_and_constraint_syntax() {
80        let crate::Statement::CreateTable(table) = crate::compile(
81            "CREATE TABLE t(v integer GENERATED ALWAYS AS IDENTITY CONSTRAINT nn NOT NULL)",
82        )
83        .unwrap()
84        .remove(0) else {
85            panic!("table");
86        };
87        let column = &table.columns[0];
88        let constraints = TableConstraintSet::default();
89        assert_eq!(
90            validate_constraint_removal("public.t", column, &constraints)
91                .unwrap_err()
92                .sqlstate(),
93            Some("55000")
94        );
95        assert_eq!(
96            validate_column_removal("public.t", column, false)
97                .unwrap_err()
98                .sqlstate(),
99            Some("42601")
100        );
101        let mut keyed = column.clone();
102        keyed.primary_key = true;
103        assert_eq!(
104            validate_constraint_removal("public.t", &keyed, &constraints)
105                .unwrap_err()
106                .sqlstate(),
107            Some("42P16")
108        );
109    }
110
111    #[test]
112    fn serial_and_nullable_columns_do_not_inherit_identity_protection() {
113        let crate::Statement::CreateTable(table) =
114            crate::compile("CREATE TABLE t(v serial CONSTRAINT nn NOT NULL, nullable integer)")
115                .unwrap()
116                .remove(0)
117        else {
118            panic!("table");
119        };
120        for column in &table.columns {
121            validate_constraint_removal("public.t", column, &TableConstraintSet::default())
122                .unwrap();
123            validate_column_removal("public.t", column, false).unwrap();
124        }
125    }
126}