-- 0005_create_agents.up.sql -- -- 個人設備管理走向 A'(ADR-018 Accepted):新增 agents 實體(一條已配對的 tunnel 連線 -- = 一個 agent);devices 語意轉為「一顆實體 USB」,掛 agent_id。 -- 對齊 .autoflow/04-architecture/personal-device-mgmt-data-model.md §2.3 / §2.6 -- 與 docs/autoflow/04-architecture/adr/adr-018-agent-device-model.md §5.1–§5.4, -- 精確 SQL 規格見 .autoflow/04-architecture/migration-0005-spec.md。 -- -- 環境事實(與 0001–0003 相同,已驗證):PostgreSQL 14.23,gen_random_uuid() 內建可直接用。 -- -- 不碰的東西(走向 A' 核心,避開破壞性 migration): -- - session_tokens.device_id 的 FK / NOT NULL:物理 schema 一字不動(改綁 representative -- device 是純語意變更,不需 DDL)。 -- - uq_devices_owner_serial_active:現有 partial unique 已是 task 6 去重正確基礎,不動。 -- ── (1) agents 表 ──────────────────────────────────────────────────────────── CREATE TABLE agents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), owner_user_id UUID NOT NULL REFERENCES users(id), name TEXT NOT NULL DEFAULT 'local-agent', platform TEXT, -- darwin/windows/linux(agent 上報,可空) agent_version TEXT, -- agent 版本(可空) last_paired_at TIMESTAMPTZ, -- 最近一次配對完成時間 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ ); -- owner-scoped active index(List agents by owner;沿用 0002 partial 慣例)。 CREATE INDEX idx_agents_owner_active ON agents (owner_user_id) WHERE deleted_at IS NULL; -- ── (2) devices 加欄位(語意轉為「實體 USB」)──────────────────────────────── -- 全部 nullable(或有 DEFAULT),對既有 row 零破壞:既有 device 加欄後 agent_id=NULL、 -- registered_at=NULL、is_representative=false,語意 = 「尚未歸入 agent 的舊資料」, -- 由 (3) data migration 補齊。 ALTER TABLE devices ADD COLUMN agent_id UUID REFERENCES agents(id); -- NULL=代表 device/舊資料 ALTER TABLE devices ADD COLUMN agent_local_device_id TEXT; -- local agent 合成 id(kl520-0),路由除錯輔助 ALTER TABLE devices ADD COLUMN registered_at TIMESTAMPTZ; -- NULL=未註冊(註冊軸) ALTER TABLE devices ADD COLUMN is_representative BOOLEAN NOT NULL DEFAULT false;-- true=agent 佔位/代表 device(非真 USB) -- agent_id 反查(List devices by agent;只索引未刪除)。 CREATE INDEX idx_devices_agent_active ON devices (agent_id) WHERE deleted_at IS NULL; -- 註冊軸 filter(task 3 黃色「未註冊」/ task 4 filter「只看未註冊」;只索引未刪除)。 CREATE INDEX idx_devices_registered ON devices (registered_at) WHERE deleted_at IS NULL; -- ── (3) data migration:既有 device(皆 serial=NULL 的連線佔位)→ 1:1 建 agent + 標 representative ── -- 純 SQL 兩句對應(本 runner 是 golang-migrate iofs,只跑 SQL,無 Go 逐筆 hook)。 -- -- 決定性推導方案(agent.id := device.id):讓每筆現有 device 產生的 agent 用「與 device 相同 -- 的 UUID」。agents 與 devices 主鍵空間獨立(不同表),沿用同一 UUID 值不衝突;且讓 3b 的 -- 回填變成「device.agent_id := device.id」的自我對應,一句 UPDATE 完成、無關聯歧義、天然冪等。 -- 取捨與替代方案見 migration-0005-spec.md §2.1 / §5 R-A(使用者已裁決採此方案)。 -- 3a. 對每筆現有未刪除 device,各建一筆 agent(id 沿用來源 device.id)。 INSERT INTO agents (id, owner_user_id, name, last_paired_at, created_at, updated_at) SELECT d.id, d.owner_user_id, 'local-agent', d.paired_at, d.created_at, now() FROM devices d WHERE d.deleted_at IS NULL; -- 3b. 回填:每筆現有 device 綁到「用它自己 id 建出來的 agent」,並標記為 representative。 -- (現有 device 本來就是「連線佔位」而非真 USB,serial 皆 NULL,故全部 is_representative=true。) UPDATE devices d SET agent_id = d.id, -- 對應 3a:agent.id 就是 device.id is_representative = true, updated_at = now() WHERE d.deleted_at IS NULL AND d.agent_id IS NULL; -- 冪等保護:已綁過的不重綁 -- 註:已 soft-deleted 的舊 device 不建 agent、不回填(它們已退出使用;未來若復活走正常 exchange 流程)。 -- 真實 USB device 在本次遷移後「還不存在」——等 agent 下次 exchange 帶 serial 上來(WP-0) -- 或 task 5 註冊時才建立,這符合「舊資料無序號」的事實(ADR-018 §4.3)。