diff --git a/.gitignore b/.gitignore index 1e14935..d289f57 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,6 @@ target/ # 宏编译产物(构建时自动生成,不入库) src/macros/*.cjo src/macros/*.dll +src/tests/*.cj.macrocall # 临时/日志 *.log diff --git a/README.md b/README.md index 377e559..b588a0d 100644 --- a/README.md +++ b/README.md @@ -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→BinaryCol`、`Rune→IntCol`、`Duration→BigIntCol`;`Option` 按内层类型 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 @@ -273,7 +273,7 @@ orm-cj/ ├── cjpm.toml # 包配置(name = orm, organization = simcu) ├── src/ │ ├── Orm.cj # 聚合导出:import simcu::orm.* 即全部可见 -│ ├── annotations/ # @Table / @Column / @Key / @AutoIncrement / @Ignore / @Required / @MaxLength +│ ├── 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. 模型映射约定 diff --git a/cjpm.toml b/cjpm.toml index 844f3a3..4aedcec 100644 --- a/cjpm.toml +++ b/cjpm.toml @@ -3,7 +3,7 @@ name = "orm" organization = "simcu" description = "SimApi 数据访问层 ORM:数据模型映射 + 增删改查 + 数据库迁移;多方言架构,内置 openGauss/PostgreSQL 方言" - version = "1.2.1" + version = "1.3.0" target-dir = "" output-type = "static" diff --git a/src/annotations/Annotations.cj b/src/annotations/Annotations.cj index 8a3ca02..1a33fa5 100644 --- a/src/annotations/Annotations.cj +++ b/src/annotations/Annotations.cj @@ -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 = "" + } +} diff --git a/src/cli/MigrationFileGenerator.cj b/src/cli/MigrationFileGenerator.cj index 48cd5a5..44176d3 100644 --- a/src/cli/MigrationFileGenerator.cj +++ b/src/cli/MigrationFileGenerator.cj @@ -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([${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") diff --git a/src/migrations/MigrationGenerator.cj b/src/migrations/MigrationGenerator.cj index 635e50b..1728e9c 100644 --- a/src/migrations/MigrationGenerator.cj +++ b/src/migrations/MigrationGenerator.cj @@ -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, downOps: ArrayList, old: EntityModel, cur: EntityModel): Unit { - var oldIdx = HashMap() - for (p in old.properties) { - if (p.isIndex) { - oldIdx[p.indexName] = p - } + var oldIdx = HashMap() + for (s in collectIndexSpecs(old)) { + oldIdx[s.indexName] = s } - var curIdx = HashMap() - for (p in cur.properties) { - if (p.isIndex) { - curIdx[p.indexName] = p - } + var curIdx = HashMap() + 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 { let ops = ArrayList() - 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 { let ops = ArrayList() - 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 { + let specs = ArrayList() + 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): HashMap { let map = HashMap() 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 = ArrayList() + /// 是否唯一 + public var unique: Bool = false + /// 部分索引过滤条件(空串表示全表索引) + public var filter: String = "" + + public init() {} +} diff --git a/src/migrations/Migrations.cj b/src/migrations/Migrations.cj index 9874f25..c1c7bf7 100644 --- a/src/migrations/Migrations.cj +++ b/src/migrations/Migrations.cj @@ -107,6 +107,8 @@ public class MigrationOperation { public var indexName: String = "" public var columnNames: ArrayList = 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, unique: Bool): MigrationBuilder { + createIndex(indexName, table, columns, unique, "") + } + + /// 创建索引(可指定部分索引过滤条件;filter 非空时生成 CREATE ... WHERE filter) + public func createIndex(indexName: String, table: String, columns: ArrayList, + 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() } diff --git a/src/migrations/ModelSnapshot.cj b/src/migrations/ModelSnapshot.cj index 503bf41..fbab5de 100644 --- a/src/migrations/ModelSnapshot.cj +++ b/src/migrations/ModelSnapshot.cj @@ -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 = 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() + for (e in m.indexes) { + let io = HashMap() + io["name"] = JString(e.indexName) + let icols = ArrayList() + 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, k: String): ArrayList { + let list = ArrayList() + match (o.get(k)) { + case Some(JArray(arr)) => + for (e in arr) { + match (e) { + case JString(s) => list.add(s) + case _ => () + } + } + case _ => () + } + list + } } diff --git a/src/model/EntityModel.cj b/src/model/EntityModel.cj index 5c8ab0a..5cb33d3 100644 --- a/src/model/EntityModel.cj +++ b/src/model/EntityModel.cj @@ -103,6 +103,22 @@ public class PropertyModel { } } +/** + * 类级声明的实体索引(复合/条件索引,来自 @TableIndex 注解或快照反序列化)。 + */ +public class EntityIndex { + /// 索引名(空则按 ix_<表名>_<首列名> 自动生成) + public var indexName: String = "" + /// 列名列表(已映射为数据库列名) + public var columnNames: ArrayList = ArrayList() + /// 是否唯一索引 + public var unique: Bool = false + /// 部分索引过滤条件(空串表示全表索引) + public var filter: String = "" + + public init() {} +} + /** * 实体模型:一个实体类 ↔ 一张表。 */ @@ -115,6 +131,8 @@ public class EntityModel { public var properties: ArrayList = ArrayList() /// 主键映射 public var keyProperty: ?PropertyModel = None + /// 类级声明的实体索引(复合/条件索引) + public var indexes: ArrayList = ArrayList() /// 轻量构造:快照反序列化 / 人工组装时使用,createInstance 不可用 public init() {} @@ -266,6 +284,21 @@ public class ModelCache { } model.properties.add(propM) } + // 类级复合/条件索引(@TableIndex):列名映射为数据库列名 + if (let Some(ti) <- ct.findAnnotation()) { + 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() 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 { + var list = ArrayList() + 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) { diff --git a/src/tests/MigrationCli_test.cj b/src/tests/MigrationCli_test.cj index d4f2c0c..4d14367 100644 --- a/src/tests/MigrationCli_test.cj +++ b/src/tests/MigrationCli_test.cj @@ -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() + 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([\"source_server\", \"name\"]), true, \"kind = 'Passport'\")")) + @Expect(src.contains("builder.dropIndex(\"ux_assets_passport_name\", \"assets\")")) + } + @TestCase public func testRegistryRoundTrip(): Unit { let entries = ArrayList()