Skip to main content

uqa_sql/schema/indexes/
names.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Constraint indexes occupy the same relation namespace as explicit indexes.
8
9#[cfg(test)]
10mod tests;
11
12use crate::{
13    ast::{TableKeyConstraint, TableKeyConstraintKind},
14    SQLError,
15};
16use uqa_core::RelationIdentity;
17
18/// Relation-namespace visibility and existing constraint identities, without index mutation.
19pub trait IndexNameCatalog {
20    fn existing_constraint_keys(&self, table: &str) -> Result<Vec<TableKeyConstraint>, SQLError>;
21    fn existing_constraint_names(
22        &self,
23        table: &str,
24    ) -> Result<std::collections::BTreeSet<String>, SQLError>;
25    fn automatic_constraint_names(
26        &self,
27        table: &str,
28    ) -> Result<std::collections::BTreeSet<String>, SQLError>;
29    fn relation_name_available(&self, qualified_name: &str) -> Result<bool, SQLError>;
30}
31
32pub fn name_constraint_indexes(
33    catalog: &dyn IndexNameCatalog,
34    table: &str,
35    keys: &mut [TableKeyConstraint],
36) -> Result<(), SQLError> {
37    let mut namer = ConstraintIndexNamer::new(catalog, table)?;
38    for key in keys {
39        namer.name(key)?;
40    }
41    Ok(())
42}
43
44/// Names the indexes of a relation's keys one at a time, each after the keys named before it, as `index_create` names each new index.
45pub struct ConstraintIndexNamer<'a> {
46    catalog: &'a dyn IndexNameCatalog,
47    relation: RelationIdentity,
48    existing: Vec<TableKeyConstraint>,
49    occupied: std::collections::BTreeSet<String>,
50    automatic: std::collections::BTreeSet<String>,
51    used: std::collections::BTreeSet<String>,
52}
53
54impl<'a> ConstraintIndexNamer<'a> {
55    pub fn new(catalog: &'a dyn IndexNameCatalog, table: &str) -> Result<Self, SQLError> {
56        Ok(Self {
57            catalog,
58            relation: RelationIdentity::from_legacy_name(table).map_err(SQLError::Internal)?,
59            existing: catalog.existing_constraint_keys(table)?,
60            occupied: catalog.existing_constraint_names(table)?,
61            automatic: catalog.automatic_constraint_names(table)?,
62            used: std::collections::BTreeSet::new(),
63        })
64    }
65
66    /// Constraints the statement creates on the relation before its keys, such as a new table's CHECK and NOT NULL constraints.
67    pub fn occupy(&mut self, names: impl IntoIterator<Item = String>) {
68        self.occupied.extend(names);
69    }
70
71    /// Name a key's index. A retained key keeps its name; an explicit name must not belong to a relation, then to another constraint of the relation; any other key takes the first free generated name.
72    pub fn name(&mut self, key: &mut TableKeyConstraint) -> Result<(), SQLError> {
73        if let Some(old) = self.existing.iter().find(|old| {
74            *old == key
75                || key.catalog_identity.is_some_and(|identity| {
76                    old.catalog_identity
77                        .is_some_and(|old| old.object_id == identity.object_id)
78                })
79        }) {
80            key.name.clone_from(&old.name);
81            self.used.extend(key.name.iter().cloned());
82            return Ok(());
83        }
84        if let Some(name) = &key.name {
85            if self.used.contains(name) || !available(self.catalog, &self.relation, name)? {
86                return Err(SQLError::Routine {
87                    sqlstate: "42P07".into(),
88                    message: format!("relation \"{name}\" already exists"),
89                });
90            }
91            if self.occupied.contains(name) {
92                return Err(crate::schema::constraint_changes::constraint_error(
93                    "42710",
94                    format!(
95                        "constraint \"{name}\" for relation \"{}\" already exists",
96                        self.relation.name
97                    ),
98                ));
99            }
100            self.used.insert(name.clone());
101            return Ok(());
102        }
103        let suffix = if key.kind == TableKeyConstraintKind::PrimaryKey {
104            "pkey"
105        } else {
106            "key"
107        };
108        let component = if key.kind == TableKeyConstraintKind::PrimaryKey {
109            String::new()
110        } else {
111            super::keys::key_names(&constraint_index_attributes(key)).join("_")
112        };
113        for number in 0_u64.. {
114            let label = if number == 0 {
115                suffix.into()
116            } else {
117                format!("{suffix}{number}")
118            };
119            let candidate = object_name(&self.relation.name, &component, &label);
120            if !self.used.contains(&candidate)
121                && !self.occupied.contains(&candidate)
122                && !self.automatic.contains(&candidate)
123                && available(self.catalog, &self.relation, &candidate)?
124            {
125                self.used.insert(candidate.clone());
126                key.name = Some(candidate);
127                break;
128            }
129        }
130        Ok(())
131    }
132}
133
134/// The attributes of a constraint's supporting index: its key columns followed by the columns it includes.
135pub fn constraint_index_attributes(key: &TableKeyConstraint) -> Vec<crate::ast::IndexKey> {
136    key.columns
137        .iter()
138        .chain(&key.included_columns)
139        .cloned()
140        .map(crate::ast::IndexKey::Column)
141        .collect()
142}
143
144fn available(
145    catalog: &dyn IndexNameCatalog,
146    table: &RelationIdentity,
147    name: &str,
148) -> Result<bool, SQLError> {
149    if name == table.name {
150        return Ok(false);
151    }
152    catalog.relation_name_available(&RelationIdentity::new(&table.schema, name).qualified_name())
153}
154
155/// `PostgreSQL` reserves the fixed suffix and balances truncation of the two varying name components before clipping at UTF-8 boundaries.
156pub(in crate::schema) fn object_name(table: &str, columns: &str, label: &str) -> String {
157    let mut table_length = table.len();
158    let mut column_length = columns.len();
159    let overhead = label.len() + 1 + usize::from(!columns.is_empty());
160    while table_length + column_length + overhead > 63 {
161        if table_length > column_length {
162            table_length -= 1;
163        } else {
164            column_length -= 1;
165        }
166    }
167    while !table.is_char_boundary(table_length) {
168        table_length -= 1;
169    }
170    while !columns.is_char_boundary(column_length) {
171        column_length -= 1;
172    }
173    if columns.is_empty() {
174        format!("{}_{label}", &table[..table_length])
175    } else {
176        format!(
177            "{}_{}_{label}",
178            &table[..table_length],
179            &columns[..column_length]
180        )
181    }
182}
183
184pub fn allocate_default_index_name(
185    catalog: &dyn IndexNameCatalog,
186    table: &RelationIdentity,
187    columns: &[crate::ast::IndexKey],
188) -> Result<String, SQLError> {
189    let component = super::keys::key_names(columns).join("_");
190    for number in 0_u64.. {
191        let label = if number == 0 {
192            "idx".to_owned()
193        } else {
194            format!("idx{number}")
195        };
196        let candidate = object_name(&table.name, &component, &label);
197        if available(catalog, table, &candidate)? {
198            return Ok(candidate);
199        }
200    }
201    unreachable!("u64 index-name suffix space is non-empty")
202}