Créer une extension Postgres en Rust, Episode 3 : Le chant du SIREN
Préambule
Cet article fait partie d'une série consacrée au framework PGRX, un environnement de développement qui facilite la conception d'extensions PostgreSQL avec le langage Rust.
Retrouvez les autres articles de la série ci-dessous:
- Episode 1 : Un nouveau monde
- Episode 2 : Demander le luhn
- Episode 3 : Le chant du SIREN
- Episode 4 : Accrocher le parapet
Important: Cette série d'articles n'est pas une introduction au langage Rust ! Il existe d'autres ressources pour ça (notamment ici et là). On se focalisera ici sur la mécanique interne d'une extension Postgres.
Chaque épisode est conçu en 2 parties : un tutoriel de 30 minutes environ, suivi d'exercices pour aller plus loin. Les exercices sont optionnels.
Objectifs
Dans cet épisode, nous allons:
- Créer un nouveau type de données pour Postgres
- Définir des opérateurs pour ce type
- Ecrire des tests contenant des requêtes SQL
Contexte
L'exemple proposé dans l'épisode précédent (cf. [Episode 2: Demander le Luhn]) a permis de voir comment utiliser une librairie Rust externe dans Postgres.
Nous entrons maintenant dans un usage plus spécifique: La création d'un nouveau type de données.
Cette fois, l'exemple va consiste à implémenter un type pour stocket
les numéros SIREN, c'est à dire les identifiants des entreprises françaises.
Ce numéro est souvent stocker comme un champs TEXT ayant le format 732 829 320
le dernier chiffre étant une somme de controle de la formule de Luhn.
L'idée ici est stocker la valeur sur disque comme un entier mais de manipuler comme une chaine de caractère
Démarrer
Si vous n'avez pas encore installer PGRX, reportez-vous à la section "Démarrage" de l'article "Episode 1 : Un nouveau monde".
ou sinon utilisez l'image docker:
docker run -it --volume `pwd`:/pgrx daamien/pgrx
Créer l'extension
On peut maintenant créer une extension nommée pg_siren
avec:
cargo pgrx new pg_siren
cd pg_siren
Ajouter une dépendance externe
Pour cette extension, on aura besoin des librairies luhn3 et serde.
cargo add luhn3 serde
Créer le type
Le format habituel d'un numéro SIREN est 483 247 862
. Il est
souvent stocké dans les modèles de données dans une colonne
de type TEXT.
Néanmoins les espaces ne sont que du formattage et le dernier chiffre
est la somme de controle de luhn
. On peut donc stocker la valeur
comme un entier (48324786
) et appliquer le formattage et la
somme de controle au moment de l'affichage de la valeur.
Pour cela, on va stocker la valeur sous la forme d'un INTEGER,
ce qui correspond au type i32
en Rust.
On crée donc une structure Siren
comme ceci:
use pgrx::{InOutFuncs};
use serde::{Serialize, Deserialize};
pgrx::pg_module_magic!();
#[derive(PostgresType, Serialize, Deserialize, Debug)]
#[inoutfuncs]
pub struct Siren (i32);
Créer les fonctions d'entrée/sortie
On va maintenant créer les fonctions d'entrée et de sortie pour ce type
impl InOutFuncs for Siren {
impl InOutFuncs for Siren {
// Parse the provided CStr into a Siren
// - Remove all spaces
// - Check the luhn checksum
// - Remove the luhn digit (division by 10)
fn input(input: & core::ffi:CStr) -> Self {
let val = input.to_str().expect("Invalid Input").replace(' ', "");
if !luhn3::decimal::valid(&val.clone().into_bytes()) {
error!("{}", "Not a valid SIREN number");
}
Siren(val.parse::<i32>().expect("Value should be a number") / 10)
}
// Output ourselves as text into the provided `StringInfo` buffer
// - Split the Integer value (self.0) in 3 parts
// - Add the luhn cheksum at the end
fn output(&self, buffer: &mut pgrx::StringInfo) {
let part1 = self.0 / 100000 % 1000;
let part2 = self.0 / 100 % 1000;
let part3 = self.0 % 100;
let part4 = luhn3::decimal::checksum(&self.0.to_string().into_bytes())
.expect("Checksum Failed") as char;
let val = format!("{part1:03} {part2:03} {part3:02}{part4}");
buffer.push_str(val.as_str());
}
}
La fonction d'entrée prend une valeur TEXT (Cstr), elle supprime les espaces, vérifie la somme de controle puis retire le dernier chiffre (division par 10).
La fonction de sortie "découpe" la valeur numérique stockée (self.0
)
en 3 partie et ajoute la somme de controle à la fin.
Lancer l'extension
On peut ensuite directement tester l'extension
cargo pgrx run
Attention! Cette commande va compiler une centaine de paquets Rust (
crates
) et peut durer une dizaine de minutes !
CREATE EXTENSION pg_siren;
SELECT '483247862'::SIREN;
siren
-------------
483 247 862
SELECT '999 999 999'::SIREN;
ERROR: Not a valid SIREN number
LINE 1: SELECT '999 999 999'::SIREN;
Ecrire un opérateur
Essayons maintenant de créer une table avec colone de type SIREN
.
CREATE TABLE company (id SIREN PRIMARY KEY, name TEXT);
ERROR: data type siren has no default operator class for access method "btree"
HINT: You must specify an operator class for the index or define a default
operator class for the data type.
C'est un échec, car le type SIREN n'a pas d'opérateurs de comparaison ce qui est nécessaire pour indexer les données.
PGRX vient nous simplifier la vie avec plusieurs macros qui vont créer automatiquement ces opérateurs. Pour cela, on ajoute 3 "dérivées" supplémentaires juste avant la déclaration du type SIREN.
#[derive(PostgresType, Serialize, Deserialize, Debug, PartialEq)]
#[derive(Eq, PartialEq, Ord, Hash, PartialOrd)]
#[derive(PostgresEq)]
#[derive(PostgresOrd)]
#[inoutfuncs]
pub struct Siren(i32);
- derive(Eq, PartialEq, ...) génère les opérateurs Rust
- derive(PostgresEq) génère l'opérateur Postgres d'égalité
- derive(PostgresOrd) génère les opérateurs Postgres de comparaison (<, >, etc. )
On recompile ensuite l'extension
cargo pgrx run
et cette fois la création de la table fonctionne
DROP EXTENSION pg_siren;
CREATE EXTENSION pg_siren;
SELECT '483247862'::SIREN > '483247870'::SIREN ;
?column?
----------
f
(1 row)
CREATE TABLE company (id SIREN PRIMARY KEY, name TEXT);
INSERT INTO company VALUES ('483247862','dalibo');
SELECT * FROM company;
id | name
-------------+--------
483 247 862 | dalibo
Ecrire des tests SQL
Cette fois pour tester, on ne peut se contenter d'appeler les fonctions Rust
directement. On veut valider le type SQL fonctionne. Pour cela, on utilise
le module Spi
de PGRX.
#[cfg(any(test, feature = "pg_test"))]
#[pg_schema]
mod tests {
use pgrx::prelude::*;
#[pg_test]
unsafe fn test_input() {
Spi::run("SELECT '483247862'::SIREN").unwrap();
}
#[pg_test]
unsafe fn test_create_table() {
Spi::run("CREATE TABLE company (id SIREN PRIMARY KEY)").unwrap();
}
#[pg_test]
#[should_panic]
unsafe fn test_wrong_input() {
Spi::run("SELECT '999 999 999'::SIREN").unwrap();
}
}
Pour aller plus loin...
Voici quelques propositions d'activités pour poursuivre la découverte:
-
Ecrire une fonction
random_siren()
qui retourne une valeur aléatoire de type SIREN. On pourra pour cela utiliser le générateur aléatoire rand. Ecrire un test associé. -
Le numéro SIRET est composé d'un numéro SIREN et d'un numéro d'établissement sur 5 chiffres. Créer un type composite et les fonctions entrée/sortie associées. Ecrire les tests associés.
Un exemple d'implémentation est disponible sur:
https://gitlab.com/daamien/pgrx-notebook/-/blob/main/pg_siren/src/lib.rs
Bilan
On voit que PGRX ne se contente pas de convertir à la volée les types Postgres
en types rust (i.e INTEGER transformés en i32
). Il permet également de créer
ses propres types et de controler comment ce type sera stocké sur disque (input)
et comment il sera affiché (output).
La suite au prochain épisode !
Dans le prochain épisode, nous allons jouer avec des crochets :)