Files
orm-cj/src/migrations/Migrations.cj
T

474 lines
16 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.
* 迁移基础设施: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
}
}
/// 解析 AddColumn/AlterColumn 为可执行 SQL 列表(兼容"同名列修改数据结构"场景):
/// - 库中无同名列 → 单条 ADD COLUMN
/// - 有同名列且类型一致 → AddColumn 幂等跳过(空列表);AlterColumn 保留原 SQL(处理非空/长度等变化)
/// - 有同名列但类型不同 → 先 DROP COLUMN 再 ADD COLUMN(重建该字段)
/// existingType 来自 information_schema.data_typeNone 表示列不存在。
public func resolveColumnSqls(op: MigrationOperation, dialect: ISqlDialect,
existingType: ?String): ArrayList<String> {
let result = ArrayList<String>()
let col = op.column.getOrThrow()
match (existingType) {
case None => result.add(toSql(op, dialect))
case Some(dbType) =>
let target = dialect.columnTypeSql(col.columnType, col.autoIncrement, col.maxLength)
if (normalizeTypeName(dbType) == normalizeTypeName(target)) {
// 类型一致:AddColumn 视为幂等跳过;AlterColumn 保留原 SQL
let isAlter = match (op.kind) {
case MigrationOperationKind.AlterColumn => true
case _ => false
}
if (isAlter) {
result.add(toSql(op, dialect))
}
} else {
// 类型不同:先删除老字段,再新增新字段
let drop = MigrationOperation(MigrationOperationKind.DropColumn)
drop.tableName = op.tableName
drop.columnName = col.name
result.add(toSql(drop, dialect))
result.add(toSql(op, dialect))
}
}
result
}
/// 类型名归一化:方言 SQL 类型 ↔ information_schema.data_type,统一为小写基础类型(忽略长度/精度括号)。
public static func normalizeTypeName(t: String): String {
let lower = asciiLower(t)
let base = if (let Some(i) <- lower.indexOf("(")) { lower[0..i] } else { lower }
match (base) {
case "bigserial" | "bigint" | "int8" => "bigint"
case "serial" | "integer" | "int" | "int4" => "integer"
case "smallint" | "int2" => "smallint"
case "varchar" | "character varying" => "varchar"
case "character" | "char" | "bpchar" => "char"
case "text" => "text"
case "boolean" | "bool" => "boolean"
case "double precision" | "float8" => "double precision"
case "real" | "float4" => "real"
case "timestamp" | "timestamp without time zone" | "timestamp with time zone" => "timestamp"
case "decimal" | "numeric" => "decimal"
case "bytea" => "bytea"
case _ => base
}
}
/// ASCII 大写转小写(A-Z → a-z,其余字符不变;仅依赖切片与 indexOf,避免 std String 缺失 API
private static func asciiLower(s: String): String {
let upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
let lower = "abcdefghijklmnopqrstuvwxyz"
let sb = StringBuilder()
for (i in 0..s.size) {
let c = s[i..i + 1]
if (let Some(idx) <- upper.indexOf(c)) {
sb.append(lower[idx..idx + 1])
} else {
sb.append(c)
}
}
sb.toString()
}
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()
}
}