Atomicity menuntut hal yang mustahil bagi berkas: saat COMMIT berlangsung, data lama dan data baru harus bertransisi tanpa pernah ada keadaan setengah jadi yang bisa dilihat. Trik SQLite adalah menulis di dua tempat — perubahan di satu berkas, cadangan keadaan lama di berkas lain yang disebut journal. Chapter ini membedah mekanisme rollback journal yang menjadi default, daftar lengkap journal_mode, lalu WAL: arsitektur alternatif yang membuat pembaca dan penulis tidak lagi saling menunggu.

Skema Contoh #

Seluruh contoh memakai tabel produk di berkas toko.db:

sql
CREATE TABLE produk (
  id INTEGER PRIMARY KEY,
  nama TEXT NOT NULL,
  stok INTEGER NOT NULL
);

INSERT INTO produk VALUES (1, 'Kopi Gayo 200g', 40);

Setiap demo di chapter ini membuat ulang tabel tersebut secara mandiri, jadi setiap blok bisa langsung disalin dan dijalankan.

Rollback Journal (Mode DELETE) #

Cara kerja default engine SQLite: sebelum sebuah halaman berkas utama diubah, salinan aslinya disalin dulu ke toko.db-journal. Saat COMMIT, perubahan ditulis ke berkas utama dan journal dihapus — momen journal terhapus itulah titik komit. Bila proses mati sebelum sempat selesai, koneksi berikutnya menemukan journal tersisa dan memulihkan halaman-halaman lama, sehingga transaksi yang belum selesai lenyap tanpa jejak.

Kita bisa menyaksikan siklus itu dari luar. Skrip berikut membuka transaksi dan menahannya dua detik — cukup untuk memotret isi folder di tengah jalan:

bash
rm -f toko.db*
sqlite3 toko.db "CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT NOT NULL, stok INTEGER NOT NULL);
INSERT INTO produk VALUES (1, 'Kopi Gayo 200g', 40);"

ls toko.db*

# transaksi ditahan terbuka selama 2 detik
( echo "BEGIN IMMEDIATE; UPDATE produk SET stok = stok - 1 WHERE id = 1;"; \
  sleep 2; echo "COMMIT;" ) | sqlite3 toko.db &
sleep 0.5
ls toko.db*
wait
ls toko.db*

Output yang Dihasilkan:

text
toko.db
toko.db
toko.db-journal
toko.db

Urutan keluarannya: sebelum transaksi hanya ada toko.db; saat transaksi berjalan, toko.db-journal muncul berdampingan dengannya; setelah COMMIT, journal lenyap lagi dan hanya toko.db yang tersisa.

Variasi journal_mode #

PRAGMA journal_mode mengatur bagaimana journal itu diperlakukan. Daftar lengkapnya:

ModePerilakuBerkas journal setelah COMMIT
DELETEdefault; journal dihapushilang
TRUNCATEjournal dipotong kosongtetap ada, 0 byte
PERSISTheader journal di-nol-kantetap ada, ukuran penuh
MEMORYjournal disimpan di memoritidak ada di disk
WALarsitektur append (bahasan berikutnya)berkas -wal
OFFjournal dimatikantidak ada

TRUNCATE dan PERSIST menghemat operasi hapus/buat berkas pada sistem operasi yang lambat melakukannya — jurnalnya hanya “dikosongkan”, bukan dihapus. MEMORY mempercepat dengan menaruh journal di RAM. Berikut tampilan mode aktif yang bisa kita cek kapan saja:

bash
sqlite3 toko.db "PRAGMA journal_mode;"

Output yang Dihasilkan:

text
delete

Mode WAL #

WAL (Write-Ahead Logging) membalik arsitektur. Alih-alih menimpa berkas utama sambil menyimpan keadaan lama di journal, penulis menambahkan (append) versi halaman baru ke berkas toko.db-wal — berkas utama toko.db tidak disentuh sampai checkpoint. Pembaca cukup menggabungkan berkas utama dan isi WAL untuk melihat data termutakhir.

Pengaktifannya satu baris, dan nilainya menempel permanen pada berkas database:

bash
rm -f toko.db*
sqlite3 toko.db "CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT NOT NULL, stok INTEGER NOT NULL);
INSERT INTO produk VALUES (1, 'Kopi Gayo 200g', 40);
PRAGMA journal_mode=wal;"

# tulis dan COMMIT, koneksi dibiarkan terbuka
( echo "BEGIN; UPDATE produk SET stok = stok - 5 WHERE id = 1;
UPDATE produk SET stok = stok - 5 WHERE id = 1;"; sleep 0.3; echo "COMMIT;"; sleep 2 ) | sqlite3 toko.db &
sleep 1
ls toko.db*
sqlite3 toko.db "PRAGMA wal_checkpoint;"
wait
ls toko.db*
sqlite3 toko.db "SELECT id, nama, stok FROM produk ORDER BY id;"

Output yang Dihasilkan:

text
wal
toko.db
toko.db-shm
toko.db-wal
0|1|1
toko.db
1|Kopi Gayo 200g|30

Ada tiga hal yang perlu dibaca dari keluaran tersebut:

  1. PRAGMA journal_mode=wal; mengembalikan wal — konfirmasi bahwa mode baru aktif dan akan tetap aktif untuk koneksi berikutnya, termasuk setelah berkas ditutup dan dibuka lagi.
  2. Saat ada koneksi aktif, muncul toko.db-wal dan toko.db-shm — berkas WAL berisi perubahan yang sudah di-commit, sedangkan berkas shared memory dipakai para koneksi untuk berkoordinasi. Setelah koneksi terakhir menutup database dengan bersih, keduanya dihapus secara otomatis dan hanya toko.db yang tersisa.
  3. PRAGMA wal_checkpoint; mengembalikan 0|1|1 — tiga angka itu artinya busy=0 (tidak ada pembaca yang menghalangi), log=1 (satu halaman menumpuk di WAL), dan checkpointed=1 (satu halaman berhasil disalin kembali ke berkas utama).

Checkpoint adalah proses memindahkan isi WAL kembali ke toko.db. Selain manual lewat PRAGMA wal_checkpoint, engine SQLite menjalankannya otomatis setiap kali WAL menumpuk melewati 1.000 halaman — itulah sebabnya berkas WAL tidak tumbuh tanpa batas pada aplikasi yang terus menulis.

Pembaca dan Penulis Berjalan Bersamaan #

Perubahan terbesar WAL dirasakan pada locking. Pada Chapter Isolation dan Locking kita melihat pembaca yang transaksinya terbuka mampu menggagalkan COMMIT penulis. Di mode WAL, kejadian itu tidak terjadi lagi — keduanya benar-benar berjalan bersamaan:

bash
# Terminal 1 (pembaca): transaksi baca terbuka ~3 detik
( echo "BEGIN; SELECT stok FROM produk WHERE id = 1;"; \
  sleep 3; echo "SELECT stok FROM produk WHERE id = 1; COMMIT;" ) | sqlite3 toko.db &
sleep 0.5

# Terminal 2 (penulis): commit saat pembaca masih membaca
sqlite3 toko.db "UPDATE produk SET stok = stok - 2 WHERE id = 1;"
wait
sqlite3 toko.db "SELECT id, nama, stok FROM produk ORDER BY id;"

Output yang Dihasilkan:

text
30
30
1|Kopi Gayo 200g|28

Penulis sukses me-commit tanpa galat database is locked padahal pembaca masih membaca. Lebih menarik lagi, SELECT kedua si pembaca masih mengembalikan 30 — pembaca memegang snapshot keadaan saat transaksinya dimulai dan tidak terpengaruh perubahan di sekitarnya. Koneksi baru setelah keduanya selesai melihat hasil akhirnya: stok 28.

Yang tetap berlaku di WAL: hanya satu penulis aktif pada satu waktu. Dua penulis yang berhadapan tetap menghasilkan galat database is locked seperti biasa, sehingga busy_timeout dari chapter sebelumnya tetap relevan.

synchronous FULL dan NORMAL #

PRAGMA synchronous mengatur seberapa sering engine SQLite menjalankan fsync — memaksa data benar-benar ditulis ke media penyimpanan, bukan mengendap di cache sistem operasi. FULL berarti fsync dijalankan pada setiap titik kritis; NORMAL menguranginya dengan menanggung risiko tertentu:

bash
sqlite3 toko.db "PRAGMA synchronous;"
sqlite3 toko.db "PRAGMA journal_mode=wal; PRAGMA synchronous=NORMAL; PRAGMA synchronous;"

Output yang Dihasilkan:

text
2
wal
1

Nilai 2 adalah FULL (default), dan 1 adalah NORMAL yang baru saja kita setel. Konsekuensi praktisnya di mode WAL: dengan FULL, setiap COMMIT menunggu fsync berkas WAL selesai; dengan NORMAL, fsync hanya terjadi saat checkpoint. Kehilangan daya listrik di mode NORMAL bisa membuat transaksi-transaksi terakhir menguap — tetapi berkas database tetap utuh dan tidak korup, karena checkpoint berikutnya akan membangun kembali keadaan yang konsisten dari WAL yang tersinkron.

Dengan ini rangkaian transaksi dan konsistensi SQLite lengkap: kita telah membedah BEGIN hingga COMMIT, jaminan ACID di baliknya, perilaku autocommit, pertarungan lock antar koneksi, savepoint, sampai mekanisme di disk yang menjaga semuanya tetap utuh.