refactor(schema): mark sqlalchemy models as schema mainline
This commit is contained in:
@@ -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 快照。
|
||||
@@ -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
@@ -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 == []
|
||||
|
||||
Reference in New Issue
Block a user