migrate.md - maoxiaoyue/hypgo GitHub Wiki
pkg/migrate — Migration Diff 自動生成
掃描 Go Model struct 的 bun tag,比對快照檔案,自動產生 up/down SQL migration。
設計理念
AI 修改 Model struct 後,手動寫 SQL migration 容易遺漏欄位。Migration Diff 讓框架自動偵測變更並產生正確的 SQL:
Model struct 變更 → 反射掃描 bun tag → 比對上次快照 → 產生 SQL
快速上手
1. 註冊 Model
import "github.com/maoxiaoyue/hypgo/pkg/migrate"
type User struct {
bun.BaseModel `bun:"table:users"`
ID int64 `bun:"id,pk,autoincrement"`
Name string `bun:"name,notnull"`
Email string `bun:"email,notnull,unique"`
Bio string `bun:"bio,type:text"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
}
registry := migrate.NewRegistry()
registry.Register((*User)(nil))
2. 掃描 & Diff
// 掃描 Model → 提取 TableSchema
tables := migrate.ScanModels(registry)
// 載入上次快照
snapshot, _ := migrate.LoadSnapshot(".hyp/schema_snapshot.json")
// 比對 → 產生 ChangeSet
changes := migrate.Diff(tables, snapshot)
// 產生 SQL
up, down := migrate.GenerateSQL(changes, "postgres")
fmt.Println(up)
// → ALTER TABLE "users" ADD COLUMN "bio" TEXT;
3. 儲存
// 儲存新快照
migrate.SaveSnapshot(".hyp/schema_snapshot.json", tables)
// 取得 migration 檔名
upFile, downFile := migrate.MigrationFiles("migrations/")
// → migrations/20260326_200000_auto.up.sql
// → migrations/20260326_200000_auto.down.sql
完整工作流程
func generateMigration() {
// 1. 註冊所有 Model
registry := migrate.NewRegistry()
registry.Register(
(*models.User)(nil),
(*models.Post)(nil),
(*models.Comment)(nil),
)
// 2. 掃描
tables := migrate.ScanModels(registry)
// 3. Diff
snapshot, _ := migrate.LoadSnapshot(".hyp/schema_snapshot.json")
changes := migrate.Diff(tables, snapshot)
if len(changes) == 0 {
fmt.Println("No changes detected")
return
}
// 4. 顯示變更
for _, c := range changes {
fmt.Println(c.String())
}
// 5. 產生 SQL
up, down := migrate.GenerateSQL(changes, "postgres")
// 6. 儲存 migration 檔案
upFile, downFile := migrate.MigrationFiles("migrations/")
os.WriteFile(upFile, []byte(up), 0644)
os.WriteFile(downFile, []byte(down), 0644)
// 7. 更新快照
migrate.SaveSnapshot(".hyp/schema_snapshot.json", tables)
}
Scanner — bun tag 解析
Scanner 從 struct field 的 bun:"..." tag 提取欄位資訊:
| bun tag | 解析結果 |
|---|---|
bun:"id,pk,autoincrement" |
PrimaryKey=true, AutoIncrement=true |
bun:"name,notnull" |
NotNull=true |
bun:"email,notnull,unique" |
NotNull=true, Unique=true |
bun:"bio,type:text" |
SQLType="text" |
bun:"score,default:0" |
Default="0" |
bun:"table:users,alias:u" |
表名="users"(BaseModel tag) |
bun:"-" |
跳過此欄位 |
Diff — 變更偵測
| 變更類型 | 說明 |
|---|---|
AddTable |
快照中不存在的新表 |
DropTable |
Model 中已移除的表 |
AddColumn |
表中新增的欄位 |
DropColumn |
表中已移除的欄位 |
AlterColumn |
欄位屬性變更(型別、NotNull、Default 等) |
SQL Generator — 多 Dialect 支援
| 特性 | PostgreSQL | MySQL |
|---|---|---|
| 識別符引號 | "users" |
`users` |
| 自增主鍵 int64 | BIGSERIAL |
BIGINT AUTO_INCREMENT |
| 自增主鍵 int | SERIAL |
INT AUTO_INCREMENT |
| 浮點數 | DOUBLE PRECISION |
DOUBLE |
| 時間戳 | TIMESTAMPTZ |
DATETIME |
| ALTER NOT NULL | SET/DROP NOT NULL |
MODIFY COLUMN |
Go → SQL 型別對映
| Go Type | SQL Type |
|---|---|
int64 |
BIGINT |
int, int32 |
INTEGER |
int16 |
SMALLINT |
float64 |
DOUBLE PRECISION / DOUBLE |
float32 |
REAL |
bool |
BOOLEAN |
string |
TEXT |
time.Time |
TIMESTAMPTZ / DATETIME |
若有 type:xxx tag,優先使用指定的 SQL 型別。
快照格式
{
"tables": {
"users": {
"name": "users",
"columns": [
{"name": "id", "go_type": "int64", "primary_key": true, "auto_increment": true},
{"name": "name", "go_type": "string", "not_null": true},
{"name": "email", "go_type": "string", "not_null": true, "unique": true}
]
}
}
}
架構
pkg/migrate/
├── registry.go ModelRegistry — Model 註冊
├── scanner.go ScanModel() — 反射提取 bun tag → TableSchema
├── snapshot.go 快照讀寫(JSON)
├── diff.go Diff() — 比對新舊 schema → ChangeSet
├── generator.go GenerateSQL() — ChangeSet → SQL(pg/mysql)
└── migrate_test.go 22 個單元測試
測試
go test ./pkg/migrate/... -v
涵蓋:Registry、Scanner(User/Post/非 struct)、toSnakeCase、Diff(AddTable/DropTable/AddColumn/DropColumn/AlterColumn/NoChanges)、SQL Generator(Postgres/MySQL/AutoIncrement/DropTable/Empty)、Snapshot(Save/Load/不存在/JSON)、完整工作流程整合測試。