- Ajouter
STRICT à la fin de la définition d’une table permet de bloquer plus tôt les erreurs de type, comme du texte arbitraire dans une colonne entière, et de renforcer l’intégrité des données
- Lors des insertions et mises à jour, les types sont vérifiés, mais les valeurs convertibles sans perte comme
'123' restent autorisées, et les types de colonnes sont limités à INT, INTEGER, REAL, TEXT, BLOB, ANY
- Pour les colonnes devant accepter plusieurs types,
ANY permet de combiner validation stricte et stockage flexible au sein d’une même table
- Il n’est pas possible de convertir directement une table existante en STRICT ; il faut créer une nouvelle table et copier les données, avec nettoyage ou conversion des données invalides si nécessaire
- Les tables STRICT ne sont prises en charge qu’à partir de SQLite 3.37.0 et, même s’il existe en théorie un coût de vérification, des tests non officiels n’ont pas montré de différence nette de performances ni de taille de fichier
Créer une table STRICT
- Les tables STRICT de SQLite appliquent une vérification de type plus stricte, comparable à celle d’autres moteurs SQL
- Il suffit d’ajouter
STRICT à la fin de la définition CREATE TABLE
CREATE TABLE people (name TEXT) STRICT;
Vérification de type à l’insertion et à la mise à jour
- Dans une table SQLite classique, une colonne
INTEGER peut contenir du texte comme 'garbage', mais une table STRICT traite ce mismatch de type comme une erreur
CREATE TABLE people_nonstrict (age INTEGER);
INSERT INTO people_nonstrict (age) VALUES ('garbage');
-- traité normalement
CREATE TABLE people_strict (age INTEGER) STRICT;
INSERT INTO people_strict (age) VALUES ('garbage');
-- erreur : cannot store TEXT value in INTEGER column
- La même validation s’applique aussi à
UPDATE, ce qui empêche également les types invalides lors d’une modification après stockage
- En revanche, si une valeur peut être convertie sans perte, elle reste autorisée dans une table STRICT
- La chaîne
'123' peut être intégralement convertie en entier 123, donc les deux insertions ci-dessous sont traitées de la même manière
INSERT INTO people_strict (age) VALUES ('123');
INSERT INTO people_strict (age) VALUES (123);
Limitation des types dès la définition de la table
- Dans une table classique, on peut utiliser dans la déclaration d’une colonne des noms de type que SQLite ne prend pas réellement en charge
- Avec une table STRICT, utiliser un nom de type non pris en charge provoque une erreur dès la création
CREATE TABLE tbl (name GARBAGE) STRICT;
CREATE TABLE tbl (name DATETIME) STRICT;
CREATE TABLE tbl (name JSON) STRICT;
CREATE TABLE tbl (name UUID) STRICT;
CREATE TABLE tbl (name BLOBB) STRICT;
- Les seuls types de colonnes autorisés sont
INT, INTEGER, REAL, TEXT, BLOB, ANY
- Un type doit être indiqué pour chaque colonne ; il n’est donc pas possible de l’omettre comme dans
CREATE TABLE tbl (name)
Conserver de la flexibilité avec ANY
- Si vous devez stocker des données dont le type n’est pas fixe, vous pouvez utiliser une colonne
ANY
- Même dans une table STRICT, une colonne
ANY peut contenir tous les types : entiers, texte, nombres réels, BLOB, etc.
CREATE TABLE tbl (value ANY) STRICT;
INSERT INTO tbl (value) VALUES (123);
INSERT INTO tbl (value) VALUES ('text');
INSERT INTO tbl (value) VALUES (12.34);
INSERT INTO tbl (value) VALUES (X'8647');
Migration d’une table existante
- Il n’existe pas de méthode
ALTER pour convertir directement une table non stricte en table STRICT ; il est donc plus simple de créer les tables de manière stricte dès le départ
- Pour convertir une table existante, il faut passer par création d’une nouvelle table STRICT → copie des données → remplacement de l’ancienne table
CREATE TABLE new_people (name TEXT) STRICT;
INSERT INTO new_people SELECT * FROM people;
DROP TABLE people;
ALTER TABLE new_people RENAME TO people;
- Si les données existantes ont de mauvais types, par exemple du texte dans une colonne entière, la copie peut échouer
- Avant la migration, il peut être nécessaire de nettoyer les données ou d’utiliser
CAST pour les convertir
- Il est aussi possible de n’appliquer STRICT qu’aux nouvelles tables, mais cela introduit des niveaux de validation différents selon les tables, ce qui peut rendre le comportement moins prévisible qu’avec un schéma uniformément souple
Quand le typage flexible peut être approprié
- L’équipe de SQLite a aussi documenté séparément les avantages du typage flexible, avec des cas où le comportement par défaut est utile
- Une table non stricte peut être raisonnable pour :
- un stockage clé-valeur pur qui conserve différents types tels quels
- un espace destiné à stocker des attributs hétérogènes
- l’import direct de CSV désordonnés lorsqu’il faut préserver même les valeurs invalides sans perte
- Mais des types inattendus peuvent provoquer des bugs subtils ; pour des tables générales, il est souvent préférable d’obtenir une erreur immédiate plutôt que de les accepter silencieusement
- Le code source de SQLite contient aussi un commentaire qualifiant les tables non strictes de
legacy, mais c’est un indice moins fiable que la documentation officielle
Compatibilité des versions
- Les tables STRICT ont été introduites dans SQLite 3.37.0, publié en novembre 2021
- Elles ne peuvent pas être utilisées avec des versions antérieures de SQLite
- Les anciennes versions ne peuvent pas non plus lire une base de données contenant des tables STRICT
- Si vous créez une table STRICT avec une version récente puis ouvrez la même base avec SQLite 3.36.0, une erreur se produit
Performances et espace de stockage
- Les tables STRICT effectuent une vérification supplémentaire du type des données lors des insertions et mises à jour, ce qui pourrait en théorie les rendre plus lentes
- Dans des tests non officiels avec l’insertion de plusieurs millions de lignes dans une table de 100 colonnes, aucune différence de performances nette n’a été observée sur plusieurs machines
- La taille du fichier de base de données sur disque était également identique, même s’il ne s’agissait pas d’un benchmark rigoureux et que certaines différences ont pu passer inaperçues
- Il est aussi possible que le fait d’éviter l’enregistrement accidentel de valeurs incompatibles avec l’affinité des colonnes améliore au contraire les performances, mais cela n’a pas été vérifié séparément
Points à évaluer avant adoption
- Les tables STRICT ne résolvent pas tous les problèmes de données, mais elles réduisent les erreurs liées aux types et renforcent l’intégrité des données
- Dans la plupart des cas, il suffit d’ajouter
STRICT à la définition de la table, ce qui rend l’adoption simple
- Pour des tables classiques, les bénéfices d’une validation stricte des types l’emportent généralement sur le coût de la migration, l’incompatibilité avec les anciennes versions et la flexibilité réduite
1 commentaires
Commentaires sur Hacker News
Comme SQLite ne permet pas de passer une table en mode strict avec
ALTERet qu’il faut copier les données d’une table non stricte vers une table stricte, une conversion dans les deux sens a été ajoutée à sqlite-utils 4.1Dans le CLI, on peut l’utiliser avec
uvx sqlite-utils transform data.db mytable --strict, et en Python avecdb.table("mytable").transform(strict=True)Les notes de version sont disponibles sur https://sqlite-utils.datasette.io/en/stable/changelog.html#v..., la documentation de l’API Python sur https://sqlite-utils.datasette.io/en/stable/python-api.html#..., et la documentation du CLI sur https://sqlite-utils.datasette.io/en/stable/cli.html#transfo...
https://sqlite.org/flextypegood.html explique pourquoi le typage flexible est la valeur par défaut et pourquoi cela ne changera probablement pas, mais cela ne correspond pas à l’expérience réelle
Contrairement à l’idée qu’on pourrait facilement repérer et corriger le fait d’avoir mis le nom d’un client dans l’entier
Customer.creditScore, une ligne corrompue est difficile à récupérer et les données ont peut-être complètement disparuIl est aussi très difficile d’adhérer à l’argument selon lequel des vérifications de type strictes ne bloqueraient que des erreurs faciles à trouver et rendraient donc la détection et la correction des bugs plus difficiles
Dans Postgres, on a plutôt tendance à ajouter autant de vérifications et de garde-fous que possible, ce qui évite ensuite d’avoir à traquer des erreurs qui n’auraient jamais dû se produire
Cela ressemble moins à un principe d’ingénierie rigoureux qu’au produit de l’époque où SQLite a été conçu et des convictions très fortes de son auteur, avec l’impression qu’après de nombreuses critiques, il maintient sa position en mobilisant n’importe quelle justification
Des types comme le JSON ou HSTORE de Postgres permettent d’obtenir la flexibilité souhaitée, mais il est presque toujours préférable de la proposer comme un choix quand on en a besoin plutôt que d’imposer par défaut un typage illimité
Ce serait bien que STRICT soit la valeur par défaut
Mis à part ce point, le développeur de SQLite est une personne remarquable qui a créé un excellent outil
PRAGMA foreign_keys = ON;Plus gênant encore, il n’existe pas de pragma équivalent pour les tables strictes, donc il faut ajouter le
STRICTnon standard à chaqueCREATE TABLEUn pragma global STRICT a bien été envisagé, mais n’a pas été implémenté ; on peut consulter https://sqlite.org/foreignkeys.html et https://sqlite.org/forum/forumpost/1b9d073a37ca5998
L’idée est d’éviter qu’un logiciel prévu pour SQLite 3.53, une fois mis à niveau vers 3.54, se mette soudainement à créer des tables strictes avec
CREATE TABLE, provoque des erreurs, puis casse l’ensembleOn en vient à se demander si, à une époque, SQLite ne prenait en charge que la syntaxe des clés étrangères sans en implémenter réellement la fonctionnalité
Il a fallu nettoyer du code déployé sur des milliers d’appareils qui stockait les chaînes
'1'et'0'dans une colonne booléenne, ce qui n’avait rien d’agréableIl n’existe pas non plus de type horodatage, donc il faut les stocker dans des colonnes texte, et même les fonctions standard de date et d’heure produisent
yyyy-mm-dd HH:MM:SS, qu’il faut supposer implicitement en UTC, au lieu du format ISOyyyy-mm-ddTHH:MM:SSZSQLite est vraiment un excellent projet, mais certaines décisions de conception sont déconcertantes
En venant d’environnements SQL d’entreprise, je n’ai jamais vraiment pris SQLite au sérieux parce que les types de champs n’y sont pas imposés par défaut, et j’ai aussi été surpris quand il est devenu la base des métadonnées d’applications sur smartphone
Cela me rappelle cette vieille histoire réseau où l’on choisit UDP pour sa faible latence et sa simplicité, puis où l’on reconstruit directement dans l’application la plupart des mécanismes de fiabilité de TCP
Mais je ne vois pas en quoi réimplémenter soi-même la vérification de types au-dessus d’un SQLite non strict apporterait des avantages comparables
PostgreSQL aussi vise par défaut des systèmes modestes, donc il faut le configurer pour obtenir les meilleures performances
De plus, TCP est fondé sur les flux, ce qui entraîne des problèmes de blocage HOL dans beaucoup d’applications, et si l’on construisait soi-même une couche de fiabilité sur UDP, on pourrait au final obtenir de meilleurs résultats que TCP malgré tout le travail supplémentaire
Ils vont dire qu’elles sont bruyantes et se conduisent mal, mais c’est parce qu’ils jugent avec leur propre cas d’usage un produit conçu dès le départ pour autre chose
Les tables strictes ont l’inconvénient de ne pas permettre certains types comme Date, mais elles devraient quand même être la valeur par défaut
Si plusieurs applications partagent une base de données, il faut pouvoir faire confiance aux types déclarés, et si l’une stocke une chaîne dans une colonne numérique, toutes les autres sont cassées
À l’inverse, dans le cas d’usage principal de SQLite comme base embarquée, il n’y a généralement qu’une seule application, donc il y a aussi l’avantage de pouvoir faire évoluer le schéma au lieu de créer une nouvelle base et copier les données
Dans ce cas, le code de l’application sait à quoi s’attendre dans chaque colonne, y compris en présence de types mixtes
INTEGER,TEXT,BLOB,REAL,NUMERIC: https://sqlite.org/datatype3.htmlEXPLAINsur une requêteSELECTarbitraire pour récupérer le nom de type indiqué de façon purement formelleDonc même si vous utilisez des noms comme
DATEouDATETIME, on ne peut même pas les inférerDaten’est pas un vrai type, cela crée seulement une colonne avec une affinité numériqueLe mode strict limite la notation des types de colonnes et empêche d’utiliser des noms plus parlants ; du coup, le code ne peut pas exploiter ces noms lorsqu’il fait la correspondance entre types de base de données et types applicatifs, ce qui entrave au contraire un système de types plus strict au niveau de l’application
Plus de détails ici : https://hn.algolia.com/?query=chrismorgan+strict+sqlite&type... ; si vous manipulez la base avec des crates comme sqlx en Rust, je pense qu’il vaut mieux éviter le mode strict
Je veux des types stricts pour les bases de données et les RPC, mais SQLite a un usage un peu différent, et en l’utilisant directement vous comprendrez peut-être mieux https://sqlite.org/flextypegood.html
Par exemple, viser le fait qu’un script arbitraire non écrit pour SQLite puisse quand même fonctionner par hasard est une propriété que les autres SGBD ne prennent généralement pas en compte
Le principal avantage du typage flexible est de faire évoluer facilement le schéma
Quand les besoins d’une base embarquée changent, on peut modifier le schéma sur place au lieu de créer une nouvelle base et de migrer les données, et si une seule application lit et écrit, ce n’est pas surprenant qu’une chaîne apparaisse dans une colonne entière
À l’inverse, si plusieurs applications se mettent à jour chacune à leur rythme et partagent la même base de données, le schéma est un contrat, donc un seul type erroné peut casser les autres applications
Si une table créée par une application est utilisée par une autre, les types de données convenus doivent être respectés strictement
Revérifier à l’exécution n’apporte pas de gain réel, et un code que l’analyse statique a confirmé comme insérant une chaîne ne va pas muter soudainement pour insérer un entier
SQLite n’est pas comme Postgres, où plusieurs applications partagent les données et où il faut faire confiance à des tiers
Dans un environnement comme Postgres, la validation à l’exécution est indispensable, mais SQLite est fondamentalement conçu pour une application et une base de données, donc il suffit de faire confiance à son propre code que l’on peut évaluer à la compilation
Dans la situation atypique où plusieurs applications partagent un seul fichier, on peut activer les tables strictes
Le typage dynamique peut convenir à des usages comme un simple magasin clé-valeur, mais il faut se demander laquelle des deux options respecte le plus le principe de moindre surprise : que
INTEGERaccepte'hello world'sans erreur, ou qu’il faille expliciter un mot-clé commeNONSTRICTou un typeANYpour pouvoir insérer une telle valeurJ’imagine que la grande majorité des utilisateurs de SQLite ne s’attendent pas à ce que la première possibilité existe
Cela correspond aussi à l’explication selon laquelle on espère que du code prévu pour d’autres SGBD fonctionnera aussi par hasard dans SQLite
Malgré tout, c’est très surprenant qu’un utilisateur qui n’a pas omis le type puisse écrire
INTEGERet y insérer du texteIl m’est déjà arrivé qu’un UUID commençant par une valeur comme
08123…soit interprété en octal, ce qui a converti à tort une partie en nombreC’était déroutant et pénible, mais j’ai réglé le problème en appliquant
STRICTet en recréant toute la tableSQLite n’a pas de type octal
J’utilise par défaut
CREATE TABLE ... STRICT WITHOUT ROWID, et je ne vois pas vraiment de raison de faire autrement