Partie 3 : Database

Lecture 3 min. ‱


Table des matiĂšres

Les articles de la série

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Ă©:

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 ❀