Please note, this is a STATIC archive of website developer.mozilla.org from November 2016, cach3.com does not collect or store any user information, there is no "phishing" involved.

Storage

Storage est une API de base de données SQLite. Elle est uniquement accessible aux appels privilégiés, c'est-à-dire provenant d'extensions et de composants de Firefox uniquement. Pour une référence complète de toutes les méthodes et propriétés de l'interface de connexion à la base de données, consultez mozIStorageConnection.

L'API est toujours considérée comme en cours de développement, ce qui signifie qu'elle peut être modifiée à n'importe quel moment. Il se peut en effet que l'API soit quelque peu modifiée entre Firefox 2 et Firefox 3.

Note : Il convient de faire la différence entre Storage et la fonctionnalité DOM:Storage qui peut être utilisée par des pages Web pour conserver des données persistantes ou l'API de restauration de session (un utilitaire de stockage XPCOM destiné aux extensions).

Préambule

Ce document traite de l'API mozStorage et de quelques particularités de sqlite. Il ne traite pas du SQL ou de l'utilisation normale de sqlite. Pour ces autres informations, vous devrez consulter vos références favorites sur SQL. Vous pourrez cependant trouver quelques liens très utiles dans la section Voir également. Pour obtenir plus d'aide sur l'API mozStorage, vous pouvez poster sur le serveur de news mozilla.dev.apps.firefox sur news.mozilla.org. Pour signaler des bogues, utilisez Bugzilla (product « Toolkit », component « Storage »).

mozStorage se présente comme n'importe quelle autres système de bases de données. La procédure complète d'utilisation est la suivante :

  • Ouverture d'une connexion vers la base de données de votre choix.
  • Création d'une requête à exécuter sur la connexion.
  • Liaison de paramètres à la requête si nécessaire.
  • Exécution de la requête.
  • Traitement des erreurs éventuelles
  • Réinitialisation de la requête.

Ouverture d'une connexion

Pour les utilisateurs de C++, la première initialisation du service Storage doit se faire dans le thread principal. Vous obtiendrez une erreur en voulant l'initialiser dans un autre thread. Vous pouvez toutefois utiliser le service depuis un thread en appelant la méthode getService depuis le thread principal pour vérifier que le service a bien été créé.

Voici un exemple C++ d'ouverture d'une connexion vers « asdf.sqlite » dans le répertoire du profil de l'utilisateur :

nsCOMPtr<nsIFile> dbFile;
rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
                            getter_AddRefs(dbFile));
NS_ENSURE_SUCCESS(rv, rv);
rv = dbFile->Append(NS_LITERAL_STRING("mon_fichier_db.sqlite"));
NS_ENSURE_SUCCESS(rv, rv);

mDBService = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
rv = mDBService->OpenDatabase(dbFile, getter_AddRefs(mDBConn));
NS_ENSURE_SUCCESS(rv, rv);

MOZ_STORAGE_SERVICE_CONTRACTID est défini dans storage/build/mozStorageCID.h. Sa valeur est "@mozilla.org/storage/service;1".

Voici un exemple en JavaScript :

let { Cc, Ci } = require('chrome');
var file = Cc["@mozilla.org/file/directory_service;1"]
                     .getService(Ci.nsIProperties)
                     .get("ProfD", Ci.nsIFile);
file.append("mon_fichier_db.sqlite");

var storageService = Cc["@mozilla.org/storage/service;1"]
                        .getService(Ci.mozIStorageService);
var mDBConn = storageService.openDatabase(file);
Note : la fonction openDatabase risque d'être modifiée à l'avenir. Elle sera probablement améliorée et simplifiée pour réduire les difficultés d'utilisation.

Il est déconseillé de nommer votre base de données avec une extension en « .sdb » pour sqlite database. En effet, Windows reconnaît cette extension comme une « base de données de compatibilité des applications » et les modifications sont inscrites dans la fonctionnalité de restauration système. Cela peut donc ralentir fortement les opérations sur le fichier.

Requêtes

Suivez ces instructions pour créer et exécuter des requêtes SQL sur votre base de données SQLite. Pour une référence plus complète, consultez mozIStorageStatement.

Création d'une requête

Il existe deux méthodes pour créer une requête. Si vous n'avez aucun paramètre et si la requête ne renvoie aucune valeur, utilisez mozIStorageConnection.executeSimpleSQL.

C++ :
rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("CREATE TABLE foo (a INTEGER)"));

JS :
mDBConn.executeSimpleSQL("CREATE TABLE foo (a INTEGER)");

Autrement, vous devrez préparer une requête en utilisant mozIStorageConnection.createStatement :

C++ :
nsCOMPtr<mozIStorageStatement> instruction;
rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("SELECT * FROM foo WHERE a = ?1"),
                              getter_AddRefs(statement));
NS_ENSURE_SUCCESS(rv, rv);

JS :
var instruction = mDBConn.createStatement("SELECT * FROM foo WHERE a = ?1");

Cet exemple utilise un sélecteur « ?1 » comme paramètre qui sera renseigné ultérieurement (voir la section suivante).

Après avoir préparé une requête, vous pouvez lui lier des paramètres, l'exécuter et la réinitialiser autant de fois que vous le souhaitez. Si vous devez faire une requête fréquemment, l'utilisation d'une requête précompilée augmentera les performances de manière significative car la requête SQL n'aura pas à être traitée à chaque fois.

Si vous êtes familier avec sqlite, vous devez savoir que les requêtes préparées deviennent invalides lorsque la structure de la base de données est modifiée. Heureusement, mozIStorageStatement détecte l'erreur et recompile la requête si nécessaire. Ainsi, après avoir créé une requête, vous n'avez pas à vous soucier d'une modification de structure ; toutes les requêtes continueront à fonctionner de manière transparente.

Liaison de paramètres

Il est généralement préférable de lier tous les paramètres séparément plutôt que d'essayer de construire à la volée des chaînes SQL contenant ces paramètres. Entre autres choses, ce mode de fonctionnement permet d'éviter une attaque par injection SQL puisque les paramètres liés ne sont jamais exécutés en SQL.

Les sélecteurs inclus dans la requête sont liés aux paramètres. Les sélecteurs sont indexés en commençant par « ?1 », puis « ?2 », et ainsi de suite. Vous devez utiliser les fonctions BindXXXParameter(0), BindXXXParameter(1), etc. pour lier ces sélecteurs.

Attention : les indices des sélecteurs débutent à partir de 1. Les entiers passés aux fonctions de liaison débutent à partir de 0. Cela signifie que « ?1 » correspond au paramètre 0, « ?2 » correspond au paramètre 1, etc.

Il est également possible d'utiliser des paramètres nommés, comme ceci : « :exemple » au lieu de «?xx ».

Un sélecteur peut apparaître plusieurs fois dans la chaîne SQL et tous les instances seront remplacées par la valeur liée. Les paramètres non liés seront interprétés comme NULL.

Les exemples ci-dessous utilisent uniquement bindUTF8StringParameter() et bindInt32Parameter(). Pour une liste de toutes les fonctions de liaison, consultez mozIStorageStatement.

Exemple C++ :

nsCOMPtr<mozIStorageStatement> instruction;
rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("SELECT * FROM foo WHERE a = ?1 AND b > ?2"),
                              getter_AddRefs(statement));
NS_ENSURE_SUCCESS(rv, rv);
rv = instruction->BindUTF8StringParameter(0, "bonjour"); // "bonjour" sera substitué à "?1"
NS_ENSURE_SUCCESS(rv, rv);
rv = instruction->BindInt32Parameter(1, 1234); // 1234 sera substitué à "?2"
NS_ENSURE_SUCCESS(rv, rv);

Exemple Javascript :

var instruction = mDBConn.createStatement("SELECT * FROM foo WHERE a = ?1 AND b > ?2");
instruction.bindUTF8StringParameter(0, "bonjour");
instruction.bindInt32Parameter(1, 1234);

En cas d'utilisation de paramètres nommés, il vous faudra utiliser la méthode getParameterIndex pour obtenir l'indice du paramètre. Voici un exemple en JavaScript :

var instruction = mDBConn.createStatement("SELECT * FROM foo WHERE a = :premierparam AND b > :secondparam");

var premierindice = instruction.getParameterIndex(":premierparam");
instruction.bindUTF8StringParameter(premierindice, "bonjour");

var secondindice = instruction.getParameterIndex(":secondparam");
instruction.bindInt32Parameter(secondindice, 1234);

Il est évidemment possible d'utiliser à la fois des paramètres nommés et indexés dans une même requête :

var instruction = mDBConn.createStatement("SELECT * FROM foo WHERE a = ?1 AND b > :secondparam");

instruction.bindUTF8StringParameter(0, "bonjour");
// on peut aussi utiliser
// var premierindice = instruction.getParameterIndex("?1");
// instruction.bindUTF8StringParameter(premierindice, "bonjour");

var secondindice = instruction.getParameterIndex(":secondparam");
instruction.bindInt32Parameter(secondindice, 1234);

Si vous désirez utiliser une clause WHERE avec une expression IN ( liste-de-valeurs ), les liaisons ne fonctionneront pas. Construisez plutôt une chaîne. Si ce n'est pas pour gérer une entrée de l'utilisateur, cela ne posera pas de problème de sécurité :

var ids = "3,21,72,89";
var sql = "DELETE FROM table WHERE id IN ( "+ ids +" )";

Exécution d'une requête

La principale manière d'exécuter une requête est d'utiliser la fonction mozIStorageStatement.executeStep. Cette fonction vous permet de récupérer chaque ligne produite par la requête en vous notifiant lorsqu'il n'y a plus de résultats.

Après un appel d'executeStep, vous utilisez la fonction d'accès appropriée de mozIStorageValueArray pour obtenir les valeurs dans une ligne de résultat (mozIStorageStatement implémente mozIStorageValueArray). L'exemple ci-dessous utilise uniquement getInt32().

Vous pouvez obtenir le type de la valeur d'une colonne spécifiée avec mozIStorageValueArray.getTypeOfIndex. Faites attention, sqlite n'est pas une base de données typée. N'importe quel type de données peut être placé dans une cellule, indépendamment du type de la colonne. Si vous lisez une donnée d'un type différent, sqlite fera de son mieux pour la convertir, et vous proposera une valeur par défaut si c'est impossible. De ce fait, il n'est pas possible d'obtenir des erreurs de typage, mais cela peut engendrer des résultats surprenants.

Les codes C++ peuvent également utiliser des fonctions AsInt32, AsDouble, etc. pour adapter la valeur retournée à un type C++. Prenez garde toutefois car aucune erreur ne vous sera signalée si votre index est invalide. D'autres erreurs sont impossibles car sqlite convertira toujours les types, même si cela n'a aucun sens.

Exemple C++ :

PRBool hasMoreData;
while (NS_SUCCEEDED(instruction->ExecuteStep(&hasMoreData)) && hasMoreData) {
  PRInt32 valeur = instruction->AsInt32(0);
  // utiliser la valeur...
}

Exemple Javascript :

while (instruction.executeStep()) {
  var valeur = instruction.getInt32(0); // utilisez la fonction appropriée !
  // utiliser la valeur...
}

mozIStorageStatement.execute() est une fonction pratique lorsque votre requête ne renvoie pas de valeurs. Elle effectue la requête en une seule étape et se réinitialise. Elle sert surtout pour des requêtes d'insertion en simplifiant le code :

var instruction = mDBConn.createStatement("INSERT INTO ma_table VALUES (?1)");
instruction.bindInt32Parameter(52);
instruction.execute();

Ceci 'en:Image:TTRW2.zip est un exemple JavaScript et XUL simple mais complet de la manière d'exécuter un SELECT SQL sur une base de données.

Réinitialisation d'une requête

Il est important de réinitialiser les requêtes qui ne servent plus. Une requête d'écriture non réinitialisée laissera un verrou sur les tables et interdira à d'autres requêtes d'y accéder. Une requête de lecture non réinitialisée interdira toute écriture.

Lorsque l'objet de requête est libéré, la base de données à laquelle il était lié est fermée. Si vous utilisez C++ et savez que toutes les références seront détruites, vous n'avez pas à réinitialiser explicitement la requête. De même, avec l'appel de la fonction mozIStorageStatement.execute(), il est inutile de réinitialiser la requête ; cette fonction le fera pour vous. Dans les autres cas, appelez mozIStorageStatement.reset().

En JavaScript, toutes les requêtes doivent être réinitialisées. Faites attention au sujet des exceptions. Assurez vous que vos requêtes soient réinitialisées même si une exception est déclenchée, sinon l'accès à la base de données ne sera plus possible. La réinitialisation d'une requête est une opération légère sans conséquences, donc n'hésitez pas à effectuer même des réinitialisations superflues.

var instruction = connection.createStatement(...);
try {
  // utiliser la requête...
} finally {
  instruction.reset();
}

Les scripts C++ doivent faire de même. L'objet de contexte mozStorageStatementScoper dans storage/public/mozStorageHelper.h s'assurera qu'une requête donnée est réinitialisée lorsque le contexte est quitté. Il est fortement recommandé d'utiliser cet objet.

void someClass::someFunction()
{
  mozStorageStatementScoper scoper(mStatement)
  // utiliser la requête...
}

ID de dernière insertion

Utilisez la propriété lastInsertRowID de la connexion pour obtenir l'id (rowid) de la dernière opération INSERT sur la base de donnée.

C'est surtout utile si une des colonnes de votre table est définie à INTEGER PRIMARY KEY ou INTEGER PRIMARY KEY AUTOINCREMENT, auquel cas SQLite assignera automatiquement une valeur pour chaque ligne insérée si vous n'en fournissez pas. La valeur de retour est du type number en JS et long long en C++.

Exemple en JS avec lastInsertRowID :

var sql = "INSERT INTO contacts_table (number_col, name_col) VALUES (?1, ?2)"
var statement = mDBConn.createStatement(sql);
statement.bindUTF8StringParameter(0, number);
statement.bindUTF8StringParameter(1, name);
statement.execute();
statement.reset();
	
var rowid = mDBConn.lastInsertRowID;

Transactions

mozIStorageConnection dispose de fonctions pour débuter et clore des transactions. Même si vous n'utilisez pas explicitement de transactions, une transaction implicite sera créée pour chacune de vos requêtes. Ceci a des implications majeures en termes de performances. Chaque transaction, et en particulier les validations, occasionne un délai supplémentaire. Les performances seront meilleures si vous placez plusieurs requêtes dans une même transaction. Consultez Storage:Performances pour plus d'informations sur les performances.

La différence principale avec d'autres systèmes de bases de données est que sqlite ne gère pas les transactions imbriquées. C'est-à-dire qu'une fois une transaction ouverte, vous ne pouvez pas en ouvrir une autre. Vous pouvez vérifier si une transaction est en cours de traitement grâce à mozIStorageConnection.transactionInProgress.

Vous pouvez également exécuter les commandes SQL « BEGIN TRANSACTION » et « END TRANSACTION » directement (c'est ce que fait la connexion avec l'appel des fonctions). Cependant, il est fortement recommandé d'utiliser mozIStorageConnection.beginTransaction et des fonctions associées parce qu'elles mémorisent l'état de la transaction dans la connexion. Dans le cas contraire, l'attribut transactionInProgress aura une valeur erronée.

sqlite comprend différents types de transactions :

  • mozIStorageConnection.TRANSACTION_DEFERRED : par défaut. Le verrou sur la base de données est obtenu lorsque c'est nécessaire (normalement la première fois où vous exécutez une requête dans la transaction).
  • mozIStorageConnection.TRANSACTION_IMMEDIATE : verrouillage immédiat en lecture de la base de données.
  • mozIStorageConnection.TRANSACTION_EXCLUSIVE : verrouillage immédiat en écriture de la base de données.

Vous pouvez définir le type de la transaction en le transmettant par mozIStorageConnection.beginTransactionAs. Gardez en tête que si une autre transaction a déjà démarré, cette opération échouera. Habituellement, le type par défaut TRANSACTION_DEFERRED suffit, et à moins de savoir exactement ce que vous faites, vous n'aurez pas besoin des autres types. Pour plus d'informations, consultez la document sqlite sur BEGIN TRANSACTION et le verrouillage.

var ourTransaction = false;
if (mDBConn.transactionInProgress) {
  ourTransaction = true;
  mDBConn.beginTransactionAs(mDBConn.TRANSACTION_DEFERRED);
}

// ... utiliser la connexion ...

if (ourTransaction)
  mDBConn.commitTransaction();

Dans un code C++, vous pouvez utiliser la classe helper mozStorageTransaction définie dans storage/public/mozStorageHelper.h. Cette classe démarrera une transaction du type donné sur la connexion spécifiée lorsqu'elle rentre dans le contexte d'exécution, et fera une validation ou une annulation de la transaction lorsqu'elle sort du contexte. Si une autre transaction est en cours, la classe helper de transaction n'effectuera aucune action.

Elle dispose également de fonctions pour réaliser explicitement des validations. L'utilisation classique est de définir dans la classe un comportement d'annulation (rollback) par défaut, et d'ensuite valider explicitement la transaction si le processus a réussi :

nsresult uneFunction()
{
  // Définir (par défaut) la transaction avec une annulation en cas d'échec
  mozStorageTransaction transaction(mDBConn, PR_FALSE);

  // ... utiliser la connexion ...

  // tout s'est bien passé, alors validation explicite
  return transaction.Commit();
}

Comment corrompre votre base de données

  • Ouvrez plus d'une connexion vers le même fichier dont le nom n'est pas déterminé strictement identique par un strcmp, comme par exemple « my.db » et « ../dir/my.db » ou sous Windows (insensible à la casse) « my.db » et « My.db ». Sqlite essaiera de traiter chacun de ces cas, mais vous ne devriez pas compter là dessus.
  • Accédez à une base de données depuis un lien symbolique ou physique.
  • Ouvrez des connexions vers la même base de données depuis plus d'un thread (voir « Sécurité des threads » ci-dessous).
  • Accédez à une connexion ou une requête depuis plus d'un thread (voir « Sécurité des threads » ci-dessous).
  • Ouvrez la base de données depuis un programme externe pendant qu'elle est ouverte dans Mozilla. Le cache de Mozilla corrompt le fichier verrou normal dans sqlite qui devrait lui permettre de travailler en sécurité.

Verrous SQLite

SQLite verrouille la totalité de la base de données ; ainsi, lorsqu'une lecture active est en cours, toute tentative d'écriture recevra un SQLITE_BUSY, et lorsqu'une écriture active est en cours, toute tentative de lecture recevra un SQLITE_BUSY. Une requête est considérée comme active à partir de la première fonction step() jusqu'à ce que la fonction reset() soit appelée. execute() effectue ces deux opérations en une seule étape. Un problème courant est d'oublier de réinitialiser (reset()) une requête après avoir terminé la boucle step().

Bien qu'une connexion sqlite soit capable de gérer plusieurs requêtes ouvertes, son modèle de verrouillage limite ce qu'elles peuvent faire simultanément (lecture ou écriture). En fait, il est possible pour plusieurs requêtes d'être actives en lecture en même temps. Mais il n'est pas possible qu'elles puissent lire et écrire en même temps sur la même table — même si elles dérivent de la même connexion.

Sqlite a deux niveaux de verrou : un au niveau de la connexion et un au niveau de la table. La plupart des utilisateurs sont habitués au verrou du niveau connexion (base de données) : lecture multiple mais une seule écriture. Les verrous du niveau table (B-Tree) sont ce qui peut devenir moins clair (en interne, chaque table de la base de données dispose de son propre B-Tree, donc les « tables » et « B-Tree » sont techniquement synonymes).

Verrous au niveau de la table

Vous devez penser que si vous possédez une seule connexion qui verrouille la base de données en écriture, vous pouvez utiliser plusieurs requêtes pour faire ce que vous voulez. Ce n'est pas entièrement le cas. Vous devez considérer le verrou de niveau table (B-Tree) qui est maintenu par le gestionnaire de requêtes lors du parcours de la base de données (c'est-à-dire les requêtes d'ouverture SELECT).

La règle générale est la suivante : un gestionnaire de requête ne peut pas modifier une table (B-Tree) qu'un autre gestionnaire de requêtes est en train de lire (avec un pointeur ouvert dessus) — même si le gestionnaire partage la même connexion (contexte de transaction, verrou de base de données, etc.) avec d'autres gestionnaires. Toute tentative sera bloquée (ou retournera SQLITE_BUSY).

Ce problème se présente lorsque vous essayez de parcourir une table avec une requête en modifiant des enregistrements en même temps. Cela ne fonctionnera pas (ou aura une forte probabilité de ne pas fonctionner, selon les optimisations de performances utilisées (voir ci-dessous). La requête en écriture sera bloquée car la requête en lecture a un pointeur ouvert sur la table.

Résolutions des problèmes de verrouillage

La solution est de suivre la méthode (1) comme ce qui est décrit plus haut. Théoriquement, la méthode (2) ne fonctionne pas avec SQLite 3.x. Dans ce scénario, les verrous de la base de données s'ajoutent (avec de multiples connexions) aux verrous de table. La connexion 2 (connexion de modification) ne pourra pas modifier (écrire dans) la base de données pendant que la connexion 1 (connexion de lecture) est en train de la lire. La connexion 2 nécessite un verrou exclusif pour exécuter une commande SQL de modification, mais elle ne peut pas l'obtenir tant que la connexion 1 effectue ses requêtes de lecture sur la base (la connexion 1 a un verrou partagé pendant ce temps, ce qui exclut toute possibilité d'obtention d'un verrou exclusif).

Une autre option consiste à passer par une table temporaire. Créez une table temporaire qui contient les résultats intéressants de la table, parcourez la (ce qui place un verrou de lecture sur la table temporaire) et ensuite effectuez sans problème les modifications sur la table réelle par des requêtes d'écriture. Ceci peut être réalisé avec des requêtes dérivées d'une connexion unique (contexte de transaction). Ce scénario s'effectue parfois en arrière plan, comme dans le cas d'un tri ORDER BY qui génère des tables temporaires en interne. Il ne faut toutefois pas s'attendre à ce que l'optimiseur le fasse dans tous les cas. La création explicite d'une table temporaire est le moyen le plus sûr pour réaliser cette dernière option.

Sécurité des threads

Le service mozStorage et sqlite prennent en compte la sécurité des threads. Cependant, aucun des autres objets mozStorage ou sqlite, ou aucune des opérations n'est à considérer comme thread-safe.

  • Le service Storage doit être créé dans le thread principal. Si vous désirez accéder au service depuis un autre thread, assurez vous d'avoir appelé getService à l'avance depuis le thread principal.
  • Vous ne pouvez pas accéder à une connexion ou une requête depuis des threads différents. Ces objets Storage ne sont pas thread-safe, et les représentations qu'en fait sqlite ne le sont pas non plus. Même en vous assurant par verrouillage qu'un seul thread travaillera sur la base en même temps, il peut y avoir des problèmes. Ce cas n'a pas été testé, et il peut y avoir certains états internes par thread dans sqlite. Il est fortement déconseillé de faire cela.
  • Vous ne pouvez pas accéder à une unique base de données depuis plusieurs connexions provenant de différents threads. Normalement, sqlite le permet. Cependant, sqlite3_enable_shared_cache(1); a été activé (voir sqlite shared-cache mode), ce qui fait que les différentes connexions partagent le même cache. C'est un avantage important en termes de performances. En revanche, il n'y a pas de verrou sur les accès au cache, ce qui signifie sa corruption si vous le sollicitez depuis plusieurs threads.

Il convient cependant de noter que les auteurs d'extensions du navigateur en JavaScript seront moins affectés par ces restrictions qu'à première vue. Si une base de données est créée et utilisé exclusivement depuis JavaScript, les threads ne seront généralement pas un problème. SpiderMonkey (le moteur JavaScript de Firefox) exécute JavaScript depuis un seul thread persistant, sauf quand celui-ci s'exécute dans un thread différent ou depuis un callback venant d'un autre thread (par exemple via certaines interfaces réseau ou de flux). Si l'on exclut l'utilisation incorrecte de JavaScript multi-threads, il ne devrait y avoir de problèmes que si une base de données déjà utilisée par un thread système non JavaScript est ensuite accédée au travers de mozStorage.

Voir également

  • Storage:Performances Comment faire en sorte que votre connexion à la base de données fonctionne bien.
  • Extension Storage Inspector Facilite la consultation de toute base de données SQLite dans le profil courant.
  • SQLite Syntax Syntaxe de requêtes comprise par SQLite.
  • SQLite Database Browser est un outil gratuit disponible sur de nombreuses plateformes. Il peut être pratique pour examiner des bases de données existantes et tester des requêtes SQL.
  • Sqlite.jsm

Étiquettes et contributeurs liés au document

 Contributeurs à cette page : jmh, fscholz, BenoitL, Mgjbot, Chbok
 Dernière mise à jour par : jmh,