feat: 新增数据填充 seed 命令(@Seed 宏自动注册,不记录运行历史)、database truncate/drop、打包只读模式,提示统一为 simcu::orm,版本 1.2.0

This commit is contained in:
2026-08-22 09:53:43 +08:00
parent 394c4a1fb5
commit 9d7b353e2f
17 changed files with 796 additions and 80 deletions
+16 -6
View File
@@ -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
}
+1 -1
View File
@@ -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
View File
@@ -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 {
+12 -9
View File
@@ -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
}