# Duty Roster Migrations

Catatan eksekusi 6 file migrasi duty roster di [`migrations/`](../migrations/).

## Konfigurasi Tool Migrasi

Tool migrasi yang dipakai project ini adalah `goose` ([Makefile:5](../Makefile#L5)) dengan direktori target:

```makefile
GOOSE_DIR ?= migrations/goose
```

**Konsekuensi:** `make migrate-up` hanya menjalankan file di [`migrations/goose/`](../migrations/goose/). Semua file `.sql` di root [`migrations/`](../migrations/) — termasuk 6 file duty roster — **tidak otomatis dieksekusi**. Mereka berfungsi sebagai script SQL ad-hoc / log historis dan harus dijalankan manual (`mysql < file.sql`) atau dipindahkan ke `migrations/goose/` dengan format `-- +goose Up` / `-- +goose Down`.

## Urutan Eksekusi (by Timestamp)

Goose mengurutkan file by filename ascending. Untuk dua file dengan timestamp sama (`20260502`), urutannya alfabetis: `duty` < `purge`.

```
1. 20260408_hems_duty_roster_crews_confirm_at.sql
2. 20260429_rename_duty_roster_tables.sql
3. 20260430_normalize_duty_roster_legacy.sql
4. 20260502_duty_roster_permissions_unification.sql
5. 20260502_purge_legacy_hems_duty_roster_permissions.sql
6. 20260504_duty_roster_drop_unique_base_date.sql
```

## Efek per File

| # | File | Efek di DB |
|---|---|---|
| 1 | [`20260408_hems_duty_roster_crews_confirm_at.sql`](../migrations/20260408_hems_duty_roster_crews_confirm_at.sql) | `ALTER TABLE hems_duty_roster_crews ADD COLUMN confirm_at DATETIME NULL` (nama tabel lama). Idempoten via `IF NOT EXISTS`. |
| 2 | [`20260429_rename_duty_roster_tables.sql`](../migrations/20260429_rename_duty_roster_tables.sql) | `RENAME TABLE hems_duty_rosters → duty_rosters` dan `hems_duty_roster_crews → duty_roster_crews`. Pakai guard cek `information_schema.TABLES` jadi idempoten. |
| 3 | [`20260430_normalize_duty_roster_legacy.sql`](../migrations/20260430_normalize_duty_roster_legacy.sql) | (a) Rename tabel **lagi** dengan guard sama (no-op kalau #2 sukses). (b) Rename kolom `hems_base_id → base_id` di `duty_rosters`. (c) Ensure `confirm_at` ada di `duty_roster_crews` baru (dan di tabel lama jika masih ada). (d) Rewire FK `roster_crews.roster_id` & `duty_roster_other_persons.roster_id` supaya reference `duty_rosters` (bukan `hems_duty_rosters`). |
| 4 | [`20260502_duty_roster_permissions_unification.sql`](../migrations/20260502_duty_roster_permissions_unification.sql) | Insert 4 permission canonical: `duty_roster.{create,read,update,delete}` (skip via `WHERE NOT EXISTS`). Grant ke role yang punya legacy `hems_duty_roster.*` ke canonical baru, anti-duplikasi via `LEFT JOIN ... NOT EXISTS`. **Tidak menghapus legacy.** |
| 5 | [`20260502_purge_legacy_hems_duty_roster_permissions.sql`](../migrations/20260502_purge_legacy_hems_duty_roster_permissions.sql) | (a) Re-insert canonical (no-op jika #4 sudah jalan). (b) Re-grant (no-op). (c) `DELETE FROM role_permissions WHERE permission.key IN (legacy)`. (d) `DELETE FROM permissions WHERE key IN (legacy)`. |
| 6 | [`20260504_duty_roster_drop_unique_base_date.sql`](../migrations/20260504_duty_roster_drop_unique_base_date.sql) | `DROP INDEX idx_roster_base_date` (sebelumnya unique). `CREATE INDEX idx_roster_base_date ON duty_rosters(base_id, duty_date)` non-unique. **Konsekuensi aplikasi**: schema tidak lagi mencegah multiple roster di pasangan `(base_id, duty_date)` yang sama — kode handler harus mendukung skenario ini (lihat [`ensureRosterDates`](../internal/transport/http/handlers/duty_roster_handler.go) yang sebelumnya dedup-by-date). |

## Overlap & Redundansi

- **#2 ⊂ #3** — File `0430_normalize_legacy` sudah include semua rename dari `0429` dengan guard yang sama. File `0429` secara fungsional redundant; akan no-op jika dijalankan setelah `0430`.
- **#4 ⊂ #5** — File `0502_purge_legacy` adalah superset dari `0502_unification`. Insert + grant identik, lalu tambah `DELETE`. File `0502_unification` redundant.

## Idempotensi Re-run

| File | Idempoten? | Catatan |
|---|---|---|
| #1 | Ya | `ADD COLUMN IF NOT EXISTS` |
| #2 | Ya | Guard via `information_schema.TABLES` |
| #3 | Ya | Setiap `ALTER` dibungkus prepared statement dengan kondisi |
| #4 | Ya | `WHERE NOT EXISTS` untuk insert + `LEFT JOIN ... IS NULL` untuk grant |
| #5 | Ya | Sama dengan #4, ditambah `DELETE` (idempoten karena hanya hapus key legacy) |
| **#6** | **TIDAK** | `DROP INDEX idx_roster_base_date` tanpa guard. Re-run setelah index sudah di-drop + recreate (sebagai non-unique) akan **gagal** karena nama indexnya sama. Mestinya pakai prepared statement guard yang cek `information_schema.STATISTICS`. |

## Versi Minimal yang Setara

Dengan urutan dijalankan penuh, efek akhirnya cukup dicapai dengan 4 file:

```
0408_..._confirm_at        (kompat untuk env yang masih punya hems_duty_*)
0430_normalize_legacy      (rename tabel + kolom + FK + confirm_at — superset dari 0429)
0502_purge_legacy          (canonical perm + delete legacy — superset dari 0502_unification)
0504_drop_unique_base_date (butuh tambah guard agar idempoten)
```

Untuk env yang sudah pasti **fresh** (tanpa sisa `hems_duty_*` apa pun), kelima file di atas bisa digabung jadi satu file goose-format dengan branch yang relevan saja. **Sebelum melakukan ini, pastikan tidak ada DB target yang masih nyangkut di mid-state** — file individual lebih aman selama transisi.

## Bug yang Pernah Ditemukan Akibat #6

Setelah `idx_roster_base_date` jadi non-unique, beberapa baris kode aplikasi yang asumsi unique (`base_id`, `duty_date`) jadi salah:

- [`ensureRosterDates`](../internal/transport/http/handlers/duty_roster_handler.go) di handler GET `/duty-roster/get-all` & `/get-all/dt` sebelumnya pakai `map[string]R` keyed by date — multiple roster di tanggal sama di-overwrite, hanya 1 yang muncul. Sudah diperbaiki pakai `map[string][]R` (multi-value).

Pastikan setiap kode baru yang query/iterasi roster per `(base_id, duty_date)` menyadari bahwa kombinasi ini tidak lagi unique.
