2 pontos por GN⁺ 2023-09-19 | 1 comentários | Compartilhar no WhatsApp
  • Se uma apresentação OpenDocument (ODP) fosse armazenada em um contêiner SQLite em vez de um arquivo ZIP, seria possível projetar formas mais seguras e rápidas de salvar, abrir e recuperar documentos
  • Hoje, o ODP organiza XML e arquivos de imagem dentro de um arquivo ZIP; um arquivo de apresentação de exemplo com 49 slides é composto por 78 itens no total, incluindo content.xml, styles.xml, meta.xml, settings.xml e imagens
  • Nessa estrutura baseada em ZIP, mesmo pequenas alterações tendem a exigir a regravação do arquivo inteiro, o que dificulta atualizações incrementais e aumenta a latência em File/Save e o volume de escrita em SSDs
  • Com SQLite, os arquivos podem ser armazenados como linhas de tabela e, indo além, o conteúdo e as versões podem ser separados por slide, permitindo ler apenas o primeiro slide ou salvar apenas os slides modificados
  • Não se trata de uma crítica ao OpenDocument nem de uma proposta de mudança no formato em si, mas de um exemplo de como o SQLite pode facilitar salvamento atômico, acessibilidade, controle de versões e recuperação em formatos de arquivo de aplicações

Escopo e objeto do experimento mental

  • O foco é o OpenDocument, mais especificamente o formato de apresentação ODP (OpenDocument Presentation)
  • O objetivo não é mudar de fato o OpenDocument, mas avaliar o uso do SQLite como contêiner em projetos futuros de formatos de arquivo
  • Os benefícios esperados são documentos menores, File/Save mais rápido, inicialização mais rápida, menor uso de memória, versionamento de documentos e melhor experiência do usuário

Estrutura atual de um arquivo ODP

  • Um arquivo ODP é um arquivo ZIP contendo arquivos XML e recursos de imagem
  • Como exemplo, um arquivo de apresentação sobre SQLite com 49 slides da SouthEast LinuxFest de 2014 tem 78 itens no total na saída de zip -l
    • Os quatro arquivos XML content.xml, styles.xml, meta.xml e settings.xml definem layout dos slides, conteúdo de texto e estilos
    • O arquivo de apresentação armazena 62 imagens como arquivos separados, desde fotos em tela cheia até pequenos ícones
    • O arquivo mimetype contém uma única linha: application/vnd.oasis.opendocument.presentation
  • Arquivos OpenDocument de processador de texto e planilha têm estrutura semelhante, mas a análise aqui se concentra no ODP

Limitações do ODP baseado em ZIP

  • Um arquivo ZIP se parece mais com um banco de dados chave/valor otimizado para escrever uma vez e ler muitas vezes, adequado a um pequeno número de chaves com grandes valores BLOB
  • Como é difícil atualizar itens individuais, quando o usuário escolhe File/Save, normalmente o arquivo ZIP inteiro é recriado
    • É possível atualizar itens individuais sem corromper o documento inteiro em caso de queda de energia ou travamento, mas isso é difícil o bastante para quase nunca ser usado na prática
    • Em uma apresentação de 50 MB, mudar um único caractere pode significar reescrever os 50 MB completos
  • O tempo de inicialização pode ficar mais lento
    • ODP armazena o conteúdo de todos os slides em um único arquivo XML grande, content.xml
    • O LibreOffice lê e faz o parse desse arquivo inteiro para exibir o primeiro slide
    • As imagens também parecem ser todas carregadas na memória e, por isso, ao dar duplo clique no arquivo aparece uma barra de progresso em vez do primeiro slide
  • O uso de memória aumenta
    • A estrutura ZIP incentiva implementações que leem o documento inteiro para a memória ao abrir, fazem a edição na memória e gravam tudo de volta no disco ao salvar
    • Um arquivo de apresentação de 50 MB pode consumir mais de 200 MB de RAM
    • Se várias apresentações estiverem abertas ao mesmo tempo junto com navegador e apps de desktop, pode ocorrer swapping
  • A recuperação após travamentos fica mais trabalhosa
    • Aplicativos da família OpenOffice fazem backup periódico do documento em memória para se proteger contra falhas
    • Durante o backup, o app pode congelar por alguns segundos, e após reiniciar é preciso passar por uma caixa de diálogo separada de recuperação
  • A acessibilidade ao conteúdo é baixa
    • Dá para extrair imagens com ferramentas ZIP, mas extrair ou editar o texto dos slides com ferramentas comuns é difícil
    • No arquivo de exemplo, content.xml tem a declaração XML na primeira linha e 211.792 caracteres de XML em uma única segunda linha

Primeira melhoria: substituir ZIP por SQLite

  • O primeiro passo é uma estrutura simples que troca entradas ZIP por linhas em uma tabela SQLite
CREATE TABLE OpenDocTree(
  filename TEXT PRIMARY KEY,
  filesize BIGINT,
  content BLOB
);
  • Nesta etapa, o restante da estrutura do formato de arquivo não muda
    • Continua sendo uma estrutura de “monte de arquivos”, mas cada arquivo vira uma linha do banco SQLite em vez de uma entrada ZIP
  • A comparação de tamanho entre um arquivo SQLite reempacotado com o mesmo conteúdo de self2014.odp gerado pelo NeoOffice usando o utilitário SQLAR é a seguinte
    • self2014.odp: 10.514.994 bytes
    • self2014.sqlar: 10.464.256 bytes
    • zip.odp, recompresso com o zip de linha de comando: 10.416.644 bytes
  • O arquivo SQLite ficou cerca de 0,5% menor que o ODP gerado pelo NeoOffice
    • Um arquivo ZIP bem comprimido com o zip de linha de comando ficou novamente cerca de 0,5% menor que o SQLite
    • Em termos de tamanho, o banco SQLite é competitivo com um arquivo ZIP
  • O SQLite fornece escrita atômica, então é possível salvar mudanças incrementais sem risco de corromper o documento em caso de travamento ou queda de energia
    • A limitação de ainda precisar regravar todo o content.xml permanece
    • Mesmo assim, os outros 77 arquivos podem ser mantidos intactos, acelerando o File/Save e reduzindo a escrita no SSD

Segunda melhoria: dividir o conteúdo em partes menores

  • Como o SQLite consegue armazenar com eficiência tanto blocos grandes quanto muitos blocos pequenos, é possível usar uma tabela de conteúdo por slide
CREATE TABLE slide(
  pageNumber INTEGER,
  slideContent TEXT
);
CREATE INDEX slide_pgnum ON slide(pageNumber);
  • Para mostrar a primeira tela, a aplicação precisaria ler apenas o primeiro slide
SELECT slideContent FROM slide WHERE pageNumber=1;
  • Com essa estrutura, dá para buscar, fazer o parse e exibir rapidamente apenas o conteúdo do primeiro slide, sem precisar ler todo o content.xml na inicialização
  • As opções de implementação também aumentam
    • Depois de exibir o primeiro slide, o restante das páginas pode ser lido em uma thread em background
    • Só o slide atual pode permanecer na memória
    • Ou apenas o slide atual e o próximo, para permitir transições rápidas
  • O salvamento também fica mais rápido, já que basta regravar apenas as páginas alteradas
  • Em trechos curtos de texto, a eficiência de compressão pode cair e o documento pode ficar maior
    • Ainda assim, como a maior parte do espaço do documento costuma ser ocupada por imagens, essa perda de compressão no texto pode ser um custo pequeno diante da melhora na experiência do usuário

Terceira melhoria: controle de versões

  • Se os slides forem armazenados como objetos individuais, o próprio documento pode conter um histórico de versões
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
);
  • Cada slide passa a ter um slideId exclusivo em vez de um número de página, e a ordem é determinada por uma lista de slideId armazenada em manifest, na tabela version
  • Ao iniciar, a aplicação primeiro escolhe qual versão exibir e normalmente pode pegar a versão mais recente
SELECT manifest, versionId FROM version ORDER BY versionId DESC LIMIT 1;
  • Também é possível consultar a versão mais recente com base em checkinTime
SELECT manifest, versionId, max(checkinTime) FROM version;
  • No SQLite, essa consulta com max(checkinTime) retorna um resultado definido, mas em muitos outros bancos SQL ela retornaria um resultado indefinido ou geraria erro
  • Quando o usuário executa File/Save, apenas os slides modificados precisam ser inseridos como novas linhas na tabela slide, junto com uma nova linha em version contendo o manifest atualizado
  • A tabela version registra horário do check-in, comentário do usuário e versão-pai, preservando o histórico de mudanças
  • Também seria possível guardar várias apresentações diferentes dentro do mesmo documento
  • Em vez de um arquivo de backup separado, uma versão especial pending permitiria registrar em silêncio e com frequência as alterações ainda não salvas
    • Como só as diferenças são gravadas, o trabalho deixa de ser escrever vários MB e passa a ser escrever apenas alguns KB
    • O tempo de salvamento pode cair de segundos para milissegundos
    • Mesmo após reiniciar depois de um travamento, a maior parte ou quase todo o trabalho do usuário pode ser preservada
    • Se o usuário quiser descartar alterações não salvas, basta voltar para uma versão anterior

Outros recursos possíveis em um formato de arquivo SQLite

  • Um contêiner SQLite pode adicionar recursos importantes a um formato de arquivo de aplicação com apenas três tabelas
  • Além disso, schema, índices, triggers, views e constraints podem ser usados para melhorar desempenho, conveniência e consistência
  • Algumas ideias de extensão são
    • Armazenar uma pilha automática de undo/redo nas tabelas do banco para permitir desfazer até sessões de edição anteriores
    • Adicionar busca full-text a um deck de slides ou a vários decks
    • Decompor settings.xml em tabelas SQL para facilitar visualização e edição por outras aplicações
    • Separar as notas do apresentador de cada slide em uma tabela própria, facilitando o acesso por apps ou scripts de terceiros
    • Ir além da sequência linear simples de slides e suportar estruturas de apresentação com caminhos alternativos e desvios conforme a reação do público

Resistência comum ao SQLite e contrapontos

  • Por causa da experiência com bancos SQL corporativos, pode haver resistência a usar SQLite como formato de arquivo de aplicação
  • Muitos bancos corporativos recomendam não colocar strings grandes ou BLOBs no banco e armazená-los em arquivos separados, mas com o SQLite é diferente
    • Qualquer coluna do SQLite pode armazenar strings ou BLOBs de até cerca de 1 GB
    • Para strings e BLOBs de até 100 KB, o desempenho de I/O é melhor do que com arquivos separados
  • Outra limitação mental comum é a ideia de que todo schema SQL precisa estar em terceira forma normal (3NF) e armazenar apenas tipos primitivos pequenos
    • A teoria relacional é importante, mas em um formato de arquivo real também pode ser perfeitamente aceitável armazenar informações complexas como XML ou JSON em campos de texto

SQLite como formato de arquivo de aplicação

  • Um arquivo de banco SQLite tem praticamente o mesmo tamanho que um arquivo ZIP com a mesma informação e, em alguns casos, pode ser até menor
  • Graças às atualizações atômicas, pequenas mudanças podem ser gravadas com segurança no documento, reduzindo I/O de disco e melhorando o desempenho de File/Save
  • A aplicação pode ler apenas o conteúdo necessário para a primeira tela, reduzindo o tempo de inicialização
  • É possível manter na memória só o conteúdo relacionado ao que está sendo exibido e deixar o restante no disco, reduzindo bastante o uso de memória
  • Um schema SQL pode representar as informações de forma mais direta e concisa do que uma estrutura chave/valor como ZIP
    • Isso melhora a acessibilidade para apps e scripts de terceiros
    • Também facilita implementar recursos avançados como versionamento embutido do documento e recuperação do trabalho após travamentos
  • O OpenDocument já é um formato estabelecido e bem projetado, e como o SQLite surgiu depois dele, isto não é uma crítica às escolhas originais
  • O documento Application File Format traz outras ideias sobre o uso do SQLite como formato de arquivo de aplicação

1 comentários

 
GN⁺ 2023-09-19
Opiniões no Hacker News
  • Estou criando um app que usa SQLite como formato de arquivo
    Como quero manter o fluxo comum em que o arquivo só muda quando o usuário edita o documento e salva, ao abrir o arquivo eu o copio para um banco de dados :memory:: https://www.sqlite.org/inmemorydb.html
    O usuário manipula à vontade, e o app reflete diretamente no formato do banco de dados, sem um modelo de documento separado. Ao salvar, escrevo de volta no arquivo de banco de dados com VACUUM: https://www.sqlite.org/lang_vacuum.html
    Funciona bem para arquivos de tamanho razoável e, no meu app, eles estão sempre dentro desse intervalo

    • Não entendo por que usar um banco de dados volátil auxiliar. Se o usuário está editando um arquivo, provavelmente nem haverá uma gravação por segundo, então o ganho de desempenho não é grande
      Seria melhor salvar automaticamente direto no banco de dados e remover o botão de salvar. É resistente a falhas, há apenas um banco de dados, o que reduz código e bugs, e uma escrita no SQLite ou tem sucesso ou falha, sem estado intermediário. Por outro lado, como citado no documento, VACUUM INTO pode deixar o banco de dados de saída incompleto ou corrompido em caso de encerramento inesperado ou perda de energia
      Se você usar SQLite da forma como ele foi originalmente pensado, não precisará se preocupar com isso durante toda a vida útil do SQLite
    • Funcionar como um app comum significa que, se o app morrer ou faltar energia, você perde dados não salvos
      É muito melhor salvar após cada operação em um local temporário, por exemplo algo como ~/.local/share/application/yourapp segundo os diretórios XDG, e, quando o usuário clicar em salvar, copiar o arquivo para o local desejado. Mesmo ao reabrir o app depois de uma perda de energia, ele se recupera quase no mesmo ponto, e você pode perder só os últimos poucos segundos
    • De forma mais simples, talvez baste mudar o banco de dados para o modo WAL ao abri-lo e desativar o checkpoint automático: https://www.sqlite.org/pragma.html#pragma_wal_autocheckpoint
      Quando o usuário salvar, você executa um checkpoint para mesclar o conteúdo do WAL ao banco de dados principal
    • Segundo a documentação, VACUUM copia o conteúdo para um arquivo temporário de banco de dados e depois sobrescreve o original; ao sobrescrever, usa um rollback journal ou WAL como em uma transação normal. Por isso, precisa de espaço livre de até cerca de duas vezes o tamanho do original
      VACUUM INTO usa o arquivo especificado em INTO em vez de um banco de dados temporário, e omite a etapa de copiar de volta por cima do original. É importante saber se o que está sendo usado de fato é VACUUM, que resiste até a quedas de energia, ou VACUUM INTO, que parece vulnerável a queda de energia durante a escrita e pode corromper um arquivo existente se o nome for o mesmo
    • Já usei uma abordagem parecida: rodava o banco de dados em memória como cache, salvava periodicamente no disco e usava a backup API: https://www.sqlite.org/backup.html
  • O problema do SQLite é que ele não é um formato de arquivo padronizado
    Ele é bem documentado e amplamente compreendido, mas nenhuma norma ISO define em detalhes como interpretar arquivos SQLite. O mesmo vale para implementações alternativas
    Zip e XML têm uma superfície de API muito menor do que SQLite. A API do SQLite vai além de algumas funções em C: é a própria linguagem SQL, e implementar sem corrupção de dados um parser SQL, um otimizador de consultas, um compilador, uma máquina virtual de bytecode, um mecanismo de busca de texto completo etc. é algo muito maior do que um parser XML
    Para um app fechado, específico de domínio, em que interoperabilidade ou padronização ISO não importam, SQLite é um bom formato de arquivo, mas entendo que essas preocupações realmente existiam para o OpenOffice

    • Fica meio ambíguo a que esse problema se refere. O formato de arquivo do SQLite é de domínio público, bem documentado, e existem parsers em várias linguagens
      A biblioteca C do SQLite também é de domínio público, com o código totalmente aberto, lida com o formato de arquivo e tem um nível de documentação melhor que o de boa parte das normas ISO. Também há bindings para praticamente todas as linguagens principais
      Se o problema for que algum formato OpenDocument a ser armazenado dentro de um arquivo SQLite ainda precisa ser criado e documentado, aí é outra questão. Normas ISO são boas, mas, se tivéssemos de esperar a ISO definir formatos de arquivo, haveria pouquíssimas coisas que poderíamos usar
    • Para ser um formato de arquivo padrão, não é necessário implementar todo o parser SQL, otimizador de consultas, compilador, máquina virtual de bytecode e mecanismo de busca de texto completo
      É como não ser necessário implementar todos os recursos de planilhas para ler uma planilha do LibreOffice. O que você precisa é da capacidade de reconstruir as tabelas; depois disso, pode percorrer os dados com código imperativo escrito na linguagem que preferir para obter as informações desejadas
    • Isso nem é um problema para a Biblioteca do Congresso dos EUA. A Biblioteca do Congresso definiu SQLite, junto com CSV, XML e JSON, como formatos de armazenamento recomendados para datasets
    • Parece que estão misturando o formato de arquivo com a forma como ele é usado. Um app que usa o formato de arquivo SQLite pode usar a biblioteca SQLite como parte do próprio app
      Reimplementar essa biblioteca seria um trabalhão, mas é o mesmo tipo de trabalho que reimplementar código que usa o formato de arquivo OpenDocument. O formato de arquivo em si é bem simples
    • Um padrão não é necessariamente obrigatório. Toda a interação entre a aplicação e o documento acontece por meio de SQL, e SQL é padronizado pelo menos nas partes importantes
      Se a compatibilidade preocupa, dá para fazer o documento também ser acessível por outros bancos de dados, como MySQL
  • Achei que a função de salvar arquivos melhoraria bastante se o Audacity adotasse SQLite, mas na prática havia muitas armadilhas
    No Linux, ao salvar como um novo arquivo em uma montagem NTFS criada via /etc/fstab, pertencente ao root mas gravável por todos, ele falhava por motivos como erro de permissão; ao salvar em um arquivo existente, funcionava normalmente
    No momento em que você edita um projeto, o arquivo em disco é modificado; então, se colocar um projeto do Audacity no Git como um bloco binário, surgem diffs desnecessários no Git. Mesmo salvando, dados antigos ou excluídos permanecem no arquivo SQLite até a janela do projeto ser fechada, então podem entrar no repositório se você não fechar a janela antes de fazer commit. Pelo que lembro, antes era preciso executar VACUUM diretamente no arquivo .aup3, mas hoje basta fechar a janela. Fica com um ar de Fast Save do Word 2003

    • Se o Audacity travar ou encerrar de forma anormal, nenhuma limpeza acontece, o que é incômodo. Antes, durante o processo de recuperação, ele avisava que havia blocos órfãos e permitia escolher se queria mantê-los ou excluí-los
      Quando um projeto que deveria ter algumas centenas de MB virava vários GB e eu precisava economizar espaço em disco, Mix and Render era a solução para trabalhos simples de uma única faixa. Ele não alterava o áudio, mas permitia limpar os resíduos ao salvar e sair
      Isso claramente não é um problema do SQLite em si, e sim da camada da aplicação. Acho que o Audacity 2 tinha o conceito de área de trabalho temporária, enquanto o Audacity 3 parece usar o próprio arquivo .aup3 como área de trabalho
      Dei uma olhada no formato do Audacity 3 e achei muito estranho: os dados do projeto que antes correspondiam ao arquivo .aup são armazenados em XML em uma tabela de uma única linha, mas em vez de gravar o texto diretamente, eles são codificados com um codificador de dicionário simples. Isso torna a interoperabilidade e a inspeção muito mais difíceis, prejudica um pouquinho o desempenho e a economia de espaço provavelmente é só um erro de arredondamento de alguns KB diante de arquivos de áudio de centenas de MB
    • É preciso reproduzir exatamente o comportamento que o usuário espera. Tudo deve ser salvo em arquivos temporários, e o arquivo original só deve ser sobrescrito quando houver uma ação explícita de salvar
      Do ponto de vista do Git, é melhor usar um formato de texto que facilite diffs e merges. Não sei o quanto um dump do SQLite é fácil nesse aspecto
    • Minha esposa usa o Audacity o dia inteiro e, a cada poucos dias, aparece um arquivo SQLite corrompido. Dá erro de chave duplicada, e não sei como corrigir ou reimportar isso no Audacity
      Se for importante, dá para consertar manualmente, mas em geral é só descartar o arquivo que volta a funcionar
  • Bom texto. Ainda assim, gosto do fato de o OpenDocument ser um conjunto de arquivos XML dentro de um arquivo Zip
    Mesmo sem uma biblioteca pesada que entenda o formato de documentos, é relativamente fácil gerar documentos como planilhas
    Às vezes, usuários de serviços web querem usar em várias ferramentas os dados que exportam como linhas de tabela. CSV em UTF-8 é aberto, convencional e utilizável, mas qualquer um que já tenha oferecido CSV a usuários finais conhece a dor de vê-los esbarrar em aplicativos de planilha
    Salvei uma planilha de exemplo como ODS do OpenDocument e como XLSX, o monstro XML da Microsoft, o OOXML, e então entendi apenas o básico do formato XML. Reduzi o arquivo Zip aos elementos obrigatórios, marquei os lugares onde o conteúdo entraria e, sob demanda, gero um novo arquivo de planilha. Agora posso emitir os mesmos dados em CSV, ODS, XLSX e JSON
    Também seria possível com SQLite, mas seria um pouco mais complexo e tornaria o desenvolvimento mais lento. Poder criar um documento-modelo em uma suíte de escritório e fuçar no XML do arquivo salvo é um recurso de nicho, mas bom
    O Excel em locales como nl_NL, em especial, é problemático porque se comporta como se o separador de colunas em arquivos CSV estivesse hardcoded como ponto e vírgula. Isso acontece porque a Microsoft decidiu, de forma infame, que holandeses não usam vírgulas em arquivos de comma separated values

    • Esse comportamento não é totalmente hardcoded; ele depende do valor de localeconv()->decimal_point. Se o valor for ,, o Excel usa ponto e vírgula tanto em arquivos CSV quanto na linguagem de expressão de fórmulas
      Antigamente era possível configurar isso ao abrir CSV/TXT no Excel, e no LibreOffice ainda é possível, mas no processo geral de simplificação da UI foi movido para algum lugar no menu/faixa Data. É preciso abrir uma nova pasta de trabalho e encontrar as opções corretas; para economizar tempo, é melhor usar o LibreOffice
  • Esta parte foi realmente surpreendente. É difícil acreditar que não seja necessária uma consulta aninhada
    SELECT manifest, versionId, max(checkinTime) FROM version;
    Dizem que, no SQLite, essa segunda consulta usando max(checkinTime) de fato funciona bem e retorna uma resposta definida. Outros motores de banco de dados SQL retornariam uma resposta indefinida ou dariam erro, mas no SQLite ela retorna o manifest e o versionId do item com o maior checkinTime

    • Pode ser um recurso útil, mas, sinceramente, eu não esperaria que uma consulta assim retornasse isso
      Nesse caso, não é necessária uma consulta aninhada; basta ordenar por checkinTime e limitar a um: select manifest, versionId, checkinTime from version order by checkinTime desc limit 1
      Pelo menos no SQLite e no PostgreSQL isso deve funcionar. No Oracle, pelo que lembro, era preciso usar where rownum=1, então uma consulta aninhada era necessária
    • Dá para ver isso mais ou menos como uma forma abreviada de GROUP BY manifest, versionId ORDER BY 3 DESC LIMIT 1, ou de uma CTE que obtém o maior checkinTime e depois faz um join
      Porém, se houver várias linhas com o mesmo maior checkinTime, há aleatoriedade e isso pode ser uma arma apontada para o próprio pé; por isso não costumo usar muito esse recurso específico do SQLite3. Para escolher deterministicamente a melhor linha, é preciso um método explícito parecido com o acima
    • Não é o comportamento normalmente esperado em SQL, mas o SQLite muitas vezes foge das expectativas. Neste caso é conveniente, mas não padrão
    • Parece que um efeito colateral conveniente da implementação acabou virando comportamento oficial depois. Parecido com a ordem das chaves em dicionários do Python
      No Postgres, dá para fazer algo semelhante com uma consulta DISTINCT ON. É uma daquelas tarefas que parecem simples em SQL, mas que achei das mais difíceis
    • É bem surpreendente que isso possa ser alegado como comportamento definido. Afinal, manifest e versionId não são funcionalmente dependentes de max(checkinTime)
      Por exemplo, pode haver duas linhas com o mesmo valor de checkinTime, e esse valor pode ser o máximo
  • Já lancei um produto que usava tanto SQLite quanto arquivos XML
    Uma das melhorias foi mover algumas tabelas com poucos dados para arquivos XML. Como os arquivos eram pequenos e quase não eram usados, isso simplificou a camada de acesso a dados e o diagnóstico, e os transformamos em XML com indentação por tabulação em várias linhas
    Pedir ao responsável técnico que precisava diagnosticar o produto para abrir um banco de dados SQLite era um grande obstáculo. Mas, nas partes principais do produto, o SQLite era esmagadoramente melhor que arquivos XML. A versão anterior usava arquivos XML, mas eles não tinham um bom método de atualização incremental, o que causava problemas de escalabilidade
    A vantagem do XML de ser um formato legível por humanos só funciona bem quando o arquivo é pequeno e o design do esquema foi adaptado para um XML legível. A necessidade de reescrever o arquivo XML inteiro a cada vez e a complexidade que surge com o aumento de funcionalidades rapidamente corroem a maior vantagem do XML
    Casos em que usuários comuns precisam mexer diretamente no interior de documentos de escritório são raros o bastante para que aprender a usar um leitor de SQLite seja uma barreira de entrada aceitável. As limitações de XML+Zip para escritas arbitrárias no meio do arquivo não podem ser superadas nem pela Lei de Moore

    • Não sei bem como o formato nativo do SQLite consegue atingir um tamanho semelhante ao de XML+Zip sem Zip. Fico curioso se campos TEXT ou BLOB do SQLite são comprimidos, ou se a suposição é que o chamador comprima o BLOB antes de gravá-lo
  • O ODT foi projetado tendo a padronização em mente. O formato anterior também era muito parecido, mas dependia fortemente de padrões existentes como XHTML, SVG e CSS
    Se não for possível referenciar padrões existentes, a própria especificação do ODT de repente ficaria enorme. O esforço para atualizar o padrão também parece considerável, e houve pouco progresso nos últimos anos
    Na prática, o formato SQLite até poderia ser oferecido como opção, mas o barco dos formatos de documentos de escritório parece já ter partido. Ainda assim, isso dá bons argumentos para a ideia de organizar a especificação do SQLite como um padrão oficial

    • A especificação é escrita de forma muito concisa e, embora defina principalmente a sintaxe, mais do que efeitos e comportamento, ainda assim é enorme, com 840 páginas
      Fora algumas falhas — por exemplo, o problema de explosão de estilos locais e intervalos de texto por causa do atributo ooo:rsid, planilhas não esparsas e o mecanismo estranho de estilização de tabelas —, é uma marcação muito bem projetada para esse tipo de dado documental. Há um bom equilíbrio entre marcação semântica e a apresentação que os usuários realmente querem criar
      Já o Office OpenXML tem tags vazias de formatação com estado e, no DOCX, elas alternam se o texto seguinte será exibido em negrito
  • Integrar um formato de arquivo ao SQLite me parece meio errado
    O SQLite é bom, mas é bastante singular nessa área, porque faz muita coisa e por isso é difícil replicá-lo tal como é
    Mas, neste caso, não é como se tantos recursos fossem necessários. Bastariam semântica básica e segura de transações e a capacidade de armazenar uma estrutura simples de tabelas; não é preciso todo o padrão SQL nem um otimizador de consultas
    Pode haver um formato de arquivo melhor, mas eu preferiria que fosse um formato separado do SQLite

    • Não sei por que isso não funcionaria: https://www.sqlite.org/appfileformat.html
      O tamanho é menor que 1 MB e https://sqlite.org/footprint.html, mesmo com todos os recursos ativados, são 750 KB: https://www.sqlite.org/about.html
      Dá para remover bastante funcionalidade na compilação, e também parece haver opções para ajustar ou reduzir o planejador de consultas: https://www.sqlite.org/compile.html
      Além disso, há a frase: “SQLite não compete com bancos de dados cliente/servidor. SQLite compete com fopen()”: https://www.sqlite.org/whentouse.html
      No fim, o que é necessário não é o banco de dados em si, mas uma biblioteca que forneça a API e o comportamento de um banco de dados
    • O aspecto das transações foi mais difícil do que eu imaginava, especialmente no acesso simultâneo a arquivos. Na época, lidar com SQLITE_BUSY era bem complicado
      Eu sei que falhas de serialização são esperadas no processamento de transações, mas no SQLite era difícil distinguir falhas persistentes, como uma espécie de autodeadlock, de problemas temporários de atualização simultânea. Se for uma falha temporária, basta executar de novo a closure que define o trabalho da transação; se for persistente, não faz sentido
      Parte do problema é que sqlite3_stmt combina tanto a natureza de uma instrução preparada quanto a de um conjunto de resultados. Você acaba mantendo isso por bastante tempo para cachear o bytecode compilado e, se parar no meio da iteração, pode estar segurando um bloqueio naquele momento. Isso pode causar falhas inesperadas de upgrade de bloqueio
      No fim, eliminei o problema criando relatórios de erro detalhados com sqlite3_next_stmt, sqlite3_stmt_busy e sqlite3_sql. Mesmo sendo para uso pessoal, o código de retry de transações estava cheio de logging opcional e comentários. A lógica de retry de transações para PostgreSQL foi muito mais fácil
      Outra coisa que me surpreendeu foi a documentação dizendo que, no modo WAL com synchronous=NORMAL, uma transação commitada pode ser revertida após perda de energia ou queda do sistema: https://sqlite.org/pragma.html#pragma_synchronous
      Isso não era relevante para a minha aplicação
    • O SQLite já é usado exatamente para esse fim. É usado como OGC GeoPackage, e conjuntos de dados da Mapbox/Maptiler também o utilizam
    • Alguns formatos são projetados, acima de tudo, para intercâmbio. O argumento do lado do SQLite é que os donos dos apps deveriam impor o formato SQLite aos usuários e, na prática, transformá-lo em padrão, deixando de fora o trabalho de torná-lo um padrão legalmente reconhecido
      Se Richard Hipp e a empresa mostrarem um padrão SQLite ISO/IEC/ANSI/ETSI do qual nunca se afastarão, uma análise jurídica de que não há patentes que possam afetá-lo e várias implementações SQLite compatíveis que preservem todos os benefícios, aí poderemos discutir recomendá-lo como formato de arquivo. Caso contrário, é pedir para criar uma forte dependência de uma implementação de fonte única e repassar isso aos usuários
      XML, ASN.1 e JFIF são padrões formais, e o ZIP também é um padrão formal adotado como ISO/IEC 21320-1:2015 durante o processo de padronização do OpenDocument
      O mais importante em documentos é que todos os outros consigam lê-los. Reduzir o tempo de atualização em disco é secundário. Não podemos deixar de aprender com o que a Microsoft fez ao distorcer órgãos de padronização para manter dependência: https://arstechnica.com/uncategorized/2008/10/norwegian-standards-body-implodes-over-ooxml-controversy/
    • Olhando para os apps da Apple, a maioria usa SQLite como formato de armazenamento. iMovie, iPhoto, Voice recording e outros fazem isso, e o Docker também
      Não pode ser uma escolha tão errada assim
  • Outro exemplo são tiles de mapa raster. Na prática, são pequenas imagens quadradas que podem chegar a milhões
    Testei Zip, tar, sistema de arquivos e SQLite, e o SQLite foi o mais rápido e o menor, melhor até do que arquivos comuns sem overhead

    • Muitos sistemas de arquivos têm problemas quando há dezenas de milhares ou mais arquivos em um único diretório, e é exatamente isso que acontece com tiles de mapa. Não surpreende que o SQLite seja mais rápido
    • Se o SQLite é mais rápido, o problema está na biblioteca Zip usada
      O SQLite tem uma grande desvantagem. Um BLOB obtido do banco de dados não pode ser acessado com mmap, então precisa ser copiado para outro lugar. Um arquivo Zip, se não estiver comprimido ou estiver comprimido com alguma codificação peculiar como PVRTC, pode ser acessado diretamente com mmap
  • O OpenDocument é composto por XML e imagens compactadas. No fim, isso significa analisar o formato inteiro e carregá-lo na memória
    Não entendo muito bem como o SQLite melhoraria isso. XML não é ideal, mas como está compactado em Zip, a penalidade de tamanho também não é grande
    Todas as vantagens listadas no artigo sobre SQLite poderiam ser implementadas usando SQLite como o modelo de runtime do documento. Isso seria possível tanto em disco quanto em memória, mas o SQLite não precisa ser o formato de transmissão
    Na verdade, o SQLite pode ficar maior do que o formato atual. Após alterações, podem surgir espaços não utilizados, e ele pode ficar fragmentado e esparso. Se for preciso otimizar toda vez, vantagens como salvamento rápido também desaparecem
    Formatos que precisam de atualizações delta e consultas rápidas por índice, sem carregar o arquivo inteiro na memória, de fato costumam usar bastante SQLite como formato de arquivo. Mas sinto que o OpenDocument foi um exemplo ruim para escolher como alvo do SQLite nesse cenário hipotético

    • XML e Zip não lidam direito com atualizações incrementais. Ao salvar, é preciso escrever o arquivo inteiro da aplicação, e se algo der errado durante a escrita, pode ocorrer corrupção
      Se usar SQLite como formato em disco e implementar a aplicação corretamente, talvez não se termine em um estado corrompido
      XML/Zip também conseguem algo parecido com um truque de renomeação, mas o SQLite oferece isso em um único arquivo em disco. Se você já usa SQLite como modelo em memória, não há motivo para não usá-lo também como formato em disco/de transmissão. Nesse ponto, é praticamente de graça
      A questão do tamanho do arquivo provavelmente pode ser tratada com VACUUM