Neden Henüz Veritabanı Kullanmıyoruz
iPad el yazısı uygulamamız için Turso/libSQL'ı değerlendirdik, her şeyi ölçtük ve düz anlık görüntü dosyalarını seçtik. Bu iş yükünün bir veritabanına ihtiyacı yok; her adım yalnızca zaten ortaya çıkmış sorunların bedelini ödemeli.
Lulucat Notes, iPad için bir el yazısı uygulamasıdır. Geçen haftaya kadar tek bir tuvali vardı ve ikinci bir not kavramı yoktu. Her biri birden fazla sayfa içeren birden fazla belgeye sahip bir not kitaplığı eklemek üzereydik; ilk mimari soru depolamaydı.
Veritabanı bariz cevap gibi görünüyordu. Not uygulamaları yapılandırılmış veri depolar. Yapılandırılmış veri de veritabanlarına gider. Turso ve Swift SDK’sını değerlendirdik, iOS simülatöründe çalıştırdık, gerçek çizgi verileriyle kıyasladık ve ardından kullanmamaya karar verdik.
Bunun yerine düz dosyaları seçtik. Sonuçlar ve bu kararı neden verdiğimiz aşağıda.

Fotoğraf Gabriel Cox tarafından çekildi; Unsplash üzerinde yayımlandı. Unsplash License.
Uygulama veriyle gerçekte ne yapıyor
Bir el yazısı uygulamasının dar ve öngörülebilir bir veri erişim düzeni vardır. Okumak, bir sayfayı açıp üzerindeki her öğeyi — tüm çizgileri ve tüm görselleri — aynı anda belleğe yüklemek demektir. Tuval her şeyi tutar; hiçbir zaman kısmi sorgu çalıştırmaz. Yazmak, bir kalem çizgisini tamamlayıp sayfaya bir öğe eklemek demektir. Nadir durumlarda kullanıcı bir çizginin bir bölümünü silebilir, bir seçimi taşıyabilir veya bir şeyi silebilir; bunların hepsi yine tek sayfalı, tek öğeli işlemlerdir.
Eşzamanlı erişim yoktur. Aynı anda bir kişi, bir belgenin bir sayfasına yazar. Belgeler arası arama da yoktur; not kitaplığının her belge için yalnızca bir başlığa, zaman damgasına, sayfa sayısına ve kapak küçük resmine ihtiyacı vardır, sayfa içeriğini okuması gerekmez.
Veritabanları sorgular, dizinler ve eşzamanlılık koordinasyonu için oluşturulur. Uygulamamız bu üçünün hiçbirini kullanmıyor.
Turso değerlendirmesi
Turso’nun libSQL motoru için resmi Swift SDK’sı olan libsql-swift’i değerlendirdik.
SDK çalışıyor. 9 test senaryosunun tamamı geçti. SDK’yı uygulamanın bir kopyasına entegre ettik, iOS simülatörü için derleyip çalıştırdık ve uygulama korumalı alanında yerel bir veritabanı oluşturduk. Tek bir işlemde, her biri 3,400 örnekleme noktası içeren 100 çizgi yazdık; toplam 4,080,000 bayt BLOB verisi. Geliştirme Mac’imizde bu işlem yaklaşık 0.019s sürdü.
PRAGMA wal_checkpoint(TRUNCATE) çalıştırdıktan sonra WAL dosyası sıfıra küçüldü. Yalnızca ana .db dosyasını başka bir konuma kopyalayıp açabildik ve tüm verileri geri okuyabildik. Motorun kendisi sağlam.
SDK’nın maliyetleri var. CLibsql.xcframework 161 MB yer kaplıyor. Bağladıktan sonra Debug simülatör derlememiz yaklaşık 1.9 MB’dan yaklaşık 8.2 MB’a çıktı. API eşzamanlı ve engelleyici; Swift Concurrency sarmalayıcıları yok. Açık bir close() yöntemi bulunmuyor. Transaction.commit() hata fırlatmıyor; alttaki C API void döndürüyor. Depodaki README SDK’yı technical preview olarak etiketliyor ve en son commit değerlendirmemizden yaklaşık bir yıl önce, Temmuz 2025’te yapılmış.
Turso ekosisteminde bir boşluk var. Turso artık yeni projeler için yeni Turso Database motorunu ve Turso Sync protokolünü öneriyor. Turso Sync’in TypeScript, Python, Go ve Rust için istemci SDK’ları var; Swift için yok. Eski Embedded Replica modu libsql-swift içinde bulunuyor, ancak Swift başlatıcısı tam anlamıyla yerel öncelikli bir mobil uygulamanın ihtiyaç duyduğu offline parametresini dışa açmıyor. Bugün libsql-swift’i benimsemek bize yerel bir SQLite fork’u verir, fakat Turso’yu farklı kılan senkronizasyon yeteneklerini vermez.
Bir veritabanı şu anda bize neye mal olur
SDK olgunlaşmış olsa bile iş yükümüz için hiçbir değer üretmeyen maliyetleri ödemeye devam ederdik:
WAL yan dosyalarının yönetimi. Çalışan bir veritabanı -wal ve -shm yardımcı dosyaları oluşturur. Bir belgeyi kopyalamak için önce checkpoint almak veya üç dosyanın tamamını atomik olarak kopyalamak gerekir. Bir .lnote paketini Files’a veya AirDrop’a dışa aktarmak, kullanıcının göremeyeceği ve geliştiricinin unutamayacağı bir dışa aktarma öncesi adım gerektirir.
Bir adaptör katmanı. Çizgilerin BLOB’lara serileştirilip geri serileştirilmesi gerekir. Sayfa öğelerinin doğal bir dizi sırası vardır ve tuval bunları doğrudan bu sırayla render eder; veritabanı ise satır sırası ve z-index sütunları ekler. Aynı verinin iki gösterimi arasında bir çeviri katmanı yazmamız ve her şema değişikliğinde bunu sürdürmemiz gerekir.
161 MB’lık bir bağımlılık. Debug derlemesi 2 MB’ın altında olan bir uygulama için, uygulamanın kendisinden 80× büyük bir bağımlılık fark edilmesi gereken bir maliyettir; özellikle de technical preview olarak etiketlenmiş ve bir yıldır etkinlik görmemiş bir bağımlılık söz konusuysa.
Bu maliyetler varsayımsal değil. Bağımlılık bağlandığı anda başlarlar. Karşılığında da uygulamamızın kullanmadığı sorgulama, dizinleme ve eşzamanlı yazma yeteneklerini satın alırız.
Uygulamaya aldığımız çözüm: anlık görüntü dosyası paketleri
Bir .lnote belgesi bir dizin paketidir:
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, belgenin yapısı için source of truth’tur: kimliğini, başlığını, zaman damgalarını ve sıralı sayfa listesini içerir. Her sayfanın tuval boyutunu, zaman damgalarını ve öğe sayısını da kaydeder. Sayfa içerik dosyaları, uygulamanın zaten kullandığı aynı niceleştirilmiş tamsayı kodlamasıyla öğe dizisini saklar; koordinatlar ve yarıçaplar 0.1 punto hassasiyetindedir, basınç binde birler olarak, zaman damgaları da göreli milisaniye olarak tutulur.
Not kitaplığı yalnızca manifest.json ve kapak küçük resimlerini okur. document.json veya herhangi bir sayfa içeriğini asla ayrıştırmaz. Bir sayfayı açmak yalnızca bir .content dosyasını yükler. Çizgi verisine dokunan tek dosya okuması budur.
Sayfalar yazma büyütmesini çözer
Bir el yazısı uygulamasında sayfa kavramı zaten vardır: kullanıcıların düşündüğü birim ve sayfalar arasında kaydırırken geçtikleri şey sayfadır. Sayfayı kalıcılık birimi yapmak, otomatik kaydetmenin yalnızca değişen sayfaları yeniden yazmasını sağlar.
Bir el yazısı sayfası — örneğin 1,000 ila 2,000 çizgi — niceleştirilmiş formatımızda yaklaşık 3–5 MB yer kaplar. 3,400 örnekleme noktası içeren 21 çizgilik bir kayıt niceleştirildiğinde yaklaşık 55 KB olur. Bir sayfa anlık görüntüsünü flash depolamaya yazmak modern donanımda 10–20 ms sürer. 0.5s gecikmeli tetiklemeyle kayıt işlemleri kullanıcıya görünmez.
Kayıt maliyeti belgedeki toplam sayfa sayısına değil, geçerli sayfadaki yazı miktarına göre ölçeklenir. 200 sayfalık bir defter, 2 sayfalık bir defter kadar hızlı kaydedilir; çünkü yalnızca kirli sayfa yeniden yazılır.
Her yazma işlemi atomik dosya işlemlerini kullanır: önce geçici bir dosyaya yazılır, sonra yeniden adlandırılır. Böylece kayıt sırasında oluşan bir çökme, kesilmiş bir sayfa dosyası üretemez. Uygulama arka plana geçtiğinde tüm kirli sayfaları hemen flush eder; bu, uygulamanın tek tuvalliyken sahip olduğu davranışla aynıdır.
İşlemsiz tutarlılık
Dosya paketlerinin işlemleri yoktur, ancak aynı amaca hizmet eden açık sahiplik kuralları vardır:
Başvurulardan önce kaynaklar. Kullanıcı bir görsel eklediğinde varlık dosyası hemen assets/ içine yazılır. Varlığa kimliğiyle başvuran sayfa anlık görüntüsü, gecikmeli otomatik kayıt tarafından daha sonra yazılır. Böylece bir sayfa diskte bulunmayan bir varlığa asla başvurmaz.
Source of truth kazanır. document.json ve pages/ dizini source of truth’tur. manifest.json ise bir önbellektir. İkisi uyuşmazsa bir sonraki kayıt önbelleği kaynağa uyacak şekilde düzeltir. Küçük resimler türetilmiş verilerdir ve her zaman yeniden oluşturulabilir.
Sahipsiz dosyalar, sarkan başvurulardan iyidir. Bir çökmeden sonra ortaya çıkabilecek en kötü sonuç, assets/ içinde hiçbir sayfanın başvurmadığı bir dosya olan sahipsiz bir varlıktır. Belge kapatıldığında sahipsiz varlıklar temizlenir. Bunun tersi, yani bir sayfanın eksik bir dosyaya başvurması, varlık başvuran sayfa anlık görüntüsünden önce yazıldığı için gerçekleşemez.
Bu kuralların üzerinde düşünmek WAL checkpointing ve işlem izolasyonundan daha kolaydır; ayrıca uygulamanın tek süreçli, tek sayfalı erişim düzenine tam olarak uyar.
Yükseltme yolu tanımlı
Bugün düz dosyaları seçmek, onları sonsuza kadar kullanacağımız anlamına gelmez. Paket yapısı, paketin kendisini değiştirmeden depolama motorunu yükseltebilmek için tasarlandı; yalnızca paketin içindekiler değişir.
Seviye 1: anlık görüntü + ekleme günlüğü. Yazma büyütmesi bir gün fark edilir hâle gelirse — örneğin binlerce çizgisi olan bir sayfada sürekli yazmak belirgin bir kayıt gecikmesine yol açarsa — her sayfa dosyası bir anlık görüntüsüne ve yalnızca eklemeli bir günlüğe ayrılır. Yeni öğeler [length][CRC][type][payload] çerçeveleri olarak eklenir. Günlük yeniden oynatılırken CRC’si uyuşmayan her çerçeve atılır; bu da çökme güvenliği sağlar. Günlük bir eşiği aştığında veya sayfa kapatıldığında anlık görüntüsüne geri birleştirilir. Bu, dış bağımlılık olmadan yaklaşık ~200 LOC ile yapılabilir.
Sayfa düzeyindeki anlık görüntüleri zaten sayfalar arası yazma büyütmesini ortadan kaldırdığı için bu seviye uzun süre gerekli olmayabilir. 5 MB’lık bir sayfayı her 0.5s yeniden yazmak flash yazma bütçelerinin içindedir.
Seviye 2: SQLite veritabanı. Uygulamanın notlar arasında tam metin aramaya, öğe başına senkronizasyona veya belgeler arası dizinlemeye ihtiyacı olursa SQLite doğru araç hâline gelir. O noktada olası motor, olgun ve kaynaktan derlenen bir Swift sarmalayıcısı olan GRDB olur; ikili boyutu neredeyse hiç büyütmez. libsql-swift’i yalnızca Turso ekosisteminin, özellikle Turso Sync for Swift’in, gerçek bir ürün ihtiyacı hâline gelmesi durumunda yeniden değerlendiririz.
Geçiş yolu mekaniktir: her sayfanın öğe dizisi strokes / images tablolarına eşlenir; her çizgi için değişmez bir BLOB bulunur ve her örnekleme noktası little-endian binary içinde 16 bytes kullanır. 100 çizgi için yapılan 0.019s kıyaslaması bu yaklaşımın uygulanabilir olduğunu doğrular. Yayına almadan önce geçiş işlemi, eski paket ile yeni veritabanındaki öğe ve varlık sayılarının eşleştiğini doğrulamalı; çökme kurtarma, WAL sınırları ve arka plan flush işlemleri de tüm testlerden geçmelidir.
Ne zaman ödeme yapmalı
Bu seçim, uygulamanın mevcut ihtiyaçlarına dayanıyor. SQLite paylaşılan durum üzerinde eşzamanlı yazıcıları, karmaşık sorguları ve çökme kurtarmayı yönetir; uygulamamızın şu anda bunların hiçbirine ihtiyacı yok. Bu yetenekler için, onları gerektiren sorunlar ortaya çıkmadan ödeme yapmak şu anda karşılığı olmayan bir maliyet yaratır.
Bir veritabanının maliyetleri — bağımlılık, WAL yönetimi, adaptör katmanı ve ikili boyut — kütüphane bağlandığı anda başlar. Faydaları ancak uygulamanın çalıştıracağı sorgular, sürdüreceği dizinler veya koordine edeceği eşzamanlı yazıcılar olduğunda ortaya çıkar. Şu anda bu üçünün de hiçbiri yok.
Depolama evrimimizin her adımı yalnızca daha önce ortaya çıkmış sorunların bedelini ödeyecek. Sayfa düzeyindeki anlık görüntüleri bugün karşılaştığımız sorunu çözer: çok sayfalı belgeleri tüm dosyayı yeniden yazmadan kaydetmek. Yazma büyütmesi ölçülebilir hâle gelirse ekleme günlüğü o sorunun bedelini öder. Arama veya senkronizasyon bir ürün ihtiyacı olursa veritabanı o sorunun bedelini öder.
Paket yapısı, manifest ve belge şeması herhangi bir depolama motoruna bağlı değildir. Sınırlar doğru yerde olduğu için geçiş maliyeti düşüktür. Bir veritabanına gerçekten ihtiyaç duyduğumuz gün onu varsayımsal bir sorun için değil, belirli ve önceden ölçülmüş bir sorun için benimseyeceğiz.