Partie 16: Directive EXPLAIN
Lecture 3 min. âą
Table des matiĂšres
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 đ
Par lâintroduction des index secondaires, nous avons ouvert tout un champ des possibles sur de la recherche de donnĂ©es plus efficiente. Mais nous nâavons vraiment parcouru que la moitiĂ© du chemin: nous savons indexer, mais pas rechercher dans cet index.
Afin de permettre de comprendre quand le query engine dĂ©cide ou non de dĂ©cider de lâutilisation des index secondaires. Nous allons introduire un modificateur dâexĂ©cution que nous allons appeler EXPLAIN. Ce sera notre mode de debug pour bĂątir des systĂšmes plus intelligents et complexes.
On a encore bien du pain sur la planche, mais au bout de 16 articles et je ne sais combien de millier de lignes de codes expliquĂ©es, vous commencez Ă prendre lâhabitude. đ
Parser
Tout commence par bĂątir un parser pour dĂ©tecter notre modificateur dâexĂ©cution.
Nous dĂ©cidons que si le premier token est EXPLAIN, alors la commande doit-ĂȘtre debugguĂ©e.
EXPLAIN SELECT ...Nous devons ainsi reconnaĂźtre le token EXPLAIN.
enum Token {
/// EXPLAIN token
Explain,
}La structure reconnue est un peu étrange
struct Explain(usize);Le usize reprĂ©sentera le nombre de bytes consommĂ©e pour parser le âEXPLAINâ sâil existe.
impl<'a> Visitable<'a, u8> for Explain {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
let cursor = scanner.cursor();
scanner.visit::<OptionalWhitespaces>()?;
recognize(Token::Explain, scanner)?;
scanner.visit::<OptionalWhitespaces>()?;
let delta = scanner.cursor() - cursor;
Ok(Self(delta))
}
}On est désormais en capacité de détecter notre nouveau modificateur.
#[test]
fn test_explain() {
let data = b"EXPLAIN SELECT * FROM table";
let mut tokenizer = Tokenizer::new(data);
let result = tokenizer.visit();
// 8 bytes incluant l'espace blanc aprĂšs EXPLAIN
assert_eq!(result, Ok(Explain(8)));
}
#[test]
fn test_explain_fail() {
let data = b"SELECT * FROM table";
let mut tokenizer = Tokenizer::new(data);
let result = tokenizer.visit::<Explain>();
// non reconnu
assert!(result.is_err());
}On réalise également un utilitaire qui reconnaßt directement notre modificateur.
pub fn detect_explain(scanner: &str) -> Option<usize> {
let mut scanner = Scanner::new(scanner.as_bytes());
scanner.visit::<Explain>().ok().map(|Explain(size)| size)
}Propagation du EXPLAIN
Une fois le modificateur EXPLAIN dĂ©tectĂ©e ou non, il faut le propager dans lâexĂ©cution.
Etant donné que le modificateur induit un comportement différent, nous allons avoir deux types de retours:
- des résultat de commandes
- le résultat du
EXPLAIN.
enum ExecuteResult {
// la commande ne renvoie pas de réponse
Nil,
// la commande renvoie une liste de tuples
Tuples(Vec<Vec<Value>>),
// le résultat de EXPLAIN
Explain(Vec<String>),
}EXPLAIN va retourner une liste de dĂ©tails. DâoĂč le
Vec<String>
Pour permettre Ă toutes les commandes de bĂ©nĂ©ficier du EXPLAIN, la dĂ©tection est rĂ©alisĂ©e avant le parse de la commande elle-mĂȘme.
impl Database {
pub fn run(&mut self, command: &str) -> Result<ExecuteResult, ExecutionError> {
// si la table master n'existe pas, la créer
if !self.tables.contains_key(MASTER_TABLE_NAME) {
let schema = "(type TEXT(10), name TEXT(50), tbl_name TEXT(50), sql TEXT(300), PRIMARY KEY (type, name, tbl_name));";
// récupÚre le schéma
let schema = schema_from_str(schema).map_err(ExecutionError::Parse)?;
// création de la table master
let master_table = Table::new(schema, MASTER_TABLE_NAME.to_string());
// insertion de la table master à la base de données
self.tables
.insert(MASTER_TABLE_NAME.to_string(), master_table);
}
// active ou non le mode de "debug"
let mut explain = false;
// décale le curseur de début de parse
let mut cursor = 0;
// détection du potentiel modificateur EXPLAIN
if let Some(explain_size) = detect_explain(command) {
// si le modificateur existe, décaler le curseur d'autant que reconnu
cursor = explain_size;
// activer le flag de explain
explain = true;
}
parse(&command[cursor..])
.map_err(ExecutionError::Command)?
.execute(self, explain)
}
}On modifie le trait Execute en rajoutant un flag boolĂ©en qui permet de passer lâexĂ©cution en âdry runâ de debug ou non.
trait Execute {
fn execute(
self,
database: &mut Database,
explain: bool,
) -> Result<ExecuteResult, ExecutionError>;
}Le flag est propagĂ©, jusquâĂ atteindre la base de donnĂ©es et sa mĂ©thode select.
impl Database {
pub fn select(
&mut self,
table_name: String,
where_clause: Option<WhereClause>,
_explain: bool,
) -> Result<ExecuteResult, SelectError> {
match self.tables.get(&table_name) {
Some(table) => table.select(where_clause),
None => Err(SelectError::TableNotExist(table_name))?,
}
}
}Pour le moment, nous nâen ferons rien, mais ce nâest quâune question de temps. đ
Conclusion
Lâarticle est court, mais va permettre de construire la suite plus simplement.
Dans la prochaine partie, nous allons attaquer le concept fondamental de la base de donnĂ©es quâest le logical plan.
Merci de votre lecture â€ïž
