Miért nem használunk még adatbázist
Kiértékeltük a Turso/libSQL-t az iPades kézírásos alkalmazásunkhoz, mindent megmértünk, és egyszerű fájlokat választottunk. A munkaterhelésnek nincs szüksége adatbázisra, és minden lépésnek csak a már létező problémákért kellene fizetnie.
A Lulucat Notes egy iPadre készült kézírásos alkalmazás. A múlt hétig egyetlen vászon volt benne, és a második jegyzet fogalmát sem ismerte. Éppen jegyzetkönyvtárat készültünk hozzáadni — több dokumentumot, amelyek mindegyike több oldalt tartalmaz —, és az első architekturális kérdés a tárolás volt.
Az adatbázis kézenfekvő válasznak tűnt. A jegyzetalkalmazások strukturált adatokat tárolnak. A strukturált adatok adatbázisba kerülnek. Kiértékeltük a Turso rendszert és a Swift SDK-ját, futtattuk az iOS-szimulátorban, valódi vonásadatokon mértük meg, majd úgy döntöttünk, hogy nem használjuk.
Ehelyett egyszerű fájlokat választottunk. Ezt találtuk, és ezért hoztuk meg ezt a döntést.

Fotó: Gabriel Cox, Unsplash. Unsplash-licenc.
Mit csinál valójában az alkalmazás az adatokkal?
Egy kézírásos alkalmazás adathozzáférési mintája szűk és kiszámítható. Az olvasás azt jelenti, hogy megnyitunk egy oldalt, és egyszerre betöltjük a memóriába az összes elemét — az összes vonást és az összes képet. A vászon mindent tartalmaz; soha nem futtat részleges lekérdezést. Az írás azt jelenti, hogy befejezünk egy tollvonást, és hozzáadunk egy elemet az oldalhoz. Ritka esetekben a felhasználó kitörli egy vonás egy részét, áthelyez egy kijelölést vagy töröl valamit, de ezek továbbra is egyetlen oldalt és egyetlen elemet érintő műveletek.
Nincs párhuzamos hozzáférés. Egyszerre egy ember ír egy dokumentum egyetlen oldalára. Dokumentumok közötti keresés sincs — a jegyzetkönyvtárnak dokumentumonként csak a címre, egy időbélyegre, az oldalak számára és egy borító-bélyegképre van szüksége, ezek egyike sem igényli az oldalak tartalmának beolvasását.
Az adatbázisokat lekérdezésekhez, indexekhez és a párhuzamosság koordinálásához építik. Az alkalmazásunk egyiket sem használja.
A Turso kiértékelése
Kiértékeltük a libsql-swift csomagot, a Turso libSQL-motorjának hivatalos Swift SDK-ját.
Az SDK működik. Mind a kilenc tesztesete sikeres. Beépítettük az alkalmazás egy másolatába, iOS-szimulátorra fordítottuk, elindítottuk, és helyi adatbázist hoztunk létre az alkalmazás sandboxában. Egyetlen tranzakcióban 100 darab, egyenként 3,400 mintavételi pontot tartalmazó vonást írtunk — 4,080,000 bájtnyi BLOB-adatot. A fejlesztői Macünkön ez körülbelül 0.019 másodpercig tartott.
A PRAGMA wal_checkpoint(TRUNCATE) futtatása után a WAL-fájl nullára zsugorodott. Csak a fő .db fájlt kellett egy másik helyre másolnunk, megnyitnunk, és minden adatot vissza tudtunk olvasni belőle. Maga a motor stabil.
Az SDK-nak költségei vannak. A CLibsql.xcframework mérete 161 MB. A linkelés után a Debug szimulátor-buildünk körülbelül 1.9 MB-ról körülbelül 8.2 MB-ra nőtt. Az API szinkron és blokkoló, Swift Concurrency-wrapperek nélkül. Nincs külön close() metódus. A Transaction.commit() nem dob hibát — az alapul szolgáló C API void értéket ad vissza. A repository README-je «technical preview»-ként jelöli az SDK-t, a legutóbbi commit pedig körülbelül egy évvel a kiértékelésünk előtt, 2025 júliusában született.
Hiányosság van a Turso ökoszisztémájában. A Turso új projektekhez most az új «Turso Database» motorját és a «Turso Sync» protokollját ajánlja. A Turso Sync kliens-SDK-kat kínál TypeScripthez, Pythonhoz, Gohoz és Rusthoz. Swifthez nem kínál. A régi Embedded Replica mód létezik a libsql-swift csomagban, de a Swift-inicializálója nem teszi elérhetővé a teljesen local-first mobilalkalmazáshoz szükséges offline paramétert. A libsql-swift mai bevezetésével kapnánk egy helyi SQLite-forkot, de nem kapnánk meg a Turso megkülönböztető szinkronizációs képességeit.
Mibe kerülne nekünk most egy adatbázis?
Még ha az SDK érett is lenne, olyan költségeket fizetnénk, amelyek nem adnak semmit a munkaterhelésünkhöz:
A WAL kísérőfájljainak kezelése. A futó adatbázis létrehozza a -wal és -shm kísérőfájlokat. Egy dokumentum másolásához előbb checkpointot kell futtatni, vagy mindhárom fájlt atomikusan kell másolni. Egy .lnote csomag Files-ba vagy AirDropba történő exportja így egy előzetes exportlépést igényelne, amelyet a felhasználó nem lát, a fejlesztő pedig nem felejthet el.
Egy adapterréteg. A vonásokat BLOB-okba kellene szerializálni, majd vissza kellene deszerializálni. Az oldal elemeinek természetes array-sorrendje van, amelyet a vászon közvetlenül renderel; egy adatbázis sor-sorrend- és z-index-oszlopokat vezetne be. Fordítási réteget írnánk ugyanazon adatok két reprezentációja közé, és minden sémamódosításkor karban kellene tartanunk.
Egy 161 MB-os függőség. Egy olyan alkalmazásnál, amelynek Debug-buildje 2 MB alatti, a saját méreténél több mint 80× nagyobb függőség észrevehető költség — különösen, ha «technical preview» címkét visel, és egy éve nem történt benne változás.
Ezek a költségek nem hipotetikusak. Abban a pillanatban elkezdődnek, amikor a függőséget belinkeljük. Cserébe olyan képességeket kapunk — lekérdezést, indexelést és párhuzamos írásokat —, amelyeket az alkalmazásunk nem használ.
A leszállított megoldás: snapshot-fájlcsomagok
Egy .lnote dokumentum könyvtárcsomag:
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
A document.json a dokumentum struktúrájának hiteles forrása: azonosítója, címe, időbélyegei és az oldalak rendezett listája, amely minden oldalhoz tartalmazza a vászon méretét, az időbélyegeket és az elemszámot. Az oldalak tartalomfájljai ugyanabban a kvantált egészszámos kódolásban tárolják az elemek tömbjét, amelyet az alkalmazás már használ — a koordinátákat és sugarakat 0.1 pontos pontossággal, a nyomást ezredértékekben, az időbélyegeket pedig relatív milliszekundumokban.
A jegyzetkönyvtár csak a manifest.json fájlt és a borító-bélyegképeket olvassa. Soha nem elemzi a document.json fájlt vagy bármelyik oldal tartalmát. Egy oldal megnyitása egyetlen .content fájlt tölt be. Ez az egyetlen fájlolvasás, amely hozzáér a vonásadatokhoz.
Az oldalak megoldják az írási amplifikációt
Egy kézírásos alkalmazásban már létezik az oldal fogalma — ez az a felhasználók számára természetes egység, amelyről lapozással a következőre lépnek. Ha az oldalt tesszük a perzisztencia egységévé, az automatikus mentés csak a módosult oldalakat írja újra.
Egyetlen kézírásos oldal — mondjuk 1,000–2,000 vonás — körülbelül 3–5 MB-ot foglal a kvantált formátumunkban. Egy 21 vonásból álló felvétel, amely vonásonként 3,400 mintavételi pontot tartalmaz, körülbelül 55 KB-ra kvantálódik. Egy oldal snapshotjának flash-tárolóra írása modern hardveren 10–20 ms-ig tart. 0.5 másodperces debounce mellett a mentések láthatatlanok a felhasználó számára.
A mentés költsége az aktuális oldalon végzett írás mennyiségével skálázódik, nem a dokumentum oldalainak teljes számával. Egy 200 oldalas jegyzetfüzet pontosan olyan gyorsan menthető, mint egy 2 oldalas, mert csak a módosult oldal íródik újra.
Minden írás atomikus fájlműveleteket használ — ideiglenes fájlba ír, majd átnevezi azt —, ezért egy mentés közbeni összeomlás nem hozhat létre csonkolt oldalt. A háttérbe lépés azonnal kiírja az összes módosult oldalt, ugyanúgy, ahogy az alkalmazás már az egyetlen vászon idején is tette.
Konzisztencia tranzakciók nélkül
A fájlcsomagokban nincsenek tranzakciók, de világos tulajdonosi szabályaik ugyanazt a célt szolgálják:
Az erőforrások megelőzik a hivatkozásokat. Amikor a felhasználó képet illeszt be, az erőforrásfájl azonnal az assets/ könyvtárba kerül. Az oldalsnapshotot, amely az azonosítójával hivatkozik az erőforrásra, a késleltetett automatikus mentés írja ki később. Egyetlen oldal sem hivatkozik olyan erőforrásra, amely nincs a lemezen.
A hiteles forrás dönt. A document.json fájl és a pages/ könyvtár a hiteles forrás. A manifest.json gyorsítótár. Ha eltérés van közöttük, a következő mentés a forráshoz igazítja a gyorsítótárat. A bélyegképek származtatott adatok, és bármikor újragenerálhatók.
Árvák inkább, mint lógó hivatkozások. Egy összeomlás legrosszabb következménye egy árva erőforrás — egy fájl az assets/ könyvtárban, amelyre egyetlen oldal sem hivatkozik. Az árvákat a dokumentum bezárásakor eltávolítjuk. A fordított helyzet — hogy egy oldal hiányzó fájlra hivatkozik — nem fordulhat elő, mert az erőforrások előbb kerülnek a lemezre, mint az azokra hivatkozó oldalsnapshot.
Ezeket a szabályokat egyszerűbb átlátni, mint a WAL checkpointolását és a tranzakciók izolációját, ráadásul pontosan illeszkednek az alkalmazás egyetlen folyamatot és egyetlen oldalt használó hozzáférési mintájához.
A frissítési út le van írva
Az egyszerű fájlok mostani választása nem jelenti azt, hogy örökre ezeknél maradunk. A csomagstruktúrát úgy terveztük, hogy a tárolómotor frissítése a csomag belsejét változtassa meg, magát a csomagot ne.
1. szint: snapshot + hozzáfűzési napló. Ha az írási amplifikáció valaha érzékelhetővé válna — például ha a több ezer vonást tartalmazó oldalon végzett folyamatos írás észrevehető mentési késést okozna —, minden oldalfájl egy snapshotra és egy csak hozzáfűzést engedő naplóra válna szét. Az új elemek [length][CRC][type][payload] keretekként kerülnének a naplóba. A napló visszajátszásakor minden olyan keretet eldobunk, amelynek CRC-je nem egyezik, ezzel összeomlásbiztonságot adva. Amikor a napló túllép egy küszöböt, vagy az oldal bezárul, a snapshotba olvasztjuk vissza. Ez ~200 LOC és nulla külső függőség.
Mivel az oldalszintű snapshotok már megszüntetik az oldalak közötti írási amplifikációt, lehet, hogy erre a szintre sokáig nem lesz szükség. Egy 5 MB-os oldal 0.5 másodpercenkénti újraírása bőven a flash-írási kereteken belül marad.
2. szint: SQLite-adatbázis. Ha az alkalmazásnak valaha teljes szöveges keresésre lenne szüksége a jegyzetek között, elemenkénti szinkronizációra vagy dokumentumok közötti indexelésre, a SQLite lenne a megfelelő eszköz. A valószínű motor ekkor a GRDB lenne: érett, forrásból fordított Swift-wrapper, szinte nulla bináris mérettöbblettel. A libsql-swift csomagot csak akkor vennénk újra fontolóra, ha a Turso ökoszisztémája — különösen a Swifthez készült Turso Sync — valódi termékigénnyé válna.
A migráció mechanikus: minden oldal elemkészlete egy strokes / images táblára képezhető le, amely vonásonként egy megváltoztathatatlan BLOB-ot tartalmaz (mintavételi pontonként 16 bájt little-endian bináris formában). A 100 vonásra mért 0.019 másodperces benchmark igazolja, hogy a megközelítés életképes. Élesítés előtt a migráció ellenőrizné, hogy a régi csomag és az új adatbázis elemszámai és erőforrásszámai megegyeznek, továbbá hogy az összeomlás utáni helyreállítás, a WAL-korlátok és a háttérben végzett kiírás minden tesztje sikeres.
Mikor érdemes fizetni?
Ez a döntés nem ítélet az adatbázisok felett. A SQLite kezeli a párhuzamos írókat, az összetett lekérdezéseket és a megosztott állapot összeomlás utáni helyreállítását — az alkalmazásunknak jelenleg egyikre sincs szüksége. Nettó veszteség olyan képességekért fizetni, mielőtt az alkalmazásban megjelennének az általuk megoldott problémák.
Az adatbázis költségei — a függőség, a WAL kezelése, az adapterréteg és a bináris méret — abban a pillanatban elkezdődnek, amikor a könyvtárat belinkeljük. Az előnyök akkor jelennek meg, amikor az alkalmazásnak futtatandó lekérdezései, karbantartandó indexei vagy koordinálandó párhuzamos írói vannak. Ebben a szakaszban egyik sem áll rendelkezésére.
A tárolásunk fejlődésének minden lépése csak a már megjelent problémákért fog fizetni. Az oldalszintű snapshotok a mai problémáért fizetnek: többoldalas dokumentumok mentéséért a teljes fájl újraírása nélkül. Ha az írási amplifikáció mérhetővé válik, a hozzáfűzési napló ezért a problémáért fizet. Ha a keresés vagy a szinkronizáció termékigénnyé válik, az adatbázis azért a problémáért fog fizetni.
A csomagstruktúra, a manifest és a dokumentumséma nem kötődik egyetlen tárolómotorhoz sem. A váltás költsége alacsony, mert a határok a megfelelő helyen vannak. Amikor eljön a nap, hogy valóban szükségünk lesz adatbázisra, egy konkrét, már megmért problémára fogjuk bevezetni — nem egy hipotetikusra.