/* * Copyright (c) 2025 SimcuTeam. All rights reserved. * SimOrm 单元测试(cjpm test),全部为纯逻辑测试,不连接数据库。 * 覆盖:模型映射与命名策略 / 模型校验(无主键、多主键、Option、不支持类型)/ * ChangeTracker 操作队列 / OpenGaussDialect SQL 生成 / DdlFactory DDL 生成 / * MigrationBuilder 链式调用 / QueryBuilder SQL 构建(假 executor 捕获 SQL)。 */ package simcu::orm.tests import std.collection.* import std.database.sql.* import std.fs.* import std.reflect.* import std.unittest.* import std.unittest.testmacro.* import simcu::orm.* // ---------- 测试实体 ---------- @Table["users"] class User { public var id: Int64 = 0 public var name: String = "" public var age: Int32 = 0 } @Table["orders"] class Order { @Key public var orderId: String = "" @Column["full_name"] public var name: String = "" @Ignore public var temp: String = "" } class Product { @Key @AutoIncrement public var pid: Int64 = 0 @Required @MaxLength[100] public var title: String = "" public var price: Float64 = 0.0 public var active: Bool = true } class NoKeyEntity { public var name: String = "" } @Table["plain_keys"] class PlainIdEntity { public var id: String = "" public var name: String = "" } class MultiKeyEntity { @Key public var a: String = "" @Key public var b: String = "" } class OptionEntity { public var id: Int64 = 0 public var nick: ?String = None } enum TestColor { | Red | Green } enum AssetKind { | Passport | Package } class UnsupportedEntity { public var id: Int64 = 0 public var color: TestColor = TestColor.Red } class EnumEntity { public var id: Int64 = 0 public var kind: AssetKind = AssetKind.Passport public var optKind: ?AssetKind = None } class UnsupportedArrayEntity { public var id: Int64 = 0 public var tags: Array = Array() } // ---------- 模型映射 ---------- @Test class ModelMappingTests { @TestCase public func testUserModel(): Unit { let model = ModelCache.get() @Expect(model.tableName, "users") @Expect(model.properties.size, 3) let kp = model.keyProperty.getOrThrow() @Expect(kp.name, "id") @Expect(kp.columnName, "id") @Expect(kp.isKey, true) @Expect(kp.autoIncrement, true) @Expect(kp.clientGenerated, false) @Expect(model.properties[1].name, "name") @Expect(model.properties[1].columnName, "name") @Expect(model.properties[2].columnName, "age") @Expect(model.mapColumn("age"), "age") @Expect(model.mapColumn("unknown"), "unknown") } @TestCase public func testOrderModel(): Unit { let model = ModelCache.get() @Expect(model.tableName, "orders") // @Ignore 字段不映射 @Expect(model.properties.size, 2) let kp = model.keyProperty.getOrThrow() @Expect(kp.name, "orderId") @Expect(kp.columnName, "orderId") @Expect(kp.isKey, true) // String 主键 → 客户端生成,不自增 @Expect(kp.clientGenerated, true) @Expect(kp.autoIncrement, false) // @Column 覆盖列名 @Expect(model.properties[1].name, "name") @Expect(model.properties[1].columnName, "full_name") @Expect(model.mapColumn("name"), "full_name") } @TestCase public func testProductModel(): Unit { let model = ModelCache.get() @Expect(model.properties.size, 4) let kp = model.keyProperty.getOrThrow() @Expect(kp.name, "pid") @Expect(kp.columnName, "pid") @Expect(kp.isKey, true) @Expect(kp.autoIncrement, true) // @Required / @MaxLength let title = model.properties[1] @Expect(title.name, "title") @Expect(title.isRequired, true) @Expect(title.maxLength, 100) @Expect(model.properties[2].columnName, "price") @Expect(model.properties[3].columnName, "active") } @TestCase public func testEntityInstance(): Unit { let model = ModelCache.get() let u = User() u.name = "alice" let instance = model.createInstance() // createInstance 走无参构造,字段为默认值 model.setValue(instance, model.properties[1], "bob") @Expect((model.getValue(instance, model.properties[1]) as String).getOrThrow(), "bob") } } // ---------- 模型校验(异常) ---------- @Test class ModelValidationTests { @TestCase public func testNoKeyThrows(): Unit { let threw = try { ModelCache.get(); false } catch (_: Exception) { true } @Expect(threw, true) } @TestCase public func testPlainIdRecognizedAsKey(): Unit { let model = ModelCache.get() let kp = model.keyProperty.getOrThrow() @Expect(kp.name, "id") @Expect(kp.columnName, "id") @Expect(kp.isKey, true) @Expect(kp.clientGenerated, true) } @TestCase public func testMultiKeyThrows(): Unit { let threw = try { ModelCache.get(); false } catch (_: Exception) { true } @Expect(threw, true) } @TestCase public func testOptionFieldMaps(): Unit { // Option 字段现在受支持:映射为可空列 let model = ModelCache.get() @Expect(model.properties.size, 2) let nick = model.properties[1] @Expect(nick.name, "nick") @Expect(nick.columnName, "nick") @Expect(nick.typeName(), "Option") @Expect(nick.isOption(), true) @Expect(nick.effectiveTypeName(), "String") @Expect(nick.isRequired, false) } @TestCase public func testEnumFieldMaps(): Unit { // 简单枚举(含 Option 内层枚举)受支持:标记 isEnum,记录完整类型名 let model = ModelCache.get() @Expect(model.properties.size, 3) let kind = model.properties[1] @Expect(kind.name, "kind") @Expect(kind.isEnum, true) @Expect(kind.enumTypeName != "", true) @Expect(kind.typeName(), "AssetKind") let optKind = model.properties[2] @Expect(optKind.name, "optKind") @Expect(optKind.isOption(), true) @Expect(optKind.isEnum, true) @Expect(optKind.enumTypeName != "", true) } @TestCase public func testUnsupportedTypeThrows(): Unit { // Array 不在白名单 → 抛异常 let threw = try { ModelCache.get(); false } catch (_: Exception) { true } @Expect(threw, true) } @TestCase public func testEnumToParamValue(): Unit { // 枚举值 → 构造器名(写库值) @Expect((TypeUtil.enumToParamValue(TestColor.Red) as String).getOrThrow(), "Red") @Expect((TypeUtil.enumToParamValue(TestColor.Green) as String).getOrThrow(), "Green") // 非枚举值原样返回 @Expect((TypeUtil.enumToParamValue("abc") as String).getOrThrow(), "abc") @Expect((TypeUtil.enumToParamValue(Int64(18)) as Int64).getOrThrow(), 18) } @TestCase public func testEnumFromNameRoundTrip(): Unit { // 完整类型名 + 构造器名 → 反射构造枚举实例(读库值),构造器名与写库一致 let full = TypeInfo.of(TestColor.Green).toString() let v = ValueReader.enumFromName(full, "Green") let et = if (let e: EnumTypeInfo <- TypeInfo.of(v)) { e } else { throw Exception("不是枚举") } let (ctor, _) = et.destruct(v) @Expect(ctor.name, "Green") // 不存在的构造器 → 抛异常 let threw = try { ValueReader.enumFromName(full, "Blue"); false } catch (_: Exception) { true } @Expect(threw, true) // 非枚举类型名 → 抛异常 let threw2 = try { ValueReader.enumFromName("std.core.String", "x"); false } catch (_: Exception) { true } @Expect(threw2, true) } } // ---------- 命名策略 ---------- @Test class NamingPolicyTests { @TestCase public func testApplyNamingPolicy(): Unit { @Expect(ModelCache.applyNamingPolicy("_userName", ColumnNamingPolicy.SnakeCase), "user_name") @Expect(ModelCache.applyNamingPolicy("_id", ColumnNamingPolicy.SnakeCase), "id") @Expect(ModelCache.applyNamingPolicy("_name", ColumnNamingPolicy.StripUnderscore), "name") @Expect(ModelCache.applyNamingPolicy("_id", ColumnNamingPolicy.StripUnderscore), "id") @Expect(ModelCache.applyNamingPolicy("_id", ColumnNamingPolicy.Keep), "_id") @Expect(ModelCache.applyNamingPolicy("userName", ColumnNamingPolicy.Keep), "userName") @Expect(ModelCache.applyNamingPolicy("__nickName", ColumnNamingPolicy.SnakeCase), "nick_name") @Expect(ModelCache.applyNamingPolicy("name2", ColumnNamingPolicy.SnakeCase), "name2") } } // ---------- ChangeTracker 操作队列 ---------- @Test class ChangeTrackerTests { @TestCase public func testQueueOrder(): Unit { let tracker = ChangeTracker() let u1 = User() let u2 = User() tracker.add(u1, EntityState.Added) tracker.add(u2, EntityState.Modified) tracker.add(u1, EntityState.Deleted) @Expect(tracker.count(), 3) let entries = tracker.getEntries() let s0 = match (entries[0].state) { case EntityState.Added => true; case _ => false } let s1 = match (entries[1].state) { case EntityState.Modified => true; case _ => false } let s2 = match (entries[2].state) { case EntityState.Deleted => true; case _ => false } @Expect(s0, true) @Expect(s1, true) @Expect(s2, true) tracker.clear() @Expect(tracker.count(), 0) } } // ---------- OpenGaussDialect SQL 生成 ---------- @Test class ISqlDialectTests { @TestCase public func testQuoteInsertUpdateDelete(): Unit { let d = OpenGaussDialect() @Expect(d.quoteName("users"), "\"users\"") let c = ArrayList() c.add("id") c.add("name") @Expect(d.buildInsert("users", c, "id", true), "INSERT INTO \"users\" (\"id\", \"name\") VALUES (?, ?) RETURNING \"id\"") @Expect(d.buildInsert("users", c, "id", false), "INSERT INTO \"users\" (\"id\", \"name\") VALUES (?, ?)") @Expect(d.buildUpdate("users", c, "id"), "UPDATE \"users\" SET \"id\" = ?, \"name\" = ? WHERE \"id\" = ?") @Expect(d.buildDelete("users", "id"), "DELETE FROM \"users\" WHERE \"id\" = ?") } @TestCase public func testSelectAndCount(): Unit { let d = OpenGaussDialect() let c = ArrayList() c.add("id") c.add("name") @Expect(d.buildSelect("users", c, "(\"age\" > ?)", "\"name\" ASC", 10, 5), "SELECT \"id\", \"name\" FROM \"users\" WHERE (\"age\" > ?) ORDER BY \"name\" ASC LIMIT 5 OFFSET 10") @Expect(d.buildSelect("users", c, "", "", 0, 0), "SELECT \"id\", \"name\" FROM \"users\"") let empty = ArrayList() @Expect(d.buildSelect("users", empty, "", "", 0, 0), "SELECT * FROM \"users\"") @Expect(d.buildCount("users", ""), "SELECT COUNT(*) FROM \"users\"") @Expect(d.buildCount("users", "(\"age\" > ?)"), "SELECT COUNT(*) FROM \"users\" WHERE (\"age\" > ?)") } } // ---------- DdlFactory DDL 生成 ---------- @Test class DdlFactoryTests { @TestCase public func testColumnTypeSql(): Unit { let d = OpenGaussDialect() @Expect(d.columnTypeSql(ColumnTypes.BigIntCol, true, 0), "BIGSERIAL") @Expect(d.columnTypeSql(ColumnTypes.BigIntCol, false, 0), "BIGINT") @Expect(d.columnTypeSql(ColumnTypes.IntCol, true, 0), "SERIAL") @Expect(d.columnTypeSql(ColumnTypes.IntCol, false, 0), "INTEGER") @Expect(d.columnTypeSql(ColumnTypes.SmallIntCol, false, 0), "SMALLINT") @Expect(d.columnTypeSql(ColumnTypes.TinyIntCol, false, 0), "SMALLINT") @Expect(d.columnTypeSql(ColumnTypes.TextCol, false, 100), "VARCHAR(100)") @Expect(d.columnTypeSql(ColumnTypes.TextCol, false, 0), "VARCHAR(255)") @Expect(d.columnTypeSql(ColumnTypes.BoolCol, false, 0), "BOOLEAN") @Expect(d.columnTypeSql(ColumnTypes.FloatCol, false, 0), "DOUBLE PRECISION") @Expect(d.columnTypeSql(ColumnTypes.RealCol, false, 0), "REAL") @Expect(d.columnTypeSql(ColumnTypes.DateTimeCol, false, 0), "TIMESTAMP") @Expect(d.columnTypeSql(ColumnTypes.DecimalCol, false, 0), "DECIMAL(18, 6)") @Expect(d.columnTypeSql(ColumnTypes.BinaryCol, false, 0), "BYTEA") } @TestCase public func testDropIndexAndAlterColumn(): Unit { let d = OpenGaussDialect() @Expect(d.buildDropIndex("ix_users_name", "users"), "DROP INDEX IF EXISTS \"ix_users_name\"") @Expect(d.buildAlterColumn("users", "\"name\" VARCHAR(128) NOT NULL"), "ALTER TABLE \"users\" ALTER COLUMN \"name\" VARCHAR(128) NOT NULL") } @TestCase public func testCreateTable(): Unit { let f = DdlFactory() let d = OpenGaussDialect() let op = MigrationOperation(MigrationOperationKind.CreateTable) op.tableName = "users" let id = ColumnDefinition("id", ColumnTypes.BigIntCol) id.primary() id.autoInc() id.notNull() let name = ColumnDefinition("name", ColumnTypes.TextCol) name.withMaxLength(100) name.notNull() let active = ColumnDefinition("active", ColumnTypes.BoolCol) active.withDefault(true) op.columnDefs.add(id) op.columnDefs.add(name) op.columnDefs.add(active) let sql = f.toSql(op, d) @Expect(sql.contains("CREATE TABLE IF NOT EXISTS \"users\""), true) @Expect(sql.contains("\"id\" BIGSERIAL NOT NULL PRIMARY KEY"), true) @Expect(sql.contains("\"name\" VARCHAR(100) NOT NULL"), true) @Expect(sql.contains("\"active\" BOOLEAN DEFAULT TRUE"), true) } @TestCase public func testAlterOperations(): Unit { let f = DdlFactory() let d = OpenGaussDialect() // ADD COLUMN let addOp = MigrationOperation(MigrationOperationKind.AddColumn) addOp.tableName = "users" let col = ColumnDefinition("email", ColumnTypes.TextCol) col.withDefault("unknown") addOp.column = Some(col) @Expect(f.toSql(addOp, d), "ALTER TABLE \"users\" ADD COLUMN \"email\" VARCHAR(255) DEFAULT 'unknown'") // DROP COLUMN let dropOp = MigrationOperation(MigrationOperationKind.DropColumn) dropOp.tableName = "users" dropOp.columnName = "email" @Expect(f.toSql(dropOp, d), "ALTER TABLE \"users\" DROP COLUMN \"email\"") // ALTER COLUMN let alterOp = MigrationOperation(MigrationOperationKind.AlterColumn) alterOp.tableName = "users" let nc = ColumnDefinition("name", ColumnTypes.TextCol) nc.withMaxLength(200) nc.notNull() alterOp.column = Some(nc) @Expect(f.toSql(alterOp, d), "ALTER TABLE \"users\" ALTER COLUMN \"name\" VARCHAR(200) NOT NULL") // RENAME COLUMN let renOp = MigrationOperation(MigrationOperationKind.RenameColumn) renOp.tableName = "users" renOp.columnName = "old_name" renOp.newColumnName = "new_name" @Expect(f.toSql(renOp, d), "ALTER TABLE \"users\" RENAME COLUMN \"old_name\" TO \"new_name\"") } @TestCase public func testTableAndIndexAndRaw(): Unit { let f = DdlFactory() let d = OpenGaussDialect() // DROP TABLE let dropOp = MigrationOperation(MigrationOperationKind.DropTable) dropOp.tableName = "users" @Expect(f.toSql(dropOp, d), "DROP TABLE IF EXISTS \"users\"") // CREATE INDEX(非唯一) let idxOp = MigrationOperation(MigrationOperationKind.CreateIndex) idxOp.indexName = "ix_users_name" idxOp.tableName = "users" idxOp.columnNames.add("name") @Expect(f.toSql(idxOp, d), "CREATE INDEX IF NOT EXISTS \"ix_users_name\" ON \"users\" (\"name\")") // CREATE UNIQUE INDEX idxOp.unique = true @Expect(f.toSql(idxOp, d), "CREATE UNIQUE INDEX IF NOT EXISTS \"ix_users_name\" ON \"users\" (\"name\")") // DROP INDEX let dropIdx = MigrationOperation(MigrationOperationKind.DropIndex) dropIdx.indexName = "ix_users_name" @Expect(f.toSql(dropIdx, d), "DROP INDEX IF EXISTS \"ix_users_name\"") // RAW SQL let rawOp = MigrationOperation(MigrationOperationKind.RawSql) rawOp.sql = "SELECT 1" @Expect(f.toSql(rawOp, d), "SELECT 1") } @TestCase public func testDefaultValueEscape(): Unit { let f = DdlFactory() let d = OpenGaussDialect() let op = MigrationOperation(MigrationOperationKind.AddColumn) op.tableName = "t" // 字符串默认值含单引号 → 转义 let s = ColumnDefinition("remark", ColumnTypes.TextCol) s.withDefault("it's ok") op.column = Some(s) @Expect(f.toSql(op, d), "ALTER TABLE \"t\" ADD COLUMN \"remark\" VARCHAR(255) DEFAULT 'it''s ok'") // 数值默认值 let op2 = MigrationOperation(MigrationOperationKind.AddColumn) op2.tableName = "t" let n = ColumnDefinition("cnt", ColumnTypes.IntCol) n.withDefault(Int64(18)) op2.column = Some(n) @Expect(f.toSql(op2, d), "ALTER TABLE \"t\" ADD COLUMN \"cnt\" INTEGER DEFAULT 18") // 布尔默认值 let op3 = MigrationOperation(MigrationOperationKind.AddColumn) op3.tableName = "t" let b = ColumnDefinition("flag", ColumnTypes.BoolCol) b.withDefault(false) op3.column = Some(b) @Expect(f.toSql(op3, d), "ALTER TABLE \"t\" ADD COLUMN \"flag\" BOOLEAN DEFAULT FALSE") } @TestCase public func testResolveColumnSqls(): Unit { let f = DdlFactory() let d = OpenGaussDialect() // 库中无同名列 → 直接 ADD let addOp = MigrationOperation(MigrationOperationKind.AddColumn) addOp.tableName = "users" let col = ColumnDefinition("email", ColumnTypes.TextCol) addOp.column = Some(col) let sqls1 = f.resolveColumnSqls(addOp, d, None) @Expect(sqls1.size, 1) @Expect(sqls1[0], "ALTER TABLE \"users\" ADD COLUMN \"email\" VARCHAR(255)") // 有同名列且类型一致 → 幂等跳过(0 条) let sqls2 = f.resolveColumnSqls(addOp, d, Some("character varying")) @Expect(sqls2.size, 0) // 有同名列但类型不同 → 先删后加(2 条) let sqls3 = f.resolveColumnSqls(addOp, d, Some("bigint")) @Expect(sqls3.size, 2) @Expect(sqls3[0], "ALTER TABLE \"users\" DROP COLUMN \"email\"") @Expect(sqls3[1], "ALTER TABLE \"users\" ADD COLUMN \"email\" VARCHAR(255)") // DateTime 列:库中为 BIGINT → 先删后加;库中已是 timestamp → 跳过 let tsOp = MigrationOperation(MigrationOperationKind.AddColumn) tsOp.tableName = "t" let ts = ColumnDefinition("created_at", ColumnTypes.DateTimeCol) tsOp.column = Some(ts) let sqls4 = f.resolveColumnSqls(tsOp, d, Some("bigint")) @Expect(sqls4.size, 2) @Expect(sqls4[0], "ALTER TABLE \"t\" DROP COLUMN \"created_at\"") let sqls5 = f.resolveColumnSqls(tsOp, d, Some("timestamp without time zone")) @Expect(sqls5.size, 0) // 类型归一化:大小写/长度精度不影响比较 @Expect(DdlFactory.normalizeTypeName("VARCHAR(100)"), "varchar") @Expect(DdlFactory.normalizeTypeName("BigInt"), "bigint") @Expect(DdlFactory.normalizeTypeName("timestamp without time zone"), "timestamp") @Expect(DdlFactory.normalizeTypeName("DECIMAL(18, 6)"), "decimal") } } // ---------- MigrationBuilder 链式调用 ---------- @Test class MigrationBuilderTests { @TestCase public func testChainedCalls(): Unit { // 链式调用 API 面验证:整条链执行不抛异常(内部 operations 由 DdlFactory/Migrator 消费) let ok = try { let b = MigrationBuilder() b.createTable("t1") { tb => tb.column("id", ColumnTypes.BigIntCol).primary().autoInc().notNull() tb.column("name", ColumnTypes.TextCol).withMaxLength(64).withDefault("none") } let idxCols = ArrayList() idxCols.add("name") b.createIndex("ix_t1_name", "t1", idxCols) b.addColumn("t1", ColumnDefinition("age", ColumnTypes.IntCol).withDefault(Int64(0))) b.dropColumn("t1", "age") b.alterColumn("t1", ColumnDefinition("name", ColumnTypes.TextCol).withMaxLength(128)) b.renameColumn("t1", "name", "nick") b.dropIndex("ix_t1_name", "t1") b.dropTable("t1") b.rawSql("VACUUM") true } catch (_: Exception) { false } @Expect(ok, true) } } // ---------- QueryBuilder(假 executor 捕获 SQL) ---------- @Test class QueryBuilderTests { @TestCase public func testFilterAndSelect(): Unit { let model = ModelCache.get() let dialect = OpenGaussDialect() let captured = ArrayList() let paramsCaptured = ArrayList>() let qb = QueryBuilder(model, dialect, { sql, params => captured.add(sql); paramsCaptured.add(params); ArrayList() }, { sql, params => 42 }) qb.filter("age", ">", Int64(18)) qb.filter("name", "=", "alice") let list = qb.toList() @Expect(list.size, 0) @Expect(captured.size, 1) @Expect(captured[0], "SELECT \"id\", \"name\", \"age\" FROM \"users\" WHERE (\"age\" > ?) AND (\"name\" = ?)") @Expect(paramsCaptured[0].size, 2) let p0 = (paramsCaptured[0][0] as Int64).getOrThrow() @Expect(p0, 18) let p1 = (paramsCaptured[0][1] as String).getOrThrow() @Expect(p1, "alice") } @TestCase public func testRawFilter(): Unit { let model = ModelCache.get() let dialect = OpenGaussDialect() let captured = ArrayList() let qb = QueryBuilder(model, dialect, { sql, params => captured.add(sql); ArrayList() }, { sql, params => 42 }) let ps = ArrayList() ps.add(Int64(18)) ps.add(true) qb.filter("age > ? AND active = ?", ps) qb.toList() @Expect(captured[0], "SELECT \"id\", \"name\", \"age\" FROM \"users\" WHERE (age > ? AND active = ?)") } @TestCase public func testOrderPaging(): Unit { let model = ModelCache.get() let dialect = OpenGaussDialect() let captured = ArrayList() let qb = QueryBuilder(model, dialect, { sql, params => captured.add(sql); ArrayList() }, { sql, params => 42 }) qb.filter("age", ">", Int64(18)) qb.orderBy("name") qb.orderByDesc("age") qb.skip(10) qb.take(5) qb.toList() @Expect(captured[0], "SELECT \"id\", \"name\", \"age\" FROM \"users\" WHERE (\"age\" > ?) ORDER BY \"name\" ASC, \"age\" DESC LIMIT 5 OFFSET 10") } @TestCase public func testCount(): Unit { let model = ModelCache.get() let dialect = OpenGaussDialect() let captured = ArrayList() let qb = QueryBuilder(model, dialect, { sql, params => captured.add(sql); ArrayList() }, { sql, params => captured.add(sql); 42 }) qb.filter("age", ">", Int64(18)) let n = qb.count() @Expect(n, 42) @Expect(captured.size, 1) @Expect(captured[0], "SELECT COUNT(*) FROM \"users\" WHERE (\"age\" > ?)") } @TestCase public func testFirstAndPage(): Unit { let model = ModelCache.get() let dialect = OpenGaussDialect() let captured = ArrayList() let qb = QueryBuilder(model, dialect, { sql, params => captured.add(sql); ArrayList() }, { sql, params => captured.add(sql); 42 }) // first() 空结果 → None let first = qb.filter("age", ">", Int64(18)).first() @Expect(first.isNone(), true) @Expect(captured[0].contains("LIMIT 1"), true) // page(2, 10):先 count 后 select,totalPages let result = qb.filter("age", ">", Int64(18)).page(2, 10) @Expect(result.total, 42) @Expect(result.page, 2) @Expect(result.pageSize, 10) @Expect(result.totalPages(), 5) @Expect(result.items.size, 0) @Expect(captured[1].startsWith("SELECT COUNT(*)"), true) @Expect(captured[2].contains("LIMIT 10 OFFSET 10"), true) } } // ---------- 模型生成迁移(MigrationGenerator) ---------- @Table["users"] class UserV1 { public var id: Int64 = 0 public var name: String = "" } @Table["users"] class UserV2 { public var id: Int64 = 0 public var name: String = "" public var age: Int32 = 0 } @Table["items"] class ItemV1 { public var id: Int64 = 0 public var title: String = "" } @Table["items"] class ItemV2 { public var id: Int64 = 0 @MaxLength[100] public var title: String = "" } @Table["extra"] class ExtraTable { public var id: Int64 = 0 } @Table["keys"] class KeyV1 { public var id: Int64 = 0 } @Table["keys"] class KeyV2 { @Key public var code: String = "" } @Test class MigrationGeneratorTests { private func gen(): MigrationGenerator { MigrationGenerator() } private func upSql(m: Migration, dialect: ISqlDialect): ArrayList { let builder = MigrationBuilder() m.up(builder) let f = DdlFactory() let sqls = ArrayList() for (op in builder.getOperations()) { sqls.add(f.toSql(op, dialect)) } sqls } private func downSql(m: Migration, dialect: ISqlDialect): ArrayList { let builder = MigrationBuilder() m.down(builder) let f = DdlFactory() let sqls = ArrayList() for (op in builder.getOperations()) { sqls.add(f.toSql(op, dialect)) } sqls } @TestCase public func testInitialMigration(): Unit { let models = ArrayList() models.add(ModelCache.get()) models.add(ModelCache.get()) let m = gen().initial("20250701000000_InitialCreate", "初始建表", models) let d = OpenGaussDialect() let up = upSql(m, d) @Expect(up.size, 2) @Expect(up[0], "CREATE TABLE IF NOT EXISTS \"users\" (\n" + " \"id\" BIGSERIAL NOT NULL PRIMARY KEY,\n" + " \"name\" VARCHAR(255),\n" + " \"age\" INTEGER\n)") @Expect(up[1], "CREATE TABLE IF NOT EXISTS \"Product\" (\n" + " \"pid\" BIGSERIAL NOT NULL PRIMARY KEY,\n" + " \"title\" VARCHAR(100) NOT NULL,\n" + " \"price\" DOUBLE PRECISION,\n" + " \"active\" BOOLEAN\n)") // down 为 up 逆序反转 let down = downSql(m, d) @Expect(down.size, 2) @Expect(down[0], "DROP TABLE IF EXISTS \"Product\"") @Expect(down[1], "DROP TABLE IF EXISTS \"users\"") } @TestCase public func testDiffAddColumnAndDropTable(): Unit { let oldModels = ArrayList() oldModels.add(ModelCache.get()) oldModels.add(ModelCache.get()) let newModels = ArrayList() newModels.add(ModelCache.get()) let m = gen().diff("20250801000000_AddAge", "新增 age 列并删表", oldModels, newModels) let d = OpenGaussDialect() let up = upSql(m, d) @Expect(up.size, 2) // 已有表加列 @Expect(up[0], "ALTER TABLE \"users\" ADD COLUMN \"age\" INTEGER") // 删除表 @Expect(up[1], "DROP TABLE IF EXISTS \"extra\"") // down 逆序反转:先恢复表,再删列 let down = downSql(m, d) @Expect(down.size, 2) @Expect(down[0], "CREATE TABLE IF NOT EXISTS \"extra\" (\n \"id\" BIGSERIAL NOT NULL PRIMARY KEY\n)") @Expect(down[1], "ALTER TABLE \"users\" DROP COLUMN \"age\"") } @TestCase public func testDiffAlterColumn(): Unit { let oldModels = ArrayList() oldModels.add(ModelCache.get()) let newModels = ArrayList() newModels.add(ModelCache.get()) let m = gen().diff("20250802000000_ChangeTitle", "调整 title 长度", oldModels, newModels) let d = OpenGaussDialect() let up = upSql(m, d) @Expect(up.size, 1) @Expect(up[0], "ALTER TABLE \"items\" ALTER COLUMN \"title\" VARCHAR(100)") let down = downSql(m, d) @Expect(down.size, 1) @Expect(down[0], "ALTER TABLE \"items\" ALTER COLUMN \"title\" VARCHAR(255)") } @TestCase public func testDiffKeyChangeThrows(): Unit { let oldModels = ArrayList() oldModels.add(ModelCache.get()) let newModels = ArrayList() newModels.add(ModelCache.get()) let threw = try { gen().diff("20250803000000_KeyChange", "改主键", oldModels, newModels) false } catch (_: Exception) { true } @Expect(threw, true) } @TestCase public func testColumnTypeMapping(): Unit { @Expect(match (MigrationGenerator.columnTypeFor("String")) { case ColumnTypes.TextCol => true; case _ => false }, true) @Expect(match (MigrationGenerator.columnTypeFor("Bool")) { case ColumnTypes.BoolCol => true; case _ => false }, true) @Expect(match (MigrationGenerator.columnTypeFor("Int8")) { case ColumnTypes.TinyIntCol => true; case _ => false }, true) @Expect(match (MigrationGenerator.columnTypeFor("Int32")) { case ColumnTypes.IntCol => true; case _ => false }, true) @Expect(match (MigrationGenerator.columnTypeFor("Int64")) { case ColumnTypes.BigIntCol => true; case _ => false }, true) @Expect(match (MigrationGenerator.columnTypeFor("Float32")) { case ColumnTypes.RealCol => true; case _ => false }, true) @Expect(match (MigrationGenerator.columnTypeFor("Float64")) { case ColumnTypes.FloatCol => true; case _ => false }, true) @Expect(match (MigrationGenerator.columnTypeFor("DateTime")) { case ColumnTypes.DateTimeCol => true; case _ => false }, true) @Expect(match (MigrationGenerator.columnTypeFor("Decimal")) { case ColumnTypes.DecimalCol => true; case _ => false }, true) @Expect(match (MigrationGenerator.columnTypeFor("Array")) { case ColumnTypes.BinaryCol => true; case _ => false }, true) // Option 按内层类型映射 @Expect(match (MigrationGenerator.columnTypeFor("Option")) { case ColumnTypes.TextCol => true; case _ => false }, true) @Expect(match (MigrationGenerator.columnTypeFor("Option")) { case ColumnTypes.BigIntCol => true; case _ => false }, true) @Expect(match (MigrationGenerator.columnTypeFor("Option")) { case ColumnTypes.DateTimeCol => true; case _ => false }, true) let threw = try { MigrationGenerator.columnTypeFor("Unknown") false } catch (_: Exception) { true } @Expect(threw, true) } @TestCase public func testOptionColumnDdl(): Unit { let models = ArrayList() models.add(ModelCache.get()) let m = gen().initial("20250910000000_OptionTable", "Option 字段建表", models) let d = OpenGaussDialect() let up = upSql(m, d) @Expect(up.size, 1) // 主键列 NOT NULL;Option 列 nullable(无 NOT NULL) @Expect(up[0].contains("\"id\" BIGSERIAL NOT NULL PRIMARY KEY"), true) @Expect(up[0].contains("\"nick\" VARCHAR(255)"), true) @Expect(up[0].contains("\"nick\" VARCHAR(255) NOT NULL"), false) } @TestCase public func testEnumColumnDdl(): Unit { // 枚举列按构造器名存 VARCHAR;Option<枚举> 列 nullable let models = ArrayList() models.add(ModelCache.get()) let m = gen().initial("20250911000000_EnumTable", "枚举字段建表", models) let d = OpenGaussDialect() let up = upSql(m, d) @Expect(up.size, 1) @Expect(up[0].contains("\"kind\" VARCHAR(255)"), true) @Expect(up[0].contains("\"optKind\" VARCHAR(255)"), true) @Expect(up[0].contains("\"optKind\" VARCHAR(255) NOT NULL"), false) } @TestCase public func testEnumSnapshotRoundTrip(): Unit { // 快照保留 isEnum 标记:diff 时枚举列能正确识别为 TextCol let ms = ArrayList() ms.add(ModelCache.get()) let snap = ModelSnapshot.capture(ms) let json = snap.toJson() @Expect(json.contains("\"enum\":true"), true) let back = ModelSnapshot.fromJson(json) @Expect(back.models[0].properties[1].isEnum, true) // 轻量模型 + 反射模型 diff:无变化 → 空操作 let m = gen().diff("20250912000000_NoChange", "无变化", back.toModels(), ms) @Expect(upSql(m, OpenGaussDialect()).size, 0) } @TestCase public func testStringToEnumNoAlter(): Unit { // String 字段改为枚举:DB 层都是 VARCHAR → 无需 alter let oldModels = ArrayList() oldModels.add(ModelCache.get()) let curModels = ArrayList() curModels.add(ModelCache.get()) let m = gen().diff("20250913000000_TypeChange", "String→enum", oldModels, curModels) // users 表被删、EnumEntity 表新增(表名不同),此用例只验证 columnDefinition 不抛异常 let d = OpenGaussDialect() let sqls = upSql(m, d) @Expect(sqls.size, 2) } } // ---------- PostgreSQL 方言 ---------- @Test class PostgreSqlDialectTests { @TestCase public func testPgInsertReturning(): Unit { let d = PostgreSqlDialect() let cols = ArrayList() cols.add("name") cols.add("age") @Expect(d.buildInsert("users", cols, "id", true), "INSERT INTO \"users\" (\"name\", \"age\") VALUES (?, ?) RETURNING \"id\"") @Expect(d.buildInsert("users", cols, "id", false), "INSERT INTO \"users\" (\"name\", \"age\") VALUES (?, ?)") } @TestCase public func testPgCrudSql(): Unit { let d = PostgreSqlDialect() let cols = ArrayList() cols.add("name") @Expect(d.buildUpdate("users", cols, "id"), "UPDATE \"users\" SET \"name\" = ? WHERE \"id\" = ?") @Expect(d.buildDelete("users", "id"), "DELETE FROM \"users\" WHERE \"id\" = ?") @Expect(d.buildCount("users", ""), "SELECT COUNT(*) FROM \"users\"") @Expect(d.buildSelect("users", ArrayList(), "\"age\" > ?", "\"id\"", 10, 5), "SELECT * FROM \"users\" WHERE \"age\" > ? ORDER BY \"id\" LIMIT 5 OFFSET 10") } @TestCase public func testPgDdlSql(): Unit { let d = PostgreSqlDialect() @Expect(d.columnTypeSql(ColumnTypes.BigIntCol, true, 0), "BIGSERIAL") @Expect(d.columnTypeSql(ColumnTypes.IntCol, true, 0), "SERIAL") @Expect(d.columnTypeSql(ColumnTypes.TextCol, false, 100), "VARCHAR(100)") @Expect(d.columnTypeSql(ColumnTypes.TextCol, false, 0), "VARCHAR(255)") @Expect(d.columnTypeSql(ColumnTypes.BinaryCol, false, 0), "BYTEA") @Expect(d.buildAlterColumn("users", "VARCHAR(100)"), "ALTER TABLE \"users\" ALTER COLUMN VARCHAR(100)") @Expect(d.buildDropIndex("ix_users_name", "users"), "DROP INDEX IF EXISTS \"ix_users_name\"") } @TestCase public func testOpenGaussInheritsPg(): Unit { let d = OpenGaussDialect() let cols = ArrayList() cols.add("name") @Expect(d.buildInsert("users", cols, "id", true), "INSERT INTO \"users\" (\"name\") VALUES (?) RETURNING \"id\"") } @TestCase public func testPgCreateDatabaseSql(): Unit { let d = PostgreSqlDialect() @Expect(d.createDatabaseSql("mydb"), "CREATE DATABASE \"mydb\"") @Expect(d.createDatabaseSql("my_db"), "CREATE DATABASE \"my_db\"") @Expect(d.databaseExistsSql(), "SELECT 1 FROM pg_database WHERE datname = ?") // openGauss 继承 PG let og = OpenGaussDialect() @Expect(og.createDatabaseSql("mydb"), "CREATE DATABASE \"mydb\"") } } // ---------- 应用继承 DbContext ---------- class FakeDatasource <: Datasource { public func setOption(_: String, _: String): Unit {} public func connect(): Connection { throw Exception("FakeDatasource 不支持真实连接") } public func isClosed(): Bool { false } public func close(): Unit {} public func ping(): Unit {} } class AppDbContext <: DbContext { let users: DbSet let orders: DbSet init(ds: Datasource) { super(ds) users = set() orders = set() } init(ds: Datasource, dialect: ISqlDialect) { super(ds, dialect) users = set() orders = set() } } @Test class DbContextInheritTests { @TestCase public func testInheritAndDbSets(): Unit { let ctx = AppDbContext(FakeDatasource()) // 继承后暴露的 DbSet 属性可用 let users = ctx.users let orders = ctx.orders @Expect(users is DbSet, true) @Expect(orders is DbSet, true) // DbSet 操作连通到 context 的变更队列 users.add(User()) @Expect(ctx.pendingCount(), 1) orders.add(Order()) @Expect(ctx.pendingCount(), 2) // 默认方言为 openGauss(继承 PG) @Expect(ctx.getDialect().quoteName("users"), "\"users\"") } @TestCase public func testCustomDialectAndMigrateApi(): Unit { let ctx = AppDbContext(FakeDatasource(), PostgreSqlDialect()) @Expect(ctx.getDialect().quoteName("users"), "\"users\"") @Expect(ctx.pendingCount(), 0) // migrate 委托 Migrator;FakeDatasource 连接时会抛异常 let threw = try { ctx.migrate(ArrayList()) false } catch (_: Exception) { true } @Expect(threw, true) } } // ---------- 模型快照 ---------- @Test class ModelSnapshotTests { private func gen(): MigrationGenerator { MigrationGenerator() } private func models(): ArrayList { let ms = ArrayList() ms.add(ModelCache.get()) ms.add(ModelCache.get()) ms } private func upSql(m: Migration, dialect: ISqlDialect): ArrayList { let builder = MigrationBuilder() m.up(builder) let f = DdlFactory() let sqls = ArrayList() for (op in builder.getOperations()) { sqls.add(f.toSql(op, dialect)) } sqls } @TestCase public func testCaptureToJsonRoundTrip(): Unit { let snap = ModelSnapshot.capture(models()) let json = snap.toJson() // 关键元数据落到 JSON @Expect(json.contains("\"table\":\"users\""), true) @Expect(json.contains("\"column\":\"id\""), true) @Expect(json.contains("\"type\":\"Int64\""), true) @Expect(json.contains("\"key\":true"), true) @Expect(json.contains("\"maxLen\":100"), true) @Expect(json.contains("\"required\":true"), true) let back = ModelSnapshot.fromJson(json) @Expect(back.models.size, 2) @Expect(back.models[0].tableName, "users") @Expect(back.models[1].tableName, "Product") // 轻量模型无反射句柄,typeName 走 typeNameOverride let lm = back.models[0] @Expect(lm.typeInfo.isNone(), true) @Expect(lm.keyProperty.getOrThrow().columnName, "id") @Expect(lm.keyProperty.getOrThrow().autoIncrement, true) // Product:类型 / 长度 / 必填 元数据保留 let pm = back.models[1] @Expect(pm.properties[0].columnName, "pid") @Expect(pm.properties[0].typeName(), "Int64") @Expect(pm.properties[1].columnName, "title") @Expect(pm.properties[1].typeName(), "String") @Expect(pm.properties[1].maxLength, 100) @Expect(pm.properties[1].isRequired, true) } @TestCase public func testSaveLoadFile(): Unit { let path = "snapshot_test_tmp.json" if (exists(Path(path))) { remove(Path(path)) } ModelSnapshot.capture(models()).save(path) @Expect(exists(Path(path)), true) let loaded = ModelSnapshot.load(path) @Expect(loaded.isSome(), true) @Expect(loaded.getOrThrow().models.size, 2) @Expect(loaded.getOrThrow().models[1].properties[1].maxLength, 100) // 文件不存在 → None if (exists(Path(path))) { remove(Path(path)) } @Expect(ModelSnapshot.load(path).isNone(), true) } @TestCase public func testSnapshotModelsWorkWithDiff(): Unit { let snap = ModelSnapshot.capture(models()) // 快照轻量模型 vs 反射模型:无变化 → 空操作 let m = gen().diff("20250901000000_NoChange", "无变化", snap.toModels(), models()) let d = OpenGaussDialect() @Expect(upSql(m, d).size, 0) } @TestCase public func testEnsureInitialThenNoChange(): Unit { let path = "snapshot_test_tmp.json" if (exists(Path(path))) { remove(Path(path)) } let d = OpenGaussDialect() // 首次:快照不存在 → initial 全量建表 + 保存快照 let m1 = gen().ensure("20250901000000_Initial", "初始建表", path, models()) let up1 = upSql(m1, d) @Expect(up1.size, 2) @Expect(up1[0].startsWith("CREATE TABLE IF NOT EXISTS \"users\""), true) @Expect(exists(Path(path)), true) @Expect(ModelSnapshot.load(path).getOrThrow().models.size, 2) // 二次:模型未变 → diff 空操作(依赖已存快照,无需调用方手存旧模型) let m2 = gen().ensure("20250902000000_NoChange", "无变化", path, models()) @Expect(upSql(m2, d).size, 0) if (exists(Path(path))) { remove(Path(path)) } } @TestCase public func testEnsureDiffWithNewTable(): Unit { let path = "snapshot_test_tmp.json" if (exists(Path(path))) { remove(Path(path)) } let d = OpenGaussDialect() gen().ensure("20250901000000_Initial", "初始建表", path, models()) // 模型新增 items 表后二次 ensure → 自动 diff 出 CreateTable let ms2 = models() ms2.add(ModelCache.get()) let m = gen().ensure("20250903000000_AddItems", "新增 items", path, ms2) let up = upSql(m, d) @Expect(up.size, 1) @Expect(up[0].startsWith("CREATE TABLE IF NOT EXISTS \"items\""), true) // 快照已覆盖为 3 个模型 @Expect(ModelSnapshot.load(path).getOrThrow().models.size, 3) if (exists(Path(path))) { remove(Path(path)) } } }