uqa_sql/schema/indexes/
names.rs1use crate::{
10 ast::{TableKeyConstraint, TableKeyConstraintKind},
11 SQLError,
12};
13use uqa_core::RelationIdentity;
14
15pub 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
81fn 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}