Tiga Lapis Kontrak, Satu Typo dari HTTP 500
Validasi aplikasi dan skema openapi menerima tipe baru, kolom ENUM MySQL menolaknya. Error 1265 baru terlihat saat INSERT.
Ringkasan
Aplikasi mengalami error 1265 dari MySQL. Error ini disebabkan oleh perbedaan tipe data di tiga lapis yang berbeda. Perlu disinkronkan agar aplikasi berjalan lancar.
Pull request untuk fitur informasi per bidang lulus seluruh test. Daftar validTypes di aplikasi sudah menerima KEPEMUDAAN dan EKRAF, skema openapi mencantumkan keduanya, dan CI hijau. Satu INSERT manual ke staging membalas error 1265 dari MySQL: data truncated for column announcement_type.
Tidak ada pemeriksaan tipe yang menangkap ini. Kesalahan baru muncul ketika baris benar-benar ditulis ke tabel.
Anggapan pertama salah. Tipe baru bukan belum terdaftar. Ia terdaftar di dua tempat dan absen di tempat ketiga.
Tiga lapis yang tidak saling membaca
Kontrak tipe pengumuman hidup di tiga lokasi berbeda. Pertama, daftar validTypes di kode aplikasi. Kedua, enum di skema openapi yang dibaca klien. Ketiga, definisi kolom ENUM di tabel announcements. Dua lapis pertama ikut commit dan ikut ter-deploy bersama aplikasi. Lapis ketiga hanya berubah kalau ada file migration yang dijalankan.
Validasi aplikasi menentukan apa yang boleh dikirim. Kolom database menentukan apa yang boleh disimpan. Keduanya tidak saling membaca, jadi tidak ada mekanisme yang memaksa ketiganya sinkron selain disiplin menaruh semuanya dalam satu commit. Selama dua lapis pertama bergerak lebih dulu, aplikasi akan lolos validasi lalu menabrak dinding di lapis ketiga.
35 baris yang membuat CMS kosong
Sebelum commit ini, 35 baris di tabel announcements memiliki bidang bernilai NULL. Nilai NULL dibaca sebagai pengumuman global, sehingga staf yang login dengan filter bidang membuka CMS dan melihat daftar kosong. Datanya ada, filternya yang tidak pernah cocok.
Migration 000088 mengisi kolom itu untuk 26 baris: OLAHRAGA menjadi olahraga sebanyak 8 baris, BUDAYA menjadi budaya 6 baris, WISATA menjadi pariwisata 6 baris, dan PROMOSI menjadi ekraf 6 baris. Sembilan baris bertipe UMUM sengaja dibiarkan NULL karena memang bersifat global.
-- 000088 up
UPDATE announcements
SET bidang = 'olahraga'
WHERE announcement_type = 'OLAHRAGA' AND bidang IS NULL;
-- 000088 down
UPDATE announcements
SET bidang = NULL
WHERE announcement_type = 'OLAHRAGA' AND bidang = 'olahraga';
Klausa AND bidang IS NULL bukan hiasan. Tanpa WHERE, UPDATE menyentuh seluruh baris tabel, dan WHERE itulah yang menentukan baris mana yang berubah [5]. Guard tersebut sekaligus menjadi idempotensinya: baris yang sudah terisi tidak akan tersentuh lagi kalau migration dijalankan ulang setelah gagal di tengah jalan. Migration yang aman diulang tidak butuh tabel pelacak status.
Error 1265 Baru Muncul Saat INSERT
Migration 000089 menjalankan ALTER TABLE pada kolom announcement_type untuk menambahkan KEPEMUDAAN dan EKRAF. MySQL mengizinkan ALTER ENUM in-place hanya kalau anggota baru ditambahkan di akhir daftar dan ukuran penyimpanan tidak berubah [4]. Urutan anggota di file migration karena itu bukan preferensi gaya, melainkan syarat agar ALTER tidak menyalin ulang seluruh tabel.
Dengan STRICT_TRANS_TABLES aktif, nilai yang tidak ada di daftar ENUM tidak dipotong diam-diam menjadi string kosong. MySQL menolak seluruh INSERT dan mengembalikan error 1265. Perilaku ini yang membuat bug tersebut terlihat sebagai kegagalan total, bukan data rusak yang baru ketahuan berbulan-bulan kemudian.
golang-migrate menerapkan pasangan migration dalam urutan versi yang ketat [3], jadi 000088 selesai lebih dulu, baru 000089. Backfill di 000088 masih membaca nilai lama berhuruf besar, sehingga urutan ini harus dipertahankan. KEPEMUDAAN mulai dari nol baris karena tidak ada data lama yang perlu dipetakan ke sana, dan itu memang disengaja.
Down yang membalik tepat barisnya
Down migration 000088 tidak boleh hanya mengosongkan kolom. SET bidang = NULL tanpa filter akan menghapus pengisian yang sah dari commit lain. Pembalikan yang benar memakai dua kondisi yang sama dengan saat pengisian: tipe asal dan nilai hasil pemetaan. Baris yang dibalik persis baris yang disentuh, dan tidak ada yang lain.
Aturan yang diambil setelah kejadian ini sederhana. Satu commit harus menyentuh tiga lapis sekaligus, dan review wajib menjalankan satu INSERT nyata ke database dengan sql_mode yang sama seperti produksi. Test yang tidak pernah menyentuh database tidak bisa membuktikan apa pun tentang lapis ketiga.
---
Sumber: