Files
orm-cj/src/migrations/Migrator.cj
T
xrain afa6096ed2 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 个单元测试全部通过
2026-08-19 09:14:01 +08:00

279 lines
9.1 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.
* 迁移执行器:历史表 + 按 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
}
}