Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed082b05f9 | ||
|
|
74eb8b576e | ||
|
|
7947a4c8f4 | ||
|
|
401e6a9bf2 | ||
|
|
985473b8a7 |
No files matched your search
@@ -6,3 +6,6 @@ src/macros/*.dll
|
||||
src/tests/*.cj.macrocall
|
||||
# 临时/日志
|
||||
*.log
|
||||
.arts/
|
||||
.cache/
|
||||
.codegraph/
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
> 仓颉版 ORM:**基于数据模型(POCO + 注解)的映射、增删改查、数据库迁移**。
|
||||
|
||||
面向多数据库的方言架构:内置 PostgreSQL(`PostgreSqlDialect`)与 openGauss(`OpenGaussDialect`,继承 PG)方言(对接 [opengauss-driver](../opengauss-driver)),可通过实现 `ISqlDialect` 接口接入 sqlite / mysql 等新数据库。包本身零外部依赖。
|
||||
面向多数据库的方言架构:内置 PostgreSQL(`PostgreSqlDialect`)与 openGauss(`OpenGaussDialect`,继承 PG)方言(对接 [opengauss-driver](../opengauss-driver)),可通过实现 `SqlDialect` 接口接入 sqlite / mysql 等新数据库。包本身零外部依赖。
|
||||
|
||||
---
|
||||
|
||||
@@ -42,7 +42,7 @@ public class User {
|
||||
|
||||
### 2. 定义 DbContext 并增删改查
|
||||
|
||||
用 `@DbContext` 宏(推荐):纯声明类即可,宏自动补 `<: DbContext`、把每个 `DbSet<T>` 声明变成 `public prop` 并注入 `this.set<T>()`、生成 `(driverName, connStr)` 构造与 `migrations()` override:
|
||||
用 `@DbContext` 宏(推荐):纯声明类即可,宏自动补 `<: DbContext`、把每个 **`let` 前缀**的 `DbSet<T>` 字段改写成 `public prop` 并注入 `this.set<T>()`(`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<User>
|
||||
prop orders: DbSet<Order>
|
||||
let users: DbSet<User>
|
||||
let orders: DbSet<Order>
|
||||
}
|
||||
|
||||
main() {
|
||||
@@ -93,9 +93,9 @@ main() {
|
||||
```cangjie
|
||||
import simcu::orm.migrations.*
|
||||
|
||||
// 定义迁移(migrationId 用 "yyyyMMddHHmmss_名称",按字典序应用)
|
||||
public class InitialCreate <: Migration {
|
||||
public init() { super("20250701000000_InitialCreate", "创建用户表") }
|
||||
// 定义迁移(migrationId 按字典序应用;CLI 生成 "yyyyMMddHHmmss" 时间戳格式,类名 MigrationId<时间戳>)
|
||||
public class MigrationId20250701000000 <: Migration {
|
||||
public init() { super("20250701000000", "创建用户表") }
|
||||
public override func up(builder: MigrationBuilder): Unit {
|
||||
builder.createTable("users") { tb =>
|
||||
tb.column("id", ColumnTypes.BigIntCol).primary().autoInc().notNull()
|
||||
@@ -137,7 +137,7 @@ main() {
|
||||
let models = ArrayList<EntityModel>()
|
||||
models.add(ModelCache.get<User>())
|
||||
models.add(ModelCache.get<Product>())
|
||||
let m0 = gen.initial("20250701000000_InitialCreate", "初始建表", models)
|
||||
let m0 = gen.initial("20250701000000", "初始建表", models)
|
||||
Migrator(datasource).migrate([m0])
|
||||
|
||||
// 2. 模型变更后:旧模型快照(上次的 models) vs 新模型 → 增量迁移(表/列增删改)
|
||||
@@ -190,8 +190,8 @@ import simcu::orm.macros.*
|
||||
|
||||
@DbContext
|
||||
public class MyDbContext {
|
||||
prop users: DbSet<User>
|
||||
prop orders: DbSet<Order>
|
||||
let users: DbSet<User>
|
||||
let orders: DbSet<Order>
|
||||
}
|
||||
|
||||
main(args: Array<String>) {
|
||||
@@ -209,8 +209,8 @@ main(args: Array<String>) {
|
||||
|
||||
| 命令 | 作用 |
|
||||
|------|------|
|
||||
| `cjpm run -- orm add <迁移名>` | 从模型生成迁移 .cj 文件 + 更新注册文件 + 写入快照;模型无变化则跳过 |
|
||||
| `cjpm run -- orm rm <迁移名或id>` | 移除已生成的迁移(删除迁移文件 + 注册条目;注册表清空时重置快照) |
|
||||
| `cjpm run -- orm add [描述]` | 从模型生成迁移 .cj 文件 + 更新注册文件 + 写入快照(描述可空,仅写入注释与历史记录);模型无变化则跳过 |
|
||||
| `cjpm run -- orm rm <迁移id或类名>` | 移除已生成的迁移(删除迁移文件 + 注册条目;注册表清空时重置快照) |
|
||||
| `cjpm run -- orm update` | 连接数据库,按 `migrationId` 字典序应用所有未执行的迁移 |
|
||||
| `cjpm run -- orm downgrade [目标]` | 回退迁移:无目标=回退最近一个;有目标=回退到该迁移之后(含该迁移的 down) |
|
||||
| `cjpm run -- orm list` | 列出已注册迁移(需真实连接读取历史表) |
|
||||
@@ -225,6 +225,7 @@ main(args: Array<String>) {
|
||||
- **首次使用(无迁移)**:不需要建 `src/migrations/` 目录,也不要写 `import <应用包名>.migrations.*`(该子包不存在会导致编译失败);首次 `add` 时 CLI 自动创建目录并生成迁移文件 + 注册文件;
|
||||
- **生成后手动加 import**:执行 `add` 生成首个迁移后,在 `main()` 所在文件手动加一行 `import <应用包名>.migrations.*`(如 `import myapp.migrations.*`)——注册文件在子包内惰性加载,只有 import 触发后 `update/downgrade/list` 才能看到迁移;之后新增迁移无需再动这行;
|
||||
- 迁移 / 注册文件生成在 `src/migrations/`,该目录是**子包**,文件头 `package <应用包名>.migrations`(如应用包 `myapp` → `package myapp.migrations`);
|
||||
- 迁移文件命名为 **id 的蛇形形式**(如 `20250701000000.cj`),仅含 migrationId,不包含描述;CLI 生成的 migrationId 为 `yyyyMMddHHmmss` 时间戳,**迁移类名自动为 `MigrationId<时间戳>`**(如 `MigrationId20250701000000`);迁移描述可空,写入文件头部 `// 描述: ...` 注释与迁移类 `super("...", "...")` 中;
|
||||
- `add` 只生成不执行;执行迁移用 `update`(回退用 `downgrade`),或在应用启动时直接 `db.migrate()`;
|
||||
- 快照持久化在 `src/migrations/snapshot.json`;`add` 用「旧快照 vs 当前模型」做 diff,所以**改模型后重新编译应用再 `add`** 即生成增量迁移;
|
||||
- 生成源码文本已包含 `import std.collection.*` 等标准库引用,应用无需额外配置。
|
||||
@@ -272,14 +273,14 @@ public class InitGames {
|
||||
orm-cj/
|
||||
├── cjpm.toml # 包配置(name = orm, organization = simcu)
|
||||
├── src/
|
||||
│ ├── Orm.cj # 聚合导出:import simcu::orm.* 即全部可见
|
||||
│ ├── orm.cj # 聚合导出:import simcu::orm.* 即全部可见
|
||||
│ ├── annotations/ # @Table / @Column / @Key / @AutoIncrement / @Ignore / @Required / @MaxLength / @Index / @TableIndex
|
||||
│ ├── macros/ # @DbContext 类级宏 + @Seed 数据填充宏(自动注册,无需手写注册表)
|
||||
│ ├── seeds/ # Seed 基类 + 进程级填充注册表(registerSeed / allSeeds)
|
||||
│ ├── model/ # EntityModel(反射映射)、ModelCache(模型缓存)、
|
||||
│ │ # ColumnNamingPolicy(列命名策略)、ValueReader、ParamBinder、GuidUtil
|
||||
│ ├── tracking/ # ChangeTracker(操作队列)、EntityState、EntityEntry
|
||||
│ ├── sql/ # ISqlDialect 接口(独立文件)+ PostgreSqlDialect(PG 实现)+
|
||||
│ ├── sql/ # SqlDialect 接口(独立文件)+ PostgreSqlDialect(PG 实现)+
|
||||
│ │ # OpenGaussDialect(继承 PG)、ColumnTypes(列类型)
|
||||
│ ├── query/ # QueryBuilder<T>(条件/排序/分页)、PagedResult<T>
|
||||
│ ├── db/ # DbContext(连接 + 提交 + 物化 + 数据库存在性/迁移状态检查)、DbSet<T>
|
||||
@@ -323,7 +324,7 @@ orm-cj/
|
||||
|
||||
### 3. DbContext / DbSet
|
||||
|
||||
**推荐:`@DbContext` 宏**。把 `DbSet` 声明为 `prop`,宏自动补继承/构造/注入/`migrations()`:
|
||||
**推荐:`@DbContext` 宏**。把 `DbSet` 声明为 **`let` 前缀**(`let xxx: DbSet<T>`),宏只处理这种形式:改写成 `public prop xxx: DbSet<T> { get() { this.set<T>() } }`(public getter 保证反射可枚举);`var` / `prop` 前缀一律不改动。同时自动补继承/构造/`migrations()`:
|
||||
|
||||
```cangjie
|
||||
import simcu::orm.*
|
||||
@@ -333,8 +334,8 @@ import std.collection.* // migrations() 签名需要 ArrayList
|
||||
|
||||
@DbContext
|
||||
public class AppDbContext {
|
||||
prop users: DbSet<User>
|
||||
prop orders: DbSet<Order>
|
||||
let users: DbSet<User>
|
||||
let orders: DbSet<Order>
|
||||
}
|
||||
|
||||
// 用法(宏生成 (driverName, connStr) 构造)
|
||||
@@ -353,8 +354,12 @@ if (!db.databaseExists() || db.hasPendingMigrations()) { db.migrate() }
|
||||
|
||||
```cangjie
|
||||
public class AppDbContext <: DbContext {
|
||||
public let users: DbSet<User>
|
||||
public let orders: DbSet<Order>
|
||||
public prop users: DbSet<User> {
|
||||
get() { this.set<User>() }
|
||||
}
|
||||
public prop orders: DbSet<Order> {
|
||||
get() { this.set<Order>() }
|
||||
}
|
||||
|
||||
public init(driverName: String, connStr: String) { super(driverName, connStr) }
|
||||
public init(datasource: Datasource) { super(datasource) }
|
||||
@@ -408,7 +413,7 @@ qb.page(2, 10) // PagedResult<User>(items/total/
|
||||
|
||||
### 5. 迁移
|
||||
|
||||
- `Migration` 基类:`super("20250701000000_InitialCreate", "描述")`,实现 `up` / `down`;
|
||||
- `Migration` 基类:`super("20250701000000", "描述")`,实现 `up` / `down`;
|
||||
- `MigrationBuilder`:`createTable` / `dropTable` / `addColumn` / `dropColumn` / `alterColumn` / `renameColumn` / `createIndex` / `dropIndex` / `rawSql`;
|
||||
- `ColumnDefinition` 链式:`.primary()` `.autoInc()` `.notNull()` `.withUnique()` `.withMaxLength(n)` `.withDefault(value)`;
|
||||
- `ColumnTypes`(定义于 `simcu::orm.sql`):`BigIntCol`(openGauss 自增 → BIGSERIAL)/ `IntCol`(自增 → SERIAL)/ `SmallIntCol` / `TinyIntCol` / `TextCol`(VARCHAR,默认 255)/ `BoolCol` / `FloatCol`(DOUBLE PRECISION)/ `RealCol` / `DateTimeCol` / `DecimalCol`(DECIMAL(18,6))/ `BinaryCol`(BYTEA);实际 DDL 映射由方言 `columnTypeSql` 决定;
|
||||
@@ -439,7 +444,7 @@ cjpm test # 纯逻辑单元测试(模型映射/SQL 生成/DDL 生成/QueryB
|
||||
- 不支持导航属性 / 延迟加载 / 级联删除(这些能力属于 v2+);
|
||||
- 不支持 LINQ 表达式树,查询以 `filter` 条件方法 + SQL 片段组合;
|
||||
- `QueryBuilder` 的 SQL 片段形式(`filter(condition, params)`)需自行保证列名合法(会做标识符引号包裹校验外的处理)——推荐优先使用三参属性形式;
|
||||
- 自增主键回读依赖驱动 `RETURNING` 支持(openGauss/PostgreSQL 原生支持;换方言时由 `ISqlDialect.buildInsert` 决定回读策略)。
|
||||
- 自增主键回读依赖驱动 `RETURNING` 支持(openGauss/PostgreSQL 原生支持;换方言时由 `SqlDialect.buildInsert` 决定回读策略)。
|
||||
|
||||
---
|
||||
|
||||
@@ -450,7 +455,7 @@ cjpm test # 纯逻辑单元测试(模型映射/SQL 生成/DDL 生成/QueryB
|
||||
| 数据库驱动(使用者引入,如 `opengauss` → `../opengauss-driver`) | 实现 `std.database.sql` 的 `Datasource`/`Connection`/`Statement` |
|
||||
| `stdx`(CANGJIE_STDX_PATH) | 标准扩展库(std.database.sql 接口) |
|
||||
|
||||
> orm-cj 包本身零依赖:连接层走 `std.database.sql` 标准接口,SQL/DDL 生成走 `ISqlDialect` 方言接口。新数据库接入 = 实现 `ISqlDialect`(CRUD + 列类型映射 + 差异 DDL 语句)+ 提供对应 `std.database.sql` 驱动。
|
||||
> orm-cj 包本身零依赖:连接层走 `std.database.sql` 标准接口,SQL/DDL 生成走 `SqlDialect` 方言接口。新数据库接入 = 实现 `SqlDialect`(CRUD + 列类型映射 + 差异 DDL 语句)+ 提供对应 `std.database.sql` 驱动。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
[package]
|
||||
cjc-version = "1.1.3"
|
||||
name = "orm"
|
||||
organization = "simcu"
|
||||
description = "SimApi 数据访问层 ORM:数据模型映射 + 增删改查 + 数据库迁移;多方言架构,内置 openGauss/PostgreSQL 方言"
|
||||
version = "1.3.0"
|
||||
target-dir = ""
|
||||
output-type = "static"
|
||||
cjc-version = "1.1.3"
|
||||
name = "orm"
|
||||
organization = "simcu"
|
||||
description = "SimApi 数据访问层 ORM:数据模型映射 + 增删改查 + 数据库迁移;多方言架构,内置 openGauss/PostgreSQL 方言"
|
||||
version = "1.4.1"
|
||||
target-dir = ""
|
||||
output-type = "static"
|
||||
|
||||
[target]
|
||||
[target.x86_64-w64-mingw32]
|
||||
compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated"
|
||||
[target.x86_64-w64-mingw32.bin-dependencies]
|
||||
path-option = [ "${CANGJIE_STDX_PATH}" ]
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated"
|
||||
[target.x86_64-unknown-linux-gnu.bin-dependencies]
|
||||
path-option = [ "${CANGJIE_STDX_PATH}" ]
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated"
|
||||
[target.aarch64-unknown-linux-gnu.bin-dependencies]
|
||||
path-option = [ "${CANGJIE_STDX_PATH}" ]
|
||||
[target.x86_64-w64-mingw32]
|
||||
compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated"
|
||||
[target.x86_64-w64-mingw32.bin-dependencies]
|
||||
path-option = ["${CANGJIE_STDX_PATH}"]
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated"
|
||||
[target.x86_64-unknown-linux-gnu.bin-dependencies]
|
||||
path-option = ["${CANGJIE_STDX_PATH}"]
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated"
|
||||
[target.aarch64-unknown-linux-gnu.bin-dependencies]
|
||||
path-option = ["${CANGJIE_STDX_PATH}"]
|
||||
File renamed without changes.
@@ -20,7 +20,7 @@ public interface CliContext {
|
||||
/// 应用主数据源
|
||||
func getDatasource(): Datasource
|
||||
/// SQL 方言
|
||||
func getDialect(): ISqlDialect
|
||||
func getDialect(): SqlDialect
|
||||
/// 迁移列表(子类 override migrations() 提供)
|
||||
func getMigrations(): ArrayList<Migration>
|
||||
/// 应用所有迁移
|
||||
@@ -13,7 +13,7 @@
|
||||
* - 模型:反射收集实例的 DbSet<T> 属性(getModels())
|
||||
* - 连接/驱动/方言:构造参数
|
||||
* - 迁移列表:子类 override migrations()(宏默认返回 orm-cj 全局注册表,
|
||||
* CLI 生成的 src/migrations/MigrationRegistry.cj 在模块加载时自动注册)
|
||||
* CLI 生成的 src/migrations/migration_registry.cj 在模块加载时自动注册)
|
||||
*
|
||||
* 注意:应用侧(DataContext.cj 或 main.cj)需要一行
|
||||
* import <应用包名>.migrations.*
|
||||
@@ -21,8 +21,8 @@
|
||||
* 才会在 main() 前执行并注册迁移;新项目需先建 src/migrations/ 目录(空包)。
|
||||
*
|
||||
* 命令:
|
||||
* add <迁移名> 模型 → 迁移 .cj 文件(落盘) + 更新快照 + 更新迁移注册文件
|
||||
* rm <迁移名/id> 移除已生成的迁移(删除迁移文件 + 注册条目 + 快照回退到前一状态)
|
||||
* add [描述] 模型 → 迁移 .cj 文件(落盘) + 更新快照 + 更新迁移注册文件(描述可空,仅写入注释与历史记录)
|
||||
* rm <迁移id/类名> 移除已生成的迁移(删除迁移文件 + 注册条目 + 快照回退到前一状态)
|
||||
* update 应用所有未执行的迁移
|
||||
* downgrade [目标] 回退迁移(执行 down;无目标=回退最近一个,有目标=回退到该迁移之后)
|
||||
* list 列出已应用/待应用迁移
|
||||
@@ -55,7 +55,7 @@ public class MigrationCli {
|
||||
// 注册文件与迁移文件同目录(src/migrations/,package = 应用包名.migrations)。
|
||||
// 子包默认惰性加载,顶层 let 不会执行;应用侧需一行
|
||||
// `import <应用包名>.migrations.*` 触发加载后,注册才生效。
|
||||
private var _registryPath = "src/migrations/MigrationRegistry.cj"
|
||||
private var _registryPath = "src/migrations/migration_registry.cj"
|
||||
|
||||
public init() {}
|
||||
|
||||
@@ -94,7 +94,7 @@ public class MigrationCli {
|
||||
|
||||
private func dispatch(args: ArrayList<String>, models: () -> ArrayList<EntityModel>,
|
||||
datasource: () -> Datasource, migrations: () -> ArrayList<Migration>,
|
||||
dialect: ISqlDialect, seedCtx: ?Any): Int64 {
|
||||
dialect: SqlDialect, seedCtx: ?Any): Int64 {
|
||||
if (args.size == 0) {
|
||||
printHelp()
|
||||
return 0
|
||||
@@ -122,7 +122,7 @@ public class MigrationCli {
|
||||
|
||||
/// database <子命令>:数据库级操作(不需要迁移注册)
|
||||
private func cmdDatabase(args: ArrayList<String>, datasource: () -> Datasource,
|
||||
dialect: ISqlDialect): Int64 {
|
||||
dialect: SqlDialect): Int64 {
|
||||
if (args.size < 2) {
|
||||
println("用法: simcu::orm database <truncate|drop>")
|
||||
return 1
|
||||
@@ -137,7 +137,7 @@ public class MigrationCli {
|
||||
}
|
||||
|
||||
/// database truncate:清空所有业务表数据(保留表结构与迁移历史)
|
||||
private func cmdTruncate(datasource: () -> Datasource, dialect: ISqlDialect): Int64 {
|
||||
private func cmdTruncate(datasource: () -> Datasource, dialect: SqlDialect): Int64 {
|
||||
try {
|
||||
let migrator = Migrator(datasource(), dialect)
|
||||
let n = migrator.truncateAll()
|
||||
@@ -151,7 +151,7 @@ public class MigrationCli {
|
||||
}
|
||||
|
||||
/// database drop:删除所有表(含迁移历史),完全清空数据库;之后可 update 重建
|
||||
private func cmdDrop(datasource: () -> Datasource, dialect: ISqlDialect): Int64 {
|
||||
private func cmdDrop(datasource: () -> Datasource, dialect: SqlDialect): Int64 {
|
||||
try {
|
||||
let migrator = Migrator(datasource(), dialect)
|
||||
let n = migrator.dropAllTables()
|
||||
@@ -164,33 +164,29 @@ public class MigrationCli {
|
||||
}
|
||||
}
|
||||
|
||||
/// add <迁移名>:模型 → 迁移文件 + 快照 + 注册
|
||||
/// add [描述]:模型 → 迁移文件 + 快照 + 注册。
|
||||
/// 描述可空(多词自动拼接),仅写入迁移文件头注释与迁移历史记录,不参与命名;
|
||||
/// migrationId = 时间戳(如 20250701000000),类名自动生成 MigrationId<时间戳>。
|
||||
private func cmdAdd(args: ArrayList<String>, models: () -> ArrayList<EntityModel>): Int64 {
|
||||
if (args.size < 2) {
|
||||
println("用法: simcu::orm add <迁移名>")
|
||||
return 1
|
||||
var description = ""
|
||||
if (args.size >= 2) {
|
||||
for (i in 1..args.size) {
|
||||
if (i > 1) {
|
||||
description = description + " "
|
||||
}
|
||||
let name = args[1]
|
||||
if (!isValidIdentifier(name)) {
|
||||
println("simcu::orm: 迁移名 '${name}' 不是合法标识符(字母/数字/下划线,首字符不能是数字)")
|
||||
return 1
|
||||
description = description + args[i]
|
||||
}
|
||||
// 迁移名以 test 结尾会生成 *_test.cj 文件,cjc 会把 *_test.cj 当作测试文件,
|
||||
// 在 cjpm build(非 test)时排除,导致注册文件引用不到该类而编译失败。
|
||||
if (name.endsWith("test")) {
|
||||
println("simcu::orm: 迁移名 '${name}' 以 test 结尾,生成的 *_test.cj 会被编译器当作测试文件排除;请换一个名称")
|
||||
return 1
|
||||
}
|
||||
let appPkg = detectPackage()
|
||||
let ts = DateTime.now().format("yyyyMMddHHmmss")
|
||||
let id = "${ts}_${name}"
|
||||
let id = ts
|
||||
let gen = MigrationGenerator()
|
||||
let current = models()
|
||||
var m: Migration
|
||||
if (let Some(snap) <- ModelSnapshot.load(_snapshotPath)) {
|
||||
m = gen.diff(id, name, snap.toModels(), current)
|
||||
m = gen.diff(id, description, snap.toModels(), current)
|
||||
} else {
|
||||
m = gen.initial(id, name, current)
|
||||
m = gen.initial(id, description, current)
|
||||
}
|
||||
// 无变化检测(对齐 dotnet ef:模型未变则不生成)
|
||||
let probe = MigrationBuilder()
|
||||
@@ -205,9 +201,11 @@ public class MigrationCli {
|
||||
let migPkg = "${appPkg}.migrations"
|
||||
let fg = MigrationFileGenerator()
|
||||
let path = fg.writeMigration(migPkg, m, _migrationsDir)
|
||||
fg.updateRegistry(migPkg, _registryPath, MigrationFileGenerator.classNameOf(id))
|
||||
let className = MigrationFileGenerator.classNameOf(id)
|
||||
fg.updateRegistry(migPkg, _registryPath, className)
|
||||
ModelSnapshot.capture(current).save(_snapshotPath)
|
||||
println("simcu::orm: 已生成迁移 ${id}")
|
||||
println(" 迁移类: ${className}")
|
||||
println(" 迁移文件: ${path}")
|
||||
println(" 注册文件: ${_registryPath}")
|
||||
println(" 快照: ${_snapshotPath}")
|
||||
@@ -217,7 +215,7 @@ public class MigrationCli {
|
||||
|
||||
/// update:应用所有未执行的迁移
|
||||
private func cmdUpdate(datasource: () -> Datasource,
|
||||
migrations: () -> ArrayList<Migration>, dialect: ISqlDialect): Int64 {
|
||||
migrations: () -> ArrayList<Migration>, dialect: SqlDialect): Int64 {
|
||||
let list = migrations()
|
||||
if (list.size == 0) {
|
||||
println("simcu::orm: 未注册任何迁移(检查 ${_registryPath})")
|
||||
@@ -231,7 +229,7 @@ public class MigrationCli {
|
||||
|
||||
/// downgrade [目标]:回退迁移(执行 down + 删除历史记录;无目标 = 回退最近一个)
|
||||
private func cmdDowngrade(datasource: () -> Datasource, migrations: () -> ArrayList<Migration>,
|
||||
dialect: ISqlDialect, args: ArrayList<String>): Int64 {
|
||||
dialect: SqlDialect, args: ArrayList<String>): Int64 {
|
||||
let list = migrations()
|
||||
if (list.size == 0) {
|
||||
println("simcu::orm: 未注册任何迁移(检查 ${_registryPath})")
|
||||
@@ -257,7 +255,7 @@ public class MigrationCli {
|
||||
/// rm <迁移名或id>:移除已生成的迁移(删除迁移文件 + 注册条目 + 快照回退)
|
||||
private func cmdRm(args: ArrayList<String>): Int64 {
|
||||
if (args.size < 2) {
|
||||
println("用法: simcu::orm rm <迁移名或id>")
|
||||
println("用法: simcu::orm rm <迁移id或类名>")
|
||||
return 1
|
||||
}
|
||||
let target = args[1]
|
||||
@@ -266,12 +264,12 @@ public class MigrationCli {
|
||||
println("simcu::orm: 迁移目录不存在(${_migrationsDir}),没有可移除的迁移")
|
||||
return 1
|
||||
}
|
||||
// 扫描迁移目录,收集迁移 id(文件名去 .cj;排除注册文件/占位文件)
|
||||
// 扫描迁移目录,收集迁移 id(文件名去 .cj = migrationId;排除注册文件/占位文件)
|
||||
let ids = ArrayList<String>()
|
||||
for (f in Directory.readFrom(dir)) {
|
||||
if (f.isRegular()) {
|
||||
let name = f.name
|
||||
if (name.endsWith(".cj") && name != "MigrationRegistry.cj" && name != "_placeholder.cj") {
|
||||
if (name.endsWith(".cj") && name != "migration_registry.cj" && name != "_placeholder.cj") {
|
||||
ids.add(name[..name.size - 3])
|
||||
}
|
||||
}
|
||||
@@ -334,7 +332,7 @@ public class MigrationCli {
|
||||
|
||||
/// list:显示已应用/待应用迁移(空库/全新库时全部显示为待应用)
|
||||
private func cmdList(datasource: () -> Datasource, migrations: () -> ArrayList<Migration>,
|
||||
dialect: ISqlDialect): Int64 {
|
||||
dialect: SqlDialect): Int64 {
|
||||
let list = migrations()
|
||||
if (list.size == 0) {
|
||||
println("simcu::orm: 未注册任何迁移(检查 ${_registryPath})")
|
||||
@@ -365,7 +363,7 @@ public class MigrationCli {
|
||||
|
||||
/// seed:列出/运行数据填充(@Seed 标注的类,按 seedId 唯一前缀匹配;--all 全部;无参=列出)
|
||||
private func cmdSeed(datasource: () -> Datasource, migrations: () -> ArrayList<Migration>,
|
||||
dialect: ISqlDialect, args: ArrayList<String>, seedCtx: ?Any): Int64 {
|
||||
dialect: SqlDialect, args: ArrayList<String>, seedCtx: ?Any): Int64 {
|
||||
let list = allSeeds()
|
||||
if (list.size == 0) {
|
||||
println("simcu::orm: 未注册任何填充(检查 @Seed 标注的类是否存在,并确认应用已 import 对应包)")
|
||||
@@ -524,29 +522,6 @@ public class MigrationCli {
|
||||
b == 0x20 || b == 0x09 || b == 0x0D || b == 0x0A
|
||||
}
|
||||
|
||||
/// 合法标识符:字母/数字/下划线,首字符不能是数字
|
||||
private static func isValidIdentifier(s: String): Bool {
|
||||
if (s.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
let bytes = s.toArray()
|
||||
var i: Int64 = 0
|
||||
while (i < bytes.size) {
|
||||
let b = bytes[i]
|
||||
let isDigit = b >= 0x30 && b <= 0x39
|
||||
let isLetter = (b >= 0x41 && b <= 0x5A) || (b >= 0x61 && b <= 0x7A)
|
||||
let isUnderscore = b == 0x5F
|
||||
if (i == 0 && isDigit) {
|
||||
return false
|
||||
}
|
||||
if (!isDigit && !isLetter && !isUnderscore) {
|
||||
return false
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
private static func printHelp(): Unit {
|
||||
println("simcu::orm cli - 数据库迁移管理程序")
|
||||
println("")
|
||||
@@ -559,8 +534,8 @@ public class MigrationCli {
|
||||
return
|
||||
}
|
||||
println("用法:")
|
||||
println(" add <迁移名> 从模型生成迁移 .cj 文件,更新快照与注册(无变化则跳过)")
|
||||
println(" rm <迁移名或id> 移除已生成的迁移(删除迁移文件 + 注册条目 + 快照回退)")
|
||||
println(" add [描述] 从模型生成迁移 .cj 文件,更新快照与注册(描述可空;无变化则跳过)")
|
||||
println(" rm <迁移id或类名> 移除已生成的迁移(删除迁移文件 + 注册条目 + 快照回退)")
|
||||
println(" update 应用所有未执行的迁移")
|
||||
println(" downgrade [目标] 回退迁移:无目标=回退最近一个;有目标=回退到该迁移之后")
|
||||
println(" list 列出已应用/待应用迁移")
|
||||
@@ -2,7 +2,7 @@
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 迁移落盘生成器(对齐 dotnet ef migrations add):
|
||||
* - Migration 对象 → .cj 源文件文本(迁移类 up/down 用 MigrationBuilder 声明式重建),
|
||||
* - 自动维护迁移注册文件 MigrationRegistry.cj(add 时插入新迁移实例)。
|
||||
* - 自动维护迁移注册文件 migration_registry.cj(add 时插入新迁移实例)。
|
||||
*
|
||||
* 生成文件的格式:
|
||||
* // 此文件由 simcu::orm CLI 自动生成,请勿手动编辑
|
||||
@@ -11,8 +11,8 @@
|
||||
* import simcu::orm.migrations.*
|
||||
* import simcu::orm.sql.*
|
||||
*
|
||||
* public class InitialCreate <: Migration {
|
||||
* public init() { super("20250701000000_InitialCreate", "初始建表") }
|
||||
* public class MigrationId20250701000000 <: Migration {
|
||||
* public init() { super("20250701000000", "初始建表") }
|
||||
* public override func up(builder: MigrationBuilder): Unit {
|
||||
* builder.createTable("users") { tb =>
|
||||
* tb.column("id", ColumnTypes.BigIntCol).primary().autoInc().notNull()
|
||||
@@ -62,6 +62,9 @@ public class MigrationFileGenerator {
|
||||
}
|
||||
let sb = StringBuilder()
|
||||
sb.append("// 此文件由 simcu::orm CLI 自动生成,请勿手动编辑\n")
|
||||
if (!m.description.isEmpty()) {
|
||||
sb.append("// 描述: ${commentSafe(m.description)}\n")
|
||||
}
|
||||
sb.append("package ${appPackage}\n\n")
|
||||
sb.append("import std.collection.*\n")
|
||||
sb.append("import simcu::orm.migrations.*\n")
|
||||
@@ -80,15 +83,12 @@ public class MigrationFileGenerator {
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
/// migrationId "20250701000000_InitialCreate" → 类名 "InitialCreate"
|
||||
/// migrationId(纯时间戳) → 类名: "20250701000000" → "MigrationId20250701000000"
|
||||
public static func classNameOf(migrationId: String): String {
|
||||
if (let Some(idx) <- migrationId.lastIndexOf("_")) {
|
||||
return migrationId[idx + 1..]
|
||||
}
|
||||
migrationId
|
||||
"MigrationId${migrationId}"
|
||||
}
|
||||
|
||||
/// migrationId → 文件名 "20250701000000_InitialCreate.cj"
|
||||
/// migrationId → 文件名: "20250701000000" → "20250701000000.cj"
|
||||
public static func fileNameOf(migrationId: String): String {
|
||||
"${migrationId}.cj"
|
||||
}
|
||||
@@ -148,6 +148,19 @@ public class MigrationFileGenerator {
|
||||
(b >= 0x30 && b <= 0x39) || (b >= 0x41 && b <= 0x5A) || (b >= 0x61 && b <= 0x7A) || b == 0x5F
|
||||
}
|
||||
|
||||
/// 注释安全化:换行/回车替换为空格,避免破坏行注释
|
||||
private static func commentSafe(s: String): String {
|
||||
var out = StringBuilder()
|
||||
for (c in s.runes()) {
|
||||
if (c == Rune(0x0A) || c == Rune(0x0D)) {
|
||||
out.append(" ")
|
||||
} else {
|
||||
out.append(c)
|
||||
}
|
||||
}
|
||||
out.toString()
|
||||
}
|
||||
|
||||
/// 追加注册:旧文件 + 新类名 → 完整注册文件源码(已写新文件并返回路径)
|
||||
public func updateRegistry(appPackage: String, registryPath: String, newClassName: String): String {
|
||||
var entries = ArrayList<String>()
|
||||
File renamed without changes.
@@ -41,13 +41,13 @@ import simcu::orm.migrations.{Migrator, Migration}
|
||||
*/
|
||||
public open class DbContext <: CliContext {
|
||||
internal let datasource: Datasource
|
||||
internal let dialect: ISqlDialect
|
||||
internal let dialect: SqlDialect
|
||||
internal let tracker = ChangeTracker()
|
||||
private let _sets = HashMap<String, Any>()
|
||||
private var _driverName: String
|
||||
private var _connString: String
|
||||
|
||||
public init(datasource: Datasource, dialect: ISqlDialect) {
|
||||
public init(datasource: Datasource, dialect: SqlDialect) {
|
||||
this.datasource = datasource
|
||||
this.dialect = dialect
|
||||
this._driverName = ""
|
||||
@@ -59,7 +59,7 @@ public open class DbContext <: CliContext {
|
||||
}
|
||||
|
||||
/// 便捷构造:由连接字符串创建 Datasource(驱动构建收敛在 DatasourceFactory)
|
||||
public init(connStr: String, dialect: ISqlDialect) {
|
||||
public init(connStr: String, dialect: SqlDialect) {
|
||||
this(DatasourceFactory.create(connStr), dialect)
|
||||
this._driverName = DatasourceFactory.detectDriver(connStr, DatasourceFactory.DEFAULT_DRIVER)
|
||||
this._connString = connStr
|
||||
@@ -70,7 +70,7 @@ public open class DbContext <: CliContext {
|
||||
}
|
||||
|
||||
/// 便捷构造:显式指定驱动类型(如 "pgsql")+ 连接字符串
|
||||
public init(driverName: String, connStr: String, dialect: ISqlDialect) {
|
||||
public init(driverName: String, connStr: String, dialect: SqlDialect) {
|
||||
this(DatasourceFactory.create(connStr, driverName), dialect)
|
||||
this._driverName = DatasourceFactory.normalizeDriverName(driverName)
|
||||
this._connString = connStr
|
||||
@@ -200,7 +200,7 @@ public open class DbContext <: CliContext {
|
||||
}
|
||||
|
||||
/// 当前方言(子类/应用可读取)
|
||||
public func getDialect(): ISqlDialect {
|
||||
public func getDialect(): SqlDialect {
|
||||
dialect
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
* 用法(对齐 EF Core 声明式体验):
|
||||
* @DbContext
|
||||
* public class DataContext {
|
||||
* public var users: DbSet<User>
|
||||
* public var bots: DbSet<Bot>
|
||||
* let users: DbSet<User>
|
||||
* let bots: DbSet<Bot>
|
||||
* }
|
||||
*
|
||||
* 展开行为:
|
||||
* - 自动补 `<: DbContext`(标注的类不能自带继承)
|
||||
* - 每个 DbSet<T> 字段/无 getter prop -> `public prop name: DbSet<T> { get() { this.set<T>() } }`
|
||||
* - 只处理「let 前缀」的 DbSet<T> 字段 -> `public prop name: DbSet<T> { get() { this.set<T>() } }`;
|
||||
* 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<T> 字段/无 getter prop -> public prop { get() { this.set<T>() } }
|
||||
// 2. 只处理「let 前缀」的 DbSet<T> 字段 -> `public prop { get() { this.set<T>() } }`;
|
||||
// var / prop 一律不改动(交给用户自行管理)
|
||||
let newDecls = ArrayList<Decl>()
|
||||
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 {
|
||||
// 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<T> { get() { this.set<T>() } }`
|
||||
private func buildSetProp(name: String, typeName: String): Decl {
|
||||
let inner = extractInnerType(typeName)
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -237,7 +237,7 @@ public class MigrationBuilder {
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移基类:migrationId 形如 "20250701000000_InitialCreate"(时间戳_名称),按字典序应用。
|
||||
* 迁移基类:migrationId 按字典序应用(CLI 生成 "yyyyMMddHHmmss" 时间戳)。
|
||||
* 两种使用方式:
|
||||
* 1. 子类 override up/down 手写声明式操作(MigrationBuilder);
|
||||
* 2. 由 MigrationGenerator 从模型生成时注入 preset 操作,基类 up/down 自动重放。
|
||||
@@ -275,12 +275,12 @@ public open class Migration {
|
||||
}
|
||||
|
||||
/**
|
||||
* DDL SQL 工厂:MigrationOperation → SQL(类型映射与差异语法委托给 ISqlDialect)。
|
||||
* DDL SQL 工厂:MigrationOperation → SQL(类型映射与差异语法委托给 SqlDialect)。
|
||||
*/
|
||||
public class DdlFactory {
|
||||
public init() {}
|
||||
|
||||
public func toSql(op: MigrationOperation, dialect: ISqlDialect): String {
|
||||
public func toSql(op: MigrationOperation, dialect: SqlDialect): String {
|
||||
match (op.kind) {
|
||||
case MigrationOperationKind.CreateTable => createTableSql(op, dialect)
|
||||
case MigrationOperationKind.DropTable => "DROP TABLE IF EXISTS ${dialect.quoteName(op.tableName)}"
|
||||
@@ -303,7 +303,7 @@ public class DdlFactory {
|
||||
/// - 有同名列且类型一致 → AddColumn 幂等跳过(空列表);AlterColumn 保留原 SQL(处理非空/长度等变化)
|
||||
/// - 有同名列但类型不同 → 先 DROP COLUMN 再 ADD COLUMN(重建该字段)
|
||||
/// existingType 来自 information_schema.data_type;None 表示列不存在。
|
||||
public func resolveColumnSqls(op: MigrationOperation, dialect: ISqlDialect,
|
||||
public func resolveColumnSqls(op: MigrationOperation, dialect: SqlDialect,
|
||||
existingType: ?String): ArrayList<String> {
|
||||
let result = ArrayList<String>()
|
||||
let col = op.column.getOrThrow()
|
||||
@@ -369,7 +369,7 @@ public class DdlFactory {
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
private func createTableSql(op: MigrationOperation, dialect: ISqlDialect): String {
|
||||
private func createTableSql(op: MigrationOperation, dialect: SqlDialect): String {
|
||||
let sb = StringBuilder()
|
||||
sb.append("CREATE TABLE IF NOT EXISTS ${dialect.quoteName(op.tableName)} (\n")
|
||||
var first = true
|
||||
@@ -384,7 +384,7 @@ public class DdlFactory {
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
private func createIndexSql(op: MigrationOperation, dialect: ISqlDialect): String {
|
||||
private func createIndexSql(op: MigrationOperation, dialect: SqlDialect): String {
|
||||
let sb = StringBuilder()
|
||||
sb.append("CREATE ")
|
||||
if (op.unique) {
|
||||
@@ -406,7 +406,7 @@ public class DdlFactory {
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
private func columnDefSql(col: ColumnDefinition, dialect: ISqlDialect): String {
|
||||
private func columnDefSql(col: ColumnDefinition, dialect: SqlDialect): String {
|
||||
let sb = StringBuilder()
|
||||
sb.append(dialect.quoteName(col.name))
|
||||
sb.append(" ${dialect.columnTypeSql(col.columnType, col.autoIncrement, col.maxLength)}")
|
||||
@@ -3,7 +3,7 @@
|
||||
* 迁移执行器:历史表 + 按 migrationId 排序应用未执行的迁移(事务内)。
|
||||
*
|
||||
* 历史表 simcu_orm_migrations (id VARCHAR(64) PK, name VARCHAR(256), applied_at TIMESTAMP),
|
||||
* 应用顺序 = migrationId 字典序(建议用 "yyyyMMddHHmmss_名称" 命名)。
|
||||
* 应用顺序 = migrationId 字典序(CLI 生成的 migrationId 为 "yyyyMMddHHmmss" 时间戳)。
|
||||
*/
|
||||
|
||||
package simcu::orm.migrations
|
||||
@@ -18,10 +18,10 @@ import simcu::orm.sql.*
|
||||
*/
|
||||
public class Migrator {
|
||||
private let _datasource: Datasource
|
||||
private let _dialect: ISqlDialect
|
||||
private let _dialect: SqlDialect
|
||||
private let _historyTable = "simcu_orm_migrations"
|
||||
|
||||
public init(datasource: Datasource, dialect: ISqlDialect) {
|
||||
public init(datasource: Datasource, dialect: SqlDialect) {
|
||||
this._datasource = datasource
|
||||
this._dialect = dialect
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -48,7 +48,7 @@ public class PagedResult<T> {
|
||||
*/
|
||||
public class QueryBuilder<T> {
|
||||
private let _model: EntityModel
|
||||
private let _dialect: ISqlDialect
|
||||
private let _dialect: SqlDialect
|
||||
private let _executor: (String, ArrayList<Any>) -> ArrayList<T>
|
||||
private let _counter: (String, ArrayList<Any>) -> Int64
|
||||
|
||||
@@ -58,7 +58,7 @@ public class QueryBuilder<T> {
|
||||
private var _skip: Int64 = 0
|
||||
private var _take: Int64 = 0
|
||||
|
||||
public init(model: EntityModel, dialect: ISqlDialect, executor: (String, ArrayList<Any>) -> ArrayList<T>,
|
||||
public init(model: EntityModel, dialect: SqlDialect, executor: (String, ArrayList<Any>) -> ArrayList<T>,
|
||||
counter: (String, ArrayList<Any>) -> Int64) {
|
||||
this._model = model
|
||||
this._dialect = dialect
|
||||
File renamed without changes.
File renamed without changes.
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 列类型:与具体数据库无关的抽象,DDL 映射由 ISqlDialect 决定。
|
||||
* 列类型:与具体数据库无关的抽象,DDL 映射由 SqlDialect 决定。
|
||||
*/
|
||||
|
||||
package simcu::orm.sql
|
||||
File renamed without changes.
@@ -13,7 +13,7 @@ import std.collection.*
|
||||
/**
|
||||
* PostgreSQL 方言。
|
||||
*/
|
||||
public open class PostgreSqlDialect <: ISqlDialect {
|
||||
public open class PostgreSqlDialect <: SqlDialect {
|
||||
public init() {}
|
||||
|
||||
public func quoteName(name: String): String {
|
||||
@@ -1,9 +1,9 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* SQL 方言接口(I 前缀命名,对齐 EF Core 接口风格):标识符引用、增删改查 SQL 生成、DDL 类型/语句生成。
|
||||
* SQL 方言接口:标识符引用、增删改查 SQL 生成、DDL 类型/语句生成。
|
||||
*
|
||||
* 新数据库接入:实现 ISqlDialect 即可(参考 PostgreSqlDialect / OpenGaussDialect)。
|
||||
* 内置实现见 PostgreSqlDialect.cj(PostgreSQL)、OpenGaussDialect.cj(openGauss,继承 PG)。
|
||||
* 新数据库接入:实现 SqlDialect 即可(参考 PostgreSqlDialect / OpenGaussDialect)。
|
||||
* 内置实现见 postgre_sql_dialect.cj(PostgreSQL)、open_gauss_dialect.cj(openGauss,继承 PG)。
|
||||
*/
|
||||
|
||||
package simcu::orm.sql
|
||||
@@ -13,7 +13,7 @@ import std.collection.*
|
||||
/**
|
||||
* SQL 方言接口。
|
||||
*/
|
||||
public interface ISqlDialect {
|
||||
public interface SqlDialect {
|
||||
/// 引用标识符(表名/列名)
|
||||
func quoteName(name: String): String
|
||||
|
||||
@@ -51,6 +51,3 @@ public interface ISqlDialect {
|
||||
/// 探测数据库是否存在的 SELECT(返回带 ? 占位符的语句,库名作为参数值传入避免拼接注入)
|
||||
func databaseExistsSql(): String
|
||||
}
|
||||
|
||||
/// 兼容别名:旧名称 SqlDialect 仍可用(新代码请使用 ISqlDialect)
|
||||
public type SqlDialect = ISqlDialect
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* @DbContext 宏回归测试:只处理「let 前缀」的 DbSet<T> 字段,var/prop 一律不改动。
|
||||
*
|
||||
* 通过注册一个 FakeDriver,让宏生成的 `init(driverName, connStr)` 走通
|
||||
* `super(...) -> DatasourceFactory.create(connStr, driverName)` 这条真实路径,
|
||||
* 从而验证:let 字段被改写为 `public prop { get() { this.set<T>() } }`(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<User>
|
||||
let orders: DbSet<Order>
|
||||
}
|
||||
|
||||
// ---------- 宏应用:let 与手写 prop 混用 ----------
|
||||
// let 字段被宏改写为 public prop;手写 prop(含 getter)保持原样、不改写。
|
||||
@DbContext
|
||||
class MixedCtx {
|
||||
let users: DbSet<User>
|
||||
prop todos: DbSet<Order> {
|
||||
get() { this.set<Order>() }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public class DbContextMacroTests {
|
||||
@TestCase
|
||||
public func testMacroGeneratesInitAndDbSets(): Unit {
|
||||
let ctx = MacroCtx("fake", "postgres://localhost/db")
|
||||
// 宏生成的 init 已把 let 字段初始化为 set<T>(),可直接使用
|
||||
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 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<Order>, true)
|
||||
todos.add(Order())
|
||||
@Expect(ctx.pendingCount(), 1)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import simcu::orm.cli.*
|
||||
/// 手写迁移(验证源码生成覆盖索引与 rawSql 转义)
|
||||
class HandWrittenMigration <: Migration {
|
||||
public init() {
|
||||
super("20250801000000_HandWritten", "手写迁移")
|
||||
super("20250801000000", "手写迁移")
|
||||
}
|
||||
public override func up(builder: MigrationBuilder): Unit {
|
||||
builder.createTable("tags") { tb =>
|
||||
@@ -37,7 +37,7 @@ class HandWrittenMigration <: Migration {
|
||||
/// 手写迁移(验证部分索引 filter 还原:createIndex 5 参带 filter)
|
||||
class FilteredIndexMigration <: Migration {
|
||||
public init() {
|
||||
super("20250802000000_FilteredIndex", "部分唯一索引")
|
||||
super("20250802000000", "部分唯一索引")
|
||||
}
|
||||
public override func up(builder: MigrationBuilder): Unit {
|
||||
let cols = ArrayList<String>()
|
||||
@@ -91,9 +91,9 @@ class MigrationFileGeneratorTests {
|
||||
|
||||
@TestCase
|
||||
public func testClassNameAndFileName(): Unit {
|
||||
@Expect(MigrationFileGenerator.classNameOf("20250701000000_InitialCreate") == "InitialCreate")
|
||||
@Expect(MigrationFileGenerator.fileNameOf("20250701000000_InitialCreate") == "20250701000000_InitialCreate.cj")
|
||||
@Expect(MigrationFileGenerator.classNameOf("NoTimestamp") == "NoTimestamp")
|
||||
// 时间戳 id → 类名 MigrationId<时间戳>,文件名 = id
|
||||
@Expect(MigrationFileGenerator.classNameOf("20250701000000") == "MigrationId20250701000000")
|
||||
@Expect(MigrationFileGenerator.fileNameOf("20250701000000") == "20250701000000.cj")
|
||||
}
|
||||
|
||||
@TestCase
|
||||
@@ -101,11 +101,12 @@ class MigrationFileGeneratorTests {
|
||||
let models = ArrayList<EntityModel>()
|
||||
models.add(buildUserModel())
|
||||
let gen = MigrationGenerator()
|
||||
let m = gen.initial("20250701000000_InitialCreate", "初始建表", models)
|
||||
let m = gen.initial("20250701000000", "初始建表", models)
|
||||
let src = MigrationFileGenerator.migrationSource("app", MigrationFileGenerator.classNameOf(m.migrationId), m)
|
||||
@Expect(src.contains("// 描述: 初始建表"))
|
||||
@Expect(src.contains("package app"))
|
||||
@Expect(src.contains("public class InitialCreate <: Migration"))
|
||||
@Expect(src.contains("super(\"20250701000000_InitialCreate\", \"初始建表\")"))
|
||||
@Expect(src.contains("public class MigrationId20250701000000 <: Migration"))
|
||||
@Expect(src.contains("super(\"20250701000000\", \"初始建表\")"))
|
||||
@Expect(src.contains("builder.createTable(\"users\") { tb =>"))
|
||||
@Expect(src.contains("tb.column(\"id\", ColumnTypes.BigIntCol).primary().autoInc().notNull()"))
|
||||
@Expect(src.contains("tb.column(\"name\", ColumnTypes.TextCol).notNull().withMaxLength(100)"))
|
||||
@@ -120,7 +121,7 @@ class MigrationFileGeneratorTests {
|
||||
newModels.add(buildUserModel())
|
||||
newModels.add(buildOrderModel())
|
||||
let gen = MigrationGenerator()
|
||||
let m = gen.diff("20250801000000_AddOrders", "新增 orders 表", oldModels, newModels)
|
||||
let m = gen.diff("20250801000000", "新增 orders 表", oldModels, newModels)
|
||||
let src = MigrationFileGenerator.migrationSource("app", MigrationFileGenerator.classNameOf(m.migrationId), m)
|
||||
@Expect(src.contains("builder.createTable(\"orders\") { tb =>"))
|
||||
@Expect(src.contains("tb.column(\"oid\", ColumnTypes.TextCol).primary().notNull()"))
|
||||
@@ -179,22 +180,24 @@ class MigrationFileGeneratorTests {
|
||||
let models = ArrayList<EntityModel>()
|
||||
models.add(buildUserModel())
|
||||
let gen = MigrationGenerator()
|
||||
let m = gen.initial("20250701000000_InitialCreate", "初始建表", models)
|
||||
let m = gen.initial("20250701000000", "初始建表", models)
|
||||
let fg = MigrationFileGenerator()
|
||||
let path = fg.writeMigration("testpkg", m, dir)
|
||||
@Expect(exists(Path(path)))
|
||||
@Expect(path.endsWith("20250701000000.cj"))
|
||||
let text = String.fromUtf8(File.readFrom(Path(path)))
|
||||
@Expect(text.contains("package testpkg"))
|
||||
@Expect(text.contains("public class InitialCreate <: Migration"))
|
||||
@Expect(text.contains("public class MigrationId20250701000000 <: Migration"))
|
||||
|
||||
// 注册文件:首次创建 + 追加 + 幂等
|
||||
let registryPath = "${dir}/MigrationRegistry.cj"
|
||||
fg.updateRegistry("testpkg", registryPath, "InitialCreate")
|
||||
fg.updateRegistry("testpkg", registryPath, "AddAge")
|
||||
fg.updateRegistry("testpkg", registryPath, "AddAge")
|
||||
let registryPath = "${dir}/migration_registry.cj"
|
||||
let cls = MigrationFileGenerator.classNameOf(m.migrationId)
|
||||
fg.updateRegistry("testpkg", registryPath, cls)
|
||||
fg.updateRegistry("testpkg", registryPath, cls)
|
||||
let reg = String.fromUtf8(File.readFrom(Path(registryPath)))
|
||||
@Expect(reg.contains("package testpkg"))
|
||||
@Expect(MigrationFileGenerator.extractRegistryEntries(reg).size == 2)
|
||||
@Expect(reg.contains("registerMigration(MigrationId20250701000000())"))
|
||||
@Expect(MigrationFileGenerator.extractRegistryEntries(reg).size == 1)
|
||||
} finally {
|
||||
if (exists(Path(dir))) {
|
||||
remove(Path(dir), recursive: true)
|
||||
@@ -222,12 +225,13 @@ class MigrationCliDispatchTests {
|
||||
}
|
||||
|
||||
@TestCase
|
||||
public func testAddMissingNameReturnsOne(): Unit {
|
||||
public func testAddWithoutDescriptionIsOk(): Unit {
|
||||
// add 无参数 = 描述可空;模型为空 → 无变化 → 跳过生成,返回 0
|
||||
let args = ArrayList<String>()
|
||||
args.add("add")
|
||||
let rc = MigrationCli.run(args, { => ArrayList<EntityModel>() },
|
||||
{ => throw Exception("不应调用 datasource") }, { => ArrayList<Migration>() })
|
||||
@Expect(rc == 1)
|
||||
@Expect(rc == 0)
|
||||
}
|
||||
|
||||
@TestCase
|
||||
@@ -252,7 +256,7 @@ class MigrationCliDispatchTests {
|
||||
public func testDowngradeWithTargetConsumesArgs(): Unit {
|
||||
let args = ArrayList<String>()
|
||||
args.add("downgrade")
|
||||
args.add("20260819061048_InitialCreate")
|
||||
args.add("20260819061048")
|
||||
let rc = MigrationCli.run(args, { => ArrayList<EntityModel>() },
|
||||
{ => throw Exception("不应调用 datasource") }, { => ArrayList<Migration>() })
|
||||
@Expect(rc == 1)
|
||||
@@ -301,7 +301,7 @@ class ChangeTrackerTests {
|
||||
// ---------- OpenGaussDialect SQL 生成 ----------
|
||||
|
||||
@Test
|
||||
class ISqlDialectTests {
|
||||
class SqlDialectTests {
|
||||
@TestCase
|
||||
public func testQuoteInsertUpdateDelete(): Unit {
|
||||
let d = OpenGaussDialect()
|
||||
@@ -709,7 +709,7 @@ class MigrationGeneratorTests {
|
||||
MigrationGenerator()
|
||||
}
|
||||
|
||||
private func upSql(m: Migration, dialect: ISqlDialect): ArrayList<String> {
|
||||
private func upSql(m: Migration, dialect: SqlDialect): ArrayList<String> {
|
||||
let builder = MigrationBuilder()
|
||||
m.up(builder)
|
||||
let f = DdlFactory()
|
||||
@@ -720,7 +720,7 @@ class MigrationGeneratorTests {
|
||||
sqls
|
||||
}
|
||||
|
||||
private func downSql(m: Migration, dialect: ISqlDialect): ArrayList<String> {
|
||||
private func downSql(m: Migration, dialect: SqlDialect): ArrayList<String> {
|
||||
let builder = MigrationBuilder()
|
||||
m.down(builder)
|
||||
let f = DdlFactory()
|
||||
@@ -987,7 +987,7 @@ class AppDbContext <: DbContext {
|
||||
orders = set<Order>()
|
||||
}
|
||||
|
||||
init(ds: Datasource, dialect: ISqlDialect) {
|
||||
init(ds: Datasource, dialect: SqlDialect) {
|
||||
super(ds, dialect)
|
||||
users = set<User>()
|
||||
orders = set<Order>()
|
||||
@@ -1044,7 +1044,7 @@ class ModelSnapshotTests {
|
||||
ms
|
||||
}
|
||||
|
||||
private func upSql(m: Migration, dialect: ISqlDialect): ArrayList<String> {
|
||||
private func upSql(m: Migration, dialect: SqlDialect): ArrayList<String> {
|
||||
let builder = MigrationBuilder()
|
||||
m.up(builder)
|
||||
let f = DdlFactory()
|
||||
File renamed without changes.
Reference in new issue
Block a user