From ed082b05f92f7e00ad0ab317b8e71ef650868504 Mon Sep 17 00:00:00 2001 From: xRain Date: Wed, 2 Sep 2026 17:54:56 +0800 Subject: [PATCH] =?UTF-8?q?DbContext=E4=B8=AD=E5=8F=AA=E6=94=B9=E5=86=99le?= =?UTF-8?q?t=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + README.md | 24 +++--- cjpm.toml | 2 +- src/macros/db_context_macro.cj | 36 ++++++--- src/tests/db_context_macro_test.cj | 118 +++++++++++++++++++++++++++++ 5 files changed, 161 insertions(+), 22 deletions(-) create mode 100644 src/tests/db_context_macro_test.cj diff --git a/.gitignore b/.gitignore index d289f57..05c6c7c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ src/macros/*.dll src/tests/*.cj.macrocall # 临时/日志 *.log +.arts/ +.cache/ +.codegraph/ \ No newline at end of file diff --git a/README.md b/README.md index 58c514b..6d9c068 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ public class User { ### 2. 定义 DbContext 并增删改查 -用 `@DbContext` 宏(推荐):纯声明类即可,宏自动补 `<: DbContext`、把每个 `DbSet` 声明变成 `public prop` 并注入 `this.set()`、生成 `(driverName, connStr)` 构造与 `migrations()` override: +用 `@DbContext` 宏(推荐):纯声明类即可,宏自动补 `<: DbContext`、把每个 **`let` 前缀**的 `DbSet` 字段改写成 `public prop` 并注入 `this.set()`(`var` / `prop` 前缀一律不改动,交给用户自行管理)、生成 `(driverName, connStr)` 构造与 `migrations()` override: ```cangjie import std.database.sql.* @@ -52,8 +52,8 @@ import simcu::orm.macros.* @DbContext public class AppDbContext { - prop users: DbSet - prop orders: DbSet + let users: DbSet + let orders: DbSet } main() { @@ -190,8 +190,8 @@ import simcu::orm.macros.* @DbContext public class MyDbContext { - prop users: DbSet - prop orders: DbSet + let users: DbSet + let orders: DbSet } main(args: Array) { @@ -324,7 +324,7 @@ orm-cj/ ### 3. DbContext / DbSet -**推荐:`@DbContext` 宏**。把 `DbSet` 声明为 `prop`,宏自动补继承/构造/注入/`migrations()`: +**推荐:`@DbContext` 宏**。把 `DbSet` 声明为 **`let` 前缀**(`let xxx: DbSet`),宏只处理这种形式:改写成 `public prop xxx: DbSet { get() { this.set() } }`(public getter 保证反射可枚举);`var` / `prop` 前缀一律不改动。同时自动补继承/构造/`migrations()`: ```cangjie import simcu::orm.* @@ -334,8 +334,8 @@ import std.collection.* // migrations() 签名需要 ArrayList @DbContext public class AppDbContext { - prop users: DbSet - prop orders: DbSet + let users: DbSet + let orders: DbSet } // 用法(宏生成 (driverName, connStr) 构造) @@ -354,8 +354,12 @@ if (!db.databaseExists() || db.hasPendingMigrations()) { db.migrate() } ```cangjie public class AppDbContext <: DbContext { - public let users: DbSet - public let orders: DbSet + public prop users: DbSet { + get() { this.set() } + } + public prop orders: DbSet { + get() { this.set() } + } public init(driverName: String, connStr: String) { super(driverName, connStr) } public init(datasource: Datasource) { super(datasource) } diff --git a/cjpm.toml b/cjpm.toml index 3cd29c7..7ade589 100644 --- a/cjpm.toml +++ b/cjpm.toml @@ -3,7 +3,7 @@ cjc-version = "1.1.3" name = "orm" organization = "simcu" description = "SimApi 数据访问层 ORM:数据模型映射 + 增删改查 + 数据库迁移;多方言架构,内置 openGauss/PostgreSQL 方言" -version = "1.4.0" +version = "1.4.1" target-dir = "" output-type = "static" diff --git a/src/macros/db_context_macro.cj b/src/macros/db_context_macro.cj index 743d02a..fd03c09 100644 --- a/src/macros/db_context_macro.cj +++ b/src/macros/db_context_macro.cj @@ -5,13 +5,14 @@ * 用法(对齐 EF Core 声明式体验): * @DbContext * public class DataContext { - * public var users: DbSet - * public var bots: DbSet + * let users: DbSet + * let bots: DbSet * } * * 展开行为: * - 自动补 `<: DbContext`(标注的类不能自带继承) - * - 每个 DbSet 字段/无 getter prop -> `public prop name: DbSet { get() { this.set() } }` + * - 只处理「let 前缀」的 DbSet 字段 -> `public prop name: DbSet { get() { this.set() } }`; + * var / prop 一律不改动(交给用户自行管理) * - 自动生成 `init(driverName: String, connStr: String)` 调用 super * - 自动生成 `migrations()` override,返回 orm-cj 全局注册表 allMigrations() * (CLI 生成的 src/migrations/MigrationRegistry.cj 在模块加载时自动注册; @@ -44,24 +45,21 @@ public macro DbContext(input: Tokens): Tokens { cd.superTypes.add(RefType(cangjieLex("DbContext"))) cd.upperBound = Token(TokenKind.UPPERBOUND) - // 2. DbSet 字段/无 getter prop -> public prop { get() { this.set() } } + // 2. 只处理「let 前缀」的 DbSet 字段 -> `public prop { get() { this.set() } }`; + // var / prop 一律不改动(交给用户自行管理) let newDecls = ArrayList() for (d in cd.body.decls) { match (d) { case vd: VarDecl => let typeName = vd.declType.toTokens().toString() - if (isDbSetType(typeName)) { + if (isDbSetType(typeName) && declIsLet(vd.toTokens().toString())) { newDecls.add(buildSetProp(vd.identifier.value, typeName)) } else { newDecls.add(d) } case pd: PropDecl => - let typeName = pd.declType.toTokens().toString() - if (isDbSetType(typeName)) { - newDecls.add(buildSetProp(pd.identifier.value, typeName)) - } else { - newDecls.add(d) - } + // prop 一律不改动 + newDecls.add(d) case _ => newDecls.add(d) } } @@ -86,6 +84,22 @@ private func isDbSetType(t: String): Bool { t.indexOf("DbSet") == Some(0) && t.indexOf("<").getOrThrow() > 0 } +/// 判断声明关键字是否为 let(保留字 let/var 只会独立出现在声明词位置, +/// 按词边界匹配避免误伤类型名/泛型实参里的子串)。 +/// prop 是 PropDecl(非 VarDecl),根本不会走到这里。 +private func declIsLet(t: String): Bool { + hasWord(t, "let") && !hasWord(t, "var") +} + +/// 判断字符串中 w 是否作为独立词出现(textified 输出用单个空格分隔词) +private func hasWord(s: String, w: String): Bool { + let padded = " " + s + " " + match (padded.indexOf(" " + w + " ")) { + case Some(_) => true + case None => false + } +} + /// 生成 `public prop name: DbSet { get() { this.set() } }` private func buildSetProp(name: String, typeName: String): Decl { let inner = extractInnerType(typeName) diff --git a/src/tests/db_context_macro_test.cj b/src/tests/db_context_macro_test.cj new file mode 100644 index 0000000..00fb8a1 --- /dev/null +++ b/src/tests/db_context_macro_test.cj @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2025 SimcuTeam. All rights reserved. + * @DbContext 宏回归测试:只处理「let 前缀」的 DbSet 字段,var/prop 一律不改动。 + * + * 通过注册一个 FakeDriver,让宏生成的 `init(driverName, connStr)` 走通 + * `super(...) -> DatasourceFactory.create(connStr, driverName)` 这条真实路径, + * 从而验证:let 字段被改写为 `public prop { get() { this.set() } }`(public 可被 + * std.reflect 枚举),getModels() 能反射采集到模型;手写 prop DbSet 保持原样不被改写。 + */ + +package simcu::orm.tests + +import std.collection.* +import std.database.sql.* +import std.time.* +import std.unittest.* +import std.unittest.testmacro.* +import simcu::orm.* +import simcu::orm.macros.* + +// ---------- 复用 sim_orm_test.cj 的实体 ---------- +// User / Order 已在同包定义,这里直接引用。 + +// ---------- 假驱动 ---------- +class FakeDriver <: Driver { + public prop name: String { + get() { "fake" } + } + public prop version: String { + get() { "0.0.1" } + } + public prop preferredPooling: Bool { + get() { false } + } + public func open(_: String, _: Array<(String, String)>): Datasource { + FakeDatasource() + } + public func getDriverStartTime(): DateTime { + DateTime.now() + } +} + +let _ = { + => + DriverManager.register("fake", FakeDriver()) +}() + +// ---------- 宏应用:仅含 let 字段 ---------- +@DbContext +class MacroCtx { + let users: DbSet + let orders: DbSet +} + +// ---------- 宏应用:let 与手写 prop 混用 ---------- +// let 字段被宏改写为 public prop;手写 prop(含 getter)保持原样、不改写。 +@DbContext +class MixedCtx { + let users: DbSet + prop todos: DbSet { + get() { this.set() } + } +} + +@Test +public class DbContextMacroTests { + @TestCase + public func testMacroGeneratesInitAndDbSets(): Unit { + let ctx = MacroCtx("fake", "postgres://localhost/db") + // 宏生成的 init 已把 let 字段初始化为 set(),可直接使用 + 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 testMacroModelsCollectedFromLetFields(): Unit { + let ctx = MacroCtx("fake", "postgres://localhost/db") + // 宏把 let DbSet 字段统一提升为 public,getModels() 反射可采集到模型 + let models = ctx.getModels() + @Expect(models.size, 2) + // 模型采集自 let 字段(users -> User, orders -> Order) + var gotUser = false + var gotOrder = false + for (m in models) { + if (m.tableName == "users") { + gotUser = true + } + if (m.tableName == "orders") { + gotOrder = true + } + } + @Expect(gotUser, true) + @Expect(gotOrder, true) + } + + @TestCase + public func testPropDbSetIsNotRewritten(): Unit { + let ctx = MixedCtx("fake", "postgres://localhost/db") + // let 字段被改写为 public prop,反射可采集 -> 1 个模型(users) + let models = ctx.getModels() + @Expect(models.size, 1) + // 手写 prop(internal,含 getter)保持原样:可正常使用, + // 但不会被宏改写成 public prop,因而不会重复进入模型采集 + let todos = ctx.todos + @Expect(todos is DbSet, true) + todos.add(Order()) + @Expect(ctx.pendingCount(), 1) + } +}