equivalência Unicode
Unicode é uma fera complicada. Uma de suas inúmeras características peculiares é que diferentes sequências de codepoints podem ser iguais. Este não é o caso em codificações legadas. Em LATIN1, por exemplo, a única coisa que é igual a ‘a’ é ‘a’ e a única coisa que é igual a ‘ä’ é ‘ä’. Em Unicode, no entanto, caracteres com sinais diacríticos podem frequentemente (dependendo do caractere em particular) ser codificados de diferentes maneiras:como um caractere pré-composto, como foi feito em codificações herdadas como LATIN1, ou decomposto, consistindo no caractere base 'a ' seguido pelo sinal diacrítico ◌̈ aqui. Isso é chamado de equivalência canônica . A vantagem de ter essas duas opções é que você pode, por um lado, converter facilmente caracteres de codificações herdadas e, por outro lado, não precisa adicionar cada combinação de acentos ao Unicode como um caractere separado. Mas esse esquema torna as coisas mais difíceis para software usando Unicode.
Contanto que você esteja apenas olhando para o caractere resultante, como em um navegador, você não deve notar uma diferença e isso não importa para você. No entanto, em um sistema de banco de dados em que pesquisar e classificar strings é uma funcionalidade fundamental e de desempenho crítico, as coisas podem ficar complicadas.
Primeiro, a biblioteca de agrupamento em uso precisa estar ciente disso. No entanto, a maioria das bibliotecas do sistema C, incluindo glibc, não são. Portanto, na glibc, quando você procura por 'ä', não encontrará 'ä'. Veja o que eu fiz lá? O segundo é codificado de forma diferente, mas provavelmente parece o mesmo para você que está lendo. (Pelo menos foi assim que eu digitei. Pode ter sido alterado em algum lugar ao longo do caminho para o seu navegador.) Confuso. Se você usa ICU para agrupamentos, isso funciona e é totalmente compatível.
Segundo, quando o PostgreSQL compara strings por igualdade, ele apenas compara os bytes, não leva em consideração a possibilidade de que a mesma string possa ser representada de maneiras diferentes. Isso é tecnicamente errado ao usar Unicode, mas é uma otimização de desempenho necessária. Para contornar isso, você pode usar ordenações não determinísticas , um recurso introduzido no PostgreSQL 12. Um agrupamento declarado dessa forma não
basta comparar os bytesmas fará qualquer pré-processamento necessário para poder comparar ou hash strings que podem ser codificadas de maneiras diferentes. Exemplo:
CREATE COLLATION ndcoll (provider = icu, locale = 'und', deterministic = false);
Formulários de normalização
Portanto, embora existam diferentes maneiras válidas de codificar determinados caracteres Unicode, às vezes é útil convertê-los em um formato consistente. Isso é chamado de normalização . Existem dois formulários de normalização :totalmente composto , o que significa que convertemos todas as sequências de codepoints em caracteres pré-compostos o máximo possível e completamente decompostos , o que significa que convertemos todos os codepoints em suas partes componentes (letra mais acento) tanto quanto possível. Na terminologia Unicode, esses formulários são conhecidos como NFC e NFD, respectivamente. Há mais alguns detalhes para isso, como colocar todos os caracteres combinados em uma ordem canônica, mas essa é a ideia geral. O ponto é que, quando você converte uma string Unicode em um dos formulários de normalização, pode compará-los ou hash-los bytewise sem ter que se preocupar com variantes de codificação. Qual deles você usa não importa, desde que todo o sistema concorde com um.
Na prática, a maior parte do mundo usa NFC. Além disso, muitos sistemas são falhos, pois não lidam corretamente com Unicode não NFC, incluindo a maioria dos recursos de agrupamento das bibliotecas C e até mesmo o PostgreSQL por padrão, conforme mencionado acima. Portanto, garantir que todo Unicode seja convertido em NFC é uma boa maneira de garantir uma melhor interoperabilidade.
Normalização no PostgreSQL
O PostgreSQL 13 agora contém dois novos recursos para lidar com a normalização Unicode:uma função para testar a normalização e outra para converter em um formulário de normalização. Por exemplo:
SELECT 'foo' IS NFC NORMALIZED; SELECT 'foo' IS NFD NORMALIZED; SELECT 'foo' IS NORMALIZED; -- NFC is the default SELECT NORMALIZE('foo', NFC); SELECT NORMALIZE('foo', NFD); SELECT NORMALIZE('foo'); -- NFC is the default
(A sintaxe é especificada no padrão SQL.)
Uma opção é usar isso em um domínio, por exemplo:
CREATE DOMAIN norm_text AS text CHECK (VALUE IS NORMALIZED);
Observe que a normalização de texto arbitrário não é totalmente barata. Portanto, aplique isso com sensatez e somente onde realmente importa.
Observe também que a normalização não é fechada sob concatenação. Isso significa que anexar duas strings normalizadas nem sempre resulta em uma string normalizada. Portanto, mesmo que você aplique cuidadosamente essas funções e também verifique se seu sistema usa apenas strings normalizadas, elas ainda podem "infiltrar-se" durante operações legítimas. Portanto, apenas assumir que strings não normalizadas não podem acontecer falhará; esta questão tem de ser tratada adequadamente.
Caracteres de compatibilidade
Há outro caso de uso para normalização. O Unicode contém algumas formas alternativas de letras e outros caracteres, para vários propósitos de legado e compatibilidade. Por exemplo, você pode escrever Fraktur:
SELECT '𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢';
Agora imagine que seu aplicativo atribui nomes de usuário ou outros identificadores, e existe um usuário chamado
'somename'
e outro chamado '𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢'
. Isso seria pelo menos confuso, mas possivelmente um risco de segurança. A exploração dessas semelhanças é frequentemente usada em ataques de phishing, URLs falsos e preocupações semelhantes. Portanto, o Unicode contém duas formas de normalização adicionais que resolvem essas semelhanças e convertem essas formas alternativas em uma letra base canônica. Esses formulários são chamados de NFKC e NFKD. Eles são os mesmos que NFC e NFD, respectivamente. Por exemplo:=> select normalize('𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢', nfkc); normalize ----------- somename
Novamente, usar restrições de verificação talvez como parte de um domínio pode ser útil:
CREATE DOMAIN username AS text CHECK (VALUE IS NFKC NORMALIZED OR VALUE IS NFKD NORMALIZED);
(A normalização real provavelmente deve ser feita no frontend da interface do usuário.)
Veja também RFC 3454 para um tratamento de strings para tratar de tais preocupações.
Resumo
Os problemas de equivalência Unicode são frequentemente ignorados sem consequências. Em muitos contextos, a maioria dos dados está no formato NFC, portanto, não surgem problemas. No entanto, ignorar esses problemas pode levar a um comportamento estranho, dados aparentemente ausentes e, em algumas situações, riscos de segurança. Portanto, a conscientização sobre esses problemas é importante para os designers de banco de dados, e as ferramentas descritas neste artigo podem ser usadas para lidar com eles.