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 (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 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