From 33b38f8687de4b9aa0c18a6c7e12c6f5480ba3d8 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 28 Aug 2026 12:45:58 +0800 Subject: [PATCH] docs(migration): update docs and skills for new migration architecture Update all relevant documentation and skills to reflect: - w_schema_versions shared version table with plugin_id discriminator - Single 00001_initial.sql per plugin (merged from multi-file approach) - gooseEngine uses goose.NewProvider with goose.WithStore(sharedStore) - pkg/migrator deleted, all 26 global SQL files moved to per-plugin - DDL/DML single-file approach (merged seed + schema) Files updated: - .agents/skills/database-migration/SKILL.md (full rewrite) - docs/WAVELET_WHITE_PAPER.md (table matrix + migration check) - docs/WAVELET_DEVELOPER_GUIDE.md (scenario 9) - docs/superpowers/specs/2026-08-27-cordis-plugin-architecture-design.md - docs/superpowers/specs/2026-08-27-cordis-downstream-developer-guide.md --- .agents/skills/database-migration/SKILL.md | 106 +++++++++++++++--- docs/WAVELET_DEVELOPER_GUIDE.md | 42 +++++-- docs/WAVELET_WHITE_PAPER.md | 13 ++- ...08-27-cordis-downstream-developer-guide.md | 42 +++++-- ...08-27-cordis-plugin-architecture-design.md | 26 ++++- 5 files changed, 193 insertions(+), 36 deletions(-) diff --git a/.agents/skills/database-migration/SKILL.md b/.agents/skills/database-migration/SKILL.md index 825f87a1..c652ab12 100644 --- a/.agents/skills/database-migration/SKILL.md +++ b/.agents/skills/database-migration/SKILL.md @@ -19,8 +19,7 @@ plugins/domain/order/ ├── plugin.go ├── models.go └── migrations/ - ├── 20260827000001_create_orders_table.sql - └── 20260827000002_add_order_discount_column.sql + └── 00001_initial.sql ← 每个插件仅一个初始迁移文件 ``` --- @@ -47,7 +46,9 @@ func (p *Plugin) Apply(ctx *core.Context) error { } ``` -### 步骤 2:编写 Goose SQL 脚本 (`migrations/YYYYMMDDNNNN_name.sql`) +### 步骤 2:编写 Goose SQL 脚本 (`migrations/00001_initial.sql`) + +每个插件只需维护一个 `00001_initial.sql`,包含其全部建表语句与种子数据。 ```sql -- +goose Up @@ -58,10 +59,14 @@ CREATE TABLE IF NOT EXISTS w_orders ( amount BIGINT NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMPTZ + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_w_orders_user_id ON w_orders(user_id); + +-- 种子数据 +INSERT INTO w_orders (id, user_id, amount, status) +VALUES ('init_001', 'system', 0, 'completed') +ON CONFLICT (id) DO NOTHING; -- +goose StatementEnd -- +goose Down @@ -72,39 +77,112 @@ DROP TABLE IF EXISTS w_orders; --- -## 3. 核心设计与防线原则 (Guardrails) +## 3. 版本管理与升级机制 + +### 3.1 版本表结构 + +所有插件共享一张 `w_schema_versions` 表,以 `plugin_id` 为区分: + +```sql +w_schema_versions ( + plugin_id VARCHAR(64) NOT NULL, -- 如 "auth", "user", "admin" + version_id BIGINT NOT NULL, -- 迁移文件版本号 (00001 → 1) + applied_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (plugin_id, version_id) +) +``` + +### 3.2 升级判定逻辑 + +启动时,`gooseEngine` 遍历每个已注册的插件: + +``` +for each plugin: + 1. 查询 w_schema_versions WHERE plugin_id = 'auth' + 2. 获取该插件的最大 version_id + 3. 读取插件 migration/ 目录下的所有 .sql 文件 + 4. 如果存在 version_id 更大的文件 → 执行升级 + 5. 如果全部已应用 → 跳过 +``` + +### 3.3 什么情况下升级? + +| 场景 | 例子 | 是否升级 | +|------|------|---------| +| 首次部署,插件第一次运行 | auth 插件,表不存在 | ✅ 执行 `00001_initial.sql` | +| 第二次启动,无变化 | 文件未变,版本已记录 | ❌ 跳过 | +| 追加新迁移文件 | 新增 `00002_add_index.sql` | ✅ 执行 `00002_*` | +| 移除一个插件 | 该插件不再注册 | ❌ 其记录在表中被忽略 | +| 新增一个插件 | 新插件有 `00001_initial.sql` | ✅ 执行 | + +### 3.4 查看全局迁移状态 + +```sql +SELECT * FROM w_schema_versions ORDER BY plugin_id, version_id; +``` + +输出示例: + +``` +plugin_id | version_id | applied_at +---------------------+------------+--------------------------- +admin | 1 | 2026-08-28 10:00:00+00 +auth | 1 | 2026-08-28 10:00:00+00 +user | 1 | 2026-08-28 10:00:00+00 +upload | 1 | 2026-08-28 10:00:00+00 +``` + +--- + +## 4. 核心设计与防线原则 (Guardrails) 1. **表单一所有者原则 (Single Owner Principle)**: - 每张数据表归属且仅归属于一个所有者插件(如 `w_orders` 归 `order` 插件)。 - **严禁**插件 B 跨包编写 SQL 直接读写插件 A 拥有的表;必须通过插件 A 暴露的 `contracts` 接口或事件总线进行交互。 + 2. **表名前缀规范**: - 所有表名必须带有前缀(如 `w_orders`、`w_auth_users`),杜绝跨插件表名冲突。 -3. **DDL 与 DML 分离**: - - 表结构变更(DDL)与初始数据插入(DML/Seed)必须分成两个独立的递增版本 SQL 文件。 + +3. **单文件初始迁移**: + - 每个插件只维护一个 `00001_initial.sql`,包含该插件所有表的建表语句与初始种子数据。 + - 未来如需追加 DDL,新增 `00002_xxx.sql`,Goose 会根据 `w_schema_versions` 判断增量执行。 + 4. **禁止物理外键**: - 关系字段统一显式建立单列或联合索引,禁止在数据库中创建物理外键约束。 + 5. **双方言兼容性(PostgreSQL & SQLite)**: - 自增主键:PG 用 `BIGSERIAL`,SQLite 用 `INTEGER PRIMARY KEY AUTOINCREMENT`。 - 时间类型:PG 用 `TIMESTAMPTZ`,SQLite 用 `DATETIME`。 - JSON 类型:PG 用 `JSONB`,SQLite 用 `JSON` 或 `TEXT`。 -6. **定时调度插入规范**: - - 若迁移中包含初始定时任务插入(`schedules` 表),绝对不能硬编码 `id`,必须依靠数据库自增分配。 + +6. **幂等性要求**: + - 所有 `CREATE TABLE` 必须使用 `IF NOT EXISTS`。 + - 所有 `INSERT` 种子数据必须使用 `ON CONFLICT DO NOTHING`。 + - 所有 `ALTER TABLE ADD COLUMN` 必须使用 `IF NOT EXISTS`(如果数据库方言支持)。 --- -## 4. ClickHouse 分析库迁移规则 (辅助 OLAP) +## 5. ClickHouse 分析库迁移规则 (辅助 OLAP) ClickHouse 作为辅助 OLAP 分析存储,采用独立迁移通道: -- 迁移文件位于专属目录(仅单方言 DDL,不创建 SQLite 镜像)。 -- 日志/分析用途表必须同时在关系型主库建回落表并接入 `logstore`。 +- 迁移文件位于专属目录 `migrations-clickhouse/`(仅单方言 DDL,不创建 SQLite 镜像)。 +- 日志/分析用途表必须同时在关系型主库建回落表并接入 `logstore` 门面。 - 分析表高频写入统一接入 `batchwriter` 进行异步批量刷盘。 --- -## 5. 质量与验证门禁 +## 6. 质量与验证门禁 ```bash make format make code-check go test ./plugins/... ``` + +验证迁移注册完整性: + +```bash +# 检查每个有 migrations/ 目录的插件是否同时有 go:embed + Register() +grep -rn 'go:embed.*migrations' plugins/domain/*/plugin.go plugins/drivers/*/plugin.go +grep -rn 'Migrations()\.Register' plugins/domain/*/plugin.go plugins/drivers/*/plugin.go +``` \ No newline at end of file diff --git a/docs/WAVELET_DEVELOPER_GUIDE.md b/docs/WAVELET_DEVELOPER_GUIDE.md index 07bfdbab..30703d09 100644 --- a/docs/WAVELET_DEVELOPER_GUIDE.md +++ b/docs/WAVELET_DEVELOPER_GUIDE.md @@ -222,21 +222,24 @@ func (Order) TableName() string { package order import ( - "embed" - "github.com/Rain-kl/Wavelet/core" + "embed" + "github.com/Rain-kl/Wavelet/core" ) //go:embed migrations/*.sql var orderMigrations embed.FS func (p *Plugin) Apply(ctx *core.Context) error { - // 注册本插件的专属迁移(系统启动时自动按版本号执行) - ctx.Migrations().Register("order", orderMigrations) - return nil + // 注册本插件的专属迁移(系统启动时自动按版本号执行) + ctx.Migrations().Register("order", orderMigrations) + return nil } ``` -#### SQL 迁移脚本规范 (`plugins/order/migrations/20260827000001_create_orders_table.sql`): +#### SQL 迁移脚本规范 (`plugins/order/migrations/00001_initial.sql`): + +每个插件只需维护一个 `00001_initial.sql`,包含该插件的全部建表语句与种子数据。 + ```sql -- +goose Up -- +goose StatementBegin @@ -246,10 +249,14 @@ CREATE TABLE IF NOT EXISTS w_orders ( amount BIGINT NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMPTZ + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_w_orders_user_id ON w_orders(user_id); + +-- 种子数据(使用 ON CONFLICT DO NOTHING 保证幂等) +INSERT INTO w_orders (id, user_id, amount, status, created_at, updated_at) +VALUES ('init_001', 'system', 0, 'completed', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) +ON CONFLICT (id) DO NOTHING; -- +goose StatementEnd -- +goose Down @@ -258,6 +265,25 @@ DROP TABLE IF EXISTS w_orders; -- +goose StatementEnd ``` +#### 版本管理机制 + +所有插件共享一张 `w_schema_versions` 表,以 `plugin_id` 区分: + +``` +w_schema_versions (plugin_id, version_id, applied_at) +``` + +启动时,引擎遍历每个插件: +1. 查询 `w_schema_versions WHERE plugin_id = 'order'` 获取当前最大版本号 +2. 扫描插件 `migrations/` 目录下的 `.sql` 文件 +3. 如果存在未应用的版本号 → 执行迁移 +4. 如果全部已应用 → 跳过 + +```sql +-- 查看全局迁移状态 +SELECT * FROM w_schema_versions ORDER BY plugin_id, version_id; +``` + --- ### 场景 10:如果有多个业务插件需要读写同一张表怎么办? diff --git a/docs/WAVELET_WHITE_PAPER.md b/docs/WAVELET_WHITE_PAPER.md index b1e6aed9..cbf8a5e8 100644 --- a/docs/WAVELET_WHITE_PAPER.md +++ b/docs/WAVELET_WHITE_PAPER.md @@ -91,7 +91,9 @@ Wavelet 是面向未来 5 年生产级云原生与高并发业务中台的 **微 3. **`core/contracts/` 契约隔离防线 (100% Pass)**: - 所有跨插件交互严格基于纯 Interface 和 DTO 定义(如 `contracts.DBService`, `contracts.AuthService`, `contracts.UserService`, `contracts.StorageService`),消除了 package 级别的强耦合。 4. **`plugins/` 单所有者原则与数据迁移独立性 (100% Pass)**: - - 每个业务插件自包含专有 `migrations/*.sql`,彻底消除单体大迁移目录合并冲突,杜绝 GORM AutoMigrate。 + - 每个业务插件自包含专有 `migrations/00001_initial.sql`,通过 `go:embed` 注册。 + - 所有插件共享 `w_schema_versions` 表,以 `plugin_id` 列区分版本,彻底消除单体大迁移目录合并冲突,杜绝 GORM AutoMigrate。 + - `pkg/migrator/` 全局迁移目录已物理删除,26 个全局 SQL 文件全部分配至对应插件。 5. **并发与生命周期析构安全 (100% Pass)**: - 全局遵循 LIFO (后进先出) Disposer 逆序优雅注销机制。在开启 `-race` 竞争检测下,所有事件并发广播、多协程注入与读写均 0 数据竞争。 @@ -136,12 +138,15 @@ Wavelet 是面向未来 5 年生产级云原生与高并发业务中台的 **微 | 数据表 | 唯一所有者插件 | 数据结构与仓储位置 | 跨插件交互方式 | | :--- | :--- | :--- | :--- | -| `w_users`
`w_access_tokens` | `plugins/domain/user` | `models.go`
`repository.go` | `core/contracts.UserService`
`contracts.UserDTO` | -| `w_auth_sources`
`w_external_accounts`
`w_passkeys`
`w_oauth_states` | `plugins/domain/auth` | `models.go`
`repository.go` | `core/contracts.AuthService`
`contracts.AuthRegistry` | +| `w_users` | `plugins/domain/user` | `models.go`
`repository.go` | `core/contracts.UserService`
`contracts.UserDTO` | +| `w_auth_sources`
`w_external_accounts`
`w_access_tokens` | `plugins/domain/auth` | `models.go`
`service.go` | `core/contracts.AuthService`
`contracts.AuthRegistry` | | `w_uploads`
`w_upload_stats` | `plugins/domain/upload` | `models/models.go`
`repository/repository.go` | `core/contracts.StorageService`
`upload.Ingest` 流水线 | | `w_system_configs`
`w_templates` | `plugins/domain/admin` | `models.go`
`repository.go` | `ctx.Settings()` / `contracts.ConfigService`
Redis Pub/Sub 广播 | | `w_message_channels`
`w_message_bindings`
`w_message_pairing_codes`
`w_push_events`
`w_push_channels`
`w_push_histories` | `plugins/domain/message_gateway` | `models.go`
`repository.go` | `EventBus` 强类型事件广播订阅 | -| `w_task_executions`
`w_schedules` | `plugins/drivers/driver_asynq_*` | `types.go`
`schedule.go` | `ctx.Task()` 与 `ctx.Schedule()` 扩展点 | +| `w_user_access_logs` | `plugins/domain/risk_control` | `logstore/` | `logstore` 门面
ClickHouse PG/SQLite 回落 | +| `w_schedules` | `plugins/drivers/driver_asynq_cron` | `schedule.go` | `ctx.Schedule()` 扩展点 | +| `w_task_executions` | `plugins/drivers/driver_asynq_worker` | `types.go`
`executor.go` | `ctx.Task()` 扩展点 | +| `w_schema_versions` | **系统内部** | `cmd/app.go` sharedStore | 自动管理,不归属于任何插件 | ### 5.3 架构防线与单向依赖保障 1. **测试脚手架绝对解耦**:底层通用的 `pkg/testhelper` 严禁反向引用任何上层业务插件。`testhelper` 维护轻量自包含的测试表脚手架,彻底杜绝包导入循环(Import Cycle)。 diff --git a/docs/superpowers/specs/2026-08-27-cordis-downstream-developer-guide.md b/docs/superpowers/specs/2026-08-27-cordis-downstream-developer-guide.md index 07bfdbab..30703d09 100644 --- a/docs/superpowers/specs/2026-08-27-cordis-downstream-developer-guide.md +++ b/docs/superpowers/specs/2026-08-27-cordis-downstream-developer-guide.md @@ -222,21 +222,24 @@ func (Order) TableName() string { package order import ( - "embed" - "github.com/Rain-kl/Wavelet/core" + "embed" + "github.com/Rain-kl/Wavelet/core" ) //go:embed migrations/*.sql var orderMigrations embed.FS func (p *Plugin) Apply(ctx *core.Context) error { - // 注册本插件的专属迁移(系统启动时自动按版本号执行) - ctx.Migrations().Register("order", orderMigrations) - return nil + // 注册本插件的专属迁移(系统启动时自动按版本号执行) + ctx.Migrations().Register("order", orderMigrations) + return nil } ``` -#### SQL 迁移脚本规范 (`plugins/order/migrations/20260827000001_create_orders_table.sql`): +#### SQL 迁移脚本规范 (`plugins/order/migrations/00001_initial.sql`): + +每个插件只需维护一个 `00001_initial.sql`,包含该插件的全部建表语句与种子数据。 + ```sql -- +goose Up -- +goose StatementBegin @@ -246,10 +249,14 @@ CREATE TABLE IF NOT EXISTS w_orders ( amount BIGINT NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMPTZ + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_w_orders_user_id ON w_orders(user_id); + +-- 种子数据(使用 ON CONFLICT DO NOTHING 保证幂等) +INSERT INTO w_orders (id, user_id, amount, status, created_at, updated_at) +VALUES ('init_001', 'system', 0, 'completed', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) +ON CONFLICT (id) DO NOTHING; -- +goose StatementEnd -- +goose Down @@ -258,6 +265,25 @@ DROP TABLE IF EXISTS w_orders; -- +goose StatementEnd ``` +#### 版本管理机制 + +所有插件共享一张 `w_schema_versions` 表,以 `plugin_id` 区分: + +``` +w_schema_versions (plugin_id, version_id, applied_at) +``` + +启动时,引擎遍历每个插件: +1. 查询 `w_schema_versions WHERE plugin_id = 'order'` 获取当前最大版本号 +2. 扫描插件 `migrations/` 目录下的 `.sql` 文件 +3. 如果存在未应用的版本号 → 执行迁移 +4. 如果全部已应用 → 跳过 + +```sql +-- 查看全局迁移状态 +SELECT * FROM w_schema_versions ORDER BY plugin_id, version_id; +``` + --- ### 场景 10:如果有多个业务插件需要读写同一张表怎么办? diff --git a/docs/superpowers/specs/2026-08-27-cordis-plugin-architecture-design.md b/docs/superpowers/specs/2026-08-27-cordis-plugin-architecture-design.md index 8ed4a2df..ab6b30cb 100644 --- a/docs/superpowers/specs/2026-08-27-cordis-plugin-architecture-design.md +++ b/docs/superpowers/specs/2026-08-27-cordis-plugin-architecture-design.md @@ -123,13 +123,30 @@ type RouterExtension interface { ### 4.2 数据迁移扩展 (`ctx.Migrations()`) 每个插件通过 Go 内置 `embed.FS` 打包专属的 Goose SQL 文件,彻底消除单体大迁移目录的合并冲突: + ```go type MigrationExtension interface { // Register 注册插件专属的 SQL 迁移文件系统 - Register(pluginID string, fs embed.FS) + Register(pluginID string, fsys fs.FS, dir ...string) } ``` +**版本隔离机制**:所有插件共享一张 `w_schema_versions` 表,以 `plugin_id` 列区分。运行时引擎(`gooseEngine`)实现 `goosedb.Store` 接口,对该表执行 `plugin_id` 限定的 CRUD 操作,确保各插件的版本互不干扰。 + +```sql +w_schema_versions ( + plugin_id VARCHAR(64) NOT NULL, + version_id BIGINT NOT NULL, + applied_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (plugin_id, version_id) +) +``` + +**启动流程**: +1. `ApplyPlugins()` 阶段:各插件调用 `ctx.Migrations().Register("order", embedFS)` 收集迁移 +2. `RunMigrations()` 阶段:引擎遍历所有 `entries`,为每个插件创建 `goose.NewProvider(dialect, sqlDB, entry.FS, goose.WithStore(store))` +3. `provider.Up()` 查询 `w_schema_versions WHERE plugin_id = 'order'` 决定版本,执行增量迁移 + ### 4.3 异步任务与定时调度扩展 (`ctx.Task()` & `ctx.Schedule()`) ```go type TaskExtension interface { @@ -211,8 +228,13 @@ CLI 命令仅作为**切面激活器 (Target Selector)**,业务插件无需感 1. App Bootstrap: 加载所有已配置插件并构建 Context ↓ 2. Apply Phase: 执行所有 plugin.Apply(ctx),收集路由、任务、调度与迁移 + └─ 各插件调用 ctx.Migrations().Register("auth", authMigrations) 等 ↓ -3. Migration Engine: 统一执行所有已激活插件的 Goose SQL 迁移 +3. Migration Engine: 遍历所有 entries,逐插件创建 Goose Provider 执行迁移 + └─ 每个插件使用独立的 sharedStore(pluginID),共享同一张 w_schema_versions 表 + └─ provider.Up() 检查 w_schema_versions WHERE plugin_id = 'auth' + └─ 未执行过 → 执行 00001_initial.sql → INSERT 版本记录 + └─ 已执行过 → 跳过 ↓ 4. Profile Dispatch: - "api": 激活 DriverTypeHTTP 驱动 (Gin.ListenAndServe)