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
+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
}
}