AI 智能體改名程式欄位卻未遷移資料,舊記錄因此讀取為 undefined
The agent renamed the field in code and never told the database, so old rows read as undefined
AI 智能體把程式中的 displayName 改為 fullName,卻沒有遷移資料庫中既有記錄,令舊用戶的名稱讀取為 undefined。新帳戶寫入新欄位,因此只使用改名後資料的測試未能發現問題。文章建議先兼容讀取新舊欄位,再回填舊資料,並加入舊格式測試。
The profile type used displayName. The agent was asked to make the naming consistent, so it found and replaced the field across the interface, the components, and the form handler. Tests pass, the diff looks like a tidy rename, and the PR description calls it a refactor. Then a returning user opens their profile and the name renders blank.
The rows written before the rename still store displayName. The new code reads fullName, which does not exist on those records, so the value comes back undefined and the UI falls back to a placeholder. A brand-new account works, because it writes the new key, which is exactly why the bug survived CI: every fixture in the test suite was built after the rename.
Why a rename looks safe in the diff
An agent works from the current source tree and the current type definitions. A find-replace that matches the field everywhere in code reads as mechanical and risk-free, and nothing in the file tree points at the rows already sitting in the database. The diff shows a consistent new name, not a storage history the model cannot see.
Tests reinforce the illusion. They construct fresh objects in the new shape, assert against the new field, and never load a record that predates the change. Green means the new code agrees with the new fixtures; it says nothing about records that already exist under the old key.
What ships when the persisted shape is not migrated
- Historical rows read as undefined. Existing users hit the missing field while new users never do, so reproduction fails on a clean environment and the ticket reads as intermittent.
- Mixed population after rollout. Old rows keep the old key, new writes use the new key, and readers that only know one side drop half the data.
-
Caches and queues keep the old shape. A cached payload, a message sitting in a queue, or a preference stored in the browser still carries
displayName; renaming code does not rewrite what was already serialized. - Rollback restores code but not data. Once some rows were written under the new key, reverting the code leaves those records unreadable under the old name.
- The regression has no test. No fixture models an old-format record, so the suite cannot fail on the cutover.
The tell is a rename PR that touches code and types but adds no migration, no backfill, and no old-shape fixture. A rename is a refactor only when every sink keeps the same shape; persisted rows, caches, queues, and stored preferences remember the old field regardless of what the source says now.
Cut the persisted shape over in phases
Consistent naming is a reasonable goal; skipping the storage cutover is the defect. Keep both keys readable until the backfill finishes:
- List every sink before renaming. Database columns, cache entries, queued messages, browser storage, exported files, and API response bodies all carry the shape; write the list into the PR instead of assuming the type covers it.
-
Read-compatible first. For one release, read either
fullNameor the legacydisplayNameso old records still resolve while new writes go to the new key. - Backfill, then cut over. Copy old-key values onto the new key in a controlled backfill, and only then remove the legacy read path. A partial backfill in production is still mixed population, so verify row counts before dropping support.
- Add an old-shape fixture to the test. Construct a record written before the rename and assert it renders correctly under the compat read; that is the case the green rename currently misses.
- Treat a response field rename as a versioned contract. When the same field moves in an API body, old clients keep sending the old name; version or accept both rather than silently dropping it. Where the spec is the contract, we keep that check local in Powerduck and re-run the documented response shape against the live endpoint instead of trusting a handwritten example.
Rename the code, carry the history with it
A clean find-replace across source files genuinely improves consistency. It does not change the rows, caches, and messages already out in the world. Read the old key, write the new one, backfill the gap, and test an old-format record before the rename can ship as a refactor rather than a silent data loss.
來源:DEV Community · dev.to