From 4b4602a0fddf0027347301aaaff47879fe0af4ee Mon Sep 17 00:00:00 2001 From: Xiaoxia AI Date: Sun, 21 Jun 2026 06:30:55 +0800 Subject: [PATCH] refactor(schema): mark sqlalchemy models as schema mainline --- docs/SCHEMA-MAINLINE.md | 52 +++++++++++++++ docs/全面代码审计报告-2026-06-21.md | 21 +++++- init-tables.sql | 75 ++++------------------ tests/unit/test_architecture_boundaries.py | 17 +++++ 4 files changed, 102 insertions(+), 63 deletions(-) create mode 100644 docs/SCHEMA-MAINLINE.md diff --git a/docs/SCHEMA-MAINLINE.md b/docs/SCHEMA-MAINLINE.md new file mode 100644 index 000000000..81c8b0e28 --- /dev/null +++ b/docs/SCHEMA-MAINLINE.md @@ -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 快照。 diff --git a/docs/全面代码审计报告-2026-06-21.md b/docs/全面代码审计报告-2026-06-21.md index c2b6efc3e..84c917094 100644 --- a/docs/全面代码审计报告-2026-06-21.md +++ b/docs/全面代码审计报告-2026-06-21.md @@ -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` ## 四、仍需继续治理的问题 diff --git a/init-tables.sql b/init-tables.sql index bb45df1c0..07aeec681 100644 --- a/init-tables.sql +++ b/init-tables.sql @@ -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 $$; diff --git a/tests/unit/test_architecture_boundaries.py b/tests/unit/test_architecture_boundaries.py index d5481582f..61be16f14 100644 --- a/tests/unit/test_architecture_boundaries.py +++ b/tests/unit/test_architecture_boundaries.py @@ -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 == []