use alloc::string::String;
use core::cmp::Ordering;
pub(crate) fn compare(collation: &str, a: &str, b: &str) -> Option<Ordering> {
let name = collation.trim();
let base = name.split(['.', '@']).next().unwrap_or(name);
if base.eq_ignore_ascii_case("C") || base.eq_ignore_ascii_case("POSIX") {
return Some(a.as_bytes().cmp(b.as_bytes()));
}
if base.eq_ignore_ascii_case("binary")
|| base
.rsplit_once('_')
.is_some_and(|(_, tail)| tail.eq_ignore_ascii_case("bin"))
{
return Some(a.as_bytes().cmp(b.as_bytes()));
}
let locale = icu_locale_core::Locale::try_from_str(&normalise(name)).ok()?;
let prefs = icu_collator::CollatorPreferences::from(&locale);
let collator = icu_collator::Collator::try_new(prefs, pg_options()).ok()?;
Some(
collator
.compare(a, b)
.then_with(|| a.as_bytes().cmp(b.as_bytes())),
)
}
fn pg_options() -> icu_collator::options::CollatorOptions {
let mut o = icu_collator::options::CollatorOptions::default();
o.alternate_handling = Some(icu_collator::options::AlternateHandling::Shifted);
o.max_variable = Some(icu_collator::options::MaxVariable::Punctuation);
o
}
pub(crate) fn is_byte_wise(collation: &str) -> bool {
let name = collation.trim();
let base = name.split(['.', '@']).next().unwrap_or(name);
base.eq_ignore_ascii_case("C")
|| base.eq_ignore_ascii_case("POSIX")
|| base.eq_ignore_ascii_case("binary")
|| base
.rsplit_once('_')
.is_some_and(|(_, tail)| tail.eq_ignore_ascii_case("bin"))
}
pub(crate) fn is_supported(collation: &str) -> bool {
compare(collation, "a", "b").is_some()
}
fn normalise(name: &str) -> String {
if name.eq_ignore_ascii_case("default")
|| name.eq_ignore_ascii_case("unicode")
|| name.eq_ignore_ascii_case("pg_unicode_fast")
|| name.eq_ignore_ascii_case("ucs_basic")
{
return String::from("und");
}
let head = name.split(['.', '@']).next().unwrap_or(name);
head.replace('_', "-")
}
pub(crate) fn column_key_is_bytewise(col: &spg_storage::ColumnSchema, mysql: bool) -> bool {
if !matches!(
col.ty,
spg_storage::DataType::Text
| spg_storage::DataType::Varchar(_)
| spg_storage::DataType::Char(_)
) {
return true;
}
if matches!(col.collation, spg_storage::Collation::CaseInsensitive) {
return false;
}
!mysql || matches!(col.collation, spg_storage::Collation::Binary)
}
#[cfg(test)]
#[path = "collate_survey.rs"]
mod survey;
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
use alloc::vec::Vec;
fn sorted<'a>(coll: &str, mut xs: Vec<&'a str>) -> Vec<&'a str> {
xs.sort_by(|a, b| compare(coll, a, b).expect("supported"));
xs
}
#[test]
fn en_us_reproduces_the_rules_measured_from_pg() {
assert_eq!(
sorted("en_US.utf8", vec!["b", "A", "a", "B"]),
["a", "A", "b", "B"]
);
assert_eq!(compare("en_US.utf8", "a", "A"), Some(Ordering::Less));
assert_eq!(
sorted("en_US.utf8", vec!["f", "ê", "E", "é", "e"]),
["e", "E", "é", "ê", "f"]
);
assert_eq!(
compare("en_US.utf8", "résumé", "resumes"),
Some(Ordering::Less)
);
assert_eq!(sorted("en_US.utf8", vec!["a", "1", "A"]), ["1", "a", "A"]);
let g = sorted("en_US.utf8", vec!["aB", "ab", "a-b", "a b"]);
assert_eq!(g[3], "aB", "case outranks punctuation: {g:?}");
assert_eq!(
sorted("en_US.utf8", vec!["a2", "a10", "a1"]),
["a1", "a10", "a2"]
);
assert_eq!(
sorted("en_US.utf8", vec!["中", "あ", "z", "Z"]),
["z", "Z", "あ", "中"]
);
}
#[test]
fn c_and_posix_are_byte_order() {
for c in ["C", "POSIX", "c", "posix"] {
assert_eq!(compare(c, "B", "a"), Some(Ordering::Less), "{c}");
assert_eq!(sorted(c, vec!["a", "B", "_"]), ["B", "_", "a"], "{c}");
}
}
#[test]
fn posix_locale_spellings_are_understood() {
for name in ["en_US", "en_US.utf8", "en_US.UTF-8", "en-US"] {
assert!(is_supported(name), "{name}");
assert_eq!(compare(name, "a", "A"), Some(Ordering::Less), "{name}");
}
}
#[test]
fn an_unknown_name_declines_instead_of_guessing() {
assert_eq!(compare("no_such_locale_at_all", "a", "b"), None);
assert!(!is_supported("no_such_locale_at_all"));
}
#[test]
fn punctuation_is_variable_weighted_like_pg() {
assert_eq!(
sorted("en_US.utf8", vec!["_under", "apple", "Zebra", "cherry"]),
["apple", "cherry", "_under", "Zebra"]
);
assert_eq!(
sorted("en_US.utf8", vec!["_id", "name", "zip", "_ts", "email"]),
["email", "_id", "name", "_ts", "zip"]
);
assert_eq!(
sorted("en_US.utf8", vec!["O'Brien", "Oakes", "Obrien", "O-Brien"]),
["Oakes", "Obrien", "O'Brien", "O-Brien"]
);
let g = sorted(
"en_US.utf8",
vec!["de luca", "deluca", "de-luca", "demarco"],
);
assert_eq!(
g[3], "demarco",
"the de-luca variants group before demarco: {g:?}"
);
}
#[test]
fn all_punctuation_values_now_match_pg() {
assert_eq!(
sorted("en_US.utf8", vec!["_", " ", "-", "."]),
[" ", "-", ".", "_"]
);
}
#[test]
fn a_locale_tailoring_actually_changes_the_answer() {
assert_eq!(compare("sv_SE.utf8", "z", "å"), Some(Ordering::Less));
assert_eq!(compare("en_US.utf8", "z", "å"), Some(Ordering::Greater));
}
#[test]
fn survey_pg18_collation_coverage() {
let all = super::survey::PG18_COLLATIONS;
let unsupported: Vec<&str> = all.iter().copied().filter(|n| !is_supported(n)).collect();
let supported = all.len() - unsupported.len();
assert_eq!(
unsupported,
Vec::<&str>::new(),
"{supported}/{} performable",
all.len()
);
}
}