feat: 新增类级 @TableIndex 注解(复合/唯一/部分索引,对齐 .NET [Index(...)])、索引 diff 统一为 IndexSpec、快照升级 v4、迁移源码还原 filter,版本 1.3.0

This commit is contained in:
2026-08-25 11:38:59 +08:00
parent 270a01fd49
commit 95b18084d5
10 changed files with 347 additions and 56 deletions
+95 -47
View File
@@ -214,72 +214,70 @@ public class MigrationGenerator {
diffIndexes(upOps, downOps, old, cur)
}
/// 索引 diff:按 indexName 匹配,处理新增/删除/变更(变更 = up 先 DROP 再 CREATE,down 反之)
/// 索引 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, PropertyModel>()
for (p in old.properties) {
if (p.isIndex) {
oldIdx[p.indexName] = p
}
var oldIdx = HashMap<String, IndexSpec>()
for (s in collectIndexSpecs(old)) {
oldIdx[s.indexName] = s
}
var curIdx = HashMap<String, PropertyModel>()
for (p in cur.properties) {
if (p.isIndex) {
curIdx[p.indexName] = p
}
var curIdx = HashMap<String, IndexSpec>()
for (s in collectIndexSpecs(cur)) {
curIdx[s.indexName] = s
}
// 新增 / 变更
for (p in cur.properties) {
if (!p.isIndex) {
continue
}
if (let Some(op) <- oldIdx.get(p.indexName)) {
if (!sameIndex(op, p)) {
upOps.add(dropIndexOp(cur.tableName, p.indexName))
downOps.add(createIndexOp(old.tableName, op))
upOps.add(createIndexOp(cur.tableName, p))
downOps.add(dropIndexOp(old.tableName, p.indexName))
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(cur.tableName, p))
downOps.add(dropIndexOp(cur.tableName, p.indexName))
upOps.add(createIndexOp(s))
downOps.add(dropIndexOp(cur.tableName, s.indexName))
}
}
// 删除
for (p in old.properties) {
if (!p.isIndex) {
continue
}
if (!curIdx.contains(p.indexName)) {
upOps.add(dropIndexOp(old.tableName, p.indexName))
downOps.add(createIndexOp(old.tableName, p))
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: PropertyModel, b: PropertyModel): Bool {
a.columnName == b.columnName && a.indexUnique == b.indexUnique
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 用)
/// 该模型所有索引(字段级 + 实体级)→ CREATE INDEX op(up 用)
private static func createIndexOpsFor(model: EntityModel): ArrayList<MigrationOperation> {
let ops = ArrayList<MigrationOperation>()
for (p in model.properties) {
if (p.isIndex) {
ops.add(createIndexOp(model.tableName, p))
}
for (s in collectIndexSpecs(model)) {
ops.add(createIndexOp(s))
}
ops
}
/// 该模型所有索引列 → DROP INDEX op(down 用,与 createIndexOpsFor 同序)
/// 该模型所有索引(字段级 + 实体级)→ DROP INDEX op(down 用,与 createIndexOpsFor 同序)
private static func dropIndexOpsFor(model: EntityModel): ArrayList<MigrationOperation> {
let ops = ArrayList<MigrationOperation>()
for (p in model.properties) {
if (p.isIndex) {
ops.add(dropIndexOp(model.tableName, p.indexName))
}
for (s in collectIndexSpecs(model)) {
ops.add(dropIndexOp(model.tableName, s.indexName))
}
ops
}
@@ -307,6 +305,35 @@ public class MigrationGenerator {
}
}
/// 收集模型上的全部索引规格:先字段级(@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) {
@@ -362,12 +389,15 @@ public class MigrationGenerator {
op
}
private static func createIndexOp(table: String, p: PropertyModel): MigrationOperation {
private static func createIndexOp(s: IndexSpec): MigrationOperation {
let op = MigrationOperation(MigrationOperationKind.CreateIndex)
op.tableName = table
op.indexName = p.indexName
op.unique = p.indexUnique
op.columnNames.add(p.columnName)
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
}
@@ -378,3 +408,21 @@ public class MigrationGenerator {
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() {}
}
+12
View File
@@ -107,6 +107,8 @@ public class MigrationOperation {
public var indexName: String = ""
public var columnNames: ArrayList<String> = ArrayList()
public var unique: Bool = false
/// 部分索引过滤条件(CREATE ... WHERE filter;空串表示全表索引)
public var filter: String = ""
public var sql: String = ""
public init(kind: MigrationOperationKind) {
@@ -202,11 +204,18 @@ public class MigrationBuilder {
public func createIndex(indexName: String, table: String, columns: ArrayList<String>,
unique: Bool): MigrationBuilder {
createIndex(indexName, table, columns, unique, "")
}
/// 创建索引(可指定部分索引过滤条件;filter 非空时生成 CREATE ... WHERE filter)
public func createIndex(indexName: String, table: String, columns: ArrayList<String>,
unique: Bool, filter: String): MigrationBuilder {
let op = MigrationOperation(MigrationOperationKind.CreateIndex)
op.indexName = indexName
op.tableName = table
op.columnNames = columns
op.unique = unique
op.filter = filter
operations.add(op)
this
}
@@ -391,6 +400,9 @@ public class DdlFactory {
first = false
}
sb.append(")")
if (op.filter != "") {
sb.append(" WHERE ${op.filter}")
}
sb.toString()
}
+65 -4
View File
@@ -10,11 +10,12 @@
* // 模型变更后:快照存在 → 载入旧模型 diff,并覆盖保存新快照
* gen.ensure("20250801000000_AddAge", "新增 age 列", "snapshot.json", models1)
*
* 快照 JSON 结构(版本 3,较 v2 新增索引标记 idx/ixName/ixUnique):
* {"version":3,"models":[{"table":"users","columns":[
* 快照 JSON 结构(版本 4,较 v3 新增实体级 indexes 数组;v3 的 idx/ixName/ixUnique 为字段级索引标记):
* {"version":4,"models":[{"table":"users","columns":[
* {"name":"id","column":"id","type":"Int64","key":true,"auto":true,
* "client":false,"required":false,"maxLen":0,"enum":false,
* "idx":false,"ixName":"","ixUnique":false}]}]}
* "idx":false,"ixName":"","ixUnique":false}],
* "indexes":[{"name":"ux","columns":["a","b"],"unique":true,"filter":"kind = 'Passport'"}]}]}
*/
package simcu::orm.migrations
@@ -31,7 +32,7 @@ public class ModelSnapshot {
/// 快照内的轻量模型列表
public var models: ArrayList<EntityModel> = ArrayList()
/// 快照格式版本
public static let formatVersion: Int64 = 3
public static let formatVersion: Int64 = 4
public init() {}
@@ -64,6 +65,16 @@ public class ModelSnapshot {
}
}
}
for (e in m.indexes) {
let li = EntityIndex()
li.indexName = e.indexName
li.unique = e.unique
li.filter = e.filter
for (c in e.columnNames) {
li.columnNames.add(c)
}
lm.indexes.add(li)
}
snap.models.add(lm)
}
snap
@@ -100,6 +111,20 @@ public class ModelSnapshot {
cols.add(JObject(co))
}
mo["columns"] = JArray(cols)
let idxs = ArrayList<JsonValue>()
for (e in m.indexes) {
let io = HashMap<String, JsonValue>()
io["name"] = JString(e.indexName)
let icols = ArrayList<JsonValue>()
for (c in e.columnNames) {
icols.add(JString(c))
}
io["columns"] = JArray(icols)
io["unique"] = JBool(e.unique)
io["filter"] = JString(e.filter)
idxs.add(JObject(io))
}
mo["indexes"] = JArray(idxs)
modelsArr.add(JObject(mo))
}
root["models"] = JArray(modelsArr)
@@ -146,6 +171,26 @@ public class ModelSnapshot {
}
case _ => throw Exception("simcu::orm: 快照 models[].columns 必须是数组")
}
// 实体级索引(v4 开始存在;缺失时容忍为空,兼容旧快照)
match (mo.get("indexes")) {
case Some(JArray(idxs)) =>
for (i in idxs) {
match (i) {
case JObject(io) =>
let li = EntityIndex()
li.indexName = getString(io, "name")
li.unique = getBool(io, "unique")
li.filter = getString(io, "filter")
for (c in getStringArray(io, "columns")) {
li.columnNames.add(c)
}
lm.indexes.add(li)
case _ => throw Exception(
"simcu::orm: 快照 indexes[] 项必须是 object")
}
}
case _ => ()
}
snap.models.add(lm)
case _ => throw Exception("simcu::orm: 快照 models[] 项必须是 object")
}
@@ -201,4 +246,20 @@ public class ModelSnapshot {
case _ => 0
}
}
/// 读取字符串数组(如索引列名列表);缺失或类型不符时返回空列表
private static func getStringArray(o: HashMap<String, JsonValue>, k: String): ArrayList<String> {
let list = ArrayList<String>()
match (o.get(k)) {
case Some(JArray(arr)) =>
for (e in arr) {
match (e) {
case JString(s) => list.add(s)
case _ => ()
}
}
case _ => ()
}
list
}
}