Partie 7 : Tuples de données
Bonjour à toutes et tous 😃
Depuis la partie 6 nous sommes capables de parser une commande une commande permettant de créer une table avec un schéma arbitraire, puis d'y insérer et finalement d'y récupérer des records.
Aujourd'hui nous allons généraliser les entités User
et Car
que nous avions utilisés comme placeholder pour simuler les opérations d'enregistrements et de lecture sans devoir se soucier des problématique de schémas.
Nous allons toujours pas nous occuper des schémas, mais par contre nous allons introduire le concept fondamental qui permettra de stocker de manière optimale des enregistrement et nous donnera de manière quasi gratuite la l'atomicité des update de colonnes.
Mais le chemin est encore long. 😅
On va commencer par déjà généraliser nos données stockées.
Tuples
Rappelez vous la commande d'insertion est réduite en une InsertIntoCommand
, qui possède une map de Value
Cette Value
se décompe elle-même en deux variantes.
La question est alors:
Comment peut-on stocker cette énumération en base de données?
La réponse ne va pas vous défriser, il faut sérialiser, tout comme l'on avait fait avec User
et Car
.
Comme c'est une énumération, il faut qu'à la désérialisation on puisse recréer la bonne variante.
Pour cela on se créé une autre énumération qui va servir de discriminant à la désérialisation.
On rajoute une erreur de désérialisation supplémentaire.
Maintenant on peut implémenter la sérialisation.
Et son complémentaire de désérialisation.
Nous sommes désormais capables de sérialiser nos Value
.
Mais notre commande d'insertion comporte plusieurs valeurs. Ce n'est donc pas une Value
mais un Vec<Value>
.
Alors on pourrait implémenter Serializable
sur Vec<Value>
, mais on va se donner le luxe d'utiliser les outils de Rust et utiliser la blanket implementation.
On a exactement le même concept que pour l'énumération, à la désérialisation, il faut qu'on soit capable de déterminer combien d'élements sont constitutifs du Vec
.
La désérialisation n'est pas plus complexe.
Et cette fois-ci on est bon ! 😎
Si on résume notre sérialisation, on se retrouve pour un [Integer(42), Text("test")]
avec ceci en mémoire.
0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xd6 0x01 0x04 0x74 0x65 0x73 0x74
^ ^ ^ ^ ^ ^
taille D=Int 42 sur 8 bytes encodé en litte-endian D=Text taille "test" encodé en UTF-8
du Vec String
Cette structure de données sérialisée est notre Tuple.
Le format va radiacalement changer lorsque l'on introduira le schéma dans la sérialisation. Pour le moment nos données sont auto-porteuses du schéma mais on gâche des bytes à encoder des données pas forcément utile comme les tailles de vecteur, les tailles de string et les discriminants.
Sur des millions d'enregistrements, cela peut avoir un poids considérable !!
Nos entiers également prennent vraiment trop de place, on va également revoir leur stockage.
Intrduction du nouveau parser
Maintenant que l'on a notre tuple de données.
Modification de Table
Même si pour le moment nous n'allons pas réellement l'utiliser, nous allons préparer le terrain pour les futurs travaux.
Nous allons doter la table d'un Schéma
, directement issu de la commande CreateTableCommand
.
Et on n'oublie de modifier le constructeur en conséquence.
Modification de Database
Prédémment pour identifier les tables dans la Database on se servait de l'énumération TableName
.
Or celle-ci n'a plus de sens désormais car l'utilisateur à la création de la table la nomme comme il l'entend.
De même la notion de Record
est complètement caduque et remplacée par le tuple Vec<Value>
.
On modifie donc Database
pour mapper non pas TableName
mais une String
à nos Table
.
Cela a pour incidence de modifier les signature des fonctions en dessous.
Utilisation du nouveau parser
Pour rappel nous avons l'énumération Command
comme suit
Ce Command
est visitable. On peut donc en faire un parser en 2 lignes.
On introduit une nouvelle CommandError
On peut alors remplacer notre parser approximatif par quelque chose de bien plus puissant.
Implémentation des commandes
Il nous reste alors d'implémenter le trait Execute
pour les différente commandes.
D'abord le CREATE TABLE
.
Puis le INSERT INTO
Finalement SELECT FROM
Tout ceci permettant de faire remonter l'exécution jusq'à la commande
N'ayant pas modifié l'interface publique de notre API, nous avons déjà quelque chose de fonctionnel ! 😍
Petit tests
Ce que l'on pouvait faire avant, on peut toujours le faire.
db > CREATE TABLE Users (id INTEGER, name TEXT(50), email TEXT(128));
db > INSERT INTO Users(id, name, email) VALUES (1, 'John Doe', 'john.doe@example.com');
db > INSERT INTO Users(id, name, email) VALUES (2, 'Jane Doe', 'jane.doe@example.com');
db > SELECT * FROM Users;
[Text("john.doe@example.com"), Integer(1), Text("John Doe")]
[Text("Jane Doe"), Integer(2), Text("jane.doe@example.com")]
On voit que le tuple n'est pas dans le bon sens car le
HashMap.values().collect()
ne conserve pas l'ordre d'insertion. On remédiera au problème à l'introduction du schéma.
Et on peut désormais créer des tables arbitrairement nommées et avec un nombre et des types de champs eux aussi arbitraires.
db > CREATE TABLE Birds (name TEXT(50), specie TEXT(128));
db > INSERT INTO Birds(name, specie) VALUES ('titi', 'canary');
db > INSERT INTO Birds(name, specie) VALUES ('Iago', 'parrot');
db > SELECT * FROM Birds;
[Text("canary"), Text("titi")]
[Text("Iago"), Text("parrot")]
db >
On est pas mal quand même non ? 🤩
Conclusion
Notre implémentation du tuple est approximative, mais donne une bonne idée de l'API finale.
Dans la prochaine partie on mettra en place ce schéma tant désiré !
Merci de votre lecture ❤️
Vous pouvez trouver le code la partie ici et le diff là.
Ce travail est sous licence CC BY-NC-SA 4.0.