fix: ledger write idempotency for TimescaleDB hypertable (BAT-016 rework)
BAT-016's initial implementation passed only against mocked repositories and broke on a real database. Two defects fixed: 1. insert_ledger_trade used `ON CONFLICT (portfolio_id, order_id, execution_id) WHERE order_id IS NOT NULL` but portfolio_ledger is a TimescaleDB hypertable, which cannot have that partial unique index (must include the partitioning column recorded_at). The statement errored against Postgres. Replaced with application-level idempotency: SELECT-before-insert inside the existing `SELECT ... FOR UPDATE` portfolio row lock, which serializes concurrent same-portfolio writes (race-safe). Duplicate returns current authoritative portfolio state (dcy-006 OQ-4 b). 2. `with self._connect() as conn:` closes the shared psycopg3 connection on exit, leaving the reused repository connection unusable. Switched to `with conn.transaction():` (commits without closing). Added tests/test_ledger_consolidation_integration.py — real-Postgres integration tests (new / duplicate / negative-balance) that close the mock-only verification gap. Full suite: 376 passed. SDLC: REQ-036/037 CHG-20260605-03 reconcile OQ-7 (ii) (DB partial unique → application-level idempotency) since the hypertable makes the DB constraint infeasible. REQ-036/037/038 → Implemented; BAT-016 → release-candidate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# REQ-036: portfolio_ledger 회계 원장 통합 마이그레이션 (ledger_trades 흡수/DROP)
|
||||
|
||||
- Status: Approved
|
||||
- Status: Implemented
|
||||
- Type: Migration
|
||||
- Priority: High
|
||||
- Owner: Justin Ko
|
||||
@@ -12,11 +12,11 @@
|
||||
## Requirement
|
||||
- Alembic 신규 revision을 작성한다. (head는 dcy-005 `20260601_0024` 다음)
|
||||
- `portfolio_ledger`에 컬럼을 추가한다:
|
||||
- `ledger_entry_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY`
|
||||
- `ledger_entry_id BIGINT GENERATED BY DEFAULT AS IDENTITY` (CHG-20260605-03: hypertable 제약상 단독 PRIMARY KEY 불가 — Identity 컬럼, 비-PK)
|
||||
- `order_id TEXT NULL`, `execution_id TEXT NULL` (dcy-006 OQ-7 (ii): NULL 허용)
|
||||
- `side TEXT NOT NULL`
|
||||
- `executed_at TIMESTAMPTZ NOT NULL`
|
||||
- partial UNIQUE index `(portfolio_id, order_id, execution_id) WHERE order_id IS NOT NULL`를 추가한다. broker write(신 path)만 idempotency를 적용하고 레거시/수동 write(order_id NULL)는 append-only 유지. (REQ-017 partial unique index 패턴 재사용)
|
||||
- **idempotency는 DB partial UNIQUE index가 아니라 application layer(REQ-037)에서 보장한다** (CHG-20260605-03). `portfolio_ledger`는 TimescaleDB hypertable(파티션 컬럼 `recorded_at`)이라 `(portfolio_id, order_id, execution_id)` partial unique index/PK 생성이 불가능하다(`cannot create a unique index without the column recorded_at`). 따라서 본 마이그레이션은 broker 식별 컬럼만 추가하고 unique 제약은 만들지 않는다. broker write dedup은 REQ-037의 `SELECT ... FOR UPDATE` 락 하 SELECT-before-insert로 처리.
|
||||
- 수치 컬럼 타입을 DOUBLE PRECISION → NUMERIC(20,8)로 변경한다: `price`, `quantity`, `fee_amount`, `tax_amount`, `cash_balance_after`, `asset_quantity_after`, `total_value_after`, `profit_buffer_after`.
|
||||
- 기존 `action` 컬럼을 제거하고 `side`로 통일한다. backfill 매핑: `action='BUY'` → `side='buy'`, `action='SELL'` → `side='sell'`. 그 외 자유형식 action 값의 처리(R-6)는 마이그레이션 본문에서 분포 확인 후 결정(개발 워크스페이스는 분포 단순).
|
||||
- 기존 데이터 backfill: `recorded_at` → `executed_at` 복사, `order_id`/`execution_id`는 NULL(synthetic 생성 불요).
|
||||
@@ -25,9 +25,9 @@
|
||||
- `app/repositories/postgres.py`의 `ensure_schema()` 내 `portfolio_ledger` 정의도 신 스키마와 동일하게 갱신한다.
|
||||
|
||||
## Acceptance Criteria
|
||||
1. Given 신규 Alembic revision 적용 후, When `\d portfolio_ledger` 확인, Then `ledger_entry_id`(PK), `order_id`(NULL 허용), `execution_id`(NULL 허용), `side`(NOT NULL), `executed_at`(NOT NULL) 컬럼이 존재한다.
|
||||
1. Given 신규 Alembic revision 적용 후, When `\d portfolio_ledger` 확인, Then `ledger_entry_id`(Identity, 비-PK — CHG-20260605-03), `order_id`(NULL 허용), `execution_id`(NULL 허용), `side`(NOT NULL), `executed_at`(NOT NULL) 컬럼이 존재한다.
|
||||
2. Given AC-1 환경, When `information_schema`로 수치 컬럼 타입 확인, Then `price`/`quantity`/`fee_amount`/`tax_amount`/`cash_balance_after`/`asset_quantity_after`/`total_value_after`/`profit_buffer_after`가 모두 NUMERIC(20,8)이다.
|
||||
3. Given AC-1 환경, When partial UNIQUE index 확인, Then `(portfolio_id, order_id, execution_id) WHERE order_id IS NOT NULL` 조건의 unique index가 존재한다.
|
||||
3. (CHG-20260605-03 재정의) Given AC-1 환경, When `portfolio_ledger` 인덱스/제약 확인, Then `(portfolio_id, order_id, execution_id)` 기반 **partial unique index/PK는 존재하지 않는다**(hypertable 제약). broker write idempotency는 REQ-037 application layer에서 보장되며, 실 DB 회귀(`tests/test_ledger_consolidation_integration.py::test_duplicate_broker_write_is_idempotent`)로 검증한다.
|
||||
4. Given AC-1 환경, When `action` 컬럼 존재 확인, Then `action` 컬럼은 존재하지 않고 `side` 컬럼으로 대체되어 있다.
|
||||
5. Given AC-1 환경, When `\d ledger_trades` 또는 테이블 목록 조회, Then `ledger_trades` 테이블이 존재하지 않는다.
|
||||
6. Given 기존 portfolio_ledger 데이터(있는 경우), When 마이그레이션 적용, Then `executed_at`은 `recorded_at` 값으로 채워지고 `order_id`/`execution_id`는 NULL이며 `side`는 기존 action에서 매핑된다.
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
## Impacted Area
|
||||
- Docs:
|
||||
- `docs/data-model.md` — `portfolio_ledger` entity에 신규 컬럼(ledger_entry_id PK, order_id/execution_id NULL, side, executed_at) + 정밀도 NUMERIC(20,8) + partial UNIQUE index 명세 추가. `ledger_trades` entity 섹션 제거. 두 ledger 테이블 → 단일 원장 통합 명시.
|
||||
- `docs/data-model.md` — `portfolio_ledger` entity에 신규 컬럼(ledger_entry_id Identity 비-PK, order_id/execution_id NULL, side, executed_at) + 정밀도 NUMERIC(20,8) 명세 추가. **idempotency는 application layer(REQ-037)이며 DB partial unique index/PK는 hypertable 제약상 없음**을 명시. `ledger_trades` entity 섹션 제거. 두 ledger 테이블 → 단일 원장 통합 명시.
|
||||
- Modules:
|
||||
- `migrations/versions/` — 신규 revision (예: `20260605_NNNN_portfolio_ledger_consolidation.py`). down_revision = `20260601_0024`.
|
||||
- `app/repositories/postgres.py:263-282` — `ensure_schema()` portfolio_ledger 정의 갱신
|
||||
@@ -46,13 +46,76 @@
|
||||
## Notes
|
||||
- 본 REQ는 dcy-006 FR-9/FR-10/FR-11/FR-12 산출물 + FR-5(data-model.md) 일부 흡수.
|
||||
- REQ-037(신 path 회계 책임 + 응답)의 선행 — 본 마이그레이션이 스키마 기반을 제공한다.
|
||||
- partial UNIQUE는 broker write만 dedup, 레거시 write(order_id NULL)는 append-only. dcy-006 OQ-7 (ii) 결정.
|
||||
- idempotency(broker write만 dedup, 레거시 write append-only)는 dcy-006 OQ-7 (ii) 결정이나, hypertable 제약으로 DB partial unique가 불가하여 application layer(REQ-037)에서 구현(CHG-20260605-03).
|
||||
- R-5(DOUBLE→NUMERIC 정밀도)/R-6(action→side 매핑 누락)/R-7(DROP 트랜잭션)는 prod 마이그레이션 시 별도 검토. 본 워크스페이스는 개발용이라 영향 미미(workspace_purpose 메모리).
|
||||
- 별도 REL(REL-003 등)로 분리 배포(dcy-006 A-4).
|
||||
- Source: dcy-006 FR-9~12, OQ-6 (iv), OQ-7 (ii).
|
||||
|
||||
## Change Log
|
||||
|
||||
### CHG-20260605-04
|
||||
- Date: 2026-06-05
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
- Status: Approved → Implemented
|
||||
|
||||
#### Intent
|
||||
- BAT-016 confirm-batch-verification 완료. portfolio_ledger 통합 마이그레이션 + ensure_schema 갱신 + ledger_trades DROP 구현. AC-1/2/4/5/6/7 검증, AC-3는 CHG-20260605-03 재정의(application-layer idempotency)로 실 DB 통합 테스트(`test_duplicate_broker_write_is_idempotent`)로 검증. 전체 376 passed.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
- No change
|
||||
|
||||
#### Impacted Area Delta
|
||||
- No change
|
||||
|
||||
#### Delivery Trace Delta
|
||||
- Batches:
|
||||
- BAT-016 (release-candidate)
|
||||
- Releases:
|
||||
- REL-003 (예정)
|
||||
|
||||
#### Revalidation Impact
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: implemented
|
||||
- Follow-up Action: REL-003 staging에서 fresh-DB alembic 적용 + DOUBLE→NUMERIC 정밀도/action 분포 최종 검증
|
||||
|
||||
#### Validation Plan
|
||||
- 실 DB 통합 테스트 + alembic schema 검증 (완료); prod staging은 release 단계
|
||||
|
||||
### CHG-20260605-03
|
||||
- Date: 2026-06-05
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
- AC/Requirement 재정의: `portfolio_ledger`가 TimescaleDB hypertable임이 BAT-016 verify에서 확인되어, dcy-006 OQ-7 (ii)의 "DB partial UNIQUE index + ledger_entry_id PK"가 물리적으로 불가능(`cannot create a unique index without the column recorded_at`). idempotency를 application layer(REQ-037 SELECT-before-insert under FOR UPDATE)로 이관. Status는 Approved 유지(구현 진행 중 설계 정합 보정).
|
||||
|
||||
#### Intent
|
||||
- 설계(DB 제약 idempotency)와 hypertable 현실의 충돌 해소. 마이그레이션은 broker 컬럼만 추가하고 unique 제약/PK는 생성하지 않으며, dedup은 application layer가 담당하도록 정합화.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
- Updated:
|
||||
- AC-1: `ledger_entry_id(PK)` → `ledger_entry_id(Identity, 비-PK)`
|
||||
- AC-3: "partial UNIQUE index 존재" → "partial unique index/PK 미존재(hypertable 제약), idempotency는 REQ-037 application layer + 실 DB 통합 테스트로 검증"
|
||||
|
||||
#### Impacted Area Delta
|
||||
- Modules: 변경 없음(이미 신규 revision + ensure_schema 지정). 마이그레이션 본문에서 partial unique index 생성 제거 반영.
|
||||
- Tests: `tests/test_ledger_consolidation_integration.py`(실 DB) 추가 — idempotency 검증 이관.
|
||||
|
||||
#### Delivery Trace Delta
|
||||
- Batches: BAT-016 (in-delivery)
|
||||
- Releases: none (REL-003 등 분리 예정)
|
||||
|
||||
#### Revalidation Impact
|
||||
- Reverification Needed: yes (BAT-016 verification은 실 DB 통합 테스트로 재수행)
|
||||
- Existing Implementation Invalidated: no (구현이 본 재정의에 맞춰 수정됨)
|
||||
- Status Recommendation: keep-current (Approved 유지)
|
||||
- Follow-up Action: BAT-016 verification 실 DB evidence 반영
|
||||
|
||||
#### Validation Plan
|
||||
- alembic schema 검증(AC-1/2/4/5/6/7) + 실 DB idempotency 통합 테스트(AC-3 대체)
|
||||
|
||||
### CHG-20260605-02
|
||||
- Date: 2026-06-05
|
||||
- Author: Justin Ko
|
||||
|
||||
Reference in New Issue
Block a user