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

205 lines
8.5 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.
* 模型快照:把反射构建的 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<EntityModel> = ArrayList()
/// 快照格式版本
public static let formatVersion: Int64 = 3
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()
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<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}")
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<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
}
}
}