uqa_sql/schema/indexes/
names.rs1#[cfg(test)]
10mod tests;
11
12use crate::{
13 ast::{TableKeyConstraint, TableKeyConstraintKind},
14 SQLError,
15};
16use uqa_core::RelationIdentity;
17
18pub 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
44pub 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 pub fn occupy(&mut self, names: impl IntoIterator<Item = String>) {
68 self.occupied.extend(names);
69 }
70
71 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
134pub 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
155pub(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}