Skip to content

eliottcoint/Managing_database

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Projet de C LP25

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.

Principe du programme

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.

Représentation de la base de données sur le support de stockage

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Ă©.

Le fichier de définition

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

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.

Le fichier de contenu

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.

Le fichier de clé

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;

Langage SQL simplifié

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 ou DROP DB

Une requĂȘte SQL se termine toujours par un caractĂšre ';'.

La structure des requĂȘtes SQL est dĂ©finie ci-dessous.

Création d'une table

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 un long long)
  • primary key pour un entier non signĂ© (unsigned long long) avec incrĂ©mentation automatique.
  • float pour un nombre flottant (type double dans l'implĂ©mentation)
  • text pour du texte.

Suppression d'une table

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.

Suppression d'une base de données

Le principe est similaire Ă  celui de la suppression d'une table : on supprime une BDD avec la commande SQL suivante : DROP DATABASE db_name;.

Insertion d'un enregistrement dans une table

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.

SĂ©lection d'enregistrements Ă  partir d'une table

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.

Suppression d'un enregistrement dans une 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).

Modification d'un enregistrement dans une table

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.

Clauses WHERE

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 clause WHERE sur un seul champ.
  • WHERE field1=value1 AND ... AND fieldN=valueN pour une clause WHERE nĂ©cessitant que toutes les conditions soient remplies.
  • WHERE field1=value1 OR ... OR fieldN=valueN pour une clause WHERE 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.

TĂąche 1 : analyse SQL

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.

VĂ©rification d'une requĂȘte avec la structure de la base de donnĂ©es

La vĂ©rification des paramĂštres de la requĂȘte dĂ©pend du type de requĂȘte.

RequĂȘte SELECT

  • 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.

RequĂȘte INSERT

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.

RequĂȘte CREATE

La requĂȘte est valable si la table Ă  crĂ©er n'existe pas, i.e. le rĂ©pertoire de la table n'existe pas.

RequĂȘte UPDATE

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.

RequĂȘte DELETE

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.

RequĂȘte DROP TABLE

La requĂȘte est valable si la table existe, i.e. si le rĂ©pertoire de la table existe.

RequĂȘte DROP DATABASE

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.

Fonctions nĂ©cessaires Ă  la requĂȘte

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.

Expansion de la requĂȘte

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.

ExĂ©cution de la requĂȘte

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.

Table

Toutes les fonctions nécessaires à la manipulation des données sont commentées pour vous permettre de les implémenter.

Database

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.

Record_list

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.

Remarques

Des modifications mineures ont été poussées sur les fichiers utils.h et sql.h.

Annexes

Convention de code

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 correct
    • int* a, int*a et int * 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 et variable1 sont corrects
    • maVariable, Variable, VariableUn et Variable1 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 du while (structure do ... 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, et while 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) et void ma_fonction(void) sont corrects
    • while(a == 3 ), for ( i=0;i<3 ; ++i), if ( a==3) et void 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 boucle for ne prend pas d'espace avant, mais en prend un aprĂšs.

Évaluation du projet

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

Structures de données

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.

DĂ©finition d'un champ de table

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.

Valeur de champ de 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.

DĂ©finition d'un enregistrement

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.

DĂ©finition d'une clause WHERE

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;

Parsing SQL

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.

Fonctions internes de la base de données

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.

Création et suppression de BDD

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 chemin path existe. Cette fonction renvoie true 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 est dirname.

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).

Création et suppression d'une table

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 table table_name existe dĂ©jĂ , 0 sinon
  • void 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'index
    • table_name.def, fichier de dĂ©finition de table
    • table_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.

Écriture dans une table

É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.

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

Lecture dans une table

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

About

School Project

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published