feat: orm-cj 仓颉版 EF Core 风格 ORM 初始版本

- 基于数据模型(POCO + 注解)的映射与增删改查:DbContext/DbSet/ChangeTracker/QueryBuilder
- ISqlDialect 方言接口 + PostgreSQL/openGauss 实现,零外部依赖
- @DbContext 宏:纯声明 DbSet 类自动展开为完整 DbContext 子类
- 迁移:Migration/Migrator/ModelSnapshot(快照 JSON)/MiniJson,从模型生成迁移
- 迁移 CLI:add/rm/update/downgrade/list/help,应用内嵌一行 db.cli(args) 接入
- DbContext 便捷方法:databaseExists/hasPendingMigrations/migrate
- 57 个单元测试全部通过
This commit is contained in:
2026-08-19 09:14:01 +08:00
commit afa6096ed2
28 changed files with 5822 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* CLI 上下文契约:MigrationCli 只依赖本接口,不依赖 simcu::orm.db,
* 从而避免 db → cli 与 cli → db 的包循环依赖。
*
* DbContext 实现本接口(DbContext.cli(args) 内部把 this 传给 MigrationCli.runOn)。
*/
package simcu::orm.cli
import std.collection.*
import std.database.sql.*
import simcu::orm.migrations.*
import simcu::orm.model.*
import simcu::orm.sql.*
public interface CliContext {
/// 模型列表(由实例的 DbSet<T> 属性反射收集)
func getModels(): ArrayList<EntityModel>
/// 应用主数据源
func getDatasource(): Datasource
/// SQL 方言
func getDialect(): ISqlDialect
/// 迁移列表(子类 override migrations() 提供)
func getMigrations(): ArrayList<Migration>
/// 应用所有迁移
func migrateAll(migrations: ArrayList<Migration>): Int64
}
+401
View File
@@ -0,0 +1,401 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 迁移 CLI(应用内驱动,对齐 dotnet ef 的命令体验)。
*
* 基于 DbContext 实例的一行接入(无需宏、无需回调):
* main(args: Array<String>) {
* let db = DataContext("pgsql", "Host=..;Database=..;Username=..;Password=..")
* if (db.cli(args)) { return }
* // 正常启动
* }
*
* 数据来源全部来自 DbContext 实例:
* - 模型:反射收集实例的 DbSet<T> 属性(getModels())
* - 连接/驱动/方言:构造参数
* - 迁移列表:子类 override migrations()(宏默认返回 orm-cj 全局注册表,
* CLI 生成的 src/migrations/MigrationRegistry.cj 在模块加载时自动注册)
*
* 注意:应用侧(DataContext.cj 或 main.cj)需要一行
* import <应用包名>.migrations.*
* 仅 import 即可触发 migrations 子包加载,注册文件(子包内)的顶层 let
* 才会在 main() 前执行并注册迁移;新项目需先建 src/migrations/ 目录(空包)。
*
* 命令:
* add <迁移名> 模型 → 迁移 .cj 文件(落盘) + 更新快照 + 更新迁移注册文件
* rm <迁移名/id> 移除已生成的迁移(删除迁移文件 + 注册条目 + 快照回退到前一状态)
* update 应用所有未执行的迁移
* downgrade [目标] 回退迁移(执行 down;无目标=回退最近一个,有目标=回退到该迁移之后)
* list 列出已应用/待应用迁移
* help 显示用法
*/
package simcu::orm.cli
import std.collection.*
import std.database.sql.*
import std.fs.*
import std.time.*
import simcu::orm.migrations.*
import simcu::orm.model.*
import simcu::orm.sql.*
/**
* 迁移 CLI 入口。
*/
public class MigrationCli {
private var _migrationsDir = "src/migrations"
private var _snapshotPath = "src/migrations/snapshot.json"
// 注册文件与迁移文件同目录(src/migrations/,package = 应用包名.migrations)。
// 子包默认惰性加载,顶层 let 不会执行;应用侧需一行
// `import <应用包名>.migrations.*` 触发加载后,注册才生效。
private var _registryPath = "src/migrations/MigrationRegistry.cj"
public init() {}
/// 基于 CliContext 实例执行 CLI(DbContext.cli(args) 的内部实现)。
/// 模型/连接/方言/迁移全部来自 ctx。
public static func runOn(ctx: CliContext, args: ArrayList<String>): Int64 {
let cli = MigrationCli()
cli.dispatch(args, { => ctx.getModels() },
{ => ctx.getDatasource() }, { => ctx.getMigrations() }, ctx.getDialect())
}
/// 低层入口:无条件执行 CLI(命令:add/rm/update/downgrade/list/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())
}
/// 应用入口一行接入:`cjpm run -- orm <命令>` 时执行 CLI 并返回 true;
/// 否则(正常启动)返回 false。用法:main() 最上方一行,如
/// if (MigrationCli.tryRun(ArrayList<String>(args), { => models() },
/// { => datasource() }, { => migrations()})) { return }
public static func tryRun(args: ArrayList<String>, models: () -> ArrayList<EntityModel>,
datasource: () -> Datasource, migrations: () -> ArrayList<Migration>): Bool {
if (args.size == 0 || args[0] != "orm") {
return false
}
let cli = MigrationCli()
cli.dispatch(args[1..], models, datasource, migrations, OpenGaussDialect())
true
}
// ---------- 命令分发 ----------
private func dispatch(args: ArrayList<String>, models: () -> ArrayList<EntityModel>,
datasource: () -> Datasource, migrations: () -> ArrayList<Migration>,
dialect: ISqlDialect): Int64 {
if (args.size == 0) {
printHelp()
return 0
}
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 "help" | "-h" | "--help" => printHelp(); 0
case _ =>
println("simorm: 未知命令 '${args[0]}',输入 'simorm help' 查看用法")
1
}
}
/// add <迁移名>:模型 → 迁移文件 + 快照 + 注册
private func cmdAdd(args: ArrayList<String>, models: () -> ArrayList<EntityModel>): Int64 {
if (args.size < 2) {
println("用法: simorm add <迁移名>")
return 1
}
let name = args[1]
if (!isValidIdentifier(name)) {
println("simorm: 迁移名 '${name}' 不是合法标识符(字母/数字/下划线,首字符不能是数字)")
return 1
}
// 迁移名以 test 结尾会生成 *_test.cj 文件,cjc 会把 *_test.cj 当作测试文件,
// 在 cjpm build(非 test)时排除,导致注册文件引用不到该类而编译失败。
if (name.endsWith("test")) {
println("simorm: 迁移名 '${name}' 以 test 结尾,生成的 *_test.cj 会被编译器当作测试文件排除;请换一个名称")
return 1
}
let appPkg = detectPackage()
let ts = DateTime.now().format("yyyyMMddHHmmss")
let id = "${ts}_${name}"
let gen = MigrationGenerator()
let current = models()
var m: Migration
if (let Some(snap) <- ModelSnapshot.load(_snapshotPath)) {
m = gen.diff(id, name, snap.toModels(), current)
} else {
m = gen.initial(id, name, current)
}
// 无变化检测(对齐 dotnet ef:模型未变则不生成)
let probe = MigrationBuilder()
m.up(probe)
if (probe.getOperations().size == 0) {
println("simorm: 未检测到模型变化,快照已是最新,跳过生成")
return 0
}
// 迁移文件与注册文件都落在 src/migrations 子目录(package = <appPkg>.migrations):
// 注册文件在子包内,顶层 let 引用同包迁移类,无需 import;
// 应用侧一行 `import <appPkg>.migrations.*` 触发子包加载后注册生效。
let migPkg = "${appPkg}.migrations"
let fg = MigrationFileGenerator()
let path = fg.writeMigration(migPkg, m, _migrationsDir)
fg.updateRegistry(migPkg, _registryPath, MigrationFileGenerator.classNameOf(id))
ModelSnapshot.capture(current).save(_snapshotPath)
println("simorm: 已生成迁移 ${id}")
println(" 迁移文件: ${path}")
println(" 注册文件: ${_registryPath}")
println(" 快照: ${_snapshotPath}")
println("执行 'simorm update' 应用该迁移")
0
}
/// update:应用所有未执行的迁移
private func cmdUpdate(datasource: () -> Datasource,
migrations: () -> ArrayList<Migration>, dialect: ISqlDialect): Int64 {
let list = migrations()
if (list.size == 0) {
println("simorm: 未注册任何迁移(检查 ${_registryPath})")
return 1
}
let migrator = Migrator(datasource(), dialect)
let n = migrator.migrate(list)
println("simorm: 已应用 ${n} 个迁移")
0
}
/// downgrade [目标]:回退迁移(执行 down + 删除历史记录;无目标 = 回退最近一个)
private func cmdDowngrade(datasource: () -> Datasource, migrations: () -> ArrayList<Migration>,
dialect: ISqlDialect, args: ArrayList<String>): Int64 {
let list = migrations()
if (list.size == 0) {
println("simorm: 未注册任何迁移(检查 ${_registryPath})")
return 1
}
let target: ?String = if (args.size >= 2) { Some(args[1]) } else { None }
try {
let migrator = Migrator(datasource(), dialect)
let n = migrator.revert(list, target)
if (n == 0) {
println("simorm: 没有可回退的迁移(已处于${if (target.isSome()) { "目标" } else { "最初" }}状态)")
} else {
println("simorm: 已回退 ${n} 个迁移")
}
0
} catch (e: Exception) {
println("simorm: 回退迁移失败")
println(" ${e.message}")
1
}
}
/// rm <迁移名或id>:移除已生成的迁移(删除迁移文件 + 注册条目 + 快照回退)
private func cmdRm(args: ArrayList<String>): Int64 {
if (args.size < 2) {
println("用法: simorm rm <迁移名或id>")
return 1
}
let target = args[1]
let dir = Path(_migrationsDir)
if (!exists(dir)) {
println("simorm: 迁移目录不存在(${_migrationsDir}),没有可移除的迁移")
return 1
}
// 扫描迁移目录,收集迁移 id(文件名去 .cj;排除注册文件/占位文件)
let ids = ArrayList<String>()
for (f in Directory.readFrom(dir)) {
if (f.isRegular()) {
let name = f.name
if (name.endsWith(".cj") && name != "MigrationRegistry.cj" && name != "_placeholder.cj") {
ids.add(name[..name.size - 3])
}
}
}
if (ids.size == 0) {
println("simorm: ${_migrationsDir} 下没有迁移文件")
return 1
}
// 匹配:精确(migrationId 或 类名)+ 唯一前缀(migrationId)
var hit: ?String = None
let prefixHits = ArrayList<String>()
for (id in ids) {
if (id == target || MigrationFileGenerator.classNameOf(id) == target) {
hit = Some(id)
} else if (id.startsWith(target)) {
prefixHits.add(id)
}
}
if (hit.isNone() && prefixHits.size == 1) {
hit = Some(prefixHits[0])
}
if (let Some(id) <- hit) {
// 1) 删除迁移文件
let fp = dir.join(MigrationFileGenerator.fileNameOf(id))
if (exists(fp)) {
remove(fp)
}
// 2) 从注册文件移除条目(重写注册文件)
let appPkg = detectPackage()
let migPkg = "${appPkg}.migrations"
let remaining = ArrayList<String>()
if (exists(Path(_registryPath))) {
let text = String.fromUtf8(File.readFrom(Path(_registryPath)))
for (e in MigrationFileGenerator.extractRegistryEntries(text)) {
if (e != MigrationFileGenerator.classNameOf(id)) {
remaining.add(e)
}
}
let code = MigrationFileGenerator.registrySource(migPkg, remaining)
remove(Path(_registryPath))
let f = File.create(Path(_registryPath))
f.write(code.toArray())
f.close()
}
// 3) 快照:注册表为空则重置快照(下次 add 从模型重新生成 initial)
if (remaining.size == 0) {
let sp = Path(_snapshotPath)
if (exists(sp)) {
remove(sp)
}
println("simorm: 已移除迁移 ${id}(注册表已空,快照已重置)")
} else {
println("simorm: 已移除迁移 ${id}")
}
return 0
}
println("simorm: 未找到迁移 '${target}'(${_migrationsDir} 下无匹配)")
1
}
/// list:显示已应用/待应用迁移
private func cmdList(datasource: () -> Datasource, migrations: () -> ArrayList<Migration>,
dialect: ISqlDialect): Int64 {
let list = migrations()
if (list.size == 0) {
println("simorm: 未注册任何迁移(检查 ${_registryPath})")
return 1
}
try {
let migrator = Migrator(datasource(), dialect)
let applied = migrator.appliedMigrationIds()
println("simorm: 迁移列表(共 ${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(" ${e.message}")
1
}
}
// ---------- 私有工具 ----------
/// 从应用根目录 cjpm.toml 推断包名(organization::name 或 name)
private func detectPackage(): String {
let p = Path("cjpm.toml")
if (!exists(p)) {
throw Exception("simorm: 未找到 cjpm.toml,请在应用根目录运行 add 命令")
}
let text = String.fromUtf8(File.readFrom(p))
var name = ""
var org = ""
for (raw in text.split("\n")) {
let line = trimSpaces(raw)
if (name.isEmpty()) {
name = tomlValue(line, "name")
}
if (org.isEmpty()) {
org = tomlValue(line, "organization")
}
}
if (name.isEmpty()) {
throw Exception("simorm: cjpm.toml 缺少 [package] name 字段")
}
if (org.isEmpty()) {
return name
}
"${org}::${name}"
}
/// 解析 TOML 简单值(name = "xxx"),行需已 trim;返回引号内字符串,失败返回 ""
private static func tomlValue(line: String, key: String): String {
if (!line.startsWith("${key} =")) {
return ""
}
let eq = line.indexOf("=").getOrThrow()
var rest = trimSpaces(line[eq + 1..])
if (rest.isEmpty() || rest[0] != UInt8(0x22)) {
return ""
}
var i: Int64 = 1
let bytes = rest.toArray()
while (i < bytes.size) {
if (bytes[i] == 0x22) {
return rest[1..i]
}
i += 1
}
""
}
/// 去除字符串首尾空白(空格/Tab/CR/LF)
private static func trimSpaces(s: String): String {
let bytes = s.toArray()
var start: Int64 = 0
var end = bytes.size
while (start < end && isSpace(bytes[start])) {
start += 1
}
while (end > start && isSpace(bytes[end - 1])) {
end -= 1
}
s[start..end]
}
private static func isSpace(b: UInt8): Bool {
b == 0x20 || b == 0x09 || b == 0x0D || b == 0x0A
}
/// 合法标识符:字母/数字/下划线,首字符不能是数字
private static func isValidIdentifier(s: String): Bool {
if (s.isEmpty()) {
return false
}
let bytes = s.toArray()
var i: Int64 = 0
while (i < bytes.size) {
let b = bytes[i]
let isDigit = b >= 0x30 && b <= 0x39
let isLetter = (b >= 0x41 && b <= 0x5A) || (b >= 0x61 && b <= 0x7A)
let isUnderscore = b == 0x5F
if (i == 0 && isDigit) {
return false
}
if (!isDigit && !isLetter && !isUnderscore) {
return false
}
i += 1
}
true
}
private static func printHelp(): Unit {
println("simorm - orm-cj 迁移 CLI(对齐 dotnet ef)")
println("")
println("用法:")
println(" <app> add <迁移名> 从模型生成迁移 .cj 文件,更新快照与注册(无变化则跳过)")
println(" <app> rm <迁移名或id> 移除已生成的迁移(删除迁移文件 + 注册条目 + 快照回退)")
println(" <app> update 应用所有未执行的迁移")
println(" <app> downgrade [目标] 回退迁移:无目标=回退最近一个;有目标=回退到该迁移之后")
println(" <app> list 列出已应用/待应用迁移")
println(" <app> help 显示本帮助")
}
}
+353
View File
@@ -0,0 +1,353 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 迁移落盘生成器(对齐 dotnet ef migrations add):
* - Migration 对象 → .cj 源文件文本(迁移类 up/down 用 MigrationBuilder 声明式重建),
* - 自动维护迁移注册文件 MigrationRegistry.cj(add 时插入新迁移实例)。
*
* 生成文件的格式:
* // 此文件由 simorm CLI 自动生成,请勿手动编辑
* package app
*
* import simcu::orm.migrations.*
* import simcu::orm.sql.*
*
* public class InitialCreate <: Migration {
* public init() { super("20250701000000_InitialCreate", "初始建表") }
* public override func up(builder: MigrationBuilder): Unit {
* builder.createTable("users") { tb =>
* tb.column("id", ColumnTypes.BigIntCol).primary().autoInc().notNull()
* }
* }
* public override func down(builder: MigrationBuilder): Unit {
* builder.dropTable("users")
* }
* }
*/
package simcu::orm.cli
import std.collection.*
import std.fs.*
import simcu::orm.migrations.*
import simcu::orm.sql.*
/**
* 迁移源码生成器:操作列表 → Cangjie 源码文本 + 注册文件维护。
*/
public class MigrationFileGenerator {
public init() {}
/// 迁移 → .cj 源文件写入 outDir,返回写入的完整文件路径
public func writeMigration(appPackage: String, m: Migration, outDir: String): String {
let className = classNameOf(m.migrationId)
let code = migrationSource(appPackage, className, m)
let dir = Path(outDir)
if (!exists(dir)) {
Directory.create(dir, recursive: true)
}
let path = dir.join(fileNameOf(m.migrationId))
if (exists(path)) {
remove(path)
}
let f = File.create(path)
f.write(code.toArray())
f.close()
path.toString()
}
/// 迁移 → .cj 源码文本(不落盘,供测试/预览)
public static func migrationSource(appPackage: String, className: String, m: Migration): String {
if (appPackage.isEmpty()) {
throw Exception("simorm: 应用包名不能为空,请在应用根目录(含 cjpm.toml)运行")
}
let sb = StringBuilder()
sb.append("// 此文件由 simorm CLI 自动生成,请勿手动编辑\n")
sb.append("package ${appPackage}\n\n")
sb.append("import std.collection.*\n")
sb.append("import simcu::orm.migrations.*\n")
sb.append("import simcu::orm.sql.*\n\n")
sb.append("public class ${className} <: Migration {\n")
sb.append(" public init() {\n")
sb.append(" super(\"${escape(m.migrationId)}\", \"${escape(m.description)}\")\n")
sb.append(" }\n\n")
sb.append(" public override func up(builder: MigrationBuilder): Unit {\n")
appendOps(sb, m, true)
sb.append(" }\n\n")
sb.append(" public override func down(builder: MigrationBuilder): Unit {\n")
appendOps(sb, m, false)
sb.append(" }\n")
sb.append("}\n")
sb.toString()
}
/// migrationId "20250701000000_InitialCreate" → 类名 "InitialCreate"
public static func classNameOf(migrationId: String): String {
if (let Some(idx) <- migrationId.lastIndexOf("_")) {
return migrationId[idx + 1..]
}
migrationId
}
/// migrationId → 文件名 "20250701000000_InitialCreate.cj"
public static func fileNameOf(migrationId: String): String {
"${migrationId}.cj"
}
/// 重写注册文件:旧文件(若存在)提取已有条目 + 追加新类名。
/// 新格式:每条顶层 let 在模块初始化时自动注册到 orm-cj 全局注册表。
/// 注意:migrationPackage 必须与迁移文件同包(应用包名.migrations):
/// 注册文件与迁移类同包,顶层 let 直接引用类名,无需额外 import;
/// 子包默认惰性加载,应用侧需一行 `import <应用包名>.migrations.*`
/// 触发加载后,顶层 let 才会执行。
/// 返回注册文件源码文本
public static func registrySource(migrationPackage: String, entries: ArrayList<String>): String {
let sb = StringBuilder()
sb.append("// 此文件由 simorm CLI 自动维护,请勿手动编辑\n")
sb.append("// 与迁移类同包(${migrationPackage}),模块加载时顶层 let 自动注册\n")
sb.append("// 迁移到 simcu::orm 全局注册表(按 migrationId 字典序应用)。\n")
sb.append("package ${migrationPackage}\n\n")
sb.append("import std.collection.*\n")
sb.append("import simcu::orm.migrations.*\n\n")
sb.append("/// 模块初始化时自动注册迁移到 simcu::orm 全局注册表(按 migrationId 字典序应用)\n")
var i: Int64 = 0
for (e in entries) {
sb.append("let _migrationRegistration${i} = registerMigration(${e}())\n")
i += 1
}
sb.toString()
}
/// 从旧注册文件文本提取已注册的迁移类名(按出现顺序去重)。
/// 解析 `let _r0 = registerMigration(Xxx())` 中的类名。
public static func extractRegistryEntries(text: String): ArrayList<String> {
let result = ArrayList<String>()
let bytes = text.toArray()
let marker = "registerMigration(".toArray()
var pos: Int64 = 0
while (true) {
let hit = indexOfBytes(bytes, marker, pos)
if (hit < 0) {
break
}
let nameStart = hit + marker.size
var end = nameStart
while (end < bytes.size && isIdentChar(bytes[end])) {
end += 1
}
let name = text[nameStart..end]
if (!name.isEmpty() && !result.contains(name)) {
result.add(name)
}
pos = end
}
result
}
/// 标识符字符:字母/数字/下划线
private static func isIdentChar(b: UInt8): Bool {
(b >= 0x30 && b <= 0x39) || (b >= 0x41 && b <= 0x5A) || (b >= 0x61 && b <= 0x7A) || b == 0x5F
}
/// 追加注册:旧文件 + 新类名 → 完整注册文件源码(已写新文件并返回路径)
public func updateRegistry(appPackage: String, registryPath: String, newClassName: String): String {
var entries = ArrayList<String>()
let p = Path(registryPath)
if (exists(p)) {
let text = String.fromUtf8(File.readFrom(p))
for (e in extractRegistryEntries(text)) {
entries.add(e)
}
}
if (!entries.contains(newClassName)) {
entries.add(newClassName)
}
let code = registrySource(appPackage, entries)
let dir = parentDirOf(registryPath)
let dirPath = Path(dir)
if (!exists(dirPath)) {
Directory.create(dirPath, recursive: true)
}
if (exists(p)) {
remove(p)
}
let f = File.create(p)
f.write(code.toArray())
f.close()
registryPath
}
// ---------- 私有 ----------
private static func appendOps(sb: StringBuilder, m: Migration, isUp: Bool): Unit {
let b = MigrationBuilder()
if (isUp) {
m.up(b)
} else {
m.down(b)
}
for (op in b.getOperations()) {
appendOp(sb, op, " ")
}
}
private static func appendOp(sb: StringBuilder, op: MigrationOperation, indent: String): Unit {
match (op.kind) {
case MigrationOperationKind.CreateTable =>
sb.append("${indent}builder.createTable(\"${escape(op.tableName)}\") { tb =>\n")
for (c in op.columnDefs) {
sb.append("${indent} tb.column(\"${escape(c.name)}\", ColumnTypes.${columnTypeName(c.columnType)})")
sb.append("${columnChainSuffix(c)}\n")
}
sb.append("${indent}}\n")
case MigrationOperationKind.DropTable =>
sb.append("${indent}builder.dropTable(\"${escape(op.tableName)}\")\n")
case MigrationOperationKind.AddColumn =>
sb.append("${indent}builder.addColumn(\"${escape(op.tableName)}\", ${columnDefExpr(op.column.getOrThrow())})\n")
case MigrationOperationKind.DropColumn =>
sb.append("${indent}builder.dropColumn(\"${escape(op.tableName)}\", \"${escape(op.columnName)}\")\n")
case MigrationOperationKind.AlterColumn =>
sb.append("${indent}builder.alterColumn(\"${escape(op.tableName)}\", ${columnDefExpr(op.column.getOrThrow())})\n")
case MigrationOperationKind.RenameColumn =>
sb.append("${indent}builder.renameColumn(\"${escape(op.tableName)}\", \"${escape(op.columnName)}\", \"${escape(op.newColumnName)}\")\n")
case MigrationOperationKind.CreateIndex =>
sb.append("${indent}builder.createIndex(\"${escape(op.indexName)}\", \"${escape(op.tableName)}\", [${colList(op.columnNames)}]")
if (op.unique) {
sb.append(", true")
}
sb.append(")\n")
case MigrationOperationKind.DropIndex =>
sb.append("${indent}builder.dropIndex(\"${escape(op.indexName)}\", \"${escape(op.tableName)}\")\n")
case MigrationOperationKind.RawSql =>
sb.append("${indent}builder.rawSql(\"${escape(op.sql)}\")\n")
}
}
/// ColumnDefinition → 内联表达式: ColumnDefinition("name", ColumnTypes.X).primary().notNull()...
private static func columnDefExpr(c: ColumnDefinition): String {
var s = "ColumnDefinition(\"${escape(c.name)}\", ColumnTypes.${columnTypeName(c.columnType)})"
s = s + columnChainSuffix(c)
s
}
/// 链式标记后缀(不生成默认值字面量,遇到则抛异常提示手写)
private static func columnChainSuffix(c: ColumnDefinition): String {
var s = ""
if (c.primaryKey) {
s = s + ".primary()"
}
if (c.autoIncrement) {
s = s + ".autoInc()"
}
if (!c.nullable) {
s = s + ".notNull()"
}
if (c.unique) {
s = s + ".withUnique()"
}
if (c.maxLength != 255) {
s = s + ".withMaxLength(${c.maxLength})"
}
if (let Some(_) <- c.defaultValue) {
throw Exception(
"simorm: CLI 生成的迁移不支持带默认值的列,请改用手写迁移(override up/down)")
}
s
}
private static func columnTypeName(t: ColumnTypes): String {
match (t) {
case ColumnTypes.BigIntCol => "BigIntCol"
case ColumnTypes.IntCol => "IntCol"
case ColumnTypes.SmallIntCol => "SmallIntCol"
case ColumnTypes.TinyIntCol => "TinyIntCol"
case ColumnTypes.TextCol => "TextCol"
case ColumnTypes.BoolCol => "BoolCol"
case ColumnTypes.FloatCol => "FloatCol"
case ColumnTypes.RealCol => "RealCol"
case ColumnTypes.DateTimeCol => "DateTimeCol"
case ColumnTypes.DecimalCol => "DecimalCol"
case ColumnTypes.BinaryCol => "BinaryCol"
}
}
private static func colList(cols: ArrayList<String>): String {
var sb = StringBuilder()
var first = true
for (c in cols) {
if (!first) {
sb.append(", ")
}
sb.append("\"${escape(c)}\"")
first = false
}
sb.toString()
}
/// 生成 Cangjie 字符串字面量转义
private static func escape(s: String): String {
var sb = StringBuilder()
for (c in s.runes()) {
if (c == Rune(0x5C)) {
sb.append("\\\\")
} else if (c == Rune(0x22)) {
sb.append("\\\"")
} else if (c == Rune(0x0A)) {
sb.append("\\n")
} else if (c == Rune(0x0D)) {
sb.append("\\r")
} else if (c == Rune(0x09)) {
sb.append("\\t")
} else {
sb.append(c)
}
}
sb.toString()
}
/// 取路径的父目录(字符串级,兼容 "/" 与 "\");无分隔符返回 "."
private static func parentDirOf(path: String): String {
let s = match (path.lastIndexOf("/")) {
case Some(i) => i
case None => -1
}
let b = match (path.lastIndexOf("\\")) {
case Some(i) => i
case None => -1
}
let idx = if (s >= 0 && b >= 0) {
if (s > b) { s } else { b }
} else if (s >= 0) {
s
} else {
b
}
if (idx > 0) {
return path[..idx]
}
"."
}
/// 字节数组子串查找(返回起始字节索引,-1 表示未找到)
private static func indexOfBytes(hay: Array<UInt8>, needle: Array<UInt8>, from: Int64): Int64 {
if (needle.size == 0 || from > hay.size - needle.size) {
return -1
}
var i = from
while (i <= hay.size - needle.size) {
var j: Int64 = 0
var ok = true
while (j < needle.size) {
if (hay[i + j] != needle[j]) {
ok = false
break
}
j += 1
}
if (ok) {
return i
}
i += 1
}
-1
}
}