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.

portrait de Jehan-Guillaume

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:

  1. POSIX ou C
  2. l’utilisation de la librairie ICU
  3. 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 !


DALIBO

DALIBO est le spécialiste français de PostgreSQL®. Nous proposons du support, de la formation et du conseil depuis 2005.