refactor(schema): mark sqlalchemy models as schema mainline

This commit is contained in:
Xiaoxia AI
2026-06-21 06:30:55 +08:00
parent e61dfc27f6
commit 4b4602a0fd
4 changed files with 102 additions and 63 deletions
+52
View File
@@ -0,0 +1,52 @@
# Schema Mainline
## 当前结论
小虾 SaaS 当前运行时数据库结构的唯一主线是:
- `packages/adapters/sqlalchemy_impl/models.py`
- `packages/adapters/sqlalchemy_impl/session.py::initialize_database()`
`initialize_database()` 使用 SQLAlchemy `Base.metadata.create_all()` 创建缺失表,并通过 PostgreSQL advisory lock 避免多实例并发初始化。
## 已废弃入口
以下文件不得用于 staging / production 建库:
- `init-tables.sql`
- `migrations/001_initial_schema.sql`
- `migrations/004_asset_management.sql`
这些 SQL 文件是历史快照,和当前 SQLAlchemy runtime schema 已经存在字段漂移。例如:
- 历史 `init-tables.sql` 使用 `assets.library_id/storage_key/mime_type`
- 当前 SQLAlchemy 使用 `assets.asset_library_id/file_url/file_type`
- 历史 SQL 文件没有完整覆盖 `generation_tasks/generated_videos/tasks/milestones/task_issues`
## 过渡原则
在正式引入 Alembic 前:
1. 运行时只允许 SQLAlchemy models 创建表。
2. 新字段必须先改 `packages/adapters/sqlalchemy_impl/models.py`
3. Repository 映射必须和 SQLAlchemy model 同步。
4. Pydantic schema 只能表达 API 契约,不作为数据库真源。
5. Domain dataclass 只能表达业务实体,不作为数据库真源。
6. 历史 SQL 文件只允许作为参考,不允许部署脚本调用。
## 下一步:Alembic 化
后续应建立 Alembic 正式迁移链:
1. 以当前 staging 数据库实际结构生成 baseline revision。
2. 以 SQLAlchemy models 作为 autogenerate metadata。
3. 之后所有 schema 变更必须走 Alembic revision。
4. CI 增加迁移检查:`alembic upgrade head`
5. 停止在生产入口调用 `Base.metadata.create_all()`,只保留开发/测试兜底。
## 禁止事项
- 不要新增 `init-*.sql` 作为运行时建表入口。
- 不要手工维护和 SQLAlchemy models 平行的 CREATE TABLE 文件。
- 不要让 API schema 或 domain entity 直接驱动数据库 schema。
- 不要在部署脚本中执行历史 SQL 快照。
+20 -1
View File
@@ -228,6 +228,25 @@
- `.gitignore` 增加 `.venv-ci-root/`
- 从 Git 索引移除 `.venv-ci-root`,本地文件保留。
### 13. P1:数据库 Schema 多真源漂移
**涉及文件**`init-tables.sql``migrations/*.sql``packages/adapters/sqlalchemy_impl/models.py``docs/SCHEMA-MAINLINE.md`
**问题**
- `init-tables.sql``migrations/*.sql`、SQLAlchemy models 同时定义表结构。
- 字段已经漂移,例如历史 SQL 使用 `assets.library_id/storage_key/mime_type`,当前 SQLAlchemy 使用 `assets.asset_library_id/file_url/file_type`
- 运行时实际由 SQLAlchemy `Base.metadata.create_all()` 建表,但文档仍引用历史 SQL。
**根因**
- Phase 迁移过程中 SQL 快照没有退役。
- Alembic 迁移链尚未建立,SQLAlchemy models 临时成为事实真源。
**修复**
- `init-tables.sql` 改为 deprecated sentinel,误执行会直接失败。
- 新增 `docs/SCHEMA-MAINLINE.md`,明确当前 schema 唯一主线是 SQLAlchemy models。
- 架构守卫禁止运行时代码引用 `init-tables.sql` 或历史 `migrations/001_initial_schema.sql`
- 明确下一步 Alembic 化路线。
## 三、已补充测试
### 新增
@@ -251,7 +270,7 @@ python -m pytest tests/unit/test_architecture_boundaries.py tests/unit/test_auth
python -m pytest tests/unit/test_login_use_case.py tests/unit/test_register_user_use_case.py tests/unit/test_password_reset_use_case.py tests/unit/test_invite_member_use_case.py tests/unit/test_session_store.py tests/unit/test_email_service.py -q
```
结果:`33 passed``52 passed`
结果:`34 passed``52 passed`
## 四、仍需继续治理的问题
+13 -62
View File
@@ -1,63 +1,14 @@
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(255) PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(255),
display_name VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
email_verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Deprecated schema snapshot.
--
-- Runtime schema creation is currently owned by SQLAlchemy models in:
-- packages/adapters/sqlalchemy_impl/models.py
--
-- Do not apply this file to staging or production. It is retained only for
-- historical reference while the project migrates toward a proper Alembic flow.
-- Applying it would create columns such as assets.library_id/storage_key/mime_type
-- that conflict with the current runtime table shape.
CREATE TABLE IF NOT EXISTS workspaces (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
owner_user_id VARCHAR(255) NOT NULL REFERENCES users(id),
subscription_plan VARCHAR(50) DEFAULT 'free',
subscription_status VARCHAR(50) DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS projects (
id VARCHAR(255) PRIMARY KEY,
workspace_id VARCHAR(255) NOT NULL REFERENCES workspaces(id),
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS assets (
id VARCHAR(255) PRIMARY KEY,
workspace_id VARCHAR(255) NOT NULL,
project_id VARCHAR(255) NOT NULL,
library_id VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
storage_key VARCHAR(500) NOT NULL,
mime_type VARCHAR(100) NOT NULL,
file_size BIGINT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS generation_tasks (
id VARCHAR(255) PRIMARY KEY,
workspace_id VARCHAR(255) NOT NULL,
project_id VARCHAR(255) NOT NULL,
asset_library_id VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
progress FLOAT DEFAULT 0,
result_count INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS generated_videos (
id VARCHAR(255) PRIMARY KEY,
workspace_id VARCHAR(255) NOT NULL,
project_id VARCHAR(255) NOT NULL,
generation_task_id VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
file_url VARCHAR(500) NOT NULL,
file_size BIGINT,
duration FLOAT,
width INTEGER,
height INTEGER,
fps FLOAT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
DO $$
BEGIN
RAISE EXCEPTION 'init-tables.sql is deprecated. Use SQLAlchemy runtime schema initialization / future Alembic migrations instead.';
END $$;
@@ -42,3 +42,20 @@ def test_runtime_code_does_not_import_deprecated_postgres_adapters():
offenders.append(str(path))
assert offenders == []
def test_legacy_sql_schema_files_are_not_runtime_entrypoints():
init_tables = Path("init-tables.sql").read_text(encoding="utf-8")
assert "RAISE EXCEPTION" in init_tables
assert "deprecated" in init_tables.lower()
offenders: list[str] = []
for root in [Path("apps"), Path("packages"), Path("infra")]:
for pattern in ("*.py", "*.sh", "*.yml", "*.yaml", "*.md"):
for path in root.rglob(pattern):
text = path.read_text(encoding="utf-8", errors="ignore")
if "init-tables.sql" in text or "migrations/001_initial_schema.sql" in text:
offenders.append(str(path))
assert offenders == []