SQLite a aussi besoin d’éditions à la Rust
(mort.coffee)- SQLite est un RDBMS autonome largement utilisé pour le stockage local et les projets embarqués, mais il conserve des valeurs par défaut défavorables à la sécurité et aux performances pour des raisons de rétrocompatibilité
- Par défaut, il n’impose pas les contraintes de clés étrangères et peut réutiliser les
ROWID, ce qui peut créer des problèmes d’intégrité des données où les publications d’un utilisateur supprimé se retrouvent associées à un autre utilisateur ayant reçu le même ID - Les tables ordinaires stockent aussi des types de données différents de ceux des colonnes déclarées ; les tables STRICT l’empêchent, mais il faut indiquer
strictpour chaque table, ce qui entre en conflit avec les usages fondés sur des noms de types personnalisés - Le
SQLITE_BUSYimmédiat lors d’écritures concurrentes, le WAL désactivé et les réglages de synchronisation conservateurs peuvent être améliorés respectivement via les pragmasbusy_timeout,journal_modeetsynchronous - Permettre de choisir un ensemble de valeurs par défaut sûres via des éditions basées sur l’année, comme
PRAGMA edition = 2026, permettrait de faire évoluer les réglages par défaut de SQLite sans casser les comportements existants
Les forces de SQLite et ses valeurs par défaut vieillissantes
- SQLite peut être considéré comme la base de données des projets embarqués et le standard de fait du stockage local ; certains logiciels serveur, comme lobste.rs, fonctionnent aussi sur SQLite
- Contrairement aux RDBMS traditionnels exécutés comme processus séparés, c’est un RDBMS sous forme de bibliothèque, ce qui permet de garder un logiciel autonome
- Comme avec un format de fichier ordinaire, il n’est pas nécessaire d’écrire un sérialiseur et un parseur personnalisés ; SQLite offre donc à la fois les avantages d’un RDBMS et ceux du stockage fondé sur des fichiers
- Mais plusieurs valeurs par défaut inadaptées continuent de poser problème, car elles affectent l’intégrité des données, la concurrence et les performances
Problèmes d’intégrité causés par l’absence d’application des clés étrangères
- Les clés étrangères sont un moyen essentiel de maintenir la cohérence d’une base de données et d’éviter les références cassées, mais SQLite ne les impose pas par défaut
- Même si
posts.user_idest déclaré comme référence àusers.id, la configuration par défaut n’empêche pas les comportements suivants- créer une publication sans ID utilisateur valide
- supprimer un utilisateur tout en conservant ses publications
INTEGER PRIMARY KEYdevient un alias duROWIDde la table, et l’algorithme d’attribution des ROWIDde SQLite réutilise des ID dans certaines situations- Si Bob écrit une publication avec l’ID
1puis supprime seulement son compte, aucune erreur de clé étrangère ne se produit - Si Alice reçoit ensuite l’ID
1réutilisé, les anciennes publications de Bob se retrouvent associées à Alice - Comme les requêtes elles-mêmes réussissent normalement, le problème est plus difficile à détecter qu’une simple référence cassée
- Si Bob écrit une publication avec l’ID
- On peut imposer les clés étrangères en activant le pragma suivant pour chaque connexion
PRAGMA foreign_keys = ON;
- Si cela avait été appliqué dès le départ, la suppression d’un utilisateur ayant encore des publications aurait été interrompue par une erreur
FOREIGN KEY constraint failed
Les tables ordinaires acceptent des types différents de ceux déclarés
- Les valeurs SQLite sont de type
NULL,INTEGER,REAL,TEXTouBLOB, mais la déclaration des colonnes d’une table ordinaire ne limite pas strictement les types stockables : elle détermine une affinité de type (type affinity) - Les règles de stockage suivantes s’appliquent à une colonne d’affinité
INTEGER- un
TEXTinterprétable comme un entier est converti enINTEGER - un
TEXTinterprétable comme un réel est converti enREAL, un flottant double précision - les autres valeurs sont stockées telles quelles, avec leur type d’origine
- un
- Les autres affinités ont leurs propres règles de conversion
- l’affinité
BLOBstocke les valeurs telles quelles - l’affinité
TEXTconserveBLOB,TEXTetNULLtels quels, et convertit les nombres enTEXT - l’affinité
REALse comporte comme l’affinitéINTEGER, mais convertit les entiers enREAL
- l’affinité
- Ainsi, même dans une colonne
duration_sec INTEGER, on peut stocker une chaîne non numérique comme'Way too long, I mean come on' - Dans un projet réel, il est aussi arrivé que des chaînes
'1'et'0'soient enregistrées au lieu des nombres1et0utilisés comme booléens, ce qui a nécessité nettoyage des données et débogage
Tables STRICT et conflit avec les types personnalisés
- Les tables STRICT lèvent des erreurs de type, comme
cannot store TEXT value in INTEGER column, lorsqu’une valeur du mauvais type est insérée dans une colonne
CREATE TABLE music (
id INTEGER PRIMARY KEY,
name TEXT,
duration_sec INTEGER
) strict;
-
Comme il n’existe pas de pragma rendant globalement toutes les tables strictes, il faut ajouter
strictà chaque instructionCREATE TABLEsans exception -
SQLite défend l’idée que les types flexibles sont utiles, mais même dans une table STRICT, il est possible de créer une colonne acceptant toutes sortes de valeurs en déclarant le type
ANY- On peut distinguer explicitement les colonnes autorisant n’importe quel type de celles qui imposent un type précis
-
Le problème de compatibilité le plus concret est que les tables STRICT ne se contentent pas d’imposer les types : elles changent aussi les règles d’interprétation des spécificateurs de type
-
Comment les tables ordinaires interprètent les noms de types
- Les tables SQLite ordinaires déterminent l’affinité à partir de la chaîne de type déclarée
- Si elle contient
"INT", elle est traitée commeINTEGER - Si elle contient
"CHAR","CLOB"ou"TEXT", elle devientTEXT - Si elle contient
"BLOB", ou si le type est omis, elle est traitée commeBLOB - Si elle contient
"REAL","FLOA"ou"DOUB", elle devientREAL - Si aucune condition ne s’applique, elle est traitée comme
NUMERIC - En combinant ce typage souple avec ces règles, on peut attacher aux colonnes des noms de types personnalisés comme
DATETIME,KEY_VALUE_SETouCOLOR - Les connecteurs ou wrappers de base de données peuvent regarder le nom du type pour sérialiser et désérialiser automatiquement
- Même sans fonctionnalité dédiée, le nom du type documente en lui-même les données attendues dans la colonne
- Passer simplement STRICT en valeur par défaut obligerait à renoncer à cet usage ; il faudrait donc des alias explicites de types personnalisés
CREATE TYPE KEY_VALUE_SET = TEXT;- Si des alias comme
KEY_VALUE_SETpouvaient être utilisés dans les tables strictes, les schémas pourraient aussi consigner le motif de données attendu pour les colonnesTEXTque l’application doit parser - Une fonctionnalité permettant d’associer des contraintes
CHECKaux types personnalisés serait aussi utile - La norme SQL:1999 comprend déjà une syntaxe d’alias de type prenant en charge les contraintes,
CREATE DOMAIN; SQLite pourrait donc l’implémenter en prenant en charge cette instruction standard
Écritures concurrentes et SQLITE_BUSY
- SQLite autorise plusieurs lectures concurrentes, mais ne traite qu’une seule écriture à la fois
- Avec la configuration par défaut, lorsque deux processus tentent simultanément d’obtenir un verrou d’écriture, l’un d’eux reçoit immédiatement une erreur
SQLITE_BUSYau lieu d’attendre- Comme une écriture qui effectue des I/O disque peut naturellement prendre du temps, il est plus logique d’attendre un certain délai que le verrou soit libéré
- Il est déjà arrivé que des systèmes réels s’interrompent par intermittence, obligeant à écrire soi-même une boucle de retry
- Le réglage suivant ne renvoie
SQLITE_BUSYqu’après avoir réessayé d’obtenir le verrou pendant au maximum 5 secondes
PRAGMA busy_timeout = 5000;
- En exploitation normale, il est préférable de concentrer toutes les écritures dans un seul processus, si possible dans un seul thread, et les écritures concurrentes ne peuvent de toute façon pas être rapides
- Il existe toutefois des cas d’exploitation légitimes où des écritures concurrentes sont nécessaires
- nettoyer une base de données de manière interactive avec l’outil en ligne de commande
sqlite3 - exécuter des scripts pour des tâches d’administration rares sans frontend séparé
- nettoyer une base de données de manière interactive avec l’outil en ligne de commande
- À cause des réglages par défaut, ces opérations peuvent provoquer un
SQLITE_BUSYinattendu et interrompre le logiciel en cours d’exécution
WAL et valeurs par défaut de performance
- Bien configuré, SQLite peut être assez rapide pour remplir une partie des rôles habituellement confiés à de gros serveurs comme PostgreSQL ou MySQL, mais ses performances par défaut ne sont pas bonnes
- On trouvera plus de détails sur l’optimisation en environnement serveur dans Optimizing SQLite for servers
- Le principal problème de valeur par défaut concerne le Write-Ahead Log, c’est-à-dire le fait que le WAL est désactivé
PRAGMA journal_mode = WAL;
- Dans la plupart des situations, WAL augmente fortement la vitesse d’écriture
- Utiliser aussi le réglage suivant permet de réduire fortement le nombre de synchronisations disque sans risque de corruption des données
PRAGMA synchronous = NORMAL;
- Le comportement précis de
synchronousest décrit dans la documentation des pragmas SQLite
Éditions SQLite basées sur l’année
- Changer directement les valeurs par défaut actuelles casserait d’anciens logiciels et pourrait rendre les utilisateurs méfiants vis-à-vis des futures mises à jour de SQLite
- Pour conserver la rétrocompatibilité tout en permettant de choisir de meilleures valeurs par défaut, on pourrait ajouter un super pragma
PRAGMA edition = 2026;
- L’édition
2026agirait au minimum comme un alias pour les réglages suivants
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
- Dans cette édition, le mode
strictserait aussi appliqué par défaut aux nouvelles tables - Comme les éditions de Rust, ce mécanisme permettrait de choisir explicitement de nouveaux comportements par défaut tout en préservant le comportement du code existant
- Contrairement à un mode unique comme
"use strict";en JavaScript, les éditions basées sur l’année peuvent faire évoluer, au fil du temps, l’ensemble des valeurs par défaut jugées raisonnables - Par exemple, si WAL2 de Hctree était intégré à la branche principale en 2034, un futur
PRAGMA edition = 2034pourrait être configuré pour choisirjournal_mode = WAL2 - Un système d’éditions pourrait constituer un compromis permettant à SQLite de faire évoluer ses valeurs par défaut de sécurité et de performance sans casser le comportement par défaut des programmes existants
2 commentaires
La théorie paraît intéressante, mais si on prend en compte les conflits de compatibilité et le surcoût, j’ai l’impression que ce serait plutôt une perte.
SQLite est un SGBD né pour fonctionner dans des environnements embarqués comme Aegis,
et dans le monde de l’embarqué, on caste des
void**à tout-va et on redéfinit des fonctions après une déclaration__weak__. Dans un tel univers, je me demande vraiment si c’est nécessaire.Avis sur Lobste.rs
J’ai écrit ce billet après avoir vu hier la nouvelle indiquant que lobste.rs était passé à SQLite (https://lobste.rs/s/ko1ji1/lobste_rs_is_now_running_on_sqlite)
En lisant la discussion de l’époque, je me suis rappelé les petits désagréments des valeurs par défaut de SQLite et j’en ai fait un billet de blog
Si tu as le temps, ça vaudrait le coup de partager ce billet aussi sur le forum SQLite : https://sqlite.org/forum/forum
En SQL,
CREATE TYPEsert à créer des types composites, et Postgres prend en charge avec cela les plages, les énumérations, les tableaux, ainsi que des types de base assez complexesLa fonctionnalité souhaitée se rapproche plutôt de
CREATE DOMAIN. On peut créer un nouveau type (newtype) à partir d’un type concret de base et aussi définir des valeurs par défaut et des contraintes, ce qui rend beaucoup plus concis un schéma où l’on répète la même contrainteCHECKdans plusieurs tables. Par exemple, on peut écrirecreate domain mything as text;oucreate domain mything as integer not null check (VALUE < 5);Il faudrait aussi traiter un piège assez dangereux :
busy_timeoutseul ne permet pas d’éviter les erreursSQLITE_BUSYqui surviennent immédiatementSi l’on démarre une transaction avec une requête en lecture seule, puis qu’on exécute une requête d’écriture pour tenter de promouvoir le verrou détenu, et qu’une autre écriture est en cours, cela échoue immédiatement, indépendamment du réglage de
busy_timeout. Pour résoudre le problème, il faut utiliserbegin immediateIl faut tenir compte du fait que certains des paramètres regroupés dans l’édition proposée sont, dans le modèle de SQLite, des propriétés de connexion, tandis que les autres sont des propriétés du fichier de base de données ou de chaque table
PRAGMA journal_mode = WALest conservé dans le fichier de base de données, maisPRAGMA foreign_keys = ONdoit être défini à chaque connexion.STRICTest une propriété de chaque table, ce qui permet de mélanger, dans une même base de données, des tables strictes et d’autres qui ne le sont pas. Il faut donc décider si ce PRAGMA unifié doit être défini comme une propriété du fichier de base de données ou de la connexion. Je me demande aussi s’il existe un mécanisme intégré qui avertit pour éviter qu’une ancienne version de la bibliothèque ne modifie et ne casse un fichier contenant des fonctionnalités qu’elle ne connaît pasL’approche par éditions semble globalement être un bon compromis entre l’objectif des développeurs de SQLite de préserver la rétrocompatibilité et des valeurs par défaut moins déroutantes pour les utilisateurs qui veulent les réglages modernes recommandés
Je suis récemment devenu à fond sur RocksDB. C’est une bibliothèque de base de données embarquée comme SQLite, mais je considère justement que l’absence de SQL est un gros avantage
La contrainte consistant à devoir exprimer toutes les opérations en SQL me semble surtout gêner la manipulation de la base de données
J’ai du mal à imaginer comment migrer vers RocksDB, ou une autre base de données clé-valeur, un système qui utilise SQL comme lobste.rs
Je ne suis pas d’accord pour dire que définir
busy_timeoutest la solution. SQLite est très performant lorsqu’on dispose d’un unique thread d’écriture et qu’on traite par lots des transactions logiques indépendantes les unes des autresLa méthode précise est expliquée ici : https://andersmurphy.com/2025/12/…
Mais il est aussi courant de modifier directement la base de données pour des tâches de nettoyage, ou d’exécuter en script des opérations d’administration ponctuelles qui ne justifient pas la création d’une interface d’administration séparée. Dans ce cas, il n’est pas souhaitable qu’un serveur ou une application puisse s’arrêter de façon aléatoire simplement parce que, par malchance, les moments d’exécution se sont chevauchés