Partie 19: Suppression d'enregistrements
Lecture 10 min. âą
Table des matiĂšres
- Page header
- Renvoyer les row ID lors du scan
- Command DELETE FROM
- Implémentation de la commande DELETE
- On peut tester
- 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 đ
Lors de la partie précédente, nous avons enfin atteint le stade auquel nous sommes en mesure de rechercher des enregistrements en nous basant sur des index.
Nous allons réutiliser ces index et le plan logique pour supprimer des enregistrements.
Mais avant cela, nous allons devoir modifier le format de stockage de notre base de données pour indiquer quels enregistrements sont supprimés.
Nous allons également devoir modifier le scanner pour renvoyer le rowid des enregistrements à supprimer.
Cet article sera plus simple que le prĂ©cĂ©dent, mais il y a quelques petites choses Ă faire tout de mĂȘme.
TrĂȘve de palabres, câest parti ! đ
Page header
Théorie
Pour le moment nos pages de stockage ont le layout suivant:
null_table tuple null_table tuple null_table tuple null_table tuple
null_table tuple null_table tuple null_table tuple null_table tuple
null_table tuple null_table tuplenull_table tuple null_table tuple
null_table tuple null_table tuple null_table tuple paddingChaque enregistrement est prĂ©fixĂ© par un tableau de bits qui indique quels champs du tuple sont NULL. Nous lâavons appelĂ© null_table. Puis le tuple en lui-meme.
Et tout de suite aprĂšs une autre null_table du tuple suivant, puis le tuple suivant et ainsi de suite.
La page se termine par un padding permettant à nos enregistrements de rester aligné sur une seule page.
Nous allons modifier le format de stockage de notre base de données pour indiquer quels enregistrements sont supprimés.
Pour cela, nous allons rajouter un header permettant de définir les métadonnées de la page.
Le nouveau format de page devient:
header null_table tuple null_table tuple null_table tuple
null_table tuple null_table tuple null_table tuple null_table tuple
null_table tuple null_table tuple null_table tuple null_table tuple
null_table tuple null_table tuple null_table tuple paddingCe header lui-mĂȘme contient une null_table qui indique quels offsets de la page doivent ĂȘtre ignorĂ©s.
Nos null_table sont alignĂ©es sur un octet, donc 8 bits au minimum. Si cela nâest pas suffisant, nous ajoutons des octets pour le reste.
Pour une page contenant 18 enregistrements, nous aurons un header de 3 octets, car 3 * 8 = 24 bits.
0000 0000 0000 0000 0000 0000Pour supprimer le premier enregistrement, il faut modifier le premier bit du premier octet du header.
0000 0001 0000 0000 0000 0000
âPour supprimer le quatriĂšme enregistrement, il faut modifier le quatriĂšme bit du premier octet du header.
0000 1000 0000 0000 0000 0000
âPour supprimer le 18e enregistrement, il faut modifier le 2e bit du 3e octet du header.
0000 0000 0000 0000 0000 0010
âJâexplique les techniques pour lire les bits dans la partie 14.
En résumé, pour vérifier si un enregistrement est supprimé, on donne son index dans la page et on regarde si le bit correspondant est 1 ou 0.
Pour cela, on décale le bit qui nous intéresse et on le compare avec 0b1.
Si ce bit vaut 0b1, alors le ET logique renvoie 0b1 et la condition est true. Le bit est 1, donc lâenregistrement est supprimĂ©.
Si ce bit vaut 0b0, alors le ET logique renvoie 0b0 et la condition est false. Le bit est 0, donc lâenregistrement nâest pas supprimĂ©.
partition = index / 8
bit = index % 8
null_table[partition] >> bit & 0b1 == 0b1 Manipulation du header de la page
Nous allons crĂ©er deux fonctions permettant de vĂ©rifier les Ă©tats dâenregistrement et de modifier lâen-tĂȘte.
pub const PARTITION_SIZE: usize = 8;
/// Check if a bit in the page header is 1
pub fn is_record_deleted(header: &[u8], index: usize) -> bool {
let partition = index / PARTITION_SIZE;
let bit = index % PARTITION_SIZE;
(header[partition] >> bit) & 0b1 == 0b1
}
/// Set a bit in the page header
pub fn set_record_deleted(header: &mut [u8], index: usize) {
let partition = index / PARTITION_SIZE;
let bit = index % PARTITION_SIZE;
header[partition] |= 0b1 << bit;
}Les méthodes sont utilisées comme suit:
#[test]
fn test_delete_then_check() {
// init empty header
let mut header = [0b0000_0000, 0b0000_0000];
// delete first record
set_record_deleted(&mut header, 0);
// check if first record is deleted
assert!(is_record_deleted(&header, 0));
// delete eleventh record
set_record_deleted(&mut header, 11);
// check if eleventh record is deleted
assert!(is_record_deleted(&header, 11));
// check final header state
assert_eq!(header, [0b0000_0001, 0b0000_1000]);
}La rĂ©fĂ©rence mutable &mut [u8] permet de modifier le header en fonction des enregistrements que lâon dĂ©sire supprimer.
Il est Ă remarquer que les bits aprĂšs le 18e (de 19 Ă 24) ne sont pas considĂ©rĂ©s comme des enregistrements et ne sont lĂ que pour respecter lâalignement sur un octet.
Association du header Ă la page
Maintenant que nous avons notre header rudimentaire, nous allons le lier aux pages.
Pour cela, il va falloir faire un peu de gymnastique mathématique.
Nous connaissons la taille dâun enregistrement, qui est dĂ©finie par le schĂ©ma de la table.
Nous connaissons la taille de la page, qui est fixe et qui vaut 4096 octets.
Nous sommes donc capables de calculer le nombre dâenregistrements par page.
$$ \text{nb of records per page} = \frac{\text{page size}}{\text{record size}} $$
Nous avons besoin dâune null_table qui accueille au moins le nombre dâenregistrement par page.
Les null_table sont des multiples de 8bits. Nous avons besoin de 1 bit par enregistrement.
$$ \text{null table size} = \left \lceil \frac{\text{nb of records per page}}{8} \right \rceil $$
Nous utilisons ici le ceil pour ĂȘtre certains dâavoir au moins 1 bit par enregistrement.
Si nous faisions la division entiĂšre pour 18 enregistrements, cela donnerait :
$$ \frac{\text{18}}{8} = 2.25 $$
Ce qui est effectivement ce que lâon sâaperçoit dans lâoccupation de la null_table. Mais le souci câest que la division entiĂšre nous donnerait seulement 2 octets pour 18 enregistrements.
Lâutilisation de ceil nous permet de prendre le plus grand entier qui est infĂ©rieur ou Ă©gal Ă notre division. Ce qui nous donne 3 octets. Avec 3 * 8 = 24 bits, nous avons suffisamment de place pour 18 enregistrements.
Une fois que nous avons la taille de notre null_table, nous pouvons la retrancher Ă la taille de la page.
$$ \text{page usable space} = \text{page size} - \text{null table size} $$
Ensuite, nous recalculons le nombre dâenregistrements par page afin dâĂ©viter tout dĂ©salignement des enregistrements suite Ă lâintroduction de lâen-tĂȘte.
$$ \text{nb of records per page computed} = \frac{\text{page usable space}}{\text{record size}} $$
Et on recalcule la taille de la page utilisable pour les enregistrements.
$$ \text{real page usable space} = \text{nb of records per page computed} * \text{record size} $$
Cette opération peut nous donner un
$$\text{nb of records per page computed} < \text{nb of records per page computed}$$
et donc potentiellement un octet de moins nĂ©cessaire pour la null_table, mais câest un sacrifice sur 4 Ko de page qui est acceptable.
Quoi quâil en soit, la taille de la page que lâon utilisera pour les calculs de pages de stockage et dâoffsets dĂ©sormais sera $\text{real page usable space}$.
impl Pager {
/// Size of a physical page
pub const PAGE_SIZE: usize = 4096;
pub fn new(row_size: usize) -> Self {
// Calculate the maximum number of rows that can fit in a page
let max_row_per_page = PAGE_SIZE / row_size;
// Calculate the size of the page header based on partitions
let page_header = max_row_per_page.div_ceil(PARTITION_SIZE);
// Determine the usable space in the page by subtracting the header size from the total page size
let usable_space = PAGE_SIZE - page_header;
// Calculate the number of rows that can fit in the usable space
let available_rows = usable_space / row_size;
// Calculate the total size of the page based on the available rows and row size
let page_size = available_rows * row_size;
Pager {
pages: BTreeMap::default(),
row_size,
page_size,
header_size: page_header,
}
}
}Nous avons dĂ©sormais un emplacement rĂ©servĂ© dans la page qui ne peut plus ĂȘtre occupĂ© par un enregistrement. Nous pouvons donc lâutiliser comme header.
Récupération du header dans la page
Maintenant que nous avons un emplacement pour notre header, il va sâagir de le positionner dans la page.
Jâai dĂ©cidĂ© de le mettre en haut de la page. Ce qui signifie en octet 0. Nous connaissons sa taille, nous pouvons donc rĂ©cupĂ©rer la slice dâoctets correspondants.
Pour cela, nous allons modifier les fonctions get et get_mut qui précédemment ne permettaient que de récupérer les octets aprÚs un certain offset.
Or, nous nous devons de récupérer le header compris entre les octets 0 et header_size.
Pour cela nous allons utiliser le trait SliceIndex qui permet de prendre un sous ensemble de slice.
impl Page {
pub fn get_mut<S: SliceIndex<[u8], Output = [u8]>>(&mut self, range: S) -> Option<&mut [u8]> {
self.inner.get_mut(range)
}
pub fn get<S: SliceIndex<[u8], Output = [u8]>>(&self, range: S) -> Option<&[u8]> {
self.inner.get(range)
}
}Une fois cela fait, on peut définir les méthodes pour récupérer le header.
Le header sera récupéré par row ID, car nous voulons déterminer si un enregistrement est supprimé ou non ou le supprimer.
Pour cela, nous réutilisons la méthode get_position qui renvoie une Position que nous avons définie précédemment.
Celui-ci nous donne alors la page concernĂ©e par lâenregistrement.
On récupÚre ensuite la page et on extrait le header.
Comme notre header est de taille header_size et quâil dĂ©bute en octet 0, la slice est donc ..self.header_size.
Nous venons de virtuellement positionner notre header en octet 0.
impl Pager {
/// Gets the header of the record at the given a position
fn get_header(&self, position: &Position) -> Result<&[u8], PageError> {
let Position { page_number, .. } = position;
let page = self
.pages
.get(page_number)
.ok_or(PageError::PageNotFound(*page_number))?;
page.get(..self.header_size)
.ok_or(PageError::PageSliceRangeTo(..self.header_size))
}
/// Gets the mutable header of the record at the given position
fn get_mut_header(&mut self, position: &Position) -> Result<&mut [u8], PageError> {
let Position { page_number, .. } = position;
let page = self
.pages
.get_mut(page_number)
.ok_or(PageError::PageNotFound(*page_number))?;
page.get_mut(..self.header_size)
.ok_or(PageError::PageSliceRangeTo(..self.header_size))
}
}Mais du coup, tous les calculs de position et dâoffsets des enregistrements doivent prendre en compte la place que prend le header et donc dĂ©caler dâautant.
Et câest pour cela que nous rajoutons un self.header_size aux offsets.
impl Pager {
fn get_position(&self, row_number: usize) -> Position {
// calcul de l'index global
let global_index = row_number * self.row_size;
// calcul la page contenant le record
let page_number = global_index / self.page_size;
// calcul l'offset de la valeur dans cette page
let offset = (global_index - page_number * self.page_size) + self.header_size;
Position {
page_number,
offset,
}
}
}DĂ©sormais, le header est devenu invisible pour les calculs de pages et dâoffsets qui conditionnent lâaccĂšs aux enregistrements.
Nous avons bien notre layout de page souhaité.
header
record
record
record
...
paddingOn en profite pour dĂ©finir deux mĂ©thodes, lâune pour supprimer un enregistrement et lâautre pour savoir si un enregistrement est supprimĂ©.
impl Pager {
/// Check if the record at the given row number is deleted.
/// returns None if the record does not exist
/// returns Some(true) if the record is deleted
/// returns Some(false) if the record is not deleted
pub fn check_if_record_deleted(&self, row_number: usize) -> Result<bool, PageError> {
let positon = self.get_position(row_number);
let index = positon.offset / self.row_size;
let header = self.get_header(&positon)?;
Ok(data::page_header::is_record_deleted(header, index))
}
/// Set a bit in the page header
/// flags the record as deleted
pub fn set_record_deleted(&mut self, row_number: usize) -> Result<(), PageError> {
let position = self.get_position(row_number);
let index = position.offset / self.row_size;
let header = self.get_mut_header(&position)?;
data::page_header::set_record_deleted(header, index);
Ok(())
}
}Pas mal, pas mal, on avance ! đ
Renvoyer les row ID lors du scan
Pour le moment, nos méthodes de scans ne renvoient que le tuple du record sans son row ID associé.
Or, pour rĂ©cupĂ©rer le header de la page concernant lâenregistrement Ă supprimer, nous avons besoin de son row ID.
Nous allons donc modifier le retour de nos itérateurs de scans.
Nous définissons un type alias pour le tuple de row ID et de valeurs.
type Row = (RowId, Vec<Value>);Puis nous modifions nos itérateurs.
impl Iterator for IndexIterator<'_> {
type Item = Result<Row, SelectError>;
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.row_ids.next() {
None => break None,
Some(row_id) => {
let row = self.query_engine.get_row(*row_id);
match row {
Ok(Some(row)) => break Some(Ok((*row_id, row))),
Ok(None) => continue,
Err(err) => break Some(Err(err)),
}
}
}
}
}
}
impl Iterator for Scanner<'_> {
type Item = Result<Row, SelectError>;
fn next(&mut self) -> Option<Self::Item> {
loop {
// on va jusqu'au dernier tuple de la table
if self.current_row >= self.query_engine.get_table().row_number {
return None;
}
let row = self.query_engine.get_row(self.current_row);
self.current_row += 1;
match row {
Ok(Some(row)) => break Some(Ok((self.current_row - 1, row))),
// on ignore les tuples supprimés
Ok(None) => continue,
Err(err) => break Some(Err(err)),
}
}
}
}Désormais nos itérateurs produiront des couples RowId et Vec<Value>.
(0, [1, 2, 3])
(1, [4, 5, 6])
(2, [7, 8, 9])Le premier tuple correspond au row ID 0, le deuxieme au 1 et ainsi de suite.
De fait, nos itérateurs de plan se voient également modifiés.
type PlanExecInput<'a> = Box<dyn Iterator<Item = Result<Row, SelectError>> + 'a>;
/// The output of the step of the plan executed
pub type PlanExecOutput<'a> =
Result<Box<dyn Iterator<Item = Result<Row, SelectError>> + 'a>, SelectError>;Un SELECT produira désormais ceci:
SELECT * FROM Client;
0 => [Integer(1), Text("Smith"), Text("John"), Text("M"), Text("Paris")]
1 => [Integer(2), Text("Martin"), Text("Marie"), Text("F"), Text("New York")]
2 => [Integer(3), Text("Haddad"), Text("Karim (Ù۱ÙÙ
)"), Text("M"), Text("Tokyo")]
3 => [Integer(4), Text("Dubois"), Text("Sophie"), Text("F"), Text("Beyrouth")]
4 => [Integer(5), Text("Tanaka"), Text("Hiroshi (ăČăă)"), Text("M"), Text("Beyrouth")]
5 => [Integer(6), Text("Yamamoto"), Text("Sakura (ăăă)"), Text("F"), Text("Paris")]
6 => [Integer(7), Text("Smith"), Text("Emily"), Text("F"), Text("Osaka")]
7 => [Integer(8), Text("Martin"), Text("Jean"), Text("M"), Text("Lyon")]
8 => [Integer(9), Text("Haddad"), Text("Layla (ÙÙÙÙ)"), Text("F"), Text("New York")]
9 => [Integer(10), Text("Dubois"), Text("Paul"), Text("M"), Text("Tokyo")]
Command DELETE FROM
Maintenant que le stockage pour la suppression est réglé, nous pouvons ajouter la commande DELETE FROM.
DELETE FROM table_name [WHERE condition];Sa grammaire est trĂšs similaire Ă celle du SELECT, exceptĂ© quâil nây a pas de projection.
On modélise la commande comme suit:
#[derive(Debug, PartialEq)]
pub struct DeleteFromCommand {
pub table_name: String,
pub where_clause: Option<WhereClause>,
}Que lâon parse de cette maniere:
impl<'a> Visitable<'a, u8> for DeleteFromCommand {
fn accept(scanner: &mut Tokenizer<'a>) -> parser::Result<Self> {
// on nettoie les potentiels espaces
scanner.visit::<OptionalWhitespaces>()?;
recognize(Token::Delete, scanner)?;
// on reconnait au moins un espace
scanner.visit::<Whitespaces>()?;
// 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, celui-ci peut soit se terminer par un blanc
// alors un nouveau groupe commence un WHERE par exemple
// soit se terminer par un point-virgule, la fin de la commande
let table_name_tokens = Forecaster::new(scanner)
.add_forecastable(UntilToken(Token::Whitespace))
.add_forecastable(UntilToken(Token::Semicolon))
.forecast()?
.ok_or(ParseError::UnexpectedToken)?;
let table_name =
String::from_utf8(table_name_tokens.data.to_vec()).map_err(ParseError::Utf8Error)?;
scanner.bump_by(table_name_tokens.data.len());
// on nettoie les potentiels espaces
scanner.visit::<OptionalWhitespaces>()?;
let where_clause = scanner.visit::<Optional<WhereClause>>()?.0;
// on reconnait le point virgule terminal
recognize(Token::Semicolon, scanner)?;
Ok(DeleteFromCommand {
table_name,
where_clause,
})
}
}On peut ensuite implémenter la commande au set de commandes connues.
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)))?
.try_or(|command| Ok(Command::CreateIndex(command)))?
.try_or(|command| Ok(Command::DeleteFrom(command)))? // <-- ici
.finish()
.ok_or(ParseError::UnexpectedToken)
}
}Et finalement ajouter la commande elle-meme.
pub enum Command {
CreateTable(create_table::CreateTableCommand),
Select(select::SelectCommand),
InsertInto(insert_into::InsertIntoCommand),
CreateIndex(create_index::CreateIndexCommand),
DeleteFrom(delete_from::DeleteFromCommand), // <-- ici
}Une fois cela fait on peut définir son exécution.
impl Execute for DeleteFromCommand {
fn execute(
self,
database: &mut Database,
explain: bool,
) -> Result<ExecuteResult, ExecutionError> {
let DeleteFromCommand {
table_name,
where_clause,
} = self;
database
.delete(&table_name, where_clause, explain)
.map_err(ExecutionError::Delete)
}
}
impl Execute for Command {
fn execute(
self,
database: &mut Database,
explain: bool,
) -> Result<ExecuteResult, ExecutionError> {
match self {
Command::CreateTable(command) => command.execute(database, explain),
Command::Select(command) => command.execute(database, explain),
Command::InsertInto(command) => command.execute(database, explain),
Command::CreateIndex(command) => command.execute(database, explain),
Command::DeleteFrom(command) => command.execute(database, explain), // <-- ici
}
}
}Implémentation de la commande DELETE
Maintenant que nous sommes capables de reconnaĂźtre la commande, il faut quâelle interagisse rĂ©ellement avec la base de donnĂ©es.
Pour implémenter la commande DELETE, nous allons réutiliser la fonction select de la base de données.
En effet, celle-ci gÚre toute la mécanique de gestion du plan de recherche optimisé par index.
En premiĂšre intention, je souhaitais utiliser lâitĂ©rateur du scan sans le collecter, et supprimer au fil de lâeau les enregistrements.
Mais le problĂšme est que lâitĂ©rateur est une rĂ©fĂ©rence non mutable sur la base de donnĂ©es. Et que supprimer un enregistrement nĂ©cessite de modifier la base de donnĂ©es.
Or en Rust, il nâest pas possible dâavoir une rĂ©fĂ©rence mutable et non mutable en mĂȘme temps sur la mĂȘme variable.
Donc je me rabats sur une solution moins optimale, mais qui fonctionne. Scanner quelques enregistrements consomme lâitĂ©rateur et le collecte sous forme de vecteur de row ID.
Ainsi, je relĂąche ma rĂ©fĂ©rence non mutable sur la base de donnĂ©es, ce qui me permets dâen dĂ©clarer une mutable sur laquelle je peux modifier la base de donnĂ©es.
// maximum number of rows to scan
const MAX_SCAN_LENGTH: usize = 1000;
impl Database {
fn collect_row_ids(
&self,
table_name: &str,
where_clause: &Option<WhereClause>,
limit: Option<usize>,
) -> Result<Vec<usize>, SelectError> {
let rows = self.select(table_name, where_clause, false)?;
let ExecuteResult::Tuples(rows) = rows else {
unreachable!("Explain mode is disabled");
};
// limit the number of rows to scan
// by default, we scan at most 1000 rows
// if the limit is not set, we scan at most 1000 rows
let limit = max(limit.unwrap_or(MAX_SCAN_LENGTH), MAX_SCAN_LENGTH);
let rows_id = rows
.map(|row| row.map(|row| row.0))
.take(limit)
.collect::<Result<Vec<usize>, SelectError>>()?;
Ok(rows_id)
}
}Pour supprimer tous les enregistrements concernĂ©s par la commande DELETE, nous allons appeler la fonction collect_row_ids en boucle jusquâĂ ce quâil nây ait plus de rĂ©sultats dans lâitĂ©rateur.
Le retour de notre appel de suppression sera le nombre de lignes supprimées.
On ajoute dans notre énumération ExecuteResult une variante pour le nombre de lignes affectées.
pub enum ExecuteResult<'a> {
Nil,
Tuples(Box<dyn Iterator<Item = Result<Row, SelectError>> + 'a>),
Explain(Plan),
AffectedRows(usize),
}On en profite également pour modifier la variante Tuples pour lui faire renvoyer les row ID associés aux tuples.
// maximum number of rows to scan by default
const DEFAULT_SCAN_LENGTH: usize = 100;
impl Database {
pub fn delete(
&mut self,
table_name: &String,
where_clause: Option<WhereClause>,
explain: bool,
) -> Result<ExecuteResult, DeletionError> {
// si on est en mode explain
if explain {
return self
.select(table_name, &where_clause, true)
.map_err(DeletionError::SelectionError);
}
// on définit un compteur de lignes supprimées
let mut total_deleted = 0;
// tant qu'il reste des lignes Ă supprimer
loop {
// on collecte les ids des lignes Ă supprimer
let rows_id = self
.collect_row_ids(table_name, &where_clause, Some(DEFAULT_SCAN_LENGTH))
.map_err(DeletionError::SelectionError)?;
// si il n'y a plus de lignes Ă supprimer
if rows_id.is_empty() {
break;
}
// on incrémente le compteur
total_deleted += rows_id.len();
// on récupÚre une reference mutable sur la table
let table = self
.tables
.get_mut(table_name)
.ok_or(DeletionError::TableNotExist(table_name.to_string()))?;
// on supprime les lignes
for row_id in rows_id {
table.delete(row_id)?;
}
}
Ok(ExecuteResult::AffectedRows(total_deleted))
}
}La suppression en tant sur telle dans la table est un appel Ă la fonction set_record_deleted du pager de la table.
impl Table {
pub fn delete(&mut self, row_number: usize) -> Result<(), DeletionError> {
self.pager
.set_record_deleted(row_number)
.map_err(DeletionError::Page)?;
Ok(())
}
}Et voilĂ câest terminĂ© ! đ
On peut tester
CREATE TABLE Client (
id INTEGER PRIMARY KEY,
nom TEXT(50),
prénom Text(50),
genre TEXT(2),
ville Text(100)
);
CREATE UNIQUE INDEX idx_identité ON Client(nom, prénom);
CREATE INDEX idx_ville ON Client(ville);INSERT INTO Client (id, nom, prénom, genre, ville) VALUES (1, 'Smith', 'John', 'M', 'Paris');
INSERT INTO Client (id, nom, prénom, genre, ville) VALUES (2, 'Martin', 'Marie', 'F', 'New York');
INSERT INTO Client (id, nom, prĂ©nom, genre, ville) VALUES (3, 'Haddad', 'Karim (Ù۱ÙÙ
)', 'M', 'Tokyo');
INSERT INTO Client (id, nom, prénom, genre, ville) VALUES (4, 'Dubois', 'Sophie', 'F', 'Beyrouth');
INSERT INTO Client (id, nom, prĂ©nom, genre, ville) VALUES (5, 'Tanaka', 'Hiroshi (ăČăă)', 'M', 'Beyrouth');
INSERT INTO Client (id, nom, prĂ©nom, genre, ville) VALUES (6, 'Yamamoto', 'Sakura (ăăă)', 'F', 'Paris');
INSERT INTO Client (id, nom, prénom, genre, ville) VALUES (7, 'Smith', 'Emily', 'F', 'Osaka');
INSERT INTO Client (id, nom, prénom, genre, ville) VALUES (8, 'Martin', 'Jean', 'M', 'Lyon');
INSERT INTO Client (id, nom, prĂ©nom, genre, ville) VALUES (9, 'Haddad', 'Layla (ÙÙÙÙ)', 'F', 'New York');
INSERT INTO Client (id, nom, prénom, genre, ville) VALUES (10, 'Dubois', 'Paul', 'M', 'Tokyo');Si on liste nos clients, nous avons 10 lignes. Avec le row ID correspondant.
db > SELECT * FROM Client;
0 => [Integer(1), Text("Smith"), Text("John"), Text("M"), Text("Paris")]
1 => [Integer(2), Text("Martin"), Text("Marie"), Text("F"), Text("New York")]
2 => [Integer(3), Text("Haddad"), Text("Karim (Ù۱ÙÙ
)"), Text("M"), Text("Tokyo")]
3 => [Integer(4), Text("Dubois"), Text("Sophie"), Text("F"), Text("Beyrouth")]
4 => [Integer(5), Text("Tanaka"), Text("Hiroshi (ăČăă)"), Text("M"), Text("Beyrouth")]
5 => [Integer(6), Text("Yamamoto"), Text("Sakura (ăăă)"), Text("F"), Text("Paris")]
6 => [Integer(7), Text("Smith"), Text("Emily"), Text("F"), Text("Osaka")]
7 => [Integer(8), Text("Martin"), Text("Jean"), Text("M"), Text("Lyon")]
8 => [Integer(9), Text("Haddad"), Text("Layla (ÙÙÙÙ)"), Text("F"), Text("New York")]
9 => [Integer(10), Text("Dubois"), Text("Paul"), Text("M"), Text("Tokyo")]Supprimons donc tous les clients Homme de la table. Nous avons Ă supprimer 5 lignes.
db > DELETE FROM Client WHERE genre = 'M';
5 rows affectedLe systÚme nous renvoie que 5 lignes ont été affectées.
On full scan la table.
db > SELECT * FROM Client;
1 => [Integer(2), Text("Martin"), Text("Marie"), Text("F"), Text("New York")]
3 => [Integer(4), Text("Dubois"), Text("Sophie"), Text("F"), Text("Beyrouth")]
5 => [Integer(6), Text("Yamamoto"), Text("Sakura (ăăă)"), Text("F"), Text("Paris")]
6 => [Integer(7), Text("Smith"), Text("Emily"), Text("F"), Text("Osaka")]
8 => [Integer(9), Text("Haddad"), Text("Layla (ÙÙÙÙ)"), Text("F"), Text("New York")]Il nây effectivement plus que des femmes.
Si on essaie de supprimer lâID 8, le systĂšme nous dit quâaucune ligne nâa Ă©tĂ© affectĂ©e.
db > DELETE FROM Client WHERE id = 8;
0 rows affectedPar contre si on supprime lâID 9, le systĂšme nous dit que 1 ligne a Ă©tĂ© affectĂ©e.
db > DELETE FROM Client WHERE id = 9;
1 rows affectedUn derniĂšre SELECT nous assure que le row ID 9 nâa plus de ligne.
db > SELECT * FROM Client;
1 => [Integer(2), Text("Martin"), Text("Marie"), Text("F"), Text("New York")]
3 => [Integer(4), Text("Dubois"), Text("Sophie"), Text("F"), Text("Beyrouth")]
5 => [Integer(6), Text("Yamamoto"), Text("Sakura (ăăă)"), Text("F"), Text("Paris")]
6 => [Integer(7), Text("Smith"), Text("Emily"), Text("F"), Text("Osaka")]Et bien Ă©videmment tout ces requĂȘtes sont dĂ©buggables.
db > EXPLAIN DELETE FROM Client WHERE genre = 'M';
Full scan Client
Filter: genre = 'M'
db > EXPLAIN DELETE FROM Client WHERE id = 9;
Scan index PRIMARY : id = 9 for table Client
Filter: id = 9
db > EXPLAIN DELETE FROM Client WHERE ville = "Paris";
Scan index idx_ville : ville = 'Paris' for table Client
Filter: ville = 'Paris'Nous avons réutiliser les index pour supprimer des lignes.
Notre base de données commence à avoir une certaine tenue. ^^
Conclusion
En réutilisant le plan logique de la partie 17 et en nous appuyant sur le travail sur les index de la partie 18, nous avons pu efficacement supprimer nos enregistrements de la base de données.
En réutilisant le principe de la null table, nous avons réalisé un raccourci sur les données de chaque page, évitant ainsi de désérialiser des enregistrements avant de nous rendre compte que ceux-ci sont supprimés.
Petit à petit, notre base de données utilise ses propres primitives pour construire des comportements plus complexes.
Dans la prochaine partie, nous allons définir le processus permettant de nettoyer nos pages et nos index.
Merci de votre lecture â€ïž
