Partie 11 : Expressions Logiques SQL

Lecture 32 min. ‱

Table des matiĂšres

Les articles de la série

Bonjour à toutes et tous 😃

L’introduction du concept de clef primaire permet de rĂ©cupĂ©rer une entrĂ©e dans la base de donnĂ©es sans devoir scanner tous les Ă©lĂ©ments un par un jusqu’à trouver le bon.

C’est pas mal du tout, mais un peu limitĂ©.

GĂ©nĂ©ralement les requĂȘtes sont un peu plus complexes et utiles.

Si l’on reprend la mĂ©taphore de notre annuaire tĂ©lĂ©phonique, c’est comme si pour trouver quelqu’un, vous n’avez que deux choix: soit vous chercher dans toutes les pages Ă  la recherche du bon nom soit vous connaissez dĂ©jĂ  le numĂ©ro de page et c’est nettement plus simple. Ça c’est le fonctionnement de l’index primaire et du full-scan.

Maintenant, imaginons que l’on veuille rĂ©cupĂ©rer les numĂ©ros de tous les hommes habitant dans la ville “X”.

Dans la vraie vie, pour ceux qui n’ont jamais vu un, si si ça doit exister maintenant 😅. Les entrĂ©es sont dĂ©composĂ© en section pour chaque ville et triĂ© par ordre alphabĂ©tique.

Nous pouvons modĂ©liser notre annuaire au travers d’une table PhoneBook.

CREATE TABLE PhoneBook(name TEXT(50) PRIMARY KEY, city TEXT(50), phone_number TEXT(50), gender TEXT(1));

Notre requĂȘte dans l’annuaire ne peut pas ĂȘtre par clef primaire.

Il faut connaĂźtre les noms des personnes pour les trouver.

SELECT * FROM PhoneBook WHERE name='Alexandre Adams'
SELECT * FROM PhoneBook WHERE name='Amandine Agostini'
SELECT * FROM PhoneBook WHERE name='Arthur Almeida'
SELECT * FROM PhoneBook WHERE name='AnaĂŻs Aubert'
...

Ce n’est pas trùs pratique
 😑

La vraie requĂȘte que l’on veut c’est:

SELECT * FROM PhoneBook WHERE city = 'X' AND gender = 'H';

Cette partie de la requĂȘte, nous allons l’appeler “expression”.

city = 'X' AND gender = 'H'

Je vous propose de mettre en place le parse de cette expression dans cette nouvelle partie de notre épopée.

Grammaire

Avant de partir en bataille avec notre parser, nous allons en définir les contours.

Expression logique
Expression ::= ColumnExpression | Expression LogicalOperator Expression
ColumnExpression ::= Column BinaryOperator LiteralValue
LogicalOperator ::= 'AND' | 'OR'
BinaryOperator ::= '=' | '!=' | '>' | '>=' | '<' | '<='
Column ::= [A-Za-z_]+
LiteralValue ::= LiteralString | LiteralInteger
LiteralString ::=  "'" [^']+ "'"
LiteralInteger ::= [0-9]+

Expression:


ColumnExpression:


LogicalOperator:


BinaryOperator:


Column:


LiteralValue:


LiteralString:


LiteralInteger:


Notre grammaire se complexifie. Nous avons tout une série de nouveaux tokens à rajouter.

Nous avons introduit Ă©galement le concept d’expression de colonne ColumnExpression.

On associe un identifiant de colonne et une valeur comparé par un opérateur binaire BinaryOperator.

id > 12
name != 'Max'

Mais nous avons Ă©galement la possibilitĂ© de d’associer deux ColumnExpression via un LogicalOperator.

Et on peut le faire autant que l’on veut.

id > 12 AND name != 'Max' OR data = 'true'

Ajout des tokens

On rajoute nos différents tokens

enum Operator {
    /// AND token
    And,
    /// OR token
    Or,
    /// > token
    GreaterThan,
    /// < token
    LessThan,
    /// >= token
    GreaterThanOrEqual,
    /// <= token
    LessThanOrEqual,
    /// != token
    Different,
    /// = token
    Equal,
}


enum Token {
    // ...
    Operator(Operator),
    // ...
}

On y associe un Matcher

impl Match for Operator {
    fn matcher(&self) -> Matcher {
        match self {
            Operator::And => Box::new(matchers::match_pattern("and")),
            Operator::Or => Box::new(matchers::match_pattern("or")),
            Operator::GreaterThan => Box::new(matchers::match_pattern(">")),
            Operator::LessThan => Box::new(matchers::match_pattern("<")),
            Operator::GreaterThanOrEqual => Box::new(matchers::match_pattern(">=")),
            Operator::LessThanOrEqual => Box::new(matchers::match_pattern("<=")),
            Operator::Different => Box::new(matchers::match_pattern("!=")),
            Operator::Equal => Box::new(matchers::match_pattern("=")),
        }
    }
}

impl Match for Token {
    fn matcher(&self) -> Matcher {
        match self {
            // ...
            Token::Operator(operator) => operator.matcher(),
            // ...
        }
    }
}

Ainsi que la taille des tokens

impl Size for Operator {
    fn size(&self) -> usize {
        match self {
            Operator::And => 3,
            Operator::Or => 2,
            Operator::GreaterThan => 1,
            Operator::LessThan => 1,
            Operator::GreaterThanOrEqual => 2,
            Operator::LessThanOrEqual => 2,
            Operator::Different => 2,
            Operator::Equal => 1,
        }
    }
}


impl Size for Token {
    fn size(&self) -> usize {
        match self {
            // ...
            Token::Operator(operator) => operator.size(),
            // ...
        }
    }
}

Parsing de l’expression

Comme la grammaire le laisse supposer, parser une Expression est plus complexe que parser les embryon de commandes que nous avons déjà dans notre arsenal.

La principal difficultĂ© Ă©tant qu’une Expression est rĂ©cursivement une Expression.

Mais avant de nous attaquer au plat de resistance et de l’avoir sur l’estomac par indigestion. Nous allons dĂ©composer le problĂšmes en sous-problĂšmes et recomposer le puzzle.

Recognizer

On avait dĂ©jĂ  un Acceptor qui permettait d’avoir des alternatives de visiteurs, le Forecaster qui donnait le choix de forecast de groupe de token dans le futur.

Il nous manquait la possiblité de choisir une collection de tokens à reconnaßtre.

Avec le Recognizer c’est chose faite !

impl<'a, 'b, T, U> Recognizer<'a, 'b, T, U> {
    pub fn new(scanner: &'b mut Scanner<'a, T>) -> Self {
        Recognizer {
            data: None,
            scanner,
        }
    }

    pub fn try_or<R: Recognizable<'a, T, U>>(
        mut self,
        element: R,
    ) -> Result<Recognizer<'a, 'b, T, U>> {
        // Propagate result
        if self.data.is_some() {
            return Ok(self);
        }
        // Or apply current recognizer
        if let Some(found) = element.recognize(self.scanner)? {
            self.data = Some(found);
        }
        Ok(self)
    }

    pub fn finish(self) -> Option<U> {
        self.data
    }
}

MĂȘme principe que pour les deux autres, on lui fourni une liste de choses Ă  reconnaĂźtre et il tente de reconnaĂźtre le token, s’il n’y arrive pas, il passe au suivant, sinon il s’arrĂȘte et propage le token reconnu.

En sorti de méthode finish on obtient alors une Option du token.

Binary Operator

La premiĂšre chose que l’on va vouloir reconnaĂźtre c’est nos opĂ©rateurs binaires.

Nous avons besoin d’une Ă©numĂ©ration pour modĂ©liser ces opĂ©rateurs

enum BinaryOperator {
    GreaterThanOrEqual,
    LessThanOrEqual,
    GreaterThan,
    LessThan,
    Equal,
    Different,
}

Ainsi que d’un TryFrom qui va permettre de passer du token reconnu Ă  l’opĂ©rateur.

impl TryFrom<Token> for BinaryOperator {
    type Error = ParseError;
    fn try_from(value: Token) -> Result<Self, Self::Error> {
        match value {
            Token::Operator(Operator::GreaterThanOrEqual) => Ok(BinaryOperator::GreaterThanOrEqual),
            Token::Operator(Operator::LessThanOrEqual) => Ok(BinaryOperator::LessThanOrEqual),
            Token::Operator(Operator::GreaterThan) => Ok(BinaryOperator::GreaterThan),
            Token::Operator(Operator::LessThan) => Ok(BinaryOperator::LessThan),
            Token::Operator(Operator::Equal) => Ok(BinaryOperator::Equal),
            Token::Operator(Operator::Different) => Ok(BinaryOperator::Different),
            _ => Err(ParseError::UnexpectedToken),
        }
    }
}

On peut alors reconnaßtre notre opérateur.

#[test]
fn test_binary_operator() {
    let data = b"> 12";
    let mut scanner = Scanner::new(data);
    let result: BinaryOperator = Recognizer::new(&mut scanner)
        // reconnaissance de l'opĂ©rateur 👇
        .try_or(Token::Operator(Operator::GreaterThan))
        .expect("Unable to parse")
        .finish()
        .expect("No token recognize")
        .element
        .try_into()
        .expect("Unable to convert");
    assert_eq!(result, BinaryOperator::GreaterThan);
}

ColumnExpression

On peut désormais représenter notre ColumnExpression

struct ColumnExpression {
    column: Column,
    operator: BinaryOperator,
    value: Value,
}

impl ColumnExpression {
    pub fn new(column: Column, operator: BinaryOperator, value: Value) -> Self {
        Self {
            column,
            operator,
            value,
        }
    }
}

Que l’on peut alors visiter.

impl<'a> Visitable<'a, u8> for ColumnExpression {
    fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
        // reconnaissance de l'identifiant de colonne
        let column = Column::accept(scanner)?;
        // nettoyage d'eventuel d'espace blanc
        OptionalWhitespaces::accept(scanner)?;

        // reconnaissance de l'opérateur binaire
        let operator = Recognizer::new(scanner)
            .try_or(Token::Operator(Operator::GreaterThanOrEqual))?
            .try_or(Token::Operator(Operator::GreaterThan))?
            .try_or(Token::Operator(Operator::LessThanOrEqual))?
            .try_or(Token::Operator(Operator::LessThan))?
            .try_or(Token::Operator(Operator::Different))?
            .try_or(Token::Operator(Operator::Equal))?
            .finish()
            .ok_or(ParseError::UnexpectedToken)?
            .element
            .try_into()?;

        // nettoyage d'eventuel d'espace blanc
        OptionalWhitespaces::accept(scanner)?;
        // reconnaissance de la valeur
        let value = Value::accept(scanner)?;
        Ok(ColumnExpression::new(column, operator, value))
    }
}

Ce qui donne:

#[test]
fn test_column_expression() {
    let data = b"id = 12";
    let mut scanner = Scanner::new(data);
    let result = scanner.visit();
    assert_eq!(
        result,
        Ok(ColumnExpression {
            column: Column("id".to_string()),
            operator: BinaryOperator::Equal,
            value: Value::Integer(12)
        })
    );
}

Parfait! On peut maintenant parser une ColumnExpression đŸ€©

Opérateur logique

On fait de mĂȘme pour les tokens:

enum LogicalOperator {
    Or,
    And,
}

impl TryFrom<Token> for LogicalOperator {
    type Error = ParseError;

    fn try_from(value: Token) -> Result<Self, Self::Error> {
        match value {
            Token::Operator(Operator::And) => Ok(LogicalOperator::And),
            Token::Operator(Operator::Or) => Ok(LogicalOperator::Or),
            _ => Err(ParseError::UnexpectedToken),
        }
    }
}

Permettant de reconnaßtre les opérateur logique.

#[test]
fn test_logical_operator() {
    let data = b"AND toto";
    let mut scanner = Scanner::new(data);
    let result: LogicalOperator = Recognizer::new(&mut scanner)
        .try_or(Token::Operator(Operator::And))
        .expect("Unable to parse")
        .finish()
        .expect("No token recognize")
        .element
        .try_into()
        .expect("Unable to convert");
    assert_eq!(result, LogicalOperator::And);
}

Expression

Notre expression est une suite de ColumnExpression séparé par de LogicalOperator.

Lorsque l’on a une expression du genre:

id > 12 AND name = 'toto'

Cela donne une expression du genre

ColumnExpression LogicalOperator ColumnExpression

Mais si on a quelque chose commen

id > 12 AND name = 'toto' AND gender = 'M'

Cela donne l’expression suivante

ColumnExpression LogicalOperator ColumnExpression LogicalOperator ColumnExpression

Sauf que l’opĂ©rateur logique n’a que deux paramĂštres:

Si on redécoupe

ColumnExpression LogicalOperator [ ColumnExpression LogicalOperator ColumnExpression ]

On réduit encore

ColumnExpression LogicalOperator LogicalExpression

Et si l’on veut que le systĂšme soit totalement rĂ©cursif, notre expression doit se rĂ©duire en

LogicalExpression

On en déduit deux choses.

Nous avons besoin d’une Ă©numĂ©ration qui permette de faire une union entre la ColumnExpression et la LogicalExpression

enum Expression {
    Logical(LogicalExpression),
    Column(ColumnExpression),
}

On la visite

impl<'a> Visitable<'a, u8> for Expression {
    fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
        Acceptor::new(scanner)
            .try_or(|value| Ok(Expression::Logical(value)))?
            .try_or(|value| Ok(Expression::Column(value)))?
            .finish()
            .ok_or(ParseError::UnexpectedToken)
    }
}

LogicalExpression

On peut alors en définir le LogicalExpression

struct LogicalExpression {
    lhs: Box<Expression>,
    operator: LogicalOperator,
    rhs: Box<Expression>,
}

Note

Le boxing Box<Expression> est obligatoire car Expression peut contenir une LogicalExpression qui contient lui mĂȘme une Expression, etc 


Cela conduit Ă  une taille infinie.

Ça la stack n’aime pas du tout, la heap n’a pas ce problùme.

On en défini également un constructeur

impl LogicalExpression {
    fn new(lhs: Expression, operator: LogicalOperator, rhs: Expression) -> Self {
        Self {
            lhs: Box::new(lhs),
            operator,
            rhs: Box::new(rhs),
        }
    }
}

Et finalement un visiteur

impl<'a> Visitable<'a, u8> for LogicalExpression {
    fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
        // on forecast la fin d'un groupe d'expression car si l'on tente
        // de visiter une Expression sans avoir au préalable délimité
        // le contenu du scanner, nous allons éternellement 
        // parser le mĂȘme morceau de LogicalExpression
        // ce qui fait exploser la stack !
        // Tout lhs fini nécessairement par AND ou OR
        let lhs_group = Forcaster::new(scanner)
            .try_or(UntilToken(Token::Operator(Operator::And)))?
            .try_or(UntilToken(Token::Operator(Operator::Or)))?
            .finish()
            .ok_or(ParseError::UnexpectedToken)?;

        let mut lhs_scanner = Scanner::new(lhs_group.data);
        // on visite l'Expression du lhs
        let lhs = lhs_scanner.visit()?;
        // on avance le curseur du scanner principal
        scanner.bump_by(lhs_group.data.len());
        
        // on nettoie d'éventuel blancs
        scanner.visit::<OptionalWhitespaces>()?;

        // on reconnait l'opérateur logique
        let operator = Recognizer::new(scanner)
            .try_or(Token::Operator(Operator::And))?
            .try_or(Token::Operator(Operator::Or))?
            .finish()
            .ok_or(ParseError::UnexpectedToken)?
            .element
            .try_into()?;
        // on reconnaßt au moins un blanc aprÚs l'opérateur logique
        scanner.visit::<Whitespaces>()?;
        // on visite l'expression suivante
        let rhs = scanner.visit()?;
        Ok(LogicalExpression::new(lhs, operator, rhs))
    }
}

Test de parse

On peut finalement parser nos Expressions

Expression simple

D’abord une simple comparaison

#[test]
fn test_logical_expression_simple() {
    let data = b"id > 12";
    let mut scanner = Scanner::new(data);
    let result = scanner.visit();
    assert_eq!(
        result,
        Ok(Expression::Column(ColumnExpression::new(
            Column("id".to_string()),
            BinaryOperator::GreaterThan,
            Value::Integer(12),
        )))
    );
}

Expression logique

Puis une utilisant un connecteur logique

#[test]
fn test_logical_expression_or() {
    let data = b"id <= 12 OR name != 'toto'";
    let mut scanner = Scanner::new(data);
    let result = scanner.visit();

    let c1 = ColumnExpression::new(
        Column("id".to_string()),
        BinaryOperator::LessThanOrEqual,
        Value::Integer(12),
    );

    let c2 = ColumnExpression::new(
        Column("name".to_string()),
        BinaryOperator::Different,
        Value::Text("toto".to_string()),
    );

    let l1 = LogicalExpression::new(
        Expression::Column(c1),
        LogicalOperator::Or,
        Expression::Column(c2),
    );

    assert_eq!(result, Ok(Expression::Logical(l1)))
}

Expression logiques composites

Puis un chaĂźnage de plusieurs expressions

#[test]
fn test_logical_expression_or_and() {
    let data = b"id <= 12 OR name != 'toto' AND gender = 'male'";
    let mut scanner = Scanner::new(data);
    let result = scanner.visit();

    let c1 = ColumnExpression::new(
        Column("id".to_string()),
        BinaryOperator::LessThanOrEqual,
        Value::Integer(12),
    );

    let c2 = ColumnExpression::new(
        Column("name".to_string()),
        BinaryOperator::Different,
        Value::Text("toto".to_string()),
    );

    let c3 = ColumnExpression::new(
        Column("gender".to_string()),
        BinaryOperator::Equal,
        Value::Text("male".to_string()),
    );

    let l1 = LogicalExpression::new(
        Expression::Column(c1),
        LogicalOperator::Or,
        Expression::Column(c2),
    );

    let l2 = LogicalExpression::new(
        Expression::Logical(l1),
        LogicalOperator::And,
        Expression::Column(c3),
    );

    assert_eq!(result, Ok(Expression::Logical(l2)))
}

Et voilĂ  ! đŸ€©

Nous sommes capables de parser des expressions logiques SQL. 😍😍😍

Pas toutes biensĂ»r, mais c’est un dĂ©but.

En tout cas ça sera suffisant pour la suite de mes explications.

Conclusion

Oui, c’était encore un Ă©pisode de parsing, mais que voulez vous, il faut bien des outils pour bĂątir des choses, et le parsing est nĂ©cessaire pour nous ouvir le champ des possibles. 😎

Dans la prochaine partie nous utiliserons nos expressions pour requĂȘter notre base donnĂ©es sur autre chose que la clef primaire.

Merci de votre lecture ❀

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