feat: 支持 Option<T> 字段(None↔NULL)、列命名默认原样(Keep)、迁移同名列先删后加,版本 1.1.0

This commit is contained in:
2026-08-21 03:41:53 +08:00
parent 132aae865b
commit 394c4a1fb5
14 changed files with 335 additions and 87 deletions
+7 -1
View File
@@ -91,8 +91,14 @@ public class MigrationGenerator {
}
/// 字段类型简单名 → 列类型(String/Bool/Int8-64/UInt8-64/Float32-64/Rune/DateTime/Duration/Decimal/Array<Byte>
/// Option<X> 按内层类型映射(Option 列默认 nullable,见 columnDefinition 的 isRequired/isKey 判定)
public static func columnTypeFor(typeName: String): ColumnTypes {
match (typeName) {
let effective = if (typeName.startsWith("Option<") && typeName.endsWith(">")) {
typeName[7..typeName.size - 1]
} else {
typeName
}
match (effective) {
case "String" => ColumnTypes.TextCol
case "Bool" => ColumnTypes.BoolCol
case "Int8" => ColumnTypes.TinyIntCol
+71
View File
@@ -289,6 +289,77 @@ public class DdlFactory {
}
}
/// 解析 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")
+44 -5
View File
@@ -48,11 +48,30 @@ public class Migrator {
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()
let sqls = ArrayList<String>()
let isColumnOp = match (op.kind) {
case MigrationOperationKind.AddColumn |
MigrationOperationKind.AlterColumn => true
case _ => false
}
if (isColumnOp) {
// 同名列类型变更时先删后加(见 DdlFactory.resolveColumnSqls
let col = op.column.getOrThrow()
let resolved = factory.resolveColumnSqls(op, _dialect,
columnTypeInDb(conn, op.tableName, col.name))
for (s in resolved) {
sqls.add(s)
}
} else {
sqls.add(factory.toSql(op, _dialect))
}
for (sql in sqls) {
let stmt = conn.prepareStatement(sql)
try {
stmt.update()
} finally {
stmt.close()
}
}
}
recordApplied(conn, m)
@@ -153,6 +172,26 @@ public class Migrator {
}
}
/// 查询库中某列的数据类型(information_schema.data_type);列不存在返回 None。
/// 执行 addColumn/alterColumn 前调用,用于识别"同名列修改数据结构"场景。
private func columnTypeInDb(conn: Connection, table: String, column: String): ?String {
let stmt = conn.prepareStatement(
"SELECT data_type FROM information_schema.columns WHERE table_schema = current_schema() " +
"AND table_name = ? AND column_name = ?")
try {
stmt.set<String>(0, table)
stmt.set<String>(1, column)
let rs = stmt.query()
if (rs.next()) {
rs.getOrNull<String>(0)
} else {
None
}
} finally {
stmt.close()
}
}
private func resolveTarget(appliedList: ArrayList<Migration>, target: String): Int64 {
var exact: Int64 = -1
let prefixHits = ArrayList<Int64>()
+1 -1
View File
@@ -12,7 +12,7 @@
*
* 快照 JSON 结构(版本 1):
* {"version":1,"models":[{"table":"users","columns":[
* {"name":"_id","column":"id","type":"Int64","key":true,"auto":true,
* {"name":"id","column":"id","type":"Int64","key":true,"auto":true,
* "client":false,"required":false,"maxLen":0}]}]}
*/