Skip to main content

uqa_sql/schema/constraint_changes/
inheritance.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Match inherited constraints and select removal behavior from their remaining origins.
8
9use super::{constraint_error, find_constraint, ConstraintLocation};
10use crate::{
11    ast::{ColumnDef, TableConstraintSet},
12    SQLError,
13};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum InheritedConstraintKey<'a> {
17    Check(&'a str),
18    NotNull(&'a str),
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct InheritedConstraint<'a> {
23    pub name: &'a str,
24    pub key: InheritedConstraintKey<'a>,
25    pub no_inherit: bool,
26    pub is_local: bool,
27}
28
29impl<'a> InheritedConstraint<'a> {
30    pub fn find(
31        columns: &'a [ColumnDef],
32        constraints: &'a TableConstraintSet,
33        name: &'a str,
34    ) -> Option<Self> {
35        let (key, no_inherit, is_local) = match find_constraint(columns, constraints, name)? {
36            ConstraintLocation::NotNull(index) => {
37                let column = &columns[index];
38                (
39                    InheritedConstraintKey::NotNull(column.name.as_str()),
40                    column.not_null_no_inherit,
41                    column.not_null_is_local,
42                )
43            }
44            ConstraintLocation::ColumnCheck(index) => {
45                let column = &columns[index];
46                (
47                    InheritedConstraintKey::Check(name),
48                    column.check_no_inherit,
49                    column.check_is_local,
50                )
51            }
52            ConstraintLocation::TableCheck(index) => {
53                let check = &constraints.checks[index];
54                (
55                    InheritedConstraintKey::Check(name),
56                    check.no_inherit,
57                    check.is_local,
58                )
59            }
60            _ => return None,
61        };
62        Some(Self {
63            name,
64            key,
65            no_inherit,
66            is_local,
67        })
68    }
69}
70
71pub fn not_null_constraint<'a>(columns: &'a [ColumnDef], name: &str) -> Option<&'a ColumnDef> {
72    columns
73        .iter()
74        .find(|column| column.name == name && column.not_null)
75}
76
77impl InheritedConstraintKey<'_> {
78    pub fn find<'a>(
79        self,
80        columns: &'a [ColumnDef],
81        constraints: &'a TableConstraintSet,
82    ) -> Option<InheritedConstraint<'a>> {
83        let name = match self {
84            Self::NotNull(column) => not_null_constraint(columns, column)?
85                .not_null_name
86                .as_deref()?,
87            Self::Check(name) => match find_constraint(columns, constraints, name)? {
88                ConstraintLocation::ColumnCheck(index) => columns[index].check_name.as_deref()?,
89                ConstraintLocation::TableCheck(index) => {
90                    constraints.checks[index].name.as_deref()?
91                }
92                _ => return None,
93            },
94        };
95        InheritedConstraint::find(columns, constraints, name)
96    }
97
98    pub fn require<'a>(
99        self,
100        table: &str,
101        columns: &'a [ColumnDef],
102        constraints: &'a TableConstraintSet,
103    ) -> Result<InheritedConstraint<'a>, SQLError> {
104        self.find(columns, constraints).ok_or_else(|| match self {
105            Self::Check(name) => constraint_error("42704", format!("constraint \"{name}\" of relation \"{table}\" does not exist")),
106            Self::NotNull(column) => constraint_error("XX000", format!("cache lookup failed for not-null constraint on column \"{column}\" of relation \"{table}\"")),
107        })
108    }
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub enum InheritedConstraintRemoval {
113    Drop,
114    Keep,
115    MakeLocal,
116}
117
118pub fn inherited_constraint_removal(
119    recurse: bool,
120    is_local: bool,
121    remaining_parents: usize,
122) -> InheritedConstraintRemoval {
123    if recurse && !is_local && remaining_parents == 0 {
124        InheritedConstraintRemoval::Drop
125    } else if !recurse && remaining_parents == 0 && !is_local {
126        InheritedConstraintRemoval::MakeLocal
127    } else {
128        InheritedConstraintRemoval::Keep
129    }
130}
131
132pub fn ensure_inherited_constraint_removable(
133    table: &str,
134    name: &str,
135    parents: usize,
136) -> Result<(), SQLError> {
137    if parents == 0 {
138        return Ok(());
139    }
140    let relation =
141        uqa_core::RelationIdentity::from_legacy_name(table).map_err(SQLError::Internal)?;
142    Err(constraint_error(
143        "42P16",
144        format!(
145            "cannot drop inherited constraint \"{name}\" of relation \"{}\"",
146            relation.name
147        ),
148    ))
149}
150
151pub fn make_constraint_local(
152    columns: &mut [ColumnDef],
153    constraints: &mut TableConstraintSet,
154    name: &str,
155) -> Result<(), SQLError> {
156    match find_constraint(columns, constraints, name) {
157        Some(ConstraintLocation::NotNull(index)) => columns[index].not_null_is_local = true,
158        Some(ConstraintLocation::ColumnCheck(index)) => columns[index].check_is_local = true,
159        Some(ConstraintLocation::TableCheck(index)) => constraints.checks[index].is_local = true,
160        _ => {
161            return Err(SQLError::Internal(format!(
162                "inherited constraint \"{name}\" disappeared"
163            )))
164        }
165    }
166    Ok(())
167}
168
169#[cfg(test)]
170mod tests;