Unicode-equivalentie
Unicode is een ingewikkeld beest. Een van de vele bijzondere kenmerken is dat verschillende reeksen codepunten gelijk kunnen zijn. Dit is niet het geval bij legacy-coderingen. In LATIN1 is bijvoorbeeld het enige dat gelijk is aan 'a' 'a' en het enige dat gelijk is aan 'ä' is 'ä'. In Unicode kunnen tekens met diakritische tekens echter vaak (afhankelijk van het specifieke teken) op verschillende manieren worden gecodeerd:ofwel als een vooraf samengesteld teken, zoals werd gedaan in legacy-coderingen zoals LATIN1 of ontleed, bestaande uit het basisteken 'a ' gevolgd door het diakritische teken ◌̈ hier. Dit heet canonieke equivalentie . Het voordeel van beide opties is dat je enerzijds gemakkelijk karakters van legacy coderingen kunt converteren en anderzijds niet elke accentcombinatie als een apart karakter aan Unicode hoeft toe te voegen. Maar dit schema maakt het moeilijker voor software die Unicode gebruikt.
Zolang je alleen naar het resulterende karakter kijkt, zoals in een browser, zou je geen verschil moeten merken en dit maakt voor jou niet uit. In een databasesysteem waar het zoeken en sorteren van strings fundamentele en prestatiekritieke functionaliteit is, kan het echter ingewikkeld worden.
Ten eerste moet de gebruikte sorteerbibliotheek hiervan op de hoogte zijn. De meeste systeem C-bibliotheken, inclusief glibc, zijn dat echter niet. Dus als u in glibc zoekt naar 'ä', zult u 'ä' niet vinden. Zie wat ik daar heb gedaan? De tweede is anders gecodeerd, maar ziet er waarschijnlijk hetzelfde uit als u leest. (Tenminste zo heb ik het ingevoerd. Het kan ergens onderweg naar uw browser zijn gewijzigd.) Verwarrend. Als u ICU gebruikt voor sorteringen, dan werkt dit en wordt het volledig ondersteund.
Ten tweede, wanneer PostgreSQL strings vergelijkt voor gelijkheid, vergelijkt het alleen de bytes, het houdt geen rekening met de mogelijkheid dat dezelfde string op verschillende manieren kan worden weergegeven. Dit is technisch verkeerd bij het gebruik van Unicode, maar het is een noodzakelijke prestatie-optimalisatie. Om dat te omzeilen, kunt u niet-deterministische sorteringen gebruiken , een functie die is geïntroduceerd in PostgreSQL 12. Een sortering die op die manier is gedeclareerd, zal niet vergelijk gewoon de bytes
maar zal alle noodzakelijke voorbewerkingen uitvoeren om strings te kunnen vergelijken of hashen die op verschillende manieren kunnen zijn gecodeerd. Voorbeeld:
CREATE COLLATION ndcoll (provider = icu, locale = 'und', deterministic = false);
Normalisatieformulieren
Dus hoewel er verschillende geldige manieren zijn om bepaalde Unicode-tekens te coderen, is het soms handig om ze allemaal naar een consistente vorm te converteren. Dit heet normalisatie . Er zijn twee normalisatievormen :volledig samengesteld , wat betekent dat we alle codepuntreeksen zoveel mogelijk converteren naar vooraf samengestelde tekens en volledig ontleed , wat betekent dat we alle codepunten zoveel mogelijk naar hun samenstellende delen (letter plus accent) converteren. In Unicode-terminologie staan deze vormen respectievelijk bekend als NFC en NFD. Er zijn wat meer details, zoals het plaatsen van alle combinerende karakters in een canonieke volgorde, maar dat is het algemene idee. Het punt is dat wanneer je een Unicode-tekenreeks converteert naar een van de normalisatievormen, je ze bytegewijs kunt vergelijken of hashen zonder je zorgen te hoeven maken over coderingsvarianten. Welke je gebruikt maakt niet uit, zolang het hele systeem maar akkoord gaat.
In de praktijk maakt het grootste deel van de wereld gebruik van NFC. En bovendien zijn veel systemen defect omdat ze niet-NFC Unicode niet correct verwerken, inclusief de sorteerfaciliteiten van de meeste C-bibliotheken, en zelfs standaard PostgreSQL, zoals hierboven vermeld. Dus ervoor zorgen dat alle Unicode wordt geconverteerd naar NFC is een goede manier om betere interoperabiliteit te garanderen.
Normalisatie in PostgreSQL
PostgreSQL 13 bevat nu twee nieuwe faciliteiten om met Unicode-normalisatie om te gaan:een functie om te testen op normalisatie en één om te converteren naar een normalisatievorm. Bijvoorbeeld:
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
(De syntaxis is gespecificeerd in de SQL-standaard.)
Een optie is om dit in een domein te gebruiken, bijvoorbeeld:
CREATE DOMAIN norm_text AS text CHECK (VALUE IS NORMALIZED);
Merk op dat normalisatie van willekeurige tekst niet helemaal goedkoop is. Pas dit dus verstandig toe en alleen waar het er echt toe doet.
Merk ook op dat normalisatie niet wordt afgesloten onder aaneenschakeling. Dat betekent dat het toevoegen van twee genormaliseerde strings niet altijd resulteert in een genormaliseerde string. Dus zelfs als je deze functies zorgvuldig toepast en ook anders controleert of je systeem alleen genormaliseerde strings gebruikt, kunnen ze nog steeds "binnensluipen" tijdens legitieme operaties. Dus gewoon aannemen dat niet-genormaliseerde strings niet kunnen gebeuren, zal mislukken; dit probleem moet goed worden opgelost.
Compatibiliteitstekens
Er is nog een andere use-case voor normalisatie. Unicode bevat een aantal alternatieve vormen van letters en andere tekens, voor verschillende legacy- en compatibiliteitsdoeleinden. U kunt bijvoorbeeld Fraktur schrijven:
SELECT '𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢';
Stel je nu voor dat je applicatie gebruikersnamen of andere dergelijke identifiers toewijst, en er is een gebruiker met de naam 'somename'
en een andere genaamd '𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢'
. Dit zou op zijn minst verwarrend zijn, maar mogelijk een veiligheidsrisico. Het misbruiken van dergelijke overeenkomsten wordt vaak gebruikt bij phishing-aanvallen, valse URL's en soortgelijke problemen. Unicode bevat dus twee extra normalisatievormen die deze overeenkomsten oplossen en dergelijke alternatieve vormen omzetten in een canonieke basisletter. Deze vormen worden NFKC en NFKD genoemd. Verder zijn ze hetzelfde als respectievelijk NFC en NFD. Bijvoorbeeld:
=> select normalize('𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢', nfkc); normalize ----------- somename
Nogmaals, het kan handig zijn om controlebeperkingen te gebruiken als onderdeel van een domein:
CREATE DOMAIN username AS text CHECK (VALUE IS NFKC NORMALIZED OR VALUE IS NFKD NORMALIZED);
(De eigenlijke normalisatie moet waarschijnlijk worden gedaan in de frontend van de gebruikersinterface.)
Zie ook RFC 3454 voor een behandeling van strings om dergelijke problemen aan te pakken.
Samenvatting
Unicode-equivalentieproblemen worden vaak genegeerd zonder gevolgen. In veel contexten zijn de meeste gegevens in NFC-vorm, dus er doen zich geen problemen voor. Het negeren van deze problemen kan echter leiden tot vreemd gedrag, schijnbaar ontbrekende gegevens en in sommige situaties veiligheidsrisico's. Bewustwording van deze problemen is dus belangrijk voor databaseontwerpers, en de tools die in dit artikel worden beschreven, kunnen worden gebruikt om ze aan te pakken.