https://lafor.ge/feed.xml

Partie 3 : Database

2024-12-05
Les articles de la série

Bonjour à toutes et tous 😀

Dans la précédente partie 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 ❤️

avatar

Auteur: Akanoa

Je découvre, j'apprends, je comprends et j'explique ce que j'ai compris dans ce blog.

Ce travail est sous licence CC BY-NC-SA 4.0.