Skip to main content

uqa_sql/schema/table_creation/
not_nulls.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! The NOT NULL constraints of a new table, which `AddRelationNotNullConstraints` creates once the table's CHECK constraints exist. The declared constraints come first, in the order `transformCreateStmt` collected them, one constraint per column: a later declaration of a column must agree with the earlier one on NO INHERIT and may name the constraint the earlier one left unnamed. A declaration names a column of the relation, a NO INHERIT declaration cannot stand on a column whose parents give a constraint, a given name is new among the relation's constraints, and a chosen name follows `ChooseConstraintName`. The constraints only parents give follow, each keeping its first parent's name unless the relation holds it already.
8
9use crate::ast::{ColumnDef, CreateTable, NotNullDeclaration};
10use crate::schema::columns::POSTGRES_SYSTEM_COLUMNS;
11use crate::schema::constraint_metadata::{
12    assign_constraint_name, identity::materialize_not_null_identity, CatalogIdentityAllocator,
13    CatalogOidClass, ConstraintMetadataError,
14};
15use crate::SQLError;
16use std::collections::BTreeSet;
17
18use super::declaration::{CreateTableAnalysisContext, InheritedDefinitions};
19
20/// A NOT NULL constraint the parents give a column: the first parent's constraint name and how many parents give it.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct InheritedNotNull {
23    pub column: String,
24    pub name: Option<String>,
25    pub parents: usize,
26}
27
28impl InheritedNotNull {
29    /// Record a parent's constraint on `column` after the ones earlier parents gave: the first parent's name is kept, as `AddRelationNotNullConstraints` keeps the first name it comes across.
30    pub fn record(inherited: &mut Vec<Self>, column: &str, name: Option<&str>) {
31        if let Some(existing) = inherited
32            .iter_mut()
33            .find(|existing| existing.column == column)
34        {
35            if existing.name.is_none() {
36                existing.name = name.map(str::to_owned);
37            }
38            existing.parents += 1;
39            return;
40        }
41        inherited.push(Self {
42            column: column.to_owned(),
43            name: name.map(str::to_owned),
44            parents: 1,
45        });
46    }
47}
48
49/// Create the NOT NULL constraints of `table` as `AddRelationNotNullConstraints` does, after its CHECK constraints: the declared constraints take their names and OIDs in declaration order, then the constraints only parents give. `relation_oid` is the relation's OID, which the `pg_constraint_conrelid_contypid_conname_index` violation of a given name the relation holds reports.
50pub fn define_not_null_constraints(
51    context: &CreateTableAnalysisContext<'_>,
52    table: &mut CreateTable,
53    inherited: &InheritedDefinitions,
54    relation_oid: u32,
55    allocate: &mut CatalogIdentityAllocator<'_>,
56) -> Result<(), SQLError> {
57    let relation = uqa_core::RelationIdentity::from_legacy_name(&table.name)
58        .map_err(SQLError::Internal)?
59        .name;
60    let declared = merge_declarations(
61        &table.columns,
62        &relation,
63        std::mem::take(&mut table.not_null_declarations),
64    )?;
65    let mut names = Names {
66        relation_oid,
67        held: held_constraint_names(table, inherited),
68        used: context
69            .index_names
70            .automatic_constraint_names(&table.name)?,
71        chosen: BTreeSet::new(),
72        relation,
73    };
74    names.used.extend(names.held.iter().cloned());
75    let mut constrained = BTreeSet::new();
76    for (index, declaration) in declared {
77        let column = &mut table.columns[index];
78        let parents_give = inherited
79            .not_nulls
80            .iter()
81            .any(|constraint| constraint.column == column.name);
82        if parents_give && declaration.no_inherit {
83            return Err(SQLError::Diagnostic {
84                sqlstate: "42804".into(),
85                message: format!(
86                    "cannot define not-null constraint with NO INHERIT on column \"{}\"",
87                    column.name
88                ),
89                detail: Some("The column has an inherited not-null constraint.".into()),
90                hint: None,
91            });
92        }
93        let name = match declaration.name {
94            Some(name) => names.given(name, allocate)?,
95            None => names.choose(&column.name)?,
96        };
97        constrained.insert(column.name.clone());
98        store(
99            column,
100            name,
101            Locality {
102                is_local: true,
103                no_inherit: declaration.no_inherit,
104                explicit: declaration.explicit,
105            },
106            allocate,
107        )?;
108    }
109    for constraint in &inherited.not_nulls {
110        if constrained.contains(&constraint.column) {
111            continue;
112        }
113        let Some(column) = table
114            .columns
115            .iter_mut()
116            .find(|column| column.name == constraint.column)
117        else {
118            return Err(SQLError::Internal(format!(
119                "inherited NOT NULL constraint of column `{}` has no column",
120                constraint.column
121            )));
122        };
123        let name = names.inherited(constraint.name.as_deref(), &column.name)?;
124        constrained.insert(column.name.clone());
125        store(
126            column,
127            name,
128            Locality {
129                is_local: false,
130                no_inherit: false,
131                explicit: false,
132            },
133            allocate,
134        )?;
135    }
136    if let Some(column) = table
137        .columns
138        .iter()
139        .find(|column| column.not_null && !constrained.contains(&column.name))
140    {
141        return Err(SQLError::Internal(format!(
142            "NOT NULL constraint of column `{}` was not declared",
143            column.name
144        )));
145    }
146    Ok(())
147}
148
149/// Resolve each declaration's column and keep one declaration per column, as the first pass of `AddRelationNotNullConstraints` does.
150fn merge_declarations(
151    columns: &[ColumnDef],
152    relation: &str,
153    declarations: Vec<NotNullDeclaration>,
154) -> Result<Vec<(usize, NotNullDeclaration)>, SQLError> {
155    let mut kept: Vec<(usize, NotNullDeclaration)> = Vec::new();
156    for declaration in declarations {
157        let Some(index) = columns
158            .iter()
159            .position(|column| column.name == declaration.column)
160        else {
161            if POSTGRES_SYSTEM_COLUMNS.contains(&declaration.column.as_str()) {
162                return Err(error(
163                    "0A000",
164                    format!(
165                        "cannot add not-null constraint on system column \"{}\"",
166                        declaration.column
167                    ),
168                ));
169            }
170            return Err(error(
171                "42703",
172                format!(
173                    "column \"{}\" of relation \"{relation}\" does not exist",
174                    declaration.column
175                ),
176            ));
177        };
178        if let Some((_, existing)) = kept.iter_mut().find(|(existing, _)| *existing == index) {
179            if existing.no_inherit != declaration.no_inherit {
180                return Err(error(
181                    "42601",
182                    format!(
183                        "conflicting NO INHERIT declaration for not-null constraint on column \"{}\"",
184                        declaration.column
185                    ),
186                ));
187            }
188            match (&existing.name, &declaration.name) {
189                (Some(first), Some(second)) if first != second => {
190                    return Err(error(
191                        "42601",
192                        format!(
193                            "conflicting not-null constraint names \"{first}\" and \"{second}\""
194                        ),
195                    ));
196                }
197                (None, Some(name)) => existing.name = Some(name.clone()),
198                _ => {}
199            }
200            existing.explicit |= declaration.explicit;
201            continue;
202        }
203        kept.push((index, declaration));
204    }
205    Ok(kept)
206}
207
208/// The namespace and CHECK constraints already held when a foreign table's NOT NULL constraints are stored.
209pub struct ForeignNotNullContext<'a> {
210    pub relation: &'a uqa_core::RelationIdentity,
211    pub relation_oid: u32,
212    pub checks: &'a [crate::ast::TableCheck],
213    pub names: &'a crate::schema::constraint_metadata::ConstraintNameScope,
214}
215
216/// Store foreign-table declarations with the same target, merge, name and identity rules as ordinary tables. Foreign tables have no inherited constraints or supported keys.
217pub fn define_foreign_not_null_constraints(
218    context: ForeignNotNullContext<'_>,
219    columns: &mut [ColumnDef],
220    declarations: Vec<NotNullDeclaration>,
221    allocate: &mut CatalogIdentityAllocator<'_>,
222) -> Result<(), SQLError> {
223    let declared = merge_declarations(columns, &context.relation.name, declarations)?;
224    let held = columns
225        .iter()
226        .filter_map(|column| column.check_name.clone().filter(|_| column.check.is_some()))
227        .chain(context.checks.iter().filter_map(|check| check.name.clone()))
228        .chain(context.names.events.iter().cloned())
229        .collect();
230    let mut names = Names {
231        relation: context.relation.name.clone(),
232        relation_oid: context.relation_oid,
233        held,
234        used: context.names.schema.clone(),
235        chosen: BTreeSet::new(),
236    };
237    names.used.extend(names.held.iter().cloned());
238    for (index, declaration) in declared {
239        let name = match declaration.name {
240            Some(name) => names.given(name, allocate)?,
241            None => names.choose(&declaration.column)?,
242        };
243        store(
244            &mut columns[index],
245            name,
246            Locality {
247                is_local: true,
248                no_inherit: declaration.no_inherit,
249                explicit: declaration.explicit,
250            },
251            allocate,
252        )?;
253    }
254    Ok(())
255}
256
257/// The constraints the relation holds when its NOT NULL constraints are created: its CHECK constraints, inherited and declared, and the keys and foreign keys a partition clones.
258fn held_constraint_names(
259    table: &CreateTable,
260    inherited: &InheritedDefinitions,
261) -> BTreeSet<String> {
262    let mut held = super::declaration::cloned_constraint_names(table, inherited);
263    held.extend(table.checks.iter().filter_map(|check| check.name.clone()));
264    held.extend(
265        table
266            .columns
267            .iter()
268            .filter(|column| column.check.is_some())
269            .filter_map(|column| column.check_name.clone()),
270    );
271    held
272}
273
274/// The names the statement's NOT NULL constraints take, as `AddRelationNotNullConstraints` and `ChooseConstraintName` choose them: `held` are the constraints the relation holds already, `used` the names of every constraint in the schema and of the constraints created so far, and `chosen` the NOT NULL names of this statement.
275struct Names {
276    relation: String,
277    relation_oid: u32,
278    held: BTreeSet<String>,
279    used: BTreeSet<String>,
280    chosen: BTreeSet<String>,
281}
282
283impl Names {
284    /// A name the statement gives: not one it gave another NOT NULL constraint, and not one a constraint of the relation holds, which `pg_constraint`'s unique index reports once `CreateConstraintEntry` has drawn the constraint's OID.
285    fn given(
286        &mut self,
287        name: String,
288        allocate: &mut CatalogIdentityAllocator<'_>,
289    ) -> Result<String, SQLError> {
290        if self.chosen.contains(&name) {
291            return Err(crate::schema::check_inheritance::duplicate_check(
292                &self.relation,
293                &name,
294            ));
295        }
296        if self.held.contains(&name) {
297            let object_id = allocate
298                .allocate_object_id("NOT NULL constraint")
299                .map_err(ConstraintMetadataError::into_sql_error)?;
300            allocate
301                .allocate_catalog_oid(CatalogOidClass::Constraint, &object_id)
302                .map_err(ConstraintMetadataError::into_sql_error)?;
303            return Err(SQLError::Diagnostic {
304                sqlstate: "23505".into(),
305                message: "duplicate key value violates unique constraint \"pg_constraint_conrelid_contypid_conname_index\"".into(),
306                detail: Some(format!(
307                    "Key (conrelid, contypid, conname)=({}, 0, {name}) already exists.",
308                    self.relation_oid
309                )),
310                hint: None,
311            });
312        }
313        Ok(self.take(name))
314    }
315
316    /// An inherited constraint keeps its first parent's name unless the relation holds it or the statement chose it.
317    fn inherited(&mut self, preferred: Option<&str>, column: &str) -> Result<String, SQLError> {
318        match preferred {
319            Some(name) if !self.chosen.contains(name) && !self.held.contains(name) => {
320                Ok(self.take(name.to_owned()))
321            }
322            _ => self.choose(column),
323        }
324    }
325
326    /// `ChooseConstraintName` with the `not_null` label: the first of `relation_column_not_null`, `relation_column_not_null1`, ... that no constraint uses.
327    fn choose(&mut self, column: &str) -> Result<String, SQLError> {
328        let mut target = None;
329        assign_constraint_name(
330            &mut target,
331            (&self.relation, column, "not_null"),
332            &mut self.used,
333        )
334        .map_err(ConstraintMetadataError::into_sql_error)?;
335        let name = target
336            .ok_or_else(|| SQLError::Internal("NOT NULL constraint name was not chosen".into()))?;
337        self.chosen.insert(name.clone());
338        Ok(name)
339    }
340
341    fn take(&mut self, name: String) -> String {
342        self.used.insert(name.clone());
343        self.chosen.insert(name.clone());
344        name
345    }
346}
347
348/// How a NOT NULL constraint stands on its column: declared by the statement or given only by parents, inheritable or not, and written by the statement or implied by a key, SERIAL or identity.
349struct Locality {
350    is_local: bool,
351    no_inherit: bool,
352    explicit: bool,
353}
354
355/// `StoreRelNotNull`: the constraint's name, locality and inheritance on its column, validated as CREATE TABLE validates every constraint it creates, and its catalog identity.
356fn store(
357    column: &mut ColumnDef,
358    name: String,
359    locality: Locality,
360    allocate: &mut CatalogIdentityAllocator<'_>,
361) -> Result<(), SQLError> {
362    column.not_null = true;
363    column.not_null_explicit = locality.explicit;
364    column.not_null_name = Some(name);
365    column.not_null_no_inherit = locality.no_inherit;
366    column.not_null_validated = true;
367    column.not_null_is_local = locality.is_local;
368    column.not_null_identity = None;
369    materialize_not_null_identity(column, allocate)
370        .map_err(ConstraintMetadataError::into_sql_error)?;
371    Ok(())
372}
373
374fn error(sqlstate: &str, message: String) -> SQLError {
375    SQLError::Routine {
376        sqlstate: sqlstate.into(),
377        message,
378    }
379}
380
381#[cfg(test)]
382mod tests;