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 relation = RelationIdentity::from_legacy_name(table).map_err(SQLError::Internal)?;
38    let existing = catalog.existing_constraint_keys(table)?;
39    let occupied = catalog.existing_constraint_names(table)?;
40    let automatic = catalog.automatic_constraint_names(table)?;
41    let mut used = std::collections::BTreeSet::new();
42    for key in keys {
43        if let Some(old) = existing.iter().find(|old| {
44            *old == key
45                || key.catalog_identity.is_some_and(|identity| {
46                    old.catalog_identity
47                        .is_some_and(|old| old.object_id == identity.object_id)
48                })
49        }) {
50            key.name.clone_from(&old.name);
51            used.extend(key.name.iter().cloned());
52            continue;
53        }
54        if let Some(name) = &key.name {
55            if occupied.contains(name) {
56                return Err(crate::schema::constraint_changes::constraint_error(
57                    "42710",
58                    format!(
59                        "constraint \"{name}\" for relation \"{}\" already exists",
60                        relation.name
61                    ),
62                ));
63            }
64            if !used.insert(name.clone()) || !available(catalog, &relation, name)? {
65                return Err(SQLError::Routine {
66                    sqlstate: "42P07".into(),
67                    message: format!("relation \"{name}\" already exists"),
68                });
69            }
70            continue;
71        }
72        let suffix = if key.kind == TableKeyConstraintKind::PrimaryKey {
73            "pkey"
74        } else {
75            "key"
76        };
77        let component = if key.kind == TableKeyConstraintKind::PrimaryKey {
78            String::new()
79        } else {
80            super::keys::key_names(&constraint_index_attributes(key)).join("_")
81        };
82        for number in 0_u64.. {
83            let label = if number == 0 {
84                suffix.into()
85            } else {
86                format!("{suffix}{number}")
87            };
88            let candidate = object_name(&relation.name, &component, &label);
89            if !used.contains(&candidate)
90                && !occupied.contains(&candidate)
91                && !automatic.contains(&candidate)
92                && available(catalog, &relation, &candidate)?
93            {
94                used.insert(candidate.clone());
95                key.name = Some(candidate);
96                break;
97            }
98        }
99    }
100    Ok(())
101}
102
103/// The attributes of a constraint's supporting index: its key columns followed by the columns it includes.
104pub fn constraint_index_attributes(key: &TableKeyConstraint) -> Vec<crate::ast::IndexKey> {
105    key.columns
106        .iter()
107        .chain(&key.included_columns)
108        .cloned()
109        .map(crate::ast::IndexKey::Column)
110        .collect()
111}
112
113fn available(
114    catalog: &dyn IndexNameCatalog,
115    table: &RelationIdentity,
116    name: &str,
117) -> Result<bool, SQLError> {
118    if name == table.name {
119        return Ok(false);
120    }
121    catalog.relation_name_available(&RelationIdentity::new(&table.schema, name).qualified_name())
122}
123
124/// `PostgreSQL` reserves the fixed suffix and balances truncation of the two varying name components before clipping at UTF-8 boundaries.
125pub(in crate::schema) fn object_name(table: &str, columns: &str, label: &str) -> String {
126    let mut table_length = table.len();
127    let mut column_length = columns.len();
128    let overhead = label.len() + 1 + usize::from(!columns.is_empty());
129    while table_length + column_length + overhead > 63 {
130        if table_length > column_length {
131            table_length -= 1;
132        } else {
133            column_length -= 1;
134        }
135    }
136    while !table.is_char_boundary(table_length) {
137        table_length -= 1;
138    }
139    while !columns.is_char_boundary(column_length) {
140        column_length -= 1;
141    }
142    if columns.is_empty() {
143        format!("{}_{label}", &table[..table_length])
144    } else {
145        format!(
146            "{}_{}_{label}",
147            &table[..table_length],
148            &columns[..column_length]
149        )
150    }
151}
152
153pub fn allocate_default_index_name(
154    catalog: &dyn IndexNameCatalog,
155    table: &RelationIdentity,
156    columns: &[crate::ast::IndexKey],
157) -> Result<String, SQLError> {
158    let component = super::keys::key_names(columns).join("_");
159    for number in 0_u64.. {
160        let label = if number == 0 {
161            "idx".to_owned()
162        } else {
163            format!("idx{number}")
164        };
165        let candidate = object_name(&table.name, &component, &label);
166        if available(catalog, table, &candidate)? {
167            return Ok(candidate);
168        }
169    }
170    unreachable!("u64 index-name suffix space is non-empty")
171}