Partie 3 : Database
Lecture 3 min. âą
Table des matiĂšres
Les articles de la série
- Partie 1 : Construire le REPL
- Partie 2 : Sérialisation des données
- Partie 3 : Database
- Partie 4 : Tables
- Partie 5 : Parser les commandes, création de la boßte à outils
- Partie 6 : Parser les commandes SQL
- Partie 7 : Tuples de données
- Partie 8 : Contraindre les données via un schéma
- Partie 9 : Découper le stockage des tables en pages
- Partie 10 : Clefs primaires
- Partie 11 : Expressions Logiques SQL
- Partie 12 : Scans et filtrage
- Partie 13 : UTF-8 et caractÚres échappés
- Partie 14 : Attributs nullifiables
- Partie 15 : Index secondaires
- Partie 16 : Directive EXPLAIN
- Partie 17 : Logical Plan
- Partie 18 : Exécution du plan logique
- Partie 19 : Suppression d'enregistrements
- Partie 20 : Réindexation et compactation de tables
Bonjour Ă toutes et tous đ
Dans la [précédente partie](/rustqlite-2 nous avons été capable de sérialiser une structure User.
Dans cette partie, nous allons voir la crĂ©ation dâune API de plus haut-niveau pour les manipuler.
Database
Sérialiser plusieurs User
Donc on arrive au point oĂč lâon est capable de sĂ©rialiser un User et de le rĂ©cupĂ©rer aprĂšs coup.
Mais est ce que lâon peut en sĂ©rialiser 2 ou plus ?
Et bien essayons !
#[test]
fn test_serde_users() {
let mut buffer = [0_u8; 1024];
let user = User {
id: 42,
username: "user".to_string(),
email: "email".to_string(),
};
let devil = User {
id: 666,
username: "Lucifer".to_string(),
email: "MorningStar".to_string(),
};
let mut writer = Cursor::new(&mut buffer[..]);
user.serialize(&mut writer)
.expect("Unable to serialize user");
devil
.serialize2(&mut writer)
.expect("Unable to serialize user");
let mut reader = Cursor::new(&buffer[..]);
let result = User::deserialize(&mut reader).expect("Unable to deserialize user");
assert_eq!(user, result);
let result = User::deserialize(&mut reader).expect("Unable to deserialize user");
assert_eq!(devil, result);
}Apparemment oui !
Et maintenant, si on veut âscannerâ notre DB, on fait comment?
Scanner, signifie passer en revue chaque enregistrement et à les désérialiser successivement.
#[test]
fn test_scan_db() {
let mut buffer = [0_u8; 1024 * 1024];
let mut cursor = Cursor::new(&mut buffer[..]);
// enregistrement
for i in 0..50 {
let user = User::new(i, format!("test_{i}"), format!("email_{i}@example.com"));
user.serialize(&mut cursor)
.expect("Unable to serialize user");
}
// scan
let mut reader = Cursor::new(&buffer[..]);
for i in 0..50 {
let user = User::new(i, format!("test_{i}"), format!("email_{i}@example.com"));
let result = User::deserialize(&mut reader).expect("Unable to deserialize user");
assert_eq!(user, result);
}
}Tout fonctionne du feu de Dieu!
Insertion et sélection
Devenons un peu plus ârĂ©alisteâ, au lieu dâutiliser le mĂȘme curseur pour sĂ©rialiser tout le monde, nous allon conserver lâoffset et dĂ©caler notre buffer dâautant entre chaque enregistrement.
On capture aussi le nombre dâĂ©lements insĂ©rĂ©. Cela nous permet de savoir quand arrĂȘter de scanner.
#[test]
fn test_insert_select() {
let mut buffer = [0_u8; 1024 * 1024];
// offset d'écriture
let mut offset = 0_usize;
// nombre d'enregistrements
let mut nb_inserts = 0;
// insertion des User
for i in 0..50 {
// chaque insert possÚde son propre curseur d'écriture
let mut cursor = Cursor::new(&mut buffer[offset..]);
let user = User::new(i, format!("test_{i}"), format!("email_{i}@example.com"));
user.serialize2(&mut cursor)
.expect("Unable to serialize user");
// on se décale d'autant que la donnée écrite
offset += cursor.position() as usize;
nb_inserts += 1;
}
// scan des User
// on créé un reader unique pour le scan
let mut reader = Cursor::new(&buffer[..]);
for i in 0..nb_inserts {
let user = User::new(i, format!("test_{i}"), format!("email_{i}@example.com"));
let result = User::deserialize2(&mut reader).expect("Unable to deserialize user");
assert_eq!(user, result);
}
}API publique
Bon on est proche du but. Plus quâĂ envelopper tout cela dans un papier cadeau.
const DATABASE_SIZE : usize = 1024*1024;
pub struct Database {
// on passe en alloué car la taille a tendance à exploser la stack => stackoverflow
inner: Vec<u8>,
offset: usize,
row_number: usize,
}
impl Database {
pub fn new() -> Self {
Self {
inner: vec![0; DATABASE_SIZE],
offset: 0,
row_number: 0,
}
}
}Jâai renommĂ© le ânb_insertsâ en ârow_numberâ car câest sĂ©mantiquement plus proche de ce que ça reprĂ©sente rĂ©ellement. Tout est un peu artificiel pour le moment.
Plus on progressera vers la réelle implémentation plus on enlÚvera ces placeholders.
On se rajoute des erreurs plus sémantique
#[derive(Debug, PartialEq)]
pub enum InsertionError {
Serialization(SerializationError),
}
impl Display for InsertionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl Error for InsertionError {}
#[derive(Debug, PartialEq)]
pub enum SelectError {
Deserialization(DeserializationError),
}
impl Display for SelectError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl Error for SelectError {}Et finalement on se créé deux mĂ©thodes qui commencent Ă donner un semblant dâutilisabilitĂ©:
- insert : insert un User dans la DB
- select : liste tous les User de la DB
impl Database {
pub fn insert<S: Serializable>(&mut self, row: S) -> Result<(), InsertionError> {
let mut writer = Cursor::new(&mut self.inner[self.offset..]);
row.serialize(&mut writer)
.map_err(InsertionError::Serialization)?;
self.offset += writer.position() as usize;
self.row_number += 1;
Ok(())
}
pub fn select<D: Deserializable>(&mut self) -> Result<Vec<D>, SelectError> {
let mut reader = Cursor::new(&self.inner[..]);
let mut rows = Vec::with_capacity(self.row_number);
for _row_number in 0..self.row_number {
rows.push(D::deserialize(&mut reader).map_err(SelectError::Deserialization)?)
}
Ok(rows)
}
}On teste parce quâon ne sait jamaisâŠ
#[test]
fn test_database() {
let mut database = Database::new();
for i in 0..50 {
let user = User::new(i, format!("test_{i}"), format!("email_{i}@example.com"));
database.insert(user).expect("insert user failed");
}
let rows = database.select::<User>().expect("select failed");
assert_eq!(rows.len(), 50);
for (i, row) in rows.iter().enumerate() {
let expected = &User::new(
i as i64,
format!("test_{i}"),
format!("email_{i}@example.com"),
);
assert_eq!(row, expected);
}
}Et oui on récupÚre bien nos Users ^^
Intégration dans la CLI
Cela va ĂȘtre trĂšs rapide. đ
Dans la mĂ©thode ârunâ on créé la Database.
pub fn run() -> Result<(), Box<dyn Error>> {
let mut database = database::Database::new();
// ...
}On modifie le trait Execute pour prendre en paramÚtre la reférence mutable vers la Database.
pub trait Execute {
fn execute(self, database: &mut Database) -> Result<(), ExecutionError>;
}On créé deux variantes dâerreurs.
#[derive(Debug, PartialEq)]
pub enum ExecutionError {
Insertion(InsertionError),
Select(SelectError),
}Et on modifie les implémentations associées
Dans MetaCommand on ne lâutilise pas donc câest rapide.
Par contre dans SqlCommand, on a un peu plus de boulot.
impl Execute for SqlCommand {
fn execute(self, database: &mut Database) -> Result<(), ExecutionError> {
match self {
SqlCommand::Insert {
id,
username,
email,
} => {
let user = User::new(id, username, email);
database.insert(user).map_err(ExecutionError::Insertion)?;
println!("User inserted successfully");
}
SqlCommand::Select => {
for user in database.select::<User>().map_err(ExecutionError::Select)? {
println!("{:?}", user);
}
}
}
Ok(())
}
}Mais comme vous le voyez, en ayant diviser les couches dâabstractions de maniĂšre trĂšs contrĂŽlĂ©e et cohĂ©rente, notre interface public devient extrĂȘmement agrĂ©able Ă utiliser. đ
Finalement on modifie Command pour propager la Database.
impl Execute for Command<'_> {
fn execute(self, database: &mut Database) -> Result<(), ExecutionError> {
match self {
Command::Meta(command) => command.execute(database),
Command::Sql(command) => command.execute(database),
Command::Unknown { command } => {
println!("Command not found: {}", command);
Ok(())
}
}
}
}Et on modifie la mĂ©thode run pour fournir la Database Ă lâexĂ©cution de la commande.
pub fn run() -> Result<(), Box<dyn Error>> {
let mut database = database::Database::new();
loop {
print!("db > ");
std::io::stdout().flush()?;
let mut command = String::new();
std::io::stdin().read_line(&mut command)?;
let command = command.trim();
match parse(command) {
Ok(command) => {
// ici
command.execute(&mut database)?;
}
Err(err) => println!("Error {err}"),
}
}
}Et on peut déjà tester !!
db > insert 1 user1 email1@example.com
User inserted successfully
db > insert 2 user2 email2@example.com
User inserted successfully
db > select
User { id: 1, username: "user1", email: "email1@example.com" }
User { id: 2, username: "user2", email: "email2@example.com" }
db > Câest basique, mais ça valide Ă©normĂ©ment de choses sur la conception. ^^
Conclusion
Dans cette partie nous avons créé le socle fondamental de notre base de données.
Dans la prochaine partie nous verront comment stocker autre chose que des User.
Vous pouvez trouver le code la partie ici et le diff lĂ .
Merci de votre lecture â€ïž
