Deployment aman saat skema SQLite berevolusi tanpa downtime menuntut disiplin yang berbeda dibanding database server seperti PostgreSQL atau MySQL. Pada SQLite, database adalah file yang diakses langsung oleh proses aplikasi. Artinya, perubahan skema bukan hanya soal menjalankan migration, tetapi juga soal kompatibilitas antara binary aplikasi lama dan baru terhadap file database yang sama.

Jika tim kecil perlu merilis perubahan skema SQLite dengan aman, pendekatan yang paling praktis adalah migrasi bertahap: pisahkan perubahan menjadi fase expand dan contract, lindungi perilaku baru dengan feature flag, cek kompatibilitas saat startup, ambil backup sebelum migrasi, lakukan canary pada node pembaca lebih dulu, dan siapkan rollback yang realistis. Fokus artikel ini adalah operasional deployment, bukan teori migrasi semata.

Konteks penting: masalah utama pada SQLite bukan hanya syntax migration, tetapi fakta bahwa satu file DB dapat dibuka oleh beberapa proses atau binary dengan asumsi skema yang berbeda. Di sinilah banyak insiden muncul.

Mengapa evolusi skema SQLite berisiko saat deployment

Pada sistem berbasis SQLite, aplikasi dan database sering didistribusikan sebagai satu unit sederhana. Ini nyaman, tetapi juga menimbulkan risiko khusus:

  • File database hidup lebih lama dari binary aplikasi. Binary baru bisa membaca file lama, dan binary lama kadang masih mencoba membaca file yang sudah dimigrasikan.
  • Tidak ada pemisahan kuat antara server DB dan klien. Validasi kompatibilitas harus Anda bangun sendiri di level aplikasi atau proses deployment.
  • Migration sering bersifat tidak simetris. Menambah kolom biasanya lebih aman daripada menghapus kolom atau mengubah makna data.
  • Rollback binary belum tentu berarti rollback database. Jika skema sudah berubah dan binary lama tidak mengerti perubahan itu, rollback aplikasi bisa gagal total.

Karena itu, target utamanya bukan sekadar “migration sukses”, melainkan versi aplikasi lama dan baru bisa hidup berdampingan selama jendela deployment.

Prinsip desain deployment yang aman

1. Anggap skema sebagai kontrak kompatibilitas

Jangan menganggap migration sebagai skrip sekali jalan. Perlakukan skema sebagai kontrak antara beberapa versi aplikasi. Setiap rilis harus menjawab dua pertanyaan:

  • Apakah binary baru bisa bekerja dengan skema lama?
  • Apakah binary lama masih bisa bertahan jika sebagian node sudah memakai skema hasil migrasi?

Jika salah satu jawabannya “tidak”, maka rollout tanpa downtime menjadi berisiko.

2. Pisahkan perubahan menjadi expand lalu contract

Strategi expand-contract adalah cara paling aman untuk evolusi skema bertahap:

  1. Expand: tambahkan struktur baru yang belum dipakai penuh, misalnya kolom baru, tabel baru, index baru, atau trigger transisi.
  2. Deploy aplikasi kompatibel ganda: binary dapat membaca format lama dan baru.
  3. Backfill atau sinkronisasi data: isi data baru secara bertahap bila diperlukan.
  4. Aktifkan feature flag: mulai arahkan traffic logika baru setelah observability menunjukkan sehat.
  5. Contract: hapus ketergantungan lama setelah semua node dan data konsisten.

Untuk SQLite, fase contract sebaiknya dianggap sebagai rilis terpisah, bukan digabung dalam deployment yang sama.

3. Hindari migration yang memaksa kompatibilitas tunggal

Perubahan berikut paling sering membuat rollback sulit:

  • Menghapus kolom yang masih dibaca binary lama.
  • Mengganti nama kolom/tabel tanpa lapisan kompatibilitas.
  • Mengubah semantik nilai tanpa migrasi data yang jelas.
  • Menambahkan constraint baru yang langsung menggagalkan write dari binary lama.

Jika perubahan seperti ini tidak bisa dihindari, perlakukan rollout sebagai operasi terjadwal dengan jendela risiko yang jelas, bukan “zero-downtime” palsu.

Pola expand-contract untuk SQLite

Contoh kasus: memecah satu kolom menjadi dua kolom baru

Misalnya tabel users awalnya punya kolom full_name, lalu aplikasi baru ingin memakai first_name dan last_name.

Fase expand:

ALTER TABLE users ADD COLUMN first_name TEXT;
ALTER TABLE users ADD COLUMN last_name TEXT;

Setelah itu, deploy binary yang:

  • Masih bisa membaca full_name jika first_name/last_name kosong.
  • Saat write, mengisi format lama dan baru jika perlu.

Contoh pseudocode aplikasi:

function saveUserName(userId, firstName, lastName) {
  const fullName = [firstName, lastName].filter(Boolean).join(" ");

  db.exec(
    "UPDATE users SET first_name = ?, last_name = ?, full_name = ? WHERE id = ?",
    [firstName, lastName, fullName, userId]
  );
}

function readUserName(row) {
  if (row.first_name || row.last_name) {
    return {
      firstName: row.first_name || "",
      lastName: row.last_name || ""
    };
  }

  return parseFullName(row.full_name);
}

Fase backfill: isi data lama ke kolom baru dengan job terkontrol, bukan saat request path jika datanya besar.

UPDATE users
SET first_name = substr(full_name, 1, instr(full_name || ' ', ' ') - 1),
    last_name = substr(full_name || ' ', instr(full_name || ' ', ' ') + 1)
WHERE full_name IS NOT NULL
  AND (first_name IS NULL OR last_name IS NULL);

Contoh SQL di atas hanya ilustrasi. Untuk parsing nama yang kompleks, lebih aman lakukan backfill di kode aplikasi agar aturannya eksplisit dan mudah diuji.

Fase contract: setelah semua binary lama berhenti dan semua pembacaan sudah memakai kolom baru, baru hapus ketergantungan pada full_name. Pada SQLite, penghapusan kolom atau perubahan struktur besar sering memerlukan pembuatan tabel baru, salin data, lalu rename. Karena itu contract lebih berisiko dan sebaiknya dipisah sebagai rilis sendiri.

Compatibility check saat startup

Karena SQLite tidak memberi mekanisme “edition” aplikasi, Anda perlu membuat cek kompatibilitas eksplisit saat startup. Tujuannya sederhana: jangan biarkan binary berjalan pada file DB yang tidak didukung.

Apa yang perlu dicek

  • Versi skema aplikasi yang diharapkan binary.
  • Versi skema saat ini pada database.
  • Rentang versi yang kompatibel, bukan hanya satu angka.
  • Status migration parsial, misalnya flag bahwa backfill belum selesai.

Pola yang umum adalah membuat tabel metadata, misalnya app_schema_meta:

CREATE TABLE IF NOT EXISTS app_schema_meta (
  key TEXT PRIMARY KEY,
  value TEXT NOT NULL
);

INSERT OR IGNORE INTO app_schema_meta(key, value) VALUES
  ('schema_version', '2026-01-expand-user-name'),
  ('min_app_version', '2.3.0'),
  ('migration_state', 'stable');

Saat startup, aplikasi membaca metadata ini dan memutuskan apakah boleh lanjut.

function assertSchemaCompatibility(db, appVersion, supportedSchemas) {
  const meta = loadSchemaMeta(db);

  if (!supportedSchemas.includes(meta.schema_version)) {
    throw new Error(
      `Skema ${meta.schema_version} tidak didukung oleh app ${appVersion}`
    );
  }

  if (meta.migration_state === 'migrating') {
    throw new Error('Database sedang dalam state migrasi parsial');
  }
}

Lebih baik proses gagal cepat saat startup daripada berhasil hidup tetapi mulai melempar error no such column beberapa menit kemudian.

Kapan startup harus fail-closed

Gunakan pendekatan fail-closed untuk node penulis dan job background penting. Untuk node pembaca sekunder, Anda bisa lebih longgar jika aplikasi benar-benar mampu berjalan dalam mode kompatibilitas baca-saja. Namun keputusan ini harus eksplisit, bukan asumsi.

Backup atau snapshot sebelum migrasi

Pada SQLite, backup sebelum migrasi bukan tambahan opsional. Ini adalah syarat minimum agar rollback database tetap mungkin.

Pilihan yang realistis

  • Snapshot filesystem/volume jika DB berada di volume yang mendukung snapshot cepat.
  • Salinan file database dan file journaling terkait dengan prosedur yang konsisten.
  • Backup menggunakan mekanisme SQLite melalui library atau tooling yang mendukung backup konsisten saat database aktif.

Yang penting bukan format backupnya, tetapi konsistensi backup. Jangan hanya menyalin file database sembarang saat ada write aktif tanpa memahami mode journaling dan locking yang dipakai. Jika prosedur backup tim belum jelas, dokumentasikan dan uji restore-nya sebelum hari rilis.

Apa yang harus diverifikasi sebelum lanjut

  • Backup berhasil dibuat.
  • Restore pernah diuji, bukan sekadar diasumsikan.
  • Versi backup diberi penanda rilis dan timestamp.
  • Lokasi backup tidak bergantung pada node yang sama jika node itu gagal total.

Rollback binary tanpa backup database hanya aman jika migration sepenuhnya backward-compatible. Jika tidak, Anda sedang bertaruh bahwa binary lama masih mengerti file DB baru.

Canary deployment pada node pembaca

Jika arsitektur Anda punya node pembaca, replika file, atau layanan baca terpisah, lakukan canary di sana terlebih dahulu. Tujuannya bukan menguji performa maksimal, tetapi menguji asumsi kompatibilitas terhadap query nyata.

Urutan rollout yang disarankan

  1. Ambil backup/snapshot.
  2. Deploy binary baru ke satu node pembaca.
  3. Jalankan startup compatibility check.
  4. Observasi error query, lock contention, dan latensi.
  5. Jika sehat, lanjutkan ke node pembaca lain.
  6. Baru kemudian migrasikan dan deploy jalur penulis.

Canary pada pembaca berguna karena banyak masalah muncul lebih cepat di sana: query memilih kolom yang hilang, ORM mengasumsikan index tertentu, atau parser hasil query gagal karena bentuk data berubah.

Batasannya

Canary baca tidak akan menemukan semua masalah write path. Misalnya constraint baru, trigger baru, atau race condition saat backfill baru terlihat ketika node penulis mulai aktif. Karena itu observability write tetap wajib.

Feature flag untuk mengontrol aktivasi perilaku baru

Jangan aktifkan logika baru bersamaan dengan deploy binary dan migrasi jika tidak perlu. Feature flag memberi Anda rem darurat tanpa redeploy.

Yang sebaiknya dipisahkan ke feature flag

  • Pembacaan dari kolom/tabel baru.
  • Penulisan ke struktur baru.
  • Validasi atau constraint aplikasi yang bergantung pada data hasil backfill.
  • Job background yang mulai memakai skema baru.

Pola aman yang umum:

  1. Deploy skema expand.
  2. Deploy binary kompatibel ganda dengan feature flag mati.
  3. Aktifkan dual-write.
  4. Backfill data lama.
  5. Aktifkan read dari skema baru pada sebagian traffic atau node.
  6. Setelah stabil, matikan fallback lama.

Dengan pola ini, rollback sering cukup dilakukan dengan mematikan flag, bukan langsung memulihkan backup.

Observability: apa yang harus dipantau selama rollout

Tanpa observability, migrasi SQLite sering terlihat “baik-baik saja” sampai error mulai menumpuk di log. Minimal, siapkan metrik dan alert berikut.

Metrik inti

  • Jumlah query gagal per jenis error.
  • Error class spesifik skema, misalnya no such table, no such column, table has no column named, datatype mismatch, constraint failed.
  • Latency query p95/p99 untuk read dan write.
  • Frekuensi lock/busy yang meningkat selama migrasi atau backfill.
  • Status migration state dan progres backfill.
  • Versi binary per node dan versi skema database.

Log yang perlu ada

  • Log startup berisi hasil compatibility check.
  • Log eksekusi migration dengan durasi dan hasil tiap langkah.
  • Log feature flag saat diubah.
  • Log query error yang menyertakan nama operasi, bukan hanya SQL mentah.

Alert yang masuk akal

  • Error query terkait skema naik di atas baseline normal.
  • Node baru gagal startup karena incompatibility.
  • Backfill berhenti di tengah atau tidak bergerak.
  • Busy/lock timeout melonjak setelah migration.
  • Rollback otomatis atau manual dipicu.

Untuk tim kecil, Anda tidak perlu sistem observability yang rumit. Yang penting adalah bisa menjawab cepat: node mana yang menjalankan binary apa, database sedang di versi skema apa, dan error pertama muncul di operasi apa.

Deteksi query gagal yang relevan untuk migrasi

Banyak tim hanya mengandalkan status migration selesai. Itu tidak cukup. Yang perlu Anda tahu adalah apakah aplikasi mulai menjalankan query yang tidak lagi valid.

Buat klasifikasi sederhana terhadap error database:

function classifySqliteError(message) {
  const text = String(message || '').toLowerCase();

  if (text.includes('no such table')) return 'schema_missing_table';
  if (text.includes('no such column')) return 'schema_missing_column';
  if (text.includes('has no column named')) return 'schema_write_unknown_column';
  if (text.includes('constraint failed')) return 'constraint_failed';
  if (text.includes('database is locked') || text.includes('database busy')) return 'lock_contention';
  return 'other_db_error';
}

Klasifikasi ini bisa dipakai untuk:

  • mengirim metrik per tipe kegagalan,
  • membuat alert khusus migrasi,
  • memutuskan apakah feature flag perlu dimatikan,
  • mempercepat postmortem karena pola error sudah terkelompok.

Strategi rollback bila binary baru tidak cocok dengan skema lama

Rollback yang realistis harus mempertimbangkan dua sumbu: binary aplikasi dan file database. Jangan hanya menyiapkan rollback aplikasi.

Skenario 1: Binary baru gagal, skema belum berubah

Ini kasus paling mudah. Rollback cukup dengan:

  1. Hentikan rollout.
  2. Kembalikan binary ke versi sebelumnya.
  3. Verifikasi node lama lolos startup check dan melayani traffic normal.

Skenario 2: Skema expand sudah diterapkan, binary lama masih kompatibel

Ini target ideal dari strategi expand-contract. Langkah rollback:

  1. Matikan feature flag perilaku baru.
  2. Hentikan node binary baru.
  3. Deploy kembali binary lama.
  4. Biarkan struktur expand tetap ada sementara.
  5. Jadwalkan investigasi sebelum mencoba rollout lagi.

Selama perubahan hanya menambah struktur yang diabaikan binary lama, rollback dapat dilakukan tanpa restore backup.

Skenario 3: Skema berubah dan binary lama tidak kompatibel

Ini skenario berbahaya. Langkah yang harus disiapkan:

  1. Masuk ke mode terbatas atau maintenance internal jika perlu untuk mencegah write liar.
  2. Pastikan semua proses penulis baru berhenti.
  3. Restore database dari backup/snapshot pra-migrasi.
  4. Deploy binary lama.
  5. Verifikasi integritas data dan jalankan smoke test utama.

Jika setelah migration ada write baru yang hanya hidup di skema baru, restore backup bisa menyebabkan kehilangan data terbaru. Karena itu, sebelum rollback penuh, tentukan apakah Anda perlu mengekstrak write terbaru dulu atau menerima kehilangan data terbatas. Keputusan ini harus ada di runbook, bukan dipikirkan saat insiden berlangsung.

Aturan praktis

Jika rollback membutuhkan restore database, maka migration Anda bukan backward-compatible. Itu bukan selalu salah, tetapi risiko operasionalnya lebih tinggi dan harus diumumkan jelas pada rilis.

Checklist rilis perubahan skema SQLite

Berikut checklist operasional yang bisa langsung dipakai tim kecil.

Sebelum rilis

  • Perubahan skema dipetakan sebagai expand atau contract.
  • Binary baru diuji terhadap skema lama.
  • Jika relevan, binary lama diuji terhadap skema hasil expand.
  • Startup compatibility check sudah ada dan diuji gagal/suksesnya.
  • Feature flag untuk read/write baru siap dipakai.
  • Backup/snapshot pra-migrasi berhasil dibuat.
  • Restore backup pernah diuji.
  • Metrik dan alert query gagal aktif.
  • Runbook rollback sudah ditinjau.

Saat rilis

  • Jalankan migration expand.
  • Verifikasi metadata versi skema berubah sesuai rencana.
  • Deploy canary ke node pembaca.
  • Pantau error no such column, constraint failed, dan lock contention.
  • Deploy ke node lain secara bertahap.
  • Aktifkan dual-write atau read path baru melalui feature flag.
  • Jalankan backfill jika diperlukan.
  • Konfirmasi progres backfill selesai dan tervalidasi.

Setelah rilis

  • Pantau error dan latensi selama jendela observasi.
  • Dokumentasikan jika ada anomali kecil, meski tidak jadi insiden.
  • Tunda fase contract sampai semua binary lama benar-benar hilang.
  • Hapus fallback lama hanya setelah bukti observability cukup.

Contoh struktur runbook insiden

Runbook tidak perlu panjang, tetapi harus langsung bisa dipakai saat gangguan. Struktur berikut cukup untuk banyak tim kecil.

1. Ringkasan insiden

  • Nama insiden: incompatibility skema SQLite saat deployment.
  • Gejala utama: startup gagal, query no such column, atau write gagal.
  • Dampak: baca terganggu, write gagal, atau sebagian node tidak sehat.

2. Pemicu dan indikator

  • Alert query error skema naik.
  • Node baru gagal startup compatibility check.
  • Backfill macet atau migration state tidak kembali ke stable.

3. Langkah diagnosis cepat

  1. Cek versi binary per node.
  2. Cek nilai metadata skema pada database.
  3. Cek feature flag yang aktif.
  4. Ambil contoh error query pertama dan operasi yang terkena.
  5. Tentukan apakah masalah ada di read path, write path, atau startup.

4. Keputusan mitigasi

  • Jika hanya perilaku baru yang rusak: matikan feature flag.
  • Jika node canary gagal: hentikan rollout.
  • Jika binary lama masih kompatibel: rollback aplikasi saja.
  • Jika binary lama tidak kompatibel: aktifkan prosedur restore backup.

5. Langkah rollback rinci

  1. Bekukan rollout.
  2. Hentikan penulis baru.
  3. Jika aman, rollback binary.
  4. Jika perlu, restore snapshot pra-migrasi.
  5. Jalankan smoke test operasi kritis: login, read utama, write utama, job background.
  6. Konfirmasi alert kembali normal.

6. Data yang harus dikumpulkan untuk postmortem

  • Timeline deploy, migration, dan aktivasi feature flag.
  • Versi binary dan metadata skema pada tiap node.
  • Contoh error query dan frekuensinya.
  • Status backup dan hasil restore jika dilakukan.
  • Keputusan mitigasi yang diambil dan alasannya.

Postmortem ringan yang berguna

Tujuan postmortem bukan menyalahkan migration atau developer tertentu, tetapi memperbaiki sistem rilis. Untuk insiden SQLite terkait skema, pertanyaan yang berguna biasanya:

  • Asumsi kompatibilitas mana yang ternyata salah?
  • Apakah startup check terlalu longgar atau tidak ada?
  • Apakah feature flag seharusnya memisahkan aktivasi read dan write?
  • Apakah observability terlambat mendeteksi error?
  • Apakah backup ada tetapi restore tidak teruji?
  • Apakah fase contract dilakukan terlalu cepat?

Dari situ, keluarkan tindakan pencegahan yang kecil tetapi konkret, misalnya:

  • menambahkan tes integrasi cross-version,
  • mewajibkan checklist kompatibilitas di pull request migration,
  • memisahkan migration expand dan contract minimal satu rilis,
  • menambahkan metrik khusus untuk error skema.

Pencegahan jangka panjang untuk tim kecil

1. Simpan matriks kompatibilitas sederhana

Tidak perlu rumit. Cukup tabel internal seperti:

  • App v2.3 mendukung schema A dan A-expand.
  • App v2.4 mendukung schema A-expand dan B.
  • App v2.2 tidak boleh dipakai setelah schema A-expand.

Dokumen kecil seperti ini sering lebih berguna daripada mengandalkan ingatan tim.

2. Uji pasangan binary-skema, bukan hanya migration tunggal

Banyak test suite hanya memverifikasi bahwa migration dapat berjalan dari nol ke versi terbaru. Itu belum cukup. Tambahkan skenario:

  • binary baru terhadap DB lama,
  • binary lama terhadap DB hasil expand,
  • rollback binary setelah expand,
  • backfill yang terputus di tengah lalu dilanjutkan.

3. Hindari contract dalam rilis yang sama

Untuk tim kecil, ini aturan yang sangat membantu. Menunda penghapusan struktur lama satu rilis hampir selalu lebih murah daripada menangani insiden rollback yang rumit.

4. Tandai migration yang irreversibel

Jika ada perubahan yang membuat restore backup menjadi satu-satunya jalan mundur, beri label eksplisit pada tiket, pull request, dan catatan rilis internal. Dengan begitu, semua orang paham risikonya sebelum tombol deploy ditekan.

Penutup

Deployment aman saat skema SQLite berevolusi tanpa downtime bisa dicapai, tetapi bukan dengan mengandalkan migration saja. Kuncinya adalah kompatibilitas lintas versi yang dirancang sengaja: expand-contract, feature flag, startup compatibility check, backup yang benar-benar bisa direstore, observability yang fokus pada error skema, serta rollback yang mempertimbangkan binary dan file database sekaligus.

Jika Anda hanya mengambil satu aturan dari artikel ini, ambil yang ini: jangan pernah membuat binary lama dan baru saling berebut interpretasi terhadap file SQLite tanpa kontrak kompatibilitas yang jelas. Sebagian besar insiden deployment skema pada SQLite berawal dari asumsi bahwa migration selesai berarti sistem aman. Dalam praktiknya, yang menentukan aman atau tidak adalah apakah seluruh proses rollout, rollback, dan observability sudah dirancang untuk versi yang hidup berdampingan.