Pengujian korupsi SQLite harus menjawab dua pertanyaan: apakah aplikasi mendeteksi database yang rusak, dan apakah prosedur pemulihannya mengembalikan data secara konsisten? Cara yang aman adalah melakukan failure injection hanya pada salinan atau fixture disposable, lalu memverifikasi database hasil restore menggunakan pemeriksaan SQLite, invariant bisnis, dan rangkaian regresi aplikasi.

Jangan pernah memodifikasi file database produksi untuk eksperimen. Simpan artefak asli sebagai bukti, buat salinan kerja, hentikan proses penulis saat membuat salinan, dan gunakan backup yang telah diverifikasi sebagai sumber pemulihan.

Memahami sumber korupsi SQLite

SQLite dirancang untuk menjaga atomicitas transaksi, tetapi database tetap dapat rusak jika lingkungan penyimpanan atau cara aplikasi mengakses file melanggar asumsi SQLite. Dokumentasi resmi SQLite tentang penyebab korupsi menyoroti beberapa kelas masalah berikut:

  • Penyalinan file saat database sedang digunakan. Menyalin hanya file utama ketika transaksi atau journal masih aktif dapat menghasilkan salinan yang tidak konsisten. Salinan juga dapat memerlukan file -wal dan -shm jika database menggunakan WAL.
  • Masalah locking filesystem. SQLite membutuhkan locking yang benar dan konsisten. Filesystem jaringan, implementasi virtual filesystem yang keliru, atau beberapa host yang tidak menghormati lock dapat menyebabkan dua penulis merusak halaman database.
  • Kegagalan sinkronisasi penyimpanan. Disk, controller, filesystem, atau konfigurasi yang tidak menjamin penulisan dan flush sesuai kontrak dapat kehilangan bagian transaksi setelah listrik mati atau sistem crash.
  • Penghapusan atau perubahan journal/WAL. Menghapus, mengganti nama, atau memodifikasi file pendamping ketika koneksi masih aktif dapat menghilangkan informasi pemulihan.
  • Penimpaan file atau offset yang salah. Bug pada aplikasi, binding, ekstensi SQLite, atau operasi file tingkat rendah dapat menulis byte ke lokasi yang tidak semestinya.
  • Disk penuh dan kegagalan perangkat. SQLite biasanya dapat mengembalikan error, tetapi kegagalan storage yang lebih luas dapat meninggalkan file, journal, atau backup dalam keadaan tidak lengkap.

Failure injection di lingkungan pengujian tidak perlu meniru setiap penyebab secara sempurna. Tujuannya adalah memastikan sistem mampu mendeteksi file yang tidak valid, memilih backup yang benar, melakukan restore secara atomik, dan memvalidasi hasilnya.

Menyiapkan fixture dan batas keselamatan

Gunakan database sintetis atau salinan yang sudah dianonimkan. Fixture harus berisi data yang cukup untuk menguji relasi, indeks, transaksi, dan aturan bisnis, tetapi tidak boleh berisi data produksi yang belum memiliki izin untuk diproses di lingkungan pengujian.

  1. Hentikan writer atau gunakan mekanisme backup SQLite yang konsisten. Jangan menjalankan cp terhadap database aktif tanpa memahami journal atau WAL.
  2. Buat checksum dan salinan read-only dari sumber fixture.
  3. Kerjakan semua injeksi pada direktori sementara.
  4. Berikan nama yang jelas seperti fixture-truncated.db atau fixture-byte-flipped.db.
  5. Hapus artefak sensitif setelah pipeline selesai, kecuali artefak tersebut memang diperlukan untuk debugging dan memiliki kebijakan retensi yang aman.

Contoh membuat salinan disposable dan menyimpan checksum:

set -eu
mkdir -p work
cp fixture/app.db work/original.db
sha256sum work/original.db > work/original.db.sha256
cp work/original.db work/corrupt.db

Untuk pengujian berbasis aplikasi, lebih baik gunakan SQLite backup API atau perintah backup dari CLI daripada menyalin file aktif secara langsung. Pendekatan tersebut menyalin isi database melalui mekanisme SQLite dan menghindari asumsi bahwa satu file selalu cukup.

Simulasi korupsi pada salinan

Memotong file

Memotong sebagian akhir file adalah simulasi sederhana untuk kehilangan data atau kegagalan penulisan. Ini bukan representasi semua jenis korupsi, tetapi berguna untuk memastikan aplikasi tidak menganggap file yang rusak sebagai database sehat.

cp work/original.db work/truncated.db
size=$(wc -c < work/truncated.db)
new_size=$((size / 2))
truncate -s "$new_size" work/truncated.db

Mengubah byte secara deterministik

Pengubahan byte pada salinan dapat mensimulasikan kerusakan halaman atau perubahan data akibat masalah storage. Gunakan offset yang tetap agar hasil CI dapat direproduksi. Hindari header SQLite pada eksperimen pertama jika ingin menguji kerusakan halaman data, tetapi tetap perlakukan hasilnya sebagai simulasi, bukan model lengkap kegagalan perangkat.

cp work/original.db work/byte-flipped.db
python3 - <<'PY'
from pathlib import Path

path = Path("work/byte-flipped.db")
data = bytearray(path.read_bytes())
offset = 4096
if offset >= len(data):
    raise SystemExit("Fixture terlalu kecil untuk offset pengujian")
data[offset] ^= 0x01
path.write_bytes(data)
PY

Jangan menguji korupsi dengan menulis ke database yang sedang dibuka aplikasi. Selain berisiko merusak data, eksperimen tersebut dapat menghasilkan hasil yang tidak deterministik karena koneksi masih memiliki page cache, transaksi aktif, atau file WAL.

Memeriksa kerusakan dengan PRAGMA

PRAGMA quick_check melakukan pemeriksaan yang lebih ringan dan cocok sebagai pemeriksaan awal atau health check terjadwal. PRAGMA integrity_check melakukan pemeriksaan yang lebih menyeluruh terhadap struktur database, termasuk konsistensi berbagai bagian internal. Keduanya bukan pengganti invariant bisnis.

sqlite3 work/corrupt.db <<'SQL'
PRAGMA quick_check;
PRAGMA integrity_check;
SQL

Database sehat biasanya menghasilkan ok untuk masing-masing pragma. Namun, kode pengujian harus memeriksa output, bukan hanya exit code proses. Sebaliknya, hasil yang berisi nama tabel, halaman, atau pesan error harus diperlakukan sebagai kegagalan validasi dan disimpan sebagai artefak CI.

Contoh pemeriksaan sederhana:

check_db() {
  db="$1"
  output=$(sqlite3 "$db" 'PRAGMA integrity_check;')
  test "$output" = "ok"
}

check_db work/restored.db

quick_check dapat dipilih ketika waktu pemeriksaan terbatas, tetapi recovery gate sebaiknya memakai integrity_check jika ukuran database dan batas waktu memungkinkan. Jalankan pemeriksaan pada file hasil restore, bukan hanya pada backup sebelum disimpan.

Backup, restore, dan verifikasi

Memverifikasi backup

Backup yang berhasil dibuat belum tentu dapat dipakai. Validasi minimum mencakup keberadaan file, ukuran yang masuk akal, checksum atau integritas transfer, kemampuan dibuka oleh SQLite, hasil integrity_check, dan invariant bisnis yang penting.

Contoh membuat salinan menggunakan SQLite CLI:

sqlite3 work/original.db '.backup work/backup.db'

sqlite3 work/backup.db 'PRAGMA integrity_check;'
sha256sum work/backup.db

Untuk backup yang berasal dari object storage atau host lain, unduh ke direktori sementara, verifikasi checksum bila tersedia, kemudian jalankan pemeriksaan di sana. Jangan mengganti database aktif hanya karena file backup dapat dibuka.

Prosedur restore aman

  1. Hentikan penulis, misalnya dengan menghentikan service atau mengalihkan trafik tulis.
  2. Ambil salinan database saat ini untuk forensik. Jangan langsung menghapusnya.
  3. Pilih backup tervalidasi berdasarkan waktu dan hasil pemeriksaan, bukan hanya nama file terbaru.
  4. Restore ke file sementara pada filesystem yang sama dengan lokasi database aplikasi.
  5. Jalankan quick_check, integrity_check, dan invariant bisnis pada file sementara.
  6. Atur permission dan ownership sesuai kebutuhan service.
  7. Ganti file secara atomik setelah semua validasi lulus, menggunakan strategi yang sesuai dengan platform dan konfigurasi journal/WAL.
  8. Mulai service dan jalankan smoke test sebelum membuka seluruh trafik.
  9. Pantau error SQLite, latency, dan operasi baca-tulis setelah pemulihan.

Pastikan file -wal dan -shm lama tidak tertinggal dan tercampur dengan database hasil restore. Detail penanganannya bergantung pada mode journal dan apakah ada proses yang masih membuka database. Karena itu, penghentian seluruh writer dan prosedur cutover yang konsisten lebih aman daripada sekadar menimpa satu file.

Catatan: .dump dapat membantu menyelamatkan data dari database yang sebagian rusak, tetapi bukan restore lossless yang selalu berhasil. Dump bisa gagal membaca halaman tertentu, tidak mempertahankan seluruh detail internal, dan tidak boleh dianggap sebagai pengganti backup tervalidasi.

Invariant yang wajib diverifikasi

Integrity check hanya memeriksa struktur SQLite. Recovery juga harus membuktikan bahwa makna data tetap benar. Pilih invariant yang mewakili aturan aplikasi, misalnya:

  • Jumlah akun aktif tidak boleh menjadi nol jika fixture memiliki akun aktif.
  • Setiap baris anak memiliki parent yang valid.
  • Nilai status hanya berasal dari himpunan yang diizinkan.
  • Saldo, total, atau counter tidak bernilai negatif jika aturan domain melarangnya.
  • Jumlah baris penting dan rentang timestamp berada dalam batas yang diharapkan.
  • Indeks unik tidak menghasilkan duplikasi setelah restore.
  • Versi schema dan migration yang tercatat sesuai dengan binary aplikasi.

Contoh query invariant:

sqlite3 work/restored.db <<'SQL'
SELECT CASE WHEN COUNT(*) > 0 THEN 'ok' ELSE 'FAIL: tidak ada akun aktif' END
FROM accounts
WHERE deleted_at IS NULL;

SELECT CASE WHEN COUNT(*) = 0 THEN 'ok' ELSE 'FAIL: order tanpa customer' END
FROM orders o
LEFT JOIN customers c ON c.id = o.customer_id
WHERE c.id IS NULL;

SELECT CASE WHEN COUNT(*) = 0 THEN 'ok' ELSE 'FAIL: status tidak valid' END
FROM orders
WHERE status NOT IN ('pending', 'paid', 'cancelled');
SQL

Di aplikasi nyata, jadikan setiap invariant sebagai assertion yang mengembalikan exit code gagal ketika hasilnya tidak sesuai. Jangan hanya mencetak pesan yang terlihat benar tetapi tetap mengembalikan status sukses.

Workflow CI untuk failure injection dan recovery

Workflow CI dapat dibuat sebagai rangkaian tahap: siapkan fixture, buat backup, injeksikan korupsi, pastikan deteksi gagal, restore dari backup, lalu jalankan pemeriksaan dan regresi.

set -eu

rm -rf work
mkdir work
cp testdata/app.db work/original.db

# Buat backup melalui SQLite, bukan menyalin file aktif.
sqlite3 work/original.db '.backup work/backup.db'

test "$(sqlite3 work/backup.db 'PRAGMA integrity_check;')" = "ok"

# Failure injection hanya pada salinan disposable.
cp work/original.db work/corrupt.db
python3 - <<'PY'
from pathlib import Path
p = Path("work/corrupt.db")
b = bytearray(p.read_bytes())
b[len(b) // 2] ^= 1
p.write_bytes(b)
PY

# Database rusak harus terdeteksi; jangan mengharapkan semua jenis korupsi
# menghasilkan pesan yang sama.
if test "$(sqlite3 work/corrupt.db 'PRAGMA integrity_check;' 2>/dev/null)" = "ok"; then
  echo "Korupsi tidak terdeteksi oleh fixture ini"
  exit 1
fi

cp work/backup.db work/restored.db
test "$(sqlite3 work/restored.db 'PRAGMA integrity_check;')" = "ok"
./ci/check-sqlite-invariants.sh work/restored.db
./ci/run-regression-tests.sh

Test regresi setelah restore sebaiknya mencakup pembukaan koneksi, query utama, transaksi baca-tulis, constraint, endpoint atau command penting, serta restart aplikasi. Jika aplikasi menggunakan cache, queue, atau file pendamping, uji juga apakah state eksternal perlu dibersihkan atau dibangun ulang.

Kriteria lulus dan batasan eksperimen

Anggap skenario recovery lulus apabila seluruh kondisi berikut terpenuhi:

  • Database produksi tidak pernah menjadi target failure injection.
  • Korupsi pada fixture terdeteksi atau diarahkan ke jalur pemulihan yang sesuai.
  • Backup yang dipilih lolos pemeriksaan format dan integrity_check.
  • Database hasil restore lolos quick_check, integrity_check, dan seluruh invariant bisnis.
  • Migration/schema cocok dengan versi aplikasi yang digunakan.
  • Smoke test dan regresi utama lulus setelah service dimulai.
  • Artefak, log, durasi recovery, dan keputusan backup tercatat tanpa membocorkan data sensitif.

Failure injection dengan truncation atau byte flip tidak membuktikan ketahanan terhadap semua kegagalan nyata. Eksperimen tersebut tidak menggantikan pengujian crash recovery, pemulihan dari backup jarak jauh, disk penuh, kegagalan filesystem, atau masalah locking pada deployment multi-proses. Untuk skenario itu, gunakan lingkungan terisolasi, data sintetis, dan rencana rollback yang dapat dihentikan kapan saja.

Terakhir, dokumentasikan RPO dan RTO, frekuensi backup, pemilik prosedur restore, serta langkah eskalasi. Recovery yang aman bukan hanya kemampuan membuka file SQLite, melainkan proses teruji yang dapat mengembalikan database konsisten tanpa mempertaruhkan database produksi.