aliases are normalized and used in normalized state, but nonnormalized aliases contain everything
This commit is contained in:
92
common/src/alias.rs
Normal file
92
common/src/alias.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use icu_casemap::CaseMapper;
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
use unicode_properties::emoji::{
|
||||
is_emoji_presentation_selector, is_tag_character, is_text_presentation_selector, is_zwj,
|
||||
};
|
||||
use unicode_properties::{GeneralCategoryGroup, UnicodeEmoji, UnicodeGeneralCategory};
|
||||
|
||||
/// PostgreSQL's identifier limit. Display aliases are used as result-column
|
||||
/// labels in a few query paths, so the limit is measured in UTF-8 bytes.
|
||||
pub const MAX_ALIAS_BYTES: usize = 63;
|
||||
|
||||
/// Whether a display alias contains control, formatting, private-use, or
|
||||
/// unassigned characters that should never be persisted as visible naming.
|
||||
pub fn has_disallowed_alias_characters(alias: &str) -> bool {
|
||||
alias
|
||||
.chars()
|
||||
.any(|character| {
|
||||
character.general_category_group() == GeneralCategoryGroup::Other
|
||||
&& !is_zwj(character)
|
||||
&& !is_emoji_presentation_selector(character)
|
||||
&& !is_text_presentation_selector(character)
|
||||
&& !is_tag_character(character)
|
||||
})
|
||||
}
|
||||
|
||||
/// The hidden key used to compare and resolve public column aliases.
|
||||
///
|
||||
/// Display spelling is never changed. Compatibility-equivalent spelling and
|
||||
/// case compare alike, whitespace runs compare as one space, and emoji are
|
||||
/// ignored. Letters, marks, numbers, punctuation and useful non-emoji symbols
|
||||
/// remain significant.
|
||||
pub fn canonical_alias(alias: &str) -> String {
|
||||
let compatible = alias.nfkc().collect::<String>();
|
||||
let folded = CaseMapper::new().fold_string(&compatible).into_owned();
|
||||
let mut canonical = String::new();
|
||||
let mut pending_space = false;
|
||||
|
||||
for character in folded.chars() {
|
||||
match character.general_category_group() {
|
||||
GeneralCategoryGroup::Letter
|
||||
| GeneralCategoryGroup::Mark
|
||||
| GeneralCategoryGroup::Number
|
||||
| GeneralCategoryGroup::Punctuation => {
|
||||
if pending_space && !canonical.is_empty() {
|
||||
canonical.push(' ');
|
||||
}
|
||||
pending_space = false;
|
||||
canonical.push(character);
|
||||
}
|
||||
GeneralCategoryGroup::Symbol if !character.is_emoji_char() => {
|
||||
if pending_space && !canonical.is_empty() {
|
||||
canonical.push(' ');
|
||||
}
|
||||
pending_space = false;
|
||||
canonical.push(character);
|
||||
}
|
||||
GeneralCategoryGroup::Separator => {
|
||||
pending_space = !canonical.is_empty();
|
||||
}
|
||||
GeneralCategoryGroup::Symbol | GeneralCategoryGroup::Other => {}
|
||||
}
|
||||
}
|
||||
|
||||
canonical.nfc().collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn aliases_compare_by_compatibility_case_and_without_emoji() {
|
||||
assert_eq!(canonical_alias("Apple"), "apple");
|
||||
assert_eq!(canonical_alias("APPLE🙂"), "apple");
|
||||
assert_eq!(canonical_alias("Apple"), "apple");
|
||||
assert_eq!(canonical_alias("Straße"), canonical_alias("STRASSE"));
|
||||
assert_eq!(canonical_alias("Apple👨👩👧"), "apple");
|
||||
assert!(!has_disallowed_alias_characters("Apple👨👩👧"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn punctuation_and_useful_symbols_remain_significant() {
|
||||
assert_eq!(canonical_alias("Apple."), "apple.");
|
||||
assert_eq!(canonical_alias("Apple!"), "apple!");
|
||||
assert_eq!(canonical_alias("Price €"), "price €");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace_runs_compare_as_one_space() {
|
||||
assert_eq!(canonical_alias("First Name"), "first name");
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#[cfg_attr(not(feature = "tantivy"), path = "search_light.rs")]
|
||||
pub mod search;
|
||||
pub mod alias;
|
||||
pub mod decimal;
|
||||
pub mod grpc_error;
|
||||
pub mod money;
|
||||
|
||||
Binary file not shown.
@@ -90,7 +90,12 @@ pub struct GeneratedColumnAlias {
|
||||
/// ignored, so a typo cannot pass silently.
|
||||
#[prost(string, tag = "1")]
|
||||
pub generated_name: ::prost::alloc::string::String,
|
||||
/// What the column should be called instead. Same rules as any column name.
|
||||
/// What the column should be called instead. Preserved exactly for display.
|
||||
/// Aliases may contain Unicode letters, numbers, spaces, punctuation and
|
||||
/// symbols, but not leading/trailing whitespace or control characters, and
|
||||
/// are limited to 63 UTF-8 bytes. Uniqueness and lookup use a compatibility-
|
||||
/// normalized, Unicode-case-folded key that collapses whitespace and ignores
|
||||
/// emoji while keeping punctuation significant.
|
||||
#[prost(string, tag = "2")]
|
||||
pub alias: ::prost::alloc::string::String,
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ pub fn canonical_search_number(input: &str) -> String {
|
||||
|
||||
/// Normalizes a column name to the JSON-key form used at index time.
|
||||
pub fn normalize_column_name(column: &str) -> String {
|
||||
column.to_ascii_lowercase()
|
||||
crate::alias::canonical_alias(column)
|
||||
}
|
||||
|
||||
/// Creates the column-aware search schema.
|
||||
|
||||
@@ -65,7 +65,7 @@ pub fn canonical_search_number(input: &str) -> String {
|
||||
}
|
||||
|
||||
pub fn normalize_column_name(column: &str) -> String {
|
||||
column.to_ascii_lowercase()
|
||||
crate::alias::canonical_alias(column)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user