feat: orm-cj 仓颉版 EF Core 风格 ORM 初始版本

- 基于数据模型(POCO + 注解)的映射与增删改查:DbContext/DbSet/ChangeTracker/QueryBuilder
- ISqlDialect 方言接口 + PostgreSQL/openGauss 实现,零外部依赖
- @DbContext 宏:纯声明 DbSet 类自动展开为完整 DbContext 子类
- 迁移:Migration/Migrator/ModelSnapshot(快照 JSON)/MiniJson,从模型生成迁移
- 迁移 CLI:add/rm/update/downgrade/list/help,应用内嵌一行 db.cli(args) 接入
- DbContext 便捷方法:databaseExists/hasPendingMigrations/migrate
- 57 个单元测试全部通过
This commit is contained in:
2026-08-19 09:14:01 +08:00
commit afa6096ed2
28 changed files with 5822 additions and 0 deletions
+262
View File
@@ -0,0 +1,262 @@
/*
* 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])
*
* 限制(v1):不支持主键/自增属性变更(抛异常提示手写迁移);不生成索引(模型无索引注解)。
*/
package simcu::orm.migrations
import std.collection.*
import simcu::orm.model.*
import simcu::orm.sql.ColumnTypes
/**
* 模型 → 迁移操作生成器。
*/
public class MigrationGenerator {
public init() {}
/// 初始迁移:空库 → 当前模型(全部 CreateTabledown 为逆序 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))
}
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))
}
}
// 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>
public static func columnTypeFor(typeName: String): ColumnTypes {
match (typeName) {
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("simorm: 字段类型 ${typeName} 无法映射到列类型")
}
}
/// PropertyModel → ColumnDefinition(主键/自增/非空/最大长度 随注解映射)
public static func columnDefinition(p: PropertyModel): ColumnDefinition {
let c = ColumnDefinition(p.columnName, columnTypeFor(p.typeName()))
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
}
// ---------- 私有 ----------
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(
"simorm: 表 ${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(
"simorm: 表 ${cur.tableName} 列 ${p.columnName} 的主键/自增属性发生变化,v1 自动迁移不支持,请手写迁移")
}
if (!sameColumn(op, p)) {
upOps.add(alterColumnOp(cur.tableName, p))
downOps.add(alterColumnOp(old.tableName, op))
}
}
}
}
private static func sameColumn(a: PropertyModel, b: PropertyModel): Bool {
typeRank(columnTypeFor(a.typeName())) == typeRank(columnTypeFor(b.typeName())) &&
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
}
}
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/downdown 按 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
}
}
+48
View File
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 进程级迁移注册表:
* - CLI 生成的注册文件(src/migrations/MigrationRegistry.cj)在模块初始化时通过顶层
* let 自动调用 registerMigration 注册迁移类实例;
* - 没有迁移文件的默认项目无需注册任何迁移,应用照常编译运行(migrations() 返回空);
* - allMigrations() 按 migrationId 字典序返回,供 DbContext.migrations() 使用。
*/
package simcu::orm.migrations
import std.collection.*
/// 已注册的全部迁移(按注册顺序存储,读取时排序)
private let _registeredMigrations = ArrayList<Migration>()
/// 注册一个迁移实例(按 migrationId 去重,幂等)。
/// CLI 生成的注册文件在顶层 `let _r0 = registerMigration(Xxx())` 中调用。
public func registerMigration(migration: Migration): Bool {
for (m in _registeredMigrations) {
if (m.migrationId == migration.migrationId) {
return false
}
}
_registeredMigrations.add(migration)
true
}
/// 全部已注册迁移,按 migrationId 字典序返回新列表(不修改注册表)
public func allMigrations(): ArrayList<Migration> {
let list = ArrayList<Migration>()
for (m in _registeredMigrations) {
list.add(m)
}
// 插入排序(迁移 id 按时间戳生成,基本有序)
var i: Int64 = 1
while (i < list.size) {
let cur = list[i]
var j = i - 1
while (j >= 0 && list[j].migrationId > cur.migrationId) {
list[j + 1] = list[j]
j -= 1
}
list[j + 1] = cur
i += 1
}
list
}
+402
View File
@@ -0,0 +1,402 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 迁移基础设施:Migration 基类 + MigrationBuilder 声明式操作 + DDL SQL 生成。
*
* 用法(对齐 EF Core 迁移):
* public class InitialCreate <: Migration {
* public init() { super("20250701000000_InitialCreate", "创建用户表") }
* public override func up(builder: MigrationBuilder): Unit {
* builder.createTable("users") { tb =>
* tb.column("id", ColumnTypes.BigIntCol).primary().autoInc().notNull()
* tb.column("name", ColumnTypes.TextCol).withMaxLength(100).notNull()
* }
* builder.createIndex("ix_users_name", "users", ["name"])
* }
* public override func down(builder: MigrationBuilder): Unit {
* builder.dropTable("users")
* }
* }
*/
package simcu::orm.migrations
import std.collection.*
import std.time.*
import simcu::orm.sql.*
/**
* 列定义(表结构/迁移用)。列类型 ColumnTypes 定义于 simcu::orm.sqlDDL 映射由方言决定。
*/
public class ColumnDefinition {
public var name: String = ""
public var columnType: ColumnTypes = ColumnTypes.TextCol
public var primaryKey: Bool = false
public var autoIncrement: Bool = false
public var nullable: Bool = true
public var unique: Bool = false
public var maxLength: Int64 = 255
public var defaultValue: ?Any = None
public init(name: String, columnType: ColumnTypes) {
this.name = name
this.columnType = columnType
}
/// 标记主键
public func primary(): ColumnDefinition {
this.primaryKey = true
this
}
/// 标记自增(仅整型列有效)
public func autoInc(): ColumnDefinition {
this.autoIncrement = true
this
}
/// 标记非空
public func notNull(): ColumnDefinition {
this.nullable = false
this
}
/// 标记唯一
public func withUnique(): ColumnDefinition {
this.unique = true
this
}
/// 指定字符串最大长度
public func withMaxLength(n: Int64): ColumnDefinition {
this.maxLength = n
this
}
/// 指定默认值
public func withDefault(value: Any): ColumnDefinition {
this.defaultValue = Some(value)
this
}
}
/**
* 迁移操作类型。
*/
public enum MigrationOperationKind {
| CreateTable
| DropTable
| AddColumn
| DropColumn
| AlterColumn
| RenameColumn
| CreateIndex
| DropIndex
| RawSql
}
/**
* 一条迁移操作(不同 kind 使用对应字段)。
*/
public class MigrationOperation {
public let kind: MigrationOperationKind
public var tableName: String = ""
public var columnName: String = ""
public var newColumnName: String = ""
public var column: ?ColumnDefinition = None
public var columnDefs: ArrayList<ColumnDefinition> = ArrayList()
public var indexName: String = ""
public var columnNames: ArrayList<String> = ArrayList()
public var unique: Bool = false
public var sql: String = ""
public init(kind: MigrationOperationKind) {
this.kind = kind
}
}
/**
* 建表辅助(createTable 的闭包参数)。
*/
public class TableBuilder {
public let tableName: String
internal let columns = ArrayList<ColumnDefinition>()
public init(tableName: String) {
this.tableName = tableName
}
/// 声明一列(返回列定义以支持链式标记)
public func column(name: String, columnType: ColumnTypes): ColumnDefinition {
let c = ColumnDefinition(name, columnType)
columns.add(c)
c
}
}
/**
* 迁移构建器:声明式累积操作(对齐 EF Core MigrationBuilder)。
*/
public class MigrationBuilder {
internal let operations = ArrayList<MigrationOperation>()
public init() {}
/// 当前累积的操作列表(供执行器/测试读取)
public func getOperations(): ArrayList<MigrationOperation> {
operations
}
public func createTable(name: String, build: (TableBuilder) -> Unit): MigrationBuilder {
let tb = TableBuilder(name)
build(tb)
let op = MigrationOperation(MigrationOperationKind.CreateTable)
op.tableName = name
op.columnDefs = tb.columns
operations.add(op)
this
}
public func dropTable(name: String): MigrationBuilder {
let op = MigrationOperation(MigrationOperationKind.DropTable)
op.tableName = name
operations.add(op)
this
}
public func addColumn(table: String, column: ColumnDefinition): MigrationBuilder {
let op = MigrationOperation(MigrationOperationKind.AddColumn)
op.tableName = table
op.column = Some(column)
operations.add(op)
this
}
public func dropColumn(table: String, columnName: String): MigrationBuilder {
let op = MigrationOperation(MigrationOperationKind.DropColumn)
op.tableName = table
op.columnName = columnName
operations.add(op)
this
}
public func alterColumn(table: String, column: ColumnDefinition): MigrationBuilder {
let op = MigrationOperation(MigrationOperationKind.AlterColumn)
op.tableName = table
op.column = Some(column)
operations.add(op)
this
}
public func renameColumn(table: String, oldName: String, newName: String): MigrationBuilder {
let op = MigrationOperation(MigrationOperationKind.RenameColumn)
op.tableName = table
op.columnName = oldName
op.newColumnName = newName
operations.add(op)
this
}
public func createIndex(indexName: String, table: String, columns: ArrayList<String>): MigrationBuilder {
createIndex(indexName, table, columns, false)
}
public func createIndex(indexName: String, table: String, columns: ArrayList<String>,
unique: Bool): MigrationBuilder {
let op = MigrationOperation(MigrationOperationKind.CreateIndex)
op.indexName = indexName
op.tableName = table
op.columnNames = columns
op.unique = unique
operations.add(op)
this
}
public func dropIndex(indexName: String, table: String): MigrationBuilder {
let op = MigrationOperation(MigrationOperationKind.DropIndex)
op.indexName = indexName
op.tableName = table
operations.add(op)
this
}
public func rawSql(sql: String): MigrationBuilder {
let op = MigrationOperation(MigrationOperationKind.RawSql)
op.sql = sql
operations.add(op)
this
}
}
/**
* 迁移基类:migrationId 形如 "20250701000000_InitialCreate"(时间戳_名称),按字典序应用。
* 两种使用方式:
* 1. 子类 override up/down 手写声明式操作(MigrationBuilder);
* 2. 由 MigrationGenerator 从模型生成时注入 preset 操作,基类 up/down 自动重放。
*/
public open class Migration {
public let migrationId: String
public let description: String
/// 由 MigrationGenerator 注入的 up 操作(按序执行)
internal var presetOperations = ArrayList<MigrationOperation>()
/// 由 MigrationGenerator 注入的 down 操作(按序执行,已按 up 逆序反转)
internal var presetDownOperations = ArrayList<MigrationOperation>()
public init(migrationId: String, description: String) {
this.migrationId = migrationId
this.description = description
}
public init(migrationId: String) {
this.migrationId = migrationId
this.description = ""
}
public open func up(builder: MigrationBuilder): Unit {
for (op in presetOperations) {
builder.operations.add(op)
}
}
public open func down(builder: MigrationBuilder): Unit {
for (op in presetDownOperations) {
builder.operations.add(op)
}
}
}
/**
* DDL SQL 工厂:MigrationOperation → SQL(类型映射与差异语法委托给 ISqlDialect)。
*/
public class DdlFactory {
public init() {}
public func toSql(op: MigrationOperation, dialect: ISqlDialect): String {
match (op.kind) {
case MigrationOperationKind.CreateTable => createTableSql(op, dialect)
case MigrationOperationKind.DropTable => "DROP TABLE IF EXISTS ${dialect.quoteName(op.tableName)}"
case MigrationOperationKind.AddColumn =>
"ALTER TABLE ${dialect.quoteName(op.tableName)} ADD COLUMN ${columnDefSql(op.column.getOrThrow(), dialect)}"
case MigrationOperationKind.DropColumn =>
"ALTER TABLE ${dialect.quoteName(op.tableName)} DROP COLUMN ${dialect.quoteName(op.columnName)}"
case MigrationOperationKind.AlterColumn =>
dialect.buildAlterColumn(op.tableName, columnDefSql(op.column.getOrThrow(), dialect))
case MigrationOperationKind.RenameColumn =>
"ALTER TABLE ${dialect.quoteName(op.tableName)} RENAME COLUMN ${dialect.quoteName(op.columnName)} TO ${dialect.quoteName(op.newColumnName)}"
case MigrationOperationKind.CreateIndex => createIndexSql(op, dialect)
case MigrationOperationKind.DropIndex => dialect.buildDropIndex(op.indexName, op.tableName)
case MigrationOperationKind.RawSql => op.sql
}
}
private func createTableSql(op: MigrationOperation, dialect: ISqlDialect): String {
let sb = StringBuilder()
sb.append("CREATE TABLE IF NOT EXISTS ${dialect.quoteName(op.tableName)} (\n")
var first = true
for (c in op.columnDefs) {
if (!first) {
sb.append(",\n")
}
sb.append(" ${columnDefSql(c, dialect)}")
first = false
}
sb.append("\n)")
sb.toString()
}
private func createIndexSql(op: MigrationOperation, dialect: ISqlDialect): String {
let sb = StringBuilder()
sb.append("CREATE ")
if (op.unique) {
sb.append("UNIQUE ")
}
sb.append("INDEX ${dialect.quoteName(op.indexName)} ON ${dialect.quoteName(op.tableName)} (")
var first = true
for (c in op.columnNames) {
if (!first) {
sb.append(", ")
}
sb.append(dialect.quoteName(c))
first = false
}
sb.append(")")
sb.toString()
}
private func columnDefSql(col: ColumnDefinition, dialect: ISqlDialect): String {
let sb = StringBuilder()
sb.append(dialect.quoteName(col.name))
sb.append(" ${dialect.columnTypeSql(col.columnType, col.autoIncrement, col.maxLength)}")
if (!col.nullable || col.primaryKey) {
sb.append(" NOT NULL")
}
if (col.primaryKey) {
sb.append(" PRIMARY KEY")
}
if (col.unique) {
sb.append(" UNIQUE")
}
if (let Some(dv) <- col.defaultValue) {
sb.append(" DEFAULT ${defaultValueSql(dv)}")
}
sb.toString()
}
private func defaultValueSql(v: Any): String {
if (let s: String <- v) {
return "'${escapeQuotes(s)}'"
}
if (let b: Bool <- v) {
return if (b) { "TRUE" } else { "FALSE" }
}
if (let i64: Int64 <- v) {
return "${i64}"
}
if (let i32: Int32 <- v) {
return "${i32}"
}
if (let i16: Int16 <- v) {
return "${i16}"
}
if (let i8: Int8 <- v) {
return "${i8}"
}
if (let u64: UInt64 <- v) {
return "${u64}"
}
if (let u32: UInt32 <- v) {
return "${u32}"
}
if (let u16: UInt16 <- v) {
return "${u16}"
}
if (let u8: UInt8 <- v) {
return "${u8}"
}
if (let f64: Float64 <- v) {
return "${f64}"
}
if (let f32: Float32 <- v) {
return "${f32}"
}
if (let r: Rune <- v) {
return "'${r}'"
}
if (let d: DateTime <- v) {
return "'${d.toString()}'"
}
throw Exception("simorm: 不支持的默认值类型,请使用 String/Bool/数值/Rune/DateTime")
}
private func escapeQuotes(s: String): String {
var sb = StringBuilder()
for (c in s.runes()) {
if (c == Rune(0x27)) {
sb.append("''")
} else {
sb.append(c)
}
}
sb.toString()
}
}
+278
View File
@@ -0,0 +1,278 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 迁移执行器:历史表 + 按 migrationId 排序应用未执行的迁移(事务内)。
*
* 历史表 simcu_orm_migrations (id VARCHAR(64) PK, name VARCHAR(256), applied_at TIMESTAMP)
* 应用顺序 = migrationId 字典序(建议用 "yyyyMMddHHmmss_名称" 命名)。
*/
package simcu::orm.migrations
import std.collection.*
import std.database.sql.*
import std.time.*
import simcu::orm.sql.*
/**
* 迁移执行器。
*/
public class Migrator {
private let _datasource: Datasource
private let _dialect: ISqlDialect
private let _historyTable = "simcu_orm_migrations"
public init(datasource: Datasource, dialect: ISqlDialect) {
this._datasource = datasource
this._dialect = dialect
}
public init(datasource: Datasource) {
this(datasource, OpenGaussDialect())
}
/// 应用所有未执行的迁移,返回本次应用的数量
public func migrate(migrations: ArrayList<Migration>): Int64 {
ensureHistoryTable()
let applied = loadApplied()
let sorted = sortMigrations(migrations)
var count: Int64 = 0
let conn = _datasource.connect()
let tx = conn.createTransaction()
tx.begin()
try {
for (m in sorted) {
if (applied.contains(m.migrationId)) {
continue
}
let builder = MigrationBuilder()
m.up(builder)
let factory = DdlFactory()
for (op in builder.operations) {
let stmt = conn.prepareStatement(factory.toSql(op, _dialect))
try {
stmt.update()
} finally {
stmt.close()
}
}
recordApplied(conn, m)
count += 1
}
tx.commit()
count
} catch (e: Exception) {
try {
tx.rollback()
} catch (_) {
()
}
throw e
} finally {
conn.close()
}
}
/// 生成"该执行但尚未执行"的迁移集合(不执行,供用户预览/校验)
public func pending(migrations: ArrayList<Migration>): ArrayList<Migration> {
let applied = loadApplied()
let sorted = sortMigrations(migrations)
let result = ArrayList<Migration>()
for (m in sorted) {
if (!applied.contains(m.migrationId)) {
result.add(m)
}
}
result
}
/// 回退最近一个已应用的迁移(执行 down + 删除历史记录),返回回退的数量
public func revert(migrations: ArrayList<Migration>): Int64 {
revert(migrations, None)
}
/// 回退到目标迁移(不含目标):执行目标之后全部已应用迁移的 down。
/// target 匹配规则:精确(migrationId / description)或唯一前缀(migrationId)。
/// 返回回退的数量;无已应用迁移或已处于目标时返回 0。
public func revert(migrations: ArrayList<Migration>, target: ?String): Int64 {
ensureHistoryTable()
let applied = loadApplied()
if (applied.size == 0) {
return 0
}
let sorted = sortMigrations(migrations)
// 已应用迁移(按 id 升序)
let appliedList = ArrayList<Migration>()
for (m in sorted) {
if (applied.contains(m.migrationId)) {
appliedList.add(m)
}
}
if (appliedList.size == 0) {
return 0
}
// 目标下标:默认回退最新一个;指定目标时回退到目标之后(不含目标)
var targetIdx = appliedList.size - 1
if (let Some(t) <- target) {
targetIdx = resolveTarget(appliedList, t) + 1
if (targetIdx >= appliedList.size) {
return 0
}
}
var count: Int64 = 0
let conn = _datasource.connect()
let tx = conn.createTransaction()
tx.begin()
try {
for (i in targetIdx..appliedList.size) {
let m = appliedList[appliedList.size - 1 - (i - targetIdx)]
let builder = MigrationBuilder()
m.down(builder)
let factory = DdlFactory()
for (op in builder.operations) {
let stmt = conn.prepareStatement(factory.toSql(op, _dialect))
try {
stmt.update()
} finally {
stmt.close()
}
}
removeApplied(conn, m)
count += 1
}
tx.commit()
count
} catch (e: Exception) {
try {
tx.rollback()
} catch (_) {
()
}
throw e
} finally {
conn.close()
}
}
private func resolveTarget(appliedList: ArrayList<Migration>, target: String): Int64 {
var exact: Int64 = -1
let prefixHits = ArrayList<Int64>()
for (i in 0..appliedList.size) {
let m = appliedList[i]
if (m.migrationId == target || m.description == target) {
exact = i
} else if (m.migrationId.startsWith(target)) {
prefixHits.add(i)
}
}
if (exact >= 0) {
return exact
}
if (prefixHits.size == 1) {
return prefixHits[0]
}
if (prefixHits.size == 0) {
throw Exception("simorm: 未找到迁移 '${target}'(已应用的迁移中无匹配)")
}
throw Exception("simorm: 迁移目标 '${target}' 不唯一,请使用完整 id")
}
private func removeApplied(conn: Connection, m: Migration): Unit {
let stmt = conn.prepareStatement(
"DELETE FROM ${_dialect.quoteName(_historyTable)} WHERE ${_dialect.quoteName("id")} = ?")
try {
stmt.set<String>(0, m.migrationId)
stmt.update()
} finally {
stmt.close()
}
}
/// 已应用的迁移 id 列表(字典序,供 CLI list 使用)
public func appliedMigrationIds(): ArrayList<String> {
let ids = loadApplied()
let result = ArrayList<String>()
for (id in ids) {
result.add(id)
}
// 插入排序(字典序)
for (i in 1..result.size) {
var j = i
while (j > 0 && result[j] < result[j - 1]) {
let t = result[j]
result[j] = result[j - 1]
result[j - 1] = t
j -= 1
}
}
result
}
private func ensureHistoryTable(): Unit {
let conn = _datasource.connect()
try {
let stmt = conn.prepareStatement(
"CREATE TABLE IF NOT EXISTS ${_dialect.quoteName(_historyTable)} (\n" +
" ${_dialect.quoteName("id")} VARCHAR(64) NOT NULL PRIMARY KEY,\n" +
" ${_dialect.quoteName("name")} VARCHAR(256) NOT NULL,\n" +
" ${_dialect.quoteName("applied_at")} TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n)")
try {
stmt.update()
} finally {
stmt.close()
}
} finally {
conn.close()
}
}
private func loadApplied(): HashSet<String> {
let conn = _datasource.connect()
try {
let stmt = conn.prepareStatement(
"SELECT ${_dialect.quoteName("id")} FROM ${_dialect.quoteName(_historyTable)}")
try {
let rs = stmt.query()
let ids = HashSet<String>()
while (rs.next()) {
ids.add(rs.getOrNull<String>(0).getOrThrow())
}
ids
} finally {
stmt.close()
}
} finally {
conn.close()
}
}
private func recordApplied(conn: Connection, m: Migration): Unit {
let stmt = conn.prepareStatement(
"INSERT INTO ${_dialect.quoteName(_historyTable)} (${_dialect.quoteName("id")}, ${_dialect.quoteName("name")}) VALUES (?, ?)")
try {
stmt.set<String>(0, m.migrationId)
stmt.set<String>(1, if (m.description.isEmpty()) { m.migrationId } else { m.description })
stmt.update()
} finally {
stmt.close()
}
}
private static func sortMigrations(migrations: ArrayList<Migration>): ArrayList<Migration> {
let arr = migrations.toArray()
// 插入排序(migrationId 字典序)
for (i in 1..arr.size) {
var j = i
while (j > 0 && arr[j].migrationId < arr[j - 1].migrationId) {
let t = arr[j]
arr[j] = arr[j - 1]
arr[j - 1] = t
j -= 1
}
}
let result = ArrayList<Migration>()
for (m in arr) {
result.add(m)
}
result
}
}
+323
View File
@@ -0,0 +1,323 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 迷你 JSON 解析/序列化(orm-cj 零依赖,仅用于模型快照 / appsettings.json 这种结构固定的 JSON)。
* 支持:object / array / string(含转义) / number / true / false / null。
*/
package simcu::orm.migrations
import std.collection.*
/**
* JSON 值(枚举承载,number 保留原始文本避免精度损失)。
*/
public enum JsonValue {
| JObject(HashMap<String, JsonValue>)
| JArray(ArrayList<JsonValue>)
| JString(String)
| JBool(Bool)
| JNumber(String)
| JNull
}
/**
* 迷你 JSON 工具。
*/
public class MiniJson {
private init() {}
public static func parse(text: String): JsonValue {
let p = JsonParser(text)
p.skipWs()
let v = p.parseValue()
p.skipWs()
if (p.pos < text.size) {
throw Exception("mini-json: 尾随字符 @${p.pos}")
}
v
}
public static func stringify(v: JsonValue): String {
let sb = StringBuilder()
write(v, sb)
sb.toString()
}
private static func write(v: JsonValue, sb: StringBuilder): Unit {
match (v) {
case JObject(map) =>
sb.append("{")
var first = true
for ((k, v) in map) {
if (!first) {
sb.append(",")
}
first = false
sb.append("\"")
escape(k, sb)
sb.append("\":")
write(v, sb)
}
sb.append("}")
case JArray(arr) =>
sb.append("[")
var first = true
for (e in arr) {
if (!first) {
sb.append(",")
}
first = false
write(e, sb)
}
sb.append("]")
case JString(s) =>
sb.append("\"")
escape(s, sb)
sb.append("\"")
case JBool(b) => sb.append(if (b) { "true" } else { "false" })
case JNumber(n) => sb.append(n)
case JNull => sb.append("null")
}
}
private static func escape(s: String, sb: StringBuilder): Unit {
for (c in s.runes()) {
let ch = UInt32(c)
if (c == Rune(0x22)) {
sb.append("\\\"")
} else if (c == Rune(0x5C)) {
sb.append("\\\\")
} else if (c == Rune(0x08)) {
sb.append("\\b")
} else if (c == Rune(0x0C)) {
sb.append("\\f")
} else if (c == Rune(0x0A)) {
sb.append("\\n")
} else if (c == Rune(0x0D)) {
sb.append("\\r")
} else if (c == Rune(0x09)) {
sb.append("\\t")
} else if (ch < 0x20) {
sb.append("\\u")
sb.append(toHex4(ch))
} else {
sb.append(c)
}
}
}
private static func toHex4(v: UInt32): String {
const digits = "0123456789abcdef"
var sb = StringBuilder()
sb.append(digits[Int64((v >> 12) & 0xF)..Int64((v >> 12) & 0xF) + 1])
sb.append(digits[Int64((v >> 8) & 0xF)..Int64((v >> 8) & 0xF) + 1])
sb.append(digits[Int64((v >> 4) & 0xF)..Int64((v >> 4) & 0xF) + 1])
sb.append(digits[Int64(v & 0xF)..Int64(v & 0xF) + 1])
sb.toString()
}
}
/**
* 递归下降解析器。
*/
internal class JsonParser {
let text: String
var pos: Int64 = 0
init(text: String) {
this.text = text
}
public func skipWs(): Unit {
while (pos < text.size) {
let c = text[pos]
if (c == 0x20 || c == 0x09 || c == 0x0A || c == 0x0D) {
pos += 1
} else {
break
}
}
}
public func parseValue(): JsonValue {
skipWs()
if (pos >= text.size) {
throw Exception("mini-json: 意外结尾")
}
let c = text[pos]
if (c == 0x7B) {
return parseObject()
}
if (c == 0x5B) {
return parseArray()
}
if (c == 0x22) {
return JString(parseString())
}
parseLiteralOrNumber()
}
private func parseObject(): JsonValue {
pos += 1 // {
let map = HashMap<String, JsonValue>()
skipWs()
if (pos < text.size && text[pos] == 0x7D) {
pos += 1
return JObject(map)
}
while (true) {
skipWs()
if (pos >= text.size || text[pos] != 0x22) {
throw Exception("mini-json: 期望字段名")
}
let key = parseString()
skipWs()
if (pos >= text.size || text[pos] != 0x3A) {
throw Exception("mini-json: 期望 ':'")
}
pos += 1
let v = parseValue()
map[key] = v
skipWs()
if (pos >= text.size) {
throw Exception("mini-json: object 未闭合")
}
if (text[pos] == 0x2C) {
pos += 1
continue
}
if (text[pos] == 0x7D) {
pos += 1
return JObject(map)
}
throw Exception("mini-json: 期望 ',' 或 '}'")
}
// 不可达:循环内所有退出路径均已 return/throw
throw Exception("mini-json: object 解析异常")
}
private func parseArray(): JsonValue {
pos += 1 // [
let arr = ArrayList<JsonValue>()
skipWs()
if (pos < text.size && text[pos] == 0x5D) {
pos += 1
return JArray(arr)
}
while (true) {
let v = parseValue()
arr.add(v)
skipWs()
if (pos >= text.size) {
throw Exception("mini-json: array 未闭合")
}
if (text[pos] == 0x2C) {
pos += 1
continue
}
if (text[pos] == 0x5D) {
pos += 1
return JArray(arr)
}
throw Exception("mini-json: 期望 ',' 或 ']'")
}
// 不可达:循环内所有退出路径均已 return/throw
throw Exception("mini-json: array 解析异常")
}
private func parseString(): String {
pos += 1 // "
let sb = StringBuilder()
while (pos < text.size) {
let c = text[pos]
if (c == 0x22) {
pos += 1
return sb.toString()
}
if (c == 0x5C) {
pos += 1
if (pos >= text.size) {
throw Exception("mini-json: 转义不完整")
}
let e = text[pos]
if (e == 0x22) {
sb.append(Rune(0x22))
} else if (e == 0x5C) {
sb.append(Rune(0x5C))
} else if (e == 0x2F) {
sb.append(Rune(0x2F))
} else if (e == 0x62) {
sb.append(Rune(0x08))
} else if (e == 0x66) {
sb.append(Rune(0x0C))
} else if (e == 0x6E) {
sb.append(Rune(0x0A))
} else if (e == 0x72) {
sb.append(Rune(0x0D))
} else if (e == 0x74) {
sb.append(Rune(0x09))
} else if (e == 0x75) {
pos += 1
if (pos + 4 > text.size) {
throw Exception("mini-json: \\u 转义不完整")
}
var code: Int64 = 0
for (i in 0..4) {
code = code * 16 + hexDigit(text[pos + i])
}
sb.append(Rune(UInt32(code)))
} else {
throw Exception("mini-json: 非法转义 '\\${e}'")
}
pos += 1
continue
}
sb.append(Rune(UInt32(c)))
pos += 1
}
throw Exception("mini-json: 字符串未闭合")
}
private func parseLiteralOrNumber(): JsonValue {
let start = pos
while (pos < text.size) {
let c = text[pos]
let ch = UInt32(c)
let isLetter = (ch >= 0x41 && ch <= 0x5A) || (ch >= 0x61 && ch <= 0x7A)
let isDigit = ch >= 0x30 && ch <= 0x39
if (isLetter || isDigit || c == 0x2D || c == 0x2B || c == 0x2E) {
pos += 1
} else {
break
}
}
let raw = text[start..pos]
if (raw == "true") {
return JBool(true)
}
if (raw == "false") {
return JBool(false)
}
if (raw == "null") {
return JNull
}
if (raw == "") {
throw Exception("mini-json: 无法识别的字面量 @${pos}")
}
JNumber(raw)
}
private static func hexDigit(c: UInt8): Int64 {
let ch = UInt32(c)
if (ch >= 0x30 && ch <= 0x39) {
return Int64(ch - 0x30)
}
if (ch >= 0x41 && ch <= 0x46) {
return Int64(ch - 0x41 + 10)
}
if (ch >= 0x61 && ch <= 0x66) {
return Int64(ch - 0x61 + 10)
}
throw Exception("mini-json: 非法 hex 字符")
}
}
+191
View File
@@ -0,0 +1,191 @@
/*
* 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 结构(版本 1):
* {"version":1,"models":[{"table":"users","columns":[
* {"name":"_id","column":"id","type":"Int64","key":true,"auto":true,
* "client":false,"required":false,"maxLen":0}]}]}
*/
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 = 1
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()
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}")
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")
if (lp.isKey) {
lm.keyProperty = Some(lp)
}
lm.properties.add(lp)
case _ => throw Exception(
"simorm: 快照 columns 项必须是 object")
}
}
case _ => throw Exception("simorm: 快照 models[].columns 必须是数组")
}
snap.models.add(lm)
case _ => throw Exception("simorm: 快照 models[] 项必须是 object")
}
}
case _ => throw Exception("simorm: 快照缺少 models 数组")
}
case _ => throw Exception("simorm: 快照根必须是 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
}
}
}