La version 15 de PostgreSQL a rendu possible les collations ICU au niveau de la base (options locale_provider='icu' icu_locale='...' de CREATE DATABASE), et au niveau du cluster (initdb --locale-provider=icu --icu-locale=...)

Avec PostgreSQL 16 actuellement en version beta1, ICU devient le fournisseur par défaut, c’est-à-dire que sans indiquer de fournisseur de collation, c’est ICU qui va être privilégié. (Mise à jour: la beta2 est revenue en arrière sur ce choix: libc est le fournisseur de collation par défaut, comme dans la 15 et les versions précédentes.).

Quelles sont les conséquences de passer à ICU? Quand on parle de collations ICU, on pense généralement aux tris (la clause ORDER BY) susceptibles de produire des résultats différents. Mais elles ont aussi une influence parfois ignorée sur les fonctions qui utilisent la classification des caractères, notamment les expressions régulières et la mise en majuscule et minuscules.

Sur la majorité des chaînes de caractères, les résultats entre les collations libc et ICU sont les mêmes. Mais voyons deux exemples où les résultats diffèrent, non seulement entre libc et ICU sur Linux mais entre les libc de différents systèmes d’exploitation: 1

Reconnaissance des chiffres

Traditionnellement dans les expressions régulières on utilise pour reconnaître les chiffres

  • [0-9]
  • \d
  • [[::digit::]] (la classe de caractères)

Avec la libc de Linux (GNU libc), ça revient au même, car seuls les caractères ASCII de base '0123456789' (codes entre 0x30 et 0x39) sont reconnus par ces sélecteurs. On peut le vérifier avec la requête suivante, qui teste tous les points de code valides du répertoire Unicode:

SELECT 
 to_hex(cp),
 chr(cp)
FROM
  -- blocs de 0 à 0xd7ff et 0xf900-0xeffff
  (select * from generate_series(1, 55295) cp
    union all
   select * from generate_series(63744, 983039) cp) AS s
WHERE
chr(cp) COLLATE "fr_FR.utf8" ~ '\d';

Résultat:

to_hex | chr 
--------+-----
 30     | 0
 31     | 1
 32     | 2
 33     | 3
 34     | 4
 35     | 5
 36     | 6
 37     | 7
 38     | 8
 39     | 9
(10 lignes)

Mais si on remplace la collation "fr_FR.utf8" par la collation ICU "fr-x-icu", surprise, pas moins de 660 points de code différents correspondent.

Voici un extrait des résultats:

to_hex | chr 
--------+-----
 30     | 0
 31     | 1
 32     | 2
 33     | 3
 34     | 4
 35     | 5
 36     | 6
 37     | 7
 38     | 8
 39     | 9
 660    | ٠
 661    | ١
 662    | ٢
 663    | ٣
 664    | ٤
 ...
 ff10   | 0
 ff11   | 1
 ff12   | 2
 ff13   | 3
 ff14   | 4
 ff15   | 5
 ff16   | 6
 ff17   | 7
 ff18   | 8
 ff19   | 9
 104a0  | 𐒠
 104a1  | 𐒡
 104a2  | 𐒢
 104a3  | 𐒣
 104a4  | 𐒤
 104a5  | 𐒥
 104a6  | 𐒦
 104a7  | 𐒧
 104a8  | 𐒨
 104a9  | 𐒩
  ...
 1d7ce  | 𝟎
 1d7cf  | 𝟏
 1d7d0  | 𝟐
 1d7d1  | 𝟑
 1d7d2  | 𝟒
 1d7d3  | 𝟓
 1d7d4  | 𝟔

On voit qu’on a d’une part des chiffres hors alphabet latin, dans des blocs Unicode comme Tamil, Telugu, Thai, etc. ainsi que des variantes calligraphiques des chiffres en alphabet latin:

  • FULLWIDTH DIGIT ZERO à NINE: 0123456789
  • MATHEMATICAL BOLD DIGIT ZERO à NINE: 𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗
  • MATHEMATICAL DOUBLE-STRUCK DIGIT ZERO à NINE: 𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡
  • MATHEMATICAL SANS-SERIF DIGIT ZERO à NINE: 𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫
  • MATHEMATICAL SANS-SERIF BOLD DIGIT ZERO à NINE: 𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵
  • MATHEMATICAL MONOSPACE DIGIT ZERO à NINE: 𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿

La conséquence est que si dans une base de données on utilisait \d ou [[:digit:]] pour filtrer les chiffres, passer à ICU laissera passer beaucoup plus de caractères. Suivant les applications, cela peut être une bonne chose ou non. Si on ne veut pas de ces caractères en tant que chiffres, on peut régler le problème en remplaçant \d et [[:digit:]] par [0-9]. Mais le vrai problème ne sera peut-être pas tant de faire le changement que de savoir qu’il est nécessaire…

Mise à jour: comme on me l’a fait remarquer sur Twitter 2, d’autres libc commme celle de FreeBSD donnent des résultats très différents de Linux. Pour illustrer cela, voici le tableau détaillé des résultats avec ICU et les libc de trois systèmes distincts.

Codepoints Character names ICU glibc FreeBSD Win10
00030→00039 DIGIT ZERO → NINE ✓ ✓ ✓ ✓
000b2 SUPERSCRIPT TWO       ✓
000b3 SUPERSCRIPT THREE       ✓
000b9 SUPERSCRIPT ONE       ✓
00660→00669 ARABIC-INDIC DIGIT ZERO → NINE ✓   ✓ ✓
006f0→006f9 EXTENDED ARABIC-INDIC DIGIT ZERO → NINE ✓   ✓ ✓
007c0→007c9 NKO DIGIT ZERO → NINE ✓   ✓ ✓
00966→0096f DEVANAGARI DIGIT ZERO → NINE ✓   ✓ ✓
009e6→009ef BENGALI DIGIT ZERO → NINE ✓   ✓ ✓
00a66→00a6f GURMUKHI DIGIT ZERO → NINE ✓   ✓ ✓
00ae6→00aef GUJARATI DIGIT ZERO → NINE ✓   ✓ ✓
00b66→00b6f ORIYA DIGIT ZERO → NINE ✓   ✓ ✓
00be6→00bef TAMIL DIGIT ZERO → NINE ✓   ✓ ✓
00c66→00c6f TELUGU DIGIT ZERO → NINE ✓   ✓ ✓
00ce6→00cef KANNADA DIGIT ZERO → NINE ✓   ✓ ✓
00d66→00d6f MALAYALAM DIGIT ZERO → NINE ✓   ✓ ✓
00de6→00def SINHALA LITH DIGIT ZERO → NINE ✓   ✓  
00e50→00e59 THAI DIGIT ZERO → NINE ✓   ✓ ✓
00ed0→00ed9 LAO DIGIT ZERO → NINE ✓   ✓ ✓
00f20→00f29 TIBETAN DIGIT ZERO → NINE ✓   ✓ ✓
01040→01049 MYANMAR DIGIT ZERO → NINE ✓   ✓ ✓
01090→01099 MYANMAR SHAN DIGIT ZERO → NINE ✓   ✓ ✓
017e0→017e9 KHMER DIGIT ZERO → NINE ✓   ✓ ✓
01810→01819 MONGOLIAN DIGIT ZERO → NINE ✓   ✓ ✓
01946→0194f LIMBU DIGIT ZERO → NINE ✓   ✓ ✓
019d0→019d9 NEW TAI LUE DIGIT ZERO → NINE ✓   ✓ ✓
01a80→01a89 TAI THAM HORA DIGIT ZERO → NINE ✓   ✓  
01a90→01a99 TAI THAM THAM DIGIT ZERO → NINE ✓   ✓  
01b50→01b59 BALINESE DIGIT ZERO → NINE ✓   ✓ ✓
1bb0→1bb9 SUNDANESE DIGIT ZERO → NINE ✓   ✓ ✓
1c40→1c49 LEPCHA DIGIT ZERO → NINE ✓   ✓ ✓
1c50→1c59 OL CHIKI DIGIT ZERO → NINE ✓   ✓ ✓
a620→a629 VAI DIGIT ZERO → NINE ✓   ✓ ✓
a8d0→a8d9 SAURASHTRA DIGIT ZERO → NINE ✓   ✓ ✓
a900→a909 KAYAH LI DIGIT ZERO → NINE ✓   ✓ ✓
a9d0→a9d9 JAVANESE DIGIT ZERO → NINE ✓   ✓  
a9f0→a9f9 MYANMAR TAI LAING DIGIT ZERO → NINE ✓   ✓  
aa50→aa59 CHAM DIGIT ZERO → NINE ✓   ✓ ✓
abf0→abf9 MEETEI MAYEK DIGIT ZERO → NINE ✓   ✓  
ff10→ff19 FULLWIDTH DIGIT ZERO → NINE ✓   ✓ ✓
104a0→104a9 OSMANYA DIGIT ZERO → NINE ✓   ✓  
10d30→10d39 HANIFI ROHINGYA DIGIT ZERO → NINE ✓   ✓  
11066→1106f BRAHMI DIGIT ZERO → NINE ✓   ✓  
110f0→110f9 SORA SOMPENG DIGIT ZERO → NINE ✓   ✓  
11136→1113f CHAKMA DIGIT ZERO → NINE ✓   ✓  
111d0→111d9 SHARADA DIGIT ZERO → NINE ✓   ✓  
112f0→112f9 KHUDAWADI DIGIT ZERO → NINE ✓   ✓  
11450→11459 NEWA DIGIT ZERO → NINE ✓   ✓  
114d0→114d9 TIRHUTA DIGIT ZERO → NINE ✓   ✓  
11650→11659 MODI DIGIT ZERO → NINE ✓   ✓  
116c0→116c9 TAKRI DIGIT ZERO → NINE ✓   ✓  
11730→11739 AHOM DIGIT ZERO → NINE ✓   ✓  
118e0→118e9 WARANG CITI DIGIT ZERO → NINE ✓   ✓  
11950→11959 DIVES AKURU DIGIT ZERO → NINE ✓      
11c50→11c59 BHAIKSUKI DIGIT ZERO → NINE ✓   ✓  
11d50→11d59 MASARAM GONDI DIGIT ZERO → NINE ✓   ✓  
11da0→11da9 GUNJALA GONDI DIGIT ZERO → NINE ✓   ✓  
16a60→16a69 MRO DIGIT ZERO → NINE ✓   ✓  
16ac0→16ac9 TANGSA DIGIT ZERO → NINE ✓      
16b50→16b59 PAHAWH HMONG DIGIT ZERO → NINE ✓   ✓  
1e140→1e149 NYIAKENG PUACHUE HMONG DIGIT ZERO → NINE ✓      
1e2f0→1e2f9 WANCHO DIGIT ZERO → NINE ✓      
1e950→1e959 ADLAM DIGIT ZERO → NINE ✓   ✓  
1d7ce→1d7d7 MATHEMATICAL BOLD DIGIT ZERO → NINE ✓   ✓  
1d7d8→1d7e1 MATHEMATICAL DOUBLE-STRUCK DIGIT ZERO → NINE ✓   ✓  
1d7e2→1d7eb MATHEMATICAL SANS-SERIF DIGIT ZERO → NINE ✓   ✓  
1d7ec→1d7f5 MATHEMATICAL SANS-SERIF BOLD DIGIT ZERO → NINE ✓   ✓  
1d7f6→1d7ff MATHEMATICAL MONOSPACE DIGIT ZERO → NINE ✓   ✓  
1fbf0→1fbf9 SEGMENTED DIGIT ZERO → NINE ✓      

Reconnaissance des espaces

Pour les caractères d’espacement il est pratique d’utiliser dans les expressions régulières:

  • \s
  • [[::space::]] (la classe de caractères)

A nouveau il y a des différences entre libc et ICU, montrées par la requête suivante, qui teste tous les points de code Unicode avec les deux fournisseurs (dans la requête la fonction icu_char_name() vient de l’extension icu_ext).

SELECT
 lpad(to_hex(cp), 4, '0') as code,
 icu_char_name(chr(cp)),
 case when chr(cp) collate "fr_FR.utf8" ~ '\s' then E'\u2713' end as "LIBC",
 case when chr(cp) collate "fr-x-icu" ~ '\s' then E'\u2713' end as "ICU" 
FROM (select * from generate_series(1, 55295) cp
union all
select * from generate_series(63744,1000000) cp) AS s
WHERE chr(cp) collate "fr-x-icu" ~ '\s'
   OR chr(cp) collate "fr_FR.utf8" ~ '\s'
;

Résultats sur différents systèmes 1:

Codepoint Character name ICU glibc FreeBSD Win10
0009 <control-0009> ✓ ✓ ✓ ✓
000a <control-000A> ✓ ✓ ✓ ✓
000b <control-000B> ✓ ✓ ✓ ✓
000c <control-000C> ✓ ✓ ✓ ✓
000d <control-000D> ✓ ✓ ✓ ✓
001c <control-001C> ✓      
001d <control-001D> ✓      
001e <control-001E> ✓      
001f <control-001F> ✓      
0020 SPACE ✓ ✓ ✓ ✓
0085 <control-0085> ✓     ✓
00a0 NO-BREAK SPACE ✓   ✓ ✓
1680 OGHAM SPACE MARK ✓ ✓ ✓ ✓
180e MONGOLIAN VOWEL SEPARATOR       ✓
2000 EN QUAD ✓ ✓ ✓ ✓
2001 EM QUAD ✓ ✓ ✓ ✓
2002 EN SPACE ✓ ✓ ✓ ✓
2003 EM SPACE ✓ ✓ ✓ ✓
2004 THREE-PER-EM SPACE ✓ ✓ ✓ ✓
2005 FOUR-PER-EM SPACE ✓ ✓ ✓ ✓
2006 SIX-PER-EM SPACE ✓ ✓ ✓ ✓
2007 FIGURE SPACE ✓   ✓ ✓
2008 PUNCTUATION SPACE ✓ ✓ ✓ ✓
2009 THIN SPACE ✓ ✓ ✓ ✓
200a HAIR SPACE ✓ ✓ ✓ ✓
2028 LINE SEPARATOR ✓ ✓ ✓ ✓
2029 PARAGRAPH SEPARATOR ✓ ✓ ✓ ✓
202f NARROW NO-BREAK SPACE ✓   ✓ ✓
205f MEDIUM MATHEMATICAL SPACE ✓ ✓ ✓ ✓
3000 IDEOGRAPHIC SPACE ✓ ✓ ✓ ✓

Conclusion: en-dehors de la tabulation (code 9) et de l’espace ASCII (code 0x20) qui sont très connus, il y a 28 autres points de codes qui sont reconnus en tant qu’espace suivant les systèmes. Sur ceux-là, une partie est reconnue par les collations ICU et non par glibc, notamment le NO-BREAK-SPACE (0xa0) qui correspond à l’entité &nbsp; du HTML, qui peut être présente dans beaucoup de textes. Le passage à ICU pourra donc occasionner avec ces caractères des différences dans l’extraction via expressions régulières (regexp_match[es]) de termes séparés par des espaces.

Mise en majuscules

Les fonctions upper() passent aussi par le fournisseur de collation pour obtenir un résultat. Pour libc, chaque caractère donnera un seul caractère en sortie, ne serait-ce que parce que l’API C est conçue comme cela. Côté ICU en revanche, des exceptions existent. Par exemple, pour les langues européennes:

  • le caractère ß (Eszett) est mis en majuscules sous la forme SS.
  • le caractère fi (Ligature minuscule latine fi) est mis en majuscules en FI.

Illustration par la requête suivante:

 WITH words(w) as  (values('muß'),('final'))
 SELECT
  w as mot,
  length(w) as longueur,
  upper(w collate "fr_FR.utf8") as "en majuscules (libc)",
  length(upper(w collate "fr_FR.utf8")),
  upper(w collate "en-x-icu") as "en majuscules (ICU)",
  length(upper(w collate "en-x-icu"))
FROM words;

Résultat:

 mot  | longueur | en majuscules (libc) | length | en majuscules (ICU) | length 
------+----------+----------------------+--------+---------------------+--------
 muß  |        3 | MUß                  |      3 | MUSS                |      4
 final |        4 | fiNAL                 |      4 | FINAL               |      5

En fait libc laisse les caractères minuscules ß et fi inchangés alors qu’ICU les convertit en deux caractères majuscules.


PostgreSQL et Unicode, un tour des nouveautés

Vous voulez en savoir plus sur les nouveautés Unicode et les collations ICU depuis PostgreSQL 10? Je ferai une présentation sur ce thème le lundi 19 juin au PgDay 2023 à Strasbourg.



  1. les exemples sont exécutés sur un système Ubuntu 22.04 (GNU libc version 2.35, ICU version 70.1), FreBSD 12.4, et Windows 10 Home build 19045.3086 avec locale French_France.1252. ↩ ↩2

  2. fil Twitter ↩