Partie 6 : Parser les commandes SQL
Lecture 58 min. âą
Table des matiĂšres
- Modifications des tokens
- Composants supplémentaires
- Commande Create Table
- Command Insert Into
- Commande Insert Into
- Commande Select From
- Parser les commandes
- Conclusion
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-5 nous avons bùti un parseur en utilisant le pattern Visitor et nous vons construit la grammaire des commandes supportées.
Aujourdhui, nous allons utiliser les outils qui sont Ă notre dispositions et peut-ĂȘtre en crĂ©er de nouveau pour parser des versions simplifiĂ©es de commande SQL:
- CREATE TABLE
- SELECT
- INSERT
Il y a un peu de travail, mais il est comparativement Ă la partie 5 sera bien plus mĂ©canique quâexploratoire.
Câest parti !
Modifications des tokens
Avant de nous lancer dans les parser Ă proprement parler, je vais retifier le tir sur quelques maladresses dans la gestion des tokens et de leur reconnaissance.
Le token Literal String que lâon a créé prĂ©cĂ©demment possĂšde deux angles morts:
- il ne gÚre pas les espaces qui seront nécessaire pour certaines valeurs
- il ne gĂšre pas les caractĂšres spĂ©ciaux comme les symboles dont â@â
Dâun autre cĂŽtĂ©, dans certain cas on veut interdire ces comportements.
On va donc créé une variante Identfier et développer deux matchers:
- un pour toutes les chaĂźnes standards
- un autre seulement pour les identifiants de champs, tables, etc âŠ
enum Literal {
/// number recognized
Number,
/// string recognized
String,
/// a special case of string without space or special characters accepted
Identifier,
}On peut en définir les matchers
fn match_string() -> impl Fn(&[u8]) -> (bool, usize) {
move |x: &[u8]| -> (bool, usize) {
if x.is_empty() {
return (false, 0);
}
let mut pos = 0;
let mut found = false;
let forbiden_chars = ['(', ')', '\'', ','];
loop {
if pos == x.len() {
return (found, pos);
}
if forbiden_chars.contains(&(x[pos] as char)) {
return (found, pos);
}
if x[pos].is_ascii_alphanumeric()
|| x[pos].is_ascii_punctuation()
|| x[pos].is_ascii_whitespace()
{
found = true;
pos += 1;
} else {
return (found, pos);
}
}
}
}
fn match_identifier() -> impl Fn(&[u8]) -> (bool, usize) {
move |x: &[u8]| -> (bool, usize) {
if x.is_empty() {
return (false, 0);
}
let mut pos = 0;
let mut found = false;
loop {
if pos == x.len() {
return (found, pos);
}
if x[pos].is_ascii_alphanumeric() || x[pos] == b'_' {
found = true;
pos += 1;
} else {
return (found, pos);
}
}
}
}Pour ensuite les appliquer
impl Match for Literal {
fn matcher(&self) -> Matcher {
match self {
Literal::Number => Box::new(matchers::match_number()),
Literal::Identifier => Box::new(matchers::match_identifier()),
Literal::String => Box::new(matchers::match_string()),
}
}
}Composants supplémentaires
Bien que notre boĂźte Ă outils soit dĂ©jĂ bien chargĂ©e, certaines commandes vont nous demander des composants que lâon ne possĂšde pas encore.
Mais que lâon peut construire Ă partir de ce que lâon a dĂ©jĂ .
Consommer des espaces blancs optionnels
Il nous manque dans notre arsenal, la capacité de consommer les espaces blancs surnuméraires dans nos commandes.
En faire un visiteur est trÚs aisé.
pub struct OptionalWhitespaces;
impl<'a> Visitable<'a, u8> for OptionalWhitespaces {
fn accept(scanner: &mut Scanner<'a, u8>) -> parser::Result<Self> {
// si on est déjà en fin de chaßne plus
// d'espaces ne sont consommables
if scanner.remaining().is_empty() {
return Ok(OptionalWhitespaces);
}
// on boucle tant qu'on est en mesure de capturer du blanc
while Token::Whitespace.recognize(scanner)?.is_some() {
// si on est déjà en fin de chaßne plus
// d'espaces ne sont consommables
if scanner.remaining().is_empty() {
break;
}
}
Ok(OptionalWhitespaces)
}
}Reconnaßtre un groupe qui se termine par un token précis
Ce cas est extrĂȘment prĂ©cis mais nĂ©cessaire pour la suite.
Lors dâun select la projection est dĂ©fini par des identifiants sĂ©parĂ© par des virgules, on sait que lâon arrive Ă la fin de la projection quand on atteint le token FROM
SELECT id, brand FROM table;
^ ^
dĂ©but finIl faut donc forecast ce groupe comme on lâa fait pour les groupe dĂ©limitĂ© par des parenthĂšses.
Comme nos tokens ont des tailles en termes de nombre de bytes différents, il faut connaßtre pour chacun combien ils prennent de place dans la slice.
On rajoute un trait Size, qui a pour but de fournir ce comportement Ă nos token.
pub trait Size {
fn size(&self) -> usize;
}
impl Size for Token {
fn size(&self) -> usize {
match self {
Token::Create => 6,
Token::Table => 5,
Token::Insert => 6,
Token::Into => 4,
Token::Values => 6,
Token::Select => 6,
Token::From => 4,
Token::Field(field) => field.size(),
Token::Literal(_literal) => 0,
_ => 1,
}
}
}
impl Size for Field {
fn size(&self) -> usize {
match self {
Field::Integer => 7,
Field::Text => 4,
}
}
}Je met un 0 Ă literal pour signifier quâil nâa pas de taille dĂ©finie, on aurait pu mettre un Option Ă la place, mais ça complique lâAPI
Pour cela on va se construire un nouvel outil qui prend un token comme paramÚtre, ce token sera le délimiteur de fin.
struct UntilToken(Token);On peut alors lui implémenter Forecast
impl<'a> Forecast<'a, u8, Token> for UntilToken {
fn forecast(&self, data: &mut Scanner<'a, u8>) -> crate::parser::Result<ForecastResult<Token>> {
let mut tokenizer = Tokenizer::new(data.remaining());
while !tokenizer.remaining().is_empty() {
match recognize(self.0, &mut tokenizer) {
Ok(_element) => {
return Ok(ForecastResult::Found {
end_slice: tokenizer.cursor() - self.0.size(),
start: self.0,
end: self.0,
});
}
Err(_err) => {
tokenizer.bump_by(1);
continue;
}
}
}
Ok(ForecastResult::NotFound)
}
}Que lâon peut utiliser de cette maniĂšre.
#[test]
fn test_until_token() {
let data = b"id, brand FROM cars";
let mut tokenizer = Tokenizer::new(data);
let forecast_result =
forecast(UntilToken(Token::From), &mut tokenizer).expect("failed to forecast");
assert_eq!(
forecast_result,
Forecasting {
start: Token::From,
end: Token::From,
data: b"id, brand ",
}
)
}Bon avec ça je pense que lâon a tout. ^^
On peut parser les commandes. đŻ
Commande Create Table
Grammaire de CREATE TABLE
CreateTable ::= 'CREATE' 'TABLE' LiteralString OpenParen ColumnDef* CloseParen Semicolon
ColumnDef ::= LiteralString ColumnType | LiteralString ColumnType Comma
ColumnType ::= 'INTEGER' | ColumTypeText
ColumTypeText ::= 'TEXT' OpenParen LiteralInteger CloseParen
LiteralString ::= [A-Za-z0-9_]*
LiteralInteger ::= [0-9]+
OpenParen ::= '('
CloseParen ::= ')'
Comma ::= ','
Semicolon ::= ';'Câest la plus difficile de toute, on va la faire en premier car elle va permettre de dĂ©gager le chemin pour toutes les autres.
Elle est composée de deux partie principales:
- Une sorte de header qui identifie le nom de la table
- La déclaration du schéma
La schéma est un structure de données qui associe des nom de colonnes à leur définition.
Type de Colonne
Ces définitions sont typées.
Integer
Ce type de colonne est le plus simple, il est constituĂ© dâun token Integer.
INTEGERUne reconnaissance de token suffit, mais il faut faire attention Ă dâĂ©ventuels espaces blancs avant et aprĂšs le token.
On créé alors IntegerField pour y accrocher notre Visitor.
struct IntegerField;
impl<'a> Visitable<'a, u8> for IntegerField {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
scanner.visit::<OptionalWhitespaces>()?;
recognize(Token::Field(TokenField::Integer), scanner)?;
scanner.visit::<OptionalWhitespaces>()?;
Ok(IntegerField)
}
}Text
Un type de colonne textuel est plus complexe car il contient la taille de la chaĂźne de caractĂšres.
On se retrouve alors avec un token Text suivi du token (, un token literal integer puis finalement un token ).
TEXT(50)Cette fois-ci la structure va contenir un champ représentant la taille.
struct TextField(usize);LâAPI de parse que lâon a construit, rend alors les opĂ©rations quasi naturelles.
impl<'a> Visitable<'a, u8> for TextField {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
// on nettoie de potentiel blancs
scanner.visit::<OptionalWhitespaces>()?;
// on reconnaĂźt le token TEXT
recognize(Token::Field(TokenField::Text), scanner)?;
// on nettoie de potentiel blancs
scanner.visit::<OptionalWhitespaces>()?;
// on reconnaĂźt le token (
recognize(Token::OpenParen, scanner)?;
// on nettoie de potentiel blancs
scanner.visit::<OptionalWhitespaces>()?;
// on reconnaĂźt le nombre
let value_token = recognize(Token::Literal(Literal::Number), scanner)?;
// on peut alors extraire les bytes qui représente ce nombre
let value_bytes = &value_token.source[value_token.start..value_token.end];
// les décoder depuis l'utf-8 ou dans le cas présent l'ASCII
let value_string =
String::from_utf8(value_bytes.to_vec()).map_err(ParseError::Utf8Error)?;
// pour finalement parser la chaĂźne vers un usize
let value = value_string.parse().map_err(ParseError::ParseIntError)?;
// on nettoie de potentiel blancs
scanner.visit::<OptionalWhitespaces>()?;
recognize(Token::CloseParen, scanner)?;
// on reconnaĂźt le token (
scanner.visit::<OptionalWhitespaces>()?;
Ok(TextField(value))
}
}FieldResult
On vient alors fusionné nos deux variantes de parse dans une énumération.
enum ColumnTypeResult {
Integer(IntegerField),
Text(TextField),
}Et leur pendant dâAPI publique
enum ColumnType {
Integer,
Text(usize),
}Pour permettre de passer du rĂ©sultat de lâacceptation Ă celui du parse on utilise une implĂ©mentation du trait From.
impl From<ColumnTypeResult> for ColumnType {
fn from(value: ColumnTypeResult) -> Self {
match value {
ColumnTypeResult::Integer(_) => ColumnType::Integer,
ColumnTypeResult::Text(TextField(value)) => ColumnType::Text(value),
}
}
}On peut alors via lâAcceptor tester successivement les types de champs possibles.
impl<'a> Visitable<'a, u8> for ColumnType {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
Acceptor::new(scanner)
.try_or(|field| Ok(ColumnTypeResult::Integer(field)))?
.try_or(|field| Ok(ColumnTypeResult::Text(field)))?
.finish()
.ok_or(ParseError::UnexpectedToken)
.map(Into::into)
}
}On a alors un mécanisme qui transforme le contenu de notre Scanner en un ColumnType.
Définition de colonne
La dĂ©finition de la colonne est un nom et un type de colonne sĂ©parĂ© dâun blanc.
id INTEGER
nom TEXT(26)On le modélise par une structure.
struct ColumnDefinition {
name: String,
field: ColumnType,
}Qui peut ĂȘtre rendu visitable
impl<'a> Visitable<'a, u8> for ColumnDefinition {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
// on nettoie de potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on reconnaĂźt une chaĂźne de caractĂšres
let name_tokens = recognize(Token::Literal(Literal::String), scanner)?;
// on en récupÚre les bytes
let name_bytes = &name_tokens.source[name_tokens.start..name_tokens.end];
// pour décoder le nom du champ
let name = String::from_utf8(name_bytes.to_vec()).map_err(ParseError::Utf8Error)?;
// l'espace blancs est obligatoire
scanner.visit::<Whitespaces>()?;
// on visite le ColumnType
let field = scanner.visit::<ColumnType>()?;
Ok(ColumnDefinition { name, field })
}
}Schéma
Un schéma est un enchaßnement de définition de colonne séparées par des virgules, entouré de parenthÚses.
(id INTEGER, nom TEXT(26))struct Schema {
pub fields: HashMap<String, ColumnType>,
}On peut alors le visiter
impl<'a> Schema {
fn parse(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on forecast notre groupe délimité par des parenthÚse
let fields_group = forecast(GroupKind::Parenthesis, scanner)?;
// on enlÚve les parenthÚses périphériques quio font toutes les deux 1 bytes
let fields_group_bytes = &fields_group.data[1..fields_group.data.len() - 1];
// on en créé un sous scanner
let mut fields_group_tokenizer = Tokenizer::new(fields_group_bytes);
// que l'on visite comme une liste de définition de colonne séparé par des virgules
let columns_definitions =
fields_group_tokenizer.visit::<SeparatedList<ColumnDefinition, SeparatorComma>>()?;
// que l'on transforme en notre map de définition de champs
let fields = columns_definitions.into_iter().fold(
HashMap::new(),
|mut fields, column_definition| {
fields.insert(column_definition.name, column_definition.field);
fields
},
);
// on n'oublie pas de déplacer le curseur du scanner externe de la taille du groupe
scanner.bump_by(fields_group.data.len());
Ok(Schema { fields })
}
}Ce qui nous donne
#[test]
fn test_parse_schema() {
let data = b"( id integer, name text ( 50 ) )";
let mut tokenizer = Tokenizer::new(data);
let schema = tokenizer.visit::<Schema>().expect("failed to parse schema");
assert_eq!(
schema,
Schema {
fields: HashMap::from([
("id".to_string(), ColumnType::Integer),
("name".to_string(), ColumnType::Text(50))
])
}
);
let data = b"(id INTEGER, name TEXT(50))";
let mut tokenizer = Tokenizer::new(data);
let schema = tokenizer.visit::<Schema>().expect("failed to parse schema");
assert_eq!(
schema,
Schema {
fields: HashMap::from([
("id".to_string(), ColumnType::Integer),
("name".to_string(), ColumnType::Text(50))
])
}
);
}La commande Create table
On rassemble tout le monde pour créer notre commande CREATE TABLE.
CREATE TABLE table1 (id INTEGER, nom TEXT(26));Modélisé en:
struct CreateTableCommand {
pub(crate) table_name: String,
schema: Schema,
}Visiter la commande est un jeu de lĂ©gos. đ
impl<'a> Visitable<'a, u8> for CreateTableCommand {
fn accept(scanner: &mut Scanner<'a, u8>) -> parser::Result<Self> {
// on nettoie de potentiel blanc
scanner.visit::<OptionalWhitespaces>()?;
// on reconnait le token CREATE
recognize(Token::Create, scanner)?;
// on reconnait au moins 1 blanc
scanner.visit::<Whitespaces>()?;
// on reconnait le token TABLE
recognize(Token::Table, scanner)?;
// on reconnait au moins 1 blanc
scanner.visit::<Whitespaces>()?;
// on reconnait une literal string représentant le nom de table
let table_name_tokens = recognize(Token::Literal(Literal::String), scanner)?;
let table_name_bytes = &table_name_tokens.source[table_name_tokens.start..table_name_tokens.end];
let table_name = String::from_utf8(table_name_bytes.to_vec()).map_err(ParseError::Utf8Error)?;
// on visite le schéma
let schema = scanner.visit::<Schema>()?;
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on reconnaĂźt le point-virgule final
recognize(Token::Semicolon, scanner)?;
Ok(CreateTableCommand { table_name, schema })
}
}On arrive finalement Ă notre but:
#[test]
fn test_parse_create_table_command() {
let data = b"create table users (id integer, name text(50) );";
let mut scanner = Scanner::new(data);
let result = CreateTableCommand::accept(&mut scanner).expect("failed to parse");
assert_eq!(
result,
CreateTableCommand {
table_name: "users".to_string(),
schema: Schema {
fields: HashMap::from([
("id".to_string(), ColumnType::Integer),
("name".to_string(), ColumnType::Text(50))
])
}
}
);
}Command Insert Into
Grammaire de INSERT INTO
InsertInto ::= 'INSERT' 'INTO' LiteralString OpenParen Column* CloseParen 'VALUES' OpenParen Value* CloseParen Semicolon
Column ::= LiteralString | LiteralString Comma
Value ::= (LiteralInteger | LiteralString ) | ((LiteralInteger | LiteralString) Comma )
LiteralString ::= "'" [A-Za-z0-9_]* "'"
LiteralInteger ::= [0-9]+
OpenParen ::= '('
CloseParen ::= ')'
Comma ::= ','
Semicolon ::= ';'Elle peut faire peur de prime abord, mais elle est relativement innofensive quand on y réfléchie bien.
Elle est composé de 2 grosses partie.
La définition de la table et celle des valeurs.
Définition des champs
Commençons par les champs. Un groupe dĂ©limitĂ© par des parenthĂšses sĂ©parĂ© par des virgules de literals string est prĂ©cĂ©dĂ© par 2 token INSERT,INTO, {table} oĂč {table} est une literal string reprĂ©sentant le nom de la table.
INSERT INTO ma_table(id, brand)Et ça rien ce nâest rien quâon a pas dĂ©jĂ rĂ©ussi Ă faire.
Il faut penser à découper astucieusement le travail.
Dâabord on reconnaĂźt une Column
struct Column(String);
impl<'a> Visitable<'a, u8> for Column {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on reconnait la literal string
let name_tokens = recognize(Token::Literal(Literal::String), scanner)?;
let name_bytes = &name_tokens.source[name_tokens.start..name_tokens.end];
let name = String::from_utf8(name_bytes.to_vec()).map_err(ParseError::Utf8Error)?;
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
Ok(Column(name))
}
}Puis ensuite, on peut en faire un groupe de Column que lâon rĂ©duit en un wrapper de Vec<String>
#[derive(Debug, PartialEq)]
pub struct Columns(Vec<String>);
impl<'a> Visitable<'a, u8> for Columns {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on reconnaĂźt le token "("
recognize(Token::OpenParen, scanner)?;
// on capture le groupe de nom de colonnes
let columns_group = scanner.visit::<SeparatedList<Column, SeparatorComma>>()?;
let colums = columns_group.into_iter();
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on reconnaĂźt le token ")"
recognize(Token::CloseParen, scanner)?;
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
Ok(Columns(colums.map(|column| column.0).collect()))
}
}Et voilĂ !
#[test]
fn test_columns() {
let data = b"(id, brand)";
let mut tokenizer = Tokenizer::new(data);
let result = tokenizer.visit::<Columns>().expect("failed to parse");
assert_eq!(result.0, vec!["id", "brand"]);
}Définitions des valeurs
Deux types de valeurs sont acceptées:
- des entiers : 1235, 42, 0
- des string : âtest dataâ, âemail@example.comâ
Les chaßnes sont entourées par des guillemets simples.
Nous avons donc deux choses Ă gĂ©rer, le type de donnĂ©e (INTEGER, TEXT) et le fait que câest une liste.
Occupons-nous dâabord des entiers.
struct IntegerValue(i64);
impl<'a> Visitable<'a, u8> for IntegerValue {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on récupÚre le nombre
let number_token = recognize(Token::Literal(Literal::Number), scanner)?;
let number_bytes = &number_token.source[number_token.start..number_token.end];
let number_string =
String::from_utf8(number_bytes.to_vec()).map_err(ParseError::Utf8Error)?;
// que l'on parse vers un i64
let number = number_string.parse().map_err(ParseError::ParseIntError)?;
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
Ok(IntegerValue(number))
}
}On fait de mĂȘme avec les champs texte
struct TextValue(String);
impl<'a> Visitable<'a, u8> for TextValue {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on reconnait le début du groupe quoted
recognize(Token::Quote, scanner)?;
// on récupÚre le literal string
let literal_token = recognize(Token::Literal(Literal::String), scanner)?;
let literal_bytes = &literal_token.source[literal_token.start..literal_token.end];
let literal_string =
String::from_utf8(literal_bytes.to_vec()).map_err(ParseError::Utf8Error)?;
// on reconnait la fin du groupe quoted
recognize(Token::Quote, scanner)?;
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
Ok(TextValue(literal_string))
}
}On réalise une union des variantes
enum ValueResult {
Integer(IntegerValue),
Text(TextValue),
}On créé lâĂ©numĂ©ration des types de valeurs
enum Value {
Integer(i64),
Text(String),
}On lui rajoute de la glue technique
impl From<ValueResult> for Value {
fn from(value: ValueResult) -> Self {
match value {
ValueResult::Integer(IntegerValue(value)) => Value::Integer(value),
ValueResult::Text(TextValue(value)) => Value::Text(value),
}
}
}Ce qui permet alors dâen faire un Acceptor et de visiter la Value.
impl<'a> Visitable<'a, u8> for Value {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
Acceptor::new(scanner)
.try_or(|value| Ok(ValueResult::Integer(value)))?
.try_or(|value| Ok(ValueResult::Text(value)))?
.finish()
.ok_or(UnexpectedToken)
.map(Into::into)
}
}Utilisable ainsi.
#[test]
fn test_integer_value() {
let data = b"123";
let mut tokenizer = Tokenizer::new(data);
let value = IntegerValue::accept(&mut tokenizer).expect("failed to parse");
assert_eq!(value, IntegerValue(123));
}
#[test]
fn test_text_value() {
let data = b"'test'";
let mut tokenizer = Tokenizer::new(data);
let value = TextValue::accept(&mut tokenizer).expect("failed to parse");
assert_eq!(value, TextValue("test".to_string()));
}On peut alors accumuler les Value.
struct Values(Vec<Value>);
impl<'a> Visitable<'a, u8> for Values {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on récupÚre le groupe de valeurs
let values_group = forecast(GroupKind::Parenthesis, scanner)?;
let values_group_bytes = &values_group.data[1..values_group.data.len() - 1];
// on créé le tokenizer intermédiaire
let mut value_group_tokenizer = Tokenizer::new(values_group_bytes);
// on reconnait les values
let values_list = value_group_tokenizer.visit::<SeparatedList<Value, SeparatorComma>>()?;
let values = values_list.into_iter().collect();
// on avance le tokenizer externe
scanner.bump_by(values_group.data.len());
Ok(Values(values))
}
}Et ça marche plutĂŽt bien đ
#[test]
fn text_values() {
let data = b"('test1', 120, 'test3')";
let mut tokenizer = Tokenizer::new(data);
let values = Values::accept(&mut tokenizer).expect("failed to parse");
assert_eq!(
values,
Values(vec![
Value::Text("test1".to_string()),
Value::Integer(120),
Value::Text("test3".to_string())
])
);
}Commande Insert Into
On modélise la commande par la structure suivante:
struct InsertIntoCommand {
table_name: String,
fields: HashMap<String, String>
}On en fait un Visiteur
impl<'a> Visitable<'a, u8> for InsertIntoCommand {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on reconnaĂźt le token INSERT
recognize(Token::Insert, scanner)?;
// on reconnait au moins un blanc
scanner.visit::<Whitespaces>()?;
// on reconnaĂźt le token INTO
recognize(Token::Into, scanner)?;
// on reconnait au moins un blanc
scanner.visit::<Whitespaces>()?;
// on reconnait le nom de la table
let name_tokens = recognize(Token::Literal(Literal::String), scanner)?;
let name_bytes = name_tokens.source[name_tokens.start..name_tokens.end].to_vec();
let table_name = String::from_utf8(name_bytes).map_err(ParseError::Utf8Error)?;
// on visite les noms de colonne
let columns = scanner.visit::<Columns>()?;
// on reconnait au moins un blanc
scanner.visit::<Whitespaces>()?;
// on reconnaĂźt le token VALUES
recognize(Token::Values, scanner)?;
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on reconnaĂźt les values
let values = scanner.visit::<Values>()?;
// on zip les couples (colonne, valeur)
let fields = zip(values.0, columns.0).fold(HashMap::new(), |mut map, (value, column)| {
map.insert(column, value);
map
});
Ok(InsertIntoCommand { table_name, fields })
}
}Qui fonctionne ainsi
#[test]
fn test_insert_into_command() {
let data = b"INSERT INTO users(id, name, email) VALUES(42, 'user 1', 'email@example.com')";
let mut tokenizer = Tokenizer::new(data);
let result = tokenizer
.visit::<InsertIntoCommand>()
.expect("failed to parse");
assert_eq!(result.table_name, "users");
assert_eq!(result.fields.len(), 3);
assert_eq!(result.fields.get("id").unwrap(), &Value::Integer(42));
assert_eq!(
result.fields.get("name").unwrap(),
&Value::Text("user 1".to_string())
);
assert_eq!(
result.fields.get("email").unwrap(),
&Value::Text("email@example.com".to_string())
);
assert_eq!(tokenizer.cursor(), 76);
}SuccĂšs ! đ
Commande Select From
Grammaire de SELECT
Select ::= 'SELECT' Column* 'FROM' LiteralString Semicolon
Column ::= '*' | LiteralString | LiteralString Comma
LiteralString ::= [A-Za-z0-9_]*
LiteralInteger ::= [0-9]+
OpenParen ::= '('
CloseParen ::= ')'
Comma ::= ','
Semicolon ::= ';'La derniĂšre commande que nous allons parser est le Select, elle semblait la plus simple mais elle mâa donnĂ© un peu de rĂ©flexion Ă avoir sur la projection.
Projection
La projection permet de sĂ©lectionner les champs qui doivent ĂȘtre renvoyĂ© lors dâun SELECT.
La projection va fonctionner de la mĂȘme maniĂšre que pour les ColunmType, il y lĂ aussi deux variantes:
- * : qui projete tous les champs
- columns : qui liste des champs précis
Occupons-nous de la version â*â.
struct ProjectionStar;
impl<'a> Visitable<'a, u8> for ProjectionStar {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
// on nettoie de potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on reconnait l'étoile
recognize(Token::Star, scanner)?;
// on nettoie de potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
Ok(ProjectionStar)
}
}Rien de bien exaltant Ă expliquer.
La version âcolonnesâ est plus intĂ©ressante.
struct ProjectionColumns(Vec<String>);
impl<'a> Visitable<'a, u8> for ProjectionColumns {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
// on nettoie de potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on capture le groupe qui se termine par le token FROM
let columns_tokens = forecast(UntilToken(Token::From), scanner)?;
let columns_bytes = columns_tokens.data;
// on crée un tokenizer pour récupérer les noms de colonnes
let mut columns_tokenizer = Tokenizer::new(columns_bytes);
// on visite une liste de colonnes
let columns_list = columns_tokenizer.visit::<SeparatedList<Column, SeparatorComma>>()?;
let columns = columns_list
.into_iter()
.map(|x| x.0)
.collect::<Vec<String>>();
// on nettoie de potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on avance le curseur
scanner.bump_by(columns_tokens.data.len());
Ok(ProjectionColumns(columns))
}
}On fusionne les deux types de projection.
enum ProjectionResult {
Star(ProjectionStar),
Columns(ProjectionColumns),
}On créé également la structure de Projection.
enum Projection {
Columns(Vec<String>),
Star,
}Et en faire la glue.
impl From<ProjectionResult> for Projection {
fn from(value: ProjectionResult) -> Self {
match value {
ProjectionResult::Star(ProjectionStar) => Projection::Star,
ProjectionResult::Columns(ProjectionColumns(columns)) => Projection::Columns(columns),
}
}
}Qui permet finalement dâen crĂ©er un visiteur.
impl<'a> Visitable<'a, u8> for Projection {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
Acceptor::new(scanner)
.try_or(|projection| Ok(ProjectionResult::Star(projection)))?
.try_or(|projection| Ok(ProjectionResult::Columns(projection)))?
.finish()
.ok_or(ParseError::UnexpectedToken)
.map(Into::into)
}
}Qui sâutilise ainsi.
#[test]
fn test_projections() {
let data = b"* FROM";
let mut tokenizer = Tokenizer::new(data);
let projection = tokenizer.visit::<Projection>().expect("failed to parse");
assert_eq!(projection, Projection::Star);
let data = b"id, name FROM";
let mut tokenizer = Tokenizer::new(data);
let projection = tokenizer.visit::<Projection>().expect("failed to parse");
assert_eq!(
projection,
Projection::Columns(vec!["id".to_string(), "name".to_string()])
);
}La commande Select From
Une fois en possession de la Projection, il est possible de mettre en place la commande Select
struct SelectCommand {
table_name: String,
projection: Projection,
}
impl<'a> Visitable<'a, u8> for SelectCommand {
fn accept(scanner: &mut Tokenizer<'a>) -> parser::Result<Self> {
// on nettoie les potentiels espaces
scanner.visit::<OptionalWhitespaces>()?;
recognize(Token::Select, scanner)?;
// on reconnait au moins un espace
scanner.visit::<Whitespaces>()?;
// on reconnait la projection
let projection = scanner.visit::<Projection>()?;
// on reconnait le token FROM
recognize(Token::From, scanner)?;
// on reconnait au moins un espace
scanner.visit::<Whitespaces>()?;
// on reconnait le nom de la table
let name_tokens = recognize(Token::Literal(Literal::Identifier), scanner)?;
let name_bytes = name_tokens.source[name_tokens.start..name_tokens.end].to_vec();
let table_name = String::from_utf8(name_bytes).map_err(ParseError::Utf8Error)?;
// on nettoie les potentiels espaces
scanner.visit::<OptionalWhitespaces>()?;
// on reconnait le point virgule terminal
recognize(Token::Semicolon, scanner)?;
Ok(SelectCommand {
table_name,
projection,
})
}
}Et ça marche comme sur des roulettes !
#[test]
fn test_parse_select_command() {
let data = b"SELECT * FROM table;";
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
}
);
let data = b"SELECT id, brand FROM table_2;";
let mut tokenizer = super::Tokenizer::new(data);
let result = tokenizer.visit::<SelectCommand>().expect("failed to parse");
assert_eq!(
result,
SelectCommand {
table_name: "table_2".to_string(),
projection: Projection::Columns(vec!["id".to_string(), "brand".to_string()])
}
)
}Parser les commandes
Comme tout est étagé et isolé. La cerise sur le gùteau est evidente.
enum Command {
CreateTable(CreateTableCommand),
Select(SelectCommand),
InsertInto(InsertIntoCommand),
}
impl<'a> Visitable<'a, u8> for Command {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
Acceptor::new(scanner)
.try_or(|command| Ok(Command::Select(command)))?
.try_or(|command| Ok(Command::CreateTable(command)))?
.try_or(|command| Ok(Command::InsertInto(command)))?
.finish()
.ok_or(ParseError::UnexpectedToken)
}
}Et tout se goupille !! đ
#[test]
fn test_select() {
let data = b"SELECT * FROM table;";
let mut tokenizer = Tokenizer::new(data);
assert_eq!(
tokenizer.visit(),
Ok(Command::Select(SelectCommand {
table_name: "table".to_string(),
projection: Projection::Star
}))
);
let data = b"SELECT id, brand FROM table_2;";
let mut tokenizer = Tokenizer::new(data);
assert_eq!(
tokenizer.visit(),
Ok(Command::Select(SelectCommand {
table_name: "table_2".to_string(),
projection: Projection::Columns(vec!["id".to_string(), "brand".to_string()])
}))
);
}
#[test]
fn test_insert() {
let data = b"INSERT INTO Users(id, name, email) VALUES (42, 'user 1', 'email@example.com')";
let mut tokenizer = Tokenizer::new(data);
assert_eq!(
tokenizer.visit(),
Ok(Command::InsertInto(InsertIntoCommand {
table_name: "Users".to_string(),
fields: vec![
("id".to_string(), Value::Integer(42)),
("name".to_string(), Value::Text("user 1".to_string())),
(
"email".to_string(),
Value::Text("email@example.com".to_string())
)
]
.into_iter()
.collect()
}))
);
}
#[test]
fn test_create() {
let data = b"CREATE TABLE Users(id INTEGER, name TEXT(50), email TEXT(128));";
let mut tokenizer = Tokenizer::new(data);
assert_eq!(
tokenizer.visit(),
Ok(Command::CreateTable(CreateTableCommand {
table_name: "Users".to_string(),
schema: Schema {
fields: HashMap::from([
("id".to_string(), ColumnType::Integer),
("name".to_string(), ColumnType::Text(50)),
("email".to_string(), ColumnType::Text(128)),
]),
}
}))
)
}Conclusion
Ok ! La parenthĂšse âparserâ est enfin terminĂ©e ! Il nâest pas parfait, il a plein de problĂšmes, mais il va permettre de pouvoir attaquer la crĂ©ation des schĂ©mas sur nos tables et donc pouvoir nous dĂ©barasser des stuctures User et Car et tous les inconvĂ©nients quâelles comportent.
Dans la prochaine partie on généralisera les données stockées !
Merci de votre lecture â€ïž
