429 lines
16 KiB
Plaintext
429 lines
16 KiB
Plaintext
/*
|
||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||
* 从数据模型生成迁移(对齐 EF Core migrations add):
|
||
* - initial(...) 全模型 → 初始迁移(CreateTable + down DropTable)
|
||
* - diff(...) 旧模型快照 vs 新模型 → 增量迁移(表/列增删改 + 自动 down 反转)
|
||
*
|
||
* 用法:
|
||
* let gen = MigrationGenerator()
|
||
* // 首次:快照文件不存在 → initial 全量迁移
|
||
* let m0 = gen.ensure("20250701000000_InitialCreate", "初始建表", "snapshot.json", models0)
|
||
* migrator.migrate([m0])
|
||
* // 模型变更后:快照存在 → 载入旧模型自动 diff(snapshot.json 自动更新)
|
||
* let m1 = gen.ensure("20250801000000_AddAge", "新增 age 列", "snapshot.json", models1)
|
||
* migrator.migrate([m1])
|
||
*
|
||
* 索引:模型字段用 @Index 标注即生成 CREATE [UNIQUE] INDEX(down 自动 DROP INDEX)。
|
||
* 限制(v1):不支持主键/自增属性变更(抛异常提示手写迁移)。
|
||
*/
|
||
|
||
package simcu::orm.migrations
|
||
|
||
import std.collection.*
|
||
import simcu::orm.model.*
|
||
import simcu::orm.sql.ColumnTypes
|
||
|
||
/**
|
||
* 模型 → 迁移操作生成器。
|
||
*/
|
||
public class MigrationGenerator {
|
||
public init() {}
|
||
|
||
/// 初始迁移:空库 → 当前模型(全部 CreateTable;down 为逆序 DropTable)
|
||
public func initial(migrationId: String, description: String, models: ArrayList<EntityModel>): Migration {
|
||
let m = Migration(migrationId, description)
|
||
let upOps = ArrayList<MigrationOperation>()
|
||
let downOps = ArrayList<MigrationOperation>()
|
||
for (model in models) {
|
||
upOps.add(createTableOp(model))
|
||
downOps.add(dropTableOp(model.tableName))
|
||
// 索引随建表一并创建:down 先 DROP INDEX 再 DROP TABLE(setPresets 已逆序处理)
|
||
for (op in createIndexOpsFor(model)) {
|
||
upOps.add(op)
|
||
}
|
||
for (op in dropIndexOpsFor(model)) {
|
||
downOps.add(op)
|
||
}
|
||
}
|
||
setPresets(m, upOps, downOps)
|
||
m
|
||
}
|
||
|
||
/// 增量迁移:旧模型(上次快照)→ 新模型,生成表/列增删改;主键/自增变更抛异常
|
||
public func diff(migrationId: String, description: String,
|
||
oldModels: ArrayList<EntityModel>, newModels: ArrayList<EntityModel>): Migration { let m = Migration(migrationId, description)
|
||
let upOps = ArrayList<MigrationOperation>()
|
||
let downOps = ArrayList<MigrationOperation>()
|
||
|
||
let oldByName = indexByTable(oldModels)
|
||
let newByName = indexByTable(newModels)
|
||
|
||
// 1. 新增表 → CreateTable(含索引)
|
||
for (n in newModels) {
|
||
if (!oldByName.contains(n.tableName)) {
|
||
upOps.add(createTableOp(n))
|
||
downOps.add(dropTableOp(n.tableName))
|
||
for (op in createIndexOpsFor(n)) {
|
||
upOps.add(op)
|
||
}
|
||
for (op in dropIndexOpsFor(n)) {
|
||
downOps.add(op)
|
||
}
|
||
}
|
||
}
|
||
// 2. 已有表 → 列增删改
|
||
for (n in newModels) {
|
||
if (let Some(old) <- oldByName.get(n.tableName)) {
|
||
diffTable(upOps, downOps, old, n)
|
||
}
|
||
}
|
||
// 3. 删除表 → DropTable
|
||
for (o in oldModels) {
|
||
if (!newByName.contains(o.tableName)) {
|
||
upOps.add(dropTableOp(o.tableName))
|
||
downOps.add(createTableOp(o))
|
||
}
|
||
}
|
||
|
||
setPresets(m, upOps, downOps)
|
||
m
|
||
}
|
||
|
||
/// 便捷入口(快照自动管理,diff 无需调用方手存模型列表):
|
||
/// 快照文件不存在 → initial 全量迁移 + 保存快照;存在 → 载入旧快照 diff + 覆盖保存新快照。
|
||
/// 返回迁移;若快照存在且模型无变化,返回的迁移 up/down 为空操作列表。
|
||
public func ensure(migrationId: String, description: String, snapshotPath: String,
|
||
models: ArrayList<EntityModel>): Migration {
|
||
if (let Some(snap) <- ModelSnapshot.load(snapshotPath)) {
|
||
let m = diff(migrationId, description, snap.toModels(), models)
|
||
ModelSnapshot.capture(models).save(snapshotPath)
|
||
return m
|
||
}
|
||
let m = initial(migrationId, description, models)
|
||
ModelSnapshot.capture(models).save(snapshotPath)
|
||
m
|
||
}
|
||
|
||
/// 字段类型简单名 → 列类型(String/Bool/Int8-64/UInt8-64/Float32-64/Rune/DateTime/Duration/Decimal/Array<Byte>)
|
||
/// Option<X> 按内层类型映射(Option 列默认 nullable,见 columnDefinition 的 isRequired/isKey 判定);
|
||
/// 简单枚举不在此映射(由 columnDefinition 按 isEnum 标记走 TextCol)。
|
||
public static func columnTypeFor(typeName: String): ColumnTypes {
|
||
let effective = if (typeName.startsWith("Option<") && typeName.endsWith(">")) {
|
||
typeName[7..typeName.size - 1]
|
||
} else {
|
||
typeName
|
||
}
|
||
match (effective) {
|
||
case "String" => ColumnTypes.TextCol
|
||
case "Bool" => ColumnTypes.BoolCol
|
||
case "Int8" => ColumnTypes.TinyIntCol
|
||
case "UInt8" => ColumnTypes.TinyIntCol
|
||
case "Int16" => ColumnTypes.SmallIntCol
|
||
case "UInt16" => ColumnTypes.SmallIntCol
|
||
case "Int32" => ColumnTypes.IntCol
|
||
case "UInt32" => ColumnTypes.IntCol
|
||
case "Int64" => ColumnTypes.BigIntCol
|
||
case "UInt64" => ColumnTypes.BigIntCol
|
||
case "Float32" => ColumnTypes.RealCol
|
||
case "Float64" => ColumnTypes.FloatCol
|
||
case "Rune" => ColumnTypes.IntCol
|
||
case "DateTime" => ColumnTypes.DateTimeCol
|
||
case "Duration" => ColumnTypes.BigIntCol
|
||
case "Decimal" => ColumnTypes.DecimalCol
|
||
case "Array<Byte>" => ColumnTypes.BinaryCol
|
||
case _ => throw Exception("simcu::orm: 字段类型 ${typeName} 无法映射到列类型")
|
||
}
|
||
}
|
||
|
||
/// PropertyModel → ColumnDefinition(主键/自增/非空/最大长度 随注解映射)
|
||
public static func columnDefinition(p: PropertyModel): ColumnDefinition {
|
||
let c = ColumnDefinition(p.columnName, columnTypeOf(p))
|
||
if (p.isKey) {
|
||
c.primary()
|
||
}
|
||
if (p.autoIncrement) {
|
||
c.autoInc()
|
||
}
|
||
if (p.isRequired || p.isKey) {
|
||
c.notNull()
|
||
}
|
||
if (p.maxLength > 0) {
|
||
c.withMaxLength(p.maxLength)
|
||
}
|
||
c
|
||
}
|
||
|
||
/// 列类型:枚举字段(含 Option 内层枚举)按构造器名存 VARCHAR,其余按类型名映射
|
||
private static func columnTypeOf(p: PropertyModel): ColumnTypes {
|
||
if (p.isEnum) {
|
||
ColumnTypes.TextCol
|
||
} else {
|
||
columnTypeFor(p.typeName())
|
||
}
|
||
}
|
||
|
||
// ---------- 私有 ----------
|
||
|
||
private func diffTable(upOps: ArrayList<MigrationOperation>, downOps: ArrayList<MigrationOperation>,
|
||
old: EntityModel, cur: EntityModel): Unit {
|
||
// 表级主键变更(主键列名不同)→ 不支持,抛异常
|
||
let oldKey = old.keyProperty.getOrThrow().columnName
|
||
let curKey = cur.keyProperty.getOrThrow().columnName
|
||
if (oldKey != curKey) {
|
||
throw Exception(
|
||
"simcu::orm: 表 ${cur.tableName} 主键从 ${oldKey} 变更为 ${curKey},v1 自动迁移不支持,请手写迁移")
|
||
}
|
||
|
||
var oldCols = HashMap<String, PropertyModel>()
|
||
for (p in old.properties) {
|
||
oldCols[p.columnName] = p
|
||
}
|
||
var curCols = HashMap<String, PropertyModel>()
|
||
for (p in cur.properties) {
|
||
curCols[p.columnName] = p
|
||
}
|
||
|
||
// 新增列
|
||
for (p in cur.properties) {
|
||
if (!oldCols.contains(p.columnName)) {
|
||
upOps.add(addColumnOp(cur.tableName, p))
|
||
downOps.add(dropColumnOp(cur.tableName, p.columnName))
|
||
}
|
||
}
|
||
// 删除列
|
||
for (p in old.properties) {
|
||
if (!curCols.contains(p.columnName)) {
|
||
upOps.add(dropColumnOp(old.tableName, p.columnName))
|
||
downOps.add(addColumnOp(old.tableName, p))
|
||
}
|
||
}
|
||
// 列定义变化(类型/长度/非空)
|
||
for (p in cur.properties) {
|
||
if (let Some(op) <- oldCols.get(p.columnName)) {
|
||
if (op.isKey != p.isKey || op.autoIncrement != p.autoIncrement) {
|
||
throw Exception(
|
||
"simcu::orm: 表 ${cur.tableName} 列 ${p.columnName} 的主键/自增属性发生变化,v1 自动迁移不支持,请手写迁移")
|
||
}
|
||
if (!sameColumn(op, p)) {
|
||
upOps.add(alterColumnOp(cur.tableName, p))
|
||
downOps.add(alterColumnOp(old.tableName, op))
|
||
}
|
||
}
|
||
}
|
||
// 索引增删改
|
||
diffIndexes(upOps, downOps, old, cur)
|
||
}
|
||
|
||
/// 索引 diff:按 indexName 匹配,处理新增/删除/变更(变更 = up 先 DROP 再 CREATE,down 反之)。
|
||
/// 统一处理字段级(@Index)与实体级(@TableIndex)索引,均归一为 IndexSpec。
|
||
private static func diffIndexes(upOps: ArrayList<MigrationOperation>, downOps: ArrayList<MigrationOperation>,
|
||
old: EntityModel, cur: EntityModel): Unit {
|
||
var oldIdx = HashMap<String, IndexSpec>()
|
||
for (s in collectIndexSpecs(old)) {
|
||
oldIdx[s.indexName] = s
|
||
}
|
||
var curIdx = HashMap<String, IndexSpec>()
|
||
for (s in collectIndexSpecs(cur)) {
|
||
curIdx[s.indexName] = s
|
||
}
|
||
// 新增 / 变更
|
||
for (s in collectIndexSpecs(cur)) {
|
||
if (let Some(os) <- oldIdx.get(s.indexName)) {
|
||
if (!sameIndex(os, s)) {
|
||
upOps.add(dropIndexOp(cur.tableName, s.indexName))
|
||
downOps.add(createIndexOp(os))
|
||
upOps.add(createIndexOp(s))
|
||
downOps.add(dropIndexOp(old.tableName, s.indexName))
|
||
}
|
||
} else {
|
||
upOps.add(createIndexOp(s))
|
||
downOps.add(dropIndexOp(cur.tableName, s.indexName))
|
||
}
|
||
}
|
||
// 删除
|
||
for (s in collectIndexSpecs(old)) {
|
||
if (!curIdx.contains(s.indexName)) {
|
||
upOps.add(dropIndexOp(old.tableName, s.indexName))
|
||
downOps.add(createIndexOp(s))
|
||
}
|
||
}
|
||
}
|
||
|
||
private static func sameIndex(a: IndexSpec, b: IndexSpec): Bool {
|
||
if (a.unique != b.unique || a.filter != b.filter) {
|
||
return false
|
||
}
|
||
if (a.columnNames.size != b.columnNames.size) {
|
||
return false
|
||
}
|
||
for (i in 0..a.columnNames.size) {
|
||
if (a.columnNames[i] != b.columnNames[i]) {
|
||
return false
|
||
}
|
||
}
|
||
true
|
||
}
|
||
|
||
/// 该模型所有索引(字段级 + 实体级)→ CREATE INDEX op(up 用)
|
||
private static func createIndexOpsFor(model: EntityModel): ArrayList<MigrationOperation> {
|
||
let ops = ArrayList<MigrationOperation>()
|
||
for (s in collectIndexSpecs(model)) {
|
||
ops.add(createIndexOp(s))
|
||
}
|
||
ops
|
||
}
|
||
|
||
/// 该模型所有索引(字段级 + 实体级)→ DROP INDEX op(down 用,与 createIndexOpsFor 同序)
|
||
private static func dropIndexOpsFor(model: EntityModel): ArrayList<MigrationOperation> {
|
||
let ops = ArrayList<MigrationOperation>()
|
||
for (s in collectIndexSpecs(model)) {
|
||
ops.add(dropIndexOp(model.tableName, s.indexName))
|
||
}
|
||
ops
|
||
}
|
||
|
||
private static func sameColumn(a: PropertyModel, b: PropertyModel): Bool {
|
||
typeRank(columnTypeOf(a)) == typeRank(columnTypeOf(b)) &&
|
||
a.maxLength == b.maxLength &&
|
||
a.isRequired == b.isRequired
|
||
}
|
||
|
||
/// 列类型序号(仅用于同表列定义比较,Cangjie 枚举无 ==)
|
||
private static func typeRank(t: ColumnTypes): Int64 {
|
||
match (t) {
|
||
case ColumnTypes.BigIntCol => 0
|
||
case ColumnTypes.IntCol => 1
|
||
case ColumnTypes.SmallIntCol => 2
|
||
case ColumnTypes.TinyIntCol => 3
|
||
case ColumnTypes.TextCol => 4
|
||
case ColumnTypes.BoolCol => 5
|
||
case ColumnTypes.FloatCol => 6
|
||
case ColumnTypes.RealCol => 7
|
||
case ColumnTypes.DateTimeCol => 8
|
||
case ColumnTypes.DecimalCol => 9
|
||
case ColumnTypes.BinaryCol => 10
|
||
}
|
||
}
|
||
|
||
/// 收集模型上的全部索引规格:先字段级(@Index),再实体级(@TableIndex)。
|
||
/// 顺序即生成顺序,down 与之对应逆序销毁。
|
||
private static func collectIndexSpecs(model: EntityModel): ArrayList<IndexSpec> {
|
||
let specs = ArrayList<IndexSpec>()
|
||
for (p in model.properties) {
|
||
if (p.isIndex) {
|
||
let s = IndexSpec()
|
||
s.table = model.tableName
|
||
s.indexName = p.indexName
|
||
s.unique = p.indexUnique
|
||
s.filter = ""
|
||
s.columnNames.add(p.columnName)
|
||
specs.add(s)
|
||
}
|
||
}
|
||
for (e in model.indexes) {
|
||
let s = IndexSpec()
|
||
s.table = model.tableName
|
||
s.indexName = e.indexName
|
||
s.unique = e.unique
|
||
s.filter = e.filter
|
||
for (c in e.columnNames) {
|
||
s.columnNames.add(c)
|
||
}
|
||
specs.add(s)
|
||
}
|
||
specs
|
||
}
|
||
|
||
private static func indexByTable(models: ArrayList<EntityModel>): HashMap<String, EntityModel> {
|
||
let map = HashMap<String, EntityModel>()
|
||
for (model in models) {
|
||
map[model.tableName] = model
|
||
}
|
||
map
|
||
}
|
||
|
||
/// 注入 up/down:down 按 up 逆序反转后存储(down() 按列表顺序执行)
|
||
private static func setPresets(m: Migration, upOps: ArrayList<MigrationOperation>,
|
||
downOps: ArrayList<MigrationOperation>): Unit {
|
||
for (op in upOps) {
|
||
m.presetOperations.add(op)
|
||
}
|
||
for (i in 0..downOps.size) {
|
||
m.presetDownOperations.add(downOps[downOps.size - 1 - i])
|
||
}
|
||
}
|
||
|
||
private static func createTableOp(model: EntityModel): MigrationOperation {
|
||
let op = MigrationOperation(MigrationOperationKind.CreateTable)
|
||
op.tableName = model.tableName
|
||
for (p in model.properties) {
|
||
op.columnDefs.add(columnDefinition(p))
|
||
}
|
||
op
|
||
}
|
||
|
||
private static func dropTableOp(table: String): MigrationOperation {
|
||
let op = MigrationOperation(MigrationOperationKind.DropTable)
|
||
op.tableName = table
|
||
op
|
||
}
|
||
|
||
private static func addColumnOp(table: String, p: PropertyModel): MigrationOperation {
|
||
let op = MigrationOperation(MigrationOperationKind.AddColumn)
|
||
op.tableName = table
|
||
op.column = Some(columnDefinition(p))
|
||
op
|
||
}
|
||
|
||
private static func dropColumnOp(table: String, column: String): MigrationOperation {
|
||
let op = MigrationOperation(MigrationOperationKind.DropColumn)
|
||
op.tableName = table
|
||
op.columnName = column
|
||
op
|
||
}
|
||
|
||
private static func alterColumnOp(table: String, p: PropertyModel): MigrationOperation {
|
||
let op = MigrationOperation(MigrationOperationKind.AlterColumn)
|
||
op.tableName = table
|
||
op.column = Some(columnDefinition(p))
|
||
op
|
||
}
|
||
|
||
private static func createIndexOp(s: IndexSpec): MigrationOperation {
|
||
let op = MigrationOperation(MigrationOperationKind.CreateIndex)
|
||
op.tableName = s.table
|
||
op.indexName = s.indexName
|
||
op.unique = s.unique
|
||
op.filter = s.filter
|
||
for (c in s.columnNames) {
|
||
op.columnNames.add(c)
|
||
}
|
||
op
|
||
}
|
||
|
||
private static func dropIndexOp(table: String, indexName: String): MigrationOperation {
|
||
let op = MigrationOperation(MigrationOperationKind.DropIndex)
|
||
op.tableName = table
|
||
op.indexName = indexName
|
||
op
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 内部统一的索引规格:把字段级(@Index)与实体级(@TableIndex)索引归一为同构描述,便于 diff 与生成。
|
||
*/
|
||
public class IndexSpec {
|
||
/// 所属表名
|
||
public var table: String = ""
|
||
/// 索引名
|
||
public var indexName: String = ""
|
||
/// 列名列表
|
||
public var columnNames: ArrayList<String> = ArrayList()
|
||
/// 是否唯一
|
||
public var unique: Bool = false
|
||
/// 部分索引过滤条件(空串表示全表索引)
|
||
public var filter: String = ""
|
||
|
||
public init() {}
|
||
}
|