आम्ही अद्याप डेटाबेस वापरत का नाही
आमच्या iPad हस्ताक्षर अॅपसाठी आम्ही Turso/libSQL चे मूल्यमापन केले, प्रत्येक गोष्ट मोजली आणि फ्लॅट स्नॅपशॉट फाइल्स निवडल्या. या workload ला डेटाबेसची गरज नाही — आणि प्रत्येक टप्प्यावर आधीच अस्तित्वात असलेल्या समस्यांसाठीच किंमत द्यायला हवी.
Lulucat Notes हे iPad साठीचे हस्ताक्षर अॅप आहे. गेल्या आठवड्यापर्यंत त्यात एकच canvas होते आणि दुसऱ्या note ची संकल्पनाही नव्हती. आता आम्ही note library जोडणार होतो — अनेक documents, प्रत्येकात अनेक pages — आणि पहिला architectural प्रश्न storage चा होता.
डेटाबेस हे स्वाभाविक उत्तर वाटत होते. Notes apps structured data साठवतात. Structured data databases मध्ये जाते. आम्ही Turso आणि त्याच्या Swift SDK चे मूल्यमापन केले, ते iOS simulator वर चालवले, खऱ्या stroke data वर benchmark केले आणि मग ते न वापरण्याचा निर्णय घेतला.
त्याऐवजी आम्ही flat files निवडल्या. आम्हाला काय आढळले आणि हा निर्णय का घेतला, ते येथे आहे.

Gabriel Cox यांचा फोटो, Unsplash वर. Unsplash License.
अॅप data सोबत प्रत्यक्षात काय करते
हस्ताक्षर अॅपचा data access pattern अरुंद आणि अंदाज करता येण्यासारखा असतो. वाचणे म्हणजे एक पेज उघडून त्यातील प्रत्येक element — सर्व strokes, सर्व images — एकाच वेळी memory मध्ये load करणे. Canvas सर्व काही धरून ठेवते; ते कधीही partial query चालवत नाही. लिहिणे म्हणजे pen stroke पूर्ण करून पेजमध्ये एक element append करणे. क्वचित user stroke चा काही भाग erase करतो, selection हलवतो किंवा काहीतरी delete करतो, पण ही कामेदेखील single-page, single-element operations असतात.
Concurrent access नाही. एक व्यक्ती एका वेळी एका document च्या एका page वर लिहिते. Cross-document search नाही — note library ला प्रत्येक document साठी title, timestamp, page count आणि cover thumbnail एवढेच हवे असते; यापैकी कशासाठीही page content वाचण्याची गरज नसते.
Queries, indexes आणि concurrency coordination यासाठीच databases बनवले जातात. आमचे app या तिन्हीपैकी एकही वापरत नाही.
Turso चे मूल्यमापन
Turso च्या libSQL engine साठीचे अधिकृत Swift SDK असलेल्या libsql-swift चे आम्ही मूल्यमापन केले.
SDK काम करते. त्यातील सर्व 9 test cases पास होतात. आम्ही ते app च्या एका copy मध्ये integrate केले, iOS simulator साठी build केले, launch केले आणि app sandbox मध्ये local database तयार केला. एका transaction मध्ये, प्रत्येकी 3,400 sampling points असलेले 100 strokes लिहिले — 4,080,000 bytes BLOB data. आमच्या development Mac वर यासाठी सुमारे 0.019 seconds लागले.
PRAGMA wal_checkpoint(TRUNCATE) चालवल्यानंतर WAL file zero वर आली. मग आम्ही मुख्य .db file एकटीच दुसऱ्या location वर copy करू शकलो, ती open केली आणि सर्व data परत वाचला. Engine स्वतः sound आहे.
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” असे label केले आहे, आणि आमच्या evaluation च्या सुमारे एक वर्ष आधी, July 2025 मध्ये latest commit झाला होता.
Turso ecosystem मध्ये एक उणीव आहे. नवीन projects साठी Turso आता नवीन “Turso Database” engine आणि “Turso Sync” protocol ची शिफारस करते. Turso Sync कडे TypeScript, Python, Go आणि Rust साठी client SDKs आहेत. Swift साठी SDK नाही. जुना Embedded Replica mode libsql-swift मध्ये आहे, पण पूर्णपणे local-first mobile app साठी आवश्यक असलेला offline parameter त्याचा Swift initializer expose करत नाही. आज libsql-swift स्वीकारल्यास आपल्याला local SQLite fork मिळतो; Turso ला वेगळे बनवणाऱ्या synchronization capabilities मात्र मिळत नाहीत.
आत्ता डेटाबेसमुळे आम्हाला काय किंमत मोजावी लागेल
SDK mature असला तरी आमच्या workload साठी काहीही उपयोगी न ठरणाऱ्या costs आम्हाला तरीही द्याव्या लागतील:
WAL sidecar फाइल्सचे व्यवस्थापन. चालू असलेला database -wal आणि -shm companion files तयार करतो. एखादा document copy करण्यासाठी आधी checkpoint करावा लागतो किंवा तिन्ही files atomic पद्धतीने copy कराव्या लागतात. .lnote package Files किंवा AirDrop वर export करण्यासाठी आता user ला न दिसणारा आणि developer विसरू न शकणारा pre-export step आवश्यक होईल.
एक adapter layer. Strokes चे BLOBs मध्ये serialization करून परत deserialization करावे लागेल. Page elements ची natural array order असते, जी canvas थेट render करते; database मुळे row ordering आणि z-index columns येतील. त्याच data च्या दोन representations मधला translation layer आम्ही लिहू आणि प्रत्येक schema change वेळी तो maintain करू.
161 MB आकाराची dependency. ज्या app चा Debug build 2 MB पेक्षा कमी आहे, त्याच app पेक्षा 80× मोठी dependency ही लक्षात घेण्यासारखी cost आहे — विशेषतः ती एक वर्ष inactive असलेली “technical preview” म्हणून label केलेली असेल तर.
हे costs hypothetical नाहीत. Dependency link झाल्याच्या क्षणापासून ते सुरू होतात. आणि आमचे app वापरत नसलेल्या capabilities — querying, indexing, concurrent writes — त्या बदल्यात मिळतात.
आम्ही पाठवलेला उपाय: snapshot file packages
.lnote document हे 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 हे document structure चे source of truth आहे: त्याचा ID, title, timestamps आणि प्रत्येक page चा canvas size, timestamps व element count असलेली ordered list of pages. Page content files मध्ये app आधीपासून वापरत असलेल्या त्याच quantized integer encoding मध्ये element array साठवला जातो — coordinates आणि radii 0.1-point precision मध्ये, pressure thousandths मध्ये आणि timestamps relative milliseconds मध्ये.
Note library फक्त manifest.json आणि cover thumbnails वाचते. ती document.json किंवा कोणतेही page content कधीही parse करत नाही. Page उघडल्यावर एक .content file load होते. Stroke data ला स्पर्श करणारे हेच एकमेव file read आहे.
Pages write amplification सोडवतात
हस्ताक्षर अॅपमध्ये page ही संकल्पना आधीपासूनच आहे — user ज्या unit चा विचार करतो आणि swipe करून ज्यामध्ये बदलतो, तीच. Page ला persistence unit बनवल्यामुळे auto-save फक्त बदललेली pages rewrite करते.
हस्ताक्षराची एक page — म्हणूया 1,000 ते 2,000 strokes — आमच्या quantized format मध्ये साधारण 3 ते 5 MB व्यापते. 3,400 sampling points असलेले एक 21-stroke recording quantize केल्यावर सुमारे 55 KB होते. Modern hardware वर flash storage मध्ये एक page snapshot लिहिण्यास 10 ते 20 milliseconds लागतात. 0.5-second debounce मुळे saves user ला दिसत नाहीत.
Save cost document मधील एकूण pages च्या संख्येवर नाही, तर सध्याच्या page वर केलेल्या writing च्या प्रमाणावर scale होतो. 200-page notebook हे 2-page notebook इतक्याच वेगाने save होते, कारण फक्त dirty page rewrite केली जाते.
प्रत्येक write atomic file operations वापरते — temporary file मध्ये write करून नंतर rename करते — म्हणून save सुरू असताना crash झाला तरी truncated page तयार होऊ शकत नाही. App background मध्ये जाताना सर्व dirty pages तात्काळ flush करते; single canvas असताना app कडे असलेल्या behavior सारखेच.
Transactions शिवाय consistency
File packages मध्ये transactions नाहीत, पण त्याच उद्देशासाठी स्पष्ट ownership rules आहेत:
References आधी resources. User ने image insert केल्यावर asset file ताबडतोब assets/ मध्ये लिहिली जाते. Asset ला ID ने reference करणारी page snapshot debounced auto-save नंतर लिहिते. Page ने disk वर नसलेल्या asset ला reference केलेली स्थिती कधीही येत नाही.
मूळ स्रोतच अंतिम असतो. document.json आणि pages/ directory हे मूळ स्रोत आहेत. manifest.json हा cache आहे. त्यांच्यात फरक असल्यास पुढील save cache ला मूळ स्रोताप्रमाणे reconcile करते. Thumbnails derived असतात आणि कधीही regenerate करता येतात.
Dangling references पेक्षा orphans चांगले. Crash चा सर्वांत वाईट परिणाम म्हणजे orphan asset — assets/ मधील, कोणत्याही page ने reference न केलेली file. Document close केल्यावर orphans cleanup केले जातात. उलटे — page ने missing file reference करणे — होऊ शकत नाही, कारण reference करणाऱ्या page snapshot आधी assets लिहिल्या जातात.
हे rules WAL checkpointing आणि transaction isolation पेक्षा समजून घेणे सोपे आहेत आणि app च्या single-process, single-page access pattern शी अगदी जुळतात.
Upgrade path लिहून ठेवला आहे
आत्ता flat files निवडणे म्हणजे कायम flat files निवडणे नाही. Package structure अशी बनवली आहे की storage engine upgrade केल्यावर package न बदलता त्याच्या आतली सामग्री बदलता येईल.
Level 1: snapshot + append journal. Write amplification कधी perceptible झाली — उदाहरणार्थ, हजारो strokes असलेल्या page वर सतत लिहिल्याने save delay जाणवू लागला — तर प्रत्येक page file snapshot आणि append-only journal मध्ये विभागली जाईल. New elements [length][CRC][type][payload] frames म्हणून append केले जातील. Journal replay करताना CRC match न होणारा प्रत्येक frame discard केला जाईल, ज्यामुळे crash safety मिळेल. Journal threshold पेक्षा मोठी झाली किंवा page close झाली की ती snapshot मध्ये merge होईल. External dependencies zero असलेला हा साधारण 200 lines of code चा उपाय आहे.
Page-level snapshots मुळे cross-page write amplification आधीच नाहीशी होते, म्हणून हा level बराच काळ लागणार नाही. 5 MB page प्रत्येक 0.5 seconds ने rewrite करणे flash write budgets मध्ये सहज बसते.
Level 2: SQLite database. App ला कधीतरी notes across full-text search, per-element synchronization किंवा cross-document indexing आवश्यक झाले, तर SQLite हे योग्य tool ठरेल. त्या वेळी बहुधा GRDB engine असेल — mature, source-compiled Swift wrapper आणि जवळजवळ zero binary size overhead असलेला. Turso ecosystem — विशेषतः Swift साठी Turso Sync — खऱ्या product need मध्ये बदलल्यासच libsql-swift पुन्हा विचारात घेतले जाईल.
Migration path यांत्रिक आहे: प्रत्येक page चा element array strokes / images table मध्ये map केला जाईल आणि प्रत्येक stroke साठी एक immutable BLOB असेल (little-endian binary मध्ये प्रत्येक sampling point साठी 16 bytes). 100 strokes साठीचा 0.019-second benchmark हा approach viable असल्याचे निश्चित करतो. Ship करण्यापूर्वी migration जुन्या package आणि नव्या database मधील element counts व asset counts जुळतात का, तसेच crash recovery, WAL bounds आणि background flush हे सर्व pass होतात का, ते verify करेल.
कधी किंमत द्यायची
हा निर्णय databases बद्दलचा न्यायनिवाडा नाही. SQLite shared state वर concurrent writers, complex queries आणि crash recovery हाताळते — यापैकी कशाचीही आमच्या app ला सध्या गरज नाही. App ला अजून आलेल्याच नाहीत अशा problems सोडवणाऱ्या capabilities साठी आधीच पैसे देणे म्हणजे net loss.
Database चे costs — dependency, WAL management, adapter layer, binary size — library link केल्याक्षणी सुरू होतात. App कडे चालवायच्या queries, maintain करायचे indexes किंवा coordinate करायचे concurrent writers आले की benefits सुरू होतात. या टप्प्यावर आमच्या app कडे या तिन्हीपैकी एकही नाही.
Storage evolution मधील प्रत्येक पाऊल आधीच दिसून आलेल्या problems साठीच किंमत देईल. Page-level snapshots आजच्या समस्येसाठी पैसे देतात: संपूर्ण file rewrite न करता multi-page documents save करणे. Write amplification measurable झाली तर append journal त्या समस्येसाठी पैसे देईल. Search किंवा sync product need झाली तर database त्या समस्येसाठी पैसे देईल.
Package structure, manifest आणि document schema कोणत्याही storage engineशी बांधलेले नाहीत. Boundaries योग्य ठिकाणी असल्यामुळे switching costs कमी आहेत. खरोखर databaseची गरज भासेल तो दिवस आला, तेव्हा hypothetical problemसाठी नव्हे तर आधीच मोजलेल्या specific problemसाठी तो स्वीकारू.