# DAO 生成标准化规划（dao_schema v2）

> 状态：**规划中（未实现）**
> 关联代码：`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）
> 背景：curd 的 DAO 与 dao_schema 的 DAO 是两套独立生成逻辑（仅 filter 模块文件共享 `renderFilterFile`），本次规划把 DAO 生成收敛到 dao_schema 单一标准。

## 1. 目标与原则

**目标**：DAO 实现只有一个生成入口（`gen dao`）。curd 不再直接产出 DAO 代码，改为产出 `dao_schema` + `entity_schema` 声明，由统一生成器展开实现。任何 DAO 查询语义（精确列、多表 join、租户）都先落到 dao_schema 声明能力，再进生成器。

**原则**：

1. 声明完整：一个 dao 方法声明包含生成所需的一切（列、join、租户、分页），产物无需手工填充；
2. 机器可校验：精确列是否单表（写方法）、跨表源是否有 FK（读方法）、租户注入是否一致——全部 lint 可查；
3. 写读分界：写方法（insert/update）列必须单表（写不能 join）；读方法（find/get/aggregate）可多表；
4. 生成约定集中：join 推导、租户注入、FK label 别名规则只在 gen 一处，curd 声明不携带重复逻辑。

## 2. 现状差距（curd 产物语义 → dao_schema 表达力）

| curd DAO 方法 | curd 产物语义 | dao_schema 现状 | 差距 |
|---|---|---|---|
| `page`/`list`/`query` | 精确 select 列 + FK label 别名 + 跨表列别名 + leftJoin + tenant WHERE + filter | `find`：`select *`、`EntitySchema` 单表列 | 精确列、多表行类型、别名、租户 |
| `detail` | 跨表列 + FK label + leftJoin + PK + tenant WHERE | `get`：单表 `select *` | 多表、精确列、租户 |
| `get` | own-table 精确列 + PK + tenant | `get` | 精确列、租户 |
| `insert` | 全行插入（generator 补 PK） | `insert` | ✅ 语义一致（generator 已实现） |
| `update` | `where({pk, tenantFk}).update(setCols)` | `update`：`where({pk}) + where filter` | 租户 WHERE |
| （无 delete） | — | `delete`：`where({pk})` | 租户 WHERE |
| 分页 | 自写 `clone().clearSelect().count()` + offset/limit | `paginate()`（@pylonts/dao） | 收敛到 `paginate()` |

**落地现状（2026-08-16 合并决策）**：`EntitySchema` 与 `RowSchema` 已合并为一个 schema——`EntitySchema` 承载数据库字段集合，**来源（单表/跨表）不是 schema 的责任**，由消费位置决定（写方法 args = 单表，读方法 results = 可跨表）。文件规则：一个文件对应一张表（主表），文件内可声明多个 entity，entity 名字可以 Row 化（如 `OrderListRow`），全部走 `defineEntity`。

**flow 耦合**：`FlowMethodRef` 含 `DaoMethodSchema`——service flow 的 invoke 可直接引用 dao 方法，dao 方法签名（参数形状）是 flow 契约的一部分。签名变化（如新增 tenant 参数）必须同步 flow invoke 的渲染与校验。

## 3. 核心概念改造

### 3.1 行类型模型：EntitySchema（结果行）

`find.results` / `get.results` 与写方法 args 统一使用 `EntitySchema`——精确列 + 跨表列，**无显式 alias 声明**：

```ts
/** A row object: a set of database columns. Columns may come from one table
 *  (write args) or span tables through the main table's foreign keys (read
 *  results) — where the columns come from is the responsibility of the
 *  consuming position. */
export interface EntitySchema extends SchemaBase {
  type: 'entity';
  api: ProjectApiSchema;
  app: FrontAppSchema;
  columns: Field[];   // 任意表列（单表 = 写方法 args；多表 = 读方法 results）
}
```

**alias 完全推导（隐式，map 计数）**：

- 单表查询（delete/insert/update 的 args、单表 get/find）：列名天然唯一，**无 alias**，SELECT/接口都用裸列名；
- 多表查询（find/get 的结果列跨表）：收集所有参与表的列名，map 计数——
  - 列名**唯一**（只在一张表出现）→ 沿用裸列名；
  - 列名**重复**（≥2 张表有同名列）→ 重名列必须带 alias；主表列优先保留裸名，跨表列 alias 按词典短语命名；
- 规则完全机器可判（列名集合 + 计数），lint 可校验产物一致性——**EntitySchema 无 alias 声明字段**。

**alias 命名公式（表短语 + 列名）**：

```
alias = {table.phrase?.name ?? table.name} + '_' + {column.name}
```

- 表侧：用表的实体短语（`TableSchema.phrase`，词典 EntityPhrase 条目，如 `mer`/`shop`）；关联表等多实体场景无短语 → 用表名；
- 列侧：**直接用列名**——字段命名本身就是短语命名（词典约定，如 amount → `amt`、rate → `rate`），列名即短语形式，无需再挂短语引用或解析词典；
- curd 生成的外部引用列别名即此规则：`merchant`（phrase `mer`）的 `name` 列被 `order` 的查询引用 → `mer_name`。

```ts
// 多表 find：merchant.name 与 shop.name 重名 → 跨表列 alias（表短语 + 列名）
select('merchant.id', 'merchant.name', 'shop.name as shop_name')
// 不重名场景：shop.address 唯一 → 裸名
select('merchant.id', 'shop.address')
```

**单表硬约束落在使用位置**：写方法的 args（insert/update）在 `defineDao` 校验期强制单表列；读方法 results 可跨表（外部引用列需 FK 可达）——insert/update 的行类型语义不变。

**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 必须对齐后者。

### 3.1.1 聚合字段 AggregateField（aggregate 查询的输出列）

聚合查询（`aggregate` 机制）的 results 实体中，列分两类：**普通列 = GROUP BY 维度**（一行一组），**聚合字段 = 计算输出列**。聚合字段由 `aggField(name, expr)` 构建，与普通列同处 `columns`，定义在实体文件（`{table}.entity.ts`）——dao 方法 `results` 只引用实体，从不在 dao 文件内联聚合字段：

```ts
import { aggField, Compute } from '@pylonts/dsl';

// entity_schema/{api}/{app}/entity/order.entity.ts
export const orderStatusStats = defineEntity({
  name: 'OrderStatusStats',
  api, app,
  columns: [
    order.columns.status,                        // GROUP BY 维度（普通列）
    aggField('total', Compute.count()),          // count → jsType: 'number'
    aggField('sumAmt', Compute.sum(order.columns.amount)), // sum(decimal) → jsType: 'string'
  ],
});
```

- `AggregateField` 是 `Field` 联合成员（`type: 'aggregate'`），实体可像任何列一样携带它；构造器 `aggField(name, expr)`，表达式 `Compute.count()` / `Compute.sum(field)` / `Compute.avg(field)`；
- **`jsType` 两态**，由表达式推导：`count()` 与整数列的 `sum/avg` → `'number'`；`decimal`/`bigint`/`rate` 列的 `sum/avg` → `'string'`（精度串，JS number 丢精度）；
- 驱动规则（mysql2 `supportBigNumbers` 开启）：COUNT/SUM/AVG 一律以 string 返回，生成物按 `jsType` 决定是否 `Number()` 转换（`'number'` → 转，`'string'` → 原样）；
- DTO 可用 `from(entity, ...)` 投影聚合字段：`jsType === 'number'` 渲染 `Type.Number()`，否则 `Type.String()`。

### 3.2 外部引用列（多表列）+ curd 层的 label 展开

**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）对多表列统一生效。

**引用表可能有 label，也可能没有**——外部引用列机制不依赖 label：`merchant.name`、`merchant.id`、`merchant.created_at` 作为外部引用列，机制完全相同（FK 推导 join → alias）。label 只是 **curd 层在选择"FK 引用表上展示哪一列"时的默认来源**（有 label 用 label，无 label 用 PK），dao 生成器完全不感知 label。

```ts
// curd list.columns = [..., order.mer_id]（FK 列，mer_id 的 FK 就是 order↔merchant 的 join 条件）
// → curd 生成的 dao_schema 声明（隐式展开，选择 merchant 的 label 列作外部引用列）：
results.columns = [..., order.mer_id, merchant.name /* 外部引用列：FK(order.mer_id→merchant.id) 推导 join */]
// → gen-dao 渲染（无 label 概念，纯外部引用机制）：
select('order.mer_id', 'merchant.name as mer_name')
  .leftJoin('merchant', 'merchant.id', 'order.mer_id')
```

"隐式展开"的隐式发生在 **curd → dao_schema 生成环节**（待决策 7 已决策），不在 gen-dao 环节——gen-dao 拿到什么列渲染什么列，声明与产物一一对应。

### 3.3 join 推导统一（gen 单一函数）

规则（两边现状已一致，收敛即可）：main table 通过自己的 `foreignKeys` 指向跨表 source；结果列、filter 条件、FK label 展开（3.2）涉及的跨表 source 集合 = LEFT JOIN 集合；只支持一层 join（main 直接 FK，多层不做）。现有 `renderFilterJoins` 泛化为 `renderJoins(mainTable, sources)`，find/get 的结果列与 filter 条件合并计算 sources。

### 3.4 租户自动注入

`app.schema.tenant` 语义已在 dsl 定义（"所有指向此表的 FK 的表自动获得租户作用域"），生成器实现：

| 方法 | 注入规则 |
|------|---------|
| `find`/`get`/`aggregate` | tenant FK 成为**强制方法参数**（camelCase）+ 无条件 WHERE |
| `update`/`delete` | tenant FK 进 WHERE（args 内或独立参数，待决策） |
| `insert` | 待决策（3.5） |

dao_schema **不新增声明**：tenant 从 `dao.api`/`dao.app` 的 `app.schema.tenant` + `dao.table` 的 FK 机器推导（有 tenant 表且表有 FK 才注入；无 FK = 无租户，报错还是跳过待决策）。

### 3.5 insert 的租户列

curd 现状 insert 无 tenant 处理（examples 未启用 tenant，路径未跑过）。两条路（待决策）：

- **A 声明保证**：add 行类型（EntitySchema）必须包含 tenant 列，insert 全行插入即可（零生成器改动，声明期 lint 校验 add 实体含 tenant 列）；
- **B 生成器注入**：insert 方法签名加 tenant 参数，生成器 `insert({ ...row, tenant_col: tenantId })`（调用方无感知，但行类型与 DB 行不一致）。

### 3.6 值表达式 ValueExpr（update 表达式 set + 条件右值）

**背景**：乐观锁（`version = version + 1`）、扣库存（`stock = stock - ? WHERE stock >= ?`）、列间比较（`stock > locked`）要求 set 值与条件右值是表达式，不能只是直接赋值/同名参数。受控算子列表（`{col, op:'+', value}`）封闭不可扩展，否决；SQL 字符串泄漏 SQL 不可 lint，否决。**采用声明式递归 AST——节点类型可扩展**。

```ts
// 值表达式：列引用 / 字面量 / 方法参数 / 二元运算（递归）
export type ValueExpr =
  | { kind: 'col';   field: Field }                             // 列引用（当前行该列）
  | { kind: 'lit';   value: string | number }                   // 字面量
  | { kind: 'param'; name: string }                             // 方法参数（进方法签名）
  | { kind: 'bin';   op: 'add' | 'sub' | 'mul' | 'div'; left: ValueExpr; right: ValueExpr };

// update 扩展：col = expr（与 args 的直接赋值列互斥，lint 校验不重叠）
interface UpdateSchema { args: EntitySchema; set?: SetExpr[]; where?: FilterSchema }
interface SetExpr { col: Field; expr: ValueExpr }

// 条件右值：缺省 = 现状同名参数语义（零破坏）；显式 = 表达式（列间比较/常量/运算）
interface FilterCondition { field: Field; op?: Operator; right?: ValueExpr; optional?: boolean }

// 便利函数：incr(col, by) / decr(col, by)（by = ValueExpr）
```

**需求覆盖矩阵**：

| 场景 | 声明 | 生成 SQL |
|---|---|---|
| 乐观锁 | **一等公民（3.7）**：`TableSchema.version` 声明后，update 自动合成——无需手写 set | `SET ..., version = version + 1 WHERE id = ? AND version = ?`（affected=0 即冲突，重试/报错为调用方语义） |
| 扣库存（防超卖） | `set: [{ col: stock, expr: bin('sub', col(stock), param('qty')) }]` + `where`（stock gte） | `SET stock = stock - ? WHERE id = ? AND stock >= ?` |
| 扣款 | 同上（balance - param('amt')） | `SET balance = balance - ? WHERE balance >= ?` |
| 列间运算 | `bin('sub', col(stock), col(locked))` | `SET stock = stock - locked` |
| 状态机 CAS | args 直接赋值 + `where`（现状已支持） | `SET state = 'refund' WHERE id = ? AND state = 'success'` |

**机器校验（lint）**：列引用属于 `dao.table` 或 args 实体；param 名唯一且进签名；lit 类型与列类型相容；bin 左右类型相容。

**安全**：AST → SQL 全受控（列名来自 Field、op 封闭、值走 knex 绑定参数），无注入面。

**扩展性**：未来加节点 = 加 kind（如 `{ kind: 'fn', name: 'greatest', args: ValueExpr[] }`、一元取负），不动已有结构、不改存量声明——AST 的价值所在。

### 3.7 乐观锁一等公民（TableSchema.version）

乐观锁是表级属性（版本列属于表，不随方法变化），声明收敛到 `TableSchema.version`，update 自动合成版本自增 + 版本条件——不要求每个方法手写 `incr(version, lit(1))`。

```ts
defineTable({
  name: 'order',
  columns: { ..., version: { type: 'integer' } },
  version: order.columns.version,   // 乐观锁版本列（列引用）
});
```

**语义**：

| 机制 | 行为 |
|---|---|
| `TableSchema.version?: Field` | 指向本表一个整数列；定义期校验：属于本表 + 类型为 integer/number |
| update（有 version 的表） | args 实体**必须含 version 列**（lint）；生成器从 row 提取 version 进 WHERE，set 自动合成 `version = version + 1`，其余列直接赋值——签名不变 `update(row)`，返回 affected rows，0 = 冲突（重试/报错为调用方语义） |
| insert | 照旧——version 初始值（0/1）由调用方在行数据里给，生成器不注入 |
| delete / get / find | 不受影响（乐观锁 delete 如需要，另议） |
| 与 `set?: SetExpr[]` 的关系 | 互斥——version 列由一等公民机制管理，set 表达式不得再引用 version 列（lint） |

**与 tenant 形态一致**：row 里的 version 列与 row 里的 tenant FK 列同构——都是"行内列既进 WHERE 又不在 set"（destructure 后进 where），签名都只有一个 row。三种隐式列（pk / tenant FK / version）的 WHERE 提取是同一生成器函数。

**价值**：乐观锁零声明成本（表声明一次，所有 update 自动生效）、不可遗漏（lint 强制 args 含 version 列）、调用方直接受影响行数判断冲突。

## 4. dao_schema v2 方法形态

```ts
// find — 精确列 + 多表 + 分页 + filter
find: {
  type: 'find',
  args?: FilterSchema,          // 查询条件（现状不变）
  results: EntitySchema,           // SELECT 列 + 行类型（可跨表）
  mode?: 'page' | 'limit',      // 分页（现状不变）
  orderBy?: OrderBySchema | OrderBySchema[],
},
// 扩展点（7.2 验证倒逼）：
//   results: EntitySchema | Field —— Field = 单列投影（valueList：List<值>）
//   Operator + 'in' / 'null' / 'notNull'（IN 条件 / NULL 条件）
//   join 类型 left/inner 声明位（filter 跨表条件 inner join 过滤主表）

// get — 机制语义：按精确键取单行（任意列组合，不限于 PK；复合 PK / getByXX 均合法）
get: {
  type: 'get',
  args: Field | Field[],        // 单字段或 AND 精确匹配的多字段（复合 PK）
  where?: FilterSchema,         // 附加条件（AND 到键上）："PK + 状态条件"取单行 / 乐观锁读取
  results: EntitySchema,           // 可多表
},

// insert — 精确列（现状语义已对，不变）
insert: { type: 'insert', args: EntitySchema },

// update — 精确 set 列 + PK + tenant（+where filter 现状不变）
update: { type: 'update', args: EntitySchema, where?: FilterSchema },
// 表达式能力（3.6 ValueExpr 模型）：set?: SetExpr[]（col = expr）、FilterCondition.right?: ValueExpr

// delete — 按精确键删除（复合 PK 支持）
delete: { type: 'delete', args: Field | Field[] },

// aggregate — filter + tenant + filter 跨表 join（现状 + tenant）
aggregate: { type: 'aggregate', args?: FilterSchema, results: EntitySchema },
```

**方法名与机制 type 正交**：dao_schema 是**低级机制**——`type` 只表达"怎么查"（按 PK 取单行 / 条件列表 / 写 / 删 / 聚合），六种机制封闭，不再随业务新增；方法名（methods map 的 key）是**业务语义**，自由命名，生成器原样保留。同一个机制可以有多个业务方法，每个方法有各自的 args/results：

```ts
defineDao({
  name: 'MerchantDao',
  table: merchantTable,
  methods: {
    get:    { type: 'get', args: pk,     results: merchantGetRow },    // curd get：单表精确列
    detail: { type: 'get', args: pk,     results: merchantDetailRow },  // curd detail：多表 + label
    getByOrderNo: { type: 'get', args: orderNoField, results: merchantGetRow },  // 任意列精确取单行
    getByCombo: { type: 'get', args: [orderNoField, merIdField], results: merchantGetRow },  // 多字段 AND（复合 PK 同理）
    page:   { type: 'find', mode: 'page', ... },   // curd page
    list:   { type: 'find', ... },                 // curd list
    query:  { type: 'find', ... },                 // curd keyword query
  },
});
```

因此 dao 语义**不需要**为业务延伸出新 type（如 `getDetail`）——延伸发生在方法名与键组合层面：`get` 的 args 是"精确键"（任意列或列组合），`getByXX`、复合 PK 都是合法声明。curd 生成 dao_schema 时业务方法名原样保留（`page`/`list`/`query`/`get`/`detail`），service/controller 对 `dao.page(...)`、`dao.detail(...)` 的引用签名不变。

**curd 方法 → dao_schema 映射**：

| curd 业务方法 | 机制 type | 差异 |
|---|---|---|
| `page` | `find` + `mode: 'page'` | `table.paginated === true` 时 curd 生成 page；映射规则：paginated → `mode: 'page'`（方法名 page），非 paginated → 无 mode（方法名 list） |
| `list` | `find` | — |
| `query`（keyword） | `find`（同一 filter） | 无分页参数；keyword 存在才生成 |
| `get` | `get`（单表 EntitySchema） | — |
| `detail` | `get`（多表 EntitySchema） | 单表 vs 多表只是 EntitySchema 内容差异 |
| `insert` / `update` | `insert` / `update` | — |
| （无 delete） | `delete` 机制保留并补租户 | — |

**对齐检查：v2 能力 ≥ curd 现状（逐项）**：

| curd 现状能力 | v2 覆盖 | 说明 |
|---|---|---|
| 精确 select 列 | ✅ 3.1 EntitySchema | — |
| FK label 自动展开 | ✅ 3.2 | 展开声明方式待决策 7 |
| 跨表列 + leftJoin 推导 | ✅ 3.3 | 结果列 + filter + label 合并 sources |
| tenant 强制参数 + WHERE | ✅ 3.4 | — |
| filter 柯里化 + keyword 端点 | ✅（现状共享 `renderFilterFile`） | — |
| 分页 | ✅ 收敛到 `paginate()` | curd 自写 count 与 `paginate()` 等价（clone + clearSelect） |
| 单列 orderBy | ✅ `find.orderBy` | 且支持多列 |
| generator 表 insert 补 PK | ✅ 现状 gen-dao 已实现 | — |
| 枚举列类型 | ✅ 3.1 enum 渲染 | **gen-dao 现状有 bug**（jsType 退化），v2 修复 |
| 类导出形态 | ✅ 一致 | `export default class` + `export const xxxDao = new XxxDao()` 两边相同 |

**行为差异点（统一后变化，消费方需同步）**：

1. **get 的 miss 返回**：curd 现状 `Promise<Row | undefined>`，gen-dao 现状 `Promise<Row | null>`——v2 统一 `| null`，curd 的 service/controller 生成器的 undefined 判断改为 null 判断；
2. **PagedRows import 来源**：curd 从 `@pylonts/core`，gen-dao 从 `@pylonts/dao`——统一 `@pylonts/dao`；
3. **select 列 qualified**：curd detail 全 table-qualified（`merchant.id`），page 单表裸列——v2 规则：单表裸列、多表重名列 alias（3.1），多表唯一列 qualified 裸名。

**新增校验**（defineDao/loadDaos）：

| 校验 | 规则 |
|------|------|
| 写方法单表 | insert/update 的 args 列必须全部来自 `dao.table` |
| 键列属于主表 | get/delete 的 args 字段必须属于 `dao.table`（跨表键不走 get/delete） |
| 读方法 FK | find/get 结果列的跨表 source 必须有 main-table FK 指向，否则报错 |
| 租户一致性 | app 有 tenant 且 table 有 FK → 方法必须能注入（机器推导，无声明可错） |

## 5. 产物形态（gen-dao 输出样例）

```ts
// {api}/src/modules/{app}/dao/MerchantDao.ts
import { knex, paginate, type PagedRows } from '@pylonts/dao';
import { merchantListFilter, type MerchantListFilterArgs } from '../filter/MerchantListFilter';

export interface MerchantListRow {           // EntitySchema.columns 渲染
  id: string;
  name: string;
  status: MerchantStatus;
  shop_name: string;                          // FK label 别名（推导）
}

export default class MerchantDao {
  // 业务名 page，机制 find + mode page
  async page(page: number, pageSize: number, filter: MerchantListFilterArgs, shopId: string): Promise<PagedRows<MerchantListRow>> {
    return paginate<MerchantListRow>(
      merchantListFilter(filter)(knex('merchant')
        .leftJoin('shop', 'shop.id', 'merchant.shop_id'))   // renderJoins（结果列 + filter 条件合并）
        .select('merchant.id', 'merchant.name', 'merchant.status', 'shop.name as shop_name')
        .where('merchant.shop_id', '=', shopId),            // tenant 无条件 WHERE
      page, pageSize,
    );
  }

  // 业务名 get，机制 get（单表 EntitySchema）
  async get(id: string, shopId: string): Promise<MerchantGetRow | null> {
    return knex('merchant')
      .select('merchant.id', 'merchant.name')
      .where({ id })
      .where('merchant.shop_id', '=', shopId)
      .first();
  }

  // 业务名 detail，机制 get（多表 EntitySchema）——同一机制的第二业务方法
  async detail(id: string, shopId: string): Promise<MerchantListRow | null> {
    return knex('merchant')
      .leftJoin('shop', 'shop.id', 'merchant.shop_id')
      .select('merchant.id', 'merchant.name', 'shop.name as shop_name')
      .where({ id })
      .where('merchant.shop_id', '=', shopId)
      .first();
  }

  // update + tenant
  async update(row: MerchantUpdateRow, shopId: string): Promise<number> {
    const { id, shop_id, ...data } = row;
    return knex('merchant').where({ id }).where('shop_id', '=', shopId).update(data);
  }
}
```

## 6. 消费方连锁影响

| 消费方 | 影响 |
|--------|------|
| curd service/controller/page 生成器 | 行类型 import 从 `../entities/{Pascal}Entity` 改为 DAO 文件导出（或统一 entities 目录，待决策）；方法签名变化（tenant 参数） |
| curd entity 生成器 | 退役：`entities/{Pascal}Entity.ts` 的 ListRow/AddRow/UpdateRow 改为产出 `entity_schema/{api}/{app}/entity/{table}.entity.ts` 声明（文件名=表名的机器规则正好容纳） |
| curd dao/dao_utils | `createSqlBuilder`/`buildJoins`/`renderPageResult`（自写 count 分页）全部退役，产物语义移入 gen-dao |
| flow（`FlowMethodRef` 含 DaoMethodSchema） | dao 方法签名变（tenant 参数）→ flow invoke 的 args 槽位渲染与 service 绑定校验同步 |
| lint dao / entity-check | 新增写方法单表、读方法 FK、别名唯一、租户一致性检查 |

## 7. 分阶段落地

| 阶段 | 内容 | 产出 |
|------|------|------|
| 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）+ 测试 |
| 2 | gen：`renderJoins` 泛化、alias 推导（map 计数重名列 + `{表短语 ?? 表名}_{列名}` 公式）、gen-dao 按 v2 升级（精确列 select、get 多表、tenant 注入、分页统一 paginate）+ 测试 | `gen/src/gen-dao.ts`、`gen/src/filter-render.ts` |
| 3 | curd：dao/entity 生成器改为产出 dao_schema + entity_schema 声明；service/controller/page import 改向；自写 count/join 代码退役 | `curd/src/dao.ts`、`curd/src/entity.ts` 等 |
| 4 | flow invoke 同步 + lint dao 新规则 + examples 全量回归 | cli lint、examples |

**阶段 2 之后、阶段 3 之前存在过渡期**：gen-dao 已升级，curd 仍直产实现（旧语义）——filter 模块文件继续幂等共享，不冲突。阶段 3 完成即两条路径合一。

## 7.1 复杂项目验证（思维实验：cca-pay trans）

用真实支付项目 `cca-back-server/cca-pay-parent/cca-pay` 的核心业务（zoom `dao.ar` ActiveRecord 形态）对照 v2 能力：

| cca-pay 现状（zoom AR） | dao_schema v2 表达 | 覆盖 |
|---|---|---|
| `dao.ar(Pay.class).insert(pay)` | `insert`（args 精确列） | ✅ |
| `.filter("state\|chId\|chTime\|errMsg\|errCode").update(pay)`（列白名单 update） | `update` args 精确列 | ✅ |
| `.where("state", success).filter("state").update(pay)`（乐观锁条件更新，判影响行数） | `update` + `where?: FilterSchema` | ✅ |
| `.get(id)` / `.where("id", id).get()` | `get` 单键 | ✅ |
| `.where("devId", devId).where("devSeq", devSeq).get()`（组合键） | `get` args `Field[]`（v2 放宽） | ✅ |
| `.where("state", success).get(id)`（PK + 状态条件取单行） | `get` + `where?: FilterSchema`（本次补充） | ✅ |
| `.where(...).count() > 0`（contains） | `aggregate` count 或 `find` + limit 1 | ✅ |
| `fill()` 可选条件分页 + 日期范围（GTE/LTE） | `find` + filter（optional 条件 + `op: 'gte'/'lte'`） | ✅ |
| `orderBy("id", DESC)` + `.page(...)` | `find.orderBy` + `mode: 'page'` | ✅ |
| `PayStatistics`（count(\*) + sum(amt) 聚合投影） | `aggregate` + `Compute.count/sum` | ✅ |
| 同一查询多投影（Pay 全行 / 子集 / 统计） | 多个方法共享同一 filter，EntitySchema 各异 | ✅ |
| 一表多实体（Pay / PayForRefund / PayStatistics 同表 t_pay） | 多 EntitySchema/EntitySchema 绑定同表 | ✅ |
| enum 存 ordinal（int） | enum `valueType: 'integer'` | ✅ |
| `@ColumnIgnore`（exception 不落库） | 列不进 EntitySchema 即可 | ✅ |
| ID 生成器（时间 + 序号 snowflake 变体） | `table.generator` + `@pylonts/id-gen` registry | ✅ |
| `@Trans` / `@EventNotifier` | `@pylonts/dao` `@Trans` / `@pylonts/event` | ✅ |
| `dao.ar(Pay.class, table)`（表名运行时参数） | dao 绑定固定 table | ❌ 待决策 8（当前全传 baseTable，防御性预留） |
| `@LockKey`（分布式锁）/ `@CacheKey`（缓存） | 无 | ❌ 非 dao 范围（service 层能力，flow 建模时会遇到） |

**结论**：trans 核心业务的 DAO 形态 16/18 可表达；两个缺口——动态表名（当前无真实分表需求，待决策 8）与分布式锁/缓存（dao 生成范围之外）。乐观锁更新与"PK+条件取单行"促使本次补充 `get.where?: FilterSchema`。

## 7.2 复杂项目验证（思维实验：v-pay 虚拟卡支付）

`v-pay-impl`（zoom AR 形态，比 trans 更重：批量、IN、NULL、聚合粒度 DAO）：

| v-pay 现状（zoom AR） | dao_schema v2 表达 | 覆盖 |
|---|---|---|
| `whereIn("bsUsrId", ids)` / `whereIn("id", cardIds)`（IN 条件，高频） | ❌ Operator 无 `in` | **缺口 1：Operator + `'in'`** |
| `whereNull("nextSendTime")`（NULL 条件） | ❌ 无条件位 | **缺口 2：Operator + `'null'/'notNull'`** |
| `valueList("id", Integer.class)`（单列投影 List） | ❌ find.results 只有 EntitySchema | **缺口 3：results 支持单 Field（单列列表）** |
| `@Batch` 批量 update（filter + List）/ 批量 insert（`ar.insert(list)`） | ❌ 单行方法 | **待决策 10（批量机制）** |
| `filter(...).ignoreNull(false).update`（null 跳过/动态部分更新） | ❌ set 列静态声明 | **待决策 11（update null 语义）** |
| INNER JOIN 实体（`builder(VOp.class).join(INNER, "v_usr_op", ...)`） | ❌ join 推导全 LEFT | **待决策 12（join 类型 left/inner 声明）** |
| 聚合粒度 DAO（VOpDaoImpl 一个类管 v_op + v_usr_op + v_op_detail 三表） | ⚠️ dao_schema 一 dao 一表 | **边界确认：多表编排 = service + 未来 Repository，dao_schema 不收编** |
| `getByBsIdAndIds`（eq + in 组合） | 缺口 1 补上后 ✅ | — |
| 插入冲突重试（while + DuplicateEntry） | insert 已够，重试为 service 层语义 | ✅ |
| fill 可选条件 + orderBy + page + count | find/aggregate | ✅ |
| 动态 Class 多投影 | 多方法共享 filter、EntitySchema 各异 | ✅ |
| `StringUtils.join(ids, ",")`（IDs 逗号串列） | 业务数据形态，与 dao 机制无关 | ✅ |

**结论**：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 规划）收编。

## 7.3 参照系：zoom mapper（Java zoom-dao 的方法约定式 DAO）

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 走第三条路：显式声明 + 生成器。

**两条"严格字段映射"路线的对比**（入口不同，目标相同）：

| | zoom @Condition | pylon ValueExpr |
|---|---|---|
| 入口 | SQL 片段字符串（`"name=? and (time>? or time<?)"`） | 结构化表达式 AST（`{ kind: 'bin', ... }`） |
| 严格映射手段 | **SQL 分析器**：解析片段、字段名严格映射到实体列、参数化绑定（全部转化为 `a=? and b=? and c in (?,?,?)` 形式防注入） | **结构即校验**：列引用是 Field 对象（定义期归属校验），值走绑定参数——无需解析器，无解析歧义 |
| 复杂度 | 高（需维护 SQL 分析器） | 低（AST 遍历 + 定义期校验） |
| pylon 选择 | 不走 | **采用**（3.6） |

**方向验证（zoom 实践印证 pylon 设计）**：

| zoom mapper | pylon | 状态 |
|---|---|---|
| `@Version` → 自动 `where version=当前值 + set version+1` | 3.7 `TableSchema.version` | 设计一致 |
| `@Filter("id,name")` 更新列白名单 | update args 精确列 | 同构 |
| **"dao 层一次只做一次 sql 元操作，事务在 Service 层"** | dao_schema 单表原子性 + service @Trans | 核心哲学一致 |
| `@Select("id")` 单列投影 List | 缺口 3 | 印证 |

**新缺口（zoom 有、pylon 无）**：

1. **save / insertOrUpdate（upsert）**：`@Keys` 唯一键判断，DB 原生 upsert（返回 1=插入/2=更新）；v-pay `create` 的 while + DuplicateEntry 重试是缺 upsert 的手工模拟——待决策 13；
2. **insertIgnore**：忽略唯一键冲突的插入（v-pay 高频）——与 13 合并讨论；
3. **`@IgnoreNull` 默认语义**：zoom 的 update **默认跳过 null 字段**（`@IgnoreNull(false)` 才写 null）——待决策 11 的权威参照（主流默认 = null 跳过）；
4. **动态 OrderBy**：`find(OrderBy.asc("id","name"))` 运行时排序参数，pylon 只有声明静态 orderBy——待决策 14。

## 8. 待决策问题

1. ~~**alias 声明**~~（已决策：完全推导，无声明字段——多表重名列 alias，唯一列裸名，map 计数机器可判；命名公式 = `{表短语 ?? 表名}_{列名}`，字段命名本身就是短语命名）
2. ~~**insert 租户列**~~（已决策：**A 声明保证**——add 实体必须含 tenant 列（lint 校验），insert 全行插入，零生成器改动）
3. ~~**update/delete 的 tenant 参数形态**~~（已决策：**update 走 row 内提取**——tenant 列从 row destructure 进 WHERE、不进 set，与 pk/version 三种隐式列同构，签名不变；**delete 走独立参数 `(id, shopId)`**——args 是键，tenant 无处可藏）
4. ~~**行类型产出位置**~~（已决策：**统一 `entities/` 目录文件**（curd 现状被消费方引用）——gen-dao 的行类型 interface 从 DAO 文件移出）
5. ~~**app 有 tenant 但表无 FK**~~（已决策：**跳过**——该 dao 无租户，存量项目不炸）
6. ~~**get 参数顺序**~~（已决策：args 声明顺序 + tenant 最后）
7. ~~**FK label 展开声明方式**~~（已决策：**隐式展开发生在 curd → dao_schema 生成环节**——curd 把被引用表 label 列写成外部引用列进 EntitySchema.columns；dao 层无 label 概念，纯外部引用列机制（3.2））
8. **动态表名 / 同结构多表**：cca-pay 的 `dao.ar(Pay.class, table)` 支持表名运行时参数（当前全部传 baseTable，防御性预留）。dao_schema 的 dao 绑定固定 table 表达不了。选项：a 暂不支持（当前无真实分表需求，真分表时再设计）；b 方法级 `tableParam?: boolean`（生成 table 参数透传 knex）；c dao 绑定"表组"（同结构多表的集合声明）。
9. ~~**update 的表达式 set 与列间比较**~~（已决策：`ValueExpr` 递归 AST + `set?: SetExpr[]` + `FilterCondition.right?: ValueExpr`，见 3.6；**乐观锁为一等公民 `TableSchema.version`（3.7），不走手写 set**）
10. **批量操作**（v-pay `@Batch` 批量 update/insert）：机制级批量（insert/update 的 args 支持 EntitySchema 数组，生成批量方法）vs service 层循环 + @Trans（语义等价，N 次 SQL vs 批量提交）？——**后续课题，不进本次**
11. **update 的 null 跳过语义**（v-pay `ignoreNull` 动态部分更新）：args 声明 `nullSkip` 标记 vs 保持静态列（声明多个 update 方法各管各列）？**zoom mapper 参照：`@IgnoreNull` 默认 true（跳过 null 列），主流默认 = null 跳过**。——**后续课题，不进本次**
12. **join 类型**（v-pay INNER JOIN 实体）：find 的跨表 join 全 LEFT；INNER（join 表过滤主表）的声明位——`renderJoins` 的 sources 加类型标记？跨表 filter 条件天然需要 inner 语义（条件必须命中）？——**后续课题，不进本次**
13. **upsert / insertIgnore**（zoom `save`/`insertOrUpdate` + `@Keys`；v-pay create 的冲突重试是手工模拟）：`save` 机制（args 实体 + 唯一键集合，生成 DB 原生 `INSERT ... ON DUPLICATE KEY UPDATE`）？还是坚持 service 层组合（insert 冲突 → 重试/update）？——**后续课题，不进本次**
14. **动态 orderBy**（zoom `find(OrderBy.asc(...))` 运行时排序参数）：find 的 orderBy 参数化（排序列 + 方向运行时传入，列域受限）vs 保持声明静态？——**后续课题，不进本次**

**本次实施范围**（已拍板）：v2 核心（EntitySchema / get·delete 放宽 / ValueExpr / TableSchema.version / tenant 注入）+ **Operator 'in'/'null'/'notNull'**（v-pay 高频刚需）。单列投影（缺口 3）与待决策 10-14 全部后续。