The Enum Value That Only Failed at INSERT Time
App validation and the OpenAPI schema accepted the new enum values; the MySQL column refused them. Error 1265 surfaced only at INSERT.
TL;DR
A MySQL error occurred due to data truncation. The issue arose from a mismatch between the application and database. A migration was needed to append new values to the column.
The pull request looked finished. The application's list of valid announcement types already carried the two new values, the OpenAPI document listed them, and the CMS form offered them to editors. A reviewer then ran the write path against staging and received MySQL error 1265: a data truncation complaint naming a column that appeared in no diff.
The working assumption had been that the new types were missing somewhere.
They were not missing. They existed in two of the three places that had to agree.
Three places, one contract
A typed value crosses three layers on its way to storage. The application holds a validTypes array that rejects bad input before any query runs. The API specification repeats the same set as an enum so clients generate correct code. The database column holds a third copy, and that copy is the only one enforced at runtime.
The first two layers live in different files, get reviewed by different people, and are cheap to change. The third does not move unless a migration moves it. That asymmetry is the whole problem: the app and the specification can reach agreement in a single afternoon while the column stays behind, and nothing in the toolchain compares them. A type checker reading the application code sees a valid union. It has no access to the column definition, so it stays silent, correctly.
The failure therefore lands at the last possible moment, when a user submits a form and the write is rejected.
The column rejects at insert time
With STRICT_TRANS_TABLES enabled, an out-of-range value produces error 1265 rather than a silent truncation. Under a non-strict SQL mode the same insert would have stored an empty string and the drift would have stayed invisible until someone read the table.
Migration 000089 appends KEPEMUDAAN and EKRAF to the end of the announcement type column. Position matters more than it looks. MySQL permits an in-place alteration of a column with an ENUM only when the new members are appended at the tail and the storage size does not change [4]. Placing the two values in alphabetical order, somewhere in the middle, would have forced a table copy instead of an in-place change.
The pair also splits cleanly. golang-migrate applies migration pairs in strict version order [3], so 000088 carries the data and 000089 carries the schema, each with its own reversal.
The backfill carries its own guard
Thirty-five rows had no bidang value as of October 2026. Those were the global announcements, and because of them staff scoped to a single bidang opened the CMS to an empty list. Migration 000088 assigns the existing values: OLAHRAGA becomes olahraga for 8 rows, BUDAYA becomes budaya for 6, WISATA becomes pariwisata for 6, PROMOSI becomes ekraf for 6, and UMUM stays global across 9 rows.
The guard does two jobs at once. An UPDATE without a WHERE clause touches every row in the table [5]; the AND bidang IS NULL predicate restricts the statement to untranslated rows and makes a second run a no-op. That idempotency is not a convenience, it is the property that lets the migration be replayed safely after a partial failure.
-- 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';
The two statements are not mirror images, and that is deliberate. The up statement guards on the state before the migration. The down statement guards on the state the migration created, combined with the type it created that state for. Remove the type predicate and the rollback nulls every row belonging to pariwisata, including rows that already had that bidang before 000088 ever ran. A down migration must reverse exactly the rows it touched, no more.
Ship the three together
The rule that came out of the review is narrow and testable: an enum change is not merged until one insert per new value has run against a strict-mode database. Not a schema diff, not a generated client, not a passing type check. One write, executed.
What the review exposed is that no layer was wrong on its own. The application validation was correct, the specification was correct, and the column was correct for the state it was in before the feature existed. The defect lived in the gap between commits, and only an executed statement could cross that gap. The static tooling never had a chance, and that is the part worth remembering: the check that caught this was not smarter than the type checker, it was simply pointed at the one layer the type checker could not see.
---
Sumber: