package aceboot::orm.common
import std.collection.*
/**
* 给已有的 QueryRunner(manager.cj,负责事务边界)扩展「迁移用的 DDL/DML + schema 内省/同步」能力。
* 迁移的 up/down 通过 QueryRunner 既能跑裸 SQL(参数化),也能用建表/补列/对比的高层助手——与具体数据库解耦。
* 经 connection()(底层 Driver)与 dialect 委托执行,不引入新可变状态。
*/
extend QueryRunner {
func dialect(): Dialect {
connection().dialect()
}
/** 执行写语句(DDL/DML),返回受影响行数(非 DML/未知返回 -1)。 */
public func exec(sql: String): Int64 {
connection().exec(sql, [])
}
/** 执行带参数的写语句(参数按方言占位符顺序绑定)。 */
public func exec(sql: String, params: Array<DbValue>): Int64 {
connection().exec(sql, params)
}
/** 执行 SELECT,返回行集。 */
public func query(sql: String): Array<Row> {
connection().query(sql, [])
}
/** 执行带参数的 SELECT。 */
public func query(sql: String, params: Array<DbValue>): Array<Row> {
connection().query(sql, params)
}
/** 表是否存在(走方言内省 SQL:SQLite→sqlite_master,PG/MySQL→information_schema)。 */
public func hasTable(table: String): Bool {
let (sql, ps) = dialect().introspectTableExists(table)
return connection().query(sql, ps).size > 0
}
/** 列出某表现有列名(顺序与底层一致)。 */
public func columnNames(table: String): Array<String> {
let (sql, ps) = dialect().introspectColumns(table)
let rows = connection().query(sql, ps)
let out = ArrayList<String>()
for (r in rows) {
out.add(r.getText("name"))
}
return out.toArray()
}
/** 某列是否存在。 */
public func hasColumn(table: String, col: String): Bool {
for (c in columnNames(table)) {
if (c == col) {
return true
}
}
return false
}
/** 列出某表现有列的「名 + 库现状类型」(顺序与底层一致),供 schema diff 比对/删列。 */
public func columnInfos(table: String): Array<(String, String)> {
let (sql, ps) = dialect().introspectColumnTypes(table)
let rows = connection().query(sql, ps)
let out = ArrayList<(String, String)>()
for (r in rows) {
out.add((r.getText("name"), r.getText("type")))
}
return out.toArray()
}
/** 建表(CREATE TABLE IF NOT EXISTS,幂等)。 */
public func createTable(table: String, cols: Array<ColumnSpec>): Unit {
connection().exec(dialect().createTable(table, cols), [])
}
/** 给已有表追加一列(ALTER TABLE ... ADD COLUMN)。 */
public func addColumn(table: String, col: ColumnSpec): Unit {
connection().exec(buildAddColumn(table, col, dialect()), [])
}
/**
* schema sync:对比「现状 → 目标」,把表 cols 同步到位——
* - 缺表则按 cols 建表;
* - 表已存在则逐列检查:
* · 缺列 → ADD COLUMN(已存在列默认保持不动);
* · 类型不一致 → ALTER COLUMN ... TYPE(PG)/MODIFY COLUMN(MySQL);
* SQLite 原地改列类型代价高,本轮 best-effort 跳过(方言 alterColumnType 返回 None)。
* · 主键列不参与改列类型(自增/SERIAL 不应被覆盖)。
* - dropMissing=true(显式开启)时:实体已无、库仍有的列 → DROP COLUMN。
* 默认 false 以防误删数据列;删列前需调用方明确开启。
*/
public func syncTable(table: String, cols: Array<ColumnSpec>, dropMissing!: Bool = false): Unit {
if (!hasTable(table)) {
createTable(table, cols)
return
}
let infos = columnInfos(table)
// 1. 缺列 ADD;2. 类型变更 ALTER/MODIFY(主键列与 SQLite 跳过)。
for (c in cols) {
if (!schemaColumnPresent(infos, c.name)) {
addColumn(table, c)
} else if (!c.isPk && !dialect().columnTypeMatches(schemaColumnType(infos, c.name), c)) {
match (dialect().alterColumnType(table, c)) {
case Some(sql) => connection().exec(sql, [])
case None => () // SQLite:best-effort 跳过原地改列类型
}
}
}
// 3. 删列(显式开启):实体侧已无、库仍有的列 → DROP COLUMN。
if (dropMissing) {
for (info in infos) {
let (name, _) = info
if (!schemaTargetHasColumn(cols, name)) {
connection().exec(dialect().dropColumn(table, name), [])
}
}
}
}
}
// infos 中是否存在名为 name 的列。
func schemaColumnPresent(infos: Array<(String, String)>, name: String): Bool {
for (info in infos) {
let (n, _) = info
if (n == name) {
return true
}
}
return false
}
// 取 infos 中 name 列的库现状类型("" 表示未找到)。
func schemaColumnType(infos: Array<(String, String)>, name: String): String {
for (info in infos) {
let (n, t) = info
if (n == name) {
return t
}
}
return ""
}
// 目标列集合中是否含名为 name 的列(删列 diff 用)。
func schemaTargetHasColumn(cols: Array<ColumnSpec>, name: String): Bool {
for (c in cols) {
if (c.name == name) {
return true
}
}
return false
}
/**
* 一次数据库迁移(对标 TypeORM Migration):name 唯一标识(也是历史表里的去重键),
* up 正向演进 schema/数据,down 回滚。两者均通过 QueryRunner 执行,跨方言可移植。
* 实现为类(class):`class AddUsers <: Migration { ... }`。
*/
public interface Migration {
/** 迁移唯一名(建议带时间戳前缀以保证执行顺序,如 "1700000000_create_user")。 */
func name(): String
/** 正向迁移:建表 / 加列 / 数据回填等。 */
func up(runner: QueryRunner): Unit
/** 回滚迁移:与 up 相反的操作。 */
func down(runner: QueryRunner): Unit
}
/** 迁移历史表名(记录已执行迁移,保证 runMigrations 幂等)。 */
let MIGRATION_HISTORY_TABLE = "ace_migrations"
/**
* 迁移执行器(对标 TypeORM MigrationExecutor):驱动一组 Migration 的执行与回滚,并维护历史表。
* - runMigrations:按数组顺序跑「尚未执行」的迁移的 up,逐条记入历史表(幂等:已执行则跳过)。
* - revertLast:回滚「最近一条已执行」迁移的 down,并从历史表删记录。
* 每条迁移在一个事务内执行(up/down + 历史表写入原子),失败则整体回滚、历史表不留半条。
*/
public class MigrationRunner {
let ds: DataSource
public init(ds: DataSource) {
this.ds = ds
}
/** 用全局数据源构造(配合 @Transactional / OrmComponent 场景)。 */
public static func fromGlobal(): MigrationRunner {
MigrationRunner(globalDataSource())
}
// 确保历史表存在(id 自增主键 / name 唯一 / executedAt 毫秒时间戳)。幂等。
// 兼容旧版历史表(只有 id+name,无 executedAt):CREATE TABLE IF NOT EXISTS 不补列,
// 需在建表后额外检测并 ALTER TABLE ADD COLUMN。
func ensureHistoryTable(): Unit {
let cols = [
ColumnSpec("id", DbType.TInt, true, ""),
ColumnSpec("name", DbType.TText, false, "", nullable: false, unique: true),
ColumnSpec("executedAt", DbType.TInt, false, "", nullable: false)
]
ds.driver.exec(ds.driver.dialect().createTable(MIGRATION_HISTORY_TABLE, cols), [])
let qr = QueryRunner(ds)
if (!qr.hasColumn(MIGRATION_HISTORY_TABLE, "executedAt")) {
qr.addColumn(MIGRATION_HISTORY_TABLE,
ColumnSpec("executedAt", DbType.TInt, false, "", nullable: false))
}
}
// 已执行迁移名集合(用于去重判定)。
func executedNames(): HashSet<String> {
let set = HashSet<String>()
let rows = ds.driver.query("SELECT name FROM " + MIGRATION_HISTORY_TABLE, [])
for (r in rows) {
set.add(r.getText("name"))
}
return set
}
// 记入一条执行历史。
func recordExecuted(name: String): Unit {
let d = ds.driver.dialect()
let sql = "INSERT INTO " + MIGRATION_HISTORY_TABLE + "(name, executedAt) VALUES(" + d.placeholder(0) + ", " +
d.placeholder(1) + ")"
ds.driver.exec(sql, [DbText(name), DbInt(nowMillis())])
}
// 删除一条执行历史。
func deleteRecord(name: String): Unit {
let d = ds.driver.dialect()
ds.driver.exec("DELETE FROM " + MIGRATION_HISTORY_TABLE + " WHERE name = " + d.placeholder(0), [DbText(name)])
}
/**
* 按数组顺序执行所有「尚未执行」的迁移的 up,逐条记入历史表。
* 幂等:历史表里已有同名记录的迁移会被跳过(可安全重复调用)。
* 返回本次实际执行的迁移名(按执行顺序)。
*/
public func runMigrations(migrations: Array<Migration>): Array<String> {
ensureHistoryTable()
let done = executedNames()
let runner = QueryRunner(ds)
let ran = ArrayList<String>()
for (mg in migrations) {
if (done.contains(mg.name())) {
continue
}
let m = mg // 固定绑定,避免闭包捕获循环变量
ds.transaction({
=>
m.up(runner)
recordExecuted(m.name())
})
ran.add(m.name())
}
return ran.toArray()
}
/**
* 回滚最近一条已执行的迁移:跑其 down 并从历史表删记录。
* 在 migrations 里按 name 找到对应迁移对象(才能调 down);找不到则抛清晰错误。
* 历史表为空(无可回滚)时返回 None。返回被回滚的迁移名。
*/
public func revertLast(migrations: Array<Migration>): ?String {
ensureHistoryTable()
let rows = ds.driver.query("SELECT name FROM " + MIGRATION_HISTORY_TABLE + " ORDER BY id DESC LIMIT 1", [])
if (rows.size == 0) {
return None
}
let lastName = rows[0].getText("name")
let runner = QueryRunner(ds)
for (mg in migrations) {
if (mg.name() == lastName) {
let m = mg
ds.transaction({
=>
m.down(runner)
deleteRecord(lastName)
})
return Some(lastName)
}
}
throw OrmException("ACE ORM: 找不到迁移 '" + lastName + "' 的定义,无法回滚(请把对应 Migration 传入 revertLast)")
}
/** 已执行迁移名列表(按执行顺序;对标 migration:show)。 */
public func executedMigrations(): Array<String> {
ensureHistoryTable()
let rows = ds.driver.query("SELECT name FROM " + MIGRATION_HISTORY_TABLE + " ORDER BY id ASC", [])
let out = ArrayList<String>()
for (r in rows) {
out.add(r.getText("name"))
}
return out.toArray()
}
}