# 定义表 (TableSchema)

## 字段类型

| 构建器 | 类型 | jsType | MySQL 列 | 备注 |
|---|---|---|---|---|
| `stringField` | string | string | VARCHAR | 必填 `maxLength` |
| `textField` | text | string | TEXT | |
| `intField` | integer | number | INT | |
| `bigintField` | bigint | string | BIGINT | 传输层走 string 保精度 |
| `decimalField` | decimal | string | DECIMAL | 必填 `precision` / `scale`，传输层走 string 避免浮点误差 |
| `booleanField` | boolean | boolean | TINYINT(1) | |
| `dateField` | date | Date | DATE | |
| `timeField` | time | string | TIME | |
| `datetimeField` | datetime | Date | DATETIME | |
| `enumField` | enum | string / number | VARCHAR(20) / TINYINT | 引用共享枚举定义，见 [enum.md](./enum.md) |
| `jsonField` | json | object | JSON | |

通用扩展属性（构建器参数）：`label`（中文标签）、`description`、`optional`、`readOnly`、`default`。

**`optional` 默认语义（MySQL 惯例）**：不写 `optional` 或写 `optional: true` → 列可空，DDL 不渲染 `NOT NULL`；写 `optional: false` → 列必填（`NOT NULL`）。业务上必填的列必须显式声明。

## 定义表

```ts
import { bigintField, defineTable, decimalField, stringField } from '@pylonts/dsl';

const id = bigintField({ readOnly: true, label: '主键' });

export const order = defineTable('order', {
  description: '订单',
  autoIncrement: id,
  columns: {
    id,
    order_no: stringField({ label: '订单号', maxLength: 32, optional: false }),
    amount: decimalField({ precision: 18, scale: 2, label: '金额', optional: false }),
  },
  primaryKey: id,
});
```

- 字段名从 map key 反写，`columns` 里的 key 就是列名。
- 字段实例不可跨表复用（复用同一字段实例会抛错），枚举除外。

## 主键生成策略

`autoIncrement` 与 `generator` 互斥，二者选一：

| 属性 | 含义 | 例子 |
|---|---|---|
| `autoIncrement` | 引用自增主键字段，数据库负责生成值（MySQL `AUTO_INCREMENT`）。设了该属性的字段在 DTO 中自动标记为 optional（写入时不需要传） | `autoIncrement: id` |
| `generator` | 主键由业务侧生成（非数据库自增），告诉下游工具用哪个 ID 生成器 | `generator: 'snowflake'` |

```ts
// 数据库自增主键
export const t1 = defineTable('t1', {
  autoIncrement: id,
  columns: { id: bigintField({ readOnly: true, label: '主键' }) },
  primaryKey: id,
});

// 业务生成主键（snowflake）
export const t2 = defineTable('t2', {
  generator: 'snowflake',
  columns: { id: bigintField({ readOnly: true, label: '主键' }) },
  primaryKey: id,
});
```

## 索引

```ts
indexes: [
  { name: 'uk_uuid', columns: c_uuid, unique: true },
  { columns: [c_enum, c_date] },   // 名字缺省时 = 字段名 join '_'
],
```

## 外键与短语检查链

短语统一定义在 `schema/_dictionary.ts`（见 [dictionary.md](./dictionary.md)），表文件从那里 import：

```ts
// schema/_dictionary.ts
import { defineEntityPhrase } from '@pylonts/dsl';

export const bd = defineEntityPhrase({ name: 'bd', label: 'BD推广员', description: '线下拓展商户的推广人员' });
```

```ts
// schema/bd.table.ts
import { bigintField, defineTable } from '@pylonts/dsl';
import { bd as bdPhrase } from './_dictionary';

const bdId = bigintField({ readOnly: true, label: 'BD ID' });

export const bd = defineTable('bd', {
  description: 'BD',
  phrase: bdPhrase,       // 链接词典条目：本表归属的实体
  columns: { id: bdId },
  primaryKey: bdId,
});
```

```ts
// schema/audit.table.ts
import { bigintField, defineTable } from '@pylonts/dsl';
import { bd } from './bd.table';

const auditBdId = bigintField({ label: 'BD' });

export const audit = defineTable('audit', {
  description: '审核',
  columns: {
    bd_id: auditBdId,    // 列名必须 = 短语 + '_' + 被引用字段名
  },
  foreignKeys: {
    fk_audit_bd: { columns: auditBdId, references: bd.columns.id },
  },
});
```

**规则（defineTable 时强制检查）**：外键字段名必须等于 `被引用表.phrase.name + "_" + 被引用字段名`。即引用 `bd.id` 的字段必须叫 `bd_id`——`bd` 来自词典（权威短语），`id` 是 `bd` 表主键。

**短语口径**：`defineEntityPhrase`（实体短语）解释的**就是短语本身**——`name` 即短语词干（如 `mer`），不是实体全名。引用 `merchant` 实体的字段用短语 `mer`（`mer_id`），**不用长语**（`merchant_id`）。短语要短（mer / bd / amt 三字母左右），语义由 `label`/`description` 解释。`TableSchema.phrase` 只接受实体短语（`defineEntityPhrase` 产物）；业务短语（`defineBusinessPhrase`）用于字段命名后缀校验，见 [field-check.md](../../lint/docs/field-check.md)。

- 被引用表未定义 `phrase` → 抛错（检查链要求每个被引用表都有短语）。
- 命名不匹配 → 抛错并提示期望名，例如：
  `foreign key bad: field must be named bd_id (phrase bd + id), got merchant_id`
- 关联表不需要 `phrase`。

> **外键是逻辑作用**：`foreignKeys` 用于定义期命名强校验与关系表达，**DDL 默认不渲染物理 FOREIGN KEY 约束**（`pylonts gen sql init` 不传 `generateForeignKeys`）。数据完整性由 Service/DAO 层保证；如需物理约束，调用 `buildCreateTableSql(schema, { generateForeignKeys: true })`。

## 扩展表（extends）

扩展表表示“本表是某张根表的延伸”：主键与根表主键同义、类型一致，生命周期跟随根表（创建 / 保存 / 删除一起做）。除这两条外，扩展表与普通表完全一样，可以有索引、外键、被其他表引用等。

### 定义

```ts
// order 根表
export const order = defineTable('order', {
  ...
  primaryKey: id,
});

// order_address 是 order 的扩展表
const addressId = stringField({ maxLength: 32 }); // 与 order.id 同类型

export const orderAddress = defineTable('order_address', {
  extends: order,          // 声明本表是 order 的扩展
  primaryKey: addressId,   // 主键与根主键同义
  columns: {
    id: addressId,
    receiver_name: stringField({ label: '收货人', maxLength: 32, optional: false }),
    ...
  },
});
```

### 规则

- 扩展表的主键与根表主键同义，类型必须一致；
- 根表不能是扩展表；
- 扩展表不能再 `extends`（禁止链式延伸）；
- 一个根表可以有多个扩展表；
- 除主键和生命周期外，扩展表与普通表完全一样：可以有普通外键、索引、枚举等，也可被其他表引用；
- 扩展表不需要像普通外键那样命名 `{phrase}_{field}`，也不需要显式声明 `foreignKeys` 来表达与根的关系，`extends` 本身就是关系。

### 与聚合的关系

- `members: { items: [orderItem] }`：数组 → 普通表 → 一对多；
- `members: { address: orderAddress }`：非数组 → 扩展表 → 一对一。

## 生成 SQL

见 [driver.md](./driver.md)。