நாங்கள் இன்னும் database-ஐ ஏன் பயன்படுத்தவில்லை
எங்கள் iPad கைஎழுத்து app-க்காக Turso/libSQL-ஐ மதிப்பிட்டு, அனைத்தையும் அளந்து, flat snapshot files-ஐத் தேர்ந்தெடுத்தோம். இந்த workload-க்கு database தேவையில்லை; ஒவ்வொரு படியும் ஏற்கனவே தோன்றிய பிரச்சினைகளுக்காக மட்டுமே செலவிட வேண்டும்.
Lulucat Notes என்பது iPad-க்கான கைஎழுத்து app. கடந்த வாரம் வரை அதில் ஒரு canvas மட்டுமே இருந்தது; இரண்டாவது note என்ற concept-உம் இல்லை. பல documents-ஐ, ஒவ்வொன்றிலும் பல pages-ஐ கொண்ட note library-ஐச் சேர்க்க இருந்தோம்; முதல் கட்டமைப்புக் கேள்வி சேமிப்பைப் பற்றியது.
Database தான் வெளிப்படையான பதிலாகத் தோன்றியது. Notes apps structured data-ஐ சேமிக்கின்றன. Structured data database-ல் செல்கிறது. Turso மற்றும் அதன் Swift SDK-ஐ மதிப்பிட்டோம், iOS simulator-ல் அதை இயக்கினோம், உண்மையான stroke data-ஐ benchmark செய்தோம், பின்னர் அதை பயன்படுத்த வேண்டாம் என்று முடிவு செய்தோம்.
அதற்குப் பதிலாக flat files-ஐத் தேர்ந்தெடுத்தோம். அதற்கான காரணங்கள் பின்வருமாறு.

Gabriel Cox எடுத்த படம், Unsplash-ல் வெளியிடப்பட்டது. Unsplash License.
App தரவை எப்படிப் பயன்படுத்துகிறது
ஒரு handwriting app-ன் data access pattern குறுகியதும் முன்கூட்டியே கணிக்கக்கூடியதுமாகும். Reading என்பது ஒரு page-ஐத் திறந்து, அதிலுள்ள ஒவ்வொரு element-ஐயும் — எல்லா strokes-ஐயும், எல்லா images-ஐயும் — ஒரே நேரத்தில் memory-க்குள் ஏற்றுவது. Canvas அனைத்தையும் வைத்திருக்கும்; அது ஒருபோதும் partial query இயக்காது. Writing என்பது ஒரு pen stroke-ஐ முடித்து, page-க்கு ஒரு element-ஐ append செய்வது. அரிதாக user ஒரு stroke-ன் பகுதியை அழிக்கலாம், 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-க்கான official Swift SDK-ஆன libsql-swift-ஐ மதிப்பிட்டோம்.
SDK வேலை செய்கிறது. அதன் 9 test cases-உம் pass ஆனது. அதை 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.019s ஆனது.
PRAGMA wal_checkpoint(TRUNCATE)-ஐ இயக்கிய பிறகு, WAL file zero-ஆகச் சுருங்கியது. Main .db file-ஐ மட்டும் வேறு இடத்துக்கு copy செய்து, அதைத் திறந்து, எல்லா data-ஐயும் மீண்டும் படிக்க முடிந்தது. Engine தானே நம்பகமாக உள்ளது.
SDK-க்கு செலவுகள் உள்ளன. CLibsql.xcframework 161 MB அளவு கொண்டது. Link செய்த பிறகு, எங்கள் Debug simulator build சுமார் 1.9 MB-இலிருந்து சுமார் 8.2 MB-ஆக உயர்ந்தது. API synchronous மற்றும் blocking-ஆக உள்ளது; Swift Concurrency wrappers இல்லை. வெளிப்படையான close() method இல்லை. Transaction.commit() throw செய்யாது — underlying C API void-ஐ return செய்கிறது. Repository-ன் README இந்த SDK-ஐ technical preview என்று குறிப்பிடுகிறது; evaluation-க்கு சுமார் ஒரு வருடம் முன்பு, ஜூலை 2025-ல் தான் சமீபத்திய commit இருந்தது.
Turso ecosystem-ல் ஒரு இடைவெளி உள்ளது. புதிய projects-க்கு புதிய Turso Database engine மற்றும் Turso Sync protocol-ஐப் பயன்படுத்துமாறு Turso இப்போது பரிந்துரைக்கிறது. Turso Sync-க்கு TypeScript, Python, Go மற்றும் Rust-க்கான client SDKs உள்ளன; Swift-க்கானது இல்லை. பழைய Embedded Replica mode libsql-swift-ல் உள்ளது, ஆனால் fully local-first mobile app-க்கு தேவையான offline parameter-ஐ அதன் Swift initializer வெளிப்படுத்தவில்லை. இன்று libsql-swift-ஐ ஏற்றுக்கொண்டால், local SQLite fork கிடைக்கும்; Turso-வை வேறுபடுத்தும் synchronization capabilities கிடைக்காது.
இப்போது database எங்களுக்கு என்ன செலவாகும்
SDK mature-ஆக இருந்தாலும், எங்கள் workload-க்கு எதையும் வாங்கித் தராத செலவுகளைச் செலுத்த வேண்டியிருக்கும்:
WAL sidecar management. இயங்கும் database -wal மற்றும் -shm companion files-ஐ உருவாக்கும். ஒரு document-ஐ copy செய்ய, முதலில் checkpoint செய்ய வேண்டும் அல்லது மூன்று files-ஐயும் atomically copy செய்ய வேண்டும். ஒரு .lnote package-ஐ Files அல்லது AirDrop-க்கு export செய்வது, user பார்க்க முடியாததும் developer மறக்கக் கூடாததுமான pre-export step-ஐ இப்போது தேவைப்படுத்தும்.
ஒரு adapter layer. Strokes-ஐ BLOBs-ஆக serialize செய்து, மீண்டும் deserialize செய்ய வேண்டும். Page elements-க்கு இயல்பான array order உள்ளது; canvas அதை நேரடியாக render செய்கிறது. Database row ordering மற்றும் z-index columns-ஐ அறிமுகப்படுத்தும். ஒரே data-வின் இரண்டு representations-க்கு நடுவே ஒரு translation layer-ஐ எழுத வேண்டும்; ஒவ்வொரு schema change-க்கும் அதை maintain செய்ய வேண்டும்.
161 MB dependency. Debug build 2 MB-க்கும் குறைவாக இருக்கும் app-க்கு, app-ஐவிட 80×-க்கும் பெரிய dependency கவனிக்க வேண்டிய cost-தான் — குறிப்பாக அது technical preview என்று label செய்யப்பட்டு, ஒரு வருடமாக activity இல்லாதபோது.
இந்தச் செலவுகள் கற்பனையானவை அல்ல. Dependency link செய்யப்படும் தருணத்திலேயே அவை தொடங்குகின்றன. அதற்குப் பதிலாக கிடைக்கும் capabilities — querying, indexing, concurrent writes — எங்கள் app பயன்படுத்தாதவை.
நாம் வெளியிட்ட தீர்வு: 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, மேலும் ஒழுங்குபடுத்தப்பட்ட pages list ஆகியவை அதில் உள்ளன. ஒவ்வொரு page-ன் canvas size, timestamps மற்றும் element count-உம் அதில் பதிவாகும். 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-ஐத் தீர்க்கின்றன
ஒரு handwriting app-ல் page என்ற concept ஏற்கனவே உள்ளது — user நினைப்பது அதையே; pages-க்கிடையே swipe செய்வதும் அதையே. Page-ஐ persistence-ன் unit-ஆக மாற்றினால், auto-save மாறிய pages-ஐ மட்டும் rewrite செய்யும்.
ஒரு handwriting page — எடுத்துக்காட்டாக, 1,000 முதல் 2,000 strokes — எங்கள் quantized format-ல் சுமார் 3–5 MB ஆகும். 3,400 sampling points கொண்ட 21-stroke recording ஒன்று quantize செய்யப்பட்ட பிறகு சுமார் 55 KB ஆகும். ஒரு page snapshot-ஐ flash storage-ல் எழுத modern hardware-ல் 10–20 ms ஆகும். 0.5s debounce-உடன் saves user-க்குத் தெரியாது.
Save cost, document-ல் உள்ள மொத்த pages-ஐப் பொறுத்தது அல்ல; current page-ல் உள்ள writing அளவைப் பொறுத்தது. 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-ஆக இருந்தபோது இருந்த behavior-ஐயே இது பின்பற்றுகிறது.
Transactions இல்லாமல் consistency
File packages-க்கு transactions இல்லை; ஆனால் அதே நோக்கத்தை நிறைவேற்றும் தெளிவான ownership rules உள்ளன:
References-க்கு முன் resources. User ஒரு image-ஐ insert செய்யும்போது, asset file உடனே assets/-ல் எழுதப்படுகிறது. அந்த asset-ஐ ID மூலம் reference செய்யும் page snapshot, debounced auto-save மூலம் பின்னர் எழுதப்படுகிறது. Disk-ல் இல்லாத asset-ஐ ஒரு page reference செய்யும் நிலை ஏற்படாது.
Source of truth wins. document.json மற்றும் pages/ directory source of truth; manifest.json ஒரு cache. அவை வேறுபட்டால், அடுத்த save cache-ஐ source-க்கு match ஆக reconcile செய்யும். Thumbnails derived data; அவற்றை எப்போது வேண்டுமானாலும் regenerate செய்யலாம்.
Dangling references-க்கு பதில் orphans. Crash-ன் worst outcome ஒரு orphan asset — assets/-ல் எந்த page-உம் reference செய்யாத file. Document close ஆகும்போது orphans clean up செய்யப்படும். இதற்கு மாறாக, missing file-ஐ page reference செய்ய முடியாது, ஏனெனில் reference செய்யும் page snapshot-க்கு முன்பே asset எழுதப்படுகிறது.
இந்த rules WAL checkpointing மற்றும் transaction isolation-ஐவிட புரிந்துகொள்ள எளிதானவை; மேலும் app-ன் single-process, single-page access pattern-க்கு சரியாகப் பொருந்துகின்றன.
Upgrade path தெளிவாக உள்ளது
இப்போது flat files-ஐத் தேர்ந்தெடுப்பது நிரந்தர முடிவு அல்ல. Package structure அப்படியாக வடிவமைக்கப்பட்டுள்ளது: package-ஐ மாற்றாமல், அதன் உள்ளே இருப்பதை மட்டும் மாற்றி storage engine-ஐ upgrade செய்யலாம்.
Level 1: snapshot + append journal. Write amplification ஒருநாள் உணரக்கூடியதாக மாறினால் — உதாரணமாக, ஆயிரக்கணக்கான strokes கொண்ட ஒரு page-ல் தொடர்ந்து எழுதும்போது save delay தெளிவாகத் தெரிந்தால் — ஒவ்வொரு page file-உம் snapshot மற்றும் append-only journal-ஆகப் பிரியும். புதிய elements [length][CRC][type][payload] frames-ஆக append செய்யப்படும். Journal-ஐ replay செய்யும்போது CRC match ஆகாத frame discard செய்யப்படும்; இதனால் crash safety கிடைக்கும். Journal ஒரு threshold-ஐத் தாண்டும்போது அல்லது page close ஆகும்போது, அது மீண்டும் snapshot-ல் merge செய்யப்படும். இது வெளிப்புற dependencies எதுவுமின்றி சுமார் ~200 LOC ஆகும்.
Page-level snapshots ஏற்கனவே cross-page write amplification-ஐ நீக்குவதால், இந்த level நீண்ட காலம் தேவையில்லாமல் இருக்கலாம். 5 MB page-ஐ ஒவ்வொரு 0.5s-க்கும் rewrite செய்வது flash write budgets-க்குள் உள்ளது.
Level 2: SQLite database. App-க்கு notes முழுவதும் full-text search, per-element synchronization அல்லது cross-document indexing தேவைப்பட்டால், SQLite சரியான tool ஆகும். அப்போது GRDB தான் சாத்தியமான engine; இது mature, source-compiled Swift wrapper, binary size overhead கிட்டத்தட்ட zero. Turso ecosystem-ல், குறிப்பாக Turso Sync for Swift, உண்மையான product need ஆக மாறினால் மட்டுமே libsql-swift-ஐ மீண்டும் பரிசீலிப்போம்.
Migration path நேரடியானது: ஒவ்வொரு page-ன் element array-உம் strokes / images table-க்கு map ஆகும்; ஒவ்வொரு stroke-க்கும் immutable BLOB ஒன்று, ஒவ்வொரு sampling point-க்கும் little-endian binary-ல் 16 bytes. 100 strokes-க்கான 0.019s benchmark இந்த அணுகுமுறை viable என்பதை உறுதிப்படுத்துகிறது. Release-க்கு முன் migration, பழைய package மற்றும் புதிய database-ல் element counts மற்றும் asset counts match ஆகின்றனவா என்பதைச் சரிபார்க்கும்; crash recovery, WAL bounds மற்றும் background flush அனைத்தும் pass ஆகின்றனவா என்பதையும் சோதிக்கும்.
எப்போது செலவு செய்ய வேண்டும்
இந்தத் தேர்வு app-ன் தற்போதைய தேவைகளுக்கு ஏற்ப செய்யப்பட்டது. SQLite shared state-ல் concurrent writers, complex queries மற்றும் crash recovery-ஐ handle செய்யும்; எங்கள் app-க்கு இவற்றில் எதுவும் இப்போது தேவையில்லை. இந்த capabilities-க்காகச் செலவிடுவது, அவற்றைத் தேவைப்படுத்தும் பிரச்சினைகள் தோன்றும் முன், இப்போது ஈடில்லாத செலவாகும்.
Database-ன் costs — dependency, WAL management, adapter layer, binary size — library link செய்யப்படும் தருணத்திலேயே தொடங்குகின்றன. App-ல் run செய்ய queries, maintain செய்ய indexes அல்லது coordinate செய்ய concurrent writers இருக்கும்போதுதான் benefits தொடங்கும். இப்போது இந்த மூன்றிலும் எதுவும் இல்லை.
எங்கள் storage evolution-ன் ஒவ்வொரு படியும் ஏற்கனவே தோன்றிய பிரச்சினைகளுக்காக மட்டுமே செலவிடும். Page-level snapshots இன்றைய பிரச்சினைக்குச் செலுத்துகின்றன: multi-page documents-ஐ முழு file-ஐ rewrite செய்யாமல் save செய்வது. Write amplification அளவிடக்கூடியதாக மாறினால், append journal அந்தப் பிரச்சினைக்குச் செலுத்தும். Search அல்லது sync product need ஆக மாறினால், database அந்தப் பிரச்சினைக்குச் செலுத்தும்.
Package structure, manifest மற்றும் document schema எந்த storage engine-க்கும் கட்டுப்பட்டவை அல்ல. Boundaries சரியான இடத்தில் இருப்பதால் switching cost குறைவு. Database உண்மையில் தேவைப்படும் நாளில், கற்பனையான பிரச்சினைக்காக அல்ல; ஏற்கனவே அளந்து நிரூபிக்கப்பட்ட ஒரு குறிப்பிட்ட பிரச்சினைக்காக அதை ஏற்றுக்கொள்வோம்.