Partie 10 : Clefs primaires
Lecture 57 min. âą
Table des matiĂšres
- Modifiation du parser
- Indexation des entrées
- Query Engine
- Utilisation du Query Engine
- Testons !!
- 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 đ
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 ::= ';'
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 ::= ';'
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:
- PRIMARY
- KEY
- NOT
- NULL
- WHERE
- =
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:
- soit câest un groupe de containtes sur un champ du schĂ©ma qui nâest pas le dernier champ du schĂ©ma : ce champ se termine par une virgule
, - soit le groupe de contraintes est sur le dernier champ du schéma : ce champ se termine par rien du tout
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.
- au moins un des champs doit-ĂȘtre une clef primaire
- il ne peut pas y avoir plus dâun champ comme clef primaire dâune table
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.
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)
}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 â€ïž
