2026-08-19 09:14:01 +08:00
|
|
|
|
/*
|
|
|
|
|
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
|
|
|
|
|
* 模型快照:把反射构建的 EntityModel 序列化为轻量模型 + JSON 持久化,
|
|
|
|
|
|
* 使 diff 不依赖调用方手存模型列表(对齐 EF Core 的快照文件)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 用法:
|
|
|
|
|
|
* // 首次:快照文件不存在 → 生成 initial 迁移并保存快照
|
|
|
|
|
|
* let gen = MigrationGenerator()
|
|
|
|
|
|
* gen.ensure("20250701000000_InitialCreate", "初始建表", "snapshot.json", models0)
|
|
|
|
|
|
* // 模型变更后:快照存在 → 载入旧模型 diff,并覆盖保存新快照
|
|
|
|
|
|
* gen.ensure("20250801000000_AddAge", "新增 age 列", "snapshot.json", models1)
|
|
|
|
|
|
*
|
2026-08-23 06:45:04 +08:00
|
|
|
|
* 快照 JSON 结构(版本 3,较 v2 新增索引标记 idx/ixName/ixUnique):
|
|
|
|
|
|
* {"version":3,"models":[{"table":"users","columns":[
|
2026-08-21 03:41:53 +08:00
|
|
|
|
* {"name":"id","column":"id","type":"Int64","key":true,"auto":true,
|
2026-08-23 06:45:04 +08:00
|
|
|
|
* "client":false,"required":false,"maxLen":0,"enum":false,
|
|
|
|
|
|
* "idx":false,"ixName":"","ixUnique":false}]}]}
|
2026-08-19 09:14:01 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
package simcu::orm.migrations
|
|
|
|
|
|
|
|
|
|
|
|
import std.collection.*
|
|
|
|
|
|
import std.convert.*
|
|
|
|
|
|
import std.fs.*
|
|
|
|
|
|
import simcu::orm.model.*
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 模型快照:持有一组轻量 EntityModel(无反射句柄,仅元数据)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
public class ModelSnapshot {
|
|
|
|
|
|
/// 快照内的轻量模型列表
|
|
|
|
|
|
public var models: ArrayList<EntityModel> = ArrayList()
|
|
|
|
|
|
/// 快照格式版本
|
2026-08-23 06:45:04 +08:00
|
|
|
|
public static let formatVersion: Int64 = 3
|
2026-08-19 09:14:01 +08:00
|
|
|
|
|
|
|
|
|
|
public init() {}
|
|
|
|
|
|
|
|
|
|
|
|
/// 从真实反射模型捕获元数据快照(剥离反射句柄)
|
|
|
|
|
|
public static func capture(models: ArrayList<EntityModel>): ModelSnapshot {
|
|
|
|
|
|
let snap = ModelSnapshot()
|
|
|
|
|
|
for (m in models) {
|
|
|
|
|
|
let lm = EntityModel()
|
|
|
|
|
|
lm.tableName = m.tableName
|
|
|
|
|
|
for (p in m.properties) {
|
|
|
|
|
|
let lp = PropertyModel()
|
|
|
|
|
|
lp.name = p.name
|
|
|
|
|
|
lp.columnName = p.columnName
|
|
|
|
|
|
lp.isKey = p.isKey
|
|
|
|
|
|
lp.autoIncrement = p.autoIncrement
|
|
|
|
|
|
lp.clientGenerated = p.clientGenerated
|
|
|
|
|
|
lp.isRequired = p.isRequired
|
|
|
|
|
|
lp.maxLength = p.maxLength
|
|
|
|
|
|
lp.typeNameOverride = p.typeName()
|
2026-08-22 09:53:43 +08:00
|
|
|
|
lp.isEnum = p.isEnum
|
2026-08-23 06:45:04 +08:00
|
|
|
|
lp.isIndex = p.isIndex
|
|
|
|
|
|
lp.indexName = p.indexName
|
|
|
|
|
|
lp.indexUnique = p.indexUnique
|
2026-08-19 09:14:01 +08:00
|
|
|
|
lm.properties.add(lp)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (let Some(kp) <- m.keyProperty) {
|
|
|
|
|
|
for (lp in lm.properties) {
|
|
|
|
|
|
if (lp.columnName == kp.columnName) {
|
|
|
|
|
|
lm.keyProperty = Some(lp)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
snap.models.add(lm)
|
|
|
|
|
|
}
|
|
|
|
|
|
snap
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 快照内轻量模型列表(可直接作为 MigrationGenerator.diff 的 oldModels)
|
|
|
|
|
|
public func toModels(): ArrayList<EntityModel> {
|
|
|
|
|
|
models
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 序列化为 JSON 文本
|
|
|
|
|
|
public func toJson(): String {
|
|
|
|
|
|
let root = HashMap<String, JsonValue>()
|
|
|
|
|
|
root["version"] = JNumber("${formatVersion}")
|
|
|
|
|
|
let modelsArr = ArrayList<JsonValue>()
|
|
|
|
|
|
for (m in models) {
|
|
|
|
|
|
let mo = HashMap<String, JsonValue>()
|
|
|
|
|
|
mo["table"] = JString(m.tableName)
|
|
|
|
|
|
let cols = ArrayList<JsonValue>()
|
|
|
|
|
|
for (p in m.properties) {
|
|
|
|
|
|
let co = HashMap<String, JsonValue>()
|
|
|
|
|
|
co["name"] = JString(p.name)
|
|
|
|
|
|
co["column"] = JString(p.columnName)
|
|
|
|
|
|
co["type"] = JString(p.typeName())
|
|
|
|
|
|
co["key"] = JBool(p.isKey)
|
|
|
|
|
|
co["auto"] = JBool(p.autoIncrement)
|
|
|
|
|
|
co["client"] = JBool(p.clientGenerated)
|
|
|
|
|
|
co["required"] = JBool(p.isRequired)
|
|
|
|
|
|
co["maxLen"] = JNumber("${p.maxLength}")
|
2026-08-22 09:53:43 +08:00
|
|
|
|
co["enum"] = JBool(p.isEnum)
|
2026-08-23 06:45:04 +08:00
|
|
|
|
co["idx"] = JBool(p.isIndex)
|
|
|
|
|
|
co["ixName"] = JString(p.indexName)
|
|
|
|
|
|
co["ixUnique"] = JBool(p.indexUnique)
|
2026-08-19 09:14:01 +08:00
|
|
|
|
cols.add(JObject(co))
|
|
|
|
|
|
}
|
|
|
|
|
|
mo["columns"] = JArray(cols)
|
|
|
|
|
|
modelsArr.add(JObject(mo))
|
|
|
|
|
|
}
|
|
|
|
|
|
root["models"] = JArray(modelsArr)
|
|
|
|
|
|
MiniJson.stringify(JObject(root))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 从 JSON 文本反序列化
|
|
|
|
|
|
public static func fromJson(text: String): ModelSnapshot {
|
|
|
|
|
|
let snap = ModelSnapshot()
|
|
|
|
|
|
match (MiniJson.parse(text)) {
|
|
|
|
|
|
case JObject(root) =>
|
|
|
|
|
|
match (root.get("models")) {
|
|
|
|
|
|
case Some(JArray(arr)) =>
|
|
|
|
|
|
for (item in arr) {
|
|
|
|
|
|
match (item) {
|
|
|
|
|
|
case JObject(mo) =>
|
|
|
|
|
|
let lm = EntityModel()
|
|
|
|
|
|
lm.tableName = getString(mo, "table")
|
|
|
|
|
|
match (mo.get("columns")) {
|
|
|
|
|
|
case Some(JArray(cols)) =>
|
|
|
|
|
|
for (c in cols) {
|
|
|
|
|
|
match (c) {
|
|
|
|
|
|
case JObject(co) =>
|
|
|
|
|
|
let lp = PropertyModel()
|
|
|
|
|
|
lp.name = getString(co, "name")
|
|
|
|
|
|
lp.columnName = getString(co, "column")
|
|
|
|
|
|
lp.typeNameOverride = getString(co, "type")
|
|
|
|
|
|
lp.isKey = getBool(co, "key")
|
|
|
|
|
|
lp.autoIncrement = getBool(co, "auto")
|
|
|
|
|
|
lp.clientGenerated = getBool(co, "client")
|
|
|
|
|
|
lp.isRequired = getBool(co, "required")
|
|
|
|
|
|
lp.maxLength = getLong(co, "maxLen")
|
2026-08-22 09:53:43 +08:00
|
|
|
|
lp.isEnum = getBool(co, "enum")
|
2026-08-23 06:45:04 +08:00
|
|
|
|
lp.isIndex = getBool(co, "idx")
|
|
|
|
|
|
lp.indexName = getString(co, "ixName")
|
|
|
|
|
|
lp.indexUnique = getBool(co, "ixUnique")
|
2026-08-19 09:14:01 +08:00
|
|
|
|
if (lp.isKey) {
|
|
|
|
|
|
lm.keyProperty = Some(lp)
|
|
|
|
|
|
}
|
|
|
|
|
|
lm.properties.add(lp)
|
|
|
|
|
|
case _ => throw Exception(
|
2026-08-22 09:53:43 +08:00
|
|
|
|
"simcu::orm: 快照 columns 项必须是 object")
|
2026-08-19 09:14:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-22 09:53:43 +08:00
|
|
|
|
case _ => throw Exception("simcu::orm: 快照 models[].columns 必须是数组")
|
2026-08-19 09:14:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
snap.models.add(lm)
|
2026-08-22 09:53:43 +08:00
|
|
|
|
case _ => throw Exception("simcu::orm: 快照 models[] 项必须是 object")
|
2026-08-19 09:14:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-22 09:53:43 +08:00
|
|
|
|
case _ => throw Exception("simcu::orm: 快照缺少 models 数组")
|
2026-08-19 09:14:01 +08:00
|
|
|
|
}
|
2026-08-22 09:53:43 +08:00
|
|
|
|
case _ => throw Exception("simcu::orm: 快照根必须是 object")
|
2026-08-19 09:14:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
snap
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 保存到文件(覆盖写入;父目录需存在)
|
|
|
|
|
|
public func save(path: String): Unit {
|
|
|
|
|
|
let p = Path(path)
|
|
|
|
|
|
if (exists(p)) {
|
|
|
|
|
|
remove(p)
|
|
|
|
|
|
}
|
|
|
|
|
|
let f = File.create(p)
|
|
|
|
|
|
f.write(toJson().toArray())
|
|
|
|
|
|
f.close()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 从文件加载;文件不存在返回 None
|
|
|
|
|
|
public static func load(path: String): ?ModelSnapshot {
|
|
|
|
|
|
let p = Path(path)
|
|
|
|
|
|
if (exists(p)) {
|
|
|
|
|
|
let bytes = File.readFrom(p)
|
|
|
|
|
|
return Some(fromJson(String.fromUtf8(bytes)))
|
|
|
|
|
|
}
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 私有 ----------
|
|
|
|
|
|
|
|
|
|
|
|
private static func getString(o: HashMap<String, JsonValue>, k: String): String {
|
|
|
|
|
|
match (o.get(k)) {
|
|
|
|
|
|
case Some(JString(s)) => s
|
|
|
|
|
|
case _ => ""
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static func getBool(o: HashMap<String, JsonValue>, k: String): Bool {
|
|
|
|
|
|
match (o.get(k)) {
|
|
|
|
|
|
case Some(JBool(b)) => b
|
|
|
|
|
|
case _ => false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static func getLong(o: HashMap<String, JsonValue>, k: String): Int64 {
|
|
|
|
|
|
match (o.get(k)) {
|
|
|
|
|
|
case Some(JNumber(n)) => Int64.parse(n)
|
|
|
|
|
|
case Some(JString(s)) => Int64.parse(s)
|
|
|
|
|
|
case _ => 0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|