Lulucat

અમે હજી ડેટાબેઝ કેમ વાપરતા નથી

Gaoge ZhangGaoge Zhang

iPad માટેની અમારી હસ્તલેખન ઍપમાં Turso/libSQLનું મૂલ્યાંકન કર્યું, બધું માપ્યું અને ફ્લેટ ફાઇલો પસંદ કરી. આ વર્કલોડને ડેટાબેઝની જરૂર નથી — અને દરેક પગલાએ પહેલેથી ઊભી થયેલી સમસ્યાઓ માટે જ કિંમત ચૂકવવી જોઈએ.

Lulucat Notes iPad માટેની હસ્તલેખન ઍપ છે. ગયા અઠવાડિયા સુધી તેમાં એક જ કૅનવાસ હતો અને બીજી નોટનો કોઈ ખ્યાલ નહોતો. અમે નોટ લાઇબ્રેરી ઉમેરવાના હતા — અનેક ડૉક્યુમેન્ટ્સ, જેમાં દરેકમાં અનેક પેજ — અને આર્કિટેક્ચરનો પહેલો પ્રશ્ન સ્ટોરેજનો હતો.

ડેટાબેઝ સ્પષ્ટ જવાબ લાગતો હતો. નોટ્સ ઍપ્સ સ્ટ્રક્ચર્ડ ડેટા સ્ટોર કરે છે. સ્ટ્રક્ચર્ડ ડેટા ડેટાબેઝમાં જાય છે. અમે Turso અને તેનું Swift SDK તપાસ્યું, તેને iOS સિમ્યુલેટર પર ચલાવ્યું, વાસ્તવિક સ્ટ્રોક ડેટાનો બેન્ચમાર્ક કર્યો અને પછી તેનો ઉપયોગ ન કરવાનો નિર્ણય લીધો.

અમે તેના બદલે ફ્લેટ ફાઇલો પસંદ કરી. અમને શું મળ્યું અને આ નિર્ણય શા માટે લીધો તે અહીં છે.

લાકડાના ડેસ્ક પર હસ્તલિખિત નોટ્સ અને પેન ધરાવતી ખુલ્લી નોટબુક.

Gabriel Cox દ્વારા Unsplash પરનો ફોટો. Unsplash લાઇસન્સ.

ઍપ ખરેખર ડેટા સાથે શું કરે છે

હસ્તલેખન ઍપનો ડેટા ઍક્સેસ પેટર્ન મર્યાદિત અને અનુમાન કરી શકાય એવો હોય છે. વાંચવાનો અર્થ છે એક પેજ ખોલવું અને તેના દરેક એલિમેન્ટને એકસાથે મેમરીમાં લોડ કરવો — બધા સ્ટ્રોક્સ અને બધી ઇમેજિસ. કૅનવાસ બધું ધરાવે છે; તે ક્યારેય partial query ચલાવતો નથી. લખવાનો અર્થ છે પેન સ્ટ્રોક પૂરો કરવો અને પેજમાં એક એલિમેન્ટ ઉમેરવું. બહુ ઓછા કિસ્સામાં યુઝર સ્ટ્રોકનો કોઈ ભાગ ભૂંસી નાખે, સિલેક્શન ખસેડે અથવા કંઈક ડિલીટ કરે, પરંતુ એ પણ single-page, single-element operations જ છે.

કોઈ concurrent access નથી. એક સમયે એક વ્યક્તિ એક ડૉક્યુમેન્ટના એક પેજ પર લખે છે. ડૉક્યુમેન્ટ્સ વચ્ચે search નથી — નોટ લાઇબ્રેરીને દરેક ડૉક્યુમેન્ટ માટે માત્ર title, timestamp, page count અને cover thumbnail જોઈએ છે; આમાંથી કંઈ માટે page content વાંચવાની જરૂર નથી.

ક્વેરીઝ, ઇન્ડેક્સ અને concurrency coordination માટે ડેટાબેઝ બનાવવામાં આવે છે. અમારી ઍપ આ ત્રણમાંથી એકેય વાપરતી નથી.

Tursoનું મૂલ્યાંકન

અમે libsql-swiftનું મૂલ્યાંકન કર્યું, જે Tursoના libSQL engine માટેનું સત્તાવાર Swift SDK છે.

SDK કામ કરે છે. તેના બધા નવ test cases પાસ થાય છે. અમે તેને ઍપની એક કૉપીમાં ઇન્ટિગ્રેટ કર્યું, iOS સિમ્યુલેટર માટે build કર્યું, તેને લૉન્ચ કર્યું અને ઍપના sandboxમાં local database બનાવ્યો. અમે એક જ transactionમાં 3,400 sampling points ધરાવતા 100 strokes લખ્યા — BLOB dataના 4,080,000 bytes. અમારા development Mac પર તેમાં લગભગ 0.019 seconds લાગ્યા.

PRAGMA wal_checkpoint(TRUNCATE) ચલાવ્યા પછી WAL ફાઇલ ઘટીને શૂન્ય થઈ ગઈ અને અમે માત્ર મુખ્ય .db ફાઇલને બીજી જગ્યાએ કૉપી કરી શક્યા, તેને ખોલી શક્યા અને બધો ડેટા પાછો વાંચી શક્યા. Engine પોતે મજબૂત છે.

SDKના ખર્ચ છે. CLibsql.xcframeworkનું વજન 161 MB છે. Link કર્યા પછી અમારું Debug simulator build લગભગ 1.9 MBથી વધીને લગભગ 8.2 MB થયું. API synchronous અને blocking છે, અને તેમાં Swift Concurrency wrappers નથી. કોઈ explicit close() method નથી. Transaction.commit() throw કરતું નથી — underlying C API void return કરે છે. Repositoryનું README SDKને «technical preview» કહે છે, અને અમારું મૂલ્યાંકન થયું તેના લગભગ એક વર્ષ પહેલાં, July 2025માં, સૌથી તાજેતરનો commit થયો હતો.

Turso ecosystemમાં એક ખાલી જગ્યા છે. નવા પ્રોજેક્ટ્સ માટે Turso હવે તેનું નવું «Turso Database» engine અને «Turso Sync» protocol ભલામણ કરે છે. Turso Sync પાસે TypeScript, Python, Go અને Rust માટે client SDKs છે. Swift માટે નથી. જૂનું Embedded Replica mode libsql-swiftમાં છે, પરંતુ તેનું Swift initializer સંપૂર્ણ local-first mobile app માટે જરૂરી offline parameter જાહેર કરતું નથી. આજે libsql-swift અપનાવીએ તો local SQLite fork મળે છે, પરંતુ Tursoને અલગ બનાવતી synchronization capabilities મળતી નથી.

અત્યારે ડેટાબેઝ અમને કેટલો ખર્ચ કરાવશે

SDK mature હોય તો પણ અમારા વર્કલોડને કોઈ લાભ ન આપતા ખર્ચ આપણે ભોગવવાના રહે:

WAL sidecar management. ચાલતો ડેટાબેઝ -wal અને -shm companion files બનાવે છે. ડૉક્યુમેન્ટ કૉપી કરવા માટે પહેલાં checkpoint કરવો પડે અથવા ત્રણેય ફાઇલો atomic રીતે કૉપી કરવી પડે. .lnote packageને Files અથવા AirDropમાં export કરવા માટે હવે export પહેલાંનું એક પગલું જોઈએ, જે યુઝરને દેખાતું નથી અને ડેવલપર ભૂલી શકતો નથી.

એક adapter layer. સ્ટ્રોક્સને BLOBમાં serialize કરીને પાછા deserialize કરવા પડશે. પેજના એલિમેન્ટ્સમાં array order સ્વાભાવિક હોય છે, જેને કૅનવાસ સીધું render કરે છે; ડેટાબેઝ row ordering અને z-index columns ઉમેરશે. એક જ ડેટાની બે representations વચ્ચે translation layer લખવી પડશે અને દરેક schema change સાથે તેને જાળવવી પડશે.

161 MB dependency. જે ઍપનું Debug build 2 MBથી નાનું છે, તેના કરતાં 80×થી મોટી dependency નોંધપાત્ર કિંમત છે — ખાસ કરીને જ્યારે તેને «technical preview» કહેવાય અને એક વર્ષથી કોઈ activity ન થઈ હોય.

આ ખર્ચો કલ્પિત નથી. Dependency link થાય તે ક્ષણથી તે શરૂ થાય છે. અને બદલામાં આપણને querying, indexing અને concurrent writes જેવી capabilities મળે છે, જે અમારી ઍપ વાપરતી નથી.

અમે ship કરેલો ઉકેલ: snapshot file packages

.lnote ડૉક્યુમેન્ટ એક directory package છે:

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 ડૉક્યુમેન્ટના structure માટે source of truth છે: તેનું ID, title, timestamps અને દરેક પેજનું canvas size, timestamps અને element count ધરાવતી ordered page list. Page content files એ જ quantized integer encodingમાં element array સ્ટોર કરે છે જે ઍપ પહેલેથી વાપરે છે — coordinates અને radii 0.1-point precision પર, pressure thousandthsમાં અને timestamps relative millisecondsમાં.

નોટ લાઇબ્રેરી માત્ર manifest.json અને cover thumbnails વાંચે છે. તે document.json અથવા કોઈ પણ પેજનું content ક્યારેય parse કરતી નથી. પેજ ખોલવાથી એક .content ફાઇલ લોડ થાય છે. Stroke dataને સ્પર્શતું આ એકમાત્ર file read છે.

પેજ write amplificationનો ઉકેલ આપે છે

હસ્તલેખન ઍપમાં પેજનો ખ્યાલ પહેલેથી છે — આ એ unit છે જેના વિશે યુઝર વિચારે છે અને એક પેજથી બીજા પેજ પર swipe કરે છે. પેજને persistence unit બનાવવાથી auto-save માત્ર બદલાયેલા પેજ ફરી લખે છે.

હસ્તલેખનનું એક પેજ — કહો કે 1,000 થી 2,000 strokes — અમારા quantized formatમાં આશરે 3–5 MB લે છે. 3,400 sampling points ધરાવતી 21-stroke recording quantize થયા પછી લગભગ 55 KB થાય છે. Modern hardware પર એક page snapshot flash storageમાં લખવામાં 10–20 ms લાગે છે. 0.5-second debounce સાથે saves યુઝરને દેખાતા નથી.

Save cost હાલના પેજ પરના writing amount સાથે વધે છે, ડૉક્યુમેન્ટમાં કુલ પેજની સંખ્યા સાથે નહીં. 200-page notebook, 2-page notebook જેટલી જ ઝડપથી save થાય છે, કારણ કે માત્ર dirty page ફરી લખાય છે.

દરેક write atomic file operations વાપરે છે — temporary fileમાં લખો, પછી rename કરો — તેથી save દરમિયાન crash થાય તો truncated page બની શકતું નથી. Backgroundમાં પ્રવેશતાં બધા dirty pages તરત flush થાય છે, જેમ ઍપ single canvas વખતે પહેલેથી કરતી હતી.

Transactions વિના consistency

File packagesમાં transactions નથી, પરંતુ સ્પષ્ટ ownership rules છે જે એ જ કામ કરે છે:

References પહેલાં resources. યુઝર image insert કરે ત્યારે asset file તરત assets/માં લખાય છે. Assetને IDથી reference કરતું page snapshot debounced auto-save દ્વારા પછી લખાય છે. કોઈ પણ સમયે પેજ disk પર ન હોય એવા assetને reference કરતું નથી.

Source of truth જીતે છે. document.json અને pages/ directory source of truth છે. manifest.json cache છે. જો બંનેમાં મતભેદ હોય, તો આગામી save cacheને source સાથે મેળ ખવડાવે છે. Thumbnails derived છે અને કોઈ પણ સમયે ફરી બનાવી શકાય છે.

Dangling references કરતાં orphans સારાં. Crashનું સૌથી ખરાબ પરિણામ orphan asset છે — assets/માં એવી file જેને કોઈ page reference કરતું નથી. Document બંધ થાય ત્યારે orphans સાફ થાય છે. વિપરીત સ્થિતિ — પેજ missing fileને reference કરે — થઈ શકતી નથી, કારણ કે reference કરતું page snapshot લખાય તે પહેલાં assets લખાઈ જાય છે.

આ rulesને WAL checkpointing અને transaction isolation કરતાં સમજવા સરળ છે, અને તે ઍપના single-process, single-page access pattern સાથે બરાબર મેળ ખાય છે.

Upgrade path લખાયેલો છે

હમણાં flat files પસંદ કરવાનો અર્થ એ નથી કે તે હંમેશાં માટે પસંદ કર્યા. Package structure એવી રીતે બનાવવામાં આવ્યું છે કે storage engine upgrade કરવાથી package બદલાયા વિના તેની અંદરની વસ્તુ બદલાય.

Level 1: snapshot + append journal. જો write amplification ક્યારેક ધ્યાનમાં આવે — માનો કે હજારો strokes ધરાવતા પેજ પર સતત લખવાથી save delay દેખાય — તો દરેક page file snapshot અને append-only journalમાં વહેંચાશે. નવા elements [length][CRC][type][payload] frames તરીકે append થશે. Journal replay કરતી વખતે જે frameનો CRC match ન થાય તે discard થાય છે, જેથી crash safety મળે છે. Journal threshold વટાવે અથવા page બંધ થાય ત્યારે તે snapshotમાં પાછું merge થાય છે. આમાં ~200 LOC અને zero external dependencies છે.

Page-level snapshots પહેલેથી cross-page write amplification દૂર કરે છે, તેથી આ level લાંબા સમય સુધી જરૂરી ન પણ બને. દર 0.5 secondsે 5 MB page rewrite કરવું flash write budgetsની અંદર છે.

Level 2: SQLite database. જો ઍપને ક્યારેક notesમાં full-text search, per-element synchronization અથવા cross-document indexing જોઈએ, તો SQLite યોગ્ય tool બને છે. તે સમયે સંભવિત engine GRDB હશે, જે mature, source-compiled Swift wrapper છે અને binary size overhead લગભગ શૂન્ય છે. Turso ecosystem — ખાસ કરીને Swift માટેનું Turso Sync — વાસ્તવિક product need બને ત્યારે જ libsql-swift પર ફરી વિચારાશે.

Migration mechanical છે: દરેક પેજનું element array એક strokes / images tableમાં map થાય છે, જેમાં દરેક stroke માટે એક immutable BLOB હોય છે (little-endian binaryમાં દરેક sampling point માટે 16 bytes). 100 strokes માટેનો 0.019-second benchmark આ approach શક્ય છે તેની પુષ્ટિ કરે છે. Ship કરતાં પહેલાં migration ચકાસશે કે જૂના package અને નવા database વચ્ચે element counts અને asset counts સરખાં છે, અને crash recovery, WAL bounds તથા background flush બધાં પાસ થાય છે.

ક્યારે કિંમત ચૂકવવી

આ નિર્ણય ડેટાબેઝ સામેનો અભિપ્રાય નથી. SQLite shared state પર concurrent writers, complex queries અને crash recovery સંભાળે છે — અમારી ઍપને અત્યારે આમાંથી કંઈની જરૂર નથી. ઍપ પાસે જે સમસ્યાઓ છે જ નહીં તે ઉકેલતી capabilities માટે પહેલાંથી ચૂકવણી કરવી net loss છે.

ડેટાબેઝના ખર્ચ — dependency, WAL management, adapter layer અને binary size — library link થાય તે ક્ષણથી શરૂ થાય છે. લાભો ત્યારે શરૂ થાય છે જ્યારે ઍપ પાસે ચલાવવાની queries, જાળવવાના indexes અથવા coordinate કરવાના concurrent writers હોય. આ તબક્કે તે ત્રણમાંથી એકેય ધરાવતી નથી.

Storage evolutionનું દરેક પગલું પહેલેથી દેખાયેલી સમસ્યાઓ માટે જ કિંમત ચૂકવશે. Page-level snapshots આજની સમસ્યા ઉકેલે છે: આખી file ફરી લખ્યા વિના multi-page documents save કરવા. Write amplification માપી શકાય તેવી બને તો append journal તે સમસ્યા માટે કિંમત ચૂકવશે. Search અથવા sync product need બને તો database તે સમસ્યા માટે કિંમત ચૂકવશે.

Package structure, manifest અને document schema કોઈ storage engine સાથે બંધાયેલા નથી. Switching cost ઓછી છે કારણ કે boundaries યોગ્ય જગ્યાએ છે. જે દિવસે આપણને ખરેખર ડેટાબેઝની જરૂર પડશે, તે દિવસે અમે તેને ચોક્કસ, પહેલેથી માપેલી સમસ્યા માટે અપનાવીશું — કલ્પિત સમસ્યા માટે નહીં.