Files
OpenFlare/.agents/skills/database-migration/SKILL.md
T
ryan f2ab94501c refactor(layout): relocate go.mod to backend/ and clean import paths to module root
- Relocated go.mod and go.sum into backend/ root directory
- Stripped redundant backend/ segments from all Go imports (github.com/Rain-kl/Wavelet/...)
- Unified Makefile, swagger, and build-test to execute in backend/ module context
- Ensured 100% build-test, code-check, format, and swagger pass
2026-08-28 13:01:51 +08:00

6.4 KiB
Raw Blame History

name, description
name description
database-migration Wavelet 项目专用:当新增或修改数据库表结构、索引、初始化数据、插件自包含 Goose SQL 迁移、embed.FS 注册、PG/SQLite 双方言支持或 ClickHouse 分析库 DDL 时必须使用。

数据库独立迁移与表结构开发规范 (Cordis 插件化架构)

本技能是 Wavelet 在 Cordis 微内核与插件化架构下,进行数据库表结构设计、Goose SQL 迁移与插件嵌入式注册的唯一指导规范。


1. 核心架构:插件自包含迁移 (Self-Contained Migrations)

在 Cordis 架构中,彻底告别集中式单体大迁移目录。 每个插件在自身包内维护专属的 migrations/ 目录,通过 Go 语言内置 //go:embed 打包为嵌入式文件系统,并在 Apply(ctx *core.Context) 时通过微内核扩展点 ctx.Migrations().Register(...) 自主注入。

backend/plugins/domain/order/
├── plugin.go
├── models.go
└── migrations/
    └── 00001_initial.sql        ← 每个插件仅一个初始迁移文件

2. 插件迁移代码集成标准

步骤 1:在插件内嵌入并注册迁移

package order

import (
	"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
}

步骤 2:编写 Goose SQL 脚本 (migrations/00001_initial.sql)

每个插件只需维护一个 00001_initial.sql,包含其全部建表语句与种子数据。

-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS w_orders (
    id VARCHAR(64) PRIMARY KEY,
    user_id VARCHAR(64) NOT NULL,
    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
);
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
-- +goose StatementBegin
DROP TABLE IF EXISTS w_orders;
-- +goose StatementEnd

3. 版本管理与升级机制

3.1 版本表结构

所有插件共享一张 w_schema_versions 表,以 plugin_id 为区分:

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 查看全局迁移状态

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. 单文件初始迁移:

    • 每个插件只维护一个 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. 幂等性要求:

    • 所有 CREATE TABLE 必须使用 IF NOT EXISTS。
    • 所有 INSERT 种子数据必须使用 ON CONFLICT DO NOTHING。
    • 所有 ALTER TABLE ADD COLUMN 必须使用 IF NOT EXISTS(如果数据库方言支持)。

5. ClickHouse 分析库迁移规则 (辅助 OLAP)

ClickHouse 作为辅助 OLAP 分析存储,采用独立迁移通道:

  • 迁移文件位于专属目录 migrations-clickhouse/(仅单方言 DDL,不创建 SQLite 镜像)。
  • 日志/分析用途表必须同时在关系型主库建回落表并接入 logstore 门面。
  • 分析表高频写入统一接入 batchwriter 进行异步批量刷盘。

6. 质量与验证门禁

make format
make code-check
go test ./plugins/...

验证迁移注册完整性:

# 检查每个有 migrations/ 目录的插件是否同时有 go:embed + Register()
grep -rn 'go:embed.*migrations' backend/plugins/domain/*/plugin.go backend/plugins/drivers/*/plugin.go
grep -rn 'Migrations()\.Register' backend/plugins/domain/*/plugin.go backend/plugins/drivers/*/plugin.go