Dordogne, le 7 septembre 2023
Nos clients support nous apportent parfois des défis techniques qui nécessitent de sortir de notre expertise PostgreSQL et de descendre dans des couches systèmes qui peuvent nous être peu familières.
Ce fut le cas en janvier 2023 lorsque la DGFIP nous a demandé s’il était possible de créer une collation personnalisée respectant l’ordre de tri des données au format EBCDIC.
Avec l’accord de la DGFIP, nous détaillons dans cet article le résultat des travaux réalisés conjointement entre leur équipe et la nôtre pour aboutir à une solution fonctionnelle.
Pour les plus pressés, vous pouvez directement aller voir le chapitre de Démonstration.
Profitons de ce contexte pour dépoussiérer un peu les notions de tables de caractères et de collations afin de chasser quelques confusions.
Table de caractères
L’EBCDIC est une table de caractères inventée par IBM, utilisée notamment dans de nombreux mainframes. Comme l’ASCII, l’UTF-8 ou même le morse, une table de caractères défini comment chaque caractère est codé.
Les tables de caractères utilisées en informatique (Charset en anglais) sont numériques afin de les représenter et de les manipuler en binaire. Voici quelques exemples de caractères avec leur code en EBCDIC et ASCII côte-à-côte :
Caractère code code
ASCII EBCDIC
'0' 48 240
'1' 49 241
'2' 50 242
...
'A' 65 193
'B' 66 194
'C' 67 195
...
'a' 97 129
'b' 98 130
'c' 99 131
Une table de caractère ne définit pas comment trier un alphabet, uniquement comment interpréter un code en tant que caractère et inversement.
Ceci dit, nous constatons que les codes employés en ASCII et EBCDIC respectent
l’ordre de l’alphabet. Et dans les faits, la fonction standard C strcmp
est
une solution très performante pour comparer des lettres en fonction du code y
étant associé.
Néanmoins, un tel tri est trivial et ne tient aucunement compte des variations
culturelles. Par exemple, nous constatons que l’ASCII place les chiffres en
premier, les majuscules en second, et les minuscules en troisième alors que
l’EBCDIC fait l’exact opposé. La fonction strcmp
retourne donc des résultats
différents pour des caractères encodés en ASCII ou en EBCDIC.
De ce fait, il nous manque une solution permettant d’obtenir toujours le même tri, quelque soit la table de caractères choisie.
Les collations
Il existe plusieurs alphabets et plusieurs façons de trier chacun d’eux, en fonction des langues et des cultures. Imaginez devoir choisir le charset de vos données en fonction de comment vous souhaitez les trier ? Que se passe-t-il lorsque la norme culturelle évolue ? Comment gérer les tris pour une même langue mais dans des cultures différentes ? Et la portabilité de ces données ?
Plutôt que de créer autant de charset que de tris possibles, la notion de collation permet de définir plusieurs tris possibles pour un même charset. Ces collations font partis par ailleurs d’un ensemble plus large de fonctionnalités linguistiques appelées les locales.
Les collations permettent donc d’effectuer des tris homogènes d’un système à
l’autre, tout en se détachant des charsets utilisés. C’est pour cette raison
que la plupart des applications modernes trient à l’aide de ces collations, en
utilisant par exemple la famille de fonctions strcoll
pour comparer les
caractères entre eux. C’est le cas des outils standards (coreutils
) comme
sort
ou ls
et bien entendu de PostgreSQL.
Nous pouvons illustrer ce qu’est une collation en observant une des différences
entre les collations françaises Canadienne fr_CA
et Française fr_FR
: le
premier tri les lettres majuscules avant leur équivalent en minuscule et le
second l’inverse. Comparez ces deux commandes :
$ LC_COLLATE=fr_CA ls
DUMMY_FILE dummy_file
$ LC_COLLATE=fr_FR ls
dummy_file DUMMY_FILE
Il existe de nombreuses collations. Sur une Fedora 38, il est possible d’en dénombrer 874, dont 22 françaises :
$ locale -a|wc -l
874
$ locale -a|grep -c '^fr[_e]'
22
Ceci dit, comme expliqué plus haut, la plupart d’entre elles sont en réalité
définies dans plusieurs charsets différents. Dans l’exemple ci-après, la
collation fr_FR
est disponible pour les tables de caractères UTF-8, ISO
8859-1 et 8859-15 :
$ locale -a|grep 'fr_FR\.'
fr_FR.iso88591
fr_FR.iso885915@euro
fr_FR.utf8
Ce tri est donc applicable pour toutes données encodées selon l’un de ces encodages.
PostgreSQL et charsets
Une table de caractères permet donc de représenter numériquement un alphabet, une collation permet de définir les différentes façon d’en trier les caractères.
Nous retrouvons ces deux concepts dans PostgreSQL. En son cœur, le moteur supporte de nombreuses tables de caractères, mais pas l’EBCDIC. Voir à ce propos :
https://www.postgresql.org/docs/current/multibyte.html#MULTIBYTE-CHARSET-SUPPORTED
PostgreSQL est donc capable de stocker vos données sur disque au format
ISO 8859-15, UTF-8 ou encore SJIS en fonction de l’ENCODING
que vous aurez
choisi à la création de votre base de données. Mais pas au format EBCDIC donc.
Mais rien n’est perdu, ce ne sont que des formats de stockage et les collations sont justement là pour définir des tris indépendants des charsets. Peut-être existe-t-il une collation respectant l’ordre EBCDIC ?
PostgreSQL et collation
Concernant les tris textuels, PostgreSQL propose trois méthodes:
POSIX
ouC
- l’utilisation de la librairie ICU
- l’utilisation de la librairie standard C
Les collations POSIX
ou C
reposent sur le standard POSIX. Cette collation
suit simplement l’ordre ASCII, le comportement étant “indéfini” pour tout autre
caractère n’y apparaissant pas. Cette définition est détaillée dans le chapitre
suivant du standard POSIX:
https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap07.html#tag_07_03_02_06
Les différents encodages supportés par PostgreSQL sont en réalité des extensions de l’ASCII, chacun le complétant avec des caractères supplémentaires. Ces deux collations sont donc supportées quelque soit l’encodage choisi pour vos bases. Leur utilisation permet par ailleurs au moteur quelques optimisations notables dans ses comparaisons, la collation étant “équivalente” à l’ordre de la table de caractères utilisé.
Concernant l’EBCDIC, nous avons vu qu’il n’est pas supporté par PostgreSQL
comme encodage d’une part, et qu’il n’est pas compatible avec l’ASCII d’autre
part, et donc incompatible avec les collations POSIX
ou C
de toute façon.
En consultant dans pg_catalog.pg_collation
les autres collations supportées
par ICU ou la libraire standard C, aucune ne concerne l’EBCDIC. De base, il
n’est donc pas possible de trier suivant l’ordre d’encodage EBCDIC sous
PostgreSQL.
Nous en venons enfin à la question posée à notre support: est-il possible de créer une collation suivant l’ordre EBCDIC en utilisant les librairies ICU ou standard C ?
Personnalisation des collations ICU
La librairie ICU est très souple, permet de “configurer” une collation en manipulant des familles de caractères, et même de modifier l’ordre des caractères au travers de règles. À propos des règles ICU, voir :
https://unicode-org.github.io/icu/userguide/collation/customization/
Cependant, avant la version 16 de PostgreSQL, CREATE COLLATION
ne permettait
pas préciser de règles supplémentaires et les fonctionnalités supportées
n’étaient pas suffisantes pour définir une collation EBCDIC.
Si vous voulez en savoir plus sur comment créer une collation EBCDIC à l’aide d’ICU à partir de PostgreSQL 16, voir le message de juin 2023 de Daniel Vérité sur la liste pgsql-hackers :
https://www.postgresql.org/message-id/35cc1684-e516-4a01-a256-351632d47066%40manitou-mail.org
Personnalisation des collations glibc
La librairie standard C supporte la création de locale, incluant une collation,
via un fichier de configuration. Ce fichier est interprété par l’outil
localedef
qui produit alors un fichier binaire utile par exemple à ses
fonctions de comparaison tenant compte des collations. Cet outil est inclut
dans la liste des outils que doit fournir tout système compatible
POSIX1.
Les fichiers sources des collations supportées par défaut (ainsi que les
fichiers de charset) sont définis dans le paquet RPM glibc-locale-source
ou
le paquet locales
pour Debian et ses dérivés.
Bien entendu, aucune collation EBCDIC n’est distribuée par défaut parmi ces fichiers par le projet GNU C Library.
Cependant, il est donc théoriquement possible de produire la définition d’une locale incluant une collation suivant l’ordre EBCDIC !
Entre autres solutions, l’équipe support de Dalibo a donc établi une procédure hypothétique que nous avons fourni à l’équipe DGFIP, mais sans le fichier de configuration. Effectivement, nous étions arrivé à la limite des compétences nécessaires parmi nos experts pour créer un tel fichier. Aussi, nous n’avions des moyens que très limités pour en valider le résultat. Il ne nous était pas possible de fournir ce fichier de façon fiable et validée, prêt pour la production.
L’équipe de la DGFIP a donc fait le reste du chemin seule, a ajusté les commandes de notre procédure et nous a fourni quelques jours après ce long fichier de configuration (télécharger), ajoutant: « Si cela peut servir à d’autres ».
comment_char %
escape_char /
% This is a french locale with IBM500-like collation
% (c) 2023 DGFiP - CC-BY
% Date: 2023-02-07
LC_IDENTIFICATION
title "French locale with IBM500-compliant collation"
source "DGFIP"
address ""
contact ""
email ""
tel ""
fax ""
language "French"
territory "France"
revision "1.2"
date "2023-02-07"
category "i18n:2012";LC_IDENTIFICATION
category "i18n:2012";LC_CTYPE
category "i18n:2012";LC_COLLATE
category "i18n:2012";LC_TIME
category "i18n:2012";LC_NUMERIC
category "i18n:2012";LC_MONETARY
category "i18n:2012";LC_MESSAGES
category "i18n:2012";LC_PAPER
category "i18n:2012";LC_NAME
category "i18n:2012";LC_ADDRESS
category "i18n:2012";LC_TELEPHONE
category "i18n:2012";LC_MEASUREMENT
END LC_IDENTIFICATION
LC_CTYPE
copy "fr_FR"
END LC_CTYPE
LC_COLLATE
% simple ebcdic-like collation (one level, defined by the value of the IBM500 encoded characters)
order_start forward
UNDEFINED IGNORE
<U0000> % /x00 NULL (NUL)
<U0001> % /x01 START OF HEADING (SOH)
<U0002> % /x02 START OF TEXT (STX)
<U0003> % /x03 END OF TEXT (ETX)
<U009C> % /x04 STRING TERMINATOR (ST)
<U0009> % /x05 CHARACTER TABULATION (HT)
<U0086> % /x06 START OF SELECTED AREA (SSA)
<U007F> % /x07 DELETE (DEL)
<U0097> % /x08 END OF GUARDED AREA (EPA)
<U008D> % /x09 REVERSE LINE FEED (RI)
<U008E> % /x0a SINGLE-SHIFT TWO (SS2)
<U000B> % /x0b LINE TABULATION (VT)
<U000C> % /x0c FORM FEED (FF)
<U000D> % /x0d CARRIAGE RETURN (CR)
<U000E> % /x0e SHIFT OUT (SO)
<U000F> % /x0f SHIFT IN (SI)
<U0010> % /x10 DATALINK ESCAPE (DLE)
<U0011> % /x11 DEVICE CONTROL ONE (DC1)
<U0012> % /x12 DEVICE CONTROL TWO (DC2)
<U0013> % /x13 DEVICE CONTROL THREE (DC3)
<U009D> % /x14 OPERATING SYSTEM COMMAND (OSC)
<U0085> % /x15 NEXT LINE (NEL)
<U0008> % /x16 BACKSPACE (BS)
<U0087> % /x17 END OF SELECTED AREA (ESA)
<U0018> % /x18 CANCEL (CAN)
<U0019> % /x19 END OF MEDIUM (EM)
<U0092> % /x1a PRIVATE USE TWO (PU2)
<U008F> % /x1b SINGLE-SHIFT THREE (SS3)
<U001C> % /x1c FILE SEPARATOR (IS4)
<U001D> % /x1d GROUP SEPARATOR (IS3)
<U001E> % /x1e RECORD SEPARATOR (IS2)
<U001F> % /x1f UNIT SEPARATOR (IS1)
<U0080> % /x20 PADDING CHARACTER (PAD)
<U0081> % /x21 HIGH OCTET PRESET (HOP)
<U0082> % /x22 BREAK PERMITTED HERE (BPH)
<U0083> % /x23 NO BREAK HERE (NBH)
<U0084> % /x24 INDEX (IND)
<U000A> % /x25 LINE FEED (LF)
<U0017> % /x26 END OF TRANSMISSION BLOCK (ETB)
<U001B> % /x27 ESCAPE (ESC)
<U0088> % /x28 CHARACTER TABULATION SET (HTS)
<U0089> % /x29 CHARACTER TABULATION WITH JUSTIFICATION (HTJ)
<U008A> % /x2a LINE TABULATION SET (VTS)
<U008B> % /x2b PARTIAL LINE FORWARD (PLD)
<U008C> % /x2c PARTIAL LINE BACKWARD (PLU)
<U0005> % /x2d ENQUIRY (ENQ)
<U0006> % /x2e ACKNOWLEDGE (ACK)
<U0007> % /x2f BELL (BEL)
<U0090> % /x30 DEVICE CONTROL STRING (DCS)
<U0091> % /x31 PRIVATE USE ONE (PU1)
<U0016> % /x32 SYNCHRONOUS IDLE (SYN)
<U0093> % /x33 SET TRANSMIT STATE (STS)
<U0094> % /x34 CANCEL CHARACTER (CCH)
<U0095> % /x35 MESSAGE WAITING (MW)
<U0096> % /x36 START OF GUARDED AREA (SPA)
<U0004> % /x37 END OF TRANSMISSION (EOT)
<U0098> % /x38 START OF STRING (SOS)
<U0099> % /x39 SINGLE GRAPHIC CHARACTER INTRODUCER (SGCI)
<U009A> % /x3a SINGLE CHARACTER INTRODUCER (SCI)
<U009B> % /x3b CONTROL SEQUENCE INTRODUCER (CSI)
<U0014> % /x3c DEVICE CONTROL FOUR (DC4)
<U0015> % /x3d NEGATIVE ACKNOWLEDGE (NAK)
<U009E> % /x3e PRIVACY MESSAGE (PM)
<U001A> % /x3f SUBSTITUTE (SUB)
<U0020> % /x40 SPACE
<U00A0> % /x41 NO-BREAK SPACE
<U00E2> % /x42 LATIN SMALL LETTER A WITH CIRCUMFLEX
<U00E4> % /x43 LATIN SMALL LETTER A WITH DIAERESIS
<U00E0> % /x44 LATIN SMALL LETTER A WITH GRAVE
<U00E1> % /x45 LATIN SMALL LETTER A WITH ACUTE
<U00E3> % /x46 LATIN SMALL LETTER A WITH TILDE
<U00E5> % /x47 LATIN SMALL LETTER A WITH RING ABOVE
<U00E7> % /x48 LATIN SMALL LETTER C WITH CEDILLA
<U00F1> % /x49 LATIN SMALL LETTER N WITH TILDE
<U005B> % /x4a LEFT SQUARE BRACKET
<U002E> % /x4b FULL STOP
<U003C> % /x4c LESS-THAN SIGN
<U0028> % /x4d LEFT PARENTHESIS
<U002B> % /x4e PLUS SIGN
<U0021> % /x4f EXCLAMATION MARK
<U0026> % /x50 AMPERSAND
<U00E9> % /x51 LATIN SMALL LETTER E WITH ACUTE
<U00EA> % /x52 LATIN SMALL LETTER E WITH CIRCUMFLEX
<U00EB> % /x53 LATIN SMALL LETTER E WITH DIAERESIS
<U00E8> % /x54 LATIN SMALL LETTER E WITH GRAVE
<U00ED> % /x55 LATIN SMALL LETTER I WITH ACUTE
<U00EE> % /x56 LATIN SMALL LETTER I WITH CIRCUMFLEX
<U00EF> % /x57 LATIN SMALL LETTER I WITH DIAERESIS
<U00EC> % /x58 LATIN SMALL LETTER I WITH GRAVE
<U00DF> % /x59 LATIN SMALL LETTER SHARP S (German)
<U005D> % /x5a RIGHT SQUARE BRACKET
<U0024> % /x5b DOLLAR SIGN
<U002A> % /x5c ASTERISK
<U0029> % /x5d RIGHT PARENTHESIS
<U003B> % /x5e SEMICOLON
<U005E> % /x5f CIRCUMFLEX ACCENT
<U002D> % /x60 HYPHEN-MINUS
<U002F> % /x61 SOLIDUS
<U00C2> % /x62 LATIN CAPITAL LETTER A WITH CIRCUMFLEX
<U00C4> % /x63 LATIN CAPITAL LETTER A WITH DIAERESIS
<U00C0> % /x64 LATIN CAPITAL LETTER A WITH GRAVE
<U00C1> % /x65 LATIN CAPITAL LETTER A WITH ACUTE
<U00C3> % /x66 LATIN CAPITAL LETTER A WITH TILDE
<U00C5> % /x67 LATIN CAPITAL LETTER A WITH RING ABOVE
<U00C7> % /x68 LATIN CAPITAL LETTER C WITH CEDILLA
<U00D1> % /x69 LATIN CAPITAL LETTER N WITH TILDE
<U00A6> % /x6a BROKEN BAR
<U002C> % /x6b COMMA
<U0025> % /x6c PERCENT SIGN
<U005F> % /x6d LOW LINE
<U003E> % /x6e GREATER-THAN SIGN
<U003F> % /x6f QUESTION MARK
<U00F8> % /x70 LATIN SMALL LETTER O WITH STROKE
<U00C9> % /x71 LATIN CAPITAL LETTER E WITH ACUTE
<U00CA> % /x72 LATIN CAPITAL LETTER E WITH CIRCUMFLEX
<U00CB> % /x73 LATIN CAPITAL LETTER E WITH DIAERESIS
<U00C8> % /x74 LATIN CAPITAL LETTER E WITH GRAVE
<U00CD> % /x75 LATIN CAPITAL LETTER I WITH ACUTE
<U00CE> % /x76 LATIN CAPITAL LETTER I WITH CIRCUMFLEX
<U00CF> % /x77 LATIN CAPITAL LETTER I WITH DIAERESIS
<U00CC> % /x78 LATIN CAPITAL LETTER I WITH GRAVE
<U0060> % /x79 GRAVE ACCENT
<U003A> % /x7a COLON
<U0023> % /x7b NUMBER SIGN
<U0040> % /x7c COMMERCIAL AT
<U0027> % /x7d APOSTROPHE
<U003D> % /x7e EQUALS SIGN
<U0022> % /x7f QUOTATION MARK
<U00D8> % /x80 LATIN CAPITAL LETTER O WITH STROKE
<U0061> % /x81 LATIN SMALL LETTER A
<U0062> % /x82 LATIN SMALL LETTER B
<U0063> % /x83 LATIN SMALL LETTER C
<U0064> % /x84 LATIN SMALL LETTER D
<U0065> % /x85 LATIN SMALL LETTER E
<U0066> % /x86 LATIN SMALL LETTER F
<U0067> % /x87 LATIN SMALL LETTER G
<U0068> % /x88 LATIN SMALL LETTER H
<U0069> % /x89 LATIN SMALL LETTER I
<U00AB> % /x8a LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
<U00BB> % /x8b RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
<U00F0> % /x8c LATIN SMALL LETTER ETH (Icelandic)
<U00FD> % /x8d LATIN SMALL LETTER Y WITH ACUTE
<U00FE> % /x8e LATIN SMALL LETTER THORN (Icelandic)
<U00B1> % /x8f PLUS-MINUS SIGN
<U00B0> % /x90 DEGREE SIGN
<U006A> % /x91 LATIN SMALL LETTER J
<U006B> % /x92 LATIN SMALL LETTER K
<U006C> % /x93 LATIN SMALL LETTER L
<U006D> % /x94 LATIN SMALL LETTER M
<U006E> % /x95 LATIN SMALL LETTER N
<U006F> % /x96 LATIN SMALL LETTER O
<U0070> % /x97 LATIN SMALL LETTER P
<U0071> % /x98 LATIN SMALL LETTER Q
<U0072> % /x99 LATIN SMALL LETTER R
<U00AA> % /x9a FEMININE ORDINAL INDICATOR
<U00BA> % /x9b MASCULINE ORDINAL INDICATOR
<U00E6> % /x9c LATIN SMALL LETTER AE
<U00B8> % /x9d CEDILLA
<U00C6> % /x9e LATIN CAPITAL LETTER AE
<U00A4> % /x9f CURRENCY SIGN
<U00B5> % /xa0 MICRO SIGN
<U007E> % /xa1 TILDE
<U0073> % /xa2 LATIN SMALL LETTER S
<U0074> % /xa3 LATIN SMALL LETTER T
<U0075> % /xa4 LATIN SMALL LETTER U
<U0076> % /xa5 LATIN SMALL LETTER V
<U0077> % /xa6 LATIN SMALL LETTER W
<U0078> % /xa7 LATIN SMALL LETTER X
<U0079> % /xa8 LATIN SMALL LETTER Y
<U007A> % /xa9 LATIN SMALL LETTER Z
<U00A1> % /xaa INVERTED EXCLAMATION MARK
<U00BF> % /xab INVERTED QUESTION MARK
<U00D0> % /xac LATIN CAPITAL LETTER ETH (Icelandic)
<U00DD> % /xad LATIN CAPITAL LETTER Y WITH ACUTE
<U00DE> % /xae LATIN CAPITAL LETTER THORN (Icelandic)
<U00AE> % /xaf REGISTERED SIGN
<U00A2> % /xb0 CENT SIGN
<U00A3> % /xb1 POUND SIGN
<U00A5> % /xb2 YEN SIGN
<U00B7> % /xb3 MIDDLE DOT
<U00A9> % /xb4 COPYRIGHT SIGN
<U00A7> % /xb5 SECTION SIGN
<U00B6> % /xb6 PILCROW SIGN
<U00BC> % /xb7 VULGAR FRACTION ONE QUARTER
<U00BD> % /xb8 VULGAR FRACTION ONE HALF
<U00BE> % /xb9 VULGAR FRACTION THREE QUARTERS
<U00AC> % /xba NOT SIGN
<U007C> % /xbb VERTICAL LINE
<U00AF> % /xbc MACRON
<U00A8> % /xbd DIAERESIS
<U00B4> % /xbe ACUTE ACCENT
<U00D7> % /xbf MULTIPLICATION SIGN
<U007B> % /xc0 LEFT CURLY BRACKET
<U0041> % /xc1 LATIN CAPITAL LETTER A
<U0042> % /xc2 LATIN CAPITAL LETTER B
<U0043> % /xc3 LATIN CAPITAL LETTER C
<U0044> % /xc4 LATIN CAPITAL LETTER D
<U0045> % /xc5 LATIN CAPITAL LETTER E
<U0046> % /xc6 LATIN CAPITAL LETTER F
<U0047> % /xc7 LATIN CAPITAL LETTER G
<U0048> % /xc8 LATIN CAPITAL LETTER H
<U0049> % /xc9 LATIN CAPITAL LETTER I
<U00AD> % /xca SOFT HYPHEN
<U00F4> % /xcb LATIN SMALL LETTER O WITH CIRCUMFLEX
<U00F6> % /xcc LATIN SMALL LETTER O WITH DIAERESIS
<U00F2> % /xcd LATIN SMALL LETTER O WITH GRAVE
<U00F3> % /xce LATIN SMALL LETTER O WITH ACUTE
<U00F5> % /xcf LATIN SMALL LETTER O WITH TILDE
<U007D> % /xd0 RIGHT CURLY BRACKET
<U004A> % /xd1 LATIN CAPITAL LETTER J
<U004B> % /xd2 LATIN CAPITAL LETTER K
<U004C> % /xd3 LATIN CAPITAL LETTER L
<U004D> % /xd4 LATIN CAPITAL LETTER M
<U004E> % /xd5 LATIN CAPITAL LETTER N
<U004F> % /xd6 LATIN CAPITAL LETTER O
<U0050> % /xd7 LATIN CAPITAL LETTER P
<U0051> % /xd8 LATIN CAPITAL LETTER Q
<U0052> % /xd9 LATIN CAPITAL LETTER R
<U00B9> % /xda SUPERSCRIPT ONE
<U00FB> % /xdb LATIN SMALL LETTER U WITH CIRCUMFLEX
<U00FC> % /xdc LATIN SMALL LETTER U WITH DIAERESIS
<U00F9> % /xdd LATIN SMALL LETTER U WITH GRAVE
<U00FA> % /xde LATIN SMALL LETTER U WITH ACUTE
<U00FF> % /xdf LATIN SMALL LETTER Y WITH DIAERESIS
<U005C> % /xe0 REVERSE SOLIDUS
<U00F7> % /xe1 DIVISION SIGN
<U0053> % /xe2 LATIN CAPITAL LETTER S
<U0054> % /xe3 LATIN CAPITAL LETTER T
<U0055> % /xe4 LATIN CAPITAL LETTER U
<U0056> % /xe5 LATIN CAPITAL LETTER V
<U0057> % /xe6 LATIN CAPITAL LETTER W
<U0058> % /xe7 LATIN CAPITAL LETTER X
<U0059> % /xe8 LATIN CAPITAL LETTER Y
<U005A> % /xe9 LATIN CAPITAL LETTER Z
<U00B2> % /xea SUPERSCRIPT TWO
<U00D4> % /xeb LATIN CAPITAL LETTER O WITH CIRCUMFLEX
<U00D6> % /xec LATIN CAPITAL LETTER O WITH DIAERESIS
<U00D2> % /xed LATIN CAPITAL LETTER O WITH GRAVE
<U00D3> % /xee LATIN CAPITAL LETTER O WITH ACUTE
<U00D5> % /xef LATIN CAPITAL LETTER O WITH TILDE
<U0030> % /xf0 DIGIT ZERO
<U0031> % /xf1 DIGIT ONE
<U0032> % /xf2 DIGIT TWO
<U0033> % /xf3 DIGIT THREE
<U0034> % /xf4 DIGIT FOUR
<U0035> % /xf5 DIGIT FIVE
<U0036> % /xf6 DIGIT SIX
<U0037> % /xf7 DIGIT SEVEN
<U0038> % /xf8 DIGIT EIGHT
<U0039> % /xf9 DIGIT NINE
<U00B3> % /xfa SUPERSCRIPT THREE
<U00DB> % /xfb LATIN CAPITAL LETTER U WITH CIRCUMFLEX
<U00DC> % /xfc LATIN CAPITAL LETTER U WITH DIAERESIS
<U00D9> % /xfd LATIN CAPITAL LETTER U WITH GRAVE
<U00DA> % /xfe LATIN CAPITAL LETTER U WITH ACUTE
<U009F> % /xff APPLICATION PROGRAM COMMAND (APC)
order_end
END LC_COLLATE
LC_MESSAGES
copy "fr_FR"
END LC_MESSAGES
LC_MONETARY
copy "fr_FR"
END LC_MONETARY
LC_NUMERIC
copy "fr_FR"
END LC_NUMERIC
LC_TIME
copy "fr_FR"
END LC_TIME
LC_PAPER
copy "i18n"
END LC_PAPER
LC_TELEPHONE
copy "fr_FR"
END LC_TELEPHONE
LC_MEASUREMENT
copy "i18n"
END LC_MEASUREMENT
LC_NAME
copy "fr_FR"
END LC_NAME
LC_ADDRESS
copy "fr_FR"
END LC_ADDRESS
Ce fichier contient une collation suivant l’ordre de la variante EBCDIC IBM500, incluant l’équivalent des caractères présents dans la norme ISO 8859-1.
Le sous-ensemble (codes 40
à FF
) de caractères contenant les variations
d’IBM500 à l’EBCDIC de base est représenté dans ce document d’IBM :
https://public.dhe.ibm.com/software/globalization/gcoc/attachments/CP00500.pdf
Démonstration
La démonstration suivante a été réalisée sur une Debian 12 fraîchement installée.
Installation de PostgreSQL :
apt install postgresql
Créer le fichier /usr/share/i18n/locales/fr_ebcdic
que nous a fourni la
DGFIP (télécharger) et générer
la locale fr_ebcdic.utf8
:
localedef -c -i fr_ebcdic -f UTF-8 fr_ebcdic.UTF-8
Malheureusement, il est nécessaire de redémarrer PostgreSQL pour que la nouvelle collation soit détectée par les fonctions2 de la librairie standard C :
systemctl restart postgresql@15-main.service
Il est ensuite possible de créer la nouvelle collation dans PostgreSQL. Deux méthodes existent, voici la méthode manuelle :
=# CREATE COLLATION fr_ebcdic (
PROVIDER = libc,
LC_COLLATE = fr_ebcdic.utf8,
LC_CTYPE = fr_ebcdic.utf8
);
CREATE COLLATION
Voici la méthode automatique où PostgreSQL recherche lui même les nouvelles collations coté système et les importe dans le schéma indiqué :
# SELECT *
FROM pg_import_system_collations('pg_catalog');
pg_import_system_collations
-----------------------------
2
# SELECT collname, collcollate, collctype
FROM pg_collation
WHERE collname LIKE 'fr\_ebcdic%';
collname | collcollate | collctype
----------------+----------------+----------------
fr_ebcdic | fr_ebcdic.utf8 | fr_ebcdic.utf8
fr_ebcdic.utf8 | fr_ebcdic.utf8 | fr_ebcdic.utf8
Nous constatons que cette fonction crée deux collations différentes avec les mêmes propriétés, l’une étant simplement moins longue à nommer que l’autre. Voir la documentation officielle à ce propos :
https://www.postgresql.org/docs/15/collation.html#id-1.6.11.4.5.6.4
Quoi qu’il en soit, nous pouvons désormais tester le tri EBCDIC dans PostgreSQL3 :
# SELECT string_agg(c, ' ' ORDER BY c COLLATE "fr_FR") AS ordre
FROM (
VALUES ('0'), ('9'),
('a'), ('ô'), ('z'), ('A'), ('À'), ('É'), ('Ü'), ('Z'),
('_'), ('°'), ('~'), ('§'), ('}')
)_(c);
ordre
-------------------------------------
_ } ~ § ° 0 9 a A À É ô Ü z Z
# SELECT string_agg(c, ' ' ORDER BY c COLLATE "fr_ebcdic") AS ordre
FROM (
VALUES ('0'), ('9'),
('a'), ('ô'), ('z'), ('A'), ('À'), ('É'), ('Ü'), ('Z'),
('_'), ('°'), ('~'), ('§'), ('}')
)_(c);
ordre
-------------------------------------
À _ É a ° ~ z § A ô } Z 0 9 Ü
Et oui, ce résultat choisi est bien juste ! Vous pouvez vous en convaincre en comparant ce résultat à la table IBM500 présentée dans le chapitre précédent.
Il est bien entendu possible de créer une base avec cette collation par défaut, ou encore de la préciser pour une colonne spécifique dans une table ou une vue, évitant ainsi de devoir modifier vos requêtes comme dans les exemples précédents. Voici un exemple pour une table :
# CREATE TABLE ebcdic (c TEXT COLLATE "fr_ebcdic" );
# INSERT INTO ebcdic
VALUES ('0'), ('9'),
('a'), ('ô'), ('z'), ('A'), ('À'), ('É'), ('Ü'), ('Z ('9'),
('_'), ('°'), ('~'), ('§'), ('}');
INSERT 0 15
# WITH s AS (SELECT c FROM ebcdic ORDER BY c)
SELECT string_agg(s.c, ' ') AS ordre
FROM s;
ordre
-------------------------------------
À _ É a ° ~ z § A ô } Z 0 9 Ü
Bonus Debian
L’exemple précédent est fonctionnel mais a été raccourci afin d’en extraire l’essentiel applicable à presque n’importe quelle distribution Linux.
Cependant, à la prochaine mise à jour système et notamment du paquet locales
,
toute locale créée manuellement sera effacée. Il est nécessaire d’informer
votre système de l’existence de votre locale personnalisée et qu’il l’active
lui même, sur la base du fichier de configuration /etc/locale.gen
de Debian.
Pour ce faire, il faut créer le fichier /usr/local/share/i18n/SUPPORTED
avec
la ligne suivante :
fr_ebcdic.UTF-8 UTF-8
Puis ajouter cette même définition aux locales à générer dans /etc/locale.gen
:
echo fr_ebcdic.UTF-8 UTF-8 >> /etc/locale.gen
Enfin, demander au système de regénérer les locales activées :
# locale-gen
Generating locales (this might take a while)...
fr_ebcdic.UTF-8... done
Generation complete.
Votre locale personnalisée survivra désormais aux prochaines mises à jours système !
Conclusion
Cette question nous a menés à la limite entre le système et PostgreSQL, et fut très instructive pour nos équipes.
Aussi, nous étions heureux de recevoir une confirmation par la pratique du fruit de nos recherches et de notre procédure théorique.
Merci encore aux équipes de la DGFIP de nous avoir permis de publier leur fichier de configuration afin d’illustrer ce cas d’usage peu commun, intéressant et probablement applicable à d’autres contextes hors EBCDIC!
Des questions, des commentaires ? Écrivez-nous !
-
https://pubs.opengroup.org/onlinepubs/9699919799/utilities/localedef.html#tag_20_69 ↩
-
https://www.postgresql.org/message-id/5178.1568063309%40sss.pgh.pa.us ↩
-
La relative complexité des requêtes n’a pour seul but que d’afficher un résultat concis ! ↩