mysql
Technical notes on web development, DevOps, and AI integration.
58 articles
- 13:28backend
A Complaint Born Without a Trace
An empty complaint timeline was not a rendering bug: the first history row was never inserted, because twin functions CreateRegistration and CreateReport had drifted apart.
TL;DR: A new complaint had no status history because CreateReport never seeded the initial NULL-to-submitted row, while its twin CreateRegistration did. The fix wraps report creation in a transaction that inserts both the report and its opening history entry atomically. A new regression test now guards the behavior, highlighting the need to test twin functions side by side.
#golang#mysql#audit-trail - 13:27testing
The Test That Counts Requests, Not Pixels
A Playwright test that intercepts network traffic to prove a form blocks submission client-side, plus a date-filter edge case that almost slipped through.
TL;DR: The test checks that submitting without a service pillar shows an inline error and sends no network request, verified via Playwright route interception. It fills all other fields and handles dev-only login plus a required phone setup wizard beforehand. It also adds coverage for a date filter edge case, ensuring today-to-today includes today using an exclusive bound the next day.
#playwright#testing#e2e - 07:59database
MySQL ENUM Migration in Three Steps: No Table Copy Required
Swapping a status vocabulary on live MySQL 5.7 tables: expand-migrate-contract, an honestly lossy down file, and an up-down-up proof before production.
TL;DR: Changing submission statuses from four to six values forces a costly MySQL table copy unless new ENUM values are appended. Instead the migration appends all values, remaps the data, then trims to the final list to stay in-place. Rollback is lossy for newer statuses, so history is tracked separately and the flow was verified with an up-down-up test.
#mysql#migration#schema - 11:27backend
Scope filters that must not swallow global rows
Adding a bidang scope filter to a public endpoint sounds like one WHERE clause. Then the global rows disappear.
TL;DR: The new scope filter hid global announcements because WHERE bidang equals value never matches NULL. The fix validates bidang with a 400 on invalid values and changes the query to keep NULL and ALL rows. It also adds bidang to the cache key and moves pillar intros into the settings table with a fallback.
#go#mysql#api - 10:49frontend
Moving /wisata: a 308 Redirect and a Menu That Survives
Renaming a public route is three moves at once: relocate the route tree, install a permanent 308, and migrate menu rows without stomping admin edits.
TL;DR: Renaming the public /wisata route to /pariwisata took more than a file move. The team added permanent 308 redirects for the old path and wildcard subpaths to preserve bookmarks and POST requests. A guarded MySQL migration renamed only untouched menu rows, shifted ordering, and left CMS-edited content intact.
#nextjs#redirect#mysql - 10:44testing
The Guard That Says No and the Migration Road Home
A test-only commit: proving the global guard via httptest, and the migration down path on MySQL 5.7 really goes backward, then comes home.
TL;DR: We finally tested two long-ignored paths: the RequireGlobalBidang guard that should block bidang access and the down migrations that had never run. A table-driven unit test now locks in the guard's behavior, including its intentional fail-open for empty contexts. An integration test migrates down to version 56 and back up, verifying columns disappear and reappear correctly.
#go#testing#mysql - 10:44backend
One Query Param, Three Layers: A Safe ?module= Filter
Adding a ?module= filter is not just a WHERE clause: 400 validation at the handler, a parameterized subquery, and a cache key per filter combination.
TL;DR: A naive SQL-only module filter would poison the shared mappoints:all cache, leaking filtered results to unfiltered requests and vice versa. The fix layers validation against six allowed modules with a 400 error, a parameterized subquery, and branched cache keys by module and category. This isolates each response variant and prevents cross-contamination.
#go#api#caching - 13:00backend
Saving Without Changes Returns 404? RowsAffected Fooled Me
A no-op form save got a 404 because MySQL counts changed rows, not matched rows. The fix: an existence check only on the zero path.
TL;DR: Hitting Save without changes returned 404 because MySQL reports zero affected rows when nothing actually changes. The default driver setting means zero is ambiguous and doesn't reliably indicate a missing record. The fix adds a quick existence check on zero instead of changing the global DSN setting.
#mysql#go#debugging - 11:09backend
Do Not Trust omitempty When Exposing a JSON-TEXT Column
A nullable JSON-TEXT column is not covered by omitempty alone: omitempty only knows Go emptiness, not SQL NULL. The story of a four-layer exposure.
TL;DR: A public detail page wasn't showing saved additional info even though the data was in the database. The fix required passing a nullable JSON column through four layers, and realizing Go's omitempty only drops nil, not empty strings. Normalizing null values to nil in the mapper and adding a fallback on the frontend finally hid the empty field correctly.
#go#mysql#api