use std::collections::BTreeSet;
use crate::identifier::Identifier;
use crate::ir::catalog::Catalog;
#[derive(Clone, Copy)]
pub enum IndexNameKind {
Pkey,
Unique,
Plain,
Exclude,
Stat,
Check,
}
impl IndexNameKind {
const fn label(self) -> &'static str {
match self {
Self::Pkey => "pkey",
Self::Unique => "key",
Self::Plain => "idx",
Self::Exclude => "excl",
Self::Stat => "stat",
Self::Check => "check",
}
}
}
#[derive(Default)]
pub struct TakenNames(BTreeSet<String>);
impl TakenNames {
pub fn from_schema(catalog: &Catalog, schema: &Identifier) -> Self {
let mut s = BTreeSet::new();
for t in &catalog.tables {
if &t.qname.schema == schema {
s.insert(t.qname.name.as_str().to_string());
for c in &t.constraints {
if &c.qname.schema == schema {
s.insert(c.qname.name.as_str().to_string());
}
}
}
}
for i in &catalog.indexes {
if &i.qname.schema == schema {
s.insert(i.qname.name.as_str().to_string());
}
}
for v in &catalog.views {
if &v.qname.schema == schema {
s.insert(v.qname.name.as_str().to_string());
}
}
for mv in &catalog.materialized_views {
if &mv.qname.schema == schema {
s.insert(mv.qname.name.as_str().to_string());
}
}
for seq in &catalog.sequences {
if &seq.qname.schema == schema {
s.insert(seq.qname.name.as_str().to_string());
}
}
for st in &catalog.statistics {
if &st.qname.schema == schema {
s.insert(st.qname.name.as_str().to_string());
}
}
Self(s)
}
pub fn insert(&mut self, name: &str) {
self.0.insert(name.to_string());
}
fn contains(&self, name: &str) -> bool {
self.0.contains(name)
}
}
const NAMEDATALEN: usize = 63;
fn truncate_bytes(s: &str, max: usize) -> &str {
if s.len() <= max {
return s;
}
let mut end = max;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
&s[..end]
}
const NAMEDATALEN_FULL: usize = 64;
fn name_addition(col_names: &[Option<&str>]) -> String {
let mut out = String::new();
let mut expr_n: u32 = 0;
for c in col_names {
let part: String = c.map_or_else(
|| {
expr_n += 1;
if expr_n == 1 {
"expr".to_string()
} else {
format!("expr{expr_n}")
}
},
|name| (*name).to_string(),
);
if !out.is_empty() {
out.push('_');
}
out.push_str(&part);
if out.len() >= NAMEDATALEN_FULL {
break;
}
}
out
}
fn make_object_name<'a>(name1: &'a str, name2: &'a str, label_full: &str) -> String {
let overhead = usize::from(!name2.is_empty()) + label_full.len()
+ 1; let availchars = NAMEDATALEN.saturating_sub(overhead);
let mut name1chars = name1.len();
let mut name2chars = name2.len();
while name1chars + name2chars > availchars {
if name1chars > name2chars {
name1chars -= 1;
} else {
name2chars -= 1;
}
}
let n1 = truncate_bytes(name1, name1chars);
if name2.is_empty() {
format!("{n1}_{label_full}")
} else {
let n2 = truncate_bytes(name2, name2chars);
format!("{n1}_{n2}_{label_full}")
}
}
fn choose_relation_name(name1: &str, name2: &str, label: &str, taken: &TakenNames) -> String {
let build = |suffix: &str| {
let label_full = format!("{label}{suffix}");
make_object_name(name1, name2, &label_full)
};
let mut candidate = build("");
let mut i: u32 = 0;
while taken.contains(&candidate) {
i += 1;
candidate = build(&i.to_string());
}
candidate
}
pub fn choose_index_name(
table: &str,
col_names: &[Option<&str>],
kind: IndexNameKind,
taken: &mut TakenNames,
) -> String {
let name = match kind {
IndexNameKind::Pkey => choose_relation_name(table, "", kind.label(), taken),
_ => choose_relation_name(table, &name_addition(col_names), kind.label(), taken),
};
taken.insert(&name);
name
}
#[cfg(test)]
mod tests {
use super::*;
fn taken(names: &[&str]) -> TakenNames {
let mut t = TakenNames::default();
for n in names {
t.insert(n);
}
t
}
#[test]
fn pkey_name() {
let mut t = TakenNames::default();
assert_eq!(
choose_index_name("clone", &[], IndexNameKind::Pkey, &mut t),
"clone_pkey"
);
}
#[test]
fn unique_two_columns() {
let mut t = TakenNames::default();
assert_eq!(
choose_index_name(
"clone",
&[Some("a"), Some("b")],
IndexNameKind::Unique,
&mut t
),
"clone_a_b_key"
);
}
#[test]
fn plain_index_suffix_idx() {
let mut t = TakenNames::default();
assert_eq!(
choose_index_name("clone", &[Some("a")], IndexNameKind::Plain, &mut t),
"clone_a_idx"
);
}
#[test]
fn collision_appends_counter() {
let mut t = taken(&["clone_a_key"]);
assert_eq!(
choose_index_name("clone", &[Some("a")], IndexNameKind::Unique, &mut t),
"clone_a_key1"
);
}
#[test]
fn expression_columns_use_expr() {
let mut t = TakenNames::default();
assert_eq!(
choose_index_name("clone", &[None, Some("a")], IndexNameKind::Plain, &mut t),
"clone_expr_a_idx"
);
}
#[test]
fn name_addition_includes_multiple_long_columns() {
let c1 = "abcdefghijklmnopqrstuvwxy";
let c2 = "zyxwvutsrqponmlkjihgfedcb";
let mut t = TakenNames::default();
assert_eq!(
choose_index_name("t", &[Some(c1), Some(c2)], IndexNameKind::Plain, &mut t),
format!("t_{c1}_{c2}_idx")
);
}
#[test]
fn stat_name() {
let mut t = TakenNames::default();
assert_eq!(
choose_index_name("c", &[Some("a"), Some("b")], IndexNameKind::Stat, &mut t),
"c_a_b_stat"
);
}
#[test]
fn long_name_truncates_to_namedatalen() {
let table = "a".repeat(40);
let col = "b".repeat(40);
let mut t = TakenNames::default();
let name = choose_index_name(&table, &[Some(&col)], IndexNameKind::Plain, &mut t);
assert!(
name.len() <= NAMEDATALEN,
"name length {} > {NAMEDATALEN}: {name:?}",
name.len()
);
assert!(
name.starts_with('a'),
"must start with the table name prefix"
);
assert!(name.ends_with("_idx"), "must end with _idx suffix");
let expected = format!("{}_{}_{}", "a".repeat(29), "b".repeat(29), "idx");
assert_eq!(name, expected, "truncated name did not match expected");
}
#[test]
fn truncated_collision_appends_counter() {
let table = "a".repeat(40);
let col = "b".repeat(40);
let first_name = format!("{}_{}_{}", "a".repeat(29), "b".repeat(29), "idx");
let mut t = taken(&[&first_name]);
let name = choose_index_name(&table, &[Some(&col)], IndexNameKind::Plain, &mut t);
assert!(
name.len() <= NAMEDATALEN,
"counter name length {} > {NAMEDATALEN}: {name:?}",
name.len()
);
let expected = format!("{}_{}_{}", "a".repeat(29), "b".repeat(28), "idx1");
assert_eq!(name, expected, "counter name did not match expected");
}
#[test]
fn long_column_addition_truncates_name2() {
let table = "t";
let col = "c".repeat(70);
let mut t = TakenNames::default();
let name = choose_index_name(table, &[Some(&col)], IndexNameKind::Plain, &mut t);
assert!(
name.len() <= NAMEDATALEN,
"name length {} > {NAMEDATALEN}: {name:?}",
name.len()
);
let expected = format!("t_{}_idx", "c".repeat(57));
assert_eq!(
name, expected,
"alternating-shrink on long name2 did not match expected"
);
}
#[test]
fn both_long_alternating_truncation() {
let table = "a".repeat(40);
let col = "b".repeat(40);
let mut t = TakenNames::default();
let name = choose_index_name(&table, &[Some(&col)], IndexNameKind::Plain, &mut t);
assert!(
name.len() <= NAMEDATALEN,
"name length {} > {NAMEDATALEN}: {name:?}",
name.len()
);
let expected = format!("{}_{}_{}", "a".repeat(29), "b".repeat(29), "idx");
assert_eq!(
name, expected,
"both-long alternating truncation did not match expected"
);
}
#[test]
fn from_schema_seeds_relation_namespace() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::create_dir_all(dir.path().join("pub")).expect("mkdir");
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n")
.expect("write schema");
std::fs::write(
dir.path().join("pub/seq.sql"),
"CREATE SEQUENCE pub.foo_a_key;\n",
)
.expect("write seq");
let (cat, _) =
crate::parse::parse_directory_with_locations(dir.path(), &[]).expect("parse");
let schema = Identifier::from_unquoted("pub").expect("ident");
let mut taken = TakenNames::from_schema(&cat, &schema);
assert_eq!(
choose_index_name("foo", &[Some("a")], IndexNameKind::Unique, &mut taken),
"foo_a_key1"
);
}
}