fix: migration ordering bug that could block fresh-DB deploys

V004_normalize_snapshots_schema.sql had an unguarded FK to a table V2
creates. Under DbUp's default ordinal filename sort, the zero-padded
"V003_"/"V004_" migrations sorted before "V1__", so on a brand-new
database V004 would hard-fail on that FK and abort every migration
after it - V1 through V8 would never run. Confirmed via production
that neither V003 nor V004 had ever actually applied.

Fix: renamed them to V9__/V10__ and added MigrationScriptNameComparer,
which sorts DbUp scripts by numeric V{n} value instead of raw string
order, so double-digit versions can never again sort ahead of earlier
single-digit ones. Added regression tests for both the fixed case and
the original bug shape.

Also updates docs/db/quantengine.dbml (renamed migrations, and closes
out the engine_history/quantengine table-name-collision question -
both schemas are live, backing different code paths, not duplicates)
and CLAUDE.md (migration ordering fix, KIS/OpenDART/KRX credential
env-var-name reference, including a CI secret/env-var name mismatch
found for OpenDART that still needs a decision).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 11:36:00 +09:00
parent 70824c2afb
commit 279d1760ef
7 changed files with 159 additions and 27 deletions
+36 -23
View File
@@ -1,13 +1,16 @@
// =============================================================================
// QuantEngine Database Schema (DBML)
// DbUp 마이그레이션(V1~V8, V003, V004)과 1:1 동기화 — 마이그레이션 추가 시 이 파일도 반드시 갱신
// DbUp 마이그레이션(V1~V10)과 1:1 동기화 — 마이그레이션 추가 시 이 파일도 반드시 갱신
// (CLAUDE.md 규칙: schema 변경 → DBML + 문서 동기화)
//
// 마이그레이션 파일명 규칙 2종 혼재 (2026-07-30 발견, 미해결):
// V1__Name.sql .. V8__Name.sql (더블언더스코어, zero-pad 없음)
// V003_name.sql, V004_name.sql (싱글언더스코어, zero-pad)
// DbUp는 파일명 알파벳순으로 실행하므로 "V003" < "V1" 순서로 적용됨 — 신규 마이그레이션은
// 반드시 하나의 규칙(권장: V{n}__Name.sql)만 사용할 것.
// 2026-07-30 해결됨: 예전에 V003_name.sql/V004_name.sql(싱글언더스코어, zero-pad)이
// V1__Name.sql(더블언더스코어, zero-pad 없음) 방식과 섞여 있어, DbUp의 기본 알파벳순
// 정렬에서 "V003" < "V1"로 먼저 실행되는 문제가 있었다 (V004는 V2가 만드는 테이블에 대한
// 하드 FK 제약이 있어 빈 DB에 처음부터 배포하면 V004에서 하드 실패 → V1~V8 전체가 실행
// 안 되는 재현성 버그였음). 조치: V003→V9, V004→V10으로 리네임 + DbMigrator.cs에
// MigrationScriptNameComparer(숫자 기반 비교자)를 추가해 "V{n}"이 몇 자리 숫자든 항상
// 숫자 크기순으로 정렬되도록 함. 신규 마이그레이션은 V{n}__Name.sql 규칙만 사용할 것
// (이 비교자 덕분에 V11, V12... 로 계속 늘어나도 더 이상 이 문제가 재발하지 않는다).
//
// 참고: Hangfire 스키마는 Hangfire.PostgreSql 라이브러리가 자동 생성
// (DbUp 마이그레이션으로 관리하지 않음, 여기서도 제외)
@@ -504,14 +507,15 @@ Table engine_history.outcome_evaluation {
}
// =============================================================================
// V003: Audit Trail Tables (quantengine schema, 2026-07-24)
// V9: Audit Trail Tables (quantengine schema, 파일: V9__Add_Audit_Trail_Tables.sql,
// 원래 이름 V003_add_audit_trail_tables.sql, 2026-07-30에 V9로 리네임 — 위 헤더 참고)
//
// 2026-07-30 확정 (실제 프로덕션 DB 조회로 검증): 이 마이그레이션은 CREATE TABLE 안에
// MySQL 전용 인라인 "INDEX name (cols)" 구문을 사용해 PostgreSQL에서 문법 오류로 실패했다.
// quantengine.schemaversions(DbUp 저널)에 V003이 아예 기록되어 있지 않고, 아래 3개 테이블도
// 프로덕션에 실제로 존재하지 않음을 직접 확인했다. V003_add_audit_trail_tables.sql의 인라인
// INDEX 구문은 이미 별도 CREATE INDEX 문으로 수정됐으므로, 다음 배포 시 DbUp가 이 마이그레이션을
// 최초로 실행해 아래 3개 테이블을 생성할 것이다.
// 2026-07-30 확정 (실제 프로덕션 DB 조회로 검증): 리네임 전 이 마이그레이션은 CREATE TABLE
// 안에 MySQL 전용 인라인 "INDEX name (cols)" 구문을 사용해 PostgreSQL에서 문법 오류로
// 실패했고, quantengine.schemaversions(DbUp 저널)에도 전혀 기록되어 있지 않았다 — 아래 3개
// 테이블은 프로덕션에 실제로 존재하지 않음을 확인했다. 인라인 INDEX 구문은 별도 CREATE INDEX
// 문으로 수정했고, V003→V9 리네임으로 실행 순서 문제도 해결했으므로 다음 배포 시 DbUp가 이
// 마이그레이션을 최초로 실행해 아래 3개 테이블을 생성할 것이다.
// =============================================================================
Table quantengine.kis_collection_runs_audit {
@@ -557,8 +561,10 @@ Table quantengine.kis_collection_errors_audit {
}
// =============================================================================
// V004: 3NF Normalization / Star Schema (quantengine schema, Adapter 패턴으로
// 기존 kis_collection_snapshots와 병행 운영 — 마이그레이션 자체 주석에 명시됨)
// V10: 3NF Normalization / Star Schema (quantengine schema, 파일:
// V10__Normalize_Snapshots_Schema.sql, 원래 이름 V004_normalize_snapshots_schema.sql,
// 2026-07-30에 V10로 리네임 — 위 헤더 참고. Adapter 패턴으로 기존
// kis_collection_snapshots와 병행 운영 — 마이그레이션 자체 주석에 명시됨)
// =============================================================================
Table quantengine.stocks {
@@ -611,14 +617,21 @@ Table quantengine.kis_collection_snapshots_v2 {
// =============================================================================
// V8: PostgreSQL History-First Operating Model (quantengine schema)
//
// ⚠️ 스키마 충돌 주의: 아래 4개 테이블(market_raw_history, factor_version_history,
// factor_output_history, decision_result_history)은 engine_history 스키마(V3, 위 참고)에
// 이미 동일한 이름으로 존재한다. 컬럼 구조를 대조한 결과 같은 테이블의 재적용이 아니라
// 서로 다른 두 가지 설계다:
// - engine_history.*: EAV형 원본 관측 이력 (field_name/field_value 페어)
// - quantengine.*(이 섹션): OHLCV 와이드 테이블 / 팩터ID-스코어 구조
// 둘 다 실제 마이그레이션 파일에 존재하므로 DBML에는 두 스키마 버전을 모두 남긴다.
// 어느 쪽이 정본인지, 혹은 통합이 필요한지는 별도 아키텍처 결정 필요 (이번 작업 범위 밖).
// ⚠️ 동명이의 테이블 (통합 대상 아님, 2026-07-30 코드 조사로 확정): 아래 4개 테이블
// (market_raw_history, factor_version_history, factor_output_history,
// decision_result_history)은 engine_history 스키마(V3, 위 참고)에도 같은 이름으로 존재하지만,
// 마이그레이션 버그도 아니고 죽은 코드도 아니다 — 둘 다 실제로 읽고 쓰는 라이브 코드가 있는
// 서로 다른 두 모델이다:
// - engine_history.*: PostgresqlHistoryStore.AppendAsync() + HistoryIngestionService가
// 쓰는 범용 append-only 이력 저장소 (EAV형 원본 관측 이력)
// - quantengine.*(이 섹션): PostgresqlHistoryStore의 RecordWaterfallExecutionAsync 등
// + Admin 엔드포인트(BulkInsertMarketExcelEndpoint, UpdateFactorThresholdEndpoint,
// ExportStreamingFactorOlapEndpoint)가 쓰는 신형 구조화 운영 모델 (OHLCV 와이드 테이블 /
// 팩터ID-스코어 구조)
// 결론: 둘 다 유지해야 한다. 다만 같은 개념에 같은 테이블명을 두 스키마에서 쓰는 것 자체가
// 향후 개발자가 착각하기 쉬우므로(예: quantengine.market_raw_history에 쓸 걸 실수로
// engine_history.market_raw_history에 씀), 신규 코드 작성 시 스키마를 명시적으로 지정하고
// 반드시 어느 모델을 쓰는지 주석으로 남길 것.
// =============================================================================
Table quantengine.market_raw_history {