2 Commits
30 changed files with 353 additions and 62 deletions

No files matched your search

+1
View File
@@ -3,5 +3,6 @@ target/
# 宏编译产物(构建时自动生成,不入库)
src/macros/*.cjo
src/macros/*.dll
src/tests/*.cj.macrocall
# 临时/日志
*.log
+5 -4
View File
@@ -166,7 +166,7 @@ main() {
}
```
- 快照 JSON 结构:`{"version":1,"models":[{"table":"users","columns":[{...}]}]}`,含表名、列名、类型、主键/自增/必填/最大长度;
- 快照 JSON 结构:`{"version":4,"models":[{"table":"users","columns":[{...}],"indexes":[{...}]}]}`,含表名、列名、类型、主键/自增/必填/最大长度,以及实体级索引(名称/列/唯一/过滤条件);
- `ModelSnapshot` 也提供 `capture(models)` / `toJson()` / `fromJson(text)` / `save(path)` / `load(path)`(文件不存在返回 `None`),可自行接入版本化/比较逻辑;
- 零依赖:`MiniJson` 内置迷你 JSON 解析/序列化,不引入第三方包。
@@ -175,7 +175,7 @@ main() {
- **增量**:`diff(id, desc, oldModels, newModels)` → 新增表 `CreateTable`、删除表 `DropTable`、列增删改(`AddColumn`/`DropColumn`/`AlterColumn`),`down` 自动按 up 逆序反转;
- **同名列修改数据结构**:`Migrator` 执行 `AddColumn`/`AlterColumn` 前先查 `information_schema`,若库中已有同名列且类型与目标不一致,则**先 `DROP COLUMN` 再 `ADD COLUMN`**(重建该字段);类型一致时 `AddColumn` 幂等跳过、`AlterColumn` 正常执行(处理非空/长度等变化);
- 字段类型 → 列类型映射:`String→TextCol`、`Bool→BoolCol`、`Int8/16/32/64→Tiny/Small/Int/BigIntCol`、`Float32→RealCol`、`Float64→FloatCol`、`DateTime→DateTimeCol`、`Decimal→DecimalCol`、`Array<Byte>→BinaryCol`、`Rune→IntCol`、`Duration→BigIntCol`;`Option<X>` 按内层类型 X 映射且列可空(`None` ↔ `NULL`);
- **不支持**(抛异常提示手写迁移):主键列名变更、主键/自增属性变更;`@Ignore`/`@Column`/`@Required`/`@MaxLength`/`@AutoIncrement`/`@Index` 均会反映到 DDL(`@Index` 生成 `CREATE INDEX`)。
- **不支持**(抛异常提示手写迁移):主键列名变更、主键/自增属性变更;`@Ignore`/`@Column`/`@Required`/`@MaxLength`/`@AutoIncrement`/`@Index` 均会反映到 DDL(`@Index` 生成 `CREATE INDEX`);类级 `@TableIndex` 生成对应的 `CREATE [UNIQUE] INDEX ... [WHERE filter]`(复合/唯一/部分索引),字段级与实体级索引统一走 `IndexSpec` diff(快照 v4 持久化)。
### 迁移 CLI
@@ -272,8 +272,8 @@ public class InitGames {
orm-cj/
├── cjpm.toml # 包配置(name = orm, organization = simcu)
├── src/
│ ├── Orm.cj # 聚合导出:import simcu::orm.* 即全部可见
│ ├── annotations/ # @Table / @Column / @Key / @AutoIncrement / @Ignore / @Required / @MaxLength
│ ├── orm.cj # 聚合导出:import simcu::orm.* 即全部可见
│ ├── annotations/ # @Table / @Column / @Key / @AutoIncrement / @Ignore / @Required / @MaxLength / @Index / @TableIndex
│ ├── macros/ # @DbContext 类级宏 + @Seed 数据填充宏(自动注册,无需手写注册表)
│ ├── seeds/ # Seed 基类 + 进程级填充注册表(registerSeed / allSeeds)
│ ├── model/ # EntityModel(反射映射)、ModelCache(模型缓存)、
@@ -308,6 +308,7 @@ orm-cj/
| `@Required` | 字段 | 非空列(仅影响建表 DDL) |
| `@MaxLength[100]` | 字段 | 字符串列最大长度(仅影响建表 DDL) |
| `@Index` | 字段 | 对该列建索引(索引名默认 `ix_<表名>_<列名>`,迁移 add 自动生成 CREATE INDEX) |
| `@TableIndex["col1,col2", true, "cond"]` | 类 | 类级复合/唯一/条件索引(对齐 .NET 类级 `[Index(...)]`);`columns` 为逗号分隔的列/字段名(字段名自动映射为列名),`unique` 是否唯一,`filter` 非空时生成 `... WHERE cond` 部分索引;索引名可省略(`name` 置于末位,留空则按 `ix_<表名>_<首列>` 自动生成),支持 `[columns]`、`[columns, unique]`、`[columns, unique, filter]`、`[columns, unique, filter, name]` 四种用法 |
### 2. 模型映射约定
+1 -1
View File
@@ -3,7 +3,7 @@
name = "orm"
organization = "simcu"
description = "SimApi 数据访问层 ORM:数据模型映射 + 增删改查 + 数据库迁移;多方言架构,内置 openGauss/PostgreSQL 方言"
version = "1.2.1"
version = "1.3.1"
target-dir = ""
output-type = "static"
@@ -101,3 +101,55 @@ public class MaxLength {
public class Index {
public const init() {}
}
/**
* 类级复合/条件索引(对齐 .NET 类级 [Index(...)],支持多列 + 唯一 + 部分索引)。
* 标注在实体类上,一个类声明一个复合索引(Cangjie 反射 findAnnotation 在同类多标注时读不到)。
*
* 用法:
* @TableIndex["source_server,name", true, "kind = 'Passport'"]
* public class Asset { ... }
*
* 参数(顺序):
* - columns 逗号分隔的列名/字段名(字段名自动映射为列名,如 "source_server,name" 或 "_sourceServer,_name")
* - unique 是否唯一索引,默认 false
* - filter 部分索引过滤条件(原始 SQL,如 "kind = 'Passport'";空串表示全表索引,不生成 WHERE),默认空
* - name 索引名,可为空;为空时自动生成 ix_<表名>_<首列名>
*
* 重载:@TableIndex[columns](全默认)、[columns, unique]、[columns, unique, filter]、[columns, unique, filter, name]。
*/
@Annotation[target: [Type]]
public class TableIndex {
public let columns: String
public let unique: Bool
public let filter: String
public let name: String
public const init(columns: String, unique: Bool, filter: String, name: String) {
this.columns = columns
this.unique = unique
this.filter = filter
this.name = name
}
public const init(columns: String, unique: Bool, filter: String) {
this.columns = columns
this.unique = unique
this.filter = filter
this.name = ""
}
public const init(columns: String, unique: Bool) {
this.columns = columns
this.unique = unique
this.filter = ""
this.name = ""
}
public const init(columns: String) {
this.columns = columns
this.unique = false
this.filter = ""
this.name = ""
}
}
File renamed without changes.
@@ -13,7 +13,7 @@
* - 模型:反射收集实例的 DbSet<T> 属性(getModels())
* - 连接/驱动/方言:构造参数
* - 迁移列表:子类 override migrations()(宏默认返回 orm-cj 全局注册表,
* CLI 生成的 src/migrations/MigrationRegistry.cj 在模块加载时自动注册)
* CLI 生成的 src/migrations/migration_registry.cj 在模块加载时自动注册)
*
* 注意:应用侧(DataContext.cj 或 main.cj)需要一行
* import <应用包名>.migrations.*
@@ -55,7 +55,7 @@ public class MigrationCli {
// 注册文件与迁移文件同目录(src/migrations/,package = 应用包名.migrations)。
// 子包默认惰性加载,顶层 let 不会执行;应用侧需一行
// `import <应用包名>.migrations.*` 触发加载后,注册才生效。
private var _registryPath = "src/migrations/MigrationRegistry.cj"
private var _registryPath = "src/migrations/migration_registry.cj"
public init() {}
@@ -271,7 +271,7 @@ public class MigrationCli {
for (f in Directory.readFrom(dir)) {
if (f.isRegular()) {
let name = f.name
if (name.endsWith(".cj") && name != "MigrationRegistry.cj" && name != "_placeholder.cj") {
if (name.endsWith(".cj") && name != "migration_registry.cj" && name != "_placeholder.cj") {
ids.add(name[..name.size - 3])
}
}
@@ -2,7 +2,7 @@
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 迁移落盘生成器(对齐 dotnet ef migrations add):
* - Migration 对象 → .cj 源文件文本(迁移类 up/down 用 MigrationBuilder 声明式重建),
* - 自动维护迁移注册文件 MigrationRegistry.cj(add 时插入新迁移实例)。
* - 自动维护迁移注册文件 migration_registry.cj(add 时插入新迁移实例)。
*
* 生成文件的格式:
* // 此文件由 simcu::orm CLI 自动生成,请勿手动编辑
@@ -211,7 +211,10 @@ public class MigrationFileGenerator {
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)}\", ArrayList<String>([${colList(op.columnNames)}])")
if (op.unique) {
if (op.filter != "") {
let uniqueLiteral = if (op.unique) { "true" } else { "false" }
sb.append(", ${uniqueLiteral}, \"${escape(op.filter)}\"")
} else if (op.unique) {
sb.append(", true")
}
sb.append(")\n")
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -214,72 +214,70 @@ public class MigrationGenerator {
diffIndexes(upOps, downOps, old, cur)
}
/// 索引 diff:按 indexName 匹配,处理新增/删除/变更(变更 = up 先 DROP 再 CREATE,down 反之)
/// 索引 diff:按 indexName 匹配,处理新增/删除/变更(变更 = up 先 DROP 再 CREATE,down 反之)。
/// 统一处理字段级(@Index)与实体级(@TableIndex)索引,均归一为 IndexSpec。
private static func diffIndexes(upOps: ArrayList<MigrationOperation>, downOps: ArrayList<MigrationOperation>,
old: EntityModel, cur: EntityModel): Unit {
var oldIdx = HashMap<String, PropertyModel>()
for (p in old.properties) {
if (p.isIndex) {
oldIdx[p.indexName] = p
}
var oldIdx = HashMap<String, IndexSpec>()
for (s in collectIndexSpecs(old)) {
oldIdx[s.indexName] = s
}
var curIdx = HashMap<String, PropertyModel>()
for (p in cur.properties) {
if (p.isIndex) {
curIdx[p.indexName] = p
}
var curIdx = HashMap<String, IndexSpec>()
for (s in collectIndexSpecs(cur)) {
curIdx[s.indexName] = s
}
// 新增 / 变更
for (p in cur.properties) {
if (!p.isIndex) {
continue
}
if (let Some(op) <- oldIdx.get(p.indexName)) {
if (!sameIndex(op, p)) {
upOps.add(dropIndexOp(cur.tableName, p.indexName))
downOps.add(createIndexOp(old.tableName, op))
upOps.add(createIndexOp(cur.tableName, p))
downOps.add(dropIndexOp(old.tableName, p.indexName))
for (s in collectIndexSpecs(cur)) {
if (let Some(os) <- oldIdx.get(s.indexName)) {
if (!sameIndex(os, s)) {
upOps.add(dropIndexOp(cur.tableName, s.indexName))
downOps.add(createIndexOp(os))
upOps.add(createIndexOp(s))
downOps.add(dropIndexOp(old.tableName, s.indexName))
}
} else {
upOps.add(createIndexOp(cur.tableName, p))
downOps.add(dropIndexOp(cur.tableName, p.indexName))
upOps.add(createIndexOp(s))
downOps.add(dropIndexOp(cur.tableName, s.indexName))
}
}
// 删除
for (p in old.properties) {
if (!p.isIndex) {
continue
}
if (!curIdx.contains(p.indexName)) {
upOps.add(dropIndexOp(old.tableName, p.indexName))
downOps.add(createIndexOp(old.tableName, p))
for (s in collectIndexSpecs(old)) {
if (!curIdx.contains(s.indexName)) {
upOps.add(dropIndexOp(old.tableName, s.indexName))
downOps.add(createIndexOp(s))
}
}
}
private static func sameIndex(a: PropertyModel, b: PropertyModel): Bool {
a.columnName == b.columnName && a.indexUnique == b.indexUnique
private static func sameIndex(a: IndexSpec, b: IndexSpec): Bool {
if (a.unique != b.unique || a.filter != b.filter) {
return false
}
if (a.columnNames.size != b.columnNames.size) {
return false
}
for (i in 0..a.columnNames.size) {
if (a.columnNames[i] != b.columnNames[i]) {
return false
}
}
true
}
/// 该模型所有索引列 → CREATE INDEX op(up 用)
/// 该模型所有索引(字段级 + 实体级)→ CREATE INDEX op(up 用)
private static func createIndexOpsFor(model: EntityModel): ArrayList<MigrationOperation> {
let ops = ArrayList<MigrationOperation>()
for (p in model.properties) {
if (p.isIndex) {
ops.add(createIndexOp(model.tableName, p))
}
for (s in collectIndexSpecs(model)) {
ops.add(createIndexOp(s))
}
ops
}
/// 该模型所有索引列 → DROP INDEX op(down 用,与 createIndexOpsFor 同序)
/// 该模型所有索引(字段级 + 实体级)→ DROP INDEX op(down 用,与 createIndexOpsFor 同序)
private static func dropIndexOpsFor(model: EntityModel): ArrayList<MigrationOperation> {
let ops = ArrayList<MigrationOperation>()
for (p in model.properties) {
if (p.isIndex) {
ops.add(dropIndexOp(model.tableName, p.indexName))
}
for (s in collectIndexSpecs(model)) {
ops.add(dropIndexOp(model.tableName, s.indexName))
}
ops
}
@@ -307,6 +305,35 @@ public class MigrationGenerator {
}
}
/// 收集模型上的全部索引规格:先字段级(@Index),再实体级(@TableIndex)。
/// 顺序即生成顺序,down 与之对应逆序销毁。
private static func collectIndexSpecs(model: EntityModel): ArrayList<IndexSpec> {
let specs = ArrayList<IndexSpec>()
for (p in model.properties) {
if (p.isIndex) {
let s = IndexSpec()
s.table = model.tableName
s.indexName = p.indexName
s.unique = p.indexUnique
s.filter = ""
s.columnNames.add(p.columnName)
specs.add(s)
}
}
for (e in model.indexes) {
let s = IndexSpec()
s.table = model.tableName
s.indexName = e.indexName
s.unique = e.unique
s.filter = e.filter
for (c in e.columnNames) {
s.columnNames.add(c)
}
specs.add(s)
}
specs
}
private static func indexByTable(models: ArrayList<EntityModel>): HashMap<String, EntityModel> {
let map = HashMap<String, EntityModel>()
for (model in models) {
@@ -362,12 +389,15 @@ public class MigrationGenerator {
op
}
private static func createIndexOp(table: String, p: PropertyModel): MigrationOperation {
private static func createIndexOp(s: IndexSpec): MigrationOperation {
let op = MigrationOperation(MigrationOperationKind.CreateIndex)
op.tableName = table
op.indexName = p.indexName
op.unique = p.indexUnique
op.columnNames.add(p.columnName)
op.tableName = s.table
op.indexName = s.indexName
op.unique = s.unique
op.filter = s.filter
for (c in s.columnNames) {
op.columnNames.add(c)
}
op
}
@@ -378,3 +408,21 @@ public class MigrationGenerator {
op
}
}
/**
* 内部统一的索引规格:把字段级(@Index)与实体级(@TableIndex)索引归一为同构描述,便于 diff 与生成。
*/
public class IndexSpec {
/// 所属表名
public var table: String = ""
/// 索引名
public var indexName: String = ""
/// 列名列表
public var columnNames: ArrayList<String> = ArrayList()
/// 是否唯一
public var unique: Bool = false
/// 部分索引过滤条件(空串表示全表索引)
public var filter: String = ""
public init() {}
}
File renamed without changes.
@@ -107,6 +107,8 @@ public class MigrationOperation {
public var indexName: String = ""
public var columnNames: ArrayList<String> = ArrayList()
public var unique: Bool = false
/// 部分索引过滤条件(CREATE ... WHERE filter;空串表示全表索引)
public var filter: String = ""
public var sql: String = ""
public init(kind: MigrationOperationKind) {
@@ -202,11 +204,18 @@ public class MigrationBuilder {
public func createIndex(indexName: String, table: String, columns: ArrayList<String>,
unique: Bool): MigrationBuilder {
createIndex(indexName, table, columns, unique, "")
}
/// 创建索引(可指定部分索引过滤条件;filter 非空时生成 CREATE ... WHERE filter)
public func createIndex(indexName: String, table: String, columns: ArrayList<String>,
unique: Bool, filter: String): MigrationBuilder {
let op = MigrationOperation(MigrationOperationKind.CreateIndex)
op.indexName = indexName
op.tableName = table
op.columnNames = columns
op.unique = unique
op.filter = filter
operations.add(op)
this
}
@@ -391,6 +400,9 @@ public class DdlFactory {
first = false
}
sb.append(")")
if (op.filter != "") {
sb.append(" WHERE ${op.filter}")
}
sb.toString()
}
File renamed without changes.
File renamed without changes.
@@ -10,11 +10,12 @@
* // 模型变更后:快照存在 → 载入旧模型 diff,并覆盖保存新快照
* gen.ensure("20250801000000_AddAge", "新增 age 列", "snapshot.json", models1)
*
* 快照 JSON 结构(版本 3,较 v2 新增索引标记 idx/ixName/ixUnique):
* {"version":3,"models":[{"table":"users","columns":[
* 快照 JSON 结构(版本 4,较 v3 新增实体级 indexes 数组;v3 的 idx/ixName/ixUnique 为字段级索引标记):
* {"version":4,"models":[{"table":"users","columns":[
* {"name":"id","column":"id","type":"Int64","key":true,"auto":true,
* "client":false,"required":false,"maxLen":0,"enum":false,
* "idx":false,"ixName":"","ixUnique":false}]}]}
* "idx":false,"ixName":"","ixUnique":false}],
* "indexes":[{"name":"ux","columns":["a","b"],"unique":true,"filter":"kind = 'Passport'"}]}]}
*/
package simcu::orm.migrations
@@ -31,7 +32,7 @@ public class ModelSnapshot {
/// 快照内的轻量模型列表
public var models: ArrayList<EntityModel> = ArrayList()
/// 快照格式版本
public static let formatVersion: Int64 = 3
public static let formatVersion: Int64 = 4
public init() {}
@@ -64,6 +65,16 @@ public class ModelSnapshot {
}
}
}
for (e in m.indexes) {
let li = EntityIndex()
li.indexName = e.indexName
li.unique = e.unique
li.filter = e.filter
for (c in e.columnNames) {
li.columnNames.add(c)
}
lm.indexes.add(li)
}
snap.models.add(lm)
}
snap
@@ -100,6 +111,20 @@ public class ModelSnapshot {
cols.add(JObject(co))
}
mo["columns"] = JArray(cols)
let idxs = ArrayList<JsonValue>()
for (e in m.indexes) {
let io = HashMap<String, JsonValue>()
io["name"] = JString(e.indexName)
let icols = ArrayList<JsonValue>()
for (c in e.columnNames) {
icols.add(JString(c))
}
io["columns"] = JArray(icols)
io["unique"] = JBool(e.unique)
io["filter"] = JString(e.filter)
idxs.add(JObject(io))
}
mo["indexes"] = JArray(idxs)
modelsArr.add(JObject(mo))
}
root["models"] = JArray(modelsArr)
@@ -146,6 +171,26 @@ public class ModelSnapshot {
}
case _ => throw Exception("simcu::orm: 快照 models[].columns 必须是数组")
}
// 实体级索引(v4 开始存在;缺失时容忍为空,兼容旧快照)
match (mo.get("indexes")) {
case Some(JArray(idxs)) =>
for (i in idxs) {
match (i) {
case JObject(io) =>
let li = EntityIndex()
li.indexName = getString(io, "name")
li.unique = getBool(io, "unique")
li.filter = getString(io, "filter")
for (c in getStringArray(io, "columns")) {
li.columnNames.add(c)
}
lm.indexes.add(li)
case _ => throw Exception(
"simcu::orm: 快照 indexes[] 项必须是 object")
}
}
case _ => ()
}
snap.models.add(lm)
case _ => throw Exception("simcu::orm: 快照 models[] 项必须是 object")
}
@@ -201,4 +246,20 @@ public class ModelSnapshot {
case _ => 0
}
}
/// 读取字符串数组(如索引列名列表);缺失或类型不符时返回空列表
private static func getStringArray(o: HashMap<String, JsonValue>, k: String): ArrayList<String> {
let list = ArrayList<String>()
match (o.get(k)) {
case Some(JArray(arr)) =>
for (e in arr) {
match (e) {
case JString(s) => list.add(s)
case _ => ()
}
}
case _ => ()
}
list
}
}
@@ -103,6 +103,22 @@ public class PropertyModel {
}
}
/**
* 类级声明的实体索引(复合/条件索引,来自 @TableIndex 注解或快照反序列化)。
*/
public class EntityIndex {
/// 索引名(空则按 ix_<表名>_<首列名> 自动生成)
public var indexName: String = ""
/// 列名列表(已映射为数据库列名)
public var columnNames: ArrayList<String> = ArrayList()
/// 是否唯一索引
public var unique: Bool = false
/// 部分索引过滤条件(空串表示全表索引)
public var filter: String = ""
public init() {}
}
/**
* 实体模型:一个实体类 ↔ 一张表。
*/
@@ -115,6 +131,8 @@ public class EntityModel {
public var properties: ArrayList<PropertyModel> = ArrayList()
/// 主键映射
public var keyProperty: ?PropertyModel = None
/// 类级声明的实体索引(复合/条件索引)
public var indexes: ArrayList<EntityIndex> = ArrayList()
/// 轻量构造:快照反序列化 / 人工组装时使用,createInstance 不可用
public init() {}
@@ -266,6 +284,21 @@ public class ModelCache {
}
model.properties.add(propM)
}
// 类级复合/条件索引(@TableIndex):列名映射为数据库列名
if (let Some(ti) <- ct.findAnnotation<TableIndex>()) {
let idx = EntityIndex()
if (ti.name != "") {
idx.indexName = ti.name
} else {
// 首个列名决定默认索引名
let firstCol = mapFirstColumn(ti.columns)
idx.indexName = "ix_${model.tableName}_${firstCol}"
}
idx.columnNames = splitColumns(ti.columns, model)
idx.unique = ti.unique
idx.filter = ti.filter
model.indexes.add(idx)
}
// 主键校验(v1 仅支持单主键)
var keys = ArrayList<PropertyModel>()
for (p in model.properties) {
@@ -299,6 +332,60 @@ public class ModelCache {
}
}
/// 把逗号分隔的列名/字段名字符串拆成数据库列名列表("source_server,name"、"_sourceServer, _name")。
/// 每一项先经 mapColumn 映射:字段名或列名都转到数据库列名。
private static func splitColumns(columns: String, model: EntityModel): ArrayList<String> {
var list = ArrayList<String>()
var sb = StringBuilder()
for (c in columns.runes()) {
if (c == Rune(0x2C)) {
// 逗号:收尾当前列
let col = model.mapColumn(trimWhitespace(sb.toString()))
if (col != "") {
list.add(col)
}
sb = StringBuilder()
} else {
sb.append(c)
}
}
let col = model.mapColumn(trimWhitespace(sb.toString()))
if (col != "") {
list.add(col)
}
list
}
/// 取逗号分隔列串中的首个列(用于默认索引名)。
private static func mapFirstColumn(columns: String): String {
var sb = StringBuilder()
for (c in columns.runes()) {
if (c == Rune(0x2C)) {
break
}
sb.append(c)
}
trimWhitespace(sb.toString())
}
/// 去除字符串首尾空白(仅空格/tab/换行,避免依赖 std.String.trim)
private static func trimWhitespace(s: String): String {
var start: Int64 = 0
var end = s.size
while (start < end && isWsAt(s, start)) {
start += 1
}
while (end > start && isWsAt(s, end - 1)) {
end -= 1
}
s[start..end]
}
private static func isWsAt(s: String, i: Int64): Bool {
let c = s[i]
c == 0x20 || c == 0x09 || c == 0x0A || c == 0x0D
}
/// 应用列命名策略
public static func applyNamingPolicy(fieldName: String, policy: ColumnNamingPolicy): String {
match (policy) {
File renamed without changes.
View File
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -34,6 +34,22 @@ class HandWrittenMigration <: Migration {
}
}
/// 手写迁移(验证部分索引 filter 还原:createIndex 5 参带 filter)
class FilteredIndexMigration <: Migration {
public init() {
super("20250802000000_FilteredIndex", "部分唯一索引")
}
public override func up(builder: MigrationBuilder): Unit {
let cols = ArrayList<String>()
cols.add("source_server")
cols.add("name")
builder.createIndex("ux_assets_passport_name", "assets", cols, true, "kind = 'Passport'")
}
public override func down(builder: MigrationBuilder): Unit {
builder.dropIndex("ux_assets_passport_name", "assets")
}
}
@Test
class MigrationFileGeneratorTests {
private func buildUserModel(): EntityModel {
@@ -123,6 +139,16 @@ class MigrationFileGeneratorTests {
@Expect(src.contains("builder.dropTable(\"tags\")"))
}
@TestCase
public func testFilteredIndexMigrationSource(): Unit {
let m = FilteredIndexMigration()
let src = MigrationFileGenerator.migrationSource("app", MigrationFileGenerator.classNameOf(m.migrationId), m)
// 生成的迁移源码应还原带 filter 的 5 参 createIndex(含 WHERE 条件)
@Expect(src.contains(
"builder.createIndex(\"ux_assets_passport_name\", \"assets\", ArrayList<String>([\"source_server\", \"name\"]), true, \"kind = 'Passport'\")"))
@Expect(src.contains("builder.dropIndex(\"ux_assets_passport_name\", \"assets\")"))
}
@TestCase
public func testRegistryRoundTrip(): Unit {
let entries = ArrayList<String>()
@@ -162,7 +188,7 @@ class MigrationFileGeneratorTests {
@Expect(text.contains("public class InitialCreate <: Migration"))
// 注册文件:首次创建 + 追加 + 幂等
let registryPath = "${dir}/MigrationRegistry.cj"
let registryPath = "${dir}/migration_registry.cj"
fg.updateRegistry("testpkg", registryPath, "InitialCreate")
fg.updateRegistry("testpkg", registryPath, "AddAge")
fg.updateRegistry("testpkg", registryPath, "AddAge")
File renamed without changes.
File renamed without changes.