Skip to main content

uqa_sql/schema/
keys.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Declaration and identity rules for newly added PRIMARY KEY and UNIQUE constraints.
8use crate::{
9    ast::{ColumnDef, ColumnType, ForeignKey, TableCheck, TableKeyConstraint},
10    SQLError,
11};
12pub fn validate_added_key_columns(
13    table: &str,
14    constraint: &TableKeyConstraint,
15    columns: &[ColumnDef],
16) -> Result<(), SQLError> {
17    let column_names: std::collections::BTreeSet<&str> =
18        columns.iter().map(|column| column.name.as_str()).collect();
19    for column in &constraint.columns {
20        if !column_names.contains(column.as_str()) {
21            return Err(SQLError::TypeMismatch(format!(
22                "ALTER TABLE ADD CONSTRAINT references unknown column `{column}`"
23            )));
24        }
25    }
26    validate_included_key_columns(constraint, &column_names)?;
27    if constraint.without_overlaps {
28        let period_column = constraint.columns.last().ok_or_else(|| {
29            SQLError::TypeMismatch(
30                "constraint using WITHOUT OVERLAPS needs at least two columns".into(),
31            )
32        })?;
33        let period_type = columns
34            .iter()
35            .find(|column| column.name == *period_column)
36            .map(|column| &column.ty)
37            .ok_or_else(|| SQLError::UnknownColumn(format!("{table}.{period_column}")))?;
38        if !matches!(
39            period_type,
40            ColumnType::Range(_) | ColumnType::Multirange(_)
41        ) {
42            return Err(SQLError::Routine {
43                sqlstate: "42804".into(),
44                message: format!(
45                    "column \"{period_column}\" in WITHOUT OVERLAPS is not a range or multirange type"
46                ),
47            });
48        }
49        if constraint.columns.len() < 2 {
50            return Err(SQLError::TypeMismatch(
51                "constraint using WITHOUT OVERLAPS needs at least two columns".into(),
52            ));
53        }
54    }
55
56    Ok(())
57}
58/// The columns a key constraint includes must exist; as in `PostgreSQL`, they may repeat and may be key columns.
59pub fn validate_included_key_columns(
60    constraint: &TableKeyConstraint,
61    column_names: &std::collections::BTreeSet<&str>,
62) -> Result<(), SQLError> {
63    match constraint
64        .included_columns
65        .iter()
66        .find(|column| !column_names.contains(column.as_str()))
67    {
68        Some(column) => Err(SQLError::Routine {
69            sqlstate: "42703".into(),
70            message: format!("column \"{column}\" named in key does not exist"),
71        }),
72        None => Ok(()),
73    }
74}
75
76pub fn validate_added_key_identity(
77    table: &str,
78    constraint: &TableKeyConstraint,
79    existing_keys: &[TableKeyConstraint],
80    checks: &[TableCheck],
81    foreign_keys: &[ForeignKey],
82) -> Result<(), SQLError> {
83    if let Some(name) = constraint.name.as_deref() {
84        let check_name_exists = checks
85            .iter()
86            .any(|existing| existing.name.as_deref() == Some(name));
87        let foreign_name_exists = foreign_keys
88            .iter()
89            .any(|existing| existing.name.as_deref() == Some(name));
90        let key_name_exists = existing_keys
91            .iter()
92            .any(|existing| existing.name.as_deref() == Some(name));
93        if check_name_exists || foreign_name_exists || key_name_exists {
94            return Err(SQLError::TypeMismatch(format!(
95                "constraint `{name}` already exists on table `{table}`"
96            )));
97        }
98    }
99    if constraint.kind == crate::ast::TableKeyConstraintKind::PrimaryKey
100        && existing_keys
101            .iter()
102            .any(|existing| existing.kind == crate::ast::TableKeyConstraintKind::PrimaryKey)
103    {
104        return Err(SQLError::TypeMismatch(format!(
105            "multiple PRIMARY KEY constraints are not allowed on table `{table}`"
106        )));
107    }
108
109    Ok(())
110}
111
112/// Apply the NOT NULL requirement of a primary key to the stored column candidate.
113pub fn apply_primary_key_columns(
114    table: &str,
115    constraint: &TableKeyConstraint,
116    columns: &mut [ColumnDef],
117) -> Result<(), String> {
118    if constraint.kind == crate::ast::TableKeyConstraintKind::PrimaryKey {
119        for key_column in &constraint.columns {
120            let column = columns
121                .iter_mut()
122                .find(|column| column.name == *key_column)
123                .ok_or_else(|| {
124                    format!("column `{key_column}` does not exist on table `{table}`")
125                })?;
126            column.not_null = true;
127        }
128    }
129    Ok(())
130}