fix(테이블타입): TABLE_TYPE_COLUMNS 에 ON CONFLICT 매칭용 UNIQUE INDEX 추가 + 중복 정리
Build & Deploy to K8s / build-and-deploy (push) Successful in 8m27s
Build & Deploy to K8s / build-and-deploy (push) Successful in 8m27s
테이블 타입관리의 모든 쓰기 API (UNIQUE/NOT NULL 토글, 컬럼 설정 저장,
input-type upsert) 가 500 반환. 원인은 mapper SQL 의
ON CONFLICT (TABLE_NAME, COLUMN_NAME, COMPANY_CODE) 가 매칭할 unique
제약/인덱스가 운영 DB 에 존재하지 않아 PG 가
"there is no unique or exclusion constraint matching the ON CONFLICT
specification" 으로 거부.
- StartupSchemaMigrator MIGRATIONS 에 V025 / RUN_090 (1) (2) 추가:
(1) ROW_NUMBER 로 (table, column, company) 중복 행 정리
(운영 메타 DB 실측 2 그룹 / 4 row — 동일 데이터의 NULL updated_date
옛 row 제거. 테넌트 DB 들은 중복 0건).
(2) UX_TABLE_TYPE_COLUMNS_TCC UNIQUE INDEX 생성 (IF NOT EXISTS — 멱등).
- RUN_090_MIGRATION.md 신설.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
# 090 마이그레이션 — TABLE_TYPE_COLUMNS 중복 정리 + ON CONFLICT 용 UNIQUE INDEX
|
||||
|
||||
작성일: 2026-05-15
|
||||
작성자: johngreen
|
||||
관련 버그: 테이블 타입관리에서 모든 쓰기 API (UNIQUE 토글 / NOT NULL 토글 / 컬럼 설정 저장) 가 500 반환.
|
||||
|
||||
## 증상
|
||||
|
||||
```
|
||||
PSQLException: ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification
|
||||
mapper: tableManagement.upsertColumnSettings / upsertNullable / upsertUnique / upsertColumnInputType
|
||||
```
|
||||
|
||||
## 원인
|
||||
|
||||
`TABLE_TYPE_COLUMNS` 의 PK 는 `id` 단일(varchar). 운영 DB 어디에도
|
||||
`(TABLE_NAME, COLUMN_NAME, COMPANY_CODE)` UNIQUE 제약/인덱스가 없음.
|
||||
mapper 의 `INSERT … ON CONFLICT (TABLE_NAME, COLUMN_NAME, COMPANY_CODE) DO UPDATE …`
|
||||
구문이 매칭할 unique constraint 를 찾지 못해 즉시 BadSqlGrammar 로 500.
|
||||
|
||||
RUN_044 가 company_code 컬럼을 추가했지만 함께 도입했어야 할 unique index 가
|
||||
빠진 채로 운영에 들어간 것으로 보이며, 그 후 mapper 가 ON CONFLICT 패턴으로 작성되면서
|
||||
실제로는 한 번도 정상 동작하지 못한 채로 잠복했던 정황 (운영 메타 DB 의 35,316 행 중
|
||||
중복 키 그룹 2개 = 추가 4 row 가 그 흔적).
|
||||
|
||||
## 조치
|
||||
|
||||
### (1) 중복 행 정리
|
||||
|
||||
각 `(TABLE_NAME, COLUMN_NAME, COMPANY_CODE)` 그룹에서
|
||||
`updated_date DESC NULLS LAST, id::bigint DESC` 로 정렬해 첫 행만 유지, 나머지 DELETE.
|
||||
|
||||
```sql
|
||||
DELETE FROM TABLE_TYPE_COLUMNS
|
||||
WHERE id IN (
|
||||
SELECT id FROM (
|
||||
SELECT id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY TABLE_NAME, COLUMN_NAME, COMPANY_CODE
|
||||
ORDER BY UPDATED_DATE DESC NULLS LAST,
|
||||
id::bigint DESC
|
||||
) AS rn
|
||||
FROM TABLE_TYPE_COLUMNS
|
||||
) r
|
||||
WHERE r.rn > 1
|
||||
);
|
||||
```
|
||||
|
||||
실측(2026-05-15) 중복:
|
||||
|
||||
| DB | 중복 그룹 | 삭제될 row |
|
||||
|---|---|---|
|
||||
| meta `invyone` | 2 (`sales_order_mng.incoterms@COMPANY_16`, `sales_order_mng.payment_term@COMPANY_16`) | 2 |
|
||||
| `siflex_invyone` | 0 | 0 |
|
||||
| `test01_invyone` | 0 | 0 |
|
||||
| `test02_invyone` | 0 | 0 |
|
||||
|
||||
남는 행은 가장 최근에 갱신된 동일 키 row (column_label/input_type 모두 동일 — 옛 NULL updated_date row 가 제거 대상).
|
||||
|
||||
### (2) UNIQUE INDEX 추가
|
||||
|
||||
```sql
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS UX_TABLE_TYPE_COLUMNS_TCC
|
||||
ON TABLE_TYPE_COLUMNS (TABLE_NAME, COLUMN_NAME, COMPANY_CODE);
|
||||
```
|
||||
|
||||
PostgreSQL 은 ON CONFLICT 가 인덱스도 인식하므로 mapper 의 모든 upsert SQL 이
|
||||
즉시 정상 동작. `IF NOT EXISTS` 로 멱등.
|
||||
|
||||
## 적용 방법
|
||||
|
||||
부팅 시 자동 적용 — 별도 작업 불필요. `StartupSchemaMigrator.MIGRATIONS` 리스트에
|
||||
V025 / RUN_090 (1) (2) 항목으로 등록되어 있어서 앱이 시작할 때 메타 DB + 모든 활성
|
||||
테넌트 DB 에 차례로 실행된다.
|
||||
|
||||
## 검증
|
||||
|
||||
```sql
|
||||
-- 중복 없음
|
||||
SELECT COUNT(*) FROM (
|
||||
SELECT 1 FROM TABLE_TYPE_COLUMNS
|
||||
GROUP BY TABLE_NAME, COLUMN_NAME, COMPANY_CODE HAVING COUNT(*) > 1
|
||||
) d;
|
||||
-- → 0
|
||||
|
||||
-- 인덱스 존재
|
||||
SELECT indexname FROM pg_indexes
|
||||
WHERE tablename = 'table_type_columns' AND indexname = 'ux_table_type_columns_tcc';
|
||||
-- → 1 row
|
||||
```
|
||||
|
||||
브라우저 검증:
|
||||
1. 솔루션 또는 테넌트 사이트 > 시스템 관리 > 테이블 타입관리 > 거래처 클릭
|
||||
2. 어느 컬럼이든 `UQ` / `NN` 토글 클릭 → 200, 토스트 "UNIQUE/NOT NULL 제약이 설정되었습니다"
|
||||
3. "컬럼 설정 저장" 버튼 클릭 → 200, 토스트 "모든 컬럼 설정을 성공적으로 저장했습니다"
|
||||
|
||||
## 영향 범위
|
||||
|
||||
- 테이블 타입관리 페이지 쓰기 API 4종 (`unique`, `nullable`, `columns/settings`, `columns/{c}/input-type`) 정상화.
|
||||
- 멱등 — 재실행 시 DELETE 0건, CREATE INDEX 도 IF NOT EXISTS 라 skip.
|
||||
- 부팅 시점 1회 실행, 런타임 트래픽에는 영향 없음.
|
||||
|
||||
## 롤백
|
||||
|
||||
```sql
|
||||
DROP INDEX IF EXISTS UX_TABLE_TYPE_COLUMNS_TCC;
|
||||
```
|
||||
DELETE 된 중복 row 는 정보 손실 없음 (남은 row 와 column_label/input_type 동일) 이라
|
||||
복구가 의미 없음. 그래도 굳이 되돌리려면 사전 백업 필요.
|
||||
Reference in New Issue
Block a user