Equivalenza Unicode
Unicode è una bestia complicata. Una delle sue numerose caratteristiche peculiari è che diverse sequenze di codepoint possono essere uguali. Questo non è il caso delle codifiche legacy. In LATIN1, per esempio, l'unica cosa che è uguale ad 'a' è 'a' e l'unica cosa che è uguale ad 'ä' è 'ä'. In Unicode, tuttavia, i caratteri con segni diacritici possono spesso (a seconda del carattere particolare) essere codificati in diversi modi:o come carattere precomposto, come avveniva nelle codifiche legacy come LATIN1, o scomposto, costituito dal carattere di base 'a ' seguito dal segno diacritico ◌̈ qui. Questa si chiama equivalenza canonica . Il vantaggio di avere entrambe queste opzioni è che puoi, da un lato, convertire facilmente i caratteri dalle codifiche legacy e, dall'altro, non è necessario aggiungere tutte le combinazioni di accenti a Unicode come carattere separato. Ma questo schema rende le cose più difficili per il software che utilizza Unicode.
Finché stai solo guardando il personaggio risultante, ad esempio in un browser, non dovresti notare alcuna differenza e questo non ti interessa. Tuttavia, in un sistema di database in cui la ricerca e l'ordinamento delle stringhe sono funzionalità fondamentali e critiche per le prestazioni, le cose possono complicarsi.
Innanzitutto, la libreria di confronto in uso deve esserne consapevole. Tuttavia, la maggior parte delle librerie C di sistema, inclusa glibc, non lo sono. Quindi in glibc, quando cerchi 'ä', non troverai 'ä'. Vedi cosa ho fatto lì? Il secondo è codificato in modo diverso ma probabilmente sembra lo stesso a te che leggi. (Almeno è così che l'ho inserito. Potrebbe essere stato modificato da qualche parte lungo la strada per il tuo browser.) Confuso. Se usi ICU per le regole di confronto, funziona ed è completamente supportato.
In secondo luogo, quando PostgreSQL confronta le stringhe per l'uguaglianza, confronta semplicemente i byte, non prende in considerazione la possibilità che la stessa stringa possa essere rappresentata in modi diversi. Questo è tecnicamente sbagliato quando si utilizza Unicode, ma è un'ottimizzazione delle prestazioni necessaria. Per ovviare a questo problema, puoi utilizzare garazioni non deterministiche , una funzionalità introdotta in PostgreSQL 12. Una collation dichiarata in questo modo non basta confrontare i byte
ma eseguirà tutta la preelaborazione necessaria per poter confrontare o hash stringhe che potrebbero essere codificate in modi diversi. Esempio:
CREATE COLLATION ndcoll (provider = icu, locale = 'und', deterministic = false);
Moduli di normalizzazione
Quindi, sebbene ci siano diversi modi validi per codificare determinati caratteri Unicode, a volte è utile convertirli tutti in un formato coerente. Questa si chiama normalizzazione . Sono disponibili due moduli di normalizzazione :completamente composto , il che significa che convertiamo il più possibile tutte le sequenze di codepoint in caratteri precomposti e completamente scomposti , il che significa che convertiamo il più possibile tutti i codepoint nei loro componenti (lettera più accento). Nella terminologia Unicode, queste forme sono conosciute rispettivamente come NFC e NFD. Ci sono alcuni dettagli in più su questo, come mettere tutti i personaggi combinati in un ordine canonico, ma questa è l'idea generale. Il punto è che, quando si converte una stringa Unicode in uno dei moduli di normalizzazione, è possibile confrontarli o eseguirne l'hashing bytewise senza doversi preoccupare delle varianti di codifica. Quello che usi non ha importanza, purché l'intero sistema sia d'accordo su uno.
In pratica, la maggior parte del mondo utilizza NFC. Inoltre, molti sistemi sono difettosi in quanto non gestiscono correttamente Unicode non NFC, comprese la maggior parte delle strutture di confronto delle librerie C e persino PostgreSQL per impostazione predefinita, come accennato in precedenza. Quindi assicurarsi che tutti gli Unicode vengano convertiti in NFC è un buon modo per garantire una migliore interoperabilità.
Normalizzazione in PostgreSQL
PostgreSQL 13 ora contiene due nuove funzionalità per gestire la normalizzazione Unicode:una funzione per testare la normalizzazione e una per convertire in un modulo di normalizzazione. Ad esempio:
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
(La sintassi è specificata nello standard SQL.)
Un'opzione è utilizzarla in un dominio, ad esempio:
CREATE DOMAIN norm_text AS text CHECK (VALUE IS NORMALIZED);
Si noti che la normalizzazione del testo arbitrario non è del tutto economica. Quindi applicalo in modo ragionevole e solo dove conta davvero.
Si noti inoltre che la normalizzazione non è chiusa per concatenazione. Ciò significa che l'aggiunta di due stringhe normalizzate non risulta sempre in una stringa normalizzata. Quindi, anche se applichi attentamente queste funzioni e controlli anche in altro modo che il tuo sistema utilizzi solo stringhe normalizzate, possono comunque "insinuarsi" durante le operazioni legittime. Quindi, supponendo che le stringhe non normalizzate non possano verificarsi, fallirà; questo problema deve essere affrontato correttamente.
Caratteri di compatibilità
C'è un altro caso d'uso per la normalizzazione. Unicode contiene alcune forme alternative di lettere e altri caratteri, per vari scopi di eredità e compatibilità. Ad esempio, puoi scrivere Fraktur:
SELECT '𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢';
Ora immagina che la tua applicazione assegni nomi utente o altri identificatori simili e che ci sia un utente chiamato 'somename'
e un altro chiamato '𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢'
. Questo sarebbe almeno fonte di confusione, ma forse un rischio per la sicurezza. Lo sfruttamento di tali somiglianze viene spesso utilizzato in attacchi di phishing, URL falsi e preoccupazioni simili. Quindi Unicode contiene due moduli di normalizzazione aggiuntivi che risolvono queste somiglianze e convertono tali moduli alternativi in una lettera base canonica. Queste forme sono chiamate NFKC e NFKD. Per il resto sono gli stessi rispettivamente di NFC e NFD. Ad esempio:
=> select normalize('𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢', nfkc); normalize ----------- somename
Anche in questo caso, l'utilizzo dei vincoli di controllo, magari come parte di un dominio, può essere utile:
CREATE DOMAIN username AS text CHECK (VALUE IS NFKC NORMALIZED OR VALUE IS NFKD NORMALIZED);
(L'effettiva normalizzazione dovrebbe probabilmente essere eseguita nel frontend dell'interfaccia utente.)
Vedi anche RFC 3454 per un trattamento delle stringhe per affrontare tali problemi.
Riepilogo
I problemi di equivalenza Unicode vengono spesso ignorati senza conseguenze. In molti contesti, la maggior parte dei dati è in formato NFC, quindi non sorgono problemi. Tuttavia, ignorare questi problemi può portare a comportamenti strani, dati apparentemente mancanti e in alcune situazioni rischi per la sicurezza. Quindi la consapevolezza di questi problemi è importante per i progettisti di database e gli strumenti descritti in questo articolo possono essere utilizzati per affrontarli.