mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 16:16:37 +08:00
docs(agents): update AGENTS.md and development skills for cordis architecture
This commit is contained in:
@@ -1,138 +1,110 @@
|
||||
---
|
||||
name: "database-migration"
|
||||
description: "Wavelet 项目专用:当新增或修改数据库表结构、索引、初始化数据、系统配置 seed、模板 seed、默认管理员、goose SQL 迁移、internal/infra/persistence/migrator、ClickHouse 分析库 DDL 或数据库升级流程时必须使用。本技能指导在 internal/infra/persistence/migrator/goose 下编写 PostgreSQL/SQLite 双方言 SQL 迁移,以及在 goose/clickhouse 下编写 ClickHouse 单方言分析表迁移,并完成验证。"
|
||||
description: "Wavelet 项目专用:当新增或修改数据库表结构、索引、初始化数据、插件自包含 Goose SQL 迁移、embed.FS 注册、PG/SQLite 双方言支持或 ClickHouse 分析库 DDL 时必须使用。"
|
||||
---
|
||||
|
||||
# Wavelet 数据库升级操作指南
|
||||
# 数据库独立迁移与表结构开发规范 (Cordis 插件化架构)
|
||||
|
||||
Wavelet 使用 `github.com/pressly/goose/v3` 执行 SQL 迁移。迁移入口是 `internal/infra/persistence/migrator.Migrate()`,SQL 文件嵌入在二进制中。
|
||||
本技能是 Wavelet 在 Cordis 微内核与插件化架构下,进行数据库表结构设计、Goose SQL 迁移与插件嵌入式注册的唯一指导规范。
|
||||
|
||||
## 基本规则
|
||||
---
|
||||
|
||||
- SQL 迁移文件放在:
|
||||
- `internal/infra/persistence/migrator/goose/postgres/`
|
||||
- `internal/infra/persistence/migrator/goose/sqlite/`
|
||||
- PostgreSQL 和 SQLite 必须使用同一个版本号、同一个语义文件名。
|
||||
- 迁移文件使用 goose SQL 标记:
|
||||
## 1. 核心架构:插件自包含迁移 (Self-Contained Migrations)
|
||||
|
||||
在 Cordis 架构中,**彻底告别集中式单体大迁移目录**。
|
||||
每个插件在自身包内维护专属的 `migrations/` 目录,通过 Go 语言内置 `//go:embed` 打包为嵌入式文件系统,并在 `Apply(ctx *core.Context)` 时通过微内核扩展点 `ctx.Migrations().Register(...)` 自主注入。
|
||||
|
||||
```
|
||||
plugins/domain/order/
|
||||
├── plugin.go
|
||||
├── models.go
|
||||
└── migrations/
|
||||
├── 20260827000001_create_orders_table.sql
|
||||
└── 20260827000002_add_order_discount_column.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 插件迁移代码集成标准
|
||||
|
||||
### 步骤 1:在插件内嵌入并注册迁移
|
||||
|
||||
```go
|
||||
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/YYYYMMDDNNNN_name.sql`)
|
||||
|
||||
```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,
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_w_orders_user_id ON w_orders(user_id);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
...
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE IF EXISTS w_orders;
|
||||
-- +goose StatementEnd
|
||||
```
|
||||
|
||||
- 不要把表结构、默认系统配置、默认模板、默认管理员初始化写回 Go 代码。
|
||||
- 编辑表结构(DDL)和插入表数据(DML/Seed)不要放在同一个 SQL 文件里,必须分成两个独立的 SQL 文件完成(例如,先通过一个文件修改表结构,再通过下一个递增版本号的文件插入/初始化数据)。
|
||||
- 插入定时任务(schedules 表数据)时绝对不能指定 `id`,必须依靠数据库自增(Identity 或 AUTOINCREMENT)自动分配,防止与用户手动或后续插入的定时任务产生 ID 冲突。
|
||||
- 不要添加物理外键;关系字段使用显式索引。
|
||||
- 数据库默认值应匹配 Go model 零值或业务兜底值。
|
||||
- 系统配置仍然保存字符串值;布尔值写 `"true"` / `"false"`,数字写十进制字符串,复杂结构写合法 JSON 字符串。
|
||||
---
|
||||
|
||||
## 新增迁移流程
|
||||
## 3. 核心设计与防线原则 (Guardrails)
|
||||
|
||||
1. 先确认涉及的 Go model、读写路径和前端/接口消费方。
|
||||
2. 选择下一个递增版本号,格式建议 `YYYYMMDDNNNN`,例如:
|
||||
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 文件。
|
||||
4. **禁止物理外键**:
|
||||
- 关系字段统一显式建立单列或联合索引,禁止在数据库中创建物理外键约束。
|
||||
5. **双方言兼容性(PostgreSQL & SQLite)**:
|
||||
- 自增主键:PG 用 `BIGSERIAL`,SQLite 用 `INTEGER PRIMARY KEY AUTOINCREMENT`。
|
||||
- 时间类型:PG 用 `TIMESTAMPTZ`,SQLite 用 `DATETIME`。
|
||||
- JSON 类型:PG 用 `JSONB`,SQLite 用 `JSON` 或 `TEXT`。
|
||||
6. **定时调度插入规范**:
|
||||
- 若迁移中包含初始定时任务插入(`schedules` 表),绝对不能硬编码 `id`,必须依靠数据库自增分配。
|
||||
|
||||
```text
|
||||
202606090002_add_example_column.sql
|
||||
```
|
||||
---
|
||||
|
||||
3. 在 PostgreSQL 和 SQLite 目录各新增同名 SQL 文件。
|
||||
4. 写 `Up`:
|
||||
- 表结构变更使用 SQL DDL。
|
||||
- 初始化/seed 数据使用 SQL `INSERT`。
|
||||
- 需要幂等时使用 `IF NOT EXISTS` 或 `ON CONFLICT ... DO NOTHING`。
|
||||
5. 写 `Down`:
|
||||
- 能安全回滚的结构变更写反向 DDL。
|
||||
- seed 数据按 key/name 等稳定标识删除。
|
||||
6. 如果变更 API handler,运行 `make swagger`。
|
||||
7. 至少运行:
|
||||
## 4. ClickHouse 分析库迁移规则 (辅助 OLAP)
|
||||
|
||||
ClickHouse 作为辅助 OLAP 分析存储,采用独立迁移通道:
|
||||
- 迁移文件位于专属目录(仅单方言 DDL,不创建 SQLite 镜像)。
|
||||
- 日志/分析用途表必须同时在关系型主库建回落表并接入 `logstore`。
|
||||
- 分析表高频写入统一接入 `batchwriter` 进行异步批量刷盘。
|
||||
|
||||
---
|
||||
|
||||
## 5. 质量与验证门禁
|
||||
|
||||
```bash
|
||||
go test ./internal/infra/persistence/migrator
|
||||
go test ./internal/model ./internal/apps/config ./internal/apps/admin/system_config
|
||||
make format
|
||||
make code-check
|
||||
go test ./plugins/...
|
||||
```
|
||||
|
||||
## 方言注意事项
|
||||
|
||||
- PostgreSQL 自增主键用 `BIGSERIAL`;SQLite 自增主键用 `INTEGER PRIMARY KEY AUTOINCREMENT`。
|
||||
- PostgreSQL 时间类型优先 `TIMESTAMPTZ`;SQLite 使用 `DATETIME`。
|
||||
- PostgreSQL JSON 字段用 `JSONB`;SQLite 用 `JSON` 或 `TEXT`。
|
||||
- 两个方言目录的字段名、索引名、seed 数据语义必须保持一致。
|
||||
|
||||
## 修改默认系统配置
|
||||
|
||||
- 新增或调整系统配置 seed 时,更新两个方言的 SQL 文件。
|
||||
- `visibility` 使用常量语义:`0` 不公开,`1` 通过 `/api/v1/config/public` 返回。
|
||||
- 公共配置 API 直接返回所有 `visibility = 1` 的配置键值,不要在 handler 中重新硬编码 key 列表。
|
||||
|
||||
## 验证重点
|
||||
|
||||
- goose 能在空库上完整执行。
|
||||
- `system_configs`、默认 `admin`、内置模板能按预期初始化。
|
||||
- 新增表/列与 Go model 的列名、类型和默认值兼容。
|
||||
- 前端或接口消费的公共配置值仍按字符串解析。
|
||||
|
||||
## ClickHouse 分析库(辅助 OLAP)
|
||||
|
||||
ClickHouse 是**辅助 OLAP 存储**,与 PostgreSQL/SQLite 主库**完全独立**的迁移与访问管线:
|
||||
|
||||
- 主库(PG/SQLite):业务事务数据、`goose_db_version`、双方言 SQL。
|
||||
- 分析库(ClickHouse):分析型数据、`goose_clickhouse_version`、单方言 SQL。日志用途表还必须在主库建回落并走 `logstore`(见该 skill);CH 目录仍只放 CH DDL。
|
||||
|
||||
**不要**把 ClickHouse 表结构混入 PG/SQLite 迁移目录,也**不要**在 `support-files/`、`internal/apps/` 或 `internal/repository/` 中手写 DDL。
|
||||
|
||||
### 目录与职责
|
||||
|
||||
| 路径 | 职责 |
|
||||
| :--- | :--- |
|
||||
| `internal/infra/persistence/migrator/goose/clickhouse/` | **唯一** ClickHouse DDL 来源(goose SQL,嵌入二进制) |
|
||||
| `internal/model/analytics/` | 分析表 Go model,列名须与 goose DDL 一致 |
|
||||
| `internal/repository/analytics/` | 所有 ClickHouse 读写(批量写入、查询、聚合) |
|
||||
| `internal/infra/persistence/clickhouse.go` | 连接初始化(`ChConn` 原生批量、`chDB` GORM 查询) |
|
||||
|
||||
### 迁移入口与版本表
|
||||
|
||||
- 入口:`migrator.MigrateClickHouse()`,在 `cmd/root.go` 的 `PreRun` 中于 `migrator.Migrate()` 之后调用。
|
||||
- 仅当 `clickhouse.enabled: true` 时执行;禁用时直接跳过(见 `TestMigrateClickHouseSkipsWhenDisabled`)。
|
||||
- 版本表:`goose_clickhouse_version`,与主库 `goose_db_version` **分离**,互不影响。
|
||||
- 方言:仅 ClickHouse,**无** SQLite 镜像目录。
|
||||
|
||||
### ClickHouse 迁移规则
|
||||
|
||||
1. **DDL 只写 goose SQL**:`CREATE TABLE IF NOT EXISTS ...`,禁止 GORM `AutoMigrate`、禁止在 repository 或 handler 中建表。
|
||||
2. **无事务**:ClickHouse 不支持 goose 事务包装;每个 `Up`/`Down` 语句独立提交。
|
||||
3. **幂等 Up**:表用 `IF NOT EXISTS`;`Down` 用 `DROP TABLE IF EXISTS`。
|
||||
4. **Down 谨慎**:MergeTree 等引擎上 `DROP TABLE` 会立即删除数据,生产环境通常只前滚;仅在开发/测试需要回滚时编写 `Down`。
|
||||
5. **DDL 与 DML 分离**:与主库相同,表结构变更与数据初始化分文件、分版本号;分析表通常无 seed,批量写入由 repository 在运行时完成。
|
||||
6. **引擎与排序键**:在 SQL 中显式声明 `ENGINE`、`PARTITION BY`、`ORDER BY` 等,与查询模式对齐(例如按 `created_at` 分区)。
|
||||
7. **禁止重复 DDL**:不要在 `support-files/`、`apps` 初始化逻辑或 `repository/analytics` 中复制建表语句。
|
||||
|
||||
### 新增分析表工作流
|
||||
|
||||
按以下顺序落地,避免列名或类型漂移:
|
||||
|
||||
1. **Model**:在 `internal/model/analytics/` 定义 struct,`gorm:"column:..."` 与 DDL 列名一一对应;实现 `TableName()`,批量写入表可提供 `InsertColumns()` / `BatchInsertSQL()`。
|
||||
2. **Goose SQL**:在 `internal/infra/persistence/migrator/goose/clickhouse/` 新增递增版本文件(格式同主库,如 `YYYYMMDDNNNN_create_xxx.sql`),编写 `-- +goose Up` / `-- +goose Down`。
|
||||
3. **Repository**:在 `internal/repository/analytics/` 实现 `BatchInsert*`(`db.ChConn` 一次 `PrepareBatch` + 多行 `Append` + 一次 `Send`)与查询(`db.ChDB`);连接未初始化时返回明确错误,**不要**在 handler 写 SQL,**不要**在 repository 内维护 channel/goroutine。
|
||||
4. **Apps**:在 `internal/apps/<domain>/` 编排采集与入队;高频写入通过 `internal/infra/persistence/batchwriter` 各域独立实例异步 flush(详见 `clickhouse-batchwriter` 技能)。**日志/分析用途表**还要同时建 PG/SQLite 回落并接入 `logstore`(见 `logstore` 技能),`FlushFunc` 调 `logstore.Active` 而不是 `analyticsrepo`;普通业务分析表仍只读 repository。
|
||||
|
||||
### ClickHouse 验证
|
||||
|
||||
至少运行:
|
||||
|
||||
```bash
|
||||
go test ./internal/infra/persistence/migrator
|
||||
go test ./internal/repository/analytics
|
||||
make code-check
|
||||
```
|
||||
|
||||
验证重点:
|
||||
|
||||
- goose 能在空 ClickHouse 实例上完整执行 `Up`。
|
||||
- `internal/model/analytics` 列名、类型与 goose SQL 一致。
|
||||
- repository 读写路径不依赖 handler 内联 SQL。
|
||||
- `clickhouse.enabled: false` 时启动不报错、不执行迁移。
|
||||
|
||||
Reference in New Issue
Block a user