← 개발 로그 목록

website: 마이그레이션 위험도 판단에 INSERT OR REPLACE/REPLACE INTO 추가

/ 3분 분량 / 개발 로그

며칠 전 UPDATE/DELETE를 destructive 패턴에 추가한 뒤로, INSERT 계열을 다시 살펴보다가 "INSERT는 다 안전하다"는 전제 자체가 허술하다는 걸 발견했다. INSERT OR REPLACE와 REPLACE INTO는 이름만 INSERT지 실제로는 PK/UNIQUE 충돌 시 기존 행을 지우고 새로 넣는 동작이라, cd.yml의...

배경은 #320에서 진행하던 보강 작업이다. cd.yml의 마이그레이션 위험도 판단은 새로 추가된 마이그레이션 파일을 grep으로 훑어서, DROP/RENAME 계열이면 컬럼 삭제·이름변경으로 보고 자동 롤백을 막고 사람에게 넘기고, UPDATE/DELETE 계열이면 "롤백해도 이미 사라진 데이터는 안 돌아온다"는 이유로 마찬가지로 destructive 취급하도록 되어 있었다. 이 로직을 다듬던 중에, INSERT 계열도 전부 한 덩어리로 묶어서 안전 취급하고 있다는 걸 다시 확인했다.

문제는 SQLite에서 INSERT OR REPLACE와 REPLACE INTO가 이름과 다르게 동작한다는 점이다. PK나 UNIQUE 제약에 걸리는 값을 넣으면, SQLite는 새 값으로 업데이트하는 게 아니라 기존 행을 통째로 지우고 새 행을 삽입한다. 그 행에 명시하지 않은 다른 컬럼값들은 조용히 사라지거나 기본값으로 초기화될 수 있다. 결과만 보면 DELETE+INSERT와 동일한, 되돌릴 수 없는 데이터 변경이다. 반면 INSERT OR IGNORE는 충돌 시 그냥 건너뛰고 기존 행을 그대로 두기 때문에 이건 계속 안전 취급해도 된다는 판단이 섰다.

그래서 cd.yml의 grep 패턴에 or[[:space:]]+replace와 replace[[:space:]]+into를 추가했다. 기존에 drop table/drop column/rename to/rename column/update ... set/delete from을 검사하던 정규식 뒤에 두 패턴을 이어 붙이는 식이라 변경 자체는 크지 않다. 대신 문서 쪽에서 이 판단의 근거를 남겨두는 데 더 신경을 썼다. CI-CD.md에는 이전 보강(UPDATE/DELETE 추가) 항목 바로 아래에 "같은 보강 직후 한 겹 더"라는 식으로 이번 변경을 이어 붙였고, db-migration.md에는 시드 데이터를 멱등하게 넣고 싶으면 INSERT OR IGNORE를 쓰고 INSERT OR REPLACE/REPLACE INTO는 쓰지 말라고 명시적으로 적었다.

이번 것도 실제 마이그레이션 파일에서 문제가 터져서 발견한 게 아니라, 이전 보강 작업을 하다가 INSERT 전체를 안전으로 뭉뚱그린 부분이 마음에 걸려서 다시 들여다본 결과다. 이 프로젝트가 SQLite 기반이라 INSERT OR REPLACE 같은 SQLite 고유 문법이 실제로 쓰일 여지가 있다는 것도 한몫했다. grep 기반 휴리스틱이라는 한계는 여전하지만, 적어도 "이름에 INSERT가 들어가면 무조건 안전"이라는 단순한 가정은 걷어낸 셈이다.