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:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user