Files
orm-cj/src/migrations/migration_generator.cj
T

429 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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() {}
}