Files
awoooi/apps/api/scripts/awooop_phase1_batch1_backfill.py
Your Name 13e51802fe feat(awooop): Phase 0 全 ADR + Phase 1 control plane schema(含 critic 四項修正)
## Phase 0(文件層,全部 Accepted)
- ADR-106/107:AwoooP 平台架構 + 儲存策略
- ADR-111~118:Bootstrap → RLS 七項核心 ADR
- ADR-119~124:SAGA → Singleton Decomposition 六項 ADR
- ADR-UI-01~04:Operator Console 四個 UI ADR

## Phase 1(DB schema + migration)
- awooop_phase1_control_plane_2026-05-04.sql:7 張新表 + trigger + RLS
  - Step 1:三角色(platform_admin/migration BYPASSRLS,awooop_app 受 RLS)
  - Step 13:GRANT awooop_app 最小權限(7 條)
  - Step 14:RLS fail-closed,移除 __platform__ 後門
- awooop_phase1_batch1_rls_2026-05-04.sql:高流量四表三步式 ADD COLUMN
- awooop_phase1_batch1_backfill.py:SKIP LOCKED 分批回填腳本
- awooop_models.py:7 個 SQLAlchemy 2.x models

## Critic 修正(4 Critical + 3 Major)
- C-1:ADD CONSTRAINT IF NOT EXISTS → DO 塊 + pg_constraint 查詢
- C-2:__mapper_args__ 字串 list → primary_key=True on mapped_column
- C-3:__platform__ RLS 後門 → 全移除,改用 BYPASSRLS role
- C-4:awooop_app role 從未建立 → Step 1 + 7 條 GRANT
- M-1:active_pointer_guard SECURITY DEFINER(FORCE RLS 跨租戶保護)
- M-2:pg_partman create_parent 加冪等防護
- M-3:immutability trigger 新增身份欄位保護(project_id/family/contract_id)

## Task 1.2 修補
- agent_loader.py:硬編碼 Mac 路徑 → AGENTS_DIR 環境變數
- Dockerfile:補 COPY .claude/agents/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 13:37:11 +08:00

114 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
AwoooP Phase 1 Batch 1 回填腳本
================================
對 incidents / knowledge_entries / playbooks / audit_logs 四張表
分批將 project_id IS NULL 的列回填為 'awoooi'
前置條件:
awooop_phase1_batch1_rls_2026-05-04.sql Step AADD COLUMN nullable已執行
執行方式:
export DATABASE_URL="postgresql+asyncpg://awoooi:<password>@192.168.0.188:5432/awoooi_prod"
cd apps/api && python scripts/awooop_phase1_batch1_backfill.py
2026-05-04 ogt + Claude Sonnet 4.6ADR-118 Batch 1 C-3 修正)
"""
import asyncio
import os
import time
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
DATABASE_URL = os.environ["DATABASE_URL"]
TABLES = [
("incidents", "incident_id"),
("knowledge_entries", "id"),
("playbooks", "id"),
("audit_logs", "id"),
]
BATCH_SIZE = 5000
SLEEP_MS = 100 # 批次間休眠 ms降低對正常流量的影響
async def count_nulls(conn, table: str) -> int:
result = await conn.execute(
text(f"SELECT count(*) FROM {table} WHERE project_id IS NULL") # noqa: S608
)
return result.scalar()
async def backfill_table(engine, table: str, pk_col: str) -> int:
total_updated = 0
print(f"\n[{table}] 開始回填...")
while True:
async with engine.begin() as conn:
result = await conn.execute(text(f"""
UPDATE {table}
SET project_id = 'awoooi'
WHERE {pk_col} IN (
SELECT {pk_col} FROM {table}
WHERE project_id IS NULL
LIMIT :batch_size
FOR UPDATE SKIP LOCKED
)
"""), {"batch_size": BATCH_SIZE})
rows = result.rowcount
total_updated += rows
if rows == 0:
break
print(f" [{table}] 已回填 {total_updated} 筆...")
await asyncio.sleep(SLEEP_MS / 1000)
print(f" [{table}] 回填完成,共 {total_updated}")
return total_updated
async def verify(engine) -> bool:
print("\n=== 驗收確認 ===")
ok = True
async with engine.connect() as conn:
for table, _ in TABLES:
null_count = await count_nulls(conn, table)
status = "" if null_count == 0 else ""
print(f" {status} {table}: {null_count} 筆 NULL project_id")
if null_count != 0:
ok = False
return ok
async def main():
print("=" * 60)
print("AwoooP Phase 1 Batch 1 Backfill")
print("=" * 60)
engine = create_async_engine(DATABASE_URL, echo=False)
t0 = time.monotonic()
for table, pk_col in TABLES:
await backfill_table(engine, table, pk_col)
passed = await verify(engine)
elapsed = time.monotonic() - t0
print(f"\n{'✅ 所有表回填完成' if passed else '❌ 仍有 NULL請重跑'}")
print(f"耗時:{elapsed:.1f}s")
print()
if passed:
print("下一步:執行 awooop_phase1_batch1_rls_2026-05-04.sql 的 Step C")
else:
print("⚠️ 請確認無長 transaction 持有 SKIP LOCKED 的列後重跑")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(main())