feat: 支持 Option<T> 字段(None↔NULL)、列命名默认原样(Keep)、迁移同名列先删后加,版本 1.1.0
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -289,6 +289,77 @@ public class DdlFactory {
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析 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<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")
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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}]}]}
|
||||
*/
|
||||
|
||||
|
||||
Reference in New Issue
Block a user