Database Migration

Runbook penggunaan sistem migration AlurKerja — generate migration dari selisih skema (schema-diff) lalu apply ke database tujuan dengan runner release-aware ala Laravel. Termasuk cara pengecekan dan expected result dari simulasi end-to-end.

A. Ringkasan & Definition of Done

Sistem migration AlurKerja terdiri dari dua alat yang terpisah tugasnya:

AlatPerintahTugas
schema-diffmake schema-diffMenghasilkan file migration dari selisih skema baseline vs DB server.
migratemake migrateMenerapkan file migration ke database tujuan dan mencatat versinya.

Halaman ini fokus pada penggunaan kedua alat — terutama runner migrate — sampai cara pengecekan dan expected result yang sudah divalidasi lewat simulasi.

Model pelacakannya mengikuti migration Laravel: setiap file migration dicatat per nama, lalu dikelompokkan ke dalam release (padanan konsep batch di Laravel). Satu release = satu unit rollback.

Definition of Done (DoD) — migration dianggap selesai bila:

  • make migrate selesai tanpa error dan menampilkan Selesai. N migration baru ter-apply (atau Nothing to migrate bila memang sudah sinkron).
  • make migrate-status menampilkan seluruh migration dalam status [x] ter-apply, 0 menunggu.
  • Jumlah objek (tabel/view/function) di database tujuan sesuai dengan yang diharapkan.

schema-diff dan migrate berada di repository alurkerja-release pada folder scripts/. Semua contoh command di halaman ini dijalankan dari root repository tersebut.

B. Prasyarat

Langkah 1–3 memastikan environment siap. Jangan lanjut sebelum semua terpenuhi.

1. Verifikasi klien psql terpasang

Aksi

psql --version

Expected output

psql (PostgreSQL) 14.x atau lebih baru.

Jika gagal: install paket klien PostgreSQL (postgresql-client di Debian/Ubuntu, brew install libpq di macOS).

2. Verifikasi akses ke database tujuan

Aksi

PGPASSWORD='<DB_PASS>' psql -X "host=<DB_HOST> port=<DB_PORT> user=<DB_USER> dbname=<DB_NAME>" -tAc 'SELECT 1'

Expected output

Angka 1 tercetak tanpa error.

Jika gagal: pastikan host/port benar, kredensial valid, dan (bila database di jaringan internal) VPN aktif.

3. Verifikasi folder migration tersedia

Aksi

ls migrations/*/*/ | head

Expected output

Terlihat folder per schema dan per release, mis. migrations/public/2026.07.3.1/ berisi file *.up.sql dan *.down.sql.

Jika kosong: file migration belum di-generate. Jalankan make schema-diff lebih dulu (lihat halaman schema-diff).

C. Konsep: struktur folder & tabel pelacak

Struktur folder

File migration ditata per schema, lalu per release:

migrations/
  public/
    2026.01.1.1/    baseline  — dari config/public.sql (1 file)
    2026.07.3.1/    release   — selisih baseline vs server (31 file per konteks)
  camunda/
    2026.01.1.1/    baseline  — dari config/camunda.sql
    2026.07.3.1/    release   — hanya tabel act_* + trigger (3 file)

Setiap migration punya pasangan *.up.sql (maju) dan *.down.sql (rollback). Nama file berprefix timestamp 14 digit, mis. 20260716090100_create_access_logs.up.sql.

Tabel pelacak migrate.migrations

Runner mencatat migration yang sudah diterapkan di tabel berikut, dibuat otomatis di schema terpisah migrate (bukan public):

CREATE TABLE migrate.migrations (
    id          bigserial   PRIMARY KEY,
    schema_name text        NOT NULL,   -- 'public' | 'camunda'
    migration   text        NOT NULL,   -- '20260716090100_create_access_logs'
    release     text        NOT NULL,   -- '2026.07.3.1'  ← "batch" ala Laravel
    applied_at  timestamptz NOT NULL DEFAULT now(),
    UNIQUE (schema_name, migration)
);

Tabel pelacak sengaja ditaruh di schema migrate, bukan public. Baseline public menjalankan CREATE SCHEMA public yang destruktif — bila tabel pelacak berada di public, ia akan ikut terhapus saat skema dibangun ulang.

Tiga aturan yang wajib dipegang

  1. Pelacakan berbasis (schema, migration). Timestamp restart di tiap schema (public dan camunda bisa punya 20260716090000 yang sama), jadi nama migration hanya unik di dalam satu schema. Runner sudah menangani ini; jangan pernah menggabung (flatten) kedua schema ke satu folder.
  2. Urutan schema: public dulu, baru camunda. camunda memiliki foreign key ke public (mis. camunda.tenant_certificatespublic.tenants). Runner menerapkannya dalam urutan ini; rollback memakai urutan terbalik.
  3. Release yang sudah deploy = beku. Jangan pernah regenerate release yang sudah diterapkan di server. Perubahan berikutnya masuk ke release baru (make schema-diff otomatis menaikkan nomor release).

D. Konfigurasi koneksi

Runner membaca koneksi dari scripts/migrate.env (di-gitignore, tidak ikut ter-commit).

4. Siapkan scripts/migrate.env

Aksi

cp scripts/migrate.env.example scripts/migrate.env
# lalu edit isinya

Isi minimal:

scripts/migrate.env
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASS=<password>
DB_NAME=<nama-database>

# Urutan schema WAJIB benar: public dulu (camunda punya FK ke public).
SCHEMAS=public camunda

Jangan pernah commit scripts/migrate.env — file ini berisi password. File sudah terdaftar di .gitignore.

E. Perintah yang tersedia

PerintahAlias makeFungsi
./scripts/migrate.sh upmake migrateApply semua *.up.sql yang belum tercatat (public → camunda).
./scripts/migrate.sh statusmake migrate-statusDaftar migration beserta status ter-apply / menunggu.
./scripts/migrate.sh rollbackmake migrate-rollbackMundurkan release terakhir (semua schema, urutan terbalik).
./scripts/migrate.sh unlockLepas paksa lock sisa proses yang mati.

Sifat penting runner:

  • Idempoten — migration yang sudah tercatat otomatis dilewati; up aman dijalankan berulang.
  • Atomik per file — tiap *.up.sql diterapkan dengan psql --single-transaction digabung dengan pencatatannya dalam satu transaksi. Bila gagal di tengah, seluruhnya di-rollback dan tidak tercatat, sehingga cukup dijalankan ulang setelah masalah diperbaiki.
  • Terkunci — dua proses up/rollback tidak bisa jalan bersamaan di database yang sama (lock berbasis baris, dengan pembersihan lock basi otomatis).

F. Simulasi end-to-end & expected result

Berikut simulasi lengkap yang sudah divalidasi pada Postgres lokal (container some-postgres, PostgreSQL 18 dengan pgvector) menggunakan database uji alur_migrate_test. Ikuti untuk memastikan sistem berfungsi sebelum dipakai ke database sungguhan.

Simulasi ini memakai database khusus uji alur_migrate_test yang dibuat kosong lalu dihapus lagi. Jangan menjalankan langkah 5 pada database berisi data.

5. Siapkan database uji kosong lalu apply (up)

Aksi

# 5a. buat database uji kosong
PGPASSWORD='<DB_PASS>' psql -X "host=<DB_HOST> port=<DB_PORT> user=<DB_USER> dbname=postgres" \
  -c "DROP DATABASE IF EXISTS alur_migrate_test WITH (FORCE)" \
  -c "CREATE DATABASE alur_migrate_test"

# 5b. buang schema public bawaan — baseline public menjalankan CREATE SCHEMA public
PGPASSWORD='<DB_PASS>' psql -X "host=<DB_HOST> port=<DB_PORT> user=<DB_USER> dbname=alur_migrate_test" \
  -c "DROP SCHEMA IF EXISTS public CASCADE"

# 5c. terapkan seluruh migration
DB_NAME=alur_migrate_test ./scripts/migrate.sh up

Expected output

Runner menerapkan seluruh migration berurutan — public dulu (baseline + release), baru camunda:

==> migrate up  (postgres@localhost:5432/alur_migrate_test)
  public   2026.01.1.1  20260101000000_baseline_public_schema ... OK
  public   2026.07.3.1  20260716090000_enable_extensions_and_types ... OK
  public   2026.07.3.1  20260716090100_create_access_logs ... OK
  ... (31 migration release public) ...
  camunda  2026.01.1.1  20260101000000_baseline_camunda_schema ... OK
  camunda  2026.07.3.1  20260716090000_add_constraints_and_indexes_to_existing_tables ... OK
  camunda  2026.07.3.1  20260716090100_create_functions_views_triggers ... OK
  camunda  2026.07.3.1  20260716090200_review_manual ... OK
  Selesai. 36 migration baru ter-apply.

Yang harus benar: total 36 migration ter-apply — 32 public (1 baseline + 31 release) dan 4 camunda (1 baseline + 3 release).

Langkah 5a–5b (drop public bawaan) hanya diperlukan saat membangun database dari nol, karena baseline public menjalankan CREATE SCHEMA public. Pada database yang sudah berisi skema, cukup jalankan up (langkah 5c) tanpa menyentuh schema.

Jika gagal di tengah: migration yang gagal di-rollback (transaksi tunggal) dan tidak tercatat. Perbaiki penyebabnya, lalu jalankan ./scripts/migrate.sh up — yang sudah sukses tidak diulang.

6. Uji idempotensi (up kedua kali)

Aksi

DB_NAME=alur_migrate_test ./scripts/migrate.sh up

Expected output

==> migrate up  (postgres@localhost:5432/alur_migrate_test)
  Nothing to migrate. Semua sudah ter-apply.

Menjalankan up berkali-kali aman dan tidak mengubah apa pun bila skema sudah sinkron.

7. Periksa status

Aksi

DB_NAME=alur_migrate_test ./scripts/migrate.sh status

Expected output (dipersingkat)

==> status  (alur_migrate_test)

  public/2026.01.1.1
    [x] 20260101000000_baseline_public_schema

  public/2026.07.3.1
    [x] 20260716090000_enable_extensions_and_types
    [x] 20260716090100_create_access_logs
    ...

  camunda/2026.07.3.1
    [x] 20260716090100_create_functions_views_triggers
    [x] 20260716090200_review_manual

  36 ter-apply, 0 menunggu.

Yang harus benar: 36 ter-apply, 0 menunggu — semua migration bertanda [x].

G. Cara pengecekan (verifikasi hasil)

Selain status, verifikasi langsung ke database bahwa objek benar-benar terbentuk.

8. Hitung objek per schema

Aksi

PGPASSWORD='<DB_PASS>' psql -X "host=<DB_HOST> port=<DB_PORT> user=<DB_USER> dbname=alur_migrate_test" -c "
  SELECT n.nspname AS schema,
         count(*) FILTER (WHERE c.relkind='r') AS tabel
  FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
  WHERE n.nspname IN ('public','camunda')
  GROUP BY n.nspname ORDER BY 1;"

Expected output

 schema  | tabel
---------+-------
 camunda |    49
 public  |   109

Yang harus benar: public 109 tabel, camunda 49 tabel (persis 49 tabel act_* Camunda).

9. Periksa isi tabel pelacak per release

Aksi

PGPASSWORD='<DB_PASS>' psql -X "host=<DB_HOST> port=<DB_PORT> user=<DB_USER> dbname=alur_migrate_test" -c "
  SELECT schema_name, release, count(*)
  FROM migrate.migrations GROUP BY 1,2 ORDER BY 1,2;"

Expected output

 schema_name |   release   | count
-------------+-------------+-------
 camunda     | 2026.01.1.1 |     1
 camunda     | 2026.07.3.1 |     3
 public      | 2026.01.1.1 |     1
 public      | 2026.07.3.1 |    31

H. Rollback

10. Mundurkan release terakhir

Aksi

DB_NAME=alur_migrate_test ./scripts/migrate.sh rollback

Expected output (dipersingkat)

==> rollback  release 2026.07.3.1  (alur_migrate_test)
  camunda  20260716090200_review_manual ... OK
  camunda  20260716090100_create_functions_views_triggers ... OK
  camunda  20260716090000_add_constraints_and_indexes_to_existing_tables ... OK
  public   20260716093000_review_manual ... OK
  ... (release public, urutan mundur) ...
  public   20260716090000_enable_extensions_and_types ... OK
  Selesai. 34 migration di release 2026.07.3.1 di-rollback.

Yang harus benar:

  • 34 migration release 2026.07.3.1 di-rollback (31 public + 3 camunda) — urutan terbalik: camunda dulu, lalu public dari timestamp terbesar ke terkecil.
  • Setelah rollback, status menunjukkan 2 ter-apply, 34 menunggu (hanya dua baseline yang tersisa).
  • Hitung objek kembali ke kondisi baseline: public 60 tabel (turun dari 109), camunda 49 (release camunda hanya menambah constraint/trigger, bukan tabel, sehingga jumlah tabel tidak berubah).

rollback selalu menyasar release terakhir yang tercatat (ditentukan lewat urutan versi, bukan leksikografis). Jalankan berulang untuk mundur release demi release.

I. Troubleshooting

GejalaPenyebabAksi
Runner lain sedang jalan (lock dipegang: ...)Proses up/rollback sebelumnya mati sebelum melepas lock.Pastikan tidak ada proses lain berjalan, lalu ./scripts/migrate.sh unlock. Lock lebih tua dari LOCK_TIMEOUT (default 2 jam) dibersihkan otomatis.
Migration gagal di tengah upError SQL pada satu file.File itu di-rollback (transaksi tunggal), tidak tercatat. Perbaiki penyebab lalu up lagi — yang sudah sukses dilewati.
schema "public" already exists saat up di DB kosongBaseline public menjalankan CREATE SCHEMA public, sementara DB baru sudah punya public bawaan.Buang schema public bawaan sebelum up: psql ... -c "DROP SCHEMA IF EXISTS public CASCADE" (lihat langkah 5b).
extension "vector" is not availableServer AlurKerja memakai extension vector (kolom knowledge_chunks.embedding); image Postgres polos tidak membawanya.Pakai image ber-pgvector (mis. pgvector/pgvector:pg18) atau install paket postgresql-<versi>-pgvector.

J. Catatan untuk database produksi

Jangan menjalankan langkah penyiapan DB kosong (langkah 5a–5b) pada database produksiDROP DATABASE/DROP SCHEMA public akan menghapus seluruh data. Pada produksi cukup jalankan up.

Untuk server on-premise yang sudah berisi skema (sudah pada kondisi baseline), file baseline tidak boleh diterapkan ulang — cukup terapkan release baru saja. Alur amannya:

  1. Tandai baseline sebagai sudah-applied di tabel migrate.migrations tanpa menjalankan SQL-nya (agar up hanya menerapkan release di atas baseline).
  2. Jalankan make migrate-status untuk memastikan hanya release baru yang berstatus menunggu.
  3. Jalankan make migrate.

Subcommand khusus untuk menandai baseline sebagai sudah-applied sedang disiapkan. Sampai tersedia, lakukan langkah 1 secara manual bersama tim yang menyiapkan runner sebelum menjalankan up ke server produksi yang sudah hidup.