2 points par GN⁺ 2023-09-19 | 1 commentaires | Partager sur WhatsApp
  • Si l’on plaçait OpenDocument Presentation (ODP) dans un conteneur SQLite plutôt que dans une archive ZIP, on pourrait concevoir des mécanismes d’enregistrement, de démarrage et de récupération de documents plus sûrs et plus rapides
  • Aujourd’hui, ODP regroupe des fichiers XML et des images dans une archive ZIP ; un exemple de présentation de 49 diapositives se compose de 78 éléments au total, dont content.xml, styles.xml, meta.xml, settings.xml et des images
  • Dans une structure fondée sur ZIP, même une petite modification conduit facilement à réécrire toute l’archive, ce qui rend les mises à jour incrémentales difficiles et entraîne des délais lors de File/Save ainsi qu’une augmentation des écritures sur SSD
  • En passant à SQLite, les fichiers peuvent être stockés sous forme de lignes de table ; on peut aller plus loin en séparant le contenu et les versions par diapositive, afin de ne lire que la première diapositive ou de n’enregistrer que celles qui ont changé
  • Il ne s’agit pas de critiquer OpenDocument ni de proposer de le modifier, mais de montrer, à travers un exemple, que SQLite peut faciliter la mise en place de fonctions d’enregistrement atomique, d’accessibilité, de gestion de versions et de récupération dans les formats de fichiers applicatifs

Portée et objet de l’expérience de pensée

  • L’objet est ODP (OpenDocument Presentation), le format de documents de présentation au sein d’OpenDocument
  • L’objectif n’est pas de modifier réellement OpenDocument, mais d’examiner, pour de futurs formats de fichiers, l’utilisation de SQLite comme conteneur
  • Les bénéfices attendus sont des documents plus petits, un File/Save plus rapide, un démarrage plus rapide, une consommation mémoire réduite, la gestion de versions des documents et une meilleure expérience utilisateur

Structure actuelle des fichiers ODP

  • Un fichier ODP est une archive ZIP contenant des fichiers XML et des ressources image
  • À titre d’exemple, le fichier d’une présentation de 49 diapositives sur SQLite donnée au SouthEast LinuxFest 2014 contient 78 éléments au total dans la sortie de zip -l
    • Les quatre fichiers XML content.xml, styles.xml, meta.xml et settings.xml définissent la mise en page des diapositives, le contenu textuel et les styles
    • Le fichier de présentation contient 62 images, de la photo plein écran aux petites icônes, stockées sous forme de fichiers séparés
    • Le fichier mimetype contient une seule ligne : application/vnd.oasis.opendocument.presentation
  • Les fichiers de traitement de texte et de feuille de calcul OpenDocument ont une structure similaire, mais l’analyse porte ici sur ODP

Limites d’ODP fondé sur ZIP

  • Une archive ZIP s’apparente à une base de données clé/valeur optimisée pour une écriture unique et de multiples lectures, et convient aux structures où un petit nombre de clés ont de grandes valeurs BLOB
  • Comme la mise à jour d’éléments individuels est difficile, lorsque l’utilisateur choisit File/Save, toute l’archive ZIP est généralement réécrite
    • Il est possible de mettre à jour des éléments individuels sans corrompre tout le document en cas de coupure de courant ou de crash, mais c’est suffisamment difficile pour être rarement utilisé en pratique
    • Dans une présentation de 50 Mo, la modification d’un seul caractère peut conduire à réécrire les 50 Mo du fichier
  • Le temps de démarrage peut être lent
    • ODP stocke tout le contenu des diapositives dans un gros fichier XML unique appelé content.xml
    • LibreOffice lit et parse l’intégralité de ce fichier pour afficher la première diapositive
    • Il semble aussi charger toutes les images en mémoire ; par conséquent, lorsque l’on double-clique sur le fichier, une barre de progression apparaît au lieu de la première diapositive
  • La consommation mémoire augmente
    • La structure ZIP incite à une implémentation qui lit tout le document en mémoire au démarrage, effectue l’édition en mémoire, puis écrit tout le document sur disque lors de l’enregistrement
    • Un fichier de présentation de 50 Mo peut utiliser plus de 200 Mo de RAM
    • Si plusieurs présentations sont ouvertes en même temps, en plus d’un navigateur et d’applications de bureau, du swapping peut se produire
  • La récupération après crash devient contraignante
    • La famille OpenOffice sauvegarde périodiquement en arrière-plan le document présent en mémoire pour se prémunir des crashs
    • Pendant la sauvegarde, l’application peut se figer quelques secondes, et après redémarrage il faut passer par une boîte de dialogue de récupération séparée
  • L’accessibilité du contenu est faible
    • Les outils ZIP permettent d’extraire les images, mais il est difficile d’extraire ou de modifier le texte des diapositives avec des outils ordinaires
    • Dans le fichier d’exemple, la première ligne de content.xml est une déclaration XML, et la deuxième ligne contient 211 792 caractères de XML sur une seule ligne

Première amélioration : remplacer ZIP par SQLite

  • La première étape consiste en une structure simple qui remplace les éléments ZIP par des lignes d’une table SQLite
CREATE TABLE OpenDocTree(
  filename TEXT PRIMARY KEY,
  filesize BIGINT,
  content BLOB
);
  • À ce stade, le reste de la structure du format de fichier ne change pas
    • Cela reste une structure de « tas de fichiers », mais chaque fichier devient une ligne dans une base de données SQLite plutôt qu’une entrée ZIP
  • La comparaison de taille entre le fichier self2014.odp créé par NeoOffice et le fichier SQLite réemballé avec l’utilitaire SQLAR donne les résultats suivants
    • self2014.odp : 10 514 994 octets
    • self2014.sqlar : 10 464 256 octets
    • zip.odp, recompressé avec zip en ligne de commande : 10 416 644 octets
  • Le fichier SQLite était environ 0,5 % plus petit que l’ODP généré par NeoOffice
    • Le fichier ZIP bien compressé avec zip en ligne de commande était à son tour environ 0,5 % plus petit que SQLite
    • Les bases de données SQLite peuvent rivaliser avec les archives ZIP en matière de taille
  • SQLite fournit des écritures atomiques, ce qui permet d’enregistrer des changements incrémentaux sans risque de corrompre le document en cas de crash ou de coupure de courant
    • La limite consistant à devoir réécrire l’intégralité de content.xml demeure
    • Mais les 77 autres fichiers peuvent rester inchangés, ce qui accélère File/Save et réduit le volume d’écritures sur SSD

Deuxième amélioration : découper le contenu en petits morceaux

  • SQLite pouvant stocker efficacement aussi bien de gros blocs que de nombreux petits morceaux, on peut créer une table de contenu par diapositive
CREATE TABLE slide(
  pageNumber INTEGER,
  slideContent TEXT
);
CREATE INDEX slide_pgnum ON slide(pageNumber);
  • Pour afficher le premier écran, l’application n’a qu’à lire la première diapositive
SELECT slideContent FROM slide WHERE pageNumber=1;
  • Avec cette structure, il est possible de récupérer, parser et afficher rapidement le seul contenu de la première diapositive, sans lire tout content.xml au démarrage
  • Les choix d’implémentation se multiplient
    • Après avoir affiché la première diapositive, le reste des pages peut être lu dans un thread d’arrière-plan
    • Une seule diapositive, celle en cours, peut être conservée en mémoire
    • Pour accélérer les transitions, on peut aussi garder en mémoire la diapositive courante et la suivante
  • L’enregistrement devient également plus rapide, puisqu’il suffit de réécrire les pages modifiées
  • Pour de courts fragments de texte, l’efficacité de la compression peut diminuer et faire augmenter la taille du document
    • Mais comme les images occupent l’essentiel de l’espace du document, la baisse d’efficacité de la compression du texte peut être considérée comme un coût modeste au regard de l’amélioration de l’expérience utilisateur

Troisième amélioration : gestion de versions

  • Si les diapositives sont stockées comme des objets individuels, on peut intégrer un historique de versions dans le même document
CREATE TABLE slide(
  slideId INTEGER PRIMARY KEY,
  derivedFrom INTEGER REFERENCES slide,
  content TEXT
);
CREATE TABLE version(
  versionId INTEGER PRIMARY KEY,
  priorVersion INTEGER REFERENCES version,
  checkinTime DATETIME,
  comment TEXT,
  manifest TEXT
);
  • Chaque diapositive possède un slideId unique plutôt qu’un numéro de page, et l’ordre est déterminé par la liste de slideId stockée dans le manifest de la table version
  • Au démarrage, l’application choisit d’abord la version à afficher et, en général, récupère la version la plus récente
SELECT manifest, versionId FROM version ORDER BY versionId DESC LIMIT 1;
  • Il est aussi possible de récupérer la version la plus récente selon checkinTime
SELECT manifest, versionId, max(checkinTime) FROM version;
  • Dans SQLite, la requête max(checkinTime) ci-dessus renvoie un résultat défini, mais dans beaucoup d’autres bases de données SQL elle peut renvoyer un résultat non défini ou produire une erreur
  • Lorsque l’utilisateur effectue File/Save, seules les diapositives modifiées peuvent être ajoutées sous forme de nouvelles lignes dans la table slide, et une nouvelle ligne version avec le manifest modifié peut être créée
  • La table version enregistre l’heure de check-in, le commentaire utilisateur et la version parente afin de préserver l’historique des changements
  • Il devient aussi possible de stocker plusieurs présentations dans le même document
  • En utilisant une version spéciale pending au lieu d’un fichier de sauvegarde séparé, les changements non enregistrés peuvent être consignés fréquemment et silencieusement
    • Comme on n’écrit que les différences plutôt que tout le document, l’opération écrit quelques Ko au lieu de plusieurs Mo
    • Le temps d’enregistrement peut se compter en millisecondes plutôt qu’en secondes
    • Même après un crash et un redémarrage, la majeure partie, voire la quasi-totalité, du travail de l’utilisateur peut être conservée
    • Si l’utilisateur veut abandonner des changements non enregistrés, il suffit de revenir à une version précédente

Autres possibilités avec un format de fichier SQLite

  • Un conteneur SQLite peut ajouter des fonctionnalités importantes à un format de fichier applicatif avec seulement trois tables
  • On peut aussi exploiter schémas, index, triggers, vues et contraintes pour améliorer les performances, la commodité et la cohérence
  • Quelques idées d’extensions
    • Stocker une pile undo/redo automatique dans les tables de la base de données, afin de pouvoir revenir jusqu’aux sessions d’édition précédentes
    • Ajouter une fonction de recherche en texte intégral à un jeu de diapositives, ou à plusieurs jeux de diapositives
    • Décomposer settings.xml en tables SQL pour le rendre plus facile à consulter et à modifier depuis d’autres applications
    • Séparer les notes de présentateur de chaque diapositive dans une table dédiée, afin que les applications ou scripts tiers puissent y accéder facilement
    • Au-delà d’un ordre linéaire simple des diapositives, prendre en charge des structures de présentation avec des chemins différents et des dérivations selon les réactions du public

Réticences fréquentes à propos de SQLite et réponses

  • L’expérience des bases de données SQL d’entreprise peut susciter des réticences à utiliser SQLite comme format de fichier applicatif
  • Beaucoup de bases de données d’entreprise recommandent de ne pas mettre de grandes chaînes ou des BLOB dans la base, mais de les stocker dans des fichiers séparés ; SQLite est différent
    • N’importe quelle colonne SQLite peut stocker une chaîne ou un BLOB d’environ 1 Go
    • Pour les chaînes et BLOB de 100 Ko ou moins, les performances d’I/O sont meilleures qu’avec des fichiers séparés
  • L’idée que tous les schémas SQL devraient être en troisième forme normale (3NF) et ne stocker que de petits types primitifs peut aussi être limitante
    • La théorie relationnelle est importante, mais dans un format de fichier réel, stocker des informations complexes comme du XML ou du JSON dans un champ texte peut être un choix acceptable

SQLite comme format de fichier applicatif

  • Un fichier de base de données SQLite a presque la même taille qu’une archive ZIP contenant les mêmes informations, et peut parfois être plus petit
  • Grâce aux mises à jour atomiques, il est possible d’écrire de petits changements dans le document en toute sécurité, ce qui réduit les I/O disque et améliore les performances de File/Save
  • L’application peut réduire le temps de démarrage en ne lisant que le contenu nécessaire au premier écran
  • Elle peut fortement réduire la consommation mémoire en ne gardant en mémoire que le contenu lié à l’affichage actuel et en laissant le reste sur disque
  • Un schéma SQL peut représenter l’information plus directement et plus succinctement qu’une structure clé/valeur comme ZIP
    • L’accessibilité pour les applications et scripts tiers s’en trouve améliorée
    • Les fonctionnalités avancées comme la gestion de versions intégrée des documents et la récupération du travail après crash deviennent plus faciles à implémenter
  • OpenDocument est un format déjà établi et bien conçu ; comme SQLite est apparu après OpenDocument, il ne s’agit pas de critiquer les choix existants
  • Le document Application File Format propose d’autres idées pour utiliser SQLite comme format de fichier applicatif

1 commentaires

 
GN⁺ 2023-09-19
Avis sur Hacker News
  • Je développe une appli qui utilise SQLite comme format de fichier
    Comme je veux conserver le flux classique où le fichier ne change que lorsque l’utilisateur modifie un document puis l’enregistre, je copie le contenu dans une base de données :memory: à l’ouverture du fichier : https://www.sqlite.org/inmemorydb.html
    L’utilisateur peut manipuler librement les données, et l’appli reflète directement les changements dans le format de base de données, sans modèle de document séparé. Lors de l’enregistrement, je réécris le tout dans le fichier de base de données avec VACUUM : https://www.sqlite.org/lang_vacuum.html
    Cela fonctionne bien avec des fichiers de taille raisonnable, et dans mon appli ils restent toujours dans cette plage

    • Je ne vois pas pourquoi utiliser une base de données volatile auxiliaire. Si l’utilisateur est en train de modifier un fichier, il n’y aura probablement même pas une écriture par seconde, donc le gain de performances ne sera pas énorme
      Il vaudrait mieux enregistrer automatiquement directement dans la base de données et supprimer le bouton Enregistrer. C’est plus robuste face aux conflits, il n’y a qu’une seule base de données, donc moins de code et de bugs, et une écriture SQLite réussit ou échoue, sans état intermédiaire. À l’inverse, comme le cite l’article, VACUUM INTO peut produire une base de données de sortie incomplète ou corrompue en cas d’arrêt inattendu ou de coupure de courant
      Si vous utilisez SQLite comme il a été conçu, vous n’aurez pas à vous soucier de ce point pendant toute la durée de vie de SQLite
    • Se comporter comme une appli classique signifie que si l’appli plante ou que le courant est coupé, on perd les données non enregistrées
      Il est nettement préférable d’enregistrer après chaque opération dans un emplacement temporaire, par exemple selon les répertoires XDG dans ~/.local/share/application/yourapp, puis de copier le fichier à l’endroit voulu quand l’utilisateur appuie sur Enregistrer. Même après une coupure de courant, en rouvrant l’appli on récupère presque le même état, avec au pire les dernières secondes perdues
    • Plus simplement, il suffirait peut-être de passer la base en mode WAL à l’ouverture et de désactiver le checkpoint automatique : https://www.sqlite.org/pragma.html#pragma_wal_autocheckpoint
      Quand l’utilisateur enregistre, on effectue un checkpoint pour fusionner le contenu du WAL dans la base de données principale
    • D’après la documentation, VACUUM copie le contenu dans un fichier de base de données temporaire, puis écrase l’original, en utilisant lors de l’écrasement un journal de rollback ou le WAL comme une transaction normale. Il faut donc disposer d’environ jusqu’à deux fois la taille de l’original en espace libre
      VACUUM INTO écrit dans le fichier indiqué par INTO au lieu d’une base de données temporaire, et saute l’étape consistant à recopier ensuite par-dessus l’original. Il est important de savoir si l’on utilise réellement VACUUM, qui résiste aux coupures de courant, ou VACUUM INTO, qui semble vulnérable à une coupure pendant l’écriture et pourrait corrompre un fichier existant s’il porte déjà ce nom
    • J’ai déjà utilisé une approche similaire : la base de données tournait en mémoire comme cache, était sauvegardée périodiquement sur disque, et j’utilisais la backup API : https://www.sqlite.org/backup.html
  • Le problème de SQLite, c’est que ce n’est pas un format de fichier standardisé
    Il est bien documenté et largement compris, mais aucune norme ISO ne définit en détail la manière d’interpréter un fichier SQLite. Il en va de même pour les implémentations alternatives
    Zip et XML ont une surface d’API beaucoup plus réduite que SQLite. L’API de SQLite ne se limite pas à quelques fonctions C : c’est le langage SQL lui-même, et implémenter sans corruption de données un parseur SQL, un optimiseur de requêtes, un compilateur, une machine virtuelle à bytecode, un moteur de recherche plein texte, etc., représente un travail bien plus important qu’un parseur XML
    Pour une appli fermée spécialisée dans un domaine où l’interopérabilité ou la normalisation ISO ne comptent pas, SQLite est un bon format de fichier, mais je comprends qu’OpenOffice ait eu de vraies préoccupations de ce côté-là

    • On ne voit pas clairement à quoi ce problème fait référence. Le format de fichier SQLite est dans le domaine public, bien documenté, et il existe des parseurs dans plusieurs langages
      La bibliothèque C SQLite est elle aussi dans le domaine public, son code source est entièrement ouvert, elle gère le format de fichier, et son niveau de documentation est supérieur à celui de la plupart des normes ISO. Il existe aussi des bindings pour presque tous les grands langages
      Si le problème est qu’un éventuel format OpenDocument stocké dans un fichier SQLite reste encore à créer et à documenter, c’est autre chose. Les normes ISO sont utiles, mais si nous avions dû attendre que l’ISO définisse les formats de fichiers, il y aurait eu très peu de choses utilisables
    • Pour devenir un format de fichier standard, il n’est pas nécessaire d’implémenter tout le parseur SQL, l’optimiseur de requêtes, le compilateur, la machine virtuelle à bytecode et le moteur de recherche plein texte
      C’est comme le fait qu’il n’est pas nécessaire d’implémenter toutes les fonctionnalités d’un tableur pour lire une feuille de calcul LibreOffice. Ce qu’il faut, c’est la capacité de reconstruire les tables ; ensuite, on peut parcourir les données avec du code impératif écrit dans le langage de son choix pour obtenir les informations voulues
    • Ce n’est pas non plus un problème pour la Bibliothèque du Congrès des États-Unis. Elle a défini SQLite, avec CSV, XML et JSON, comme format de stockage recommandé pour les jeux de données
    • Vous semblez mélanger le format de fichier et la manière de l’utiliser. Une appli qui utilise le format de fichier SQLite peut simplement intégrer la bibliothèque SQLite comme partie de l’application
      Réimplémenter cette bibliothèque serait un gros chantier, mais c’est le même type de travail que réimplémenter du code utilisant le format de fichier OpenDocument. Le format de fichier lui-même est assez simple
    • Une norme n’est pas forcément nécessaire. Toutes les interactions entre l’application et le document passent par SQL, et SQL est standardisé, au moins dans ses parties importantes
      Si la compatibilité vous inquiète, on peut faire en sorte que le document soit aussi accessible depuis une autre base de données, comme MySQL
  • Je pensais que l’adoption de SQLite par Audacity allait grandement améliorer la fonction d’enregistrement des fichiers, mais en pratique il y avait beaucoup de pièges.
    Sous Linux, si l’on enregistre dans un nouveau fichier sur un montage NTFS appartenant à root mais accessible en écriture à tous, créé via /etc/fstab, cela échouait pour des raisons du type erreur de permissions ; en revanche, enregistrer dans un fichier existant fonctionnait normalement.
    Dès qu’on modifie un projet, le fichier sur disque est modifié ; si l’on met un projet Audacity dans Git comme un gros blob binaire, cela crée des diffs Git inutiles. Même après enregistrement, des données obsolètes ou supprimées restent dans le fichier SQLite jusqu’à la fermeture de la fenêtre du projet ; si l’on ne ferme pas la fenêtre avant de committer, elles peuvent se retrouver dans le dépôt. Je me souviens qu’avant il fallait lancer manuellement VACUUM sur le fichier .aup3, mais maintenant fermer la fenêtre suffit. Ça rappelle le Fast Save de Word 2003.

    • Si Audacity plante ou se ferme anormalement, aucun nettoyage n’a lieu, ce qui est pénible. Avant, lors de la récupération, il signalait la présence de blocs orphelins et permettait de choisir de les conserver ou de les supprimer.
      Quand un projet qui aurait dû faire quelques centaines de Mo se retrouvait à plusieurs Go et qu’il fallait économiser de l’espace disque, Mix and Render était la solution pour les travaux simples à une seule piste. Cela ne changeait pas l’audio, mais permettait de nettoyer les résidus lors de l’enregistrement et de la fermeture.
      Ce n’est clairement pas un problème propre à SQLite, mais un problème de couche applicative. Audacity 2 semblait avoir une notion d’espace de travail temporaire, tandis qu’Audacity 3 semble utiliser le fichier .aup3 lui-même comme espace de travail.
      J’ai regardé le format Audacity 3 : il stocke les données de projet correspondant aux anciens fichiers .aup sous forme de XML dans une table à une seule ligne, mais au lieu de les écrire telles quelles en texte, il les encode avec un simple codeur à dictionnaire, ce qui m’a beaucoup surpris. Cela rend l’interopérabilité et l’inspection bien plus difficiles, nuit même très légèrement aux performances, et le gain d’espace doit représenter quelques Ko, une erreur d’arrondi face à des fichiers audio de plusieurs centaines de Mo.
    • Il faut reproduire exactement la fonctionnalité attendue par l’utilisateur. Tout doit être enregistré dans des fichiers temporaires, et le fichier d’origine ne doit être écrasé que lors d’une action d’enregistrement explicite.
      Du point de vue de Git, il vaut mieux utiliser un format texte facile à comparer et à fusionner. Je ne sais pas à quel point un dump SQLite est pratique de ce point de vue.
    • Ma femme utilise Audacity toute la journée, et tous les quelques jours elle se retrouve avec un fichier SQLite corrompu. Il y a des erreurs de clés en double, et je ne sais pas comment les corriger ou les réimporter dans Audacity.
      Si c’est important, on peut le réparer manuellement, mais en général il suffit de jeter le fichier pour que ça refonctionne.
  • Très bon article. Cela dit, j’aime le fait qu’OpenDocument soit un ensemble de fichiers XML dans une archive Zip.
    On peut générer assez facilement des documents comme des feuilles de calcul, sans grosse bibliothèque connaissant le format du document.
    Il arrive que les utilisateurs d’un service web veuillent exploiter, dans plusieurs outils, des données exportées sous forme de lignes de tableau. Le CSV UTF-8 est ouvert, conventionnel et utilisable, mais quiconque a déjà fourni du CSV à des utilisateurs finaux connaît la douleur des blocages dans les applications de tableur.
    J’ai enregistré une feuille de calcul d’exemple en ODS d’OpenDocument et en XLSX, le monstre XML de Microsoft appelé OOXML, puis j’ai simplement compris les bases du format XML. J’ai réduit l’archive Zip aux éléments indispensables, marqué l’emplacement où le contenu devait être inséré, et généré un nouveau fichier de feuille de calcul à la demande. Je peux maintenant produire les mêmes données en CSV, ODS, XLSX et JSON.
    Ce serait aussi possible avec SQLite, mais ce serait un peu plus complexe et le développement serait plus lent. Pouvoir créer un document modèle avec une suite bureautique, puis aller fouiller dans le XML du fichier enregistré, est une fonctionnalité de niche mais appréciable.
    Excel avec une locale comme nl_NL est particulièrement problématique, car il se comporte comme si le séparateur de colonnes des fichiers CSV était codé en dur comme étant le point-virgule. Microsoft a en effet décidé, tristement célèbrement, que les Néerlandais n’utilisent pas de virgule dans les fichiers de valeurs séparées par des virgules.

    • Ce comportement n’est pas complètement codé en dur ; il dépend de la valeur de localeconv()->decimal_point. Si la valeur est ,, Excel utilise le point-virgule à la fois dans les fichiers CSV et dans le langage d’expression des formules.
      Avant, on pouvait le configurer à l’ouverture d’un CSV/TXT dans Excel, et c’est encore possible dans LibreOffice, mais avec la simplification générale de l’UI, l’option a été déplacée quelque part dans le menu ou l’onglet de ruban Data. Il faut ouvrir un nouveau classeur et trouver la bonne option ; pour gagner du temps, mieux vaut utiliser LibreOffice.
  • Cette partie m’a vraiment surpris. J’ai du mal à croire qu’une sous-requête ne soit pas nécessaire.
    SELECT manifest, versionId, max(checkinTime) FROM version;
    On dit que, dans SQLite, cette deuxième requête utilisant max(checkinTime) fonctionne effectivement bien et renvoie une réponse définie. Dans d’autres moteurs de base de données SQL, elle renverrait une réponse non définie ou une erreur, mais dans SQLite elle renvoie le manifest et le versionId de l’entrée ayant le checkinTime maximal.

    • Cela peut être une fonctionnalité utile, mais honnêtement, je ne m’attendrais pas à ce qu’une telle requête renvoie cela.
      Dans ce cas, il n’y a pas besoin de sous-requête : il suffit de trier par checkinTime puis de limiter à une ligne : select manifest, versionId, checkinTime from version order by checkinTime desc limit 1
      Cela devrait fonctionner au moins avec SQLite et PostgreSQL. Dans Oracle, il fallait utiliser where rownum=1, si je me souviens bien, ce qui nécessitait une sous-requête.
    • On peut voir cela grosso modo comme une forme abrégée de GROUP BY manifest, versionId ORDER BY 3 DESC LIMIT 1, ou d’une CTE qui calcule le checkinTime maximal puis fait une jointure.
      Mais s’il y a plusieurs lignes avec le même checkinTime maximal, il y a de l’aléatoire et cela peut devenir un fusil pointé sur le pied, donc je n’utilise pas vraiment les fonctionnalités propres à SQLite3 de ce genre. Pour choisir la meilleure ligne de manière déterministe, il faut une méthode explicite semblable à celle ci-dessus.
    • Ce n’est pas le comportement généralement attendu en SQL, mais SQLite s’écarte souvent des attentes. Dans ce cas, c’est pratique, mais non standard.
    • On dirait qu’un effet secondaire pratique de l’implémentation est ensuite devenu un comportement officiel. Un peu comme l’ordre des clés dans les dictionnaires Python.
      Dans Postgres, on peut faire quelque chose de similaire avec une requête DISTINCT ON. C’est l’une des tâches qui m’ont semblé les plus difficiles en SQL alors qu’elle paraît simple.
    • Il est assez surprenant qu’on puisse affirmer que c’est un comportement défini. manifest et versionId ne sont pas fonctionnellement dépendants de max(checkinTime).
      Par exemple, il peut y avoir deux lignes ayant la même valeur de checkinTime, et cette valeur peut être le maximum.
  • J’ai déjà lancé un produit qui utilisait à la fois SQLite et des fichiers XML
    L’une des améliorations consistait à déplacer vers des fichiers XML quelques tables contenant peu de données. Les fichiers étaient petits et rarement utilisés, ce qui simplifiait la couche d’accès aux données et le diagnostic ; ils étaient écrits en XML indenté par tabulations sur plusieurs lignes
    Demander à un responsable technique chargé de diagnostiquer le produit d’ouvrir une base de données SQLite était assez contraignant. Mais pour les parties principales du produit, SQLite était de très loin supérieur aux fichiers XML. Les versions précédentes utilisaient des fichiers XML, qui posaient des problèmes de montée en charge faute d’une bonne méthode de mise à jour incrémentale
    L’avantage du XML — son format lisible par l’humain — ne fonctionne vraiment que lorsque le fichier est petit et que le schéma est conçu pour un XML facile à lire. Le fait de devoir réécrire tout le fichier XML à chaque fois, ainsi que la complexité qui apparaît à mesure que les fonctionnalités se multiplient, rognent rapidement le principal atout du XML
    Les cas où un utilisateur ordinaire doit manipuler directement l’intérieur d’un document bureautique sont suffisamment rares pour que l’apprentissage d’un lecteur SQLite constitue une barrière à l’entrée acceptable. Les limites de XML+Zip pour les écritures arbitraires au milieu d’un fichier ne peuvent pas être surmontées même par la loi de Moore

    • Je ne vois pas très bien comment le format natif de SQLite parvient, sans Zip, à une taille comparable à XML+Zip. Je me demande si les champs TEXT ou BLOB de SQLite sont compressés, ou s’il suppose que l’appelant compresse le BLOB avant l’écriture
  • ODT a été conçu avec la standardisation en tête. Le format précédent était aussi très similaire, mais il s’appuyait fortement sur des standards existants comme XHTML, SVG et CSS
    S’il n’était pas possible de référencer des standards existants, la spécification ODT elle-même deviendrait soudain énorme. L’effort nécessaire pour mettre à jour le standard semble aussi considérable, et il y a eu peu de progrès ces dernières années
    En pratique, le format SQLite pourrait être proposé comme option, mais le navire des formats de documents bureautiques semble déjà avoir pris le large. Cela dit, c’est un bon argument en faveur de la formalisation de la spécification SQLite en standard officiel

    • La spécification est rédigée de façon très concise et ne définit principalement que la syntaxe plutôt que les effets et le comportement, mais elle reste volumineuse avec ses 840 pages
      À part quelques défauts — par exemple l’explosion des styles locaux et des plages de texte due à l’attribut ooo:rsid, les feuilles de calcul non clairsemées, ou encore le mécanisme étrange de style des tableaux — c’est un balisage très bien conçu pour ce type de données documentaires. Il offre un bon équilibre entre le balisage sémantique et les présentations que les utilisateurs veulent réellement obtenir
      À l’inverse, Office OpenXML contient des balises vides de mise en forme avec état qui, dans DOCX, activent ou désactivent la mise en gras du texte qui suit
  • Associer un format de fichier à SQLite a quelque chose qui sent mauvais
    SQLite est excellent, mais assez particulier dans ce domaine. Comme il fait beaucoup de choses, il est difficile à reproduire tel quel
    Or, dans ce cas, a-t-on vraiment besoin d’autant de fonctionnalités ? Non. Il suffit d’une sémantique de transaction basique et sûre, et de la capacité à stocker une structure de tables simple ; pas besoin de tout le standard SQL ni même d’un optimiseur de requêtes
    Il existe peut-être de meilleurs formats de fichier, mais ce serait mieux que ce soit un format distinct de SQLite

    • Je ne vois pas pourquoi ce ne serait pas possible : https://www.sqlite.org/appfileformat.html
      Sa taille est inférieure à 1 Mo et https://sqlite.org/footprint.html, avec toutes les fonctionnalités activées, il fait 750 Ko : https://www.sqlite.org/about.html
      On peut retirer pas mal de fonctionnalités à la compilation, et il semble aussi exister des options pour ajuster ou réduire le planificateur de requêtes : https://www.sqlite.org/compile.html
      En plus, il y a cette formule : « SQLite ne concurrence pas les bases de données client/serveur. SQLite concurrence fopen() » : https://www.sqlite.org/whentouse.html
      Au bout du compte, ce dont on a besoin, ce n’est pas d’une base de données en soi, mais d’une bibliothèque qui fournisse une API et un comportement de base de données
    • L’aspect transactionnel s’est révélé plus difficile que prévu, surtout avec les accès concurrents aux fichiers. À l’époque, gérer SQLITE_BUSY était assez pénible
      Je sais que les échecs de sérialisation sont attendus dans le traitement des transactions, mais avec SQLite il était difficile de distinguer les échecs persistants, comme une sorte d’auto-interblocage, des problèmes temporaires de mises à jour concurrentes. Si l’échec est temporaire, il suffit de réexécuter la closure qui définit le travail de la transaction ; s’il est persistant, cela ne sert à rien
      Une partie du problème vient du fait que sqlite3_stmt combine à la fois la nature d’une instruction préparée et celle d’un jeu de résultats. On a tendance à le conserver longtemps pour mettre en cache le bytecode compilé, mais si l’itération s’arrête au milieu, il peut encore détenir un verrou à ce moment-là. Cela peut provoquer des échecs inattendus de montée en niveau du verrou
      J’ai fini par éliminer le problème en produisant des rapports d’erreur détaillés avec sqlite3_next_stmt, sqlite3_stmt_busy et sqlite3_sql. Même pour un usage personnel, le code de reprise de transaction était rempli de journalisation optionnelle et de commentaires. La logique de reprise de transaction pour PostgreSQL était bien plus simple
      Un autre point qui m’a surpris est la documentation indiquant qu’en mode WAL avec synchronous=NORMAL, une transaction validée peut être annulée après une perte d’alimentation ou un crash système : https://sqlite.org/pragma.html#pragma_synchronous
      Ce n’était pas pertinent pour mon application
    • SQLite est déjà utilisé exactement à cette fin. Il sert à OGC GeoPackage, et les jeux de données Mapbox/Maptiler l’utilisent aussi
    • Certains formats sont avant tout conçus pour l’échange. L’argument du camp SQLite revient à dire que les propriétaires d’apps devraient imposer le format SQLite aux utilisateurs et en faire de facto un standard, sans passer par le travail nécessaire pour en faire un standard juridique
      Si Richard Hipp et son entreprise présentaient un standard SQLite ISO/IEC/ANSI/ETSI dont ils ne s’écarteraient jamais, une analyse juridique montrant l’absence de brevets ayant un impact, ainsi que plusieurs implémentations compatibles de SQLite conservant tous ses avantages, alors on pourrait discuter de sa recommandation comme format de fichier. Sinon, cela revient à mettre une forte dépendance sur une implémentation à source unique et à la faire peser aussi sur les utilisateurs
      XML, ASN.1 et JFIF sont des standards officiels, et ZIP est également devenu un standard officiel, ISO/IEC 21320-1:2015, lors du processus de normalisation d’OpenDocument
      Le plus important pour un document, c’est que tout le monde puisse le lire. Réduire le temps de mise à jour sur disque est secondaire. Il ne faut pas oublier ce que l’on a appris lorsque Microsoft a dévoyé les organismes de normalisation pour maintenir l’enfermement propriétaire : https://arstechnica.com/uncategorized/2008/10/norwegian-standards-body-implodes-over-ooxml-controversy/
    • Quand on regarde les apps Apple, la plupart utilisent SQLite comme format de stockage. C’est le cas d’iMovie, d’iPhoto, de Voice recording, et Docker fait de même
      Ce choix ne peut pas être si mauvais
  • Un autre exemple est celui des tuiles de cartes raster. Ce sont, en pratique, de petites images carrées qui peuvent se compter par millions
    J’ai testé Zip, tar, le système de fichiers et SQLite : SQLite était le plus rapide et le plus compact, meilleur même que les archives ordinaires sans surcoût

    • Beaucoup de systèmes de fichiers ont des problèmes lorsqu’un même répertoire contient plus de dizaines de milliers de fichiers, et c’est exactement ce qui se produit avec les tuiles de cartes. Que SQLite soit plus rapide n’a rien de surprenant
    • Si SQLite est plus rapide, le problème vient de la bibliothèque Zip utilisée
      SQLite a un gros inconvénient : les BLOB obtenus depuis la base de données ne peuvent pas être mmap et doivent donc être copiés ailleurs. Un fichier Zip, s’il n’est pas compressé ou s’il est compressé avec un encodage particulier comme PVRTC, peut être mmap tel quel
  • OpenDocument, ce sont des images compressées et du XML. Au final, cela signifie qu’il faut analyser l’ensemble du format et le charger en mémoire
    Je ne vois pas très bien en quoi SQLite améliorerait cela. XML n’est pas idéal, mais comme il est compressé en Zip, la pénalité de taille n’est pas très importante
    Tous les avantages listés dans l’article sur SQLite peuvent être mis en œuvre si l’on utilise SQLite comme modèle d’exécution du document. C’est possible à la fois sur disque et en mémoire, mais SQLite n’a pas besoin d’être le format de transmission
    Au contraire, SQLite pourrait être plus volumineux que le format actuel. Au fil des modifications, de l’espace inutilisé peut apparaître, avec de la fragmentation et des zones clairsemées. S’il faut optimiser à chaque fois, les avantages comme l’enregistrement rapide disparaissent aussi
    Les formats qui ont besoin de mises à jour delta et de recherches rapides via index, sans devoir charger tout le fichier en mémoire, utilisent effectivement souvent SQLite comme format de fichier. Mais j’ai l’impression qu’OpenDocument était un mauvais exemple à choisir comme cible pour SQLite dans ce scénario hypothétique

    • XML et Zip ne gèrent pas correctement les mises à jour incrémentales. À l’enregistrement, il faut réécrire tout le fichier applicatif, et si un problème survient pendant l’écriture, cela peut provoquer une corruption
      Si l’on utilise SQLite comme format sur disque et que l’application est correctement implémentée, on peut éviter de se retrouver dans un état corrompu
      XML/Zip peut aussi obtenir quelque chose de similaire avec une astuce de renommage, mais SQLite fournit cela dans un unique fichier sur disque. Si l’on utilise déjà SQLite comme modèle en mémoire, il n’y a pas vraiment de raison de ne pas l’utiliser aussi comme format disque/de transmission. À ce stade, c’est presque gratuit
      Le problème de taille de fichier devrait pouvoir se traiter avec VACUUM