/* * 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.sql,DDL 映射由方言决定。 */ 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 = ArrayList() public var indexName: String = "" public var columnNames: ArrayList = 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() 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() public init() {} /// 当前累积的操作列表(供执行器/测试读取) public func getOperations(): ArrayList { 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): MigrationBuilder { createIndex(indexName, table, columns, false) } public func createIndex(indexName: String, table: String, columns: ArrayList, 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() /// 由 MigrationGenerator 注入的 down 操作(按序执行,已按 up 逆序反转) internal var presetDownOperations = ArrayList() 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_type;None 表示列不存在。 public func resolveColumnSqls(op: MigrationOperation, dialect: ISqlDialect, existingType: ?String): ArrayList { let result = ArrayList() 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() } }