TQL

Antoni Boucher

Problèmes de l’utilisation du SQL

  • Vérification des erreurs à l’exécution

  • Dialectes légèrement différents d’une base de données à l’autre

  • Nécessite d’apprendre ou d’utiliser un langage différent de celui qui est utilisé pour un projet

  • Sécurité (injections SQL)

Langage dédié embarqué

  • Langage utilisé pour un besoin spécifique utilisant la syntaxe du langage hôte

    • Permet d’effectuer une analyse sémantique

    • Permet de corriger les problèmes de sécurité

    • Peut générer du SQL pour différentes bases de données

    • Pas besoin d’utiliser le SQL

  • Problème potentiel : dégradation de la performance

Module d’extension du compilateur Rust

  • Compile du code Rust en SQL à la compilation

  • Permet de régler le problème de performance

  • TQL :

    • Attribut pour la structure des tables

    • Macro procédurale pour générer le SQL

Rust

  • Conçu par Mozilla, sorti en 2010

  • Multi-paradigme (impératif, fonctionnel, concurrent)

  • Typage fort et statique (avec inférence de types)

  • Sécurisé

  • Orienté bas niveau

  • Gestion automatique de la mémoire (sans ramasse-miettes)

  • Moteur de rendu Servo

Points forts de Rust

  • Abstractions qui ne coûte rien

  • Gestion de la concurrence

  • Sûr :

    • Pas de pointeurs nuls

    • Pas d’utilisation de pointeurs après la désallocation

    • Pas d’utilisation de variables avant leur initialisation

    • Variables immuables par défaut

    • Pas de fuite de mémoire

Exemple de code

fn main() {
    println!("Hello World!"); (1)
}
1println! est une macro.

Variable

let nombre: i32 = 42; (1)
let nombre = 42;      (2)
let mut nombre = 24;  (3)
1Variable de type entier signé de 32 bits
2Indiquer le type est facultatif grâce à l’inférence de type
3Variable muable

Structure

struct Personne {
    prenom: String,
    nom: String,
    age: u8,                          (1)
}

impl Personne {
    fn nom_complet(&self) -> String { (2) (3)
        self.prenom + " " + &self.nom
    }
}
1Entier non signé de 8 bits
2self est l’objet courant
3& indique que l’objet courant est reçu par référence

Utilisation d’une structure

let personne = Personne {
    nom: "Hoare",
    prenom: "Graydon",
};

let nom_complet = personne.nom_complet();

Attribut

#[derive(Clone)]
struct Personne {
    prenom: String,
    nom: String,
    age: u8,
}

Filtrage par motif

match nombre {
    1 => println!("un"),
    2 => println!("deux"),
    3 | 4 => println!("trois ou quatre"),
    5 ... 10 => println!("entre 5 et 10"),
    _ => println!("autre"),
}

Filtrage par motif (déstructuration)

enum Liste {
    Vide,
    Cons(i32, Box<Liste>),
}

fn somme_liste(liste: &Liste) -> i32 {
    match *liste {
        Liste::Vide => 0,
        Liste::Cons(element, ref reste) =>
            element + somme_liste(reste),
    }
}

Filtrage par motif (if)

fn est_vide(liste: &Liste) -> bool {
    if let Liste::Vide = *liste {
        true
    }
    else {
        false
    }
}

Expressions

let texte =
    if nombre > 42 {        (1)
        "plus grand que 42" (2)
    }
    else {
        "plus petit ou égal à 42"
    };
1if est une expression
2absence du point-virgule

Fonction

fn max(nombre1: i32, nombre2: i32) -> i32 {
    if nombre1 > nombre2 {
        nombre1
    }
    else {
        nombre2
    }
}

Sous-ensemble du SQL supporté

  • Création de table

  • Suppression de table

  • Sélection

  • Insertion

  • Mise à jour

  • Suppression

Fonctionnalités non supportées

  • SELECT DISTINCT

  • Pour la création de table :

    • DEFAULT

    • UNIQUE

    • INDEXES

  • Pour la suppression de données : CASCADE

Nombres

  • 25 % du SQL est implémenté

  • 80 % des fonctionnalités les plus courantes

Extension syntaxique pour Rust

Attribut

  • L’attribut #[SqlTable] indique qu’une structure représente une table SQL

#[SqlTable]
struct Personne {
    id: PrimaryKey,
    nom: String,
    prenom: String,
    age: i32,
}

Clé étrangère

#[SqlTable]
struct Article {
    id: PrimaryKey,
    auteur: String,
    titre: String,
    revision: i32,
    date_ajout: DateTime<UTC>,
}
#[SqlTable]
struct Commentaire {
    id: PrimaryKey,
    auteur: String,
    message: String,
    date_post: DateTime<UTC>,
    article: ForeignKey<Article>, (1)
}
1Clé étrangère vers la table Article

Macros procédurales

  • La macro to_sql!() converti le code Rust en SQL.

  • La macro sql!() exécute en plus la requête.

sql!(Personne.insert(nom = "Hoare", prenom = "Graydon"));

Exemple d’utilisation

let personnes = sql!(
    Personne.filter(nom == "Hoare" && age < 30)
    .sort(-age)[10..20]
);

for personne in personnes {
    println!("{} {}", personne.prenom, personne.nom);
}
SELECT Personne.id, Personne.nom,
    Personne.prenom, Personne.age,
    Personne.date_naissance, Personne.poids
FROM Personne
WHERE nom = 'Hoare'
    AND age < 30
ORDER BY age DESC
LIMIT 20 OFFSET 10

Exemple d’erreur

let personne = sql!(Personne.get("personne_id")).unwrap();
code.rs:10:34: 10:46 error: mismatched types:
 expected `i32`,
    found `String` [E0308]
code.rs:10 let personne = sql!(Personne.get("personne_id")).unwrap();
                                            ^~
code.rs:10:34: 10:46 help: run `rustc --explain E0308` to see a detailed explanation
code.rs:10:34: 10:46 note: in this expansion of sql! (defined in TQL)

Optimisations

  • Simplification des expressions composées de littéraux

Personne[0 + 10 - 2 .. 50 - (4 + 2)]
SELECT Personne.id, Personne.nom, Personne.prenom, Personne.age
FROM Personne
LIMIT 44
OFFSET 8

Autres optimisations envisageables

Sélection des champs utiles

Si seul le champ age est utilisé suite à la requête, la requête suivante :

Personne.all()

pourrait être compilée en :

SELECT Personne.age
FROM Personne

Avantage : diminue le transfert d’informations.

Littéraux dans un appel de méthode

Cette requête :

Personne.filter(nom.contains("oar"))

compile en :

SELECT Personne.id, Personne.nom, Personne.prenom, Personne.age
FROM Personne
WHERE nom LIKE '%' || 'oar' || '%'

pourrait être compilée en :

SELECT Personne.id, Personne.nom, Personne.prenom, Personne.age
FROM Personne
WHERE nom LIKE '%oar%'

Préparation de la requête

for i in (0..5) {
    let personnes = sql!(Personne.all());
}
let result = connection.prepare("SELECT Personne.id, Personne.nom,
    Personne.prenom, Personne.age, Personne.date_naissance,
    Personne.poids FROM Personne").unwrap();
for i in (0..5) {
    let personnes = {
        result.query(&[]).unwrap().iter().map(|row| {
            Personne {
                id: row.get(0),
                nom: row.get(1),
                prenom: row.get(2),
                age: row.get(3),
            }
        }).collect::<Vec<_>>()
    };
}

Conclusion

  • Conversion du code Rust en SQL à la compilation.

  • Détection des erreurs de types et d’identifiants à la compilation.

  • Abstraction au SQL.

Performance

speed
Figure 1. Vitesse d’exécution en secondes de codes utilisant différentes abstractions de bases de données

Utilisabilité

lines
Figure 2. Nombre de lignes dans les codes utilisés pour le test de performance

Utilisabilité

characters
Figure 3. Nombre de caractères dans les codes utilisés pour le test de performance

Démonstration