1149 lines
43 KiB
Plaintext
1149 lines
43 KiB
Plaintext
/*
|
||
* 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<Int32> = Array<Int32>()
|
||
}
|
||
|
||
// ---------- 模型映射 ----------
|
||
|
||
@Test
|
||
class ModelMappingTests {
|
||
@TestCase
|
||
public func testUserModel(): Unit {
|
||
let model = ModelCache.get<User>()
|
||
@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<Order>()
|
||
@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<Product>()
|
||
@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<User>()
|
||
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<NoKeyEntity>(); false } catch (_: Exception) { true }
|
||
@Expect(threw, true)
|
||
}
|
||
|
||
@TestCase
|
||
public func testPlainIdRecognizedAsKey(): Unit {
|
||
let model = ModelCache.get<PlainIdEntity>()
|
||
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<MultiKeyEntity>(); false } catch (_: Exception) { true }
|
||
@Expect(threw, true)
|
||
}
|
||
|
||
@TestCase
|
||
public func testOptionFieldMaps(): Unit {
|
||
// Option 字段现在受支持:映射为可空列
|
||
let model = ModelCache.get<OptionEntity>()
|
||
@Expect(model.properties.size, 2)
|
||
let nick = model.properties[1]
|
||
@Expect(nick.name, "nick")
|
||
@Expect(nick.columnName, "nick")
|
||
@Expect(nick.typeName(), "Option<String>")
|
||
@Expect(nick.isOption(), true)
|
||
@Expect(nick.effectiveTypeName(), "String")
|
||
@Expect(nick.isRequired, false)
|
||
}
|
||
|
||
@TestCase
|
||
public func testEnumFieldMaps(): Unit {
|
||
// 简单枚举(含 Option 内层枚举)受支持:标记 isEnum,记录完整类型名
|
||
let model = ModelCache.get<EnumEntity>()
|
||
@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<Int32> 不在白名单 → 抛异常
|
||
let threw = try { ModelCache.get<UnsupportedArrayEntity>(); 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<String>()
|
||
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<String>()
|
||
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<String>()
|
||
@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<String>()
|
||
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<User>()
|
||
let dialect = OpenGaussDialect()
|
||
let captured = ArrayList<String>()
|
||
let paramsCaptured = ArrayList<ArrayList<Any>>()
|
||
let qb = QueryBuilder<User>(model, dialect,
|
||
{ sql, params => captured.add(sql); paramsCaptured.add(params); ArrayList<User>() },
|
||
{ 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<User>()
|
||
let dialect = OpenGaussDialect()
|
||
let captured = ArrayList<String>()
|
||
let qb = QueryBuilder<User>(model, dialect,
|
||
{ sql, params => captured.add(sql); ArrayList<User>() },
|
||
{ sql, params => 42 })
|
||
let ps = ArrayList<Any>()
|
||
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<User>()
|
||
let dialect = OpenGaussDialect()
|
||
let captured = ArrayList<String>()
|
||
let qb = QueryBuilder<User>(model, dialect,
|
||
{ sql, params => captured.add(sql); ArrayList<User>() },
|
||
{ 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<User>()
|
||
let dialect = OpenGaussDialect()
|
||
let captured = ArrayList<String>()
|
||
let qb = QueryBuilder<User>(model, dialect,
|
||
{ sql, params => captured.add(sql); ArrayList<User>() },
|
||
{ 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<User>()
|
||
let dialect = OpenGaussDialect()
|
||
let captured = ArrayList<String>()
|
||
let qb = QueryBuilder<User>(model, dialect,
|
||
{ sql, params => captured.add(sql); ArrayList<User>() },
|
||
{ 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<String> {
|
||
let builder = MigrationBuilder()
|
||
m.up(builder)
|
||
let f = DdlFactory()
|
||
let sqls = ArrayList<String>()
|
||
for (op in builder.getOperations()) {
|
||
sqls.add(f.toSql(op, dialect))
|
||
}
|
||
sqls
|
||
}
|
||
|
||
private func downSql(m: Migration, dialect: ISqlDialect): ArrayList<String> {
|
||
let builder = MigrationBuilder()
|
||
m.down(builder)
|
||
let f = DdlFactory()
|
||
let sqls = ArrayList<String>()
|
||
for (op in builder.getOperations()) {
|
||
sqls.add(f.toSql(op, dialect))
|
||
}
|
||
sqls
|
||
}
|
||
|
||
@TestCase
|
||
public func testInitialMigration(): Unit {
|
||
let models = ArrayList<EntityModel>()
|
||
models.add(ModelCache.get<User>())
|
||
models.add(ModelCache.get<Product>())
|
||
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<EntityModel>()
|
||
oldModels.add(ModelCache.get<UserV1>())
|
||
oldModels.add(ModelCache.get<ExtraTable>())
|
||
let newModels = ArrayList<EntityModel>()
|
||
newModels.add(ModelCache.get<UserV2>())
|
||
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<EntityModel>()
|
||
oldModels.add(ModelCache.get<ItemV1>())
|
||
let newModels = ArrayList<EntityModel>()
|
||
newModels.add(ModelCache.get<ItemV2>())
|
||
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<EntityModel>()
|
||
oldModels.add(ModelCache.get<KeyV1>())
|
||
let newModels = ArrayList<EntityModel>()
|
||
newModels.add(ModelCache.get<KeyV2>())
|
||
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<Byte>")) { case ColumnTypes.BinaryCol => true; case _ => false }, true)
|
||
// Option<X> 按内层类型映射
|
||
@Expect(match (MigrationGenerator.columnTypeFor("Option<String>")) { case ColumnTypes.TextCol => true; case _ => false }, true)
|
||
@Expect(match (MigrationGenerator.columnTypeFor("Option<Int64>")) { case ColumnTypes.BigIntCol => true; case _ => false }, true)
|
||
@Expect(match (MigrationGenerator.columnTypeFor("Option<DateTime>")) { 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<EntityModel>()
|
||
models.add(ModelCache.get<OptionEntity>())
|
||
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<EntityModel>()
|
||
models.add(ModelCache.get<EnumEntity>())
|
||
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<EntityModel>()
|
||
ms.add(ModelCache.get<EnumEntity>())
|
||
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<EntityModel>()
|
||
oldModels.add(ModelCache.get<User>())
|
||
let curModels = ArrayList<EntityModel>()
|
||
curModels.add(ModelCache.get<EnumEntity>())
|
||
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<String>()
|
||
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<String>()
|
||
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<String>(), "\"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<String>()
|
||
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<User>
|
||
let orders: DbSet<Order>
|
||
|
||
init(ds: Datasource) {
|
||
super(ds)
|
||
users = set<User>()
|
||
orders = set<Order>()
|
||
}
|
||
|
||
init(ds: Datasource, dialect: ISqlDialect) {
|
||
super(ds, dialect)
|
||
users = set<User>()
|
||
orders = set<Order>()
|
||
}
|
||
}
|
||
|
||
@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<User>, true)
|
||
@Expect(orders is DbSet<Order>, 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<Migration>())
|
||
false
|
||
} catch (_: Exception) {
|
||
true
|
||
}
|
||
@Expect(threw, true)
|
||
}
|
||
}
|
||
|
||
// ---------- 模型快照 ----------
|
||
|
||
@Test
|
||
class ModelSnapshotTests {
|
||
private func gen(): MigrationGenerator {
|
||
MigrationGenerator()
|
||
}
|
||
|
||
private func models(): ArrayList<EntityModel> {
|
||
let ms = ArrayList<EntityModel>()
|
||
ms.add(ModelCache.get<User>())
|
||
ms.add(ModelCache.get<Product>())
|
||
ms
|
||
}
|
||
|
||
private func upSql(m: Migration, dialect: ISqlDialect): ArrayList<String> {
|
||
let builder = MigrationBuilder()
|
||
m.up(builder)
|
||
let f = DdlFactory()
|
||
let sqls = ArrayList<String>()
|
||
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<ItemV2>())
|
||
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)) }
|
||
}
|
||
}
|