@pylonts/dsl 1.1.11 → 1.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/convert.d.ts +6 -8
  2. package/dist/curd.js +1 -1
  3. package/dist/dao.d.ts +10 -7
  4. package/dist/dao.js +20 -7
  5. package/dist/dsl.d.ts +18 -1
  6. package/dist/dsl.js +40 -0
  7. package/dist/dto.d.ts +13 -8
  8. package/dist/dto.js +68 -13
  9. package/dist/entity.d.ts +4 -3
  10. package/dist/entity.js +1 -1
  11. package/dist/filter.d.ts +6 -4
  12. package/dist/filter.js +1 -1
  13. package/dist/flow-script.js +8 -2
  14. package/dist/flow.d.ts +10 -2
  15. package/dist/flow.js +44 -4
  16. package/dist/mermaid-driver.js +2 -2
  17. package/dist/project.d.ts +5 -2
  18. package/dist/project.js +21 -2
  19. package/dist/service.d.ts +13 -8
  20. package/dist/service.js +1 -1
  21. package/dist/third-service.d.ts +10 -53
  22. package/dist/third-service.js +3 -78
  23. package/dist/typebox-driver.d.ts +0 -6
  24. package/dist/typebox-driver.js +8 -36
  25. package/dist/utils.d.ts +2 -2
  26. package/docs/curd.md +55 -20
  27. package/docs/dao-generation.md +477 -477
  28. package/docs/project.md +32 -24
  29. package/docs/token.md +326 -326
  30. package/package.json +1 -1
  31. package/src/action.ts +51 -51
  32. package/src/controller.ts +53 -53
  33. package/src/convert.ts +76 -78
  34. package/src/curd.ts +104 -104
  35. package/src/dao.ts +504 -485
  36. package/src/dsl.ts +296 -257
  37. package/src/dto.ts +323 -266
  38. package/src/entity.ts +43 -42
  39. package/src/expr.ts +64 -64
  40. package/src/filter.ts +71 -69
  41. package/src/flow-script.ts +702 -695
  42. package/src/flow.ts +1272 -1226
  43. package/src/index.ts +46 -46
  44. package/src/mermaid-driver.ts +339 -339
  45. package/src/mysql-driver.ts +108 -108
  46. package/src/project.ts +138 -114
  47. package/src/service.ts +112 -107
  48. package/src/third-service.ts +68 -191
  49. package/src/typebox-driver.ts +234 -268
  50. package/src/utils.ts +74 -74
@@ -1,478 +1,478 @@
1
- # DAO 生成标准化规划(dao_schema v2)
2
-
3
- > 状态:**规划中(未实现)**
4
- > 关联代码:`dsl/src/dao.ts`(DaoSchema 六方法)、`dsl/src/entity.ts`(EntitySchema 单表列)、`dsl/src/project.ts`(app.schema.tenant)、`gen/src/gen-dao.ts`(dao_schema 生成器)、`curd/src/dao.ts` + `curd/src/dao_utils.ts`(curd 直产 DAO 实现)、`dsl/src/flow.ts`(FlowMethodRef 含 DaoMethodSchema)
5
- > 背景:curd 的 DAO 与 dao_schema 的 DAO 是两套独立生成逻辑(仅 filter 模块文件共享 `renderFilterFile`),本次规划把 DAO 生成收敛到 dao_schema 单一标准。
6
-
7
- ## 1. 目标与原则
8
-
9
- **目标**:DAO 实现只有一个生成入口(`gen dao`)。curd 不再直接产出 DAO 代码,改为产出 `dao_schema` + `entity_schema` 声明,由统一生成器展开实现。任何 DAO 查询语义(精确列、多表 join、租户)都先落到 dao_schema 声明能力,再进生成器。
10
-
11
- **原则**:
12
-
13
- 1. 声明完整:一个 dao 方法声明包含生成所需的一切(列、join、租户、分页),产物无需手工填充;
14
- 2. 机器可校验:精确列是否单表(写方法)、跨表源是否有 FK(读方法)、租户注入是否一致——全部 lint 可查;
15
- 3. 写读分界:写方法(insert/update)列必须单表(写不能 join);读方法(find/get/aggregate)可多表;
16
- 4. 生成约定集中:join 推导、租户注入、FK label 别名规则只在 gen 一处,curd 声明不携带重复逻辑。
17
-
18
- ## 2. 现状差距(curd 产物语义 → dao_schema 表达力)
19
-
20
- | curd DAO 方法 | curd 产物语义 | dao_schema 现状 | 差距 |
21
- |---|---|---|---|
22
- | `page`/`list`/`query` | 精确 select 列 + FK label 别名 + 跨表列别名 + leftJoin + tenant WHERE + filter | `find`:`select *`、`EntitySchema` 单表列 | 精确列、多表行类型、别名、租户 |
23
- | `detail` | 跨表列 + FK label + leftJoin + PK + tenant WHERE | `get`:单表 `select *` | 多表、精确列、租户 |
24
- | `get` | own-table 精确列 + PK + tenant | `get` | 精确列、租户 |
25
- | `insert` | 全行插入(generator 补 PK) | `insert` | ✅ 语义一致(generator 已实现) |
26
- | `update` | `where({pk, tenantFk}).update(setCols)` | `update`:`where({pk}) + where filter` | 租户 WHERE |
27
- | (无 delete) | — | `delete`:`where({pk})` | 租户 WHERE |
28
- | 分页 | 自写 `clone().clearSelect().count()` + offset/limit | `paginate()`(@pylonts/dao) | 收敛到 `paginate()` |
29
-
30
- **落地现状(2026-08-16 合并决策)**:`EntitySchema` 与 `RowSchema` 已合并为一个 schema——`EntitySchema` 承载数据库字段集合,**来源(单表/跨表)不是 schema 的责任**,由消费位置决定(写方法 args = 单表,读方法 results = 可跨表)。文件规则:一个文件对应一张表(主表),文件内可声明多个 entity,entity 名字可以 Row 化(如 `OrderListRow`),全部走 `defineEntity`。
31
-
32
- **flow 耦合**:`FlowMethodRef` 含 `DaoMethodSchema`——service flow 的 invoke 可直接引用 dao 方法,dao 方法签名(参数形状)是 flow 契约的一部分。签名变化(如新增 tenant 参数)必须同步 flow invoke 的渲染与校验。
33
-
34
- ## 3. 核心概念改造
35
-
36
- ### 3.1 行类型模型:EntitySchema(结果行)
37
-
38
- `find.results` / `get.results` 与写方法 args 统一使用 `EntitySchema`——精确列 + 跨表列,**无显式 alias 声明**:
39
-
40
- ```ts
41
- /** A row object: a set of database columns. Columns may come from one table
42
- * (write args) or span tables through the main table's foreign keys (read
43
- * results) — where the columns come from is the responsibility of the
44
- * consuming position. */
45
- export interface EntitySchema extends SchemaBase {
46
- type: 'entity';
47
- api: ProjectApiSchema;
48
- app: FrontAppSchema;
49
- columns: Field[]; // 任意表列(单表 = 写方法 args;多表 = 读方法 results)
50
- }
51
- ```
52
-
53
- **alias 完全推导(隐式,map 计数)**:
54
-
55
- - 单表查询(delete/insert/update 的 args、单表 get/find):列名天然唯一,**无 alias**,SELECT/接口都用裸列名;
56
- - 多表查询(find/get 的结果列跨表):收集所有参与表的列名,map 计数——
57
- - 列名**唯一**(只在一张表出现)→ 沿用裸列名;
58
- - 列名**重复**(≥2 张表有同名列)→ 重名列必须带 alias;主表列优先保留裸名,跨表列 alias 按词典短语命名;
59
- - 规则完全机器可判(列名集合 + 计数),lint 可校验产物一致性——**EntitySchema 无 alias 声明字段**。
60
-
61
- **alias 命名公式(表短语 + 列名)**:
62
-
63
- ```
64
- alias = {table.phrase?.name ?? table.name} + '_' + {column.name}
65
- ```
66
-
67
- - 表侧:用表的实体短语(`TableSchema.phrase`,词典 EntityPhrase 条目,如 `mer`/`shop`);关联表等多实体场景无短语 → 用表名;
68
- - 列侧:**直接用列名**——字段命名本身就是短语命名(词典约定,如 amount → `amt`、rate → `rate`),列名即短语形式,无需再挂短语引用或解析词典;
69
- - curd 生成的外部引用列别名即此规则:`merchant`(phrase `mer`)的 `name` 列被 `order` 的查询引用 → `mer_name`。
70
-
71
- ```ts
72
- // 多表 find:merchant.name 与 shop.name 重名 → 跨表列 alias(表短语 + 列名)
73
- select('merchant.id', 'merchant.name', 'shop.name as shop_name')
74
- // 不重名场景:shop.address 唯一 → 裸名
75
- select('merchant.id', 'shop.address')
76
- ```
77
-
78
- **单表硬约束落在使用位置**:写方法的 args(insert/update)在 `defineDao` 校验期强制单表列;读方法 results 可跨表(外部引用列需 FK 可达)——insert/update 的行类型语义不变。
79
-
80
- **enum 列的类型渲染**:EntitySchema 渲染的 TS interface 中,enum 列必须用枚举 JS 名(`status: MerchantStatus`)+ 类型 import,**不得**用 `Field.jsType`(enum 的 jsType 是 `'string' | 'number'`,会丢失枚举类型)——复用 filter 产物的 `enumImportOf` 机制。**现状差距**:gen-dao 的 `renderRowInterface` 用 `c.jsType`,enum 列退化为 string/number;curd 的 entity 生成器用 jsName + import,是正确的。v2 必须对齐后者。
81
-
82
- ### 3.1.1 聚合字段 AggregateField(aggregate 查询的输出列)
83
-
84
- 聚合查询(`aggregate` 机制)的 results 实体中,列分两类:**普通列 = GROUP BY 维度**(一行一组),**聚合字段 = 计算输出列**。聚合字段由 `aggField(name, expr)` 构建,与普通列同处 `columns`,定义在实体文件(`{table}.entity.ts`)——dao 方法 `results` 只引用实体,从不在 dao 文件内联聚合字段:
85
-
86
- ```ts
87
- import { aggField, Compute } from '@pylonts/dsl';
88
-
89
- // entity_schema/{api}/{app}/entity/order.entity.ts
90
- export const orderStatusStats = defineEntity({
91
- name: 'OrderStatusStats',
92
- api, app,
93
- columns: [
94
- order.columns.status, // GROUP BY 维度(普通列)
95
- aggField('total', Compute.count()), // count → jsType: 'number'
96
- aggField('sumAmt', Compute.sum(order.columns.amount)), // sum(decimal) → jsType: 'string'
97
- ],
98
- });
99
- ```
100
-
101
- - `AggregateField` 是 `Field` 联合成员(`type: 'aggregate'`),实体可像任何列一样携带它;构造器 `aggField(name, expr)`,表达式 `Compute.count()` / `Compute.sum(field)` / `Compute.avg(field)`;
102
- - **`jsType` 两态**,由表达式推导:`count()` 与整数列的 `sum/avg` → `'number'`;`decimal`/`bigint`/`rate` 列的 `sum/avg` → `'string'`(精度串,JS number 丢精度);
103
- - 驱动规则(mysql2 `supportBigNumbers` 开启):COUNT/SUM/AVG 一律以 string 返回,生成物按 `jsType` 决定是否 `Number()` 转换(`'number'` → 转,`'string'` → 原样);
104
- - DTO 可用 `from(entity, ...)` 投影聚合字段:`jsType === 'number'` 渲染 `Type.Number()`,否则 `Type.String()`。
105
-
106
- ### 3.2 外部引用列(多表列)+ curd 层的 label 展开
107
-
108
- **dao 层无 label 概念**:EntitySchema.columns 可包含**任何表的列**——属于其他表的列 = 外部引用列(external reference)。外部引用列的 join 关系由 **FK 推导**:**FK 就是 join 条件**——main table 的某个 FK 指向该列所属表,渲染 `table.column as alias` + LEFT JOIN(join 条件 = FK 的 columns/references)。alias 公式(3.1)对多表列统一生效。
109
-
110
- **引用表可能有 label,也可能没有**——外部引用列机制不依赖 label:`merchant.name`、`merchant.id`、`merchant.created_at` 作为外部引用列,机制完全相同(FK 推导 join → alias)。label 只是 **curd 层在选择"FK 引用表上展示哪一列"时的默认来源**(有 label 用 label,无 label 用 PK),dao 生成器完全不感知 label。
111
-
112
- ```ts
113
- // curd list.columns = [..., order.mer_id](FK 列,mer_id 的 FK 就是 order↔merchant 的 join 条件)
114
- // → curd 生成的 dao_schema 声明(隐式展开,选择 merchant 的 label 列作外部引用列):
115
- results.columns = [..., order.mer_id, merchant.name /* 外部引用列:FK(order.mer_id→merchant.id) 推导 join */]
116
- // → gen-dao 渲染(无 label 概念,纯外部引用机制):
117
- select('order.mer_id', 'merchant.name as mer_name')
118
- .leftJoin('merchant', 'merchant.id', 'order.mer_id')
119
- ```
120
-
121
- "隐式展开"的隐式发生在 **curd → dao_schema 生成环节**(待决策 7 已决策),不在 gen-dao 环节——gen-dao 拿到什么列渲染什么列,声明与产物一一对应。
122
-
123
- ### 3.3 join 推导统一(gen 单一函数)
124
-
125
- 规则(两边现状已一致,收敛即可):main table 通过自己的 `foreignKeys` 指向跨表 source;结果列、filter 条件、FK label 展开(3.2)涉及的跨表 source 集合 = LEFT JOIN 集合;只支持一层 join(main 直接 FK,多层不做)。现有 `renderFilterJoins` 泛化为 `renderJoins(mainTable, sources)`,find/get 的结果列与 filter 条件合并计算 sources。
126
-
127
- ### 3.4 租户自动注入
128
-
129
- `app.schema.tenant` 语义已在 dsl 定义("所有指向此表的 FK 的表自动获得租户作用域"),生成器实现:
130
-
131
- | 方法 | 注入规则 |
132
- |------|---------|
133
- | `find`/`get`/`aggregate` | tenant FK 成为**强制方法参数**(camelCase)+ 无条件 WHERE |
134
- | `update`/`delete` | tenant FK 进 WHERE(args 内或独立参数,待决策) |
135
- | `insert` | 待决策(3.5) |
136
-
137
- dao_schema **不新增声明**:tenant 从 `dao.api`/`dao.app` 的 `app.schema.tenant` + `dao.table` 的 FK 机器推导(有 tenant 表且表有 FK 才注入;无 FK = 无租户,报错还是跳过待决策)。
138
-
139
- ### 3.5 insert 的租户列
140
-
141
- curd 现状 insert 无 tenant 处理(examples 未启用 tenant,路径未跑过)。两条路(待决策):
142
-
143
- - **A 声明保证**:add 行类型(EntitySchema)必须包含 tenant 列,insert 全行插入即可(零生成器改动,声明期 lint 校验 add 实体含 tenant 列);
144
- - **B 生成器注入**:insert 方法签名加 tenant 参数,生成器 `insert({ ...row, tenant_col: tenantId })`(调用方无感知,但行类型与 DB 行不一致)。
145
-
146
- ### 3.6 值表达式 ValueExpr(update 表达式 set + 条件右值)
147
-
148
- **背景**:乐观锁(`version = version + 1`)、扣库存(`stock = stock - ? WHERE stock >= ?`)、列间比较(`stock > locked`)要求 set 值与条件右值是表达式,不能只是直接赋值/同名参数。受控算子列表(`{col, op:'+', value}`)封闭不可扩展,否决;SQL 字符串泄漏 SQL 不可 lint,否决。**采用声明式递归 AST——节点类型可扩展**。
149
-
150
- ```ts
151
- // 值表达式:列引用 / 字面量 / 方法参数 / 二元运算(递归)
152
- export type ValueExpr =
153
- | { kind: 'col'; field: Field } // 列引用(当前行该列)
154
- | { kind: 'lit'; value: string | number } // 字面量
155
- | { kind: 'param'; name: string } // 方法参数(进方法签名)
156
- | { kind: 'bin'; op: 'add' | 'sub' | 'mul' | 'div'; left: ValueExpr; right: ValueExpr };
157
-
158
- // update 扩展:col = expr(与 args 的直接赋值列互斥,lint 校验不重叠)
159
- interface UpdateSchema { args: EntitySchema; set?: SetExpr[]; where?: FilterSchema }
160
- interface SetExpr { col: Field; expr: ValueExpr }
161
-
162
- // 条件右值:缺省 = 现状同名参数语义(零破坏);显式 = 表达式(列间比较/常量/运算)
163
- interface FilterCondition { field: Field; op?: Operator; right?: ValueExpr; optional?: boolean }
164
-
165
- // 便利函数:incr(col, by) / decr(col, by)(by = ValueExpr)
166
- ```
167
-
168
- **需求覆盖矩阵**:
169
-
170
- | 场景 | 声明 | 生成 SQL |
171
- |---|---|---|
172
- | 乐观锁 | **一等公民(3.7)**:`TableSchema.version` 声明后,update 自动合成——无需手写 set | `SET ..., version = version + 1 WHERE id = ? AND version = ?`(affected=0 即冲突,重试/报错为调用方语义) |
173
- | 扣库存(防超卖) | `set: [{ col: stock, expr: bin('sub', col(stock), param('qty')) }]` + `where`(stock gte) | `SET stock = stock - ? WHERE id = ? AND stock >= ?` |
174
- | 扣款 | 同上(balance - param('amt')) | `SET balance = balance - ? WHERE balance >= ?` |
175
- | 列间运算 | `bin('sub', col(stock), col(locked))` | `SET stock = stock - locked` |
176
- | 状态机 CAS | args 直接赋值 + `where`(现状已支持) | `SET state = 'refund' WHERE id = ? AND state = 'success'` |
177
-
178
- **机器校验(lint)**:列引用属于 `dao.table` 或 args 实体;param 名唯一且进签名;lit 类型与列类型相容;bin 左右类型相容。
179
-
180
- **安全**:AST → SQL 全受控(列名来自 Field、op 封闭、值走 knex 绑定参数),无注入面。
181
-
182
- **扩展性**:未来加节点 = 加 kind(如 `{ kind: 'fn', name: 'greatest', args: ValueExpr[] }`、一元取负),不动已有结构、不改存量声明——AST 的价值所在。
183
-
184
- ### 3.7 乐观锁一等公民(TableSchema.version)
185
-
186
- 乐观锁是表级属性(版本列属于表,不随方法变化),声明收敛到 `TableSchema.version`,update 自动合成版本自增 + 版本条件——不要求每个方法手写 `incr(version, lit(1))`。
187
-
188
- ```ts
189
- defineTable({
190
- name: 'order',
191
- columns: { ..., version: { type: 'integer' } },
192
- version: order.columns.version, // 乐观锁版本列(列引用)
193
- });
194
- ```
195
-
196
- **语义**:
197
-
198
- | 机制 | 行为 |
199
- |---|---|
200
- | `TableSchema.version?: Field` | 指向本表一个整数列;定义期校验:属于本表 + 类型为 integer/number |
201
- | update(有 version 的表) | args 实体**必须含 version 列**(lint);生成器从 row 提取 version 进 WHERE,set 自动合成 `version = version + 1`,其余列直接赋值——签名不变 `update(row)`,返回 affected rows,0 = 冲突(重试/报错为调用方语义) |
202
- | insert | 照旧——version 初始值(0/1)由调用方在行数据里给,生成器不注入 |
203
- | delete / get / find | 不受影响(乐观锁 delete 如需要,另议) |
204
- | 与 `set?: SetExpr[]` 的关系 | 互斥——version 列由一等公民机制管理,set 表达式不得再引用 version 列(lint) |
205
-
206
- **与 tenant 形态一致**:row 里的 version 列与 row 里的 tenant FK 列同构——都是"行内列既进 WHERE 又不在 set"(destructure 后进 where),签名都只有一个 row。三种隐式列(pk / tenant FK / version)的 WHERE 提取是同一生成器函数。
207
-
208
- **价值**:乐观锁零声明成本(表声明一次,所有 update 自动生效)、不可遗漏(lint 强制 args 含 version 列)、调用方直接受影响行数判断冲突。
209
-
210
- ## 4. dao_schema v2 方法形态
211
-
212
- ```ts
213
- // find — 精确列 + 多表 + 分页 + filter
214
- find: {
215
- type: 'find',
216
- args?: FilterSchema, // 查询条件(现状不变)
217
- results: EntitySchema, // SELECT 列 + 行类型(可跨表)
218
- mode?: 'page' | 'limit', // 分页(现状不变)
219
- orderBy?: OrderBySchema | OrderBySchema[],
220
- },
221
- // 扩展点(7.2 验证倒逼):
222
- // results: EntitySchema | Field —— Field = 单列投影(valueList:List<值>)
223
- // Operator + 'in' / 'null' / 'notNull'(IN 条件 / NULL 条件)
224
- // join 类型 left/inner 声明位(filter 跨表条件 inner join 过滤主表)
225
-
226
- // get — 机制语义:按精确键取单行(任意列组合,不限于 PK;复合 PK / getByXX 均合法)
227
- get: {
228
- type: 'get',
229
- args: Field | Field[], // 单字段或 AND 精确匹配的多字段(复合 PK)
230
- where?: FilterSchema, // 附加条件(AND 到键上):"PK + 状态条件"取单行 / 乐观锁读取
231
- results: EntitySchema, // 可多表
232
- },
233
-
234
- // insert — 精确列(现状语义已对,不变)
235
- insert: { type: 'insert', args: EntitySchema },
236
-
237
- // update — 精确 set 列 + PK + tenant(+where filter 现状不变)
238
- update: { type: 'update', args: EntitySchema, where?: FilterSchema },
239
- // 表达式能力(3.6 ValueExpr 模型):set?: SetExpr[](col = expr)、FilterCondition.right?: ValueExpr
240
-
241
- // delete — 按精确键删除(复合 PK 支持)
242
- delete: { type: 'delete', args: Field | Field[] },
243
-
244
- // aggregate — filter + tenant + filter 跨表 join(现状 + tenant)
245
- aggregate: { type: 'aggregate', args?: FilterSchema, results: EntitySchema },
246
- ```
247
-
248
- **方法名与机制 type 正交**:dao_schema 是**低级机制**——`type` 只表达"怎么查"(按 PK 取单行 / 条件列表 / 写 / 删 / 聚合),六种机制封闭,不再随业务新增;方法名(methods map 的 key)是**业务语义**,自由命名,生成器原样保留。同一个机制可以有多个业务方法,每个方法有各自的 args/results:
249
-
250
- ```ts
251
- defineDao({
252
- name: 'MerchantDao',
253
- table: merchantTable,
254
- methods: {
255
- get: { type: 'get', args: pk, results: merchantGetRow }, // curd get:单表精确列
256
- detail: { type: 'get', args: pk, results: merchantDetailRow }, // curd detail:多表 + label
257
- getByOrderNo: { type: 'get', args: orderNoField, results: merchantGetRow }, // 任意列精确取单行
258
- getByCombo: { type: 'get', args: [orderNoField, merIdField], results: merchantGetRow }, // 多字段 AND(复合 PK 同理)
259
- page: { type: 'find', mode: 'page', ... }, // curd page
260
- list: { type: 'find', ... }, // curd list
261
- query: { type: 'find', ... }, // curd keyword query
262
- },
263
- });
264
- ```
265
-
266
- 因此 dao 语义**不需要**为业务延伸出新 type(如 `getDetail`)——延伸发生在方法名与键组合层面:`get` 的 args 是"精确键"(任意列或列组合),`getByXX`、复合 PK 都是合法声明。curd 生成 dao_schema 时业务方法名原样保留(`page`/`list`/`query`/`get`/`detail`),service/controller 对 `dao.page(...)`、`dao.detail(...)` 的引用签名不变。
267
-
268
- **curd 方法 → dao_schema 映射**:
269
-
270
- | curd 业务方法 | 机制 type | 差异 |
271
- |---|---|---|
272
- | `page` | `find` + `mode: 'page'` | `table.paginated === true` 时 curd 生成 page;映射规则:paginated → `mode: 'page'`(方法名 page),非 paginated → 无 mode(方法名 list) |
273
- | `list` | `find` | — |
274
- | `query`(keyword) | `find`(同一 filter) | 无分页参数;keyword 存在才生成 |
275
- | `get` | `get`(单表 EntitySchema) | — |
276
- | `detail` | `get`(多表 EntitySchema) | 单表 vs 多表只是 EntitySchema 内容差异 |
277
- | `insert` / `update` | `insert` / `update` | — |
278
- | (无 delete) | `delete` 机制保留并补租户 | — |
279
-
280
- **对齐检查:v2 能力 ≥ curd 现状(逐项)**:
281
-
282
- | curd 现状能力 | v2 覆盖 | 说明 |
283
- |---|---|---|
284
- | 精确 select 列 | ✅ 3.1 EntitySchema | — |
285
- | FK label 自动展开 | ✅ 3.2 | 展开声明方式待决策 7 |
286
- | 跨表列 + leftJoin 推导 | ✅ 3.3 | 结果列 + filter + label 合并 sources |
287
- | tenant 强制参数 + WHERE | ✅ 3.4 | — |
288
- | filter 柯里化 + keyword 端点 | ✅(现状共享 `renderFilterFile`) | — |
289
- | 分页 | ✅ 收敛到 `paginate()` | curd 自写 count 与 `paginate()` 等价(clone + clearSelect) |
290
- | 单列 orderBy | ✅ `find.orderBy` | 且支持多列 |
291
- | generator 表 insert 补 PK | ✅ 现状 gen-dao 已实现 | — |
292
- | 枚举列类型 | ✅ 3.1 enum 渲染 | **gen-dao 现状有 bug**(jsType 退化),v2 修复 |
293
- | 类导出形态 | ✅ 一致 | `export default class` + `export const xxxDao = new XxxDao()` 两边相同 |
294
-
295
- **行为差异点(统一后变化,消费方需同步)**:
296
-
297
- 1. **get 的 miss 返回**:curd 现状 `Promise<Row | undefined>`,gen-dao 现状 `Promise<Row | null>`——v2 统一 `| null`,curd 的 service/controller 生成器的 undefined 判断改为 null 判断;
298
- 2. **PagedRows import 来源**:curd 从 `@pylonts/core`,gen-dao 从 `@pylonts/dao`——统一 `@pylonts/dao`;
299
- 3. **select 列 qualified**:curd detail 全 table-qualified(`merchant.id`),page 单表裸列——v2 规则:单表裸列、多表重名列 alias(3.1),多表唯一列 qualified 裸名。
300
-
301
- **新增校验**(defineDao/loadDaos):
302
-
303
- | 校验 | 规则 |
304
- |------|------|
305
- | 写方法单表 | insert/update 的 args 列必须全部来自 `dao.table` |
306
- | 键列属于主表 | get/delete 的 args 字段必须属于 `dao.table`(跨表键不走 get/delete) |
307
- | 读方法 FK | find/get 结果列的跨表 source 必须有 main-table FK 指向,否则报错 |
308
- | 租户一致性 | app 有 tenant 且 table 有 FK → 方法必须能注入(机器推导,无声明可错) |
309
-
310
- ## 5. 产物形态(gen-dao 输出样例)
311
-
312
- ```ts
313
- // {api}/src/modules/{app}/dao/MerchantDao.ts
314
- import { knex, paginate, type PagedRows } from '@pylonts/dao';
315
- import { merchantListFilter, type MerchantListFilterArgs } from '../filter/MerchantListFilter';
316
-
317
- export interface MerchantListRow { // EntitySchema.columns 渲染
318
- id: string;
319
- name: string;
320
- status: MerchantStatus;
321
- shop_name: string; // FK label 别名(推导)
322
- }
323
-
324
- export default class MerchantDao {
325
- // 业务名 page,机制 find + mode page
326
- async page(page: number, pageSize: number, filter: MerchantListFilterArgs, shopId: string): Promise<PagedRows<MerchantListRow>> {
327
- return paginate<MerchantListRow>(
328
- merchantListFilter(filter)(knex('merchant')
329
- .leftJoin('shop', 'shop.id', 'merchant.shop_id')) // renderJoins(结果列 + filter 条件合并)
330
- .select('merchant.id', 'merchant.name', 'merchant.status', 'shop.name as shop_name')
331
- .where('merchant.shop_id', '=', shopId), // tenant 无条件 WHERE
332
- page, pageSize,
333
- );
334
- }
335
-
336
- // 业务名 get,机制 get(单表 EntitySchema)
337
- async get(id: string, shopId: string): Promise<MerchantGetRow | null> {
338
- return knex('merchant')
339
- .select('merchant.id', 'merchant.name')
340
- .where({ id })
341
- .where('merchant.shop_id', '=', shopId)
342
- .first();
343
- }
344
-
345
- // 业务名 detail,机制 get(多表 EntitySchema)——同一机制的第二业务方法
346
- async detail(id: string, shopId: string): Promise<MerchantListRow | null> {
347
- return knex('merchant')
348
- .leftJoin('shop', 'shop.id', 'merchant.shop_id')
349
- .select('merchant.id', 'merchant.name', 'shop.name as shop_name')
350
- .where({ id })
351
- .where('merchant.shop_id', '=', shopId)
352
- .first();
353
- }
354
-
355
- // update + tenant
356
- async update(row: MerchantUpdateRow, shopId: string): Promise<number> {
357
- const { id, shop_id, ...data } = row;
358
- return knex('merchant').where({ id }).where('shop_id', '=', shopId).update(data);
359
- }
360
- }
361
- ```
362
-
363
- ## 6. 消费方连锁影响
364
-
365
- | 消费方 | 影响 |
366
- |--------|------|
367
- | curd service/controller/page 生成器 | 行类型 import 从 `../entities/{Pascal}Entity` 改为 DAO 文件导出(或统一 entities 目录,待决策);方法签名变化(tenant 参数) |
368
- | curd entity 生成器 | 退役:`entities/{Pascal}Entity.ts` 的 ListRow/AddRow/UpdateRow 改为产出 `entity_schema/{api}/{app}/entity/{table}.entity.ts` 声明(文件名=表名的机器规则正好容纳) |
369
- | curd dao/dao_utils | `createSqlBuilder`/`buildJoins`/`renderPageResult`(自写 count 分页)全部退役,产物语义移入 gen-dao |
370
- | flow(`FlowMethodRef` 含 DaoMethodSchema) | dao 方法签名变(tenant 参数)→ flow invoke 的 args 槽位渲染与 service 绑定校验同步 |
371
- | lint dao / entity-check | 新增写方法单表、读方法 FK、别名唯一、租户一致性检查 |
372
-
373
- ## 7. 分阶段落地
374
-
375
- | 阶段 | 内容 | 产出 |
376
- |------|------|------|
377
- | 1 | dsl:`EntitySchema`(`columns: Field[]`,alias 隐式推导)、find/get 的 results 升级、get/delete args 放宽为 `Field \| Field[]`(任意键组合/复合 PK)+ get `where?: FilterSchema`、`ValueExpr` 表达式模型(3.6,update set + 条件右值)、`TableSchema.version` 乐观锁一等公民(3.7)、写方法单表/读方法 FK 校验、tenant 语义文档化 | `dsl/src/db.ts`、`dsl/src/dao.ts`、`dsl/src/entity.ts`(或新 row.ts)、`dsl/src/filter.ts`、`dsl/src/expr.ts`(或并入 dao.ts)+ 测试 |
378
- | 2 | gen:`renderJoins` 泛化、alias 推导(map 计数重名列 + `{表短语 ?? 表名}_{列名}` 公式)、gen-dao 按 v2 升级(精确列 select、get 多表、tenant 注入、分页统一 paginate)+ 测试 | `gen/src/gen-dao.ts`、`gen/src/filter-render.ts` |
379
- | 3 | curd:dao/entity 生成器改为产出 dao_schema + entity_schema 声明;service/controller/page import 改向;自写 count/join 代码退役 | `curd/src/dao.ts`、`curd/src/entity.ts` 等 |
380
- | 4 | flow invoke 同步 + lint dao 新规则 + examples 全量回归 | cli lint、examples |
381
-
382
- **阶段 2 之后、阶段 3 之前存在过渡期**:gen-dao 已升级,curd 仍直产实现(旧语义)——filter 模块文件继续幂等共享,不冲突。阶段 3 完成即两条路径合一。
383
-
384
- ## 7.1 复杂项目验证(思维实验:cca-pay trans)
385
-
386
- 用真实支付项目 `cca-back-server/cca-pay-parent/cca-pay` 的核心业务(zoom `dao.ar` ActiveRecord 形态)对照 v2 能力:
387
-
388
- | cca-pay 现状(zoom AR) | dao_schema v2 表达 | 覆盖 |
389
- |---|---|---|
390
- | `dao.ar(Pay.class).insert(pay)` | `insert`(args 精确列) | ✅ |
391
- | `.filter("state\|chId\|chTime\|errMsg\|errCode").update(pay)`(列白名单 update) | `update` args 精确列 | ✅ |
392
- | `.where("state", success).filter("state").update(pay)`(乐观锁条件更新,判影响行数) | `update` + `where?: FilterSchema` | ✅ |
393
- | `.get(id)` / `.where("id", id).get()` | `get` 单键 | ✅ |
394
- | `.where("devId", devId).where("devSeq", devSeq).get()`(组合键) | `get` args `Field[]`(v2 放宽) | ✅ |
395
- | `.where("state", success).get(id)`(PK + 状态条件取单行) | `get` + `where?: FilterSchema`(本次补充) | ✅ |
396
- | `.where(...).count() > 0`(contains) | `aggregate` count 或 `find` + limit 1 | ✅ |
397
- | `fill()` 可选条件分页 + 日期范围(GTE/LTE) | `find` + filter(optional 条件 + `op: 'gte'/'lte'`) | ✅ |
398
- | `orderBy("id", DESC)` + `.page(...)` | `find.orderBy` + `mode: 'page'` | ✅ |
399
- | `PayStatistics`(count(\*) + sum(amt) 聚合投影) | `aggregate` + `Compute.count/sum` | ✅ |
400
- | 同一查询多投影(Pay 全行 / 子集 / 统计) | 多个方法共享同一 filter,EntitySchema 各异 | ✅ |
401
- | 一表多实体(Pay / PayForRefund / PayStatistics 同表 t_pay) | 多 EntitySchema/EntitySchema 绑定同表 | ✅ |
402
- | enum 存 ordinal(int) | enum `valueType: 'integer'` | ✅ |
403
- | `@ColumnIgnore`(exception 不落库) | 列不进 EntitySchema 即可 | ✅ |
404
- | ID 生成器(时间 + 序号 snowflake 变体) | `table.generator` + `@pylonts/id-gen` registry | ✅ |
405
- | `@Trans` / `@EventNotifier` | `@pylonts/dao` `@Trans` / `@pylonts/event` | ✅ |
406
- | `dao.ar(Pay.class, table)`(表名运行时参数) | dao 绑定固定 table | ❌ 待决策 8(当前全传 baseTable,防御性预留) |
407
- | `@LockKey`(分布式锁)/ `@CacheKey`(缓存) | 无 | ❌ 非 dao 范围(service 层能力,flow 建模时会遇到) |
408
-
409
- **结论**:trans 核心业务的 DAO 形态 16/18 可表达;两个缺口——动态表名(当前无真实分表需求,待决策 8)与分布式锁/缓存(dao 生成范围之外)。乐观锁更新与"PK+条件取单行"促使本次补充 `get.where?: FilterSchema`。
410
-
411
- ## 7.2 复杂项目验证(思维实验:v-pay 虚拟卡支付)
412
-
413
- `v-pay-impl`(zoom AR 形态,比 trans 更重:批量、IN、NULL、聚合粒度 DAO):
414
-
415
- | v-pay 现状(zoom AR) | dao_schema v2 表达 | 覆盖 |
416
- |---|---|---|
417
- | `whereIn("bsUsrId", ids)` / `whereIn("id", cardIds)`(IN 条件,高频) | ❌ Operator 无 `in` | **缺口 1:Operator + `'in'`** |
418
- | `whereNull("nextSendTime")`(NULL 条件) | ❌ 无条件位 | **缺口 2:Operator + `'null'/'notNull'`** |
419
- | `valueList("id", Integer.class)`(单列投影 List) | ❌ find.results 只有 EntitySchema | **缺口 3:results 支持单 Field(单列列表)** |
420
- | `@Batch` 批量 update(filter + List)/ 批量 insert(`ar.insert(list)`) | ❌ 单行方法 | **待决策 10(批量机制)** |
421
- | `filter(...).ignoreNull(false).update`(null 跳过/动态部分更新) | ❌ set 列静态声明 | **待决策 11(update null 语义)** |
422
- | INNER JOIN 实体(`builder(VOp.class).join(INNER, "v_usr_op", ...)`) | ❌ join 推导全 LEFT | **待决策 12(join 类型 left/inner 声明)** |
423
- | 聚合粒度 DAO(VOpDaoImpl 一个类管 v_op + v_usr_op + v_op_detail 三表) | ⚠️ dao_schema 一 dao 一表 | **边界确认:多表编排 = service + 未来 Repository,dao_schema 不收编** |
424
- | `getByBsIdAndIds`(eq + in 组合) | 缺口 1 补上后 ✅ | — |
425
- | 插入冲突重试(while + DuplicateEntry) | insert 已够,重试为 service 层语义 | ✅ |
426
- | fill 可选条件 + orderBy + page + count | find/aggregate | ✅ |
427
- | 动态 Class 多投影 | 多方法共享 filter、EntitySchema 各异 | ✅ |
428
- | `StringUtils.join(ids, ",")`(IDs 逗号串列) | 业务数据形态,与 dao 机制无关 | ✅ |
429
-
430
- **结论**:v-pay 的单行/查询形态大部分可表达;4 个机制缺口(in、null 条件、单列投影、join 类型)+ 2 个模型级待决策(批量、update null 语义)。最重的发现是**聚合粒度 DAO**——VOpDaoImpl 是聚合仓储雏形(v_op 根 + v_usr_op 成员 + detail),这印证 dao_schema 单表原子性的边界正确:多表落库编排归 service(flow)+ @Trans,批量映射插入 = service 循环 + @Trans,未来由 Repository(aggregate.md 规划)收编。
431
-
432
- ## 7.3 参照系:zoom mapper(Java zoom-dao 的方法约定式 DAO)
433
-
434
- zoom mapper = 接口方法名约定 + 参数注解(`@Like`/`@WhereIn`/`@Condition`/`@IgnoreNull`/`@Filter`/`@Version`/`@Select`/`@OrderBy`)驱动 SQL 生成。**TS 生态无此形态**(Java 编译期注解处理的产物);TS 的成熟参数化编排 = Prisma 式参数对象(`findMany({ where, orderBy, take, skip })`)或 Drizzle/MikroORM 链式构建器。pylon dao_schema 走第三条路:显式声明 + 生成器。
435
-
436
- **两条"严格字段映射"路线的对比**(入口不同,目标相同):
437
-
438
- | | zoom @Condition | pylon ValueExpr |
439
- |---|---|---|
440
- | 入口 | SQL 片段字符串(`"name=? and (time>? or time<?)"`) | 结构化表达式 AST(`{ kind: 'bin', ... }`) |
441
- | 严格映射手段 | **SQL 分析器**:解析片段、字段名严格映射到实体列、参数化绑定(全部转化为 `a=? and b=? and c in (?,?,?)` 形式防注入) | **结构即校验**:列引用是 Field 对象(定义期归属校验),值走绑定参数——无需解析器,无解析歧义 |
442
- | 复杂度 | 高(需维护 SQL 分析器) | 低(AST 遍历 + 定义期校验) |
443
- | pylon 选择 | 不走 | **采用**(3.6) |
444
-
445
- **方向验证(zoom 实践印证 pylon 设计)**:
446
-
447
- | zoom mapper | pylon | 状态 |
448
- |---|---|---|
449
- | `@Version` → 自动 `where version=当前值 + set version+1` | 3.7 `TableSchema.version` | 设计一致 |
450
- | `@Filter("id,name")` 更新列白名单 | update args 精确列 | 同构 |
451
- | **"dao 层一次只做一次 sql 元操作,事务在 Service 层"** | dao_schema 单表原子性 + service @Trans | 核心哲学一致 |
452
- | `@Select("id")` 单列投影 List | 缺口 3 | 印证 |
453
-
454
- **新缺口(zoom 有、pylon 无)**:
455
-
456
- 1. **save / insertOrUpdate(upsert)**:`@Keys` 唯一键判断,DB 原生 upsert(返回 1=插入/2=更新);v-pay `create` 的 while + DuplicateEntry 重试是缺 upsert 的手工模拟——待决策 13;
457
- 2. **insertIgnore**:忽略唯一键冲突的插入(v-pay 高频)——与 13 合并讨论;
458
- 3. **`@IgnoreNull` 默认语义**:zoom 的 update **默认跳过 null 字段**(`@IgnoreNull(false)` 才写 null)——待决策 11 的权威参照(主流默认 = null 跳过);
459
- 4. **动态 OrderBy**:`find(OrderBy.asc("id","name"))` 运行时排序参数,pylon 只有声明静态 orderBy——待决策 14。
460
-
461
- ## 8. 待决策问题
462
-
463
- 1. ~~**alias 声明**~~(已决策:完全推导,无声明字段——多表重名列 alias,唯一列裸名,map 计数机器可判;命名公式 = `{表短语 ?? 表名}_{列名}`,字段命名本身就是短语命名)
464
- 2. ~~**insert 租户列**~~(已决策:**A 声明保证**——add 实体必须含 tenant 列(lint 校验),insert 全行插入,零生成器改动)
465
- 3. ~~**update/delete 的 tenant 参数形态**~~(已决策:**update 走 row 内提取**——tenant 列从 row destructure 进 WHERE、不进 set,与 pk/version 三种隐式列同构,签名不变;**delete 走独立参数 `(id, shopId)`**——args 是键,tenant 无处可藏)
466
- 4. ~~**行类型产出位置**~~(已决策:**统一 `entities/` 目录文件**(curd 现状被消费方引用)——gen-dao 的行类型 interface 从 DAO 文件移出)
467
- 5. ~~**app 有 tenant 但表无 FK**~~(已决策:**跳过**——该 dao 无租户,存量项目不炸)
468
- 6. ~~**get 参数顺序**~~(已决策:args 声明顺序 + tenant 最后)
469
- 7. ~~**FK label 展开声明方式**~~(已决策:**隐式展开发生在 curd → dao_schema 生成环节**——curd 把被引用表 label 列写成外部引用列进 EntitySchema.columns;dao 层无 label 概念,纯外部引用列机制(3.2))
470
- 8. **动态表名 / 同结构多表**:cca-pay 的 `dao.ar(Pay.class, table)` 支持表名运行时参数(当前全部传 baseTable,防御性预留)。dao_schema 的 dao 绑定固定 table 表达不了。选项:a 暂不支持(当前无真实分表需求,真分表时再设计);b 方法级 `tableParam?: boolean`(生成 table 参数透传 knex);c dao 绑定"表组"(同结构多表的集合声明)。
471
- 9. ~~**update 的表达式 set 与列间比较**~~(已决策:`ValueExpr` 递归 AST + `set?: SetExpr[]` + `FilterCondition.right?: ValueExpr`,见 3.6;**乐观锁为一等公民 `TableSchema.version`(3.7),不走手写 set**)
472
- 10. **批量操作**(v-pay `@Batch` 批量 update/insert):机制级批量(insert/update 的 args 支持 EntitySchema 数组,生成批量方法)vs service 层循环 + @Trans(语义等价,N 次 SQL vs 批量提交)?——**后续课题,不进本次**
473
- 11. **update 的 null 跳过语义**(v-pay `ignoreNull` 动态部分更新):args 声明 `nullSkip` 标记 vs 保持静态列(声明多个 update 方法各管各列)?**zoom mapper 参照:`@IgnoreNull` 默认 true(跳过 null 列),主流默认 = null 跳过**。——**后续课题,不进本次**
474
- 12. **join 类型**(v-pay INNER JOIN 实体):find 的跨表 join 全 LEFT;INNER(join 表过滤主表)的声明位——`renderJoins` 的 sources 加类型标记?跨表 filter 条件天然需要 inner 语义(条件必须命中)?——**后续课题,不进本次**
475
- 13. **upsert / insertIgnore**(zoom `save`/`insertOrUpdate` + `@Keys`;v-pay create 的冲突重试是手工模拟):`save` 机制(args 实体 + 唯一键集合,生成 DB 原生 `INSERT ... ON DUPLICATE KEY UPDATE`)?还是坚持 service 层组合(insert 冲突 → 重试/update)?——**后续课题,不进本次**
476
- 14. **动态 orderBy**(zoom `find(OrderBy.asc(...))` 运行时排序参数):find 的 orderBy 参数化(排序列 + 方向运行时传入,列域受限)vs 保持声明静态?——**后续课题,不进本次**
477
-
1
+ # DAO 生成标准化规划(dao_schema v2)
2
+
3
+ > 状态:**规划中(未实现)**
4
+ > 关联代码:`dsl/src/dao.ts`(DaoSchema 六方法)、`dsl/src/entity.ts`(EntitySchema 单表列)、`dsl/src/project.ts`(app.schema.tenant)、`gen/src/gen-dao.ts`(dao_schema 生成器)、`curd/src/dao.ts` + `curd/src/dao_utils.ts`(curd 直产 DAO 实现)、`dsl/src/flow.ts`(FlowMethodRef 含 DaoMethodSchema)
5
+ > 背景:curd 的 DAO 与 dao_schema 的 DAO 是两套独立生成逻辑(仅 filter 模块文件共享 `renderFilterFile`),本次规划把 DAO 生成收敛到 dao_schema 单一标准。
6
+
7
+ ## 1. 目标与原则
8
+
9
+ **目标**:DAO 实现只有一个生成入口(`gen dao`)。curd 不再直接产出 DAO 代码,改为产出 `dao_schema` + `entity_schema` 声明,由统一生成器展开实现。任何 DAO 查询语义(精确列、多表 join、租户)都先落到 dao_schema 声明能力,再进生成器。
10
+
11
+ **原则**:
12
+
13
+ 1. 声明完整:一个 dao 方法声明包含生成所需的一切(列、join、租户、分页),产物无需手工填充;
14
+ 2. 机器可校验:精确列是否单表(写方法)、跨表源是否有 FK(读方法)、租户注入是否一致——全部 lint 可查;
15
+ 3. 写读分界:写方法(insert/update)列必须单表(写不能 join);读方法(find/get/aggregate)可多表;
16
+ 4. 生成约定集中:join 推导、租户注入、FK label 别名规则只在 gen 一处,curd 声明不携带重复逻辑。
17
+
18
+ ## 2. 现状差距(curd 产物语义 → dao_schema 表达力)
19
+
20
+ | curd DAO 方法 | curd 产物语义 | dao_schema 现状 | 差距 |
21
+ |---|---|---|---|
22
+ | `page`/`list`/`query` | 精确 select 列 + FK label 别名 + 跨表列别名 + leftJoin + tenant WHERE + filter | `find`:`select *`、`EntitySchema` 单表列 | 精确列、多表行类型、别名、租户 |
23
+ | `detail` | 跨表列 + FK label + leftJoin + PK + tenant WHERE | `get`:单表 `select *` | 多表、精确列、租户 |
24
+ | `get` | own-table 精确列 + PK + tenant | `get` | 精确列、租户 |
25
+ | `insert` | 全行插入(generator 补 PK) | `insert` | ✅ 语义一致(generator 已实现) |
26
+ | `update` | `where({pk, tenantFk}).update(setCols)` | `update`:`where({pk}) + where filter` | 租户 WHERE |
27
+ | (无 delete) | — | `delete`:`where({pk})` | 租户 WHERE |
28
+ | 分页 | 自写 `clone().clearSelect().count()` + offset/limit | `paginate()`(@pylonts/dao) | 收敛到 `paginate()` |
29
+
30
+ **落地现状(2026-08-16 合并决策)**:`EntitySchema` 与 `RowSchema` 已合并为一个 schema——`EntitySchema` 承载数据库字段集合,**来源(单表/跨表)不是 schema 的责任**,由消费位置决定(写方法 args = 单表,读方法 results = 可跨表)。文件规则:一个文件对应一张表(主表),文件内可声明多个 entity,entity 名字可以 Row 化(如 `OrderListRow`),全部走 `defineEntity`。
31
+
32
+ **flow 耦合**:`FlowMethodRef` 含 `DaoMethodSchema`——service flow 的 invoke 可直接引用 dao 方法,dao 方法签名(参数形状)是 flow 契约的一部分。签名变化(如新增 tenant 参数)必须同步 flow invoke 的渲染与校验。
33
+
34
+ ## 3. 核心概念改造
35
+
36
+ ### 3.1 行类型模型:EntitySchema(结果行)
37
+
38
+ `find.results` / `get.results` 与写方法 args 统一使用 `EntitySchema`——精确列 + 跨表列,**无显式 alias 声明**:
39
+
40
+ ```ts
41
+ /** A row object: a set of database columns. Columns may come from one table
42
+ * (write args) or span tables through the main table's foreign keys (read
43
+ * results) — where the columns come from is the responsibility of the
44
+ * consuming position. */
45
+ export interface EntitySchema extends SchemaBase {
46
+ type: 'entity';
47
+ api: ProjectApiSchema;
48
+ app: FrontAppSchema;
49
+ columns: Field[]; // 任意表列(单表 = 写方法 args;多表 = 读方法 results)
50
+ }
51
+ ```
52
+
53
+ **alias 完全推导(隐式,map 计数)**:
54
+
55
+ - 单表查询(delete/insert/update 的 args、单表 get/find):列名天然唯一,**无 alias**,SELECT/接口都用裸列名;
56
+ - 多表查询(find/get 的结果列跨表):收集所有参与表的列名,map 计数——
57
+ - 列名**唯一**(只在一张表出现)→ 沿用裸列名;
58
+ - 列名**重复**(≥2 张表有同名列)→ 重名列必须带 alias;主表列优先保留裸名,跨表列 alias 按词典短语命名;
59
+ - 规则完全机器可判(列名集合 + 计数),lint 可校验产物一致性——**EntitySchema 无 alias 声明字段**。
60
+
61
+ **alias 命名公式(表短语 + 列名)**:
62
+
63
+ ```
64
+ alias = {table.phrase?.name ?? table.name} + '_' + {column.name}
65
+ ```
66
+
67
+ - 表侧:用表的实体短语(`TableSchema.phrase`,词典 EntityPhrase 条目,如 `mer`/`shop`);关联表等多实体场景无短语 → 用表名;
68
+ - 列侧:**直接用列名**——字段命名本身就是短语命名(词典约定,如 amount → `amt`、rate → `rate`),列名即短语形式,无需再挂短语引用或解析词典;
69
+ - curd 生成的外部引用列别名即此规则:`merchant`(phrase `mer`)的 `name` 列被 `order` 的查询引用 → `mer_name`。
70
+
71
+ ```ts
72
+ // 多表 find:merchant.name 与 shop.name 重名 → 跨表列 alias(表短语 + 列名)
73
+ select('merchant.id', 'merchant.name', 'shop.name as shop_name')
74
+ // 不重名场景:shop.address 唯一 → 裸名
75
+ select('merchant.id', 'shop.address')
76
+ ```
77
+
78
+ **单表硬约束落在使用位置**:写方法的 args(insert/update)在 `defineDao` 校验期强制单表列;读方法 results 可跨表(外部引用列需 FK 可达)——insert/update 的行类型语义不变。
79
+
80
+ **enum 列的类型渲染**:EntitySchema 渲染的 TS interface 中,enum 列必须用枚举 JS 名(`status: MerchantStatus`)+ 类型 import,**不得**用 `Field.jsType`(enum 的 jsType 是 `'string' | 'number'`,会丢失枚举类型)——复用 filter 产物的 `enumImportOf` 机制。**现状差距**:gen-dao 的 `renderRowInterface` 用 `c.jsType`,enum 列退化为 string/number;curd 的 entity 生成器用 jsName + import,是正确的。v2 必须对齐后者。
81
+
82
+ ### 3.1.1 聚合字段 AggregateField(aggregate 查询的输出列)
83
+
84
+ 聚合查询(`aggregate` 机制)的 results 实体中,列分两类:**普通列 = GROUP BY 维度**(一行一组),**聚合字段 = 计算输出列**。聚合字段由 `aggField(name, expr)` 构建,与普通列同处 `columns`,定义在实体文件(`{table}.entity.ts`)——dao 方法 `results` 只引用实体,从不在 dao 文件内联聚合字段:
85
+
86
+ ```ts
87
+ import { aggField, Compute } from '@pylonts/dsl';
88
+
89
+ // entity_schema/{api}/{app}/entity/order.entity.ts
90
+ export const orderStatusStats = defineEntity({
91
+ name: 'OrderStatusStats',
92
+ api, app,
93
+ columns: [
94
+ order.columns.status, // GROUP BY 维度(普通列)
95
+ aggField('total', Compute.count()), // count → jsType: 'number'
96
+ aggField('sumAmt', Compute.sum(order.columns.amount)), // sum(decimal) → jsType: 'string'
97
+ ],
98
+ });
99
+ ```
100
+
101
+ - `AggregateField` 是 `Field` 联合成员(`type: 'aggregate'`),实体可像任何列一样携带它;构造器 `aggField(name, expr)`,表达式 `Compute.count()` / `Compute.sum(field)` / `Compute.avg(field)`;
102
+ - **`jsType` 两态**,由表达式推导:`count()` 与整数列的 `sum/avg` → `'number'`;`decimal`/`bigint`/`rate` 列的 `sum/avg` → `'string'`(精度串,JS number 丢精度);
103
+ - 驱动规则(mysql2 `supportBigNumbers` 开启):COUNT/SUM/AVG 一律以 string 返回,生成物按 `jsType` 决定是否 `Number()` 转换(`'number'` → 转,`'string'` → 原样);
104
+ - DTO 可用 `from(entity, ...)` 投影聚合字段:`jsType === 'number'` 渲染 `Type.Number()`,否则 `Type.String()`。
105
+
106
+ ### 3.2 外部引用列(多表列)+ curd 层的 label 展开
107
+
108
+ **dao 层无 label 概念**:EntitySchema.columns 可包含**任何表的列**——属于其他表的列 = 外部引用列(external reference)。外部引用列的 join 关系由 **FK 推导**:**FK 就是 join 条件**——main table 的某个 FK 指向该列所属表,渲染 `table.column as alias` + LEFT JOIN(join 条件 = FK 的 columns/references)。alias 公式(3.1)对多表列统一生效。
109
+
110
+ **引用表可能有 label,也可能没有**——外部引用列机制不依赖 label:`merchant.name`、`merchant.id`、`merchant.created_at` 作为外部引用列,机制完全相同(FK 推导 join → alias)。label 只是 **curd 层在选择"FK 引用表上展示哪一列"时的默认来源**(有 label 用 label,无 label 用 PK),dao 生成器完全不感知 label。
111
+
112
+ ```ts
113
+ // curd list.columns = [..., order.mer_id](FK 列,mer_id 的 FK 就是 order↔merchant 的 join 条件)
114
+ // → curd 生成的 dao_schema 声明(隐式展开,选择 merchant 的 label 列作外部引用列):
115
+ results.columns = [..., order.mer_id, merchant.name /* 外部引用列:FK(order.mer_id→merchant.id) 推导 join */]
116
+ // → gen-dao 渲染(无 label 概念,纯外部引用机制):
117
+ select('order.mer_id', 'merchant.name as mer_name')
118
+ .leftJoin('merchant', 'merchant.id', 'order.mer_id')
119
+ ```
120
+
121
+ "隐式展开"的隐式发生在 **curd → dao_schema 生成环节**(待决策 7 已决策),不在 gen-dao 环节——gen-dao 拿到什么列渲染什么列,声明与产物一一对应。
122
+
123
+ ### 3.3 join 推导统一(gen 单一函数)
124
+
125
+ 规则(两边现状已一致,收敛即可):main table 通过自己的 `foreignKeys` 指向跨表 source;结果列、filter 条件、FK label 展开(3.2)涉及的跨表 source 集合 = LEFT JOIN 集合;只支持一层 join(main 直接 FK,多层不做)。现有 `renderFilterJoins` 泛化为 `renderJoins(mainTable, sources)`,find/get 的结果列与 filter 条件合并计算 sources。
126
+
127
+ ### 3.4 租户自动注入
128
+
129
+ `app.schema.tenant` 语义已在 dsl 定义("所有指向此表的 FK 的表自动获得租户作用域"),生成器实现:
130
+
131
+ | 方法 | 注入规则 |
132
+ |------|---------|
133
+ | `find`/`get`/`aggregate` | tenant FK 成为**强制方法参数**(camelCase)+ 无条件 WHERE |
134
+ | `update`/`delete` | tenant FK 进 WHERE(args 内或独立参数,待决策) |
135
+ | `insert` | 待决策(3.5) |
136
+
137
+ dao_schema **不新增声明**:tenant 从 `dao.api`/`dao.app` 的 `app.schema.tenant` + `dao.table` 的 FK 机器推导(有 tenant 表且表有 FK 才注入;无 FK = 无租户,报错还是跳过待决策)。
138
+
139
+ ### 3.5 insert 的租户列
140
+
141
+ curd 现状 insert 无 tenant 处理(examples 未启用 tenant,路径未跑过)。两条路(待决策):
142
+
143
+ - **A 声明保证**:add 行类型(EntitySchema)必须包含 tenant 列,insert 全行插入即可(零生成器改动,声明期 lint 校验 add 实体含 tenant 列);
144
+ - **B 生成器注入**:insert 方法签名加 tenant 参数,生成器 `insert({ ...row, tenant_col: tenantId })`(调用方无感知,但行类型与 DB 行不一致)。
145
+
146
+ ### 3.6 值表达式 ValueExpr(update 表达式 set + 条件右值)
147
+
148
+ **背景**:乐观锁(`version = version + 1`)、扣库存(`stock = stock - ? WHERE stock >= ?`)、列间比较(`stock > locked`)要求 set 值与条件右值是表达式,不能只是直接赋值/同名参数。受控算子列表(`{col, op:'+', value}`)封闭不可扩展,否决;SQL 字符串泄漏 SQL 不可 lint,否决。**采用声明式递归 AST——节点类型可扩展**。
149
+
150
+ ```ts
151
+ // 值表达式:列引用 / 字面量 / 方法参数 / 二元运算(递归)
152
+ export type ValueExpr =
153
+ | { kind: 'col'; field: Field } // 列引用(当前行该列)
154
+ | { kind: 'lit'; value: string | number } // 字面量
155
+ | { kind: 'param'; name: string } // 方法参数(进方法签名)
156
+ | { kind: 'bin'; op: 'add' | 'sub' | 'mul' | 'div'; left: ValueExpr; right: ValueExpr };
157
+
158
+ // update 扩展:col = expr(与 args 的直接赋值列互斥,lint 校验不重叠)
159
+ interface UpdateSchema { args: EntitySchema; set?: SetExpr[]; where?: FilterSchema }
160
+ interface SetExpr { col: Field; expr: ValueExpr }
161
+
162
+ // 条件右值:缺省 = 现状同名参数语义(零破坏);显式 = 表达式(列间比较/常量/运算)
163
+ interface FilterCondition { field: Field; op?: Operator; right?: ValueExpr; optional?: boolean }
164
+
165
+ // 便利函数:incr(col, by) / decr(col, by)(by = ValueExpr)
166
+ ```
167
+
168
+ **需求覆盖矩阵**:
169
+
170
+ | 场景 | 声明 | 生成 SQL |
171
+ |---|---|---|
172
+ | 乐观锁 | **一等公民(3.7)**:`TableSchema.version` 声明后,update 自动合成——无需手写 set | `SET ..., version = version + 1 WHERE id = ? AND version = ?`(affected=0 即冲突,重试/报错为调用方语义) |
173
+ | 扣库存(防超卖) | `set: [{ col: stock, expr: bin('sub', col(stock), param('qty')) }]` + `where`(stock gte) | `SET stock = stock - ? WHERE id = ? AND stock >= ?` |
174
+ | 扣款 | 同上(balance - param('amt')) | `SET balance = balance - ? WHERE balance >= ?` |
175
+ | 列间运算 | `bin('sub', col(stock), col(locked))` | `SET stock = stock - locked` |
176
+ | 状态机 CAS | args 直接赋值 + `where`(现状已支持) | `SET state = 'refund' WHERE id = ? AND state = 'success'` |
177
+
178
+ **机器校验(lint)**:列引用属于 `dao.table` 或 args 实体;param 名唯一且进签名;lit 类型与列类型相容;bin 左右类型相容。
179
+
180
+ **安全**:AST → SQL 全受控(列名来自 Field、op 封闭、值走 knex 绑定参数),无注入面。
181
+
182
+ **扩展性**:未来加节点 = 加 kind(如 `{ kind: 'fn', name: 'greatest', args: ValueExpr[] }`、一元取负),不动已有结构、不改存量声明——AST 的价值所在。
183
+
184
+ ### 3.7 乐观锁一等公民(TableSchema.version)
185
+
186
+ 乐观锁是表级属性(版本列属于表,不随方法变化),声明收敛到 `TableSchema.version`,update 自动合成版本自增 + 版本条件——不要求每个方法手写 `incr(version, lit(1))`。
187
+
188
+ ```ts
189
+ defineTable({
190
+ name: 'order',
191
+ columns: { ..., version: { type: 'integer' } },
192
+ version: order.columns.version, // 乐观锁版本列(列引用)
193
+ });
194
+ ```
195
+
196
+ **语义**:
197
+
198
+ | 机制 | 行为 |
199
+ |---|---|
200
+ | `TableSchema.version?: Field` | 指向本表一个整数列;定义期校验:属于本表 + 类型为 integer/number |
201
+ | update(有 version 的表) | args 实体**必须含 version 列**(lint);生成器从 row 提取 version 进 WHERE,set 自动合成 `version = version + 1`,其余列直接赋值——签名不变 `update(row)`,返回 affected rows,0 = 冲突(重试/报错为调用方语义) |
202
+ | insert | 照旧——version 初始值(0/1)由调用方在行数据里给,生成器不注入 |
203
+ | delete / get / find | 不受影响(乐观锁 delete 如需要,另议) |
204
+ | 与 `set?: SetExpr[]` 的关系 | 互斥——version 列由一等公民机制管理,set 表达式不得再引用 version 列(lint) |
205
+
206
+ **与 tenant 形态一致**:row 里的 version 列与 row 里的 tenant FK 列同构——都是"行内列既进 WHERE 又不在 set"(destructure 后进 where),签名都只有一个 row。三种隐式列(pk / tenant FK / version)的 WHERE 提取是同一生成器函数。
207
+
208
+ **价值**:乐观锁零声明成本(表声明一次,所有 update 自动生效)、不可遗漏(lint 强制 args 含 version 列)、调用方直接受影响行数判断冲突。
209
+
210
+ ## 4. dao_schema v2 方法形态
211
+
212
+ ```ts
213
+ // find — 精确列 + 多表 + 分页 + filter
214
+ find: {
215
+ type: 'find',
216
+ args?: FilterSchema, // 查询条件(现状不变)
217
+ results: EntitySchema, // SELECT 列 + 行类型(可跨表)
218
+ mode?: 'page' | 'limit', // 分页(现状不变)
219
+ orderBy?: OrderBySchema | OrderBySchema[],
220
+ },
221
+ // 扩展点(7.2 验证倒逼):
222
+ // results: EntitySchema | Field —— Field = 单列投影(valueList:List<值>)
223
+ // Operator + 'in' / 'null' / 'notNull'(IN 条件 / NULL 条件)
224
+ // join 类型 left/inner 声明位(filter 跨表条件 inner join 过滤主表)
225
+
226
+ // get — 机制语义:按精确键取单行(任意列组合,不限于 PK;复合 PK / getByXX 均合法)
227
+ get: {
228
+ type: 'get',
229
+ args: Field | Field[], // 单字段或 AND 精确匹配的多字段(复合 PK)
230
+ where?: FilterSchema, // 附加条件(AND 到键上):"PK + 状态条件"取单行 / 乐观锁读取
231
+ results: EntitySchema, // 可多表
232
+ },
233
+
234
+ // insert — 精确列(现状语义已对,不变)
235
+ insert: { type: 'insert', args: EntitySchema },
236
+
237
+ // update — 精确 set 列 + PK + tenant(+where filter 现状不变)
238
+ update: { type: 'update', args: EntitySchema, where?: FilterSchema },
239
+ // 表达式能力(3.6 ValueExpr 模型):set?: SetExpr[](col = expr)、FilterCondition.right?: ValueExpr
240
+
241
+ // delete — 按精确键删除(复合 PK 支持)
242
+ delete: { type: 'delete', args: Field | Field[] },
243
+
244
+ // aggregate — filter + tenant + filter 跨表 join(现状 + tenant)
245
+ aggregate: { type: 'aggregate', args?: FilterSchema, results: EntitySchema },
246
+ ```
247
+
248
+ **方法名与机制 type 正交**:dao_schema 是**低级机制**——`type` 只表达"怎么查"(按 PK 取单行 / 条件列表 / 写 / 删 / 聚合),六种机制封闭,不再随业务新增;方法名(methods map 的 key)是**业务语义**,自由命名,生成器原样保留。同一个机制可以有多个业务方法,每个方法有各自的 args/results:
249
+
250
+ ```ts
251
+ defineDao({
252
+ name: 'MerchantDao',
253
+ table: merchantTable,
254
+ methods: {
255
+ get: { type: 'get', args: pk, results: merchantGetRow }, // curd get:单表精确列
256
+ detail: { type: 'get', args: pk, results: merchantDetailRow }, // curd detail:多表 + label
257
+ getByOrderNo: { type: 'get', args: orderNoField, results: merchantGetRow }, // 任意列精确取单行
258
+ getByCombo: { type: 'get', args: [orderNoField, merIdField], results: merchantGetRow }, // 多字段 AND(复合 PK 同理)
259
+ page: { type: 'find', mode: 'page', ... }, // curd page
260
+ list: { type: 'find', ... }, // curd list
261
+ query: { type: 'find', ... }, // curd keyword query
262
+ },
263
+ });
264
+ ```
265
+
266
+ 因此 dao 语义**不需要**为业务延伸出新 type(如 `getDetail`)——延伸发生在方法名与键组合层面:`get` 的 args 是"精确键"(任意列或列组合),`getByXX`、复合 PK 都是合法声明。curd 生成 dao_schema 时业务方法名原样保留(`page`/`list`/`query`/`get`/`detail`),service/controller 对 `dao.page(...)`、`dao.detail(...)` 的引用签名不变。
267
+
268
+ **curd 方法 → dao_schema 映射**:
269
+
270
+ | curd 业务方法 | 机制 type | 差异 |
271
+ |---|---|---|
272
+ | `page` | `find` + `mode: 'page'` | `table.paginated === true` 时 curd 生成 page;映射规则:paginated → `mode: 'page'`(方法名 page),非 paginated → 无 mode(方法名 list) |
273
+ | `list` | `find` | — |
274
+ | `query`(keyword) | `find`(同一 filter) | 无分页参数;keyword 存在才生成 |
275
+ | `get` | `get`(单表 EntitySchema) | — |
276
+ | `detail` | `get`(多表 EntitySchema) | 单表 vs 多表只是 EntitySchema 内容差异 |
277
+ | `insert` / `update` | `insert` / `update` | — |
278
+ | (无 delete) | `delete` 机制保留并补租户 | — |
279
+
280
+ **对齐检查:v2 能力 ≥ curd 现状(逐项)**:
281
+
282
+ | curd 现状能力 | v2 覆盖 | 说明 |
283
+ |---|---|---|
284
+ | 精确 select 列 | ✅ 3.1 EntitySchema | — |
285
+ | FK label 自动展开 | ✅ 3.2 | 展开声明方式待决策 7 |
286
+ | 跨表列 + leftJoin 推导 | ✅ 3.3 | 结果列 + filter + label 合并 sources |
287
+ | tenant 强制参数 + WHERE | ✅ 3.4 | — |
288
+ | filter 柯里化 + keyword 端点 | ✅(现状共享 `renderFilterFile`) | — |
289
+ | 分页 | ✅ 收敛到 `paginate()` | curd 自写 count 与 `paginate()` 等价(clone + clearSelect) |
290
+ | 单列 orderBy | ✅ `find.orderBy` | 且支持多列 |
291
+ | generator 表 insert 补 PK | ✅ 现状 gen-dao 已实现 | — |
292
+ | 枚举列类型 | ✅ 3.1 enum 渲染 | **gen-dao 现状有 bug**(jsType 退化),v2 修复 |
293
+ | 类导出形态 | ✅ 一致 | `export default class` + `export const xxxDao = new XxxDao()` 两边相同 |
294
+
295
+ **行为差异点(统一后变化,消费方需同步)**:
296
+
297
+ 1. **get 的 miss 返回**:curd 现状 `Promise<Row | undefined>`,gen-dao 现状 `Promise<Row | null>`——v2 统一 `| null`,curd 的 service/controller 生成器的 undefined 判断改为 null 判断;
298
+ 2. **PagedRows import 来源**:curd 从 `@pylonts/core`,gen-dao 从 `@pylonts/dao`——统一 `@pylonts/dao`;
299
+ 3. **select 列 qualified**:curd detail 全 table-qualified(`merchant.id`),page 单表裸列——v2 规则:单表裸列、多表重名列 alias(3.1),多表唯一列 qualified 裸名。
300
+
301
+ **新增校验**(defineDao/loadDaos):
302
+
303
+ | 校验 | 规则 |
304
+ |------|------|
305
+ | 写方法单表 | insert/update 的 args 列必须全部来自 `dao.table` |
306
+ | 键列属于主表 | get/delete 的 args 字段必须属于 `dao.table`(跨表键不走 get/delete) |
307
+ | 读方法 FK | find/get 结果列的跨表 source 必须有 main-table FK 指向,否则报错 |
308
+ | 租户一致性 | app 有 tenant 且 table 有 FK → 方法必须能注入(机器推导,无声明可错) |
309
+
310
+ ## 5. 产物形态(gen-dao 输出样例)
311
+
312
+ ```ts
313
+ // {api}/src/modules/{app}/dao/MerchantDao.ts
314
+ import { knex, paginate, type PagedRows } from '@pylonts/dao';
315
+ import { merchantListFilter, type MerchantListFilterArgs } from '../filter/MerchantListFilter';
316
+
317
+ export interface MerchantListRow { // EntitySchema.columns 渲染
318
+ id: string;
319
+ name: string;
320
+ status: MerchantStatus;
321
+ shop_name: string; // FK label 别名(推导)
322
+ }
323
+
324
+ export default class MerchantDao {
325
+ // 业务名 page,机制 find + mode page
326
+ async page(page: number, pageSize: number, filter: MerchantListFilterArgs, shopId: string): Promise<PagedRows<MerchantListRow>> {
327
+ return paginate<MerchantListRow>(
328
+ merchantListFilter(filter)(knex('merchant')
329
+ .leftJoin('shop', 'shop.id', 'merchant.shop_id')) // renderJoins(结果列 + filter 条件合并)
330
+ .select('merchant.id', 'merchant.name', 'merchant.status', 'shop.name as shop_name')
331
+ .where('merchant.shop_id', '=', shopId), // tenant 无条件 WHERE
332
+ page, pageSize,
333
+ );
334
+ }
335
+
336
+ // 业务名 get,机制 get(单表 EntitySchema)
337
+ async get(id: string, shopId: string): Promise<MerchantGetRow | null> {
338
+ return knex('merchant')
339
+ .select('merchant.id', 'merchant.name')
340
+ .where({ id })
341
+ .where('merchant.shop_id', '=', shopId)
342
+ .first();
343
+ }
344
+
345
+ // 业务名 detail,机制 get(多表 EntitySchema)——同一机制的第二业务方法
346
+ async detail(id: string, shopId: string): Promise<MerchantListRow | null> {
347
+ return knex('merchant')
348
+ .leftJoin('shop', 'shop.id', 'merchant.shop_id')
349
+ .select('merchant.id', 'merchant.name', 'shop.name as shop_name')
350
+ .where({ id })
351
+ .where('merchant.shop_id', '=', shopId)
352
+ .first();
353
+ }
354
+
355
+ // update + tenant
356
+ async update(row: MerchantUpdateRow, shopId: string): Promise<number> {
357
+ const { id, shop_id, ...data } = row;
358
+ return knex('merchant').where({ id }).where('shop_id', '=', shopId).update(data);
359
+ }
360
+ }
361
+ ```
362
+
363
+ ## 6. 消费方连锁影响
364
+
365
+ | 消费方 | 影响 |
366
+ |--------|------|
367
+ | curd service/controller/page 生成器 | 行类型 import 从 `../entities/{Pascal}Entity` 改为 DAO 文件导出(或统一 entities 目录,待决策);方法签名变化(tenant 参数) |
368
+ | curd entity 生成器 | 退役:`entities/{Pascal}Entity.ts` 的 ListRow/AddRow/UpdateRow 改为产出 `entity_schema/{api}/{app}/entity/{table}.entity.ts` 声明(文件名=表名的机器规则正好容纳) |
369
+ | curd dao/dao_utils | `createSqlBuilder`/`buildJoins`/`renderPageResult`(自写 count 分页)全部退役,产物语义移入 gen-dao |
370
+ | flow(`FlowMethodRef` 含 DaoMethodSchema) | dao 方法签名变(tenant 参数)→ flow invoke 的 args 槽位渲染与 service 绑定校验同步 |
371
+ | lint dao / entity-check | 新增写方法单表、读方法 FK、别名唯一、租户一致性检查 |
372
+
373
+ ## 7. 分阶段落地
374
+
375
+ | 阶段 | 内容 | 产出 |
376
+ |------|------|------|
377
+ | 1 | dsl:`EntitySchema`(`columns: Field[]`,alias 隐式推导)、find/get 的 results 升级、get/delete args 放宽为 `Field \| Field[]`(任意键组合/复合 PK)+ get `where?: FilterSchema`、`ValueExpr` 表达式模型(3.6,update set + 条件右值)、`TableSchema.version` 乐观锁一等公民(3.7)、写方法单表/读方法 FK 校验、tenant 语义文档化 | `dsl/src/db.ts`、`dsl/src/dao.ts`、`dsl/src/entity.ts`(或新 row.ts)、`dsl/src/filter.ts`、`dsl/src/expr.ts`(或并入 dao.ts)+ 测试 |
378
+ | 2 | gen:`renderJoins` 泛化、alias 推导(map 计数重名列 + `{表短语 ?? 表名}_{列名}` 公式)、gen-dao 按 v2 升级(精确列 select、get 多表、tenant 注入、分页统一 paginate)+ 测试 | `gen/src/gen-dao.ts`、`gen/src/filter-render.ts` |
379
+ | 3 | curd:dao/entity 生成器改为产出 dao_schema + entity_schema 声明;service/controller/page import 改向;自写 count/join 代码退役 | `curd/src/dao.ts`、`curd/src/entity.ts` 等 |
380
+ | 4 | flow invoke 同步 + lint dao 新规则 + examples 全量回归 | cli lint、examples |
381
+
382
+ **阶段 2 之后、阶段 3 之前存在过渡期**:gen-dao 已升级,curd 仍直产实现(旧语义)——filter 模块文件继续幂等共享,不冲突。阶段 3 完成即两条路径合一。
383
+
384
+ ## 7.1 复杂项目验证(思维实验:cca-pay trans)
385
+
386
+ 用真实支付项目 `cca-back-server/cca-pay-parent/cca-pay` 的核心业务(zoom `dao.ar` ActiveRecord 形态)对照 v2 能力:
387
+
388
+ | cca-pay 现状(zoom AR) | dao_schema v2 表达 | 覆盖 |
389
+ |---|---|---|
390
+ | `dao.ar(Pay.class).insert(pay)` | `insert`(args 精确列) | ✅ |
391
+ | `.filter("state\|chId\|chTime\|errMsg\|errCode").update(pay)`(列白名单 update) | `update` args 精确列 | ✅ |
392
+ | `.where("state", success).filter("state").update(pay)`(乐观锁条件更新,判影响行数) | `update` + `where?: FilterSchema` | ✅ |
393
+ | `.get(id)` / `.where("id", id).get()` | `get` 单键 | ✅ |
394
+ | `.where("devId", devId).where("devSeq", devSeq).get()`(组合键) | `get` args `Field[]`(v2 放宽) | ✅ |
395
+ | `.where("state", success).get(id)`(PK + 状态条件取单行) | `get` + `where?: FilterSchema`(本次补充) | ✅ |
396
+ | `.where(...).count() > 0`(contains) | `aggregate` count 或 `find` + limit 1 | ✅ |
397
+ | `fill()` 可选条件分页 + 日期范围(GTE/LTE) | `find` + filter(optional 条件 + `op: 'gte'/'lte'`) | ✅ |
398
+ | `orderBy("id", DESC)` + `.page(...)` | `find.orderBy` + `mode: 'page'` | ✅ |
399
+ | `PayStatistics`(count(\*) + sum(amt) 聚合投影) | `aggregate` + `Compute.count/sum` | ✅ |
400
+ | 同一查询多投影(Pay 全行 / 子集 / 统计) | 多个方法共享同一 filter,EntitySchema 各异 | ✅ |
401
+ | 一表多实体(Pay / PayForRefund / PayStatistics 同表 t_pay) | 多 EntitySchema/EntitySchema 绑定同表 | ✅ |
402
+ | enum 存 ordinal(int) | enum `valueType: 'integer'` | ✅ |
403
+ | `@ColumnIgnore`(exception 不落库) | 列不进 EntitySchema 即可 | ✅ |
404
+ | ID 生成器(时间 + 序号 snowflake 变体) | `table.generator` + `@pylonts/id-gen` registry | ✅ |
405
+ | `@Trans` / `@EventNotifier` | `@pylonts/dao` `@Trans` / `@pylonts/event` | ✅ |
406
+ | `dao.ar(Pay.class, table)`(表名运行时参数) | dao 绑定固定 table | ❌ 待决策 8(当前全传 baseTable,防御性预留) |
407
+ | `@LockKey`(分布式锁)/ `@CacheKey`(缓存) | 无 | ❌ 非 dao 范围(service 层能力,flow 建模时会遇到) |
408
+
409
+ **结论**:trans 核心业务的 DAO 形态 16/18 可表达;两个缺口——动态表名(当前无真实分表需求,待决策 8)与分布式锁/缓存(dao 生成范围之外)。乐观锁更新与"PK+条件取单行"促使本次补充 `get.where?: FilterSchema`。
410
+
411
+ ## 7.2 复杂项目验证(思维实验:v-pay 虚拟卡支付)
412
+
413
+ `v-pay-impl`(zoom AR 形态,比 trans 更重:批量、IN、NULL、聚合粒度 DAO):
414
+
415
+ | v-pay 现状(zoom AR) | dao_schema v2 表达 | 覆盖 |
416
+ |---|---|---|
417
+ | `whereIn("bsUsrId", ids)` / `whereIn("id", cardIds)`(IN 条件,高频) | ❌ Operator 无 `in` | **缺口 1:Operator + `'in'`** |
418
+ | `whereNull("nextSendTime")`(NULL 条件) | ❌ 无条件位 | **缺口 2:Operator + `'null'/'notNull'`** |
419
+ | `valueList("id", Integer.class)`(单列投影 List) | ❌ find.results 只有 EntitySchema | **缺口 3:results 支持单 Field(单列列表)** |
420
+ | `@Batch` 批量 update(filter + List)/ 批量 insert(`ar.insert(list)`) | ❌ 单行方法 | **待决策 10(批量机制)** |
421
+ | `filter(...).ignoreNull(false).update`(null 跳过/动态部分更新) | ❌ set 列静态声明 | **待决策 11(update null 语义)** |
422
+ | INNER JOIN 实体(`builder(VOp.class).join(INNER, "v_usr_op", ...)`) | ❌ join 推导全 LEFT | **待决策 12(join 类型 left/inner 声明)** |
423
+ | 聚合粒度 DAO(VOpDaoImpl 一个类管 v_op + v_usr_op + v_op_detail 三表) | ⚠️ dao_schema 一 dao 一表 | **边界确认:多表编排 = service + 未来 Repository,dao_schema 不收编** |
424
+ | `getByBsIdAndIds`(eq + in 组合) | 缺口 1 补上后 ✅ | — |
425
+ | 插入冲突重试(while + DuplicateEntry) | insert 已够,重试为 service 层语义 | ✅ |
426
+ | fill 可选条件 + orderBy + page + count | find/aggregate | ✅ |
427
+ | 动态 Class 多投影 | 多方法共享 filter、EntitySchema 各异 | ✅ |
428
+ | `StringUtils.join(ids, ",")`(IDs 逗号串列) | 业务数据形态,与 dao 机制无关 | ✅ |
429
+
430
+ **结论**:v-pay 的单行/查询形态大部分可表达;4 个机制缺口(in、null 条件、单列投影、join 类型)+ 2 个模型级待决策(批量、update null 语义)。最重的发现是**聚合粒度 DAO**——VOpDaoImpl 是聚合仓储雏形(v_op 根 + v_usr_op 成员 + detail),这印证 dao_schema 单表原子性的边界正确:多表落库编排归 service(flow)+ @Trans,批量映射插入 = service 循环 + @Trans,未来由 Repository(aggregate.md 规划)收编。
431
+
432
+ ## 7.3 参照系:zoom mapper(Java zoom-dao 的方法约定式 DAO)
433
+
434
+ zoom mapper = 接口方法名约定 + 参数注解(`@Like`/`@WhereIn`/`@Condition`/`@IgnoreNull`/`@Filter`/`@Version`/`@Select`/`@OrderBy`)驱动 SQL 生成。**TS 生态无此形态**(Java 编译期注解处理的产物);TS 的成熟参数化编排 = Prisma 式参数对象(`findMany({ where, orderBy, take, skip })`)或 Drizzle/MikroORM 链式构建器。pylon dao_schema 走第三条路:显式声明 + 生成器。
435
+
436
+ **两条"严格字段映射"路线的对比**(入口不同,目标相同):
437
+
438
+ | | zoom @Condition | pylon ValueExpr |
439
+ |---|---|---|
440
+ | 入口 | SQL 片段字符串(`"name=? and (time>? or time<?)"`) | 结构化表达式 AST(`{ kind: 'bin', ... }`) |
441
+ | 严格映射手段 | **SQL 分析器**:解析片段、字段名严格映射到实体列、参数化绑定(全部转化为 `a=? and b=? and c in (?,?,?)` 形式防注入) | **结构即校验**:列引用是 Field 对象(定义期归属校验),值走绑定参数——无需解析器,无解析歧义 |
442
+ | 复杂度 | 高(需维护 SQL 分析器) | 低(AST 遍历 + 定义期校验) |
443
+ | pylon 选择 | 不走 | **采用**(3.6) |
444
+
445
+ **方向验证(zoom 实践印证 pylon 设计)**:
446
+
447
+ | zoom mapper | pylon | 状态 |
448
+ |---|---|---|
449
+ | `@Version` → 自动 `where version=当前值 + set version+1` | 3.7 `TableSchema.version` | 设计一致 |
450
+ | `@Filter("id,name")` 更新列白名单 | update args 精确列 | 同构 |
451
+ | **"dao 层一次只做一次 sql 元操作,事务在 Service 层"** | dao_schema 单表原子性 + service @Trans | 核心哲学一致 |
452
+ | `@Select("id")` 单列投影 List | 缺口 3 | 印证 |
453
+
454
+ **新缺口(zoom 有、pylon 无)**:
455
+
456
+ 1. **save / insertOrUpdate(upsert)**:`@Keys` 唯一键判断,DB 原生 upsert(返回 1=插入/2=更新);v-pay `create` 的 while + DuplicateEntry 重试是缺 upsert 的手工模拟——待决策 13;
457
+ 2. **insertIgnore**:忽略唯一键冲突的插入(v-pay 高频)——与 13 合并讨论;
458
+ 3. **`@IgnoreNull` 默认语义**:zoom 的 update **默认跳过 null 字段**(`@IgnoreNull(false)` 才写 null)——待决策 11 的权威参照(主流默认 = null 跳过);
459
+ 4. **动态 OrderBy**:`find(OrderBy.asc("id","name"))` 运行时排序参数,pylon 只有声明静态 orderBy——待决策 14。
460
+
461
+ ## 8. 待决策问题
462
+
463
+ 1. ~~**alias 声明**~~(已决策:完全推导,无声明字段——多表重名列 alias,唯一列裸名,map 计数机器可判;命名公式 = `{表短语 ?? 表名}_{列名}`,字段命名本身就是短语命名)
464
+ 2. ~~**insert 租户列**~~(已决策:**A 声明保证**——add 实体必须含 tenant 列(lint 校验),insert 全行插入,零生成器改动)
465
+ 3. ~~**update/delete 的 tenant 参数形态**~~(已决策:**update 走 row 内提取**——tenant 列从 row destructure 进 WHERE、不进 set,与 pk/version 三种隐式列同构,签名不变;**delete 走独立参数 `(id, shopId)`**——args 是键,tenant 无处可藏)
466
+ 4. ~~**行类型产出位置**~~(已决策:**统一 `entities/` 目录文件**(curd 现状被消费方引用)——gen-dao 的行类型 interface 从 DAO 文件移出)
467
+ 5. ~~**app 有 tenant 但表无 FK**~~(已决策:**跳过**——该 dao 无租户,存量项目不炸)
468
+ 6. ~~**get 参数顺序**~~(已决策:args 声明顺序 + tenant 最后)
469
+ 7. ~~**FK label 展开声明方式**~~(已决策:**隐式展开发生在 curd → dao_schema 生成环节**——curd 把被引用表 label 列写成外部引用列进 EntitySchema.columns;dao 层无 label 概念,纯外部引用列机制(3.2))
470
+ 8. **动态表名 / 同结构多表**:cca-pay 的 `dao.ar(Pay.class, table)` 支持表名运行时参数(当前全部传 baseTable,防御性预留)。dao_schema 的 dao 绑定固定 table 表达不了。选项:a 暂不支持(当前无真实分表需求,真分表时再设计);b 方法级 `tableParam?: boolean`(生成 table 参数透传 knex);c dao 绑定"表组"(同结构多表的集合声明)。
471
+ 9. ~~**update 的表达式 set 与列间比较**~~(已决策:`ValueExpr` 递归 AST + `set?: SetExpr[]` + `FilterCondition.right?: ValueExpr`,见 3.6;**乐观锁为一等公民 `TableSchema.version`(3.7),不走手写 set**)
472
+ 10. **批量操作**(v-pay `@Batch` 批量 update/insert):机制级批量(insert/update 的 args 支持 EntitySchema 数组,生成批量方法)vs service 层循环 + @Trans(语义等价,N 次 SQL vs 批量提交)?——**后续课题,不进本次**
473
+ 11. **update 的 null 跳过语义**(v-pay `ignoreNull` 动态部分更新):args 声明 `nullSkip` 标记 vs 保持静态列(声明多个 update 方法各管各列)?**zoom mapper 参照:`@IgnoreNull` 默认 true(跳过 null 列),主流默认 = null 跳过**。——**后续课题,不进本次**
474
+ 12. **join 类型**(v-pay INNER JOIN 实体):find 的跨表 join 全 LEFT;INNER(join 表过滤主表)的声明位——`renderJoins` 的 sources 加类型标记?跨表 filter 条件天然需要 inner 语义(条件必须命中)?——**后续课题,不进本次**
475
+ 13. **upsert / insertIgnore**(zoom `save`/`insertOrUpdate` + `@Keys`;v-pay create 的冲突重试是手工模拟):`save` 机制(args 实体 + 唯一键集合,生成 DB 原生 `INSERT ... ON DUPLICATE KEY UPDATE`)?还是坚持 service 层组合(insert 冲突 → 重试/update)?——**后续课题,不进本次**
476
+ 14. **动态 orderBy**(zoom `find(OrderBy.asc(...))` 运行时排序参数):find 的 orderBy 参数化(排序列 + 方向运行时传入,列域受限)vs 保持声明静态?——**后续课题,不进本次**
477
+
478
478
  **本次实施范围**(已拍板):v2 核心(EntitySchema / get·delete 放宽 / ValueExpr / TableSchema.version / tenant 注入)+ **Operator 'in'/'null'/'notNull'**(v-pay 高频刚需)。单列投影(缺口 3)与待决策 10-14 全部后续。