# FR-035 — SSP Table Rename 設計 / 影響分析

> 類型：Refactor（DB table rename，跨 BE + jedi-oscal + 4 DB 環境）
> 提出：2026-06-10 ｜ Branch：`feature/ssp-table-rename`

## 需求（白話）

最早把完整的 `system_security_plan` 當 table 前綴命名，太冗長。簡化為 `ssp`，跟既有的 `ssp_components` / `ssp_inventory_items` 等子表命名對齊。三張要改：

| 現名 | 新名 |
|------|------|
| `oscal.system_security_plans` | `oscal.ssps` |
| `oscal.system_security_plan_control_implementations` | `oscal.ssp_control_implementations` |
| `oscal.system_security_plans_system_characteristics` | `oscal.ssp_system_characteristics` |

## 決策（user 拍板 2026-06-10）

1. **核心表用 `ssps`（複數）** — 與既有 `ssp_*` 子表複數慣例一致（`system_security_plans` 本就複數）。
2. **改 jedi-oscal 套件、走 dev path dependency** — ORM 定義在套件內；dev 階段 pyproject 切 path，feature 完成才發版推 Nexus。
3. **index / constraint / sequence / pkey 舊名一併 rename 乾淨** — 不只 `ALTER TABLE`。

## Scope 邊界

- **表名 only，欄位不動**：`system_security_plan_id`、`ssp_id` 等欄位維持原樣（避免爆炸性 blast radius）。
- 已 `ssp_` 前綴的子表（components / inventory_items / leveraged_authorizations / control_implementation_objectives / docx_parse_jobs / reference_documents…）**不改**。
- FE / E2E test repo：grep 確認**零**表名引用，不動。
- API field / JSON key（`sspUid` 等）非 DB 表名，**不在範圍**。

## 影響盤點（grep 4 repo + live DB introspection）

| 區域 | 內容 |
|------|------|
| **jedi-oscal**（8 檔） | 3 個 `__tablename__` + 6 個 `ix_system_security_plans_*` index + 8 條 schema-qualified FK 字串 + docstring。其他 jedi-* 套件零引用。 |
| **BE 主專案** | FK 字串 `project_system_characteristic.py`；raw SQL `module_frame_template_copy_service.py`（4 條）+ `smoke_test_ssp_docx_parser.py`（2 條）；測試斷言 3 檔；doc-gen / markdown（留收尾批次）。 |
| **FE / test** | 零。 |
| **live DB**（DEV introspection） | **無 view、無 function/stored procedure** 依賴這三表（消除整類風險）；8 條 incoming FK + 3 條 outgoing FK（`ALTER RENAME` 由 OID 自動跟著走，不斷）；6 個 index + 3 個 sequence + pkey/uk 舊名殘留待 rename。 |

## 技術做法

- **in-place `ALTER TABLE ... RENAME`**（非 drop/recreate）：資料保留、FK 由 OID 自動跟隨。DEV 有 2,375+ 筆資料，零丟失。
- **Lockstep 上版**：DB rename + jedi-oscal 新 ORM + app 重啟必須同步 —— 舊 ORM 對改名表、或新 ORM 對舊表都會炸（踩過 `feedback_cross_schema_fk_must_qualify`）。
- **Migration**：`scripts/sql/2026-06-10-ssp-table-rename.sql`，可攜，`psql --single-transaction -v ON_ERROR_STOP=1` 用 `cmmgr` 套；尾段 `INSERT public.schema_migrations`。表權限（GRANT cm_app）隨 table object 跟隨，不需 re-GRANT。

## 風險與緩解

| 風險 | 緩解 |
|------|------|
| 舊 app 撞改名表 → 500 / NoReferencedTableError | Lockstep：migration + 新 ORM 部署成對，每環境一起上 |
| hardcode 的 constraint 名各環境不同 | 套用前對 catalog 重新驗名（DEV 已驗，名稱 1:1 符合） |
| grep 看不到的 raw SQL 漏改 | grep gate（api/app/domain/infra）+ live API e2e smoke |
| 誤改欄位 | scope 邊界明列欄位排除；diff 驗 `-` 行無 `system_security_plan_id` |

## 驗證結果（DEV，2026-06-10）

- BEGIN/ROLLBACK 乾跑零錯 → 真實套用 DEV：9 條 FK 全 valid、資料保留（219/2375/200）。
- BE boot 無 mapper 錯誤；live API `GET /ssp/<uid>/control-implementations` 回真實資料、log 零 SQL 錯誤。
- pytest baseline diff：零新回歸（4 個既有 fail 與本次無關）。

## 狀態

- jedi-oscal ORM：commit `278bfbb`（monorepo，未 push、未發版）。
- BE 程式 + migration + 測試：commit `3ca0ad9d`（未 push）。
- **待辦**：STG/POC/PROD 套 migration（user-gated）→ jedi-oscal 發版 Nexus + BE 還原 pin（user-gated）→ doc/changelog 收尾。

實作步驟逐項見 [`implementation-plan.md`](implementation-plan.md)。
