fix: preserve multi-cause reconcile warnings and align slot blocking
Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
co-authored by
genichin
parent
e47bebacaf
commit
45a5e4157d
@@ -1,6 +1,6 @@
|
||||
-- BAT-050 / REQ-102: reconcile-warning durable store (dcy-034).
|
||||
-- PK: slot_id (TEXT, UUID string). pause_store 동형 설계.
|
||||
-- ON CONFLICT DO UPDATE: 동일 slot_id 재삽입 시 최신 값으로 갱신.
|
||||
-- Initial schema; migration 028 upgrades PK to (slot_id, case_code)
|
||||
-- so B/C causes can coexist for the same slot.
|
||||
CREATE TABLE IF NOT EXISTS slot_reconcile_warning (
|
||||
slot_id TEXT PRIMARY KEY,
|
||||
portfolio_id TEXT NOT NULL,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
-- REQ-123 / issue #83:
|
||||
-- Preserve simultaneous warning causes by keying warnings per (slot_id, case_code).
|
||||
ALTER TABLE slot_reconcile_warning
|
||||
DROP CONSTRAINT IF EXISTS slot_reconcile_warning_pkey;
|
||||
|
||||
ALTER TABLE slot_reconcile_warning
|
||||
ADD CONSTRAINT slot_reconcile_warning_pkey PRIMARY KEY (slot_id, case_code);
|
||||
@@ -1,8 +1,4 @@
|
||||
"""PostgreSQL-backed durable reconcile-warning store (REQ-102, BAT-050).
|
||||
|
||||
pause_store.py와 동형 설계. PK: slot_id. case_code: 'B' | 'C'.
|
||||
Survives daemon restart (dcy-034 NFR-1).
|
||||
"""
|
||||
"""PostgreSQL-backed durable reconcile-warning store (REQ-102, BAT-050)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
@@ -12,7 +8,7 @@ from trex_core.adapters.persistence.postgresql import _require_psycopg
|
||||
|
||||
|
||||
class PostgreSQLReconcileWarningStore:
|
||||
"""`slot_reconcile_warning` table CRUD. slot_id PK is the gate (REQ-102)."""
|
||||
"""`slot_reconcile_warning` table CRUD keyed by (slot_id, case_code)."""
|
||||
|
||||
def __init__(self, dsn: str, *, psycopg_module: Any = None) -> None:
|
||||
self._psycopg = _require_psycopg(psycopg_module)
|
||||
@@ -34,9 +30,8 @@ class PostgreSQLReconcileWarningStore:
|
||||
"INSERT INTO slot_reconcile_warning "
|
||||
"(slot_id, portfolio_id, case_code, actor, reason, created_at, updated_at) "
|
||||
"VALUES (%s, %s, %s, %s, %s, %s, %s) "
|
||||
"ON CONFLICT (slot_id) DO UPDATE "
|
||||
"ON CONFLICT (slot_id, case_code) DO UPDATE "
|
||||
"SET portfolio_id = EXCLUDED.portfolio_id, "
|
||||
" case_code = EXCLUDED.case_code, "
|
||||
" actor = EXCLUDED.actor, "
|
||||
" reason = EXCLUDED.reason, "
|
||||
" updated_at = EXCLUDED.updated_at",
|
||||
@@ -44,13 +39,19 @@ class PostgreSQLReconcileWarningStore:
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def clear_warned(self, slot_id: str) -> None:
|
||||
def clear_warned(self, slot_id: str, *, case_code: str | None = None) -> None:
|
||||
with self._psycopg.connect(self._dsn, autocommit=False) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"DELETE FROM slot_reconcile_warning WHERE slot_id = %s",
|
||||
(slot_id,),
|
||||
)
|
||||
if case_code is None:
|
||||
cur.execute(
|
||||
"DELETE FROM slot_reconcile_warning WHERE slot_id = %s",
|
||||
(slot_id,),
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"DELETE FROM slot_reconcile_warning WHERE slot_id = %s AND case_code = %s",
|
||||
(slot_id, case_code),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def is_warned(self, slot_id: str) -> bool:
|
||||
@@ -65,7 +66,7 @@ class PostgreSQLReconcileWarningStore:
|
||||
def list_warned(self) -> set[str]:
|
||||
with self._psycopg.connect(self._dsn) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT slot_id FROM slot_reconcile_warning")
|
||||
cur.execute("SELECT DISTINCT slot_id FROM slot_reconcile_warning")
|
||||
return {row[0] for row in cur.fetchall()}
|
||||
|
||||
def list_c_case_warned(self) -> set[str]:
|
||||
@@ -73,26 +74,30 @@ class PostgreSQLReconcileWarningStore:
|
||||
with self._psycopg.connect(self._dsn) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT slot_id FROM slot_reconcile_warning WHERE case_code = %s",
|
||||
"SELECT DISTINCT slot_id FROM slot_reconcile_warning WHERE case_code = %s",
|
||||
("C",),
|
||||
)
|
||||
return {row[0] for row in cur.fetchall()}
|
||||
|
||||
def get_warning_info(self, slot_id: str) -> dict | None:
|
||||
"""Return {"portfolio_id", "case_code", "actor", "reason"} or None (REQ-105/106)."""
|
||||
"""Return warning info with full active case set, or None."""
|
||||
with self._psycopg.connect(self._dsn) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT portfolio_id, case_code, actor, reason "
|
||||
"FROM slot_reconcile_warning WHERE slot_id = %s LIMIT 1",
|
||||
"FROM slot_reconcile_warning WHERE slot_id = %s "
|
||||
"ORDER BY CASE WHEN case_code = 'C' THEN 0 ELSE 1 END, updated_at DESC",
|
||||
(slot_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
rows = cur.fetchall()
|
||||
if not rows:
|
||||
return None
|
||||
case_codes = sorted({str(row[1]) for row in rows if row[1]})
|
||||
preferred = rows[0]
|
||||
return {
|
||||
"portfolio_id": row[0],
|
||||
"case_code": row[1],
|
||||
"actor": row[2],
|
||||
"reason": row[3],
|
||||
"portfolio_id": preferred[0],
|
||||
"case_code": preferred[1],
|
||||
"case_codes": case_codes,
|
||||
"actor": preferred[2],
|
||||
"reason": preferred[3],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user