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
33pub fn validate_column_removal(table: &str, column: &ColumnDef) -> Result<(), SQLError> {
34    if !column.not_null {
35        return Ok(());
36    }
37    reject_identity(table, column, "42601")
38}
39
40fn reject_identity(table: &str, column: &ColumnDef, sqlstate: &str) -> Result<(), SQLError> {
41    if !column
42        .auto_increment
43        .as_ref()
44        .is_some_and(|definition| definition.is_identity())
45    {
46        return Ok(());
47    }
48    let relation =
49        uqa_core::RelationIdentity::from_legacy_name(table).map_err(SQLError::Internal)?;
50    Err(constraint_error(
51        sqlstate,
52        format!(
53            "column \"{}\" of relation \"{}\" is an identity column",
54            column.name, relation.name
55        ),
56    ))
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn identity_not_null_removal_distinguishes_column_and_constraint_syntax() {
65        let crate::Statement::CreateTable(table) = crate::compile(
66            "CREATE TABLE t(v integer GENERATED ALWAYS AS IDENTITY CONSTRAINT nn NOT NULL)",
67        )
68        .unwrap()
69        .remove(0) else {
70            panic!("table");
71        };
72        let column = &table.columns[0];
73        let constraints = TableConstraintSet::default();
74        assert_eq!(
75            validate_constraint_removal("public.t", column, &constraints)
76                .unwrap_err()
77                .sqlstate(),
78            Some("55000")
79        );
80        assert_eq!(
81            validate_column_removal("public.t", column)
82                .unwrap_err()
83                .sqlstate(),
84            Some("42601")
85        );
86        let mut keyed = column.clone();
87        keyed.primary_key = true;
88        assert_eq!(
89            validate_constraint_removal("public.t", &keyed, &constraints)
90                .unwrap_err()
91                .sqlstate(),
92            Some("42P16")
93        );
94    }
95
96    #[test]
97    fn serial_and_nullable_columns_do_not_inherit_identity_protection() {
98        let crate::Statement::CreateTable(table) =
99            crate::compile("CREATE TABLE t(v serial CONSTRAINT nn NOT NULL, nullable integer)")
100                .unwrap()
101                .remove(0)
102        else {
103            panic!("table");
104        };
105        for column in &table.columns {
106            validate_constraint_removal("public.t", column, &TableConstraintSet::default())
107                .unwrap();
108            validate_column_removal("public.t", column).unwrap();
109        }
110    }
111}