Ketika COMMIT dilaporkan sukses, SQLite sesungguhnya sedang menepati sebuah kontrak berisi empat jaminan yang dikenal sebagai ACID. Kontrak ini menjelaskan mengapa transfer uang, pencatatan pesanan, dan perubahan skema bisa dilakukan tanpa takut data setengah jadi. Mari kita bedah apa isi tiap jaminan dan bagaimana engine SQLite mewujudkannya.

Empat Jaminan dalam Satu Kata #

HurufJaminanArtinya praktis
AAtomicityTransaksi terjadi seluruhnya atau tidak sama sekali
CConsistencyTransaksi berpindah dari keadaan valid ke keadaan valid
IIsolationTransaksi berjalan seolah-olah sendirian di database
DDurabilitySetelah COMMIT sukses, perubahan bertahan meski listrik mati

Sepanjang chapter ini kita memakai skema berikut di toko.db:

sql
CREATE TABLE rekening (
  id INTEGER PRIMARY KEY,
  pemilik TEXT NOT NULL,
  saldo INTEGER NOT NULL CHECK (saldo >= 0)
);
CREATE TABLE mutasi (
  id INTEGER PRIMARY KEY,
  rekening_id INTEGER NOT NULL REFERENCES rekening(id),
  keterangan TEXT NOT NULL,
  jumlah INTEGER NOT NULL
);

PRAGMA foreign_keys = ON;
INSERT INTO rekening VALUES (1, 'Budi', 500000), (2, 'Siti', 200000);
INSERT INTO mutasi VALUES (1, 2, 'transfer masuk', 150000);

Atomicity: Journal Menyimpan Keadaan Lama #

Sebelum engine SQLite mengubah isi berkas database, salinan asli halaman yang akan diubah terlebih dahulu disalin ke berkas rollback journal (toko.db-journal). Saat COMMIT, urutan kerjanya kira-kira: journal dipaksa masuk ke media penyimpanan, perubahan ditulis ke berkas utama, lalu journal dihapus. Momen journal terhapus itulah titik komit — sebelum itu, semua masih bisa dikembalikan.

Jika proses mati di tengah jalan, koneksi berikutnya yang membuka database menemukan sisa journal yang “panas”, lalu menyalin kembali halaman-halaman lama ke berkas utama. Percobaan berikut membuktikannya — buka transaksi, ubah saldo, lalu bunuh prosesnya sebelum COMMIT:

text
sqlite> BEGIN;
sqlite> UPDATE rekening SET saldo = saldo - 150000 WHERE id = 1;

Tekan Ctrl-D untuk keluar, atau bunuh proses sqlite3 dengan kill -9 — keduanya meninggalkan transaksi tanpa penutup. Periksa hasilnya dari koneksi baru:

bash
sqlite3 toko.db "SELECT id, pemilik, saldo FROM rekening ORDER BY id;"

Output yang Dihasilkan:

text
1|Budi|500000
2|Siti|200000

Saldo Budi kembali ke 500000 seolah-olah UPDATE tidak pernah terjadi. Tidak ada perubahan setengah jadi yang tersimpan — inilah atomicity bekerja.

Consistency: Constraint Dijaga per Statement #

Setiap statement diperiksa terhadap seluruh constraint — NOT NULL, UNIQUE, CHECK, foreign key — sebelum efeknya diterapkan. Akibatnya, keadaan sementara di tengah transaksi pun harus valid; transaksi tidak mungkin berakhir di keadaan yang melanggar skema:

sql
INSERT INTO mutasi VALUES (2, 99, 'transfer ke akun tak dikenal', 75000);

Output yang Dihasilkan:

text
Runtime error: FOREIGN KEY constraint failed (19)

Rekening dengan id 99 tidak ada, jadi mutasi yatim ini ditolak seketika. Perhatikan bahwa pemeriksaan terjadi pada level statement, bukan level COMMIT — kita tidak perlu menunggu transaksi selesai untuk tahu ada yang salah. Pengecualian hanya berlaku untuk foreign key DEFERRABLE INITIALLY DEFERRED yang menunda pemeriksaan hingga COMMIT.

Isolation: Pembaca Melihat Snapshot #

Koneksi yang sedang membaca selalu melihat keadaan per COMMIT terakhir, bukan perubahan yang masih mengambang di transaksi orang lain — kita sudah menyaksikannya di Chapter Transaksi Dasar: BEGIN, COMMIT, ROLLBACK saat koneksi kedua membaca saldo lama. Untuk penulisan, SQLite menerapkan aturan tegas: hanya satu penulis pada satu waktu untuk seluruh berkas database. Mekanisme locking di baliknya — termasuk galat database is locked — dibedah tuntas pada Chapter Isolation dan Locking.

Durability: COMMIT Berarti fsync #

Saat COMMIT, engine memaksa sistem operasi menuliskan data ke media penyimpanan fisik lewat fsync() sebelum melaporkan sukses. Setelah itu, pemadaman listrik tidak akan menghapus perubahan. Seberapa keras paksaan ini diatur oleh PRAGMA synchronous:

sql
PRAGMA synchronous;

Output yang Dihasilkan:

text
2

Nilai 2 berarti FULL — bawaan SQLite. Tiga pilihan yang tersedia:

NilaiNamaPerilaku
0OFFTanpa fsync; tercepat, tetapi pemadaman bisa merusak database
1NORMALfsync lebih jarang; aman untuk WAL, transaksi terakhir bisa hilang
2FULLfsync penuh di setiap komit (bawaan)

Dengan synchronous = OFF, jaminan durability praktis dilepas: bukan hanya transaksi terakhir yang hilang, berkas database bisa menjadi korup. Di mode WAL yang dibahas pada Chapter Journal Modes dan WAL, kombinasi synchronous = NORMAL adalah pilihan populer karena tetap menjaga berkas dari kerusakan.