use std::collections::BTreeSet;
use crate::identifier::Identifier;
use crate::ir::catalog::Catalog;
#[derive(Clone, Copy)]
pub enum IndexNameKind {
Pkey,
Unique,
Plain,
Exclude,
Stat,
}
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",
}
}
}
#[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 choose_relation_name(name1: &str, name2: &str, label: &str, taken: &TakenNames) -> String {
let build = |suffix: &str| {
let label_full = format!("{label}{suffix}");
let overhead = 1 + (if name2.is_empty() { 0 } else { name2.len() + 1 }) + label_full.len();
let budget1 = NAMEDATALEN.saturating_sub(overhead);
let n1 = truncate_bytes(name1, budget1);
if name2.is_empty() {
format!("{n1}_{label_full}")
} else {
format!("{n1}_{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(18), "b".repeat(40), "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(18), "b".repeat(40), "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(17), "b".repeat(40), "idx1");
assert_eq!(name, expected, "counter name 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"
);
}
}