Prečo zatiaľ nepoužívame databázu
Pre našu aplikáciu na písanie rukou v iPade sme vyhodnotili Turso/libSQL, všetko zmerali a zvolili ploché snapshot súbory. Táto pracovná záťaž databázu nepotrebuje — každý ďalší krok by mal platiť iba za problémy, ktoré už existujú.
Lulucat Notes je aplikácia na písanie rukou pre iPad. Ešte minulý týždeň mala jedno plátno a nepoznala pojem druhá poznámka. Chystali sme sa pridať knižnicu poznámok — viacero dokumentov, každý s viacerými stranami — a prvou architektonickou otázkou bolo úložisko.
Databáza vyzerala ako samozrejmá odpoveď. Aplikácie na poznámky ukladajú štruktúrované dáta. Štruktúrované dáta patria do databáz. Vyhodnotili sme Turso a jeho Swift SDK, spustili sme ho v simulátore iOS, zmerali reálne dáta ťahov a potom sme sa rozhodli, že ho nepoužijeme.
Namiesto toho sme zvolili ploché súbory. Tu je, čo sme zistili a prečo sme sa tak rozhodli.

Foto: Gabriel Cox na Unsplash. Licencia Unsplash.
Čo aplikácia s dátami naozaj robí
Aplikácia na písanie rukou má úzky a predvídateľný spôsob prístupu k dátam. Čítanie znamená otvoriť stranu a naraz načítať do pamäte každý jej prvok — všetky ťahy aj všetky obrázky. Plátno obsahuje všetko; nikdy nespúšťa čiastočný dotaz. Písanie znamená dokončiť ťah pera a pripojiť jeden prvok k strane. V zriedkavých prípadoch používateľ vymaže časť ťahu, presunie výber alebo niečo odstráni, no stále ide o operácie s jednou stranou a jedným prvkom.
Súbežný prístup neexistuje. Jeden človek naraz píše na jednej strane jedného dokumentu. Neexistuje ani vyhľadávanie naprieč dokumentmi — knižnica poznámok potrebuje pre každý dokument iba názov, časovú pečiatku, počet strán a miniatúru obálky; obsah strán čítať nemusí.
Databázy sú vytvorené na dotazy, indexy a koordináciu súbežnosti. Naša aplikácia nepoužíva ani jednu z týchto troch vecí.
Vyhodnotenie Turso
Vyhodnotili sme libsql-swift, oficiálne Swift SDK pre engine libSQL od Turso.
SDK funguje. Všetkých 9 testovacích prípadov je úspešných. Integrovali sme ho do kópie aplikácie, zostavili ju pre simulátor iOS, spustili a vytvorili lokálnu databázu v sandboxe aplikácie. V jednej transakcii sme zapísali 100 ťahov, každý s 3400 vzorkovacími bodmi — 4,080,000 bajtov BLOB dát. Na našom vývojárskom Macu to trvalo približne 0.019s.
Po spustení PRAGMA wal_checkpoint(TRUNCATE) sa WAL súbor zmenšil na nulu a mohli sme skopírovať samotný hlavný .db súbor na iné miesto, otvoriť ho a načítať všetky dáta. Samotný engine je spoľahlivý.
SDK má náklady. CLibsql.xcframework má 161 MB. Po linkovaní narástol náš Debug build simulátora z približne 1.9 MB na približne 8.2 MB. API je synchrónne a blokujúce, bez wrapperov pre Swift Concurrency. Neexistuje explicitná metóda close(). Transaction.commit() nevyhadzuje výnimku — základné C API vracia void. README repozitára označuje SDK ako „technical preview“ a najnovší commit vznikol približne rok pred naším vyhodnotením, v júli 2025.
V ekosystéme Turso je medzera. Turso teraz pre nové projekty odporúča svoj nový engine „Turso Database“ a protokol „Turso Sync“. Turso Sync má klientske SDK pre TypeScript, Python, Go a Rust. Pre Swift ho nemá. Starší režim Embedded Replica existuje v libsql-swift, ale jeho Swift inicializátor neodhaľuje parameter offline, ktorý potrebuje plne local-first mobilná aplikácia. Ak dnes zvolíme libsql-swift, dostaneme lokálny fork SQLite, ale nie synchronizačné schopnosti, ktoré robia Turso výnimočným.
Koľko by nás databáza stála práve teraz
Aj keby bolo SDK vyzreté, stále by sme platili náklady, ktoré našej pracovnej záťaži nič neprinášajú:
Správa sprievodných súborov WAL. Spustená databáza vytvára sprievodné súbory -wal a -shm. Kopírovanie dokumentu znamená najprv vykonať checkpoint alebo atómovo skopírovať všetky tri súbory. Export balíka .lnote do Files alebo AirDrop by teraz vyžadoval krok pred exportom, ktorý používateľ nevidí a vývojár naň nemôže zabudnúť.
Adaptérová vrstva. Ťahy by sa museli serializovať do BLOB-ov a znova deserializovať. Prvky strany majú prirodzené poradie v poli, ktoré plátno vykresľuje priamo; databáza by pridala poradie riadkov a stĺpce z-index. Písali by sme prekladovú vrstvu medzi dvoma reprezentáciami tých istých dát a udržiavali ju pri každej zmene schémy.
Závislosť s veľkosťou 161 MB. Pre aplikáciu, ktorej Debug build má menej než 2 MB, je závislosť väčšia než 80× samotnej aplikácie nákladom, ktorý stojí za pozornosť — najmä ak je označená ako „technical preview“ a rok sa neaktualizovala.
Tieto náklady nie sú hypotetické. Začínajú v momente, keď sa závislosť linkuje. A kupujú nám schopnosti — dotazy, indexovanie a súbežné zápisy — ktoré naša aplikácia nepoužíva.
Riešenie, ktoré sme vydali: balíky snapshot súborov
Dokument .lnote je adresárový balík:
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 je zdrojom pravdy pre štruktúru dokumentu: jeho ID, názov, časové pečiatky a usporiadaný zoznam strán, pričom každá strana obsahuje veľkosť plátna, časové pečiatky a počet prvkov. Súbory s obsahom strán ukladajú pole prvkov v rovnakom kvantizovanom celočíselnom kódovaní, ktoré aplikácia už používa — súradnice a polomery s presnosťou 0.1 bodu, tlak v tisícinách a časové pečiatky v relatívnych milisekundách.
Knižnica poznámok číta iba manifest.json a miniatúry obálok. Nikdy neparsuje document.json ani obsah žiadnej strany. Otvorenie strany načíta jeden .content súbor. Je to jediné čítanie súboru, ktoré sa dotýka dát ťahov.
Strany riešia write amplification
Aplikácia na písanie rukou už má pojem strany — je to jednotka, o ktorej používatelia premýšľajú a medzi ktorou prechádzajú potiahnutím. Keď sa strana stane jednotkou perzistencie, automatické ukladanie prepisuje iba strany, ktoré sa zmenili.
Jedna ručne popísaná strana — povedzme 1,000–2,000 ťahov — zaberá v našom kvantizovanom formáte približne 3–5 MB. Jedna nahrávka s 21 ťahmi a 3400 vzorkovacími bodmi sa po kvantizácii zmestí do približne 55 KB. Zápis jedného snapshotu strany do flashového úložiska trvá na modernom hardvéri 10–20 ms. S debounce nastaveným na 0.5s zostávajú uloženia pre používateľa neviditeľné.
Cena uloženia rastie podľa množstva písania na aktuálnej strane, nie podľa celkového počtu strán v dokumente. 200-stranový zošit sa uloží presne tak rýchlo ako 2-stranový, pretože sa prepisuje iba dirty strana.
Každý zápis používa atómové operácie so súbormi — zápis do dočasného súboru a následné premenovanie — takže pád uprostred ukladania nemôže vytvoriť skrátenú stranu. Pri prechode aplikácie do backgroundu sa všetky dirty strany okamžite flushnú, čo zodpovedá správaniu, ktoré aplikácia už mala s jediným plátnom.
Konzistentnosť bez transakcií
Súborové balíky nemajú transakcie, ale majú jasné pravidlá vlastníctva, ktoré slúžia rovnakému účelu:
Zdroje pred odkazmi. Keď používateľ vloží obrázok, súbor assetu sa okamžite zapíše do assets/. Snapshot strany, ktorý odkazuje na asset podľa ID, sa zapíše neskôr odloženým automatickým ukladaním. Strana v žiadnom okamihu neodkazuje na asset, ktorý na disku neexistuje.
Zdroj pravdy vyhráva. document.json a adresár pages/ sú zdrojom pravdy. manifest.json je cache. Ak sa nezhodujú, ďalšie uloženie zosúladí cache so zdrojom. Miniatúry sú odvodené a dajú sa kedykoľvek znova vygenerovať.
Osirelé súbory namiesto visiacich odkazov. Najhorším výsledkom pádu je osirelý asset — súbor v assets/, na ktorý neodkazuje žiadna strana. Osirelé súbory sa vyčistia pri zatvorení dokumentu. Opak — strana odkazujúca na chýbajúci súbor — nemôže nastať, pretože assety sa zapíšu pred snapshotom odkazujúcej strany.
Tieto pravidlá sa vysvetľujú ľahšie než WAL checkpointing a izolácia transakcií a presne zodpovedajú prístupu aplikácie: jeden proces, jedna strana.
Cesta upgradu je zapísaná
To, že sme si teraz vybrali ploché súbory, neznamená, že ich vyberáme navždy. Štruktúra balíka je navrhnutá tak, aby upgrade úložiskového enginu zmenil obsah balíka bez zmeny balíka samotného.
Úroveň 1: snapshot + append journal. Ak sa write amplification niekedy stane citeľným — povedzme, že nepretržité písanie na strane s tisíckami ťahov spôsobí viditeľné oneskorenie uloženia — každý súbor strany sa rozdelí na snapshot a journal iba na pridávanie. Nové prvky sa pridajú ako rámce [length][CRC][type][payload]. Pri prehrávaní journalu sa zahodí každý rámec, ktorého CRC nesedí, čo zabezpečí ochranu pred pádom. Keď journal prekročí prah alebo sa strana zatvorí, zlúči sa späť so snapshotom. Je to približne ~200 LOC bez externých závislostí.
Keďže snapshoty na úrovni strán už odstraňujú write amplification medzi stranami, táto úroveň možno nebude dlho potrebná. Prepisovanie 5 MB strany každých 0.5s sa pohodlne zmestí do rozpočtu zápisov flashového úložiska.
Úroveň 2: databáza SQLite. Ak bude aplikácia niekedy potrebovať full-textové vyhľadávanie v poznámkach, synchronizáciu jednotlivých prvkov alebo indexovanie naprieč dokumentmi, SQLite bude správny nástroj. Pravdepodobným enginom vtedy bude GRDB, vyspelý Swift wrapper kompilovaný zo zdroja s takmer nulovým nárastom veľkosti binárky. K libsql-swift by sme sa vrátili iba vtedy, ak sa ekosystém Turso — konkrétne Turso Sync pre Swift — stane skutočnou produktovou potrebou.
Migrácia je mechanická: pole prvkov každej strany sa namapuje na tabuľku strokes / images s jedným nemenným BLOB-om na ťah (16 bajtov na vzorkovací bod v binárnom formáte little-endian). Benchmark 0.019s pre 100 ťahov potvrdzuje, že tento prístup je uskutočniteľný. Pred vydaním migrácia overí, či sa počty prvkov a assetov zhodujú medzi starým balíkom a novou databázou a či prejdú obnova po páde, limity WAL aj background flush.
Kedy platiť
Toto rozhodnutie nie je súdom nad databázami. SQLite zvláda súbežných zapisovateľov, zložité dotazy a obnovu po páde nad zdieľaným stavom — naša aplikácia teraz nič z toho nepotrebuje. Platiť za schopnosti skôr, než aplikácia narazí na problémy, ktoré riešia, je čistá strata.
Náklady databázy — závislosť, správa WAL, adaptérová vrstva a veľkosť binárky — začínajú v okamihu linkovania knižnice. Prínosy začínajú vtedy, keď má aplikácia dotazy na vykonanie, indexy na údržbu alebo súbežných zapisovateľov na koordináciu. V tejto fáze nemá ani jednu z týchto troch vecí.
Každý krok vývoja nášho úložiska bude platiť iba za problémy, ktoré sa už objavili. Snapshoty na úrovni strán platia za dnešný problém: ukladať viacstranové dokumenty bez prepisovania celého súboru. Ak sa write amplification stane merateľným, append journal zaplatí za tento problém. Ak sa vyhľadávanie alebo synchronizácia stane produktovou potrebou, za tento problém zaplatí databáza.
Štruktúra balíka, manifest a schéma dokumentu nie sú viazané na žiadny úložiskový engine. Náklady na zmenu sú nízke, pretože hranice sú na správnych miestach. Keď príde deň, keď budeme databázu naozaj potrebovať, prijmeme ju pre konkrétny, už zmeraný problém — nie pre hypotetický.