Zašto još ne koristimo bazu podataka
Procijenili smo Turso/libSQL za našu aplikaciju za rukopis na iPadu, sve izmjerili i odabrali obične datoteke. Radno opterećenje ne treba bazu podataka, a svaki bi korak trebao plaćati samo za probleme koji su se već pojavili.
Lulucat Notes je aplikacija za pisanje rukopisom na iPadu. Do prošlog tjedna imala je samo jedno platno i nije poznavala pojam druge bilješke. Spremali smo se dodati biblioteku bilješki — više dokumenata, od kojih svaki ima više stranica — a prvo arhitektonsko pitanje bilo je gdje spremati podatke.
Baza podataka činila se očitim odgovorom. Aplikacije za bilješke spremaju strukturirane podatke. Strukturirani podaci idu u baze podataka. Procijenili smo Turso i njegov Swift SDK, pokrenuli ga u iOS simulatoru, testirali na stvarnim podacima o potezima i zatim odlučili da ga nećemo koristiti.
Umjesto toga odabrali smo obične datoteke. Evo što smo otkrili i zašto smo tako odlučili.

Fotografija: Gabriel Cox na Unsplashu. Licenca Unsplasha.
Što aplikacija zapravo radi s podacima
Aplikacija za pisanje rukopisom ima uzak i predvidljiv obrazac pristupa podacima. Čitanje znači otvoriti stranicu i odjednom učitati sve njezine elemente u memoriju — sve poteze i sve slike. Platno sadrži sve; nikad ne izvodi djelomični upit. Pisanje znači dovršiti potez olovke i dodati jedan element na stranicu. U rijetkim slučajevima korisnik izbriše dio poteza, pomakne odabir ili nešto ukloni, ali i to su operacije nad jednom stranicom i jednim elementom.
Nema istodobnog pristupa. Jedna osoba u jednom trenutku piše na jednoj stranici jednog dokumenta. Nema pretraživanja između dokumenata — biblioteci bilješki za svaki dokument trebaju samo naslov, vremenska oznaka, broj stranica i minijatura naslovnice, a ništa od toga ne zahtijeva čitanje sadržaja stranice.
Baze podataka izgrađene su za upite, indekse i koordinaciju istodobnosti. Naša aplikacija ne koristi nijedno od toga.
Procjena platforme Turso
Procijenili smo libsql-swift, službeni Swift SDK za Tursoov mehanizam libSQL.
SDK radi. Svih njegovih devet testnih slučajeva prolazi. Integrirali smo ga u kopiju aplikacije, izgradili aplikaciju za iOS simulator, pokrenuli je i stvorili lokalnu bazu podataka u sandboxu aplikacije. U jednoj smo transakciji zapisali 100 poteza, svaki s 3,400 uzoraka — 4,080,000 bajtova BLOB podataka. Na našem razvojnom Macu to je trajalo oko 0.019 sekundi.
Nakon pokretanja PRAGMA wal_checkpoint(TRUNCATE), WAL datoteka smanjila se na nulu. Mogli smo kopirati samo glavnu .db datoteku na drugo mjesto, otvoriti je i ponovno pročitati sve podatke. Sam je mehanizam solidan.
SDK ima troškove. CLibsql.xcframework zauzima 161 MB. Nakon povezivanja, naš Debug build za simulator povećao se s približno 1.9 MB na približno 8.2 MB. API je sinkron i blokira, bez wrappera za Swift Concurrency. Ne postoji izričita metoda close(). Transaction.commit() ne baca iznimku — temeljni C API vraća void. README repozitorija SDK naziva «technical preview», a najnoviji commit bio je otprilike godinu dana prije naše procjene, u srpnju 2025.
U Tursoovu ekosustavu postoji praznina. Turso sada za nove projekte preporučuje svoj novi mehanizam «Turso Database» i protokol «Turso Sync». Turso Sync ima klijentske SDK-ove za TypeScript, Python, Go i Rust. Za Swift ga nema. Stari način Embedded Replica postoji u libsql-swift, ali njegov Swift inicijalizator ne izlaže parametar offline, potreban potpuno lokalnoj mobilnoj aplikaciji po modelu local-first. Ako danas usvojimo libsql-swift, dobit ćemo lokalni fork SQLitea, ali ne i mogućnosti sinkronizacije po kojima se Turso izdvaja.
Koliko bi nas baza podataka sada koštala
Čak i da je SDK zreo, plaćali bismo troškove koji našem radnom opterećenju ne donose ništa:
Upravljanje WAL pomoćnim datotekama. Baza podataka koja radi stvara prateće datoteke -wal i -shm. Kopiranje dokumenta zahtijeva prethodni checkpoint ili atomsko kopiranje sve tri datoteke. Izvoz paketa .lnote u Files ili AirDrop sada bi zahtijevao korak prije izvoza koji korisnik ne može vidjeti, a razvojni programer ne može zaboraviti.
Adapterski sloj. Poteze bi trebalo serijalizirati u BLOB-ove i zatim ih deserializirati. Elementi stranice imaju prirodan redoslijed arraya koji platno izravno iscrtava; baza podataka uvela bi stupce za redoslijed redaka i z-index. Pisali bismo sloj za prevođenje između dvaju prikaza istih podataka i održavali ga pri svakoj promjeni sheme.
Ovisnost od 161 MB. Za aplikaciju čiji je Debug build manji od 2 MB, ovisnost veća od 80× same aplikacije trošak je koji vrijedi primijetiti — posebno kad je označena kao «technical preview» i nije se mijenjala godinu dana.
Ti troškovi nisu hipotetski. Počinju čim se ovisnost poveže. A zauzvrat dobivamo mogućnosti — upite, indeksiranje i istodobne upise — koje naša aplikacija ne koristi.
Rješenje koje smo isporučili: paketi datoteka sa snimkama
Dokument .lnote je paket direktorija:
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 izvor je istine za strukturu dokumenta: njegov ID, naslov, vremenske oznake i uređeni popis stranica s veličinom platna, vremenskim oznakama i brojem elemenata za svaku stranicu. Datoteke sadržaja stranica spremaju niz elemenata u istom kvantiziranom cjelobrojnom kodiranju koje aplikacija već koristi — koordinate i radijuse s preciznošću od 0.1 točke, pritisak u tisućinkama i vremenske oznake u relativnim milisekundama.
Biblioteka bilješki čita samo manifest.json i minijature naslovnice. Nikad ne raščlanjuje document.json ni sadržaj ijedne stranice. Otvaranje stranice učitava jednu datoteku .content. To je jedino čitanje datoteke koje dodiruje podatke poteza.
Stranice rješavaju pojačanje upisa
Aplikacija za pisanje rukopisom već ima pojam stranice — to je jedinica o kojoj korisnici razmišljaju, a između stranica prelaze povlačenjem. Ako stranicu učinimo jedinicom trajnog spremanja, automatsko spremanje ponovno će zapisivati samo promijenjene stranice.
Jedna stranica rukopisa — recimo 1,000 do 2,000 poteza — zauzima približno 3–5 MB u našem kvantiziranom formatu. Jedna snimka od 21 poteza s 3,400 uzoraka po potezu kvantizira se na oko 55 KB. Zapisivanje snimke stranice na flash pohranu traje 10–20 ms na suvremenom hardveru. Uz debounce od 0.5 sekundi spremanja su korisniku nevidljiva.
Trošak spremanja raste s količinom pisanja na trenutačnoj stranici, a ne s ukupnim brojem stranica u dokumentu. Bilježnica od 200 stranica sprema se točno jednako brzo kao bilježnica od 2 stranice, jer se ponovno zapisuje samo izmijenjena stranica.
Svaki upis koristi atomske operacije nad datotekama — zapisivanje u privremenu datoteku, a zatim preimenovanje — pa rušenje usred spremanja ne može proizvesti skraćenu stranicu. Prelazak u pozadinu odmah zapisuje sve izmijenjene stranice, kao što je aplikacija već radila s jednim platnom.
Dosljednost bez transakcija
Paketi datoteka nemaju transakcije, ali imaju jasna pravila vlasništva koja služe istoj svrsi:
Resursi prije referenci. Kad korisnik umetne sliku, datoteka resursa odmah se zapisuje u assets/. Snimka stranice, koja referencira resurs prema njegovu ID-u, zapisuje se kasnije odgođenim automatskim spremanjem. Ni u jednom trenutku stranica ne referencira resurs koji ne postoji na disku.
Izvor istine pobjeđuje. document.json i direktorij pages/ zajedno su izvor istine. manifest.json je predmemorija. Ako se ne podudaraju, sljedeće spremanje usklađuje predmemoriju s izvorom. Minijature su izvedene i mogu se ponovno generirati u bilo kojem trenutku.
Bolje siročad nego viseće reference. Najgori ishod rušenja je siročić-resurs — datoteka u assets/ na koju nijedna stranica ne referencira. Siročad se čisti pri zatvaranju dokumenta. Obrnuto — da stranica referencira datoteku koja nedostaje — ne može se dogoditi jer se resursi zapisuju prije snimke stranice koja na njih referencira.
Ta su pravila jednostavnija za razumjeti od WAL checkpointinga i izolacije transakcija, a točno odgovaraju obrascu pristupa aplikacije s jednim procesom i jednom stranicom.
Put nadogradnje je zapisan
Odabir običnih datoteka sada ne znači da ćemo ih odabrati zauvijek. Struktura paketa osmišljena je tako da nadogradnja mehanizma pohrane promijeni ono što je unutar paketa, a ne sam paket.
Razina 1: snimka + dnevnik dodavanja. Ako pojačanje upisa ikad postane primjetno — recimo, ako neprekidno pisanje na stranici s tisućama poteza uzrokuje vidljivo kašnjenje spremanja — svaka bi se datoteka stranice podijelila na snimku i dnevnik koji dopušta samo dodavanje. Novi elementi dodavali bi se kao okviri [length][CRC][type][payload]. Pri ponovnoj reprodukciji dnevnika odbacuje se svaki okvir čiji se CRC ne podudara, čime se dobiva sigurnost pri rušenju. Kad dnevnik prijeđe prag ili se stranica zatvori, ponovno se spaja sa snimkom. To je ~200 LOC i nula vanjskih ovisnosti.
Budući da snimke na razini stranice već uklanjaju pojačanje upisa između stranica, ova razina možda još dugo neće biti potrebna. Ponovno zapisivanje stranice od 5 MB svakih 0.5 sekundi dobro je unutar proračuna flash upisa.
Razina 2: baza podataka SQLite. Ako aplikaciji ikad zatreba pretraživanje punog teksta kroz bilješke, sinkronizacija po elementu ili indeksiranje između dokumenata, SQLite će biti pravi alat. Tada bi vjerojatni mehanizam bio GRDB, zreli Swift wrapper kompajliran iz izvornog koda, s gotovo nultim binarnim dodatkom. libsql-swift ponovno bismo razmotrili samo ako Tursoov ekosustav — konkretno Turso Sync za Swift — postane stvarna potreba proizvoda.
Migracija je mehanička: niz elemenata svake stranice preslikava se u tablicu strokes / images s jednim nepromjenjivim BLOB-om po potezu (16 bajtova po uzorku u binarnom little-endian obliku). Mjerenje od 0.019 sekundi za 100 poteza potvrđuje da je pristup izvediv. Prije isporuke migracija bi provjerila podudaraju li se broj elemenata i broj resursa između starog paketa i nove baze podataka te prolaze li oporavak nakon rušenja, WAL granice i pražnjenje u pozadini.
Kada platiti
Ova odluka nije osuda baza podataka. SQLite upravlja istodobnim zapisivačima, složenim upitima i oporavkom od rušenja nad dijeljenim stanjem — našoj aplikaciji trenutačno ništa od toga ne treba. Plaćati te mogućnosti prije nego što aplikacija dobije probleme koje one rješavaju neto je gubitak.
Troškovi baze podataka — ovisnost, upravljanje WAL-om, adapterski sloj i binarna veličina — počinju čim se biblioteka poveže. Koristi počinju kad aplikacija ima upite koje treba izvršiti, indekse koje treba održavati ili istodobne zapisivače koje treba koordinirati. U ovoj fazi nema ništa od toga.
Svaki korak u razvoju naše pohrane plaćat će samo probleme koji su se već pojavili. Snimke na razini stranice plaćaju za današnji problem: spremanje dokumenata s više stranica bez ponovnog zapisivanja cijele datoteke. Ako pojačanje upisa postane mjerljivo, dnevnik dodavanja platit će za taj problem. Ako pretraživanje ili sinkronizacija postanu potreba proizvoda, baza podataka platit će za taj problem.
Struktura paketa, manifest i shema dokumenta nisu vezani uz nijedan mehanizam pohrane. Trošak prebacivanja je nizak jer su granice na pravim mjestima. Kad dođe dan da nam baza podataka doista zatreba, usvojit ćemo je zbog konkretnog, već izmjerenog problema — ne zbog hipotetskog.