/* * 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) * * 快照 JSON 结构(版本 3,较 v2 新增索引标记 idx/ixName/ixUnique): * {"version":3,"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}]}]} */ 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 = ArrayList() /// 快照格式版本 public static let formatVersion: Int64 = 3 public init() {} /// 从真实反射模型捕获元数据快照(剥离反射句柄) public static func capture(models: ArrayList): 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() lp.isEnum = p.isEnum lp.isIndex = p.isIndex lp.indexName = p.indexName lp.indexUnique = p.indexUnique 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 { models } /// 序列化为 JSON 文本 public func toJson(): String { let root = HashMap() root["version"] = JNumber("${formatVersion}") let modelsArr = ArrayList() for (m in models) { let mo = HashMap() mo["table"] = JString(m.tableName) let cols = ArrayList() for (p in m.properties) { let co = HashMap() 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}") co["enum"] = JBool(p.isEnum) co["idx"] = JBool(p.isIndex) co["ixName"] = JString(p.indexName) co["ixUnique"] = JBool(p.indexUnique) 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") lp.isEnum = getBool(co, "enum") lp.isIndex = getBool(co, "idx") lp.indexName = getString(co, "ixName") lp.indexUnique = getBool(co, "ixUnique") if (lp.isKey) { lm.keyProperty = Some(lp) } lm.properties.add(lp) case _ => throw Exception( "simcu::orm: 快照 columns 项必须是 object") } } case _ => throw Exception("simcu::orm: 快照 models[].columns 必须是数组") } snap.models.add(lm) case _ => throw Exception("simcu::orm: 快照 models[] 项必须是 object") } } case _ => throw Exception("simcu::orm: 快照缺少 models 数组") } case _ => throw Exception("simcu::orm: 快照根必须是 object") } 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, k: String): String { match (o.get(k)) { case Some(JString(s)) => s case _ => "" } } private static func getBool(o: HashMap, k: String): Bool { match (o.get(k)) { case Some(JBool(b)) => b case _ => false } } private static func getLong(o: HashMap, k: String): Int64 { match (o.get(k)) { case Some(JNumber(n)) => Int64.parse(n) case Some(JString(s)) => Int64.parse(s) case _ => 0 } } }