Partie 10 : Clefs primaires

Lecture 57 min. ‱

Table des matiĂšres

Les articles de la série

Bonjour à toutes et tous 😃

Pour le moment notre seule façon de rĂ©cupĂ©rer de la donnĂ©e depuis notre table consiste Ă  rĂ©aliser un full-scan, ce qui est veut dire partir du premier byte de la table et dĂ©sĂ©sĂ©rialiser jusqu’à la fin.

Autant dire que ce n’est pas vraiment optimale si on cherche un tuple en milieu de table. 😅

Tout comme Ă  l’époque du tĂ©lĂ©phone Ă  fil, il y avait un gros bouquin qui rĂ©fĂ©rençait le nom de la personne par rapport Ă  son numĂ©ro. Nous allons faire de mĂȘme avec nos tuples.

La question maintenant est de savoir dans notre cas qui est le “nom” et qu’est ce que le “numĂ©ro” de notre “annuaire” Ă  nous.

Il va nous falloir plus d’outils pour rĂ©pondre Ă  ces questions.

Modifiation du parser

La premiĂšre chose que l’on va rajouter c’est des nouvelles capacitĂ© Ă  notre parser.

Nouvelle grammaire

Pour pouvoir dĂ©finir qui sera le “nom” de notre annuaire, nous allons dĂ©finir un flag sur un des champs du schĂ©ma de la table Ă  la crĂ©ation de celle-ci.

Ce flag va se nommer PRIMARY KEY, un champ disposant de cette contrainte se nomme la “clef primaire” de la table, autrement dit la maniĂšre la plus simple de retrouver de la donnĂ©es dans une table.

Voici la nouvelle grammaire du parser pour la commande CREATE TABLE.

CREATE TABLE avec PRIMARY KEY
CreateTable ::= 'CREATE' 'TABLE' LiteralString OpenParen ColumnDef* CloseParen Semicolon
ColumnDef ::= LiteralString ColumnType | LiteralString ColumnType Comma
ColumnType ::= 'INTEGER' Constraint* | ColumTypeText Constraint*
ColumTypeText ::= 'TEXT' OpenParen LiteralInteger CloseParen
LiteralString ::=  [A-Za-z0-9_]*
LiteralInteger ::= [0-9]+
Constraint ::= 'PRIMARY' 'KEY' | 'NOT' 'NULL'
OpenParen ::= '('
CloseParen ::= ')'
Comma ::= ','
Semicolon ::= ';'

CreateTable:


ColumnDef:


ColumnType:


ColumTypeText:


LiteralString:


LiteralInteger:


Constraint:


Les colonnes disposent dĂ©sormais d’un nouveau composant Constraint qui va ĂȘtre soit l’enchaĂźnement des tokens PRIMARY KEY soit NOT NULL.

Il est possible de d’avoir Ă  la fois PRIMARY KEY et NOT NULL sur une mĂȘme colonne.

La deuxiĂšme modification va ĂȘtre sur la commande SELECT.

Pour le moment, on rĂ©alise uniquement ce que l’on nomme des “full-scan”, c’est Ă  dire que l’on part de l’index 0 de nos tuples, dĂ©marrant Ă  la page 0 de notre table, et on itĂšre jusqu’à ne plus avoir d’entrĂ©e Ă  retourner.

Cela est du Ă  la pauvretĂ© de notre commande SELECT qui ne permet pas de nommer ce que l’on dĂ©sire. Il nous manque littĂ©ralement des “mots” pour nous exprimer! 😅

Nous allons les rajouter avec cette nouvelle grammaire:

SELECT FROM avec clause WHERE
Select ::= 'SELECT' Column* 'FROM' LiteralString WhereClause Semicolon
Column ::= '*' | LiteralString | LiteralString Comma
WhereCaluse ::= 'WHERE' Identifier '=' LiteralValue
LiteralString ::=  "'" [A-Za-z0-9_]* "'"
Identifier ::=  [A-Za-z0-9_]*
LiteralInteger ::= [0-9]+
LiteralValue ::= LiteralInteger | LiteralString
OpenParen ::= '('
CloseParen ::= ')'
Comma ::= ','
Semicolon ::= ';'

Select:


Column:


WhereCaluse:


LiteralString:


Identifier:


LiteralInteger:


LiteralValue:


Elle introduit le nouveau mot-clef WHERE qui permet d’écrire une expression du type champ = value, ce qui signifie : “renvoie moi le champ qui possĂšde cette valeur”.

Ajouts des tokens

Nous allons rajouter 6 tokens supplémentaires:

enum Token {
    // ...
    /// PRIMARY token
    Primary,
    /// KEY token
    Key,
    /// NOT token
    Not,
    /// NULL token
    Null,
    /// = token
    Equal,
    /// WHERE token
    Where,
    /// Technical token never matched
    Technic
    // ...
}

J’en profite pour dĂ©finir un token technique qui permet d’utiliser l’API Token sans devoir dĂ©finir quelque chose de prĂ©cis Ă  reconnaĂźtre.

On rajoute les matchers qui vont bien:

impl Match for Token {
    fn matcher(&self) -> Matcher {
        match self {
            // ...
            Token::Primary => Box::new(matchers::match_pattern("primary")),
            Token::Key => Box::new(matchers::match_pattern("key")),
            Token::Not => Box::new(matchers::match_pattern("not")),
            Token::Null => Box::new(matchers::match_pattern("null")),
            Token::Equal => Box::new(matchers::match_char('=')),
            Token::Where => Box::new(matchers::match_pattern("where")),
            Token::Technic => Box::new(matchers::match_predicate(|_| (false, 0))),
            // ...
        }
    }
}

En n’oubliant pas de rajouter les implĂ©mentation de Size pour ces nouveaux tokens

impl Size for Token {
    fn size(&self) -> usize {
        match self {
            // ...
            Token::Primary => 7,
            Token::Key => 3,
            Token::Not => 3,
            Token::Null => 4,
            Token::Where => 5,
            // ...
        }
    }
}

Forecaster UntilEnd

Son travail est simple, reconnaĂźtre la fin de la slice.

pub struct UntilEnd;

impl<'a> Forecast<'a, u8, Token> for UntilEnd {
    fn forecast(&self, data: &mut Scanner<'a, u8>) -> crate::parser::Result<ForecastResult<Token>> {
        Ok(ForecastResult::Found {
            end_slice: data.remaining().len(),
            start: Token::Technic,
            end: Token::Technic,
        })
    }
}

On consomme jusqu’à la fin, son existence peut sembler absurde, mais il va permettre de construire des API d’alternatives de reconnaissances de pattern bien plus naturellement.

Définition des contraintes

Pour parser nos contraintes PRIMARY KEY et NOT NUL, il faut pouvoir les reconnaĂźtre.

Pour se faire on rajoute deux visiteurs:

PrimaryKeyConstraint qui reconnaĂźt l’enchaĂźnement de tokens PRIMARY suivi d’un nombre supĂ©rieur Ă  1 d’espace puis le token KEY.

struct PrimaryKeyConstraint;

impl<'a> Visitable<'a, u8> for PrimaryKeyConstraint {
    fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
        recognize(Token::Primary, scanner)?;
        scanner.visit::<Whitespaces>()?;
        recognize(Token::Key, scanner)?;
        Ok(PrimaryKeyConstraint)
    }
}

On fait de mĂȘme avec le NotNullConstraint

struct NotNullConstraint;

impl<'a> Visitable<'a, u8> for NotNullConstraint {
    fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
        scanner.visit::<OptionalWhitespaces>()?;
        recognize(Token::Not, scanner)?;
        scanner.visit::<Whitespaces>()?;
        recognize(Token::Null, scanner)?;
        Ok(NotNullConstraint)
    }
}

On fusionne les visiteurs dans une énumération

enum ConstraintResult {
    PrimaryKey(PrimaryKeyConstraint),
    NotNull(NotNullConstraint),
}

Et son pendant Constraint associé à sa glu technique.

enum Constraint {
    PrimaryKey,
    NotNull,
}

impl From<ConstraintResult> for Constraint {
    fn from(value: ConstraintResult) -> Self {
        match value {
            ConstraintResult::PrimaryKey(_constraint) => Constraint::PrimaryKey,
            ConstraintResult::NotNull(_constraint) => Constraint::NotNull,
        }
    }
}

On peut alors en faire un Visteur.

impl<'a> Visitable<'a, u8> for Constraint {
    fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
        Acceptor::new(scanner)
            .try_or(|constraint| Ok(ConstraintResult::PrimaryKey(constraint)))?
            .try_or(|constraint| Ok(ConstraintResult::NotNull(constraint)))?
            .finish()
            .ok_or(ParseError::UnexpectedToken)
            .map(Into::into)
    }
}

Ajout des contraintes sur la définition de la colonne

On rajoute la possibilitĂ© d’avoir des contraintes sur une colonne.

struct ColumnDefinition {
    name: String,
    field: ColumnType,
    // ici 👇
    constraints: Vec<Constraint>,
}

Un groupe de contraintes est séparé par un espace blanc.

Lorsque l’on dĂ©fini des containtes, on peut se retrouver dans deux cas:

impl<'a> Visitable<'a, u8> for ColumnDefinition {
    fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {

        // On reconnaĂźt un groupe qui se termine
        let maybe_constraints = Forcaster::new(scanner)
            // soit par une virgule
            .try_or(UntilToken(Token::Comma))?
            // soit c'est la fin de la slice
            .try_or(UntilEnd)?
            .finish();

        let mut constraints = Vec::new();
        // si un groupe est trouvé
        if let Some(Forecasting { data, .. }) = maybe_constraints {
            let mut constrait_tokenizer = Tokenizer::new(data);
            // si le groupe contient au moins un byte
            if !constrait_tokenizer.remaining().is_empty() {
                // on en fait une liste de contraintes séparées par au moins un blanc
                let constraints_group =
                    constrait_tokenizer.visit::<SeparatedList<Constraint, Whitespaces>>()?;
                constraints = constraints_group.into_iter().collect();
                // on avance le scanner
                scanner.bump_by(data.len());
            }
        }

        // on retourne la colonne avec ses contraintes
        Ok(ColumnDefinition {
            name,
            field,
            constraints,
        })

    }
}

Modification du visiteur Schema

impl<'a> Schema {
    fn parse(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
        // on visite la liste de colonnes
        let columns_definitions =
            fields_group_tokenizer.visit::<SeparatedList<ColumnDefinition, SeparatorComma>>()?;

        // que l'on transforme en des définitions de colonnes
        let fields =
            columns_definitions
                .into_iter()
                .fold(Vec::new(), |mut fields, column_definition| {
                    fields.push((
                        column_definition.name,
                        crate::data::ColumnDefinition::new(
                            column_definition.field,
                            // 👇 avec les contraintes associĂ©es 
                            column_definition.constraints,
                        ),
                    ));
                    fields
                });
    }
}

Ce qui donne:

#[test]
fn test_constraints() {
    let data = b"(id INTEGER NOT NULL PRIMARY KEY, name_1 TEXT(50) NOT NULL)";
    let mut tokenizer = Tokenizer::new(data);
    let schema = tokenizer.visit::<Schema>().expect("failed to parse schema");
    assert_eq!(
        schema,
        Schema::new(
            vec![
                (
                    "id".to_string(),
                    ColumnDefinition::new(
                        ColumnType::Integer,
                        vec![Constraint::NotNull, Constraint::PrimaryKey,]
                    )
                ),
                (
                    "name_1".to_string(),
                    ColumnDefinition::new(ColumnType::Text(50), vec![Constraint::NotNull])
                )
            ],
        )
    )
}

Nous sommes dĂ©sormais capables de parser les contraintes de la tables ! đŸ€©

Clef primaire du schéma

Une des contraintes qui le plus important pour nous Ă  l’instant prĂ©sent est PRIMARY KEY, celle ci dispose plusieurs rĂšgle de dĂ©finition.

Nous allons matérialiser cela à la fois via des erreurs

enum ParseError {
    // ...
    PrimaryKeyNotExists,
    NonUniquePrimaryKey(Vec<Vec<String>>),
}

et une méthode qui a pour objet de trouver la contrainte unique de clef primaire et renvoyer dans les autres cas.

Note

La primary key est un Vec<String> car il est possible d’associer plus d’un champ pour construire une clef primaire.

On nomme ça des clefs composites, si vous voulez prendre de l’avance sur la suite. 😇

fn parse_primary_key(
    fields: &Vec<(String, crate::data::ColumnDefinition)>,
) -> crate::parser::Result<Vec<String>> {
    let mut primary_keys = Vec::new();
    // on boucle sur les champs du schéma
    for (field_name, column_definition) in fields {
        // si au moins une des contraintes de la colonne est PRIMARY KEY
        if !column_definition
            .constraints
            .iter()
            .filter(|constraint| constraint == &&Constraint::PrimaryKey)
            .collect::<Vec<_>>()
            .is_empty()
        {
            // alors on rajoute le nom de la colonne
            primary_keys.push(vec![field_name.clone()]);
        }
    }

    // si aucune PRIMARY KEY n'est trouvable, le schéma est invalide
    if primary_keys.is_empty() {
        return Err(ParseError::PrimaryKeyNotExists);
    }
    // si plus d'un champ est une PRIMARY KEY, le schéma est également invalide
    if primary_keys.len() > 1 {
        return Err(ParseError::NonUniquePrimaryKey(primary_keys));
    }

    Ok(primary_keys.remove(0))
}

On défini un nouveau champ primary_key sur notre Schema.

struct Schema {
    pub fields: HashMap<String, ColumnDefinition>,
    pub columns: Vec<String>,
    // ici 👇
    pub primary_key: PrimaryKey,
}

Lors du parse on va alors appeler notre méthode parse_primary_key depuis notre visiteur de Schéma.

impl<'a> Schema {
    fn parse(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
        // ...

        // on récupÚre la primary key
        let primary_key = parse_primary_key(&fields)?;

        Ok(Schema::new(fields, primary_key))
    }
}

Ce qui permet de parser nos schémas

#[test]
fn test_constraints() {
    let data = b"(id INTEGER NOT NULL PRIMARY KEY, name_1 TEXT(50) NOT NULL)";
    let mut tokenizer = Tokenizer::new(data);
    let schema = tokenizer.visit::<Schema>().expect("failed to parse schema");
    assert_eq!(
        schema,
        Schema::new(
            vec![
                (
                    "id".to_string(),
                    ColumnDefinition::new(
                        ColumnType::Integer,
                        vec![Constraint::NotNull, Constraint::PrimaryKey,]
                    )
                ),
                (
                    "name_1".to_string(),
                    ColumnDefinition::new(ColumnType::Text(50), vec![Constraint::NotNull])
                )
            ],
            vec!["id".to_string()]
        )
    )
}

#[test]
fn test_constraints_non_existent_primary_key() {
    let data = b"(id INTEGER NOT NULL, name_1 TEXT(50) NOT NULL)";
    let mut tokenizer = Tokenizer::new(data);
    let schema = tokenizer.visit::<Schema>();
    assert_eq!(schema, Err(crate::parser::ParseError::PrimaryKeyNotExists))
}

#[test]
fn test_constraints_multiple_primary_key() {
    let data = b"(id INTEGER NOT NULL PRIMARY KEY, name_1 TEXT(50) NOT NULL PRIMARY KEY)";
    let mut tokenizer = Tokenizer::new(data);
    let schema = tokenizer.visit::<Schema>();
    assert_eq!(
        schema,
        Err(crate::parser::ParseError::NonUniquePrimaryKey(vec![
            vec!["id".to_string()],
            vec!["name_1".to_string()]
        ]))
    )
}

Nous avons dĂ©sormais la certitude d’avoir une clef primaire unique dans la dĂ©finition du schĂ©ma de notre table.

On avance ! đŸ€©

Clause Where

Notre clause est extrĂȘmement simplifiĂ© par rapport Ă  la rĂ©alitĂ© de SQL (chaque chose en son temps 😅).

On associe un champ et une valeur séparé par un token =.

Notre association se matérialise par la structure

struct WhereClause {
    field: String,
    value: Value,
}

Que l’on visite

impl<'a> Visitable<'a, u8> for WhereClause {
    fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
        scanner.visit::<OptionalWhitespaces>()?;
        // doit commencer par WHERE
        recognize(Token::Where, scanner)?;
        // suivi d'un espace
        scanner.visit::<Whitespaces>()?;
        // suivi d'un nom de colonne
        let column = scanner.visit::<Column>()?;
        // on reconnaĂźt le = 
        recognize(Token::Equal, scanner)?;
        // suivi d'un espace
        scanner.visit::<Whitespaces>()?;
        // suivi d'une valeur
        let value = scanner.visit::<Value>()?;
        Ok(Self {
            field: column.0,
            value,
        })
    }
}

Ce qui donne

#[test]
fn test_where_clause() {
    let mut scanner = Scanner::new(b"WHERE id = 42");
    let where_clause = scanner.visit::<WhereClause>().unwrap();
    assert_eq!(where_clause.field, "id".to_string());
    assert_eq!(where_clause.value, Value::Integer(42));
}

Modification de la commande SELECT

On rajoute alors notre clause where qui peut optionnellement exister

struct SelectCommand {
    pub table_name: String,
    pub projection: Projection,
    // ici 👇
    pub where_clause: Option<WhereClause>,
}

Que l’on peut alors visiter

impl<'a> Visitable<'a, u8> for SelectCommand {
    fn accept(scanner: &mut Tokenizer<'a>) -> parser::Result<Self> {
        // ...

        
        // le modificateur d'optionalité permet de ne pas contraindre l'existence du WHERE
        let where_clause = scanner.visit::<Optional<WhereClause>>()?.0;

        // on reconnait le point virgule terminal
        recognize(Token::Semicolon, scanner)?;

        Ok(SelectCommand {
            table_name,
            projection,
            where_clause,
        })
    }
}

Donnant:

#[test]
fn test_parse_select_command_with_where_clause() {
    let data = b"SELECT * FROM table WHERE id = 1;";
    let mut tokenizer = super::Tokenizer::new(data);
    let result = tokenizer.visit::<SelectCommand>().expect("failed to parse");
    assert_eq!(
        result,
        SelectCommand {
            table_name: "table".to_string(),
            projection: Projection::Star,
            where_clause: Some(WhereClause {
                field: "id".to_string(),
                value: Value::Integer(1)
            })
        }
    );
}

Indexation des entrées

Maintenant que notre parser est amĂ©liorĂ© pour dĂ©finir la clef primaire de la table, nous allons pouvoir l’utiliser pour remplir notre “annuaire”.

Et voici la rĂ©ponse Ă  la question “Qui est le nom et le numĂ©ro de notre annuaire ?”.

Notre “nom” sera un tableau de valeur, un tuple en fait

Et notre “numĂ©ro” sera l’ID du tuple dans la table.

type PrimaryIndexes = BTreeMap<Vec<Value>, usize>;

Notre “annuaire” sera stockĂ© au niveau de la table

struct Table {
    schema: Schema,
    row_number: usize,
    pager: Pager,
    // ici 👇
    primary_indexes: BTreeMap<Vec<Value>, usize>,
}

impl Table {
    pub fn new(schema: Schema) -> Self {
        Self {
            pager: Pager::new(schema.size()),
            schema,
            row_number: 0,
            primary_indexes: BTreeMap::new(),
        }
    }
}

Lors de l’insertion, nous allons rĂ©cupĂ©rer les valeurs des champs qui constituent la clef primaire de notre table.

fn get_pk_value(
    row: &HashMap<String, Value>,
    pk_fields: &Vec<String>,
) -> Result<Vec<Value>, InsertionError> {
    let mut pk = vec![];
    for pk_field in pk_fields {
        if let Some(value) = row.get(pk_field) {
            pk.push(value.clone());
        } else {
            return Err(InsertionError::PrimaryKeyNotExists(pk_fields.to_vec()));
        }
    }
    Ok(pk)
}

Note

Si la clef primaire est impossible Ă  trouver, ceci est une erreur, mĂȘme si dans les faits, cette erreur ne peut pas exister car le schĂ©ma vĂ©rifie prĂ©emptivement le tuple Ă  insĂ©rer.

Il est alors possible avant insertion dans la page de venir rajouter notre clef primaire dans l’index primaire.

impl Table {
    pub fn insert(&mut self, row: &HashMap<String, Value>) -> Result<(), InsertionError> {
        // récupération de la slice de données stockée par la page du tuple
        let page = self.pager.write(self.row_number);

        let mut writer = Cursor::new(page);
        // appel à la sérialisation au travers du schéma
        // la vérification est également faite à ce moment

        let pk_fields = &self.schema.primary_key;
        let pk = get_pk_value(row, pk_fields)?;
        
        // insertion dans l'index primaire de notre tuple 
        // et de l'ID courant d'insertion
        self.primary_indexes.insert(pk, self.row_number);

        self.schema
            .serialize(&mut writer, row)
            .map_err(InsertionError::Serialization)?;
        self.row_number += 1;
        Ok(())
    }
}

Query Engine

Pour simplifier les recherches et les scans de tables et surtout pour Ă©viter que le fichier tables n’enfle dĂ©mesurĂ©ment, nous allons mettre la logique de recherche au sein d’un QueryEngine.

Celui-ci prend une référence de Table en paramÚtre.

pub struct QueryEngine<'a> {
    table: &'a Table,
}

impl<'a> QueryEngine<'a> {
    pub fn new(table: &'a Table) -> Self {
        Self { table }
    }
}

On peut alors créer des comportement de recherche différent.

D’abord le full-scan, qui est dĂ©placement du code actuelle de la table vers le QueryEngine

impl QueryEngine<'_> {
    pub fn full_scan(&self, row_number: usize) -> Result<Vec<Vec<Value>>, SelectError> {
        let mut rows = Vec::with_capacity(row_number);
        for row_number in 0..row_number {
            let page = self
                .table
                .pager
                .read(row_number)
                .ok_or(SelectError::PageNotExist(row_number))?;
            let mut reader = Cursor::new(page);
            rows.push(
                // désérialisation par le schéma
                self.table
                    .schema
                    .deserialize(&mut reader)
                    .map_err(SelectError::Deserialization)?,
            )
        }
        Ok(rows)
    }
}

Puis notre recherche par index primaire.

Pour cela nous avons besoin de d’un utilitaire get_row qui rĂ©cupĂšre un tuple par rapport Ă  son ID.

impl QueryEngine<'_> {
    fn get_row(&self, row_number: usize) -> Result<Vec<Value>, SelectError> {
        let page = self
            .table
            .pager
            .read(row_number)
            .ok_or(SelectError::PageNotExist(row_number))?;
        let mut reader = Cursor::new(page);
        let row = self
            .table
            .schema
            .deserialize(&mut reader)
            .map_err(SelectError::Deserialization)?;
        Ok(row)
    }
}

Et une mĂ©thode get_by_pk, le pk signifie “Primary Key”.

impl QueryEngine<'_> {
    pub fn get_by_pk(
        &self,
        values: &Vec<Value>,
        primary_indexes: &PrimaryIndexes,
    ) -> Result<Vec<Vec<Value>>, SelectError> {
        // récupération l'ID du tuple
        let row_number = primary_indexes
            .get(values)
            .ok_or(SelectError::PrimaryKeyNotExists(values.clone()))?;
        // récupération de la page
        let row = self.get_row(*row_number)?;
        Ok(vec![row])
    }
}

Utilisation du Query Engine

Tout étant correctement découpé, il est possible de segmenter nos recherches entre le full scan et la recherche par PK.

impl Table {
    pub fn select(
        &self,
        where_clause: Option<WhereClause>,
    ) -> Result<Vec<Vec<Value>>, SelectError> {
        // instanciation du Query Engine pour la table
        let engine = QueryEngine::new(self);

        match where_clause {
            // s'il n'y a pas de clause where on scan tout
            None => engine.full_scan(self.row_number),
            // sinon si la clause where concerne la clef primaire
            Some(WhereClause { field, value })
                if self.schema.primary_key == vec![field.clone()] =>
            {
                // on récupÚre l'entrée désignée
                engine.get_by_pk(&vec![value], &self.primary_indexes)
            }
            _ => Err(SelectError::NotImplemented),
        }
    }
}

Testons !!

On se créé une table qui possĂšde une clef primaire “id”.

db > CREATE TABLE Users (id INTEGER PRIMARY KEY, name TEXT(20), email TEXT(50));
db > INSERT INTO Users (id, name, email) VALUES (42, 'john.doe', 'john.doe@example.com');
db > INSERT INTO Users (id, name, email) VALUES (666, 'jane.doe', 'jane.doe@example.com');
db > INSERT INTO Users (id, name, email) VALUES (1, 'admin', 'admin@example.com');

On full scan la table

db > SELECT * FROM Users;
[Integer(42), Text("john.doe"), Text("john.doe@example.com")]
[Integer(666), Text("jane.doe"), Text("jane.doe@example.com")]
[Integer(1), Text("admin"), Text("admin@example.com")]

On récupÚre par PK.

db > SELECT * FROM Users WHERE id = 1;
[Integer(1), Text("admin"), Text("admin@example.com")]
db > SELECT * FROM Users WHERE id = 42;
[Integer(42), Text("john.doe"), Text("john.doe@example.com")]
db > SELECT * FROM Users WHERE id = 666;
[Integer(666), Text("jane.doe"), Text("jane.doe@example.com")]
db >

Succùs total et absolu !! 😍

Conclusion

Notre Query Engine a encore plein de problĂšme et notre clef primaire ne supporte pas encore les clefs composites. Mais on a un dĂ©but de quelque chose. ^^’

Dans la prochaine partie, nous verrons les expressions qui permettront de faire des recherches plus intéressantes.

Merci de votre lecture ❀

Vous pouvez trouver le code la partie ici et le diff lĂ .