- ensure_schema() broker_credentials: daemon_id NOT NULL, PK=(daemon_id,broker)로 갱신 - _row_to_broker_credential: Step2 컬럼 순서(daemon_id 선두)로 갱신 - upsert_broker_credential: ON CONFLICT (daemon_id,broker)로 수정, fallback try/except 제거 - list_broker_credentials: fallback 제거, SELECT 컬럼 순서 갱신 - test_bat020_broker_credential_domain.py: AC-3/AC-4 행 순서를 Step2 포맷으로 갱신 - tests/test_bat021_step2_ensure_schema_and_upsert.py: 12개 신규 테스트 - docs/runbooks/bat-021-req047-step2-migration-runbook.md: 운영 적용 런북 - docs/batches/bat-021: batch 문서(index/planning/design/implementation/verification) 실 PostgreSQL 통합검증(AC-1~7 전건 PASS): Step1 upgrade → backfill → Step2 upgrade → downgrade 시나리오 포함 NULL 사전검증(upgrade 거부) 및 downgrade 안전장치(중복 시 RuntimeError) 확인 521 passed, 0 failed (기존 509 + 신규 12)
4.8 KiB
BAT-021 Design
변경 설계
T-1/T-2/T-3: app/repositories/postgres.py 수정
ensure_schema() broker_credentials (T-1)
현재 ensure_schema()의 broker_credentials CREATE TABLE은:
CREATE TABLE IF NOT EXISTS broker_credentials (
user_id TEXT NOT NULL,
broker TEXT NOT NULL,
account_ref TEXT NOT NULL,
encrypted_payload TEXT NOT NULL,
credential_version INTEGER NOT NULL DEFAULT 1,
last_rotated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, broker)
)
Step2 이후 최종 스키마:
daemon_id TEXT NOT NULL추가PK = (daemon_id, broker)user_id컬럼 유지 (NOT NULL — 신규 INSERT는 항상 user_id 포함)
갱신 후:
CREATE TABLE IF NOT EXISTS broker_credentials (
daemon_id TEXT NOT NULL,
broker TEXT NOT NULL,
user_id TEXT NOT NULL,
account_ref TEXT NOT NULL,
encrypted_payload TEXT NOT NULL,
credential_version INTEGER NOT NULL DEFAULT 1,
last_rotated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (daemon_id, broker)
)
ensure_schema()는 IF NOT EXISTS를 사용하므로 테이블이 이미 존재하면 CREATE를 스킵한다.
Step2 마이그레이션이 적용된 DB에서는 이 정의와 실제 스키마가 정합된다.
Step1만 적용된 DB(Step2 미적용)에서 ensure_schema()를 호출하면 테이블은 이미 존재하므로 스킵되며 영향 없다.
Step2 이후 추가될 인덱스:
CREATE INDEX IF NOT EXISTS idx_broker_credentials_daemon_broker
ON broker_credentials (daemon_id, broker)
이 인덱스는 PK 자체이므로 별도 인덱스는 불필요. Step1에서 만든 idx_broker_credentials_daemon_id 인덱스는 그대로 유지.
upsert_broker_credential (T-2)
현재:
- Primary INSERT:
ON CONFLICT (user_id, broker)— Step2 이후 PK가 바뀌어 동작 안 함 - Fallback:
ON CONFLICT (user_id, broker)without daemon_id — Step1 미적용 환경용
Step2 이후 세계에서:
- daemon_id는 항상 NOT NULL (Step2 PK 제약)
- ON CONFLICT는
(daemon_id, broker)로 변경 - Fallback 불필요 → 제거
단, BAT-020의 fallback try/except는 psycopg 트랜잭션 abort 이슈가 있었다:
트랜잭션이 abort 상태에서 fallback 쿼리를 실행하면 InFailedSqlTransaction 오류가 발생.
이 batch에서 fallback을 제거하면서 이 이슈도 동시에 해소된다.
갱신 후 upsert_broker_credential:
cur.execute(
"""
INSERT INTO broker_credentials (daemon_id, user_id, broker, account_ref, encrypted_payload)
VALUES (%s, %s, %s, %s, %s)
ON CONFLICT (daemon_id, broker) DO UPDATE
SET account_ref = EXCLUDED.account_ref,
encrypted_payload = EXCLUDED.encrypted_payload,
user_id = EXCLUDED.user_id,
credential_version = broker_credentials.credential_version + 1,
last_rotated_at = NOW()
RETURNING daemon_id, user_id, broker, account_ref, encrypted_payload, credential_version, last_rotated_at
""",
(daemon_id, user_id, broker, account_ref, encrypted_payload),
)
RETURNING 절에서 daemon_id 컬럼 순서 변경: _row_to_broker_credential의 현재 row 매핑 확인 필요.
현재 _row_to_broker_credential:
return BrokerCredential(
user_id=row[0], # user_id
broker=row[1], # broker
account_ref=row[2],
encrypted_payload=row[3],
credential_version=int(row[4]),
last_rotated_at=row[5],
daemon_id=row[6], # optional
)
RETURNING 컬럼 순서를 유지해야 한다. 새 RETURNING:
daemon_id(0), user_id(1), broker(2), account_ref(3), encrypted_payload(4), credential_version(5), last_rotated_at(6)
→ _row_to_broker_credential 매핑도 함께 수정 필요.
list_broker_credentials도 같은 방향으로 정렬:
SELECT daemon_id, user_id, broker, account_ref, encrypted_payload, credential_version, last_rotated_at
_row_to_broker_credential 갱신:
return BrokerCredential(
daemon_id=row[0],
user_id=row[1],
broker=row[2],
account_ref=row[3],
encrypted_payload=row[4],
credential_version=int(row[5]),
last_rotated_at=row[6],
)
list_broker_credentials fallback 제거 (T-3)
Step2 이후 daemon_id 컬럼이 항상 존재하므로 try/except fallback 제거.
영향 범위
app/repositories/postgres.py— ensure_schema, upsert_broker_credential, list_broker_credentials, _row_to_broker_credentialtests/— bat021 신규 테스트, 기존 FakeRepo 의존 테스트 영향 없음 (FakeRepo는 실제 DB 쿼리 미사용)
하위 호환성
Step2 PK 전환 이후에만 이 코드가 올바르게 동작한다.
Step2 미적용 DB에서는 ON CONFLICT (daemon_id, broker) 제약이 없어 오류 발생 가능.
런북에서 Step2 적용 이전에 새 코드를 배포하지 말 것을 명시한다.
배포 순서: Step1 → backfill → Step2 → 새 코드 배포.