Le contexte du projet est une base de donnĂ©es gĂ©rĂ©e par votre programme. La base de donnĂ©es est constituĂ©e de plusieurs tables, chacune gĂ©rĂ©e par des fichiers portant le nom de la table gĂ©rĂ©e et une extension en fonction du rĂŽle du fichier (structure, index, contenu). La base de donnĂ©es est dans un rĂ©pertoire portant son nom, et chaque ensemble de fichiers d'une table est dans un sous-rĂ©pertoire du mĂȘme nom, contenu par le rĂ©pertoire de la base de donnĂ©es elle-mĂȘme.
Le programme qui gÚre la base de données gÚre les arguments suivants :
-d
suivi d'un nom, le nom de la base de données à ouvrir/créer-l
suivi d'un chemin, le chemin vers le répertoire parent de la base de données (i.e. le répertoire qui contient celui de la base de données)
Pour plus de clarté, le programme est décomposé en plusieurs parties correspondant chacune à un aspect de la gestion de la base de données.
Les sections suivantes définissent les différents éléments du projet, qui seront les critÚres d'évaluation du projet. Ce document est actuellement incomplet et sera mis à jour dans les jours à venir.
Une base de données est un outil permettant la gestion de données structurées ou non. Dans ce projet, nous nous intéressons à créer une base de données simplifiée manipulée par un sous-ensemble de SQL.
Pour vous permettre de mener à bien ce projet, vous devez comprendre comment les données seront stockées par la base de données, quelles sont les commandes SQL que vous devrez implémenter, et comment faire l'interface entre le contenu de la base de données sur votre support de stockage et le résultat du parsing SQL.
Des fichiers avec les définitions du code vous sont fournis avec ce README afin de garantir que la structure de votre programme est correcte.
Le programme que vous devez réaliser fournira sous une forme simplifiée les moyens d'interagir avec une base de données. L'interface de l'utilisateur avec la base de données étant faite avec une variante du langage SQL, vous serez amenés à implémenter les 4 étapes suivantes :
- Parse : cette Ă©tape consiste Ă parcourir la requĂȘte SQL sous forme de texte, pour en extraire les diffĂ©rents Ă©lĂ©ments et les stocker dans une reprĂ©sentation en mĂ©moire (sous forme de structures de donnĂ©es). Cette Ă©tape s'assure que la syntaxe de la requĂȘte est correcte.
- Check : cette Ă©tape consiste Ă vĂ©rifier que la sĂ©mantique de la requĂȘte est correcte (tables et champs qui existent, types des donnĂ©es, etc.)
- Expand : cette Ă©tape complĂšte les Ă©ventuelles donnĂ©es manquantes dans la requĂȘte (par exemple : transformation du caractĂšre
*
en l'ensemble des champs de la table, etc.) - Execute : la requĂȘte, maintenant analysĂ©e, vĂ©rifiĂ©e et complĂšte, peut ĂȘtre exĂ©cutĂ©e. Il s'agit Ă cette Ă©tape de lire/Ă©crire les donnĂ©es nĂ©cessaires sur le support de stockage contenant la base de donnĂ©es.
Pour les besoins de ce projet, nous considérons qu'une base de données est stockée dans un répertoire qui lui est propre et qui porte son nom, i.e. la base de données inventaire
est stockée dans un répertoire nommé "inventaire"
. Une base de donnĂ©es est constituĂ©e de tables, qui sont des ensembles structurĂ©s de donnĂ©es de mĂȘme nature. Dans le rĂ©pertoire de la base de donnĂ©es, chaque table dispose de son propre rĂ©pertoire. Ce rĂ©pertoire de table contient 3 ou 4 fichiers relatifs Ă cette table : le fichier de dĂ©finition, le fichier d'index, le fichier de contenu et le cas Ă©chĂ©ant, le fichier de clĂ©.
Ce fichier contient la description des champs de la table. La liste des champs est définie par un champ par ligne avec son type, défini par l'énumération field_type_t
(c.f. la définition des types utilisés par le programme). Ce fichier permet de connaßtre l'ordre et le type des champs stockés par la table.
Une ligne définissant un champ est écrite de la maniÚre suivante : N nom
oĂč N
est le numéro de type de champ issu de l'énumération field_type_t
, et nom
est le nom du champ.
Le fichier d'index est composé d'enregistrements de taille fixe. Chaque enregistrement est composé de 3 champs :
Active | Offset | Length |
---|
dont voici la signification :
- active : ce champ est défini sur un octet (type
uint8_t
). Il a pour valeur zĂ©ro si cet index n'est pas actif (il peut donc ĂȘtre rĂ©utilisĂ©), et une valeur diffĂ©rente de zĂ©ro dans le cas contraire. Une valeur de zĂ©ro signifie que le contenu correspondant doit ĂȘtre ignorĂ© en lecture et peut ĂȘtre rĂ©utilisĂ© en Ă©criture. - offset: ce champ est dĂ©fini sur 4 octets (type
uint32_t
). Il définit la position de ce champ dans le fichier de contenu. Il définit la position en octets à partir du début du fichier. - length: ce champ est défini sur 2 octets (type
uint16_t
). Il dĂ©finit la taille de l'enregistrement courant. Cette taille sera toujours la mĂȘme pour une table donnĂ©e (donc chaque ligne d'index a une valeur length identique, pour simplifier l'accĂšs aux donnĂ©es)
Ce fichier est utile pour accéder aux enregistrements de la table.
Ce fichier contient les données de la table. Chaque enregistrement est stocké dans l'ordre de la définition de la table (obtenue en lisant son fichier de définition). Les données de type float
, int
et primary key
sont stockées sous leur forme binaire. Les chaßnes de caractÚres sont stockées intégralement (il s'agit de tableaux de longueur fixe). Tous les enregistrements d'une table ont donc une longueur identique.
Par exemple, avec les valeurs données ci dessous (longueur des chaßnes de caractÚres égale à 150), les enregistrements d'une table définie par un int
(type SQL), un float
et deux text
auront une taille de 316 octets, avec accÚs à l'entier au premier octet de l'enregistrement courant, accÚs au nombre réel au neuviÚme entier, accÚs à la premiÚre chaßne de caractÚres au 17Úme octet et accÚs à la seconde chaßne de caractÚres au 167Úme octet.
Si un champ de la table est défini du type primary key
, un quatriÚme fichier est créé : il contient une valeur binaire d'un unsigned long long
initialisé à 1
et incrémenté à chaque insertion d'un enregistrement dans la table, lorsqu'aucune valeur pour cette clé n'est spécifiée. Dans le cas contraire, la valeur est mise à jour avec la valeur maximale de ce champ dans la table, incrémentée de 1
;
Interagir avec une base de donnĂ©es se fait notamment avec le langage SQL (Structured Query Language). Dans ce projet, vous serez amenĂ©s Ă manipuler les requĂȘtes (simplifiĂ©es) suivantes :
CREATE TABLE
INSERT
SELECT
DELETE
UPDATE
DROP TABLE
DROP DATABASE
ouDROP DB
Une requĂȘte SQL se termine toujours par un caractĂšre ';'
.
La structure des requĂȘtes SQL est dĂ©finie ci-dessous.
La structure d'une commande création de table est la suivante : CREATE TABLE table_name (field1_name field1_type, ...fieldN_name fieldN_type);
Le nom d'une table, et les noms de champs obĂ©issent aux mĂȘme rĂšgles que les noms de variables en C. Les types des champs seront les suivants :
int
pour un entier (représenté par unlong long
)primary key
pour un entier non signé (unsigned long long
) avec incrémentation automatique.float
pour un nombre flottant (typedouble
dans l'implémentation)text
pour du texte.
La suppression d'une table se fait avec la commande SQL suivante : DROP TABLE table_name;
. Elle permet de supprimer la table nommée table_name
.
Le principe est similaire Ă celui de la suppression d'une table : on supprime une BDD avec la commande SQL suivante : DROP DATABASE db_name;
.
La commande SQL utilisée est la suivante : INSERT INTO table_name (field1, ... fieldN) VALUES (value1, ... valueN);
Cette commande ajoute à la table nommée table_name
les valeurs value1
Ă valueN
dans les champs field1
Ă fieldN
(donc le champ field1
aura la valeur value1
, le champ field2
aura la valeur value2
et ainsi de suite). Les valeurs sont encadrées par des quotes simples quand il s'agit de chaines de caractÚres.
La commande SQL utilisée est la suivante : SELECT * FROM table_name WHERE condition;
ou SELECT field1, ... fieldN FROM table_name [WHERE condition];
Cette commande affiche l'ensemble des champs des enregistrements de la table satisfaisant Ă la clause WHERE
. Si cette derniÚre est absente, l'ensemble des enregistrements de la table sont affichés. Seuls les champs listés entre les mots-clé SELECT
et FROM
sont affichés. L'étoile *
est un méta-caractÚre qui indique que l'on souhaite afficher tous les champs de la table.
La commande SQL utilisée est la suivante : DELETE FROM table_name [WHERE condition];
Cette commande supprime tous les enregistrements de la table correspondant Ă la clause WHERE
. En l'absence de cette derniĂšre, toute le contenu de la table est supprimĂ© (mais pas la table elle-mĂȘme).
La commande SQL utilisée est la suivante : UPDATE table_name SET field1=value1, ..., fieldN=valueN [WHERE condition];
Cette commande affecte les nouvelles valeurs définies aprÚs SET
Ă l'ensemble des enregistrements correspondant Ă la clause WHERE
. En l'absence de cette derniÚre, tous les enregistrements sont modifiés.
Dans les 3 requĂȘtes suivantes, il peut ĂȘtre nĂ©cessaire de filtrer des champs pour les visualiser ou les modifier. C'est le rĂŽle de la clause facultative WHERE
(le fait qu'elle ne soit pas requise est matérialisée ci dessous par des [ ]
de part et d'autre de la clause WHERE
).
Cette clause est composée du mot-clé WHERE
suivi d'un ensemble de conditions. Nous nous contenterons ici de ne combiner ensemble soit que des AND
, soit que des OR
. Une condition sera donc de la forme :
WHERE field1=value1
pour une clauseWHERE
sur un seul champ.WHERE field1=value1 AND ... AND fieldN=valueN
pour une clauseWHERE
nécessitant que toutes les conditions soient remplies.WHERE field1=value1 OR ... OR fieldN=valueN
pour une clauseWHERE
nécessitant qu'au moins une des conditions soit remplie.
Pour gérer des clauses WHERE
, vous devrez implémenter la fonction suivante : int create_filter_from_sql(char *where_clause, s_filter *filter)
. La fonction va lire la requĂȘte, en extraire le nom de la table, et construire un filtre stockĂ© dans la variable pointĂ©e par filter
.
La premiĂšre tĂąche Ă accomplir pour faire fonctionner votre base de donnĂ©es est de lui donner la capacitĂ© d'interprĂ©ter les requĂȘtes SQL que vous lui transmettrez (au clavier). Une fois lancĂ©, le programme se met en attente de la saisie au clavier, et va analyser les requĂȘtes qui sont saisies. Pour cela, il va vous ĂȘtre nĂ©cessaire d'implĂ©menter la fonction :
query_result_t *parse(char *sql, query_result_t *result);
Il s'agit d'une fonction qui prend en paramĂštres une commande SQL Ă analyser (paramĂštre sql
), et une pointeur sur une structure de type query_result_t
(paramĂštre result
) déjà allouée en mémoire (statiquement ou dynamiquement). L'ensemble des types de données est défini dans une section ultérieure.
La fonction retourne le pointeur sur result
aprĂšs l'avoir rempli avec les valeurs rĂ©sultant de l'analyse de la requĂȘte. Elle retourne NULL
en cas d'Ă©chec.
Pour réaliser cette fonction, vous aurez besoin des fonctions spécialisées suivantes :
query_result_t *parse_select(char *sql, query_result_t *result);
query_result_t *parse_create(char *sql, query_result_t *result);
query_result_t *parse_insert(char *sql, query_result_t *result);
query_result_t *parse_update(char *sql, query_result_t *result);
query_result_t *parse_delete(char *sql, query_result_t *result);
query_result_t *parse_drop_db(char *sql, query_result_t *result);
query_result_t *parse_drop_table(char *sql, query_result_t *result);
Chacune de ces fonctions traite l'analyse d'un seul type de requĂȘte. Leur signature est la mĂȘme que celle du parser global, ainsi que le comportement.
L'ensemble de ces fonctions s'appuie sur des fonctions d'aide pour parser les différents types de sous-chaßne SQL :
char *get_sep_space(char *sql);
char *get_sep_space_and_char(char *sql, char c);
char *get_keyword(char *sql, char *keyword);
char *get_field_name(char *sql, char *field_name);
bool has_reached_sql_end(char *sql);
char *parse_fields_or_values_list(char *sql, table_record_t *result);
char *parse_create_fields_list(char *sql, table_definition_t *result);
char *parse_equality(char *sql, field_record_t *equality);
char *parse_set_clause(char *sql, table_record_t *result);
char *parse_where_clause(char *sql, filter_t *filter);
Chaque fonction prend comme premier paramÚtre la position actuelle dans la commande SQL définie par sql
.
get_sep_space
Cette fonction vérifie la présence d'une séquence de un à un nombre indéterminé d'espaces à partir de la position de sql
. Elle renvoie un pointeur sur le premier non-espace rencontré.
get_sep_space_and_char
Cette fonction vérifie la présence d'une séquence de 0 ou plus espaces, puis du caractÚre c
une unique fois, puis d'encore 0 ou plus espaces. Elle renvoie un pointeur sur le caractÚre suivant cette séquence. Cette fonction est par exemple utile pour les séquences séparées par des virgules.
get_keyword
Cette fonction vérifie que le mot-clé passé en paramÚtre est identique au mot dont le premier caractÚre est pointé par sql
. La casse n'est pas prise en compte (par exemple, SELECT
et select
sont valides lors de l'appel de get_keyword(sql, "select")
). Le pointeur renvoyé pointe sur le caractÚre qui suit le mot-clé.
get_field_name
Cette fonction permet d'extraire le nom d'un champ, le nom d'une table, ou la valeur d'un champ (avant sa conversion dans un type donné). Ces différentes valeurs en SQL peuvent comporter des espaces (dans le cas du texte) mais il est alors nécessaire d'encadrer le champ par des quotes simples '
. La fonction prend en second paramĂštre le buffer oĂč copier la valeur du champ. Ce dernier ne contient pas les quotes le dĂ©limitant quand il y en a. Le pointeur renvoyĂ© pointe sur le caractĂšre suivant le champ ou la quote de fermeture.
has_reached_sql_end
Cette fonction teste si le restant de la chaßne pointée par sql
est la fin de la requĂȘte SQL, i.e. elle n'est composĂ©e que d'espaces jusqu'au caractĂšre de fin de chaĂźne. Elle renvoie true
si c'est le cas, false
sinon.
parse_fields_or_values_list
Cette fonction extrait (en s'appuyant sur les prĂ©cĂ©dentes) une liste de champs ou de valeurs (tels qu'on les trouve dans les requĂȘte select ou insert). Ce type de liste est composĂ©e de champs sĂ©parĂ©s par des virgules. Le rĂ©sultat de cette fonction est Ă©crit dans la structure table_record_t
pointée par result
. La fonction retourne un pointeur sur le caractĂšre suivant la liste de valeurs.
parse_create_fields_list
Cette fonction extrait dans une structure table_definition_t
pointée par result
la dĂ©finition d'une table. La dĂ©finition est une succession de paires (nom de champ, type de champ sĂ©parĂ©s par un espace) sĂ©parĂ©es par des virgules. Cette fonction est utilisĂ©e pour la requĂȘte create table. La fonction retourne un pointeur sur le caractĂšre suivant la liste de dĂ©finitions de champs.
parse_equality
Cette fonction extrait une Ă©galitĂ© (qui peut ĂȘtre une affectation selon le type de clause analysĂ©e) et la stocke dans la structure field_record_t
pointée par equality
, avec un type de données marqué comme TYPE_UNKNOWN
. Ces égalités existent dans les clauses set et where. La fonction retourne un pointeur sur le caractÚre suivant l'égalité.
parse_set_clause
Cette fonction parse une clause set, composée d'une liste d'au moins une égalité (voir parse_equality). Les égalités sont séparées par des virgules lorsqu'il y en a plus d'une. Le résultat est stocké dans une structure table_record_t
pointée par result
. La fonction retourne un pointeur sur le caractÚre suivant la liste d'égalités.
parse_where_clause
Cette fonction parse une clause where, composée d'une liste d'au moins une égalité (voir parse_equality). Les égalités sont séparées par un opérateur logique (OR
ou AND
) lorsqu'il y en a plus d'une. Le résultat est stocké dans une structure filter_t
pointée par filter
. La fonction retourne un pointeur sur le caractĂšre suivant la clause where.
Valeur de retour
Ă l'exception de la fonction has_reached_sql_end
, chacune de ces fonctions retourne un pointeur sur le caractĂšre de la chaĂźne sql
aprÚs la fin de l'analyse du champ concerné. Toutes retournent NULL
en cas d'Ă©chec, signifiant que la requĂȘte SQL est mal formĂ©e.
La vĂ©rification des paramĂštres de la requĂȘte dĂ©pend du type de requĂȘte.
- la table existe
- les champs de la liste de champs existent tous (voir la définition de la table)
- la clause
WHERE
(si elle existe) correspond à des champs de la table et les valeurs recherchées sont convertibles au type du champ correspondant dans la définition de la table.
La requĂȘte est valable si :
- la table existe
- les champs de la liste des champs à affecter (entre le nom de la table et le mot-clé
VALUES
) existent tous - le nombre de champs et de valeurs (seconde liste entre parenthÚses aprÚs le mot-clé
VALUES
) est égal - Les valeurs (dans leur ordre de listage) sont convertibles au type de leur champ, tel que défini dans la définition de table.
La requĂȘte est valable si la table Ă crĂ©er n'existe pas, i.e. le rĂ©pertoire de la table n'existe pas.
La requĂȘte est valable si :
- la table existe
- la clause
SET
(si elle existe) correspond à des champs de la table et les valeurs affectées sont convertibles au type du champ correspondant dans la définition de la table. - la clause
WHERE
(si elle existe) correspond à des champs de la table et les valeurs recherchées sont convertibles au type du champ correspondant dans la définition de la table.
La requĂȘte est valable si :
- la table existe
- la clause
WHERE
(si elle existe) correspond à des champs de la table et les valeurs recherchées sont convertibles au type du champ correspondant dans la définition de la table.
La requĂȘte est valable si la table existe, i.e. si le rĂ©pertoire de la table existe.
La requĂȘte est valable si la base de donnĂ©es existe, i.e. si le rĂ©pertoire de la base de donnĂ©es existe.
Les fonctions nécessaires sont les suivantes :
bool check_query(query_result_t *query);
bool check_query_select(update_or_select_query_t *query);
bool check_query_update(update_or_select_query_t *query);
bool check_query_create(create_query_t *query);
bool check_query_insert(insert_query_t *query);
bool check_query_delete(delete_query_t *query);
bool check_query_drop_table(char *table_name);
bool check_query_drop_db(char *db_name);
bool check_fields_list(table_record_t *fields_list, table_definition_t *table_definition);
bool check_value_types(table_record_t *fields_list, table_definition_t *table_definition);
field_definition_t *find_field_definition(char *field_name, table_definition_t *table_definition);
bool is_value_valid(field_record_t *value, field_definition_t *field_definition);
bool is_int(char *value);
bool is_float(char *value);
bool is_key(char *value);
Leur fonctionnement est donné en commentaires doxygen dans le fichier check.c
envoyé avec cette partie du sujet.
L'expansion de la requĂȘte est nĂ©cessaire pour les requĂȘtes dont les champs peuvent ĂȘtre dĂ©finis incomplĂštement. Il s'agit des requĂȘtes INSERT
et SELECT
. En effet, insert peut spĂ©cifier seulement une partie des champs Ă affecter, les autres Ă©tant crĂ©Ă©s avec des valeurs par dĂ©faut. Concernant la requĂȘte SELECT
, la liste de champs Ă afficher peut ĂȘtre un sous ensemble des champs de la table (il n'y a dans ce cas rien Ă Ă©tendre) ou le mot-clĂ© *
qui signifie "tous les champs". Dans ce dernier cas, la liste de champs du SELECT
doit ĂȘtre remplacĂ©e par la liste des champs de la dĂ©finition de la table cible.
Les fonctions à définir sont les suivantes :
void expand(query_result_t *query);
void expand_select(update_or_select_query_t *query);
void expand_insert(insert_query_t *query);
bool is_field_in_record(table_record_t *record, char *field_name);
void make_default_value(field_record_t *field, char *table_name);
expand
est la fonction racine qui appellera une des deux fonctions spécialisées. expand_insert
va vérifier si la liste de champs est une *
. Dans ce cas, elle va remplacer cette liste par la liste nominative des champs définis pour la table cible.
expand_insert
va parcourir la dĂ©finition de la table cible et chercher les champs de la table non dĂ©finis par la requĂȘte. Quand un champ est trouvĂ©, il est ajoutĂ© Ă la requĂȘte avec la valeur par dĂ©faut (0 pour les valeurs numĂ©riques, chaĂźne vide pour le texte et identifiant suivant pour les types primary key
).
is_field_in_record
teste si un champ dont le nom est field_name
existe dans l'enregistrement de table de la requĂȘte. Elle renvoie true
si c'est le cas, false
sinon.
make_default_value
affecte la valeur par défaut à un champ en se basant sur son type.
L'exĂ©cution de la requĂȘte consiste Ă appliquer la requĂȘte au contenu de la base de donnĂ©es stockĂ©e sur la machine. Pour celĂ , un certain nombre de fonctions sont nĂ©cessaires.
Tout d'abord, les fichiers query_exec.[hc]
fournissent les fonctions nĂ©cessaires Ă l'exĂ©cution des requĂȘtes.
void execute(query_result_t *query);
void execute_create(create_query_t *query);
void execute_insert(insert_query_t *query);
void execute_select(update_or_select_query_t *query);
void execute_update(update_or_select_query_t *query);
void execute_delete(delete_query_t *query);
void execute_drop_table(char *table_name);
void execute_drop_database(char *db_name);
Comme aux Ă©tapes prĂ©cĂ©dentes, la requĂȘte est d'abord passĂ©e Ă la fonction globale execute
qui va se baser sur le champ query_type
pour appeler une des fonctions dĂ©diĂ©es Ă la requĂȘte Ă exĂ©cuter. Les fonctions spĂ©cialisĂ©es vont soit Ă©crire dans les fichiers de la base de donnĂ©es (execute_create
, execute_insert
, execute_delete
, execute_update
), soit lire et afficher les données des fichiers de tables (execute_select
), soit effacer des fichiers de tables (execute_drop_table
) voire toute une base de données (execute_drop_database
). L'ensemble de ces fonctions s'appuient sur de nouvelles fonctions dans les fichiers déjà créés.
Toutes les fonctions nécessaires à la manipulation des données sont commentées pour vous permettre de les implémenter.
Il est nécessaire d'implémenter la fonction recursive_rmdir
qui sera utilisée pour supprimer le répertoire de la base de données ainsi que les répertoires et fichiers des tables qu'elle contient.
Ce fichier est nouveau et permet de gĂ©rer une liste chaĂźnĂ©e de rĂ©sultats de requĂȘte pour la requĂȘte SELECT
. Le code pour les listes vous est fourni pour simplifier votre travail. Vous devez implĂ©menter les fonctions permettant de parcourir et afficher une liste de rĂ©sultats avec colonnes alignĂ©es. Les fonctions Ă implĂ©menter peuvent s'appuyer sur d'autres fonctions si ça permet de simplifier le dĂ©veloppement. Ces nouvelles fonctions devront ĂȘtre documentĂ©es en entĂȘte avec des commentaires doxygen similaires Ă ceux du sujet, ainsi que commentĂ©es dans la dĂ©finition si nĂ©cessaire.
Des modifications mineures ont été poussées sur les fichiers utils.h
et sql.h
.
Il est attendu dans ce projet que le code rendu satisfasse un certain nombre de conventions (ce ne sont pas des contraintes du langages mais des choix au début d'un projet) :
- indentations : les indentations seront faites sur un nombre d'espaces Ă votre discrĂ©tion, mais ce nombre doit ĂȘtre cohĂ©rent dans l'ensemble du code.
- Déclaration des pointeurs : l'étoile du pointeur est séparée du type pointé par un espace, et collée au nom de la variable, ainsi :
int *a
est correctint* a
,int*a
etint * a
sont incorrects
- Nommage des variables, des types et des fonctions : vous utiliserez le snake case, i.e. des noms sans majuscule et dont les parties sont séparées par des underscores
_
, par exemple :ma_variable
,variable
,variable_1
etvariable1
sont correctsmaVariable
,Variable
,VariableUn
etVariable1
sont incorrects
- Position des accolades : une accolade s'ouvre sur la ligne qui débute son bloc (fonction, if, for, etc.) et est immédiatement suivie d'un saut de ligne. Elle se ferme sur la ligne suivant la derniÚre instruction. L'accolade fermante n'est jamais suivie d'instructions à l'exception du
else
ou duwhile
(structuredo ... while
) qui suit l'accolade fermante. Par exemple :
for (...) {
/*do something*/
}
if (true) {
/*do something*/
} else {
/*do something else*/
}
int max(int a, int b) {
return a;
}
sont corrects mais :
for (int i=0; i<5; ++i)
{ printf("%d\n", i);
}
for (int i=0; i<5; ++i) {
printf("%d\n", i); }
if () {/*do something*/
}
else
{
/*do something else*/}
sont incorrects.
- Espacement des parenthĂšses : la parenthĂšse ouvrante aprĂšs
if
,for
, etwhile
est séparée de ces derniers par un espace. AprÚs un nom de fonction, l'espace est collé au dernier caractÚre du nom. Il n'y a pas d'espace aprÚs une parenthÚse ouvrante, ni avant une parenthÚse fermante :while (a == 3)
,for (int i=0; i<3; ++i)
,if (a == 3)
etvoid ma_fonction(void)
sont correctswhile(a == 3 )
,for ( i=0;i<3 ; ++i)
,if ( a==3)
etvoid ma_fonction (void )
sont incorrects
- Basé sur les exemples ci dessus, également, les opérateurs sont précédés et suivis d'un espace, sauf dans la définition d'une boucle
for
oĂč ils sont collĂ©s aux membres de droite et de gauche. - Le
;
qui sépare les termes de la bouclefor
ne prend pas d'espace avant, mais en prend un aprĂšs.
Le projet est évalué sur les critÚres suivants :
- capacité à effectuer les traitements demandés dans le sujet,
- capacitĂ© Ă traiter les cas particuliers sujets Ă erreur (requĂȘtes SQL mal formĂ©es, pointeurs NULL, etc.)
- Respect des conventions d'Ă©criture de code
- Documentation du code
- Avec des commentaires au format doxygen en entĂȘtes de fonction
- Des commentaires pertinents sur le flux d'une fonction (astuces, cas limites, détails de l'algorithme, etc.)
- Absence ou faible quantité de fuites mémoire (vérifiables avec
valgrind
) - Présentation du projet lors de la derniÚre séance de TP
On considérera pour faciliter le développement de cette base de données que certaines valeurs limites sont fixées :
#define TEXT_LENGTH 150
#define MAX_FIELDS_COUNT 16
Les chaßnes de caractÚres ne dépasseront donc pas 150 caractÚres (incluant le \0
de fin de chaĂźne), et une table, ou toute requĂȘte, ne peut dĂ©passer 16
champs.
Votre base de données nécessitera également un certain nombre de types composés pour fonctionner. Leurs définitions et utilités sont définies dans les sous sections qui suivent.
La définition d'un champ d'une table est gérée avec la structure ci dessous :
typedef enum {
TYPE_PRIMARY_KEY,
TYPE_INTEGER,
TYPE_FLOAT,
TYPE_TEXT
} field_type_t;
typedef struct {
char column_name[TEXT_LENGTH];
field_type_t column_type;
} field_definition_t;
typedef struct {
int fields_count;
field_definition_t definitions[MAX_FIELDS_COUNT];
} table_definition_t;
C'est elle qui permet de définir le nom d'un champ ainsi que son type parmi l'énumération des types supportés. Cette structure est utilisée pour créer une table, pour en lister des données ainsi que pour s'assurer que l'ajout ou la modification de données est conforme à la structure de la table.
Une valeur de champ de table est quant à elle la définition d'une valeur contenue dans un enregistrement de la table. Cette valeur doit donc associer un nom de champ avec une valeur, en s'assurant que le type de la valeur est conforme au type du champ dans la table.
typedef struct {
char column_name[TEXT_LENGTH];
field_type_t field_type;
union {
double float_value;
long long int_value;
unsigned long long primary_key_value;
char text_value[TEXT_LENGTH];
} field_value;
} field_record_t;
L'accĂšs Ă la valeur se fera par un switch
sur le type de donnée du champ field_type
.
Un enregistrement de table se présente sous la forme suivante :
typedef struct {
int fields_count;
field_record_t fields[MAX_FIELDS_COUNT];
} table_record_t;
Le champ fields_count
indique combien d'éléments du tableau fields
sont affectĂ©s pour ĂȘtre utilisĂ©s dans une requĂȘte.
La clause WHERE
est définie sur 1 à N champs. On considÚre dans ce projet qu'elle ne peut contenir qu'un seul opérateur logique, qui sera soit OR
, soit AND
(qui sera répété N-1 fois pour N critÚres de filtre).
La structure Ă utiliser est la suivante :
typedef enum {
OP_OR,
OP_AND,
OP_ERROR
} operator_t;
typedef struct {
table_record_t values;
operator_t logic_operator;
} filter_t;
Les types nécessaires au parsing sont les suivants :
typedef enum {
QUERY_NONE,
QUERY_CREATE_TABLE,
QUERY_DROP_TABLE,
QUERY_SELECT,
QUERY_UPDATE,
QUERY_DELETE,
QUERY_INSERT,
QUERY_DROP_DB,
} query_type_t;
typedef struct {
char table_name[TEXT_LENGTH];
table_definition_t table_definition;
} create_query_t;
typedef struct {
char table_name[TEXT_LENGTH];
filter_t where_clause;
} drop_query_t;
typedef struct {
char table_name[TEXT_LENGTH];
table_record_t fields_names;
table_record_t fields_values;
} insert_query_t;
typedef struct {
char table_name[TEXT_LENGTH];
table_record_t set_clause;
filter_t where_clause;
} update_or_select_query_t;
typedef struct {
query_type_t query_type;
union {
char table_name[TEXT_LENGTH];
char database_name[TEXT_LENGTH];
create_query_t create_query;
drop_query_t drop_query;
insert_query_t insert_query;
update_or_select_query_t update_query;
update_or_select_query_t select_query;
} query_content;
} query_result_t;
Le type query_result_t
est renvoyĂ© par toutes les fonctions de parsing de haut niveau. Il comporte un champ indiquant le type de requĂȘte, puis une union Ă laquelle on accĂ©dera en fonction de la valeur du champ query_type
. Chaque membre de l'union dĂ©finit les Ă©lĂ©ments nĂ©cessaires pour effectuer la requĂȘte, notamment la table ou la base de donnĂ©es cible, les champs affectĂ©s et leurs valeurs, ainsi que l'Ă©ventuelle clause where.
Les fonctions décrites dans cette section sont utilisées par la base de données pour accéder au contenu de la base, le lire, ou en ajouter. Elles reposent sur les résultats des conversion depuis le SQL vers les structures internes.
Trois fonctions sont nécessaires à la manipulation globale de la base de données :
bool directory_exists(char *path)
qui cherche si le répertoire de cheminpath
existe. Cette fonction renvoietrue
si le répertoire existe,false
sinon.void create_db_directory(char *name)
qui crĂ©e le rĂ©pertoire de la base de donnĂ©es (il faut que cette derniĂšre n'existe pas encore, d'oĂč la fonction prĂ©cĂ©dente)void recursive_rmdir(char *dirname)
qui permet de supprimer récursivement un répertoire dont le chemin estdirname
.
Il est également nécessaire que votre programme stocke dans une variable le nom de la base de données en cours d'utilisation (option -d
), ainsi que son chemin (option -l
).
La création d'une table, puis son utilisation, repose sur la structure s_field_definition
. Comme pour la BDD, vous devrez d'abord créer les 3 fonctions de base pour gérer l'existence de la table.
int table_exists(char *table_name)
qui renvoie 1 si la tabletable_name
existe déjà , 0 sinonvoid drop_table(char *table_name)
void create_table(char *table_name, s_field_definition fields[], int fields_count)
qui crée la table
La création de la table est un processus de plusieurs étapes (si elle n'existe pas encore) :
- Création du répertoire nommé comme la variable
table_name
- Dans ce répertoire, création des fichiers suivants :
table_name.idx
, fichier d'indextable_name.def
, fichier de définition de tabletable_name.data
, fichier de contenu de la table- le cas échéant, un fichier
table_name.key
pour la clé primaire (il ne peut y en avoir qu'une par table)
- Ăcriture du contenu de la table
table_name.def
Par la suite, il vous sera nécessaire de comparer les arguments d'une commande SQL avec la structure de la base de données. Ce sera le rÎle de la fonction get_table_definition
.
Ăcrire dans une table se fait dans le cas des requĂȘtes INSERT
et UPDATE
. Ajouter ou modifier un enregistrement d'une table se fait par la procédure suivante :
- Construire le buffer binaire qui sera écrit dans le fichier de données
- Chercher un index et un emplacement libres dans le fichier d'index et le fichier de contenu. Ce choix s'appuie sur l'une des deux conditions suivantes :
- Toutes les conditions ci dessous sont vraies :
- L'index courant est inactif
- L'enregistrement pointé a une taille supérieure ou égale à celle du buffer construit à la premiÚre étape
- à défaut, un nouvel index et un nouveau contenu sont créés en fins de fichiers d'index et de contenu.
- Toutes les conditions ci dessous sont vraies :
Les fonctions requises pour réaliser cette tùche sont les suivantes :
add_row_to_table
format_row
compute_record_length
find_first_free_record
Ainsi qu'Ă©ventuellement :get_next_key
update_key
Pour la lecture dans une table, vous devez utiliser la structure écrite dans le fichier .def, l'index dans le fichier .idx qui vous permettront ensuite d'accéder et de récupérer correctement les données de la table dans le fichier .data.
Pour cela, vous aurez besoin des fonctions suivantes :
open_definition_file
open_index_file
open_content_file
get_table_definition
compute_record_length
get_filtered_records
get_table_record
is_matching_filter
find_field_in_table_record
display_table_record_list