Miksi emme vielä käytä tietokantaa
Arvioimme Turso/libSQL:ää iPadin käsinkirjoitussovellusta varten, mittasimme kaiken ja valitsimme litteät tiedostot. Työkuorma ei tarvitse tietokantaa, ja jokaisen vaiheen pitäisi maksaa vain jo olemassa olevien ongelmien ratkaisemisesta.
Lulucat Notes on iPadille tarkoitettu käsinkirjoitussovellus. Vielä viime viikolla siinä oli yksi kanvas eikä lainkaan käsitettä toisesta muistiinpanosta. Olimme lisäämässä muistiinpanokirjastoa — useita asiakirjoja, joissa kussakin on useita sivuja — ja ensimmäinen arkkitehtuurikysymys koski tallennusta.
Tietokanta tuntui ilmeiseltä vastaukselta. Muistiinpanosovellukset tallentavat rakenteista dataa. Rakenteinen data kuuluu tietokantoihin. Arvioimme Turso-palvelua ja sen Swift SDK:ta, saimme sen toimimaan iOS-simulaattorissa, mittasimme aitoja vetoja ja päätimme sitten olla käyttämättä sitä.
Valitsimme tavalliset tiedostot. Tässä on, mitä löysimme ja miksi teimme tämän päätöksen.

Valokuva: Gabriel Cox, Unsplash. Unsplashin käyttöoikeus.
Mitä sovellus oikeasti tekee datalla
Käsinkirjoitussovelluksen datan käyttökuvio on suppea ja ennakoitava. Lukeminen tarkoittaa sivun avaamista ja kaikkien sen elementtien lataamista muistiin kerralla — kaikkien vetojen ja kaikkien kuvien. Kanvas sisältää kaiken; se ei koskaan suorita osittaista kyselyä. Kirjoittaminen tarkoittaa kynänvedon päättämistä ja yhden elementin lisäämistä sivulle. Harvoissa tapauksissa käyttäjä pyyhkii osan vedosta, siirtää valintaa tai poistaa jotain, mutta nämäkin ovat yhden sivun ja yhden elementin operaatioita.
Samanaikaista käyttöä ei ole. Yksi henkilö kirjoittaa kerrallaan yhden asiakirjan yhdelle sivulle. Asiakirjojen välistä hakua ei ole — muistiinpanokirjasto tarvitsee kustakin asiakirjasta vain otsikon, aikaleiman, sivumäärän ja kansikuvan pikkukuvan, eikä mikään niistä vaadi sivusisällön lukemista.
Tietokannat on rakennettu kyselyitä, indeksejä ja samanaikaisuuden koordinointia varten. Sovelluksemme ei käytä mitään näistä kolmesta.
Turson arviointi
Arvioimme libsql-swift:n, Turson libSQL-moottorin virallisen Swift SDK:n.
SDK toimii. Kaikki sen yhdeksän testitapausta menevät läpi. Integroimme sen sovelluksen kopioon, rakensimme iOS-simulaattoria varten, käynnistimme sovelluksen ja loimme paikallisen tietokannan sovelluksen hiekkalaatikkoon. Kirjoitimme 100 vetoa, joissa kussakin oli 3,400 näytettä — yhteensä 4,080,000 tavua BLOB-dataa — yhdessä transaktiossa. Kehitys-Macillamme tähän kului noin 0.019 sekuntia.
Kun ajoimme PRAGMA wal_checkpoint(TRUNCATE):n, WAL-tiedosto kutistui nollaan. Pystyimme kopioimaan pelkän pääasiallisen .db-tiedoston toiseen sijaintiin, avaamaan sen ja lukemaan kaiken datan takaisin. Moottori itsessään on kunnossa.
SDK:lla on kustannuksia. CLibsql.xcframework painaa 161 MB. Linkityksen jälkeen Debug-simulaattorirakennelmamme kasvoi noin 1.9 MB:stä noin 8.2 MB:iin. API on synkroninen ja blokkaava, eikä siinä ole Swift Concurrency -kääreitä. Erillistä close()-metodia ei ole. Transaction.commit() ei heitä poikkeusta — taustalla oleva C-API palauttaa void. Repositorion README kutsuu SDK:ta «technical preview» -versioksi, ja viimeisin commit oli noin vuosi ennen arviointiamme, heinäkuussa 2025.
Turson ekosysteemissä on aukko. Turso suosittelee nyt uusiin projekteihin uutta «Turso Database» -moottoriaan ja «Turso Sync» -protokollaansa. Turso Syncillä on asiakas-SDK:t TypeScriptille, Pythonille, Golle ja Rustille. Swiftille sitä ei ole. Vanha Embedded Replica -tila on olemassa libsql-swift:ssä, mutta sen Swift-initialisoija ei tuo näkyviin offline-parametria, jota täysin paikallisensisijainen mobiilisovellus tarvitsee. libsql-swift:n käyttöönotto antaa meille nyt paikallisen SQLite-haaran, mutta ei synkronointiominaisuuksia, jotka tekevät Tursosta erityisen.
Mitä tietokanta maksaisi meille juuri nyt
Vaikka SDK olisi kypsä, maksaisimme silti kustannuksia, joista ei ole työkuormallemme hyötyä:
WAL-sivutiedostojen hallinta. Käynnissä oleva tietokanta luo rinnakkaiset -wal- ja -shm-tiedostot. Asiakirjan kopiointi vaatii joko checkpointin tekemisen ensin tai kaikkien kolmen tiedoston kopioimisen atomisesti. .lnote-paketin vienti Tiedostoihin tai AirDropiin vaatisi nyt viennin esivaiheen, jota käyttäjä ei näe ja jonka kehittäjä voi unohtaa.
Sovitin- eli adapterikerros. Vedot pitäisi sarjallistaa BLOB:eiksi ja purkaa takaisin. Sivun elementeillä on luonnollinen taulukkojärjestys, jonka kanvas piirtää suoraan; tietokanta toisi mukaan rivijärjestys- ja z-index-sarakkeet. Kirjoittaisimme käännöskerroksen kahden saman datan esitysmuodon väliin ja ylläpitäisimme sitä jokaisen skeemamuutoksen yhteydessä.
161 MB:n riippuvuus. Sovellukselle, jonka Debug-rakennelma on alle 2 MB, yli 80× sovelluksen kokoinen riippuvuus on huomionarvoinen kustannus — etenkin kun se on merkitty «technical preview» -versioksi ja ollut vuoden toimettomana.
Nämä kustannukset eivät ole hypoteettisia. Ne alkavat heti, kun riippuvuus linkitetään. Vastineeksi saamme ominaisuuksia — kyselyt, indeksoinnin ja samanaikaiset kirjoitukset — joita sovelluksemme ei käytä.
Toimittamamme ratkaisu: snapshot-tiedostopaketit
.lnote-asiakirja on hakemistopaketti:
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 on asiakirjan rakenteen totuuden lähde: sen tunnus, otsikko, aikaleimat ja järjestetty sivulista, jossa on kunkin sivun kanvaskoko, aikaleimat ja elementtien lukumäärä. Sivujen sisältötiedostot tallentavat elementtitaulukon samalla kvantisoidulla kokonaislukukoodauksella, jota sovellus jo käyttää — koordinaatit ja säteet 0.1 pisteen tarkkuudella, paine tuhannesosina ja aikaleimat suhteellisina millisekunteina.
Muistiinpanokirjasto lukee vain manifest.json:n ja kansikuvan pikkukuvat. Se ei koskaan jäsennä document.json:ää tai minkään sivun sisältöä. Sivun avaaminen lataa yhden .content-tiedoston. Se on ainoa tiedostonluku, joka koskee veto-dataa.
Sivut ratkaisevat kirjoituksen monistumisen
Käsinkirjoitussovelluksessa on jo sivun käsite — se on käyttäjien mielessä oleva yksikkö, ja sivulta toiselle siirrytään pyyhkäisemällä. Kun sivusta tehdään pysyvän tallennuksen yksikkö, automaattitallennus kirjoittaa uudelleen vain muuttuneet sivut.
Yksi käsinkirjoitettu sivu — esimerkiksi 1,000–2,000 vetoa — vie kvantisoidussa muodossamme noin 3–5 MB. Yksi 21 vedon tallenne, jossa on 3,400 näytettä vetoa kohti, kvantisoituu noin 55 KB:hen. Yhden sivusnapshotin kirjoittaminen flash-tallennustilaan kestää nykylaitteistolla 10–20 ms. Kun tallennusta viivästetään 0.5 sekunnin ajan, tallennukset ovat käyttäjälle näkymättömiä.
Tallennuksen kustannus skaalautuu nykyisen sivun kirjoitusmäärän, ei asiakirjan sivujen kokonaismäärän mukaan. 200-sivuinen muistikirja tallentuu täsmälleen yhtä nopeasti kuin 2-sivuinen, koska vain muuttunut sivu kirjoitetaan uudelleen.
Jokainen kirjoitus käyttää atomisia tiedosto-operaatioita — kirjoita väliaikaiseen tiedostoon ja nimeä se sitten uudelleen — joten kaatuminen kesken tallennuksen ei voi tuottaa katkaistua sivua. Taustalle siirtyminen tyhjentää kaikki muuttuneet sivut heti, kuten sovellus teki jo yhden kanvaksen aikana.
Johdonmukaisuus ilman transaktioita
Tiedostopaketeilla ei ole transaktioita, mutta niillä on selkeät omistussäännöt, jotka palvelevat samaa tarkoitusta:
Resurssit ennen viittauksia. Kun käyttäjä lisää kuvan, resurssitiedosto kirjoitetaan heti assets/-hakemistoon. Sivusnapshot, joka viittaa resurssiin sen tunnisteella, kirjoitetaan myöhemmin viivästetyn automaattitallennuksen yhteydessä. Sivu ei missään vaiheessa viittaa resurssiin, jota levyllä ei ole.
Totuuden lähde voittaa. document.json ja pages/-hakemisto ovat totuuden lähde. manifest.json on välimuisti. Jos ne ovat ristiriidassa, seuraava tallennus sovittaa välimuistin lähteen mukaiseksi. Pikkukuvat ovat johdettuja, ja ne voidaan luoda uudelleen milloin tahansa.
Orvot ennen rikkinäisiä viittauksia. Kaatumisen pahin seuraus on orpo resurssi — assets/-hakemistossa oleva tiedosto, johon yksikään sivu ei viittaa. Orvot siivotaan asiakirjan sulkemisen yhteydessä. Päinvastainen tilanne — sivu viittaisi puuttuvaan tiedostoon — ei voi syntyä, koska resurssit kirjoitetaan ennen niitä viittaavaa sivusnapshotia.
Näitä sääntöjä on helpompi ajatella kuin WAL-checkpointingia ja transaktioeristystä, ja ne vastaavat täsmälleen sovelluksen yhden prosessin ja yhden sivun käyttökuviota.
Päivityspolku on kirjoitettu auki
Tavallisten tiedostojen valitseminen nyt ei tarkoita, että valitsemme ne ikuisesti. Paketin rakenne on suunniteltu niin, että tallennusmoottorin päivittäminen muuttaa paketin sisältöä muuttamatta pakettia itseään.
Taso 1: snapshot + liittämispäiväkirja. Jos kirjoituksen monistuminen muuttuisi joskus havaittavaksi — esimerkiksi jatkuva kirjoittaminen tuhansia vetoja sisältävälle sivulle aiheuttaisi huomattavan tallennusviiveen — jokainen sivutiedosto jaettaisiin snapshotiksi ja vain liittämiseen tarkoitetuksi päiväkirjaksi. Uudet elementit lisättäisiin [length][CRC][type][payload]-kehyksinä. Päiväkirjaa toistettaessa hylätään jokainen kehys, jonka CRC ei täsmää, mikä antaa kaatumisturvan. Kun päiväkirja ylittää rajan tai sivu suljetaan, se yhdistetään takaisin snapshotiksi. Tähän tarvitaan ~200 LOC ja nolla ulkoista riippuvuutta.
Koska sivutason snapshotit poistavat jo sivujen välisen kirjoituksen monistumisen, tätä tasoa ei ehkä tarvita pitkään aikaan. 5 MB:n sivun uudelleenkirjoittaminen 0.5 sekunnin välein on hyvin flash-kirjoitusbudjetin sisällä.
Taso 2: SQLite-tietokanta. Jos sovellus tarvitsee joskus muistiinpanojen täyden tekstin haun, elementtikohtaisen synkronoinnin tai asiakirjojen välisen indeksoinnin, SQLite on oikea työkalu. Todennäköinen moottori olisi silloin GRDB, kypsä lähdekoodista käännettävä Swift-kääre, jonka binäärikoko on lähes olematon. libsql-swift:a harkittaisiin uudelleen vain, jos Turson ekosysteemistä — erityisesti Swiftin Turso Syncistä — tulisi todellinen tuotetarve.
Siirtyminen on mekaaninen: kunkin sivun elementtitaulukko vastaa strokes / images -taulua, jossa on yksi muuttumaton BLOB vetoa kohden (16 tavua näytettä kohden little-endian-binaarina). 100 vedon 0.019 sekunnin mittaus vahvistaa lähestymistavan toimivuuden. Ennen toimitusta siirtymä tarkistaisi, että vanhan paketin ja uuden tietokannan elementti- ja resurssimäärät täsmäävät ja että kaatumisen jälkeinen palautus, WAL-rajat ja taustatyhjennys läpäisevät testit.
Milloin kannattaa maksaa
Tämä päätös ei ole kannanotto tietokantoja vastaan. SQLite käsittelee samanaikaisia kirjoittajia, monimutkaisia kyselyitä ja jaetun tilan palautusta kaatumisen jälkeen — sovelluksemme ei tällä hetkellä tarvitse mitään näistä. Näistä ominaisuuksista maksaminen ennen niitä ratkaisevien ongelmien ilmestymistä on nettotappio.
Tietokannan kustannukset — riippuvuus, WAL:n hallinta, sovitinkerros ja binäärikoko — alkavat heti, kun kirjasto linkitetään. Hyödyt alkavat, kun sovelluksella on suoritettavia kyselyitä, ylläpidettäviä indeksejä tai koordinoitavia samanaikaisia kirjoittajia. Tässä vaiheessa sillä ei ole mitään näistä kolmesta.
Jokainen tallennuksen kehityksen vaihe maksaa vain ongelmista, jotka ovat jo ilmenneet. Sivutason snapshotit ratkaisevat tämänhetkisen ongelman: monisivuisten asiakirjojen tallentamisen ilman koko tiedoston uudelleenkirjoittamista. Jos kirjoituksen monistuminen muuttuu mitattavaksi, liittämispäiväkirja ratkaisee sen. Jos hausta tai synkronoinnista tulee tuotetarve, tietokanta ratkaisee sen.
Paketin rakenne, manifesti ja asiakirjan skeema eivät sido meitä mihinkään tallennusmoottoriin. Vaihtamisen kustannus on pieni, koska rajat ovat oikeissa kohdissa. Kun todella tarvitsemme tietokantaa, otamme sen käyttöön tiettyä, jo mitattua ongelmaa varten — emme hypoteettista ongelmaa varten.