## 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>
114 lines
3.2 KiB
Python
114 lines
3.2 KiB
Python
#!/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 A(ADD 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.6(ADR-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())
|