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
9use crate::{
10    ast::{TableKeyConstraint, TableKeyConstraintKind},
11    SQLError,
12};
13use uqa_core::RelationIdentity;
14
15/// Relation-namespace visibility and existing constraint identities, without index mutation.
16pub trait IndexNameCatalog {
17    fn existing_constraint_keys(&self, table: &str) -> Result<Vec<TableKeyConstraint>, SQLError>;
18    fn relation_name_available(&self, qualified_name: &str) -> Result<bool, SQLError>;
19}
20
21pub fn name_constraint_indexes(
22    catalog: &dyn IndexNameCatalog,
23    table: &str,
24    keys: &mut [TableKeyConstraint],
25) -> Result<(), SQLError> {
26    let relation = RelationIdentity::from_legacy_name(table).map_err(SQLError::Internal)?;
27    let existing = catalog.existing_constraint_keys(table)?;
28    let mut used = std::collections::BTreeSet::new();
29    for key in keys {
30        if key.name.is_some() && existing.iter().any(|old| old == key) {
31            used.insert(key.name.clone().expect("named constraint"));
32            continue;
33        }
34        if let Some(name) = &key.name {
35            if !used.insert(name.clone()) || !available(catalog, &relation, name)? {
36                return Err(SQLError::Routine {
37                    sqlstate: "42P07".into(),
38                    message: format!("relation \"{name}\" already exists"),
39                });
40            }
41            continue;
42        }
43        let suffix = if key.kind == TableKeyConstraintKind::PrimaryKey {
44            "pkey"
45        } else {
46            "key"
47        };
48        let component = if key.kind == TableKeyConstraintKind::PrimaryKey {
49            String::new()
50        } else {
51            key.columns.join("_")
52        };
53        for number in 0_u64.. {
54            let label = if number == 0 {
55                suffix.into()
56            } else {
57                format!("{suffix}{number}")
58            };
59            let candidate = object_name(&relation.name, &component, &label);
60            if !used.contains(&candidate) && available(catalog, &relation, &candidate)? {
61                used.insert(candidate.clone());
62                key.name = Some(candidate);
63                break;
64            }
65        }
66    }
67    Ok(())
68}
69
70fn available(
71    catalog: &dyn IndexNameCatalog,
72    table: &RelationIdentity,
73    name: &str,
74) -> Result<bool, SQLError> {
75    if name == table.name {
76        return Ok(false);
77    }
78    catalog.relation_name_available(&RelationIdentity::new(&table.schema, name).qualified_name())
79}
80
81/// `PostgreSQL` reserves the fixed suffix and balances truncation of the two varying name components before clipping at UTF-8 boundaries.
82fn object_name(table: &str, columns: &str, label: &str) -> String {
83    let mut table_length = table.len();
84    let mut column_length = columns.len();
85    let overhead = label.len() + 1 + usize::from(!columns.is_empty());
86    while table_length + column_length + overhead > 63 {
87        if table_length > column_length {
88            table_length -= 1;
89        } else {
90            column_length -= 1;
91        }
92    }
93    while !table.is_char_boundary(table_length) {
94        table_length -= 1;
95    }
96    while !columns.is_char_boundary(column_length) {
97        column_length -= 1;
98    }
99    if columns.is_empty() {
100        format!("{}_{label}", &table[..table_length])
101    } else {
102        format!(
103            "{}_{}_{label}",
104            &table[..table_length],
105            &columns[..column_length]
106        )
107    }
108}
109
110pub fn allocate_default_index_name(
111    catalog: &dyn IndexNameCatalog,
112    table: &RelationIdentity,
113    columns: &[crate::ast::IndexKey],
114) -> Result<String, SQLError> {
115    fn component(raw: &str) -> String {
116        let mut out = String::with_capacity(raw.len());
117        let mut previous_was_separator = false;
118        for ch in raw.chars() {
119            if ch.is_alphanumeric() || ch == '_' {
120                out.extend(ch.to_lowercase());
121                previous_was_separator = false;
122            } else if !previous_was_separator && !out.is_empty() {
123                out.push('_');
124                previous_was_separator = true;
125            }
126        }
127        while out.ends_with('_') {
128            out.pop();
129        }
130        out
131    }
132
133    let mut parts = std::iter::once(component(&table.name))
134        .chain(
135            super::keys::key_names(columns)
136                .iter()
137                .map(|column| component(column)),
138        )
139        .filter(|part| !part.is_empty())
140        .collect::<Vec<_>>();
141    if parts.is_empty() {
142        parts.push("index".to_string());
143    }
144    let base = format!("{}_idx", parts.join("_"));
145    let available = |name: &str| -> Result<bool, SQLError> {
146        let candidate = RelationIdentity::new(&table.schema, name).qualified_name();
147        catalog.relation_name_available(&candidate)
148    };
149    if available(&base)? {
150        return Ok(base);
151    }
152    for suffix in 1_u64.. {
153        let candidate = format!("{base}{suffix}");
154        if available(&candidate)? {
155            return Ok(candidate);
156        }
157    }
158    unreachable!("u64 index-name suffix space is non-empty")
159}