Node.js, SQLite veritabanlarıyla çalışmak için node:sqlite adlı yerleşik bir modülle gelir: npm paketi kurmadan new Database('veri.db') ile dosya tabanlı ya da ':memory:' ile bellek içi bir veritabanı açar, prepare() ile hazırlanmış sorgular oluşturup run(), get(), all() ve iterate() ile çalıştırırsınız. Bütün çağrılar senkrondur. Modül belgelerde 1.2 - Release candidate kararlılık düzeyindedir; YAML kaydına göre v25.7.0 ile bu düzeye çıktı.

Bu yazı yalnızca node:sqlite API'sini anlatıyor. SQLite'ın ne olduğu, hangi işlere uygun olduğu ve genel SQL bilgisi için SQLite nedir yazımıza bakabilirsiniz. Örnekler Node.js v26.13.0 ile çalıştırıldı ve çıktılar olduğu gibi aktarıldı.

Bayrak gerekiyor mu? Database ve DatabaseSync

Modül v22.5.0 ile --experimental-sqlite bayrağının arkasında geldi. YAML kaydına göre v23.4.0 ve v22.13.0 ile bayrak kaldırıldı; artık doğrudan import { Database } from 'node:sqlite' yazabilirsiniz. Modülü bilerek kapatmak isterseniz --no-experimental-sqlite bayrağı vardır; o zaman içe aktarma ERR_UNKNOWN_BUILTIN_MODULE hatası verir. Modül yalnızca node: önekiyle yüklenir.

Eski örneklerde DatabaseSync ve StatementSync adlarını görürsünüz. Sınıflar Database ve Statement olarak yeniden adlandırıldı; eski adlar geriye uyumluluk için duruyor ama DEP0210 ve DEP0211 kodlarıyla v26.11.0 itibarıyla belgede kullanımdan kaldırılmış (documentation-only) olarak işaretli. v26.13.0'da typeof DatabaseSync hâlâ 'function' döndürüyor, yani mevcut kodunuz çalışır; yeni kodda Database kullanın. Kullanımdan kaldırma türlerinin ne anlama geldiğini deprecation uyarıları yazısında anlattık.

Peki "release candidate" ne demek? Node.js belgelerindeki kararlılık dizinine göre 1 numaralı düzeyin tamamı deneyseldir: bu özellikler semantik sürümleme kurallarına bağlı değildir ve belgeler onları üretim ortamında kullanmayı önermez. Deneysel düzey kendi içinde üçe ayrılır. 1.0 erken geliştirme demektir, özellik bitmemiştir ve büyük değişikliklere açıktır. 1.1 etkin geliştirme demektir, özellik asgari kullanılabilirliğe yaklaşmıştır. 1.2 ise kararlı hâle gelmeye hazır olduğu umulan aşamadır; belgelere göre bundan sonra geriye uyumsuz değişiklik beklenmez ama kullanıcı geri bildirimine göre yine de olabilir. Yani node:sqlite deneysel ailenin en olgun basamağındadır. Kullanmadan önce proje sürümünüzün belgesini kontrol etmeniz ve sürüm yükseltirken sqlite sayfasındaki changes kayıtlarına bakmanız iyi bir alışkanlıktır.

İlk veritabanı: tablo, ekleme, sorgu

İlk veritabanı: tablo, ekleme, sorgu: database.exec(sql), statement.run(...), statement.get(...), statement.all(...)
javascript
// temel.mjs
import { Database } from 'node:sqlite';

const db = new Database('magaza.db');
db.exec(`
  DROP TABLE IF EXISTS urun;
  CREATE TABLE urun (
    id INTEGER PRIMARY KEY,
    ad TEXT NOT NULL,
    fiyat REAL NOT NULL,
    stokta INTEGER NOT NULL
  ) STRICT;
`);

const ekle = db.prepare('INSERT INTO urun (ad, fiyat, stokta) VALUES (?, ?, ?)');
console.log(ekle.run('Klavye', 750, true));
// çıktı: { changes: 1, lastInsertRowid: 1 }
ekle.run('Fare', 320, true);
ekle.run('Monitör', 5400, false);

const ucuz = db.prepare('SELECT ad, fiyat FROM urun WHERE fiyat < $limit ORDER BY fiyat');
console.log(ucuz.all({ limit: 1000 }));
// çıktı: [
// çıktı:   [Object: null prototype] { ad: 'Fare', fiyat: 320 },
// çıktı:   [Object: null prototype] { ad: 'Klavye', fiyat: 750 }
// çıktı: ]

const bul = db.prepare('SELECT * FROM urun WHERE id = ?');
console.log(bul.get(3));
// çıktı: [Object: null prototype] {
// çıktı:   id: 3,
// çıktı:   ad: 'Monitör',
// çıktı:   fiyat: 5400,
// çıktı:   stokta: 0
// çıktı: }
console.log(bul.get(99)); // çıktı: undefined

for (const satir of db.prepare('SELECT ad FROM urun').iterate()) {
  console.log('-', satir.ad);
}
// çıktı: - Klavye
// çıktı: - Fare
// çıktı: - Monitör
console.log(db.location().endsWith('magaza.db'), db.isOpen); // çıktı: true true
db.close();
console.log(db.isOpen); // çıktı: false

Birkaç ayrıntıya dikkat edin. exec() birden çok SQL ifadesini sonuç döndürmeden çalıştırır; şema kurulumu ya da dosyadan okunan SQL için uygundur. run() değişen satır sayısını (changes) ve son eklenen satırın kimliğini (lastInsertRowid) içeren bir nesne döndürür. get() ilk satırı ya da hiç satır yoksa undefined döndürür. Dönen satırlar [Object: null prototype] olarak görünür: bunlar prototipi olmayan düz nesnelerdir, alanlarına normal şekilde erişirsiniz. true olarak eklediğimiz stokta değeri 1, false ise 0 olarak saklandı. location() dosyanın yolunu verir; bellek içi veritabanında null döner.

YöntemDöndürdüğüNe zaman
database.exec(sql)undefinedŞema, toplu SQL, işlem komutları
statement.run(...){ changes, lastInsertRowid }INSERT, UPDATE, DELETE
statement.get(...)ilk satır veya undefinedTek kayıt arama
statement.all(...)satır dizisi (boş olabilir)Küçük ve orta sonuç kümeleri
statement.iterate(...)yineleyiciSatırları tek tek işlemek
statement.columns()sütun bilgisi dizisiSonuç şemasını öğrenmek

Bağlantı seçenekleri

new Database(yol, seçenekler) ikinci argümanda bağlantının davranışını ayarlar. Yol bir metin, Buffer ya da URL olabilir. Sık kullanılanlar şunlar:

SeçenekVarsayılanAçıklama
opentruefalse verilirse veritabanı open() ile sonradan açılır
readOnlyfalseSalt okunur açar; dosya yoksa açma başarısız olur
enableForeignKeyConstraintstrueYabancı anahtar kısıtlarını uygular
timeout0Kilit için beklenecek en uzun süre (ms)
readBigIntsfalseINTEGER değerlerini BigInt olarak okur
returnArraysfalseSatırları nesne yerine dizi olarak döndürür
allowExtensionfalseloadExtension() ile eklenti yüklemeye izin verir
defensivetrueSQL ile dosyayı bilerek bozan özellikleri kapatır

defensive seçeneği v25.5.0 ile varsayılan olarak açık hâle geldi. limits seçeneğiyle de dize uzunluğu, SQL uzunluğu ya da değişken sayısı gibi SQLite sınırlarını düşürebilirsiniz; belgeler bunu güvenilmeyen girdiyle aşırı kaynak tüketimini önlemenin yolu olarak anlatır.

timeout değeri sıfır olduğunda, başka bir bağlantı veritabanını kilitlemişse sorgu beklemeden hata verir. Aynı dosyayı birden fazla süreç ya da bağlantı kullanıyorsa birkaç yüz milisaniyelik bir bekleme süresi vermek, geçici kilitlerin hataya dönüşmesini azaltır. open: false ile bağlantı nesnesini önceden oluşturup gerçek açılışı database.open() ile sonraya bırakabilirsiniz; zaten açık bir bağlantıda open() çağırmak hata fırlatır. Salt okunur bir rapor süreci için readOnly: true kullanmak, kodda yanlışlıkla yazma yapılmasını SQLite düzeyinde engeller.

Parametre bağlama

Hazırlanmış ifadeler hem hızlıdır hem de SQL enjeksiyonuna karşı koruma sağlar; bu yüzden belgeler kullanıcı girdisi içeren sorgularda elle birleştirilmiş SQL yerine bunları önerir. İki tür parametre vardır. Anonim parametreler ? ile yazılır ve sırayla bağlanır; ?2 gibi numaralı biçim belirli bir indeksi seçer. İsimli parametreler $, : ya da @ ile başlar ve ilk argümandaki nesneden bağlanır. Yukarıdaki örnekte $limit parametresini { limit: 1000 } ile, yani önek olmadan bağladık; buna "bare" isim denir ve varsayılan olarak açıktır. Önek SQL'de yine zorunludur.

Bağlanmayan parametreler NULL olur; v26.10.0 ile undefined bağlamak da NULL yazar. İfadede olmayan bir isim verirseniz ERR_INVALID_STATE hatası alırsınız; örneğin Unknown named parameter 'a' mesajı. Bunu yok saymak için allowUnknownNamedParameters seçeneği vardır. Boolean değerler ile ArrayBuffer desteği v26.8.0 ile eklendi.

Belgeler iki karışıklığa karşı uyarır. Numaralı (?2) ve isimli parametreler aynı indeks alanını paylaştığı için ikisini aynı ifadede karıştırmayın. Bare isimler açıkken aynı ifadede $k ve @k gibi yalnızca önekiyle ayrılan iki parametre kullanırsanız, k anahtarının hangisine bağlanacağı belirlenemediği için hata alırsınız. Ayrıca önekli anahtar yazmak ({ $limit: 1000 }) belgelere göre biraz daha hızlı bağlanır; bare isimler yalnızca yazım kolaylığıdır.

JavaScript ve SQLite tipleri

JavaScript ve SQLite tipleri: NULL, INTEGER, REAL, TEXT
SQLiteJavaScript'ten yazarkenOkurken
NULLnull veya undefinednull
INTEGERnumber, bigint, booleannumber (veya bigint)
REALnumbernumber
TEXTstringstring
BLOBTypedArray, DataView, ArrayBufferUint8Array
javascript
// tip.mjs
import { Database } from 'node:sqlite';

const db = new Database(':memory:');
const s = db.prepare('SELECT ? AS deger');
console.log(s.get(true), s.get(null), s.get(undefined));
// çıktı: [Object: null prototype] { deger: 1 } [Object: null prototype] { deger: null } [Object: null prototype] { deger: null }
console.log(s.get(new Uint8Array([1, 2])));
// çıktı: [Object: null prototype] { deger: Uint8Array(2) [ 1, 2 ] }
try {
  s.get({}, new Date());
} catch (err) {
  console.log(err.code, err.message);
  // çıktı: ERR_INVALID_ARG_TYPE Provided value cannot be bound to SQLite parameter 1.
}

Son çağrıda ilk argüman olarak boş bir nesne verdik, çünkü bir nesne her zaman isimli parametre nesnesi olarak yorumlanır; Date ise desteklenen bir tip değildir. Tarihleri ISO metni ya da sayı olarak saklayın. Bir diğer tuzak büyük tam sayılardır: JavaScript'in güvenli tam sayı aralığını aşan bir INTEGER okunurken readBigInts kapalıysa ERR_OUT_OF_RANGE fırlatılır. Sayı tipleri arasındaki fark için Number ve BigInt yazısına bakabilirsiniz.

İşlemler (transaction)

Modülün ayrı bir işlem yardımcısı yok; BEGIN, COMMIT ve ROLLBACK komutlarını exec() ile çalıştırırsınız. Bir para transferinde iki güncellemeden biri başarısız olursa diğerinin de geri alınması gerekir:

javascript
// islem.mjs
import { Database } from 'node:sqlite';

const db = new Database(':memory:');
db.exec(`
  CREATE TABLE hesap (ad TEXT PRIMARY KEY, bakiye INTEGER NOT NULL CHECK (bakiye >= 0)) STRICT;
  INSERT INTO hesap VALUES ('ali', 100), ('ayse', 50);
`);

const guncelle = db.prepare('UPDATE hesap SET bakiye = bakiye + ? WHERE ad = ?');

function transfer(kimden, kime, tutar) {
  db.exec('BEGIN');
  try {
    guncelle.run(-tutar, kimden);
    guncelle.run(tutar, kime);
    db.exec('COMMIT');
  } catch (err) {
    db.exec('ROLLBACK');
    throw err;
  }
}

transfer('ali', 'ayse', 30);
try {
  transfer('ali', 'ayse', 500);
} catch (err) {
  console.log(err.code, err.errcode, err.message);
  // çıktı: ERR_SQLITE_ERROR 275 CHECK constraint failed: bakiye >= 0
}
console.log(db.isTransaction); // çıktı: false
console.log(db.prepare('SELECT * FROM hesap ORDER BY ad').all());
// çıktı: [
// çıktı:   [Object: null prototype] { ad: 'ali', bakiye: 70 },
// çıktı:   [Object: null prototype] { ad: 'ayse', bakiye: 80 }
// çıktı: ]

İkinci transfer CHECK kısıtına takıldı ve geri alındı; bakiyeler yalnızca ilk transferin sonucunu gösteriyor. SQLite hataları ERR_SQLITE_ERROR koduyla gelir, errcode alanında SQLite'ın kendi sayısal kodu bulunur. isTransaction özelliği bağlantının o an bir işlemin içinde olup olmadığını söyler; catch bloğunda ROLLBACK yazmayı unutursanız bu değer true kalır ve sonraki sorgular yarım kalmış işlemin içinde çalışır. try/catch kalıbının kendisi için hata yönetimi yazısına bakabilirsiniz.

createTagStore ile güvenli ve önbellekli sorgular

v24.9.0 ile gelen createTagStore(), hazırlanmış ifadeleri LRU önbellekte tutan ve etiketli şablon dizgileriyle çalışan bir yardımcı döndürür. Burada ${deger} metne eklenmez, parametre olarak bağlanır. Etiketsiz bir şablon dizgisi ise değeri SQL metninin içine yapıştırır ve enjeksiyona açıktır:

javascript
// ileri.mjs
import { Database } from 'node:sqlite';

const db = new Database(':memory:');
db.exec('CREATE TABLE kullanici (id INTEGER PRIMARY KEY, eposta TEXT)');

// Tagged template: ${} değeri parametre olarak bağlanır, metne eklenmez
const sql = db.createTagStore();
const kotu = "x'); DROP TABLE kullanici; --";
sql.run`INSERT INTO kullanici (eposta) VALUES (${kotu})`;
sql.run`INSERT INTO kullanici (eposta) VALUES (${'elif@ornek.com'})`;
console.log(sql.all`SELECT * FROM kullanici ORDER BY id`);
// çıktı: [
// çıktı:   [Object: null prototype] {
// çıktı:     id: 1,
// çıktı:     eposta: "x'); DROP TABLE kullanici; --"
// çıktı:   },
// çıktı:   [Object: null prototype] { id: 2, eposta: 'elif@ornek.com' }
// çıktı: ]
console.log(sql.size); // çıktı: 2

// Kullanıcı tanımlı fonksiyon
db.function('kucuk_harf', (s) => s.toLocaleLowerCase('tr'));
console.log(db.prepare("SELECT kucuk_harf('İSTANBUL') AS sehir").get());
// çıktı: [Object: null prototype] { sehir: 'istanbul' }

// BigInt ve dizi olarak sonuç
const buyuk = db.prepare('SELECT 9007199254740993 AS n');
try {
  buyuk.get();
} catch (err) {
  console.log(err.code); // çıktı: ERR_OUT_OF_RANGE
}
buyuk.setReadBigInts(true);
console.log(buyuk.get()); // çıktı: [Object: null prototype] { n: 9007199254740993n }
const dizi = db.prepare('SELECT 1 AS a, 2 AS b');
dizi.setReturnArrays(true);
console.log(dizi.get()); // çıktı: [ 1, 2 ]

Kötü niyetli metin tabloyu silmedi, düz bir e-posta değeri olarak kaydedildi. sql.size önbellekteki ifade sayısıdır: iki farklı sorgu metni kullandık, iki INSERT aynı metni paylaştığı için tek ifade sayıldı. Belgelere göre önbellek eşleşmesi büyük-küçük harfe duyarlıdır ve etiketli ifadelere ? gibi yer tutucular yazılmamalıdır. Etiketli şablonların dil düzeyindeki işleyişi template literal yazısında anlatılıyor.

database.function() SQL içinden çağrılabilen bir JavaScript fonksiyonu tanımlar; örnekte Türkçe kurallarla küçük harfe çeviren bir fonksiyon yazdık. deterministic ve varargs gibi seçenekleri vardır. Gruplama fonksiyonları için database.aggregate() kullanılır.

Mini örnek: görev listesi

Şimdi öğrendiklerimizi küçük bir görev listesinde birleştirelim. Amaç, ifadeleri bir kez hazırlayıp tekrar tekrar kullanmak, isimli parametrelerle okunaklı sorgular yazmak ve bir güncellemenin gerçekten bir satırı değiştirip değiştirmediğini changes alanından anlamaktır:

javascript
// gorev.mjs
import { Database } from 'node:sqlite';

const db = new Database(':memory:');
db.exec(`
  CREATE TABLE IF NOT EXISTS gorev (
    id INTEGER PRIMARY KEY,
    baslik TEXT NOT NULL,
    bitti INTEGER NOT NULL DEFAULT 0
  ) STRICT
`);

const ekle = db.prepare('INSERT INTO gorev (baslik) VALUES (:baslik)');
const bitir = db.prepare('UPDATE gorev SET bitti = 1 WHERE id = :id');
const acik = db.prepare('SELECT id, baslik FROM gorev WHERE bitti = 0 ORDER BY id');

for (const baslik of ['Faturayı öde', 'Yedek al', 'Raporu gönder']) {
  ekle.run({ baslik });
}
console.log(bitir.run({ id: 2 }).changes); // çıktı: 1
console.log(bitir.run({ id: 42 }).changes); // çıktı: 0
console.log(acik.all().map((g) => `${g.id}. ${g.baslik}`));
// çıktı: [ '1. Faturayı öde', '3. Raporu gönder' ]
console.log(acik.columns().map((c) => `${c.name}:${c.type}`));
// çıktı: [ 'id:INTEGER', 'baslik:TEXT' ]
console.log(acik.sourceSQL);
// çıktı: SELECT id, baslik FROM gorev WHERE bitti = 0 ORDER BY id

Var olmayan 42 numaralı görevi bitirmeye çalıştığımızda hata almadık; changes değeri 0 döndü. Bir HTTP API yazıyorsanız bu değeri kontrol edip 404 yanıtı vermek doğru yaklaşımdır. ekle.run({ baslik }) çağrısında nesnenin kısa yazımını kullandık; anahtar baslik olduğu için SQL'deki :baslik parametresine bare isim olarak bağlandı. columns() sonuç sütunlarının adını ve tanımlı tipini, sourceSQL ise ifadenin kaynak metnini verir; bu ikisi günlük kaydı ve hata ayıklama sırasında işe yarar. Aynı ifadeyi döngü içinde yeniden hazırlamak yerine bir kez hazırlayıp saklamak, hazırlanmış ifadelerin asıl avantajıdır. Bu yapıyı bir REST servisine taşımak isterseniz yönlendirme ve doğrulama tarafı için Node.js REST API yazısına bakabilirsiniz.

Yedekleme, serialize ve kaynakları kapatmak

backup(kaynakDb, yol, seçenekler) fonksiyonu çalışan bir veritabanının yedeğini alır ve aktarılan sayfa sayısıyla çözülen bir Promise döndürür; progress geri çağırması her adımda kalan ve toplam sayfa sayısını bildirir. Yedekleme sürerken veritabanı kullanılabilir, ancak başka bağlantılardan gelen değişiklikler yedeklemeyi yeniden başlatır. v26.1.0 ile gelen serialize() veritabanını bir Uint8Array olarak döndürür, deserialize() de böyle bir diziyi bağlantıya yükler; bellek içi bir veritabanını kopyalamak için kullanışlıdır.

database.close() bağlantıyı kapatır; zaten kapalıysa hata fırlatır. Database ve v26.8.0 itibarıyla Statement nesneleri Symbol.dispose desteklediği için belgelerdeki örneklerde using query = db.prepare(...) yazımı görülür; blok bitince ifade kendiliğinden kapanır. İfadeleri statement.close() ile elle de sonlandırabilirsiniz.

İzin modeli ve sqlite

İzin modeli belgeleri önemli bir uyarı içerir: dosya sistemi kısıtları node:fs modülü için geçerlidir ve node:sqlite gibi başka yollarla dosyaya erişimi garanti altına almaz. Denememizde --permission ile yazma izni vermediğimiz bir klasörde new Database() yeni bir dosya oluşturabildi. Ayrıca izin modeli açıkken çalışma zamanı eklentileri (loadExtension) yüklenemez. Ayrıntılı örnek izin modeli yazısında.

Sık yapılan hatalar

Sık yapılan hatalar: SQL'i elle birleştirmek, Senkron olduğunu unutmak, ROLLBACK unutmak, Date nesnesi bağlamak
  • SQL'i elle birleştirmek: kullanıcı girdisini `... ${x}` ile metne eklemek enjeksiyona açıktır; prepare() parametreleri ya da createTagStore() kullanın.
  • Senkron olduğunu unutmak: bütün çağrılar olay döngüsünü bloklar; çok uzun sorgular sunucunuzun başka isteklere yanıt vermesini geciktirir.
  • ROLLBACK unutmak: hata sonrasında işlem açık kalır; isTransaction ile kontrol edin.
  • Date nesnesi bağlamak: desteklenmez ve ERR_INVALID_ARG_TYPE verir; ISO metni ya da sayı saklayın.
  • Büyük kimlikleri number ile okumak: güvenli aralığı aşan değerler ERR_OUT_OF_RANGE verir; readBigInts açın.
  • Eski sınıf adını yeni kodda kullanmak: DatabaseSync çalışır ama belgede kullanımdan kaldırılmıştır; Database yazın.
  • İzin modeline güvenmek: sqlite dosya erişimi --allow-fs-* kısıtlarıyla garanti altında değildir.

Sık Sorulan Sorular

node:sqlite üretimde kullanılabilir mi?

Modül "release candidate" düzeyindedir, yani hâlâ deneysel ailenin içindedir. Node.js belgeleri deneysel özellikleri üretimde kullanmayı genel olarak önermez; 1.2 düzeyinde geriye uyumsuz değişiklik beklenmediğini ama yine de olabileceğini söyler. Kararı verirken bu riski, harici bir paket bağımlılığından kurtulmanın getirisiyle tartın; kullanıyorsanız Node.js sürümünüzü sabitleyin ve yükseltmeden önce sqlite sayfasındaki changes kayıtlarını okuyun.

Asenkron bir API var mı?

Belgelenen Database ve Statement sınıflarının bütün yöntemleri senkron çalışır. Yalnızca backup() bir Promise döndürür. Ağır sorguları ana iş parçacığından uzak tutmak istiyorsanız worker threads ile ayrı bir iş parçacığında çalıştırmayı düşünebilirsiniz.

Bellek içi veritabanı ne zaman işe yarar?

':memory:' ile açılan veritabanı süreç bitince kaybolur. Testlerde her test için temiz bir veritabanı kurmak, geçici hesaplamalar ve önbellekler için idealdir. Test kurulumu için node:test rehberine bakabilirsiniz.

Sorgu sonuçları neden [Object: null prototype] görünüyor?

Satırlar prototipi olmayan nesneler olarak üretilir; util.inspect bunu belirtmek için bu etiketi basar. Alanlara satir.ad gibi erişmek ya da JSON.stringify ile serileştirmek normal şekilde çalışır.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Node.js öğrenme rehberi · SQLite nedir · İzin modeli · node:test · Deprecation uyarıları · Node.js REST API