Per què encara no fem servir una base de dades
Hem avaluat Turso/libSQL per a la nostra aplicació d'escriptura manual per a iPad, ho hem mesurat tot i hem triat fitxers d'instantània plans. La càrrega de treball no necessita una base de dades, i cada pas només hauria de pagar pels problemes que ja existeixen.
Lulucat Notes és una aplicació d’escriptura manual per a iPad. Fins a la setmana passada només tenia un llenç i no tenia el concepte d’una segona nota. Estàvem a punt d’afegir-hi una biblioteca de notes — diversos documents, cadascun amb diverses pàgines — i la primera qüestió arquitectònica era l’emmagatzematge.
Una base de dades semblava la resposta òbvia. Les aplicacions de notes emmagatzemen dades estructurades. Les dades estructurades van a bases de dades. Vam avaluar Turso i el seu Swift SDK, el vam fer funcionar al simulador d’iOS, vam fer-hi proves de rendiment amb dades de traços reals i després vam decidir no fer-lo servir.
En lloc d’això, vam triar fitxers plans. Això és el que vam descobrir i el motiu de la decisió.

Fotografia de Gabriel Cox a Unsplash. Llicència d’Unsplash.
Què fa realment l’aplicació amb les dades
Una aplicació d’escriptura manual té un patró d’accés a les dades estret i previsible. Llegir vol dir obrir una pàgina i carregar a la memòria tots els elements que conté —tots els traços i totes les imatges— alhora. El llenç ho conté tot; no executa mai una consulta parcial. Escriure vol dir acabar un traç de bolígraf i afegir un element a la pàgina. En casos excepcionals, l’usuari esborra una part d’un traç, mou una selecció o elimina alguna cosa, però continuen sent operacions d’una sola pàgina i d’un sol element.
No hi ha accés concurrent. Una sola persona escriu en una sola pàgina d’un sol document cada vegada. Tampoc hi ha cerques entre documents: la biblioteca de notes només necessita un títol, una marca de temps, un recompte de pàgines i una miniatura de portada per a cada document; cap d’aquestes dades requereix llegir el contingut de la pàgina.
Les consultes, els índexs i la coordinació de la concurrència són allò per a què es construeixen les bases de dades. La nostra aplicació no en fa servir cap.
L’avaluació de Turso
Vam avaluar libsql-swift, el Swift SDK oficial per al motor libSQL de Turso.
L’SDK funciona. Tots nou casos de prova passen. El vam integrar en una còpia de l’aplicació, la vam compilar per al simulador d’iOS, la vam iniciar i vam crear una base de dades local al sandbox de l’aplicació. Vam escriure 100 traços de 3,400 punts de mostreig cadascun —4,080,000 bytes de dades BLOB— en una sola transacció. Va trigar aproximadament 0.019 segons al nostre Mac de desenvolupament.
Després d’executar PRAGMA wal_checkpoint(TRUNCATE), el fitxer WAL es va reduir a zero i vam poder copiar només el fitxer .db principal a una altra ubicació, obrir-lo i tornar a llegir totes les dades. El motor en si és sòlid.
L’SDK té costos. CLibsql.xcframework pesa 161 MB. Després d’enllaçar-lo, la compilació Debug del simulador va passar d’aproximadament 1.9 MB a aproximadament 8.2 MB. L’API és síncrona i bloquejant, i no hi ha embolcalls de Swift Concurrency. No hi ha cap mètode close() explícit. Transaction.commit() no llança cap excepció: l’API C subjacent retorna void. El README del repositori etiqueta l’SDK com a «technical preview», i el commit més recent era aproximadament un any anterior a la nostra avaluació, el juliol de 2025.
A l’ecosistema de Turso li falta una peça. Ara Turso recomana el seu nou motor «Turso Database» i el protocol «Turso Sync» per als projectes nous. Turso Sync té SDK de client per a TypeScript, Python, Go i Rust. No en té cap per a Swift. El mode Embedded Replica anterior existeix a libsql-swift, però el seu inicialitzador de Swift no exposa el paràmetre offline necessari per a una aplicació mòbil completament local-first. Adoptar libsql-swift avui ens dona una bifurcació local de SQLite, però no les capacitats de sincronització que fan distintiu Turso.
Què ens costaria ara una base de dades
Fins i tot si l’SDK fos madur, pagaríem costos que no aportarien res a la nostra càrrega de treball:
Gestió dels fitxers laterals de WAL. Una base de dades en execució crea els fitxers companys -wal i -shm. Copiar un document vol dir fer primer un checkpoint o bé copiar els tres fitxers atòmicament. Exportar ara un paquet .lnote a Files o AirDrop requereix un pas previ d’exportació que l’usuari no pot veure i que el desenvolupador no pot oblidar.
Una capa d’adaptació. Caldria serialitzar els traços en BLOB i deserialitzar-los de nou. Els elements de la pàgina tenen un ordre natural de matriu que el llenç renderitza directament; una base de dades introduiria un ordre de files i columnes z-index. Escriuríem una capa de traducció entre dues representacions de les mateixes dades i l’hauríem de mantenir amb cada canvi d’esquema.
Una dependència de 161 MB. Per a una aplicació la compilació Debug de la qual ocupa menys de 2 MB, una dependència de més de 80× la mida de l’aplicació és un cost que val la pena notar —especialment si està etiquetada com a «technical preview» i fa un any que no té activitat.
Aquests costos no són hipotètics. Comencen en el moment que s’enllaça la dependència. I ens compren capacitats —consultes, indexació i escriptures concurrents— que la nostra aplicació no utilitza.
La solució que hem distribuït: paquets de fitxers d’instantània
Un document .lnote és un paquet de directori:
Documents/Notes/<UUID>.lnote/
manifest.json # library cache: title, time, page count, cover
document.json # source of truth: document metadata + page order
pages/
<page-uuid>.content # one snapshot per page
assets/ # document-level shared resources
<asset-uuid>.jpg
thumbnails/
<page-uuid>.jpg # per-page thumbnail; first page doubles as cover
document.json és la font de veritat de l’estructura del document: el seu ID, títol, marques de temps i una llista ordenada de pàgines, amb la mida del llenç, les marques de temps i el recompte d’elements de cada pàgina. Els fitxers de contingut de les pàgines emmagatzemen la matriu d’elements amb la mateixa codificació d’enters quantificats que l’aplicació ja fa servir —coordenades i radis amb una precisió de 0.1 punts, pressió en mil·lèsimes i marques de temps en mil·lisegons relatius.
La biblioteca de notes només llegeix manifest.json i les miniatures de portada. No analitza mai document.json ni el contingut de cap pàgina. Obrir una pàgina carrega un únic fitxer .content. Aquesta és l’única lectura de fitxer que toca les dades dels traços.
Les pàgines resolen l’amplificació d’escriptura
Una aplicació d’escriptura manual ja té el concepte de pàgina: és la unitat en què pensa l’usuari, i l’usuari es desplaça entre pàgines amb un gest. Fer que la pàgina sigui la unitat de persistència vol dir que el desament automàtic només reescriu les pàgines que han canviat.
Una pàgina d’escriptura manual —per exemple, de 1,000 a 2,000 traços— ocupa aproximadament 3–5 MB en el nostre format quantificat. Una gravació de 21 traços amb 3,400 punts de mostreig es quantifica fins a aproximadament 55 KB. Escriure una instantània d’una pàgina a l’emmagatzematge flash triga 10–20 ms en maquinari modern. Amb un debounce de 0.5 segons, els desaments són invisibles per a l’usuari.
El cost del desament creix amb la quantitat d’escriptura de la pàgina actual, no amb el nombre total de pàgines del document. Un quadern de 200 pàgines es desa exactament tan ràpid com un quadern de 2 pàgines, perquè només es reescriu la pàgina modificada.
Cada escriptura utilitza operacions de fitxer atòmiques —escriure en un fitxer temporal i després canviar-li el nom—, de manera que un bloqueig durant el desament no pot produir una pàgina truncada. Entrar en segon pla buida immediatament totes les pàgines modificades, igual que feia l’aplicació quan només tenia un llenç.
Consistència sense transaccions
Els paquets de fitxers no tenen transaccions, però sí regles clares de propietat que compleixen la mateixa funció:
Recursos abans que referències. Quan l’usuari insereix una imatge, el fitxer de l’asset s’escriu immediatament a assets/. La instantània de pàgina, que fa referència a l’asset pel seu ID, s’escriu més tard amb el desament automàtic amb debounce. En cap moment una pàgina fa referència a un asset que no existeix al disc.
La font de veritat guanya. document.json i el directori pages/ són la font de veritat. manifest.json és una memòria cau. Si no coincideixen, el desament següent reconcilia la memòria cau perquè coincideixi amb la font. Les miniatures són derivades i es poden regenerar en qualsevol moment.
Millor orfes que referències penjants. El pitjor resultat d’un bloqueig és un asset orfe: un fitxer a assets/ que cap pàgina no fa servir. Els assets orfes es netegen quan es tanca el document. El cas contrari —una pàgina que fa referència a un fitxer que falta— no pot passar, perquè els assets s’escriuen abans que la instantània de pàgina que hi fa referència.
Aquestes regles són més fàcils de raonar que el checkpointing de WAL i l’aïllament de transaccions, i coincideixen exactament amb el patró d’accés d’un sol procés i una sola pàgina de l’aplicació.
El camí d’actualització està escrit
Triar fitxers plans ara no vol dir triar-los per sempre. L’estructura del paquet està dissenyada perquè actualitzar el motor d’emmagatzematge canviï el contingut del paquet sense canviar el paquet mateix.
Nivell 1: instantània + diari d’afegits. Si algun dia l’amplificació d’escriptura es fa perceptible —per exemple, si escriure contínuament en una pàgina amb milers de traços provoca un retard de desament que es nota—, cada fitxer de pàgina es divideix en una instantània i un diari només d’afegits. Els elements nous s’afegeixen com a trames [length][CRC][type][payload]. En reproduir el diari, es descarta qualsevol trama el CRC de la qual no coincideixi, cosa que proporciona seguretat davant de bloquejos. Quan el diari supera un llindar o es tanca la pàgina, es torna a fusionar amb la instantània. Són aproximadament 200 LOC sense cap dependència externa.
Com que les instantànies a nivell de pàgina ja eliminen l’amplificació d’escriptura entre pàgines, potser no necessitarem aquest nivell durant molt de temps. Reescriure una pàgina de 5 MB cada 0.5 segons queda folgadament dins dels pressupostos d’escriptura de la memòria flash.
Nivell 2: base de dades SQLite. Si algun dia l’aplicació necessita cerca de text complet entre notes, sincronització per element o indexació entre documents, SQLite es convertirà en l’eina adequada. El motor més probable en aquell moment serà GRDB, un embolcall de Swift madur i compilat des del codi font, amb una sobrecàrrega de mida binària gairebé nul·la. Només tornaríem a considerar libsql-swift si l’ecosistema Turso —concretament Turso Sync per a Swift— es convertís en una necessitat real del producte.
El camí de migració és mecànic: la matriu d’elements de cada pàgina es correspon amb una taula strokes / images, amb un BLOB immutable per traç (16 bytes per punt de mostreig en binari little-endian). El benchmark de 0.019 segons per a 100 traços confirma que l’enfocament és viable. Abans de distribuir-lo, la migració comprovaria que els recomptes d’elements i d’assets coincideixen entre el paquet antic i la base de dades nova, i que la recuperació després d’un bloqueig, els límits de WAL i el buidatge en segon pla superen totes les proves.
Quan cal pagar
Aquesta decisió no és un judici sobre les bases de dades. SQLite gestiona escriptors concurrents, consultes complexes i recuperació de l’estat compartit després d’un bloqueig —cap d’aquestes coses no les necessita actualment la nostra aplicació. Pagar per aquestes capacitats abans que l’aplicació tingui els problemes que resolen és una pèrdua neta.
Els costos d’una base de dades —la dependència, la gestió de WAL, la capa d’adaptació i la mida binària— comencen en el moment que s’enllaça la biblioteca. Els beneficis comencen quan l’aplicació té consultes per executar, índexs per mantenir o escriptors concurrents per coordinar. En aquesta etapa no en té cap dels tres.
Cada pas de l’evolució del nostre emmagatzematge només pagarà pels problemes que ja hagin aparegut. Les instantànies a nivell de pàgina paguen pel problema que tenim avui: desar documents de diverses pàgines sense reescriure tot el fitxer. Si l’amplificació d’escriptura es fa mesurable, un diari d’afegits pagarà per aquest problema. Si la cerca o la sincronització es converteixen en una necessitat del producte, una base de dades pagarà per aquest problema.
L’estructura del paquet, el manifest i l’esquema del document no estan lligats a cap motor d’emmagatzematge. El cost del canvi és baix perquè els límits són al lloc correcte. Quan arribi el dia que realment necessitem una base de dades, l’adoptarem per a un problema concret i ja mesurat —no per a un problema hipotètic.