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
+2
View File
@@ -25,4 +25,6 @@ public interface CliContext {
func getMigrations(): ArrayList<Migration>
/// 应用所有迁移
func migrateAll(migrations: ArrayList<Migration>): Int64
/// 数据填充运行上下文:返回应用自己的 DbContext 实例(seed 的 run(ctx) 参数)
func getSeedContext(): Any
}
+215 -38
View File
@@ -26,7 +26,13 @@
* update 应用所有未执行的迁移
* downgrade [目标] 回退迁移(执行 down;无目标=回退最近一个,有目标=回退到该迁移之后)
* list 列出已应用/待应用迁移
* seed 列出数据填充(@Seed 标注的类);seed <id> 运行指定;seed --all 运行全部
* database truncate 清空所有业务表数据(保留表结构与迁移历史)
* database drop 删除所有表(含迁移历史),完全清空数据库,之后 update 可重建
* help 显示用法
*
* 开发模式:在项目目录(含 cjpm.toml)通过 cjpm run 运行时开启全部命令;
* 打包部署后(工作目录无 cjpm.toml)自动只读,仅允许 list/update/seed(数据只读/填充)。
*/
package simcu::orm.cli
@@ -37,6 +43,7 @@ import std.fs.*
import std.time.*
import simcu::orm.migrations.*
import simcu::orm.model.*
import simcu::orm.seeds.*
import simcu::orm.sql.*
/**
@@ -57,15 +64,16 @@ public class MigrationCli {
public static func runOn(ctx: CliContext, args: ArrayList<String>): Int64 {
let cli = MigrationCli()
cli.dispatch(args, { => ctx.getModels() },
{ => ctx.getDatasource() }, { => ctx.getMigrations() }, ctx.getDialect())
{ => ctx.getDatasource() }, { => ctx.getMigrations() }, ctx.getDialect(),
Some(ctx.getSeedContext()))
}
/// 低层入口:无条件执行 CLI(命令:add/rm/update/downgrade/list/help)。
/// 低层入口:无条件执行 CLI(命令:add/rm/update/downgrade/list/seed/help)。
/// 应用侧推荐直接用 DbContext.cli(args) 一行接入。
public static func run(args: ArrayList<String>, models: () -> ArrayList<EntityModel>,
datasource: () -> Datasource, migrations: () -> ArrayList<Migration>): Int64 {
let cli = MigrationCli()
cli.dispatch(args, models, datasource, migrations, OpenGaussDialect())
cli.dispatch(args, models, datasource, migrations, OpenGaussDialect(), None)
}
/// 应用入口一行接入:`cjpm run -- orm <命令>` 时执行 CLI 并返回 true;
@@ -78,7 +86,7 @@ public class MigrationCli {
return false
}
let cli = MigrationCli()
cli.dispatch(args[1..], models, datasource, migrations, OpenGaussDialect())
cli.dispatch(args[1..], models, datasource, migrations, OpenGaussDialect(), None)
true
}
@@ -86,39 +94,91 @@ public class MigrationCli {
private func dispatch(args: ArrayList<String>, models: () -> ArrayList<EntityModel>,
datasource: () -> Datasource, migrations: () -> ArrayList<Migration>,
dialect: ISqlDialect): Int64 {
dialect: ISqlDialect, seedCtx: ?Any): Int64 {
if (args.size == 0) {
printHelp()
return 0
}
// 开发模式(cjpm run 运行)允许全部迁移命令;打包部署后仅允许 list/update/seed(数据只读/填充)
let allowedInProd = args[0] == "list" || args[0] == "update" || args[0] == "seed" || args[0] == "help" || args[0] == "-h" || args[0] == "--help"
if (!isDevMode() && !allowedInProd) {
println("simcu::orm cli: 打包部署环境仅支持 list/update/seed 命令(迁移管理需在项目目录通过 cjpm run 运行)")
return 1
}
match (args[0]) {
case "add" => cmdAdd(args, models)
case "rm" => cmdRm(args)
case "update" => cmdUpdate(datasource, migrations, dialect)
case "downgrade" => cmdDowngrade(datasource, migrations, dialect, args)
case "list" => cmdList(datasource, migrations, dialect)
case "seed" => cmdSeed(datasource, migrations, dialect, args, seedCtx)
case "database" => cmdDatabase(args, datasource, dialect)
case "help" | "-h" | "--help" => printHelp(); 0
case _ =>
println("simorm: 未知命令 '${args[0]}',输入 'simorm help' 查看用法")
println("simcu::orm: 未知命令 '${args[0]}',输入 'simcu::orm help' 查看用法")
1
}
}
/// database <子命令>:数据库级操作(不需要迁移注册)
private func cmdDatabase(args: ArrayList<String>, datasource: () -> Datasource,
dialect: ISqlDialect): Int64 {
if (args.size < 2) {
println("用法: simcu::orm database <truncate|drop>")
return 1
}
match (args[1]) {
case "truncate" => cmdTruncate(datasource, dialect)
case "drop" => cmdDrop(datasource, dialect)
case _ =>
println("simcu::orm: 未知 database 子命令 '${args[1]}'(支持: truncate, drop)")
1
}
}
/// database truncate:清空所有业务表数据(保留表结构与迁移历史)
private func cmdTruncate(datasource: () -> Datasource, dialect: ISqlDialect): Int64 {
try {
let migrator = Migrator(datasource(), dialect)
let n = migrator.truncateAll()
println("simcu::orm: 已清空 ${n} 张业务表(表结构与迁移历史保留)")
0
} catch (e: Exception) {
println("simcu::orm: 清空数据库失败")
println(" ${e.message}")
1
}
}
/// database drop:删除所有表(含迁移历史),完全清空数据库;之后可 update 重建
private func cmdDrop(datasource: () -> Datasource, dialect: ISqlDialect): Int64 {
try {
let migrator = Migrator(datasource(), dialect)
let n = migrator.dropAllTables()
println("simcu::orm: 已删除 ${n} 张表,数据库已完全清空(执行 update 可重建全部表)")
0
} catch (e: Exception) {
println("simcu::orm: 清空数据库失败")
println(" ${e.message}")
1
}
}
/// add <迁移名>:模型 → 迁移文件 + 快照 + 注册
private func cmdAdd(args: ArrayList<String>, models: () -> ArrayList<EntityModel>): Int64 {
if (args.size < 2) {
println("用法: simorm add <迁移名>")
println("用法: simcu::orm add <迁移名>")
return 1
}
let name = args[1]
if (!isValidIdentifier(name)) {
println("simorm: 迁移名 '${name}' 不是合法标识符(字母/数字/下划线,首字符不能是数字)")
println("simcu::orm: 迁移名 '${name}' 不是合法标识符(字母/数字/下划线,首字符不能是数字)")
return 1
}
// 迁移名以 test 结尾会生成 *_test.cj 文件,cjc 会把 *_test.cj 当作测试文件,
// 在 cjpm build(非 test)时排除,导致注册文件引用不到该类而编译失败。
if (name.endsWith("test")) {
println("simorm: 迁移名 '${name}' 以 test 结尾,生成的 *_test.cj 会被编译器当作测试文件排除;请换一个名称")
println("simcu::orm: 迁移名 '${name}' 以 test 结尾,生成的 *_test.cj 会被编译器当作测试文件排除;请换一个名称")
return 1
}
let appPkg = detectPackage()
@@ -136,7 +196,7 @@ public class MigrationCli {
let probe = MigrationBuilder()
m.up(probe)
if (probe.getOperations().size == 0) {
println("simorm: 未检测到模型变化,快照已是最新,跳过生成")
println("simcu::orm: 未检测到模型变化,快照已是最新,跳过生成")
return 0
}
// 迁移文件与注册文件都落在 src/migrations 子目录(package = <appPkg>.migrations):
@@ -147,11 +207,11 @@ public class MigrationCli {
let path = fg.writeMigration(migPkg, m, _migrationsDir)
fg.updateRegistry(migPkg, _registryPath, MigrationFileGenerator.classNameOf(id))
ModelSnapshot.capture(current).save(_snapshotPath)
println("simorm: 已生成迁移 ${id}")
println("simcu::orm: 已生成迁移 ${id}")
println(" 迁移文件: ${path}")
println(" 注册文件: ${_registryPath}")
println(" 快照: ${_snapshotPath}")
println("执行 'simorm update' 应用该迁移")
println("执行 'simcu::orm update' 应用该迁移")
0
}
@@ -160,12 +220,12 @@ public class MigrationCli {
migrations: () -> ArrayList<Migration>, dialect: ISqlDialect): Int64 {
let list = migrations()
if (list.size == 0) {
println("simorm: 未注册任何迁移(检查 ${_registryPath})")
println("simcu::orm: 未注册任何迁移(检查 ${_registryPath})")
return 1
}
let migrator = Migrator(datasource(), dialect)
let n = migrator.migrate(list)
println("simorm: 已应用 ${n} 个迁移")
println("simcu::orm: 已应用 ${n} 个迁移")
0
}
@@ -174,7 +234,7 @@ public class MigrationCli {
dialect: ISqlDialect, args: ArrayList<String>): Int64 {
let list = migrations()
if (list.size == 0) {
println("simorm: 未注册任何迁移(检查 ${_registryPath})")
println("simcu::orm: 未注册任何迁移(检查 ${_registryPath})")
return 1
}
let target: ?String = if (args.size >= 2) { Some(args[1]) } else { None }
@@ -182,13 +242,13 @@ public class MigrationCli {
let migrator = Migrator(datasource(), dialect)
let n = migrator.revert(list, target)
if (n == 0) {
println("simorm: 没有可回退的迁移(已处于${if (target.isSome()) { "目标" } else { "最初" }}状态)")
println("simcu::orm: 没有可回退的迁移(已处于${if (target.isSome()) { "目标" } else { "最初" }}状态)")
} else {
println("simorm: 已回退 ${n} 个迁移")
println("simcu::orm: 已回退 ${n} 个迁移")
}
0
} catch (e: Exception) {
println("simorm: 回退迁移失败")
println("simcu::orm: 回退迁移失败")
println(" ${e.message}")
1
}
@@ -197,13 +257,13 @@ public class MigrationCli {
/// rm <迁移名或id>:移除已生成的迁移(删除迁移文件 + 注册条目 + 快照回退)
private func cmdRm(args: ArrayList<String>): Int64 {
if (args.size < 2) {
println("用法: simorm rm <迁移名或id>")
println("用法: simcu::orm rm <迁移名或id>")
return 1
}
let target = args[1]
let dir = Path(_migrationsDir)
if (!exists(dir)) {
println("simorm: 迁移目录不存在(${_migrationsDir}),没有可移除的迁移")
println("simcu::orm: 迁移目录不存在(${_migrationsDir}),没有可移除的迁移")
return 1
}
// 扫描迁移目录,收集迁移 id(文件名去 .cj;排除注册文件/占位文件)
@@ -217,7 +277,7 @@ public class MigrationCli {
}
}
if (ids.size == 0) {
println("simorm: ${_migrationsDir} 下没有迁移文件")
println("simcu::orm: ${_migrationsDir} 下没有迁移文件")
return 1
}
// 匹配:精确(migrationId 或 类名)+ 唯一前缀(migrationId)
@@ -262,47 +322,147 @@ public class MigrationCli {
if (exists(sp)) {
remove(sp)
}
println("simorm: 已移除迁移 ${id}(注册表已空,快照已重置)")
println("simcu::orm: 已移除迁移 ${id}(注册表已空,快照已重置)")
} else {
println("simorm: 已移除迁移 ${id}")
println("simcu::orm: 已移除迁移 ${id}")
}
return 0
}
println("simorm: 未找到迁移 '${target}'(${_migrationsDir} 下无匹配)")
println("simcu::orm: 未找到迁移 '${target}'(${_migrationsDir} 下无匹配)")
1
}
/// list:显示已应用/待应用迁移
/// list:显示已应用/待应用迁移(空库/全新库时全部显示为待应用)
private func cmdList(datasource: () -> Datasource, migrations: () -> ArrayList<Migration>,
dialect: ISqlDialect): Int64 {
let list = migrations()
if (list.size == 0) {
println("simorm: 未注册任何迁移(检查 ${_registryPath})")
println("simcu::orm: 未注册任何迁移(检查 ${_registryPath})")
return 1
}
try {
let migrator = Migrator(datasource(), dialect)
let applied = migrator.appliedMigrationIds()
println("simorm: 迁移列表(共 ${list.size} 个)")
let applied = if (migrator.hasHistoryTable()) {
migrator.appliedMigrationIds()
} else {
println("simcu::orm: 数据库为空库(尚未应用任何迁移),全部迁移均待应用")
ArrayList<String>()
}
println("simcu::orm: 迁移列表(共 ${list.size} 个)")
for (m in list) {
let mark = if (applied.contains(m.migrationId)) { "[已应用]" } else { "[待应用]" }
println(" ${mark} ${m.migrationId} ${m.description}")
}
0
} catch (e: Exception) {
println("simorm: 读取迁移历史失败(数据库未创建?)")
println("simcu::orm: 数据库连接失败,无法读取迁移历史")
println(" 请检查数据库配置(项目目录 appsettings.json 的 ConnectionStrings:Pgsql,")
println(" 或环境变量 ORM_CONNECTION_STRING)")
println(" ${e.message}")
1
}
}
/// seed:列出/运行数据填充(@Seed 标注的类,按 seedId 唯一前缀匹配;--all 全部;无参=列出)
private func cmdSeed(datasource: () -> Datasource, migrations: () -> ArrayList<Migration>,
dialect: ISqlDialect, args: ArrayList<String>, seedCtx: ?Any): Int64 {
let list = allSeeds()
if (list.size == 0) {
println("simcu::orm: 未注册任何填充(检查 @Seed 标注的类是否存在,并确认应用已 import 对应包)")
return 1
}
if (args.size < 2 || args[1] == "list") {
println("simcu::orm: 填充列表(共 ${list.size} 个)")
for (s in list) {
println(" ${s.seedId} ${s.description}")
}
println("运行: simcu::orm seed <id> 运行指定填充; simcu::orm seed --all 运行全部")
return 0
}
// 运行前检查:迁移必须全部已应用(否则填充的表可能不存在)
try {
let migrator = Migrator(datasource(), dialect)
if (!migrator.hasHistoryTable()) {
println("simcu::orm: 数据库为空库,请先执行 'simcu::orm update' 应用迁移后再运行填充")
return 1
}
let applied = migrator.appliedMigrationIds()
var pending = 0
for (m in migrations()) {
if (!applied.contains(m.migrationId)) {
pending += 1
}
}
if (pending > 0) {
println("simcu::orm: 有 ${pending} 个迁移未应用,请先执行 'simcu::orm update' 再运行填充")
return 1
}
} catch (e: Exception) {
println("simcu::orm: 检查迁移状态失败(${e.message})")
return 1
}
if (args[1] == "--all") {
var failed = false
for (s in list) {
try {
runSeed(s, seedCtx)
println("simcu::orm: 已执行 ${s.seedId} ${s.description}")
} catch (e: Exception) {
println("simcu::orm: 执行 ${s.seedId} 失败: ${e.message}")
failed = true
}
}
if (failed) {
println("simcu::orm: 存在执行失败的填充,请检查上方错误")
return 1
}
println("simcu::orm: 全部 ${list.size} 个填充执行完成")
return 0
}
// 指定 id:精确(seedId)或唯一前缀
let target = args[1]
var hit: ?Seed = None
let prefixHits = ArrayList<Seed>()
for (s in list) {
if (s.seedId == target) {
hit = Some(s)
} else if (s.seedId.startsWith(target)) {
prefixHits.add(s)
}
}
if (hit.isNone() && prefixHits.size == 1) {
hit = Some(prefixHits[0])
}
if (let Some(s) <- hit) {
try {
runSeed(s, seedCtx)
println("simcu::orm: 已执行 ${s.seedId} ${s.description}")
0
} catch (e: Exception) {
println("simcu::orm: 执行 ${s.seedId} 失败: ${e.message}")
1
}
} else {
println("simcu::orm: 未找到填充 '${target}'(共 ${list.size} 个可用)")
1
}
}
/// 运行单个填充:ctx 为应用自己的 DbContext 实例(CLI 入口自带)
private func runSeed(seed: Seed, seedCtx: ?Any): Unit {
if (seedCtx.isNone()) {
throw Exception("simcu::orm: seed 需要应用 DbContext 上下文(请通过 DbContext.cli 入口运行)")
}
seed.run(seedCtx.getOrThrow())
}
// ---------- 私有工具 ----------
/// 从应用根目录 cjpm.toml 推断包名(organization::name 或 name)
private func detectPackage(): String {
let p = Path("cjpm.toml")
if (!exists(p)) {
throw Exception("simorm: 未找到 cjpm.toml,请在应用根目录运行 add 命令")
throw Exception("simcu::orm: 未找到 cjpm.toml,请在应用根目录运行 add 命令")
}
let text = String.fromUtf8(File.readFrom(p))
var name = ""
@@ -317,7 +477,7 @@ public class MigrationCli {
}
}
if (name.isEmpty()) {
throw Exception("simorm: cjpm.toml 缺少 [package] name 字段")
throw Exception("simcu::orm: cjpm.toml 缺少 [package] name 字段")
}
if (org.isEmpty()) {
return name
@@ -388,14 +548,31 @@ public class MigrationCli {
}
private static func printHelp(): Unit {
println("simorm - orm-cj 迁移 CLI(对齐 dotnet ef)")
println("simcu::orm cli - 数据库迁移管理程序")
println("")
if (!isDevMode()) {
println("当前为打包部署环境(未通过 cjpm run 运行),仅支持:")
println(" list 列出已应用/待应用迁移")
println(" update 应用所有未执行的迁移(将数据库升级到最新)")
println(" seed 列出/运行数据填充(seed <id> 运行指定;seed --all 运行全部)")
println(" help 显示本帮助")
return
}
println("用法:")
println(" <app> add <迁移名> 从模型生成迁移 .cj 文件,更新快照与注册(无变化则跳过)")
println(" <app> rm <迁移名或id> 移除已生成的迁移(删除迁移文件 + 注册条目 + 快照回退)")
println(" <app> update 应用所有未执行的迁移")
println(" <app> downgrade [目标] 回退迁移:无目标=回退最近一个;有目标=回退到该迁移之后")
println(" <app> list 列出已应用/待应用迁移")
println(" <app> help 显示本帮助")
println(" add <迁移名> 从模型生成迁移 .cj 文件,更新快照与注册(无变化则跳过)")
println(" rm <迁移名或id> 移除已生成的迁移(删除迁移文件 + 注册条目 + 快照回退)")
println(" update 应用所有未执行的迁移")
println(" downgrade [目标] 回退迁移:无目标=回退最近一个;有目标=回退到该迁移之后")
println(" list 列出已应用/待应用迁移")
println(" seed 列出数据填充;seed <id> 运行指定;seed --all 运行全部")
println(" database truncate 清空所有业务表数据(保留表结构与迁移历史)")
println(" database drop 删除所有表(含迁移历史),完全清空数据库,之后 update 可重建")
println(" help 显示本帮助")
}
/// 开发模式(通过 cjpm run 运行):当前工作目录存在 cjpm.toml。
/// 打包部署后 cjpm.toml 不在工作目录,自动进入只读模式(仅 list/update/seed)。
private static func isDevMode(): Bool {
exists(Path("cjpm.toml"))
}
}
+5 -5
View File
@@ -5,7 +5,7 @@
* - 自动维护迁移注册文件 MigrationRegistry.cj(add 时插入新迁移实例)。
*
* 生成文件的格式:
* // 此文件由 simorm CLI 自动生成,请勿手动编辑
* // 此文件由 simcu::orm CLI 自动生成,请勿手动编辑
* package app
*
* import simcu::orm.migrations.*
@@ -58,10 +58,10 @@ public class MigrationFileGenerator {
/// 迁移 → .cj 源码文本(不落盘,供测试/预览)
public static func migrationSource(appPackage: String, className: String, m: Migration): String {
if (appPackage.isEmpty()) {
throw Exception("simorm: 应用包名不能为空,请在应用根目录(含 cjpm.toml)运行")
throw Exception("simcu::orm: 应用包名不能为空,请在应用根目录(含 cjpm.toml)运行")
}
let sb = StringBuilder()
sb.append("// 此文件由 simorm CLI 自动生成,请勿手动编辑\n")
sb.append("// 此文件由 simcu::orm CLI 自动生成,请勿手动编辑\n")
sb.append("package ${appPackage}\n\n")
sb.append("import std.collection.*\n")
sb.append("import simcu::orm.migrations.*\n")
@@ -102,7 +102,7 @@ public class MigrationFileGenerator {
/// 返回注册文件源码文本
public static func registrySource(migrationPackage: String, entries: ArrayList<String>): String {
let sb = StringBuilder()
sb.append("// 此文件由 simorm CLI 自动维护,请勿手动编辑\n")
sb.append("// 此文件由 simcu::orm CLI 自动维护,请勿手动编辑\n")
sb.append("// 与迁移类同包(${migrationPackage}),模块加载时顶层 let 自动注册\n")
sb.append("// 迁移到 simcu::orm 全局注册表(按 migrationId 字典序应用)。\n")
sb.append("package ${migrationPackage}\n\n")
@@ -249,7 +249,7 @@ public class MigrationFileGenerator {
}
if (let Some(_) <- c.defaultValue) {
throw Exception(
"simorm: CLI 生成的迁移不支持带默认值的列,请改用手写迁移(override up/down)")
"simcu::orm: CLI 生成的迁移不支持带默认值的列,请改用手写迁移(override up/down)")
}
s
}