dda8264057
记录 Stage 0 PR3 的实施落点、LOCK 决策、跟进项与 6 commit 列表。 验收:3134 passed + 25 skipped + 0 failed(PR2 末 3087 + 47 新 ws_context/ ws_repo/membership_repo 测试 + 2 PG-only skipped)。RDS live 验证通过: workspaces / workspace_memberships 表 + partial unique on owner 都建出来。 T3.6 (membership ORM) 提前到 T3.4-T3.5 (workspace repo) 之前,因为 WorkspaceRepository.get/list_by_user 会 JOIN memberships。 Stage 0 PR3 T3.10. PR3 完成。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
5.4 KiB
Markdown
88 lines
5.4 KiB
Markdown
# PR3 · workspaces + workspace_memberships 表 + 仓储
|
||
|
||
> 实现笔记。对应 [docs/superpowers/plans/2026-05-10-stage-0-multi-tenant-foundation.md](../../superpowers/plans/2026-05-10-stage-0-multi-tenant-foundation.md) PR3(T3.1-T3.10)。
|
||
>
|
||
> 状态:**已落地**,9 commits 提交在 branch `feat/stage-0-pr3-workspaces`。
|
||
|
||
## 范围
|
||
|
||
新建 `workspaces` + `workspace_memberships` 两张表 + 仓储 + ContextVar + 47 个单测。这是 Stage 0 schema 基础设施的**核心**——后续 PR4 注册改造、PR5 ALTER 现有表、PR6 入口校验都依赖这层。
|
||
|
||
不在范围(后续 PR):
|
||
- 注册流程改造 / JWT 加 wid+role(PR4)
|
||
- alembic revision(PR4 起,因 ALTER 现有表才需要)
|
||
- workspace_id 列加到 threads_meta 等(PR5)
|
||
- 入口路由 `(wid, tid)` 校验(PR6)
|
||
|
||
## 验收
|
||
|
||
- [x] 47 个新单测全过(16 workspace_context + 23 workspace_repo + 8 membership_repo)
|
||
- [x] 全套 `pytest tests/` 3134 passed + 25 skipped + 0 failed(PR2 末 3087 + 47 新;+2 PG-only skip)
|
||
- [x] partial unique on owner 在 SQLite ephemeral DB(test_cannot_add_second_owner)+ 远程 RDS PG 17.9(live 验证)双驱动通过
|
||
- [x] CASCADE 在 SQLite 上验过(test_delete_cascades_to_memberships / test_cascade_delete_user_removes_memberships)
|
||
- [x] RDS 上 `workspaces` + `workspace_memberships` 表 + 索引(含 partial unique)live create_all 出来:
|
||
```
|
||
idx_one_owner_per_workspace
|
||
UNIQUE INDEX ... (workspace_id) WHERE ((role)::text = 'owner'::text)
|
||
idx_workspace_memberships_user (user_id, workspace_id)
|
||
workspace_memberships_pkey UNIQUE (workspace_id, user_id)
|
||
```
|
||
- [ ] CI workflow 上跑 @pytest.mark.postgres → 实跑 partial-unique PG twin — **待 PR push 后 GH Actions 验**
|
||
|
||
## 关键决策(LOCK 项 — 与 plan §横切 + workspace-schema-design §5 对齐)
|
||
|
||
| 项 | 选择 | 理由 |
|
||
|---|---|---|
|
||
| 表名 | `workspaces` + `workspace_memberships` | workspace-schema-design 锁定 |
|
||
| ID 类型 | `String(36)` UUID v4 字符串 | 与 `users.id` 类型对齐,跨 SQLite/PG 可移植 |
|
||
| Membership PK | 复合 `(workspace_id, user_id)` | 工业实践默认;surrogate id 没必要 |
|
||
| Role 类型 | `String(16)` + 应用层校验 | 不用 PG enum(ALTER TYPE 加值痛苦);Stage 0 写 owner,Stage 2 开 admin/member |
|
||
| Partial unique on owner | `Index(..., sqlite_where=text(...), postgresql_where=text(...))` 双 where 并存 | SQLite + PG 都支持;T3.8 PG twin 验语义一致 |
|
||
| Owner FK 删除策略 | `ON DELETE RESTRICT` | 删 user 时阻拦(必须先转让 owner);transfer 流程放 PR4 |
|
||
| Membership FK 删除策略 | `ON DELETE CASCADE` (workspace_id + user_id) | 删 workspace / 删 user 时自动清成员;逻辑上正确 |
|
||
| `invited_by` 删除策略 | `ON DELETE SET NULL` | 邀请人离开不影响成员;只清字段 |
|
||
| Slug 校验位置 | 应用层 `_validate_slug` + DB UNIQUE | DB 不知道 regex / 黑名单;应用层规则可演进 |
|
||
| Slug 黑名单 | 25 个保留字(admin/api/auth/...)frozenset 在 workspace/sql.py | workspace-schema-design §2.1 完整列表 |
|
||
| WorkspaceRepository.create | 不自动建 owner 成员 | 单一职责;注册流程在事务内同时写 workspace + membership |
|
||
| WorkspaceRepository.get | JOIN memberships 做 access 检查 | 默认安全;显式 `user_id=None` 跳过用于迁移 |
|
||
| WorkspaceRepository.get_by_slug | **不**做 access 检查 | path-based routing 用:先 slug→wid 再 route handler 查成员 |
|
||
| Owner transfer | 不在仓储层实现 | 需要两行事务原子 swap,应在 auth router 内带权限检查 |
|
||
|
||
## 跟进项(不在 PR3 范围)
|
||
|
||
- PR4 注册流程:`/auth/initialize` + `/auth/register` 用 `WorkspaceRepository.create + WorkspaceMembershipRepository.add(role='owner')` 在一个事务里建
|
||
- PR4 alembic 0001:加 `users.default_workspace_id` 列(也 FK 到 workspaces)
|
||
- PR4 `_auto_workspace_context` autouse fixture:现在 tests/test_workspace_repo 测试自己 set/reset 而不依赖 autouse;PR4 引入后简化
|
||
- backend/CLAUDE.md:等 PR4 完成时一并补 Database + Workspace 段(PR2 follow-up + 现在的 schema)
|
||
|
||
## 涉及文件
|
||
|
||
| 类别 | 文件 |
|
||
|---|---|
|
||
| ContextVar | `runtime/workspace_context.py` |
|
||
| ORM | `persistence/workspace/{__init__.py, model.py, sql.py}` |
|
||
| ORM | `persistence/workspace_membership/{__init__.py, model.py, sql.py}` |
|
||
| 注册 | `persistence/models/__init__.py`(加 2 个 import) |
|
||
| 测试 | `tests/test_workspace_context.py`(16 test) |
|
||
| 测试 | `tests/test_workspace_repo.py`(23 test) |
|
||
| 测试 | `tests/test_workspace_membership_repo.py`(8 test) |
|
||
| 测试 | `tests/test_workspace_partial_unique_postgres.py`(2 PG-only) |
|
||
|
||
## 提交记录
|
||
|
||
9 commits(含 T3.9 live verification 不发 commit):
|
||
|
||
```
|
||
f63089ae feat(runtime): add workspace_context module + tests T3.1 + T3.2
|
||
8323bf68 feat(persistence): add WorkspaceRow ORM model T3.3
|
||
d2d2d29c feat(persistence): add WorkspaceMembershipRow ORM model T3.6
|
||
a40df035 feat(persistence): WorkspaceRepository + 23 unit tests T3.4 + T3.5
|
||
36ffe271 feat(persistence): WorkspaceMembershipRepository + 8 unit tests T3.7
|
||
3313047f test(workspace): partial unique on owner — Postgres twin test T3.8
|
||
```
|
||
|
||
T3.6 比 T3.4/T3.5 早提(plan 中 T3.4-T3.5 写仓储要 JOIN memberships,所以 T3.6 必须先)。
|
||
|
||
T3.9 是 live verification,不产 commit;记在本文档的"验收"段。
|
||
T3.10(impl doc)即本文件。
|