feat: 新增数据填充 seed 命令(@Seed 宏自动注册,不记录运行历史)、database truncate/drop、打包只读模式,提示统一为 simcu::orm,版本 1.2.0
This commit is contained in:
@@ -91,7 +91,8 @@ public class MigrationGenerator {
|
||||
}
|
||||
|
||||
/// 字段类型简单名 → 列类型(String/Bool/Int8-64/UInt8-64/Float32-64/Rune/DateTime/Duration/Decimal/Array<Byte>)
|
||||
/// Option<X> 按内层类型映射(Option 列默认 nullable,见 columnDefinition 的 isRequired/isKey 判定)
|
||||
/// Option<X> 按内层类型映射(Option 列默认 nullable,见 columnDefinition 的 isRequired/isKey 判定);
|
||||
/// 简单枚举不在此映射(由 columnDefinition 按 isEnum 标记走 TextCol)。
|
||||
public static func columnTypeFor(typeName: String): ColumnTypes {
|
||||
let effective = if (typeName.startsWith("Option<") && typeName.endsWith(">")) {
|
||||
typeName[7..typeName.size - 1]
|
||||
@@ -116,13 +117,13 @@ public class MigrationGenerator {
|
||||
case "Duration" => ColumnTypes.BigIntCol
|
||||
case "Decimal" => ColumnTypes.DecimalCol
|
||||
case "Array<Byte>" => ColumnTypes.BinaryCol
|
||||
case _ => throw Exception("simorm: 字段类型 ${typeName} 无法映射到列类型")
|
||||
case _ => throw Exception("simcu::orm: 字段类型 ${typeName} 无法映射到列类型")
|
||||
}
|
||||
}
|
||||
|
||||
/// PropertyModel → ColumnDefinition(主键/自增/非空/最大长度 随注解映射)
|
||||
public static func columnDefinition(p: PropertyModel): ColumnDefinition {
|
||||
let c = ColumnDefinition(p.columnName, columnTypeFor(p.typeName()))
|
||||
let c = ColumnDefinition(p.columnName, columnTypeOf(p))
|
||||
if (p.isKey) {
|
||||
c.primary()
|
||||
}
|
||||
@@ -138,6 +139,15 @@ public class MigrationGenerator {
|
||||
c
|
||||
}
|
||||
|
||||
/// 列类型:枚举字段(含 Option 内层枚举)按构造器名存 VARCHAR,其余按类型名映射
|
||||
private static func columnTypeOf(p: PropertyModel): ColumnTypes {
|
||||
if (p.isEnum) {
|
||||
ColumnTypes.TextCol
|
||||
} else {
|
||||
columnTypeFor(p.typeName())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 私有 ----------
|
||||
|
||||
private func diffTable(upOps: ArrayList<MigrationOperation>, downOps: ArrayList<MigrationOperation>,
|
||||
@@ -147,7 +157,7 @@ public class MigrationGenerator {
|
||||
let curKey = cur.keyProperty.getOrThrow().columnName
|
||||
if (oldKey != curKey) {
|
||||
throw Exception(
|
||||
"simorm: 表 ${cur.tableName} 主键从 ${oldKey} 变更为 ${curKey},v1 自动迁移不支持,请手写迁移")
|
||||
"simcu::orm: 表 ${cur.tableName} 主键从 ${oldKey} 变更为 ${curKey},v1 自动迁移不支持,请手写迁移")
|
||||
}
|
||||
|
||||
var oldCols = HashMap<String, PropertyModel>()
|
||||
@@ -178,7 +188,7 @@ public class MigrationGenerator {
|
||||
if (let Some(op) <- oldCols.get(p.columnName)) {
|
||||
if (op.isKey != p.isKey || op.autoIncrement != p.autoIncrement) {
|
||||
throw Exception(
|
||||
"simorm: 表 ${cur.tableName} 列 ${p.columnName} 的主键/自增属性发生变化,v1 自动迁移不支持,请手写迁移")
|
||||
"simcu::orm: 表 ${cur.tableName} 列 ${p.columnName} 的主键/自增属性发生变化,v1 自动迁移不支持,请手写迁移")
|
||||
}
|
||||
if (!sameColumn(op, p)) {
|
||||
upOps.add(alterColumnOp(cur.tableName, p))
|
||||
@@ -189,7 +199,7 @@ public class MigrationGenerator {
|
||||
}
|
||||
|
||||
private static func sameColumn(a: PropertyModel, b: PropertyModel): Bool {
|
||||
typeRank(columnTypeFor(a.typeName())) == typeRank(columnTypeFor(b.typeName())) &&
|
||||
typeRank(columnTypeOf(a)) == typeRank(columnTypeOf(b)) &&
|
||||
a.maxLength == b.maxLength &&
|
||||
a.isRequired == b.isRequired
|
||||
}
|
||||
|
||||
@@ -456,7 +456,7 @@ public class DdlFactory {
|
||||
if (let d: DateTime <- v) {
|
||||
return "'${d.toString()}'"
|
||||
}
|
||||
throw Exception("simorm: 不支持的默认值类型,请使用 String/Bool/数值/Rune/DateTime")
|
||||
throw Exception("simcu::orm: 不支持的默认值类型,请使用 String/Bool/数值/Rune/DateTime")
|
||||
}
|
||||
|
||||
private func escapeQuotes(s: String): String {
|
||||
|
||||
+105
-2
@@ -109,6 +109,109 @@ public class Migrator {
|
||||
revert(migrations, None)
|
||||
}
|
||||
|
||||
/// 清空所有业务表数据(TRUNCATE ... RESTART IDENTITY CASCADE,保留表结构与迁移历史表),
|
||||
/// 返回被清空的表数量。供 CLI `database truncate` 使用。
|
||||
public func truncateAll(): Int64 {
|
||||
let conn = _datasource.connect()
|
||||
try {
|
||||
let stmt = conn.prepareStatement(
|
||||
"SELECT table_name FROM information_schema.tables WHERE table_schema = current_schema()")
|
||||
let names = ArrayList<String>()
|
||||
try {
|
||||
let rs = stmt.query()
|
||||
while (rs.next()) {
|
||||
let n = rs.getOrNull<String>(0).getOrThrow()
|
||||
if (n != _historyTable) {
|
||||
names.add(n)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
stmt.close()
|
||||
}
|
||||
if (names.size == 0) {
|
||||
return 0
|
||||
}
|
||||
var sql = "TRUNCATE TABLE "
|
||||
for (i in 0..names.size) {
|
||||
if (i > 0) {
|
||||
sql += ", "
|
||||
}
|
||||
sql += _dialect.quoteName(names[i])
|
||||
}
|
||||
sql += " RESTART IDENTITY CASCADE"
|
||||
let ts = conn.prepareStatement(sql)
|
||||
try {
|
||||
ts.update()
|
||||
} finally {
|
||||
ts.close()
|
||||
}
|
||||
names.size
|
||||
} finally {
|
||||
conn.close()
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除数据库内所有表(含迁移历史表),完全清空数据库(库本身保留)。
|
||||
/// 返回删除的表数量。供 CLI `database drop` 使用;之后可 `update` 重建全部表。
|
||||
public func dropAllTables(): Int64 {
|
||||
let conn = _datasource.connect()
|
||||
try {
|
||||
let stmt = conn.prepareStatement(
|
||||
"SELECT table_name FROM information_schema.tables WHERE table_schema = current_schema()")
|
||||
let names = ArrayList<String>()
|
||||
try {
|
||||
let rs = stmt.query()
|
||||
while (rs.next()) {
|
||||
names.add(rs.getOrNull<String>(0).getOrThrow())
|
||||
}
|
||||
} finally {
|
||||
stmt.close()
|
||||
}
|
||||
if (names.size == 0) {
|
||||
return 0
|
||||
}
|
||||
var sql = "DROP TABLE "
|
||||
for (i in 0..names.size) {
|
||||
if (i > 0) {
|
||||
sql += ", "
|
||||
}
|
||||
sql += _dialect.quoteName(names[i])
|
||||
}
|
||||
sql += " CASCADE"
|
||||
let ts = conn.prepareStatement(sql)
|
||||
try {
|
||||
ts.update()
|
||||
} finally {
|
||||
ts.close()
|
||||
}
|
||||
names.size
|
||||
} finally {
|
||||
conn.close()
|
||||
}
|
||||
}
|
||||
|
||||
/// 迁移历史表是否已存在(用于区分“空库/全新库”与“连接失败”:历史表不存在不视为错误)。
|
||||
public func hasHistoryTable(): Bool {
|
||||
let conn = _datasource.connect()
|
||||
try {
|
||||
// 注意:PG 中双引号是标识符引用,字符串比较必须用单引号(表名为内部常量,无注入风险)
|
||||
let stmt = conn.prepareStatement(
|
||||
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = current_schema() " +
|
||||
"AND table_name = '${_historyTable}'")
|
||||
try {
|
||||
let rs = stmt.query()
|
||||
if (!rs.next()) {
|
||||
return false
|
||||
}
|
||||
rs.getOrNull<Int64>(0).getOrThrow() > 0
|
||||
} finally {
|
||||
stmt.close()
|
||||
}
|
||||
} finally {
|
||||
conn.close()
|
||||
}
|
||||
}
|
||||
|
||||
/// 回退到目标迁移(不含目标):执行目标之后全部已应用迁移的 down。
|
||||
/// target 匹配规则:精确(migrationId / description)或唯一前缀(migrationId)。
|
||||
/// 返回回退的数量;无已应用迁移或已处于目标时返回 0。
|
||||
@@ -210,9 +313,9 @@ public class Migrator {
|
||||
return prefixHits[0]
|
||||
}
|
||||
if (prefixHits.size == 0) {
|
||||
throw Exception("simorm: 未找到迁移 '${target}'(已应用的迁移中无匹配)")
|
||||
throw Exception("simcu::orm: 未找到迁移 '${target}'(已应用的迁移中无匹配)")
|
||||
}
|
||||
throw Exception("simorm: 迁移目标 '${target}' 不唯一,请使用完整 id")
|
||||
throw Exception("simcu::orm: 迁移目标 '${target}' 不唯一,请使用完整 id")
|
||||
}
|
||||
|
||||
private func removeApplied(conn: Connection, m: Migration): Unit {
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
* // 模型变更后:快照存在 → 载入旧模型 diff,并覆盖保存新快照
|
||||
* gen.ensure("20250801000000_AddAge", "新增 age 列", "snapshot.json", models1)
|
||||
*
|
||||
* 快照 JSON 结构(版本 1):
|
||||
* {"version":1,"models":[{"table":"users","columns":[
|
||||
* 快照 JSON 结构(版本 2,较 v1 新增 enum 标记):
|
||||
* {"version":2,"models":[{"table":"users","columns":[
|
||||
* {"name":"id","column":"id","type":"Int64","key":true,"auto":true,
|
||||
* "client":false,"required":false,"maxLen":0}]}]}
|
||||
* "client":false,"required":false,"maxLen":0,"enum":false}]}]}
|
||||
*/
|
||||
|
||||
package simcu::orm.migrations
|
||||
@@ -30,7 +30,7 @@ public class ModelSnapshot {
|
||||
/// 快照内的轻量模型列表
|
||||
public var models: ArrayList<EntityModel> = ArrayList()
|
||||
/// 快照格式版本
|
||||
public static let formatVersion: Int64 = 1
|
||||
public static let formatVersion: Int64 = 2
|
||||
|
||||
public init() {}
|
||||
|
||||
@@ -50,6 +50,7 @@ public class ModelSnapshot {
|
||||
lp.isRequired = p.isRequired
|
||||
lp.maxLength = p.maxLength
|
||||
lp.typeNameOverride = p.typeName()
|
||||
lp.isEnum = p.isEnum
|
||||
lm.properties.add(lp)
|
||||
}
|
||||
if (let Some(kp) <- m.keyProperty) {
|
||||
@@ -88,6 +89,7 @@ public class ModelSnapshot {
|
||||
co["client"] = JBool(p.clientGenerated)
|
||||
co["required"] = JBool(p.isRequired)
|
||||
co["maxLen"] = JNumber("${p.maxLength}")
|
||||
co["enum"] = JBool(p.isEnum)
|
||||
cols.add(JObject(co))
|
||||
}
|
||||
mo["columns"] = JArray(cols)
|
||||
@@ -123,23 +125,24 @@ public class ModelSnapshot {
|
||||
lp.clientGenerated = getBool(co, "client")
|
||||
lp.isRequired = getBool(co, "required")
|
||||
lp.maxLength = getLong(co, "maxLen")
|
||||
lp.isEnum = getBool(co, "enum")
|
||||
if (lp.isKey) {
|
||||
lm.keyProperty = Some(lp)
|
||||
}
|
||||
lm.properties.add(lp)
|
||||
case _ => throw Exception(
|
||||
"simorm: 快照 columns 项必须是 object")
|
||||
"simcu::orm: 快照 columns 项必须是 object")
|
||||
}
|
||||
}
|
||||
case _ => throw Exception("simorm: 快照 models[].columns 必须是数组")
|
||||
case _ => throw Exception("simcu::orm: 快照 models[].columns 必须是数组")
|
||||
}
|
||||
snap.models.add(lm)
|
||||
case _ => throw Exception("simorm: 快照 models[] 项必须是 object")
|
||||
case _ => throw Exception("simcu::orm: 快照 models[] 项必须是 object")
|
||||
}
|
||||
}
|
||||
case _ => throw Exception("simorm: 快照缺少 models 数组")
|
||||
case _ => throw Exception("simcu::orm: 快照缺少 models 数组")
|
||||
}
|
||||
case _ => throw Exception("simorm: 快照根必须是 object")
|
||||
case _ => throw Exception("simcu::orm: 快照根必须是 object")
|
||||
}
|
||||
snap
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user