# TM 语法手册

<DownloadButton filename="tm-syntax.md" title="下载本文档" />

TM（Table Model，表模型）用于定义数据库表的结构和关联关系。本文档详细介绍 TM 的完整语法规范。

## 1. 基本结构

TM 文件使用 JavaScript ES6 模块语法，导出一个 `model` 对象：

```javascript
export const model = {
    name: 'FactSalesModel',      // 模型名称（必填，唯一标识）
    caption: '销售事实表',         // 模型显示名称
    description: '销售订单明细数据', // 模型描述
    tableName: 'fact_sales',     // 对应的数据库表名（必填）
    idColumn: 'sales_key',       // 主键列名

    dimensions: [...],           // 维度定义（关联其他表）
    properties: [...],           // 属性定义（本表字段）
    measures: [...]              // 度量定义（可聚合字段）
};
```

### 1.1 模型基础字段

| 字段 | 类型 | 必填 | 说明                         |
|------|------|------|----------------------------|
| `name` | string | 是 | 模型唯一标识，QM 中通过此名称引用         |
| `caption` | string | 否 | 模型显示名称，建议填写，使用mcp时会传递给AI   |
| `description` | string | 否 | 模型详细描述，建议填写，使用mcp时会传递给AI   |
| `tableName` | string | 是¹ | 对应的数据库表名、mongo集合名          |
| `viewSql` | string | 否¹ | 视图SQL，与 tableName 二选一      |
| `schema` | string | 否 | 数据库 Schema（跨 Schema 访问时使用） |
| `idColumn` | string | 否 | 主键列名                       |
| `type` | string | 否 | 模型类型，默认 `jdbc`、`mongo`、`vector` |
| `deprecated` | boolean | 否 | 标记为废弃，默认 false             |
| `dataSourceName` | string | 否 | 命名数据源引用，支持不同模型使用不同数据源 |
| `autoLoadDimensions` | boolean | 否 | 自动加载维度定义 |
| `autoLoadMeasures` | boolean | 否 | 自动加载度量定义 |
| `preAggregations` | array | 否 | 预聚合配置列表，详见[第11章](#_11-预聚合-preaggregations) |

> ¹ `tableName` 和 `viewSql` 二选一，优先使用 `tableName`

### 1.2 AI 增强配置

可为模型、维度、属性、度量添加 `ai` 配置，用于优化 AI 自然语言查询：

```javascript
{
    name: 'salesAmount',
    caption: '销售金额',
    type: 'MONEY',
    ai: {
        enabled: true,              // 是否激活AI分析（默认 true）
        prompt: '客户实际支付金额',   // 替代 description 的提示词
        levels: [1, 2]              // 激活等级列表
    }
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `enabled` | boolean | 是否激活AI分析，默认 true |
| `prompt` | string | 提示词，若填写则替代 description |
| `levels` | number[] | 激活等级列表，字段可属于多个级别 |

---

## 2. 维度定义 (dimensions)

维度用于定义与其他表的关联关系，查询时自动生成 JOIN。

### 2.1 基本维度

```javascript
dimensions: [
    {
        name: 'customer',              // 维度名称（用于查询时引用）
        caption: '客户',                // 维度显示名称
        description: '购买商品的客户信息', // 维度描述

        tableName: 'dim_customer',     // 关联的维度表名
        foreignKey: 'customer_key',    // 本表的外键字段
        primaryKey: 'customer_key',    // 维度表的主键字段
        captionColumn: 'customer_name', // 维度的显示字段

        keyCaption: '客户Key',          // 主键字段的显示名称
        keyDescription: '客户代理键，自增整数', // 主键字段的描述

        // 维度属性（维度表中可查询的字段）
        properties: [
            {
                column: 'customer_id',
                caption: '客户ID',
                description: '客户唯一标识'
            },
            {
                column: 'customer_type',
                caption: '客户类型',
                description: '客户类型：个人/企业'
            },
            { column: 'province', caption: '省份' },
            { column: 'city', caption: '城市' },
            { column: 'member_level', caption: '会员等级' }
        ]
    }
]
```

### 2.2 维度字段说明

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | string | 是 | 维度名称，查询时使用 `维度名$属性名` 格式引用 |
| `caption` | string | 否 | 维度显示名称 |
| `description` | string | 否 | 维度详细描述 |
| `tableName` | string | 是¹ | 关联的维度表名 |
| `viewSql` | string | 否¹ | 维度视图SQL，与 tableName 二选一 |
| `schema` | string | 否 | 维度表的 Schema |
| `foreignKey` | string | 是 | 事实表中的外键字段 |
| `primaryKey` | string | 是 | 维度表的主键字段 |
| `captionColumn` | string | 否 | 维度的显示字段，用于 `维度名$caption`（简单形式） |
| `captionDef` | object | 否 | Caption 高级定义（优先级高于 captionColumn），见下文 |
| `keyCaption` | string | 否 | 主键字段的显示名称，默认为 `${caption}Key` |
| `keyDescription` | string | 否 | 主键字段的描述信息 |
| `type` | string | 否 | 维度类型：`NORMAL`/`DAY`/`DATETIME`/`DICT`/`BOOL`/`INTEGER`/`DOUBLE` |
| `properties` | array | 否 | 维度表中可查询的属性列表 |
| `forceIndex` | string | 否 | 强制使用的索引名称 |
| `alias` | string | 否 | 维度别名（重定义列名前缀） |
| `deprecated` | boolean | 否 | 标记为废弃 |
| `dimensionDataSql` | function | 否 | 维度数据 SQL 函数（用于访问控制） |
| `onBuilder` | function | 否 | 维度连接构建函数 |

> **`DAY` vs `DATETIME`**：`DAY` 仅包含日期部分（yyyy-MM-dd），`DATETIME` 包含日期和时间（yyyy-MM-dd HH:mm:ss）。

#### captionDef 高级定义

当 `captionColumn` 不够用时（例如需要跨数据库方言的公式），可以使用 `captionDef`：

```javascript
{
    name: 'orderDate',
    captionDef: {
        // 方式1：通用公式（所有方言生效）
        formulaDef: {
            value: 'DATE_FORMAT(order_date, "%Y-%m")'
        },

        // 方式2：方言专属公式（优先级最高）
        dialectFormulaDef: {
            mysql: { value: 'DATE_FORMAT(order_date, "%Y-%m")' },
            postgresql: { value: "TO_CHAR(order_date, 'YYYY-MM')" },
            sqlserver: { value: "FORMAT(order_date, 'yyyy-MM')" }
        },

        // 方式3：直接列引用（最低优先级）
        column: 'order_date'
    }
}
```

**优先级**：`dialectFormulaDef` > `formulaDef` > `column` > 外层 `captionColumn`

> ¹ `tableName` 和 `viewSql` 二选一，优先使用 `tableName`

### 2.3 嵌套维度（雪花模型）

嵌套维度用于实现雪花模型，即维度表之间存在层级关系。

```javascript
{
    // 一级维度：产品（与事实表直接关联）
    name: 'product',
    tableName: 'dim_product',
    foreignKey: 'product_key',       // 事实表上的外键
    primaryKey: 'product_key',
    captionColumn: 'product_name',
    caption: '商品',

    properties: [
        { column: 'product_id', caption: '商品ID' },
        { column: 'brand', caption: '品牌' },
        { column: 'unit_price', caption: '单价', type: 'MONEY' }
    ],

    // 嵌套子维度：品类（与产品维度关联，而非事实表）
    dimensions: [
        {
            name: 'category',
            alias: 'productCategory',   // 别名，简化 QM 访问
            tableName: 'dim_category',
            foreignKey: 'category_key', // 在父维度表(dim_product)上的外键
            primaryKey: 'category_key',
            captionColumn: 'category_name',
            caption: '品类',

            properties: [
                { column: 'category_id', caption: '品类ID' },
                { column: 'category_level', caption: '品类层级' }
            ],

            // 继续嵌套：品类组（与品类维度关联）
            dimensions: [
                {
                    name: 'group',
                    alias: 'categoryGroup',
                    tableName: 'dim_category_group',
                    foreignKey: 'group_key',  // 在父维度表(dim_category)上的外键
                    primaryKey: 'group_key',
                    captionColumn: 'group_name',
                    caption: '品类组',

                    properties: [
                        { column: 'group_id', caption: '品类组ID' },
                        { column: 'group_type', caption: '组类型' }
                    ]
                }
            ]
        }
    ]
}
```

**嵌套维度关键点**：

| 字段 | 说明 |
|------|------|
| `alias` | 维度别名，用于在 QM 中简化列名访问，避免路径过长 |
| `foreignKey` | **重要**：嵌套维度的 foreignKey 指向父维度表上的列 |
| `dimensions` | 子维度列表，可继续嵌套形成多层结构 |

**语法设计原则**：嵌套维度引用使用两种分隔符，各管一件事：

| 分隔符 | 职责 | 示例 |
|--------|------|------|
| `.`（点号） | **维度层级导航** — 定位到哪个维度 | `product.category.group` |
| `$`（美元符） | **属性访问** — 取维度的哪个字段 | `category$caption` |

组合使用：`product.category$caption` = 沿 product → category 路径，取 caption 属性。**不能用多个 `$` 替代 `.`**（如 ~~`product$category$caption`~~），否则解析器无法区分维度路径和属性名。

**QM 中引用嵌套维度**（三种等效写法）：

```javascript
// 写法1：别名（推荐，简短直观）
// 需在 TM 中定义 alias，如 alias: 'productCategory'
{ ref: fs.productCategory$caption }
{ ref: fs.categoryGroup$caption }

// 写法2：完整路径（精确，无需 alias）
{ ref: fs.product.category$caption }
{ ref: fs.product.category.group$caption }

// 写法3：DSL 查询中使用下划线格式（输出列名格式）
columns: ["product_category$caption", "product_category_group$caption"]
```

**输出列名转换**：路径中的 `.` 在输出时自动转为 `_`，避免 JavaScript 属性名冲突：

| QM 引用 | 输出列名 |
|---------|---------|
| `product$caption` | `product$caption` |
| `product.category$caption` | `product_category$caption` |
| `product.category.group$caption` | `product_category_group$caption` |

**生成的 SQL JOIN**：

```sql
SELECT ...
FROM fact_sales f
LEFT JOIN dim_product p ON f.product_key = p.product_key
LEFT JOIN dim_category c ON p.category_key = c.category_key
LEFT JOIN dim_category_group g ON c.group_key = g.group_key
```

### 2.4 父子维度（层级结构）

父子维度用于处理树形层级结构数据（如组织架构、商品分类），通过闭包表实现高效查询。

```javascript
{
    name: 'team',
    tableName: 'dim_team',
    foreignKey: 'team_id',
    primaryKey: 'team_id',
    captionColumn: 'team_name',
    caption: '团队',
    description: '销售所属团队',
    keyDescription: '团队ID，字符串格式',

    // 父子维度配置
    closureTableName: 'team_closure',  // 闭包表名（必填）
    parentKey: 'parent_id',            // 闭包表祖先列（必填）
    childKey: 'team_id',               // 闭包表后代列（必填）

    properties: [
        { column: 'team_id', caption: '团队ID', type: 'STRING' },
        { column: 'team_name', caption: '团队名称', type: 'STRING' },
        { column: 'parent_id', caption: '上级团队', type: 'STRING' },
        { column: 'team_level', caption: '层级', type: 'INTEGER' },
        { column: 'manager_name', caption: '负责人', type: 'STRING' }
    ]
}
```

**父子维度专用字段**：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `closureTableName` | string | 是 | 闭包表名称 |
| `closureTableSchema` | string | 否 | 闭包表 Schema |
| `parentKey` | string | 是 | 闭包表中的祖先列（如 `parent_id`） |
| `childKey` | string | 是 | 闭包表中的后代列（如 `team_id`） |

**闭包表结构示例**：

```sql
CREATE TABLE team_closure (
    parent_id VARCHAR(50),  -- 祖先节点ID
    child_id  VARCHAR(50),  -- 后代节点ID
    depth     INT,          -- 层级深度（0表示自己）
    PRIMARY KEY (parent_id, child_id)
);
```

> 详细说明请参考 [父子维度文档](./parent-child.md)

---

## 3. 属性定义 (properties)

属性用于定义表自身的字段（非聚合字段）。

### 3.1 基本属性

```javascript
properties: [
    {
        column: 'order_id',        // 数据库列名（必填）
        name: 'orderId',           // 属性名称（可选）
        caption: '订单ID',          // 显示名称
        description: '订单唯一标识', // 详细描述
        type: 'STRING'             // 数据类型
    },
    {
        column: 'order_status',
        caption: '订单状态',
        type: 'STRING'
    },
    {
        column: 'created_at',
        caption: '创建时间',
        type: 'DATETIME'
    }
]
```

### 3.2 字典引用属性

使用 `dictRef` 将数据库值映射为显示标签：

```javascript
import { dicts } from '../dicts.fsscript';

properties: [
    {
        column: 'order_status',
        caption: '订单状态',
        type: 'STRING',
        dictRef: dicts.order_status  // 引用字典
    },
    {
        column: 'payment_method',
        caption: '支付方式',
        type: 'STRING',
        dictRef: dicts.payment_method
    }
]
```

**字典定义示例** (dicts.fsscript):

```javascript
import { registerDict } from '@jdbcModelDictService';

export const dicts = {
    order_status: registerDict({
        id: 'order_status',
        caption: '订单状态',
        items: [
            { value: 'PENDING', label: '待处理' },
            { value: 'CONFIRMED', label: '已确认' },
            { value: 'SHIPPED', label: '已发货' },
            { value: 'COMPLETED', label: '已完成' },
            { value: 'CANCELLED', label: '已取消' }
        ]
    }),

    payment_method: registerDict({
        id: 'payment_method',
        caption: '支付方式',
        items: [
            { value: '1', label: '现付' },
            { value: '2', label: '到付' },
            { value: '3', label: '货到付款' }
        ]
    })
};
```

### 3.3 计算属性

使用 `formulaDef` 定义计算字段。`builder` 函数接收表别名，返回原生 SQL 表达式：

```javascript
properties: [
    {
        column: 'customer_name',
        name: 'fullName',
        caption: '客户全名',
        type: 'STRING',
        formulaDef: {
            builder: (alias) => {
                return `CONCAT(${alias}.first_name, ' ', ${alias}.last_name)`;
            },
            description: '拼接姓和名'
        }
    },
    {
        column: 'amount',
        name: 'amountInWan',
        caption: '金额（万元）',
        type: 'MONEY',
        formulaDef: {
            builder: (alias) => {
                return `${alias}.amount / 10000`;
            },
            description: '将金额换算为万元'
        }
    }
]
```

::: warning 方言注意
`builder` 生成的是**原生 SQL**，直接嵌入查询。涉及 JSON 提取等方言差异时，需按目标数据库编写：

| 场景 | MySQL | PostgreSQL | SQL Server |
|------|-------|-----------|------------|
| JSON 文本提取 | `col ->> '$.key'` | `col ->> 'key'` | `JSON_VALUE(col, '$.key')` |
| 类型转换 | `CAST(col AS SIGNED)` | `col::integer` | `CAST(col AS INT)` |

通用函数（`CONCAT`、`COALESCE`、`ROUND` 等）在所有方言下可安全使用。
:::

### 3.4 属性字段说明

| 字段 | 类型 | 必填 | 说明                        |
|------|------|------|---------------------------|
| `column` | string | 是 | 数据库列名                     |
| `name` | string | 否 | 属性名称，默认为 column 的驼峰形式     |
| `alias` | string | 否 | 属性别名                      |
| `caption` | string | 否 | 显示名称                      |
| `description` | string | 否 | 详细描述，若字段含义复杂，建议填写，有助于AI推断 |
| `type` | string | 否 | 数据类型（见 [5. 数据类型](#5-数据类型)）           |
| `format` | string | 否 | 格式化模板（用于日期等）              |
| `dictRef` | string | 否 | 字典引用，用于值到标签的转换            |
| `formulaDef` | object | 否 | 公式定义（见 3.5）               |

### 3.5 公式定义 (formulaDef)

| 字段 | 类型 | 说明 |
|------|------|------|
| `builder` | function | SQL 构建函数，参数 `alias` 为表别名 |
| `value` | string | 公式表达式（基于度量名称） |
| `description` | string | 公式的文字描述 |

> `builder` 和 `value` 二选一，`builder` 更灵活，可直接操作 SQL

---

## 4. 度量定义 (measures)

度量用于定义可聚合的数值字段。

### 4.1 基本度量

```javascript
measures: [
    {
        column: 'quantity',         // 数据库列名（必填）
        name: 'salesQuantity',      // 度量名称（可选）
        caption: '销售数量',          // 显示名称
        description: '商品销售件数',  // 详细描述
        type: 'INTEGER',            // 数据类型
        aggregation: 'sum'          // 聚合方式（必填）
    },
    {
        column: 'sales_amount',
        name: 'salesAmount',
        caption: '销售金额',
        type: 'MONEY',
        aggregation: 'sum'
    },
    {
        column: 'unit_price',
        caption: '单价',
        type: 'MONEY',
        aggregation: 'avg'          // 平均值聚合
    }
]
```

### 4.2 计算度量

使用 `formulaDef` 定义计算度量：

```javascript
measures: [
    {
        column: 'tax_amount',
        name: 'taxAmount2',
        caption: '税额*2',
        description: '用于测试计算字段',
        type: 'MONEY',
        formulaDef: {
            builder: (alias) => {
                return `${alias}.tax_amount + 1`;
            },
            description: '税额加一'
        }
    }
]
```

### 4.3 COUNT 聚合

不基于具体列的计数：

```javascript
measures: [
    {
        name: 'recordCount',
        caption: '记录数',
        aggregation: 'count',
        type: 'INTEGER'
    }
]
```

### 4.4 度量字段说明

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `column` | string | 否¹ | 数据库列名 |
| `name` | string | 否 | 度量名称，默认为 column 的驼峰形式 |
| `alias` | string | 否 | 度量别名 |
| `caption` | string | 否 | 显示名称 |
| `description` | string | 否 | 详细描述 |
| `type` | string | 否 | 数据类型（见 [5. 数据类型](#5-数据类型)） |
| `aggregation` | string | 是 | 聚合方式（见 4.5） |
| `formulaDef` | object | 否 | 公式定义（见 3.5） |

> ¹ `count` 聚合可以不指定 column

### 4.5 聚合方式

| 值 | 说明 | 适用类型 |
|----|------|----------|
| `sum` | 求和 | 数值类型 |
| `avg` | 平均值 | 数值类型 |
| `count` | 计数 | 所有类型 |
| `max` | 最大值 | 数值/日期类型 |
| `min` | 最小值 | 数值/日期类型 |
| `none` | 不聚合 | 所有类型 |

---

## 5. 数据类型

### 5.1 类型列表

| 类型 | 别名 | 说明 | Java 类型 | 使用场景 |
|------|------|------|-----------|----------|
| `STRING` | `TEXT` | 字符串 | String | 文本、编�� |
| `INTEGER` | - | 整数 | Integer | 计数、枚举 |
| `BIGINT` | `LONG` | 长整数 | Long | 大数值主键 |
| `MONEY` | `NUMBER`, `BigDecimal` | 金额/精确小数 | BigDecimal | 金额、价格 |
| `DATETIME` | - | 日期时间 | Date | 时间戳 |
| `DAY` | `DATE` | 日期 | Date | 日期（yyyy-MM-dd） |
| `BOOL` | `Boolean` | 布尔值 | Boolean | 是/否标志 |
| `DICT` | - | 字典值 | Integer | 字典编码 |
| `VECTOR` | - | 向量 | List\<Float\> | 向量检索字段 |

### 5.2 类型选择建议

- **金额字段**：使用 `MONEY`，避免浮点精度问题
- **主键字段**：代理键用 `INTEGER` 或 `BIGINT`，业务键用 `STRING`
- **日期字段**：时间戳用 `DATETIME`，仅日期用 `DAY`
- **枚举字段**：优先使用 `dictRef` + `STRING`，而非创建维度表

---

## 6. 完整示例

### 6.1 事实表模型

```javascript
// FactSalesModel.tm
/**
 * 销售事实表模型定义
 *
 * @description 电商测试数据 - 销售事实表（订单明细）
 *              包含日期、商品、客户、门店、渠道、促销等维度关联
 */
import { dicts } from '../dicts.fsscript';

export const model = {
    name: 'FactSalesModel',
    caption: '销售事实表',
    tableName: 'fact_sales',
    idColumn: 'sales_key',

    // 维度定义 - 关联维度表
    dimensions: [
        {
            name: 'salesDate',
            tableName: 'dim_date',
            foreignKey: 'date_key',
            primaryKey: 'date_key',
            captionColumn: 'full_date',
            caption: '销售日期',
            description: '订单发生的日期',
            keyDescription: '日期主键，格式yyyyMMdd，如20240101',

            properties: [
                { column: 'year', caption: '年', description: '销售发生的年份' },
                { column: 'quarter', caption: '季度', description: '销售发生的季度（1-4）' },
                { column: 'month', caption: '月', description: '销售发生的月份（1-12）' },
                { column: 'month_name', caption: '月份名称' },
                { column: 'day_of_week', caption: '周几' },
                { column: 'is_weekend', caption: '是否周末' }
            ]
        },
        {
            name: 'product',
            tableName: 'dim_product',
            foreignKey: 'product_key',
            primaryKey: 'product_key',
            captionColumn: 'product_name',
            caption: '商品',
            description: '销售的商品信息',

            properties: [
                { column: 'product_id', caption: '商品ID' },
                { column: 'category_name', caption: '一级品类名称' },
                { column: 'sub_category_name', caption: '二级品类名称' },
                { column: 'brand', caption: '品牌' },
                { column: 'unit_price', caption: '商品售价', type: 'MONEY' },
                { column: 'unit_cost', caption: '商品成本', type: 'MONEY' }
            ]
        },
        {
            name: 'customer',
            tableName: 'dim_customer',
            foreignKey: 'customer_key',
            primaryKey: 'customer_key',
            captionColumn: 'customer_name',
            caption: '客户',

            properties: [
                { column: 'customer_id', caption: '客户ID' },
                { column: 'customer_type', caption: '客户类型' },
                { column: 'gender', caption: '性别' },
                { column: 'age_group', caption: '年龄段' },
                { column: 'province', caption: '省份' },
                { column: 'city', caption: '城市' },
                { column: 'member_level', caption: '会员等级' }
            ]
        }
    ],

    // 属性定义 - 事实表自身属性
    properties: [
        {
            column: 'sales_key',
            caption: '销售代理键',
            type: 'BIGINT'
        },
        {
            column: 'order_id',
            caption: '订单ID',
            type: 'STRING'
        },
        {
            column: 'order_line_no',
            caption: '订单行号',
            type: 'INTEGER'
        },
        {
            column: 'order_status',
            caption: '订单状态',
            type: 'STRING',
            dictRef: dicts.order_status
        },
        {
            column: 'payment_method',
            caption: '支付方式',
            type: 'STRING',
            dictRef: dicts.payment_method
        },
        {
            column: 'created_at',
            caption: '创建时间',
            type: 'DATETIME'
        }
    ],

    // 度量定义
    measures: [
        {
            column: 'quantity',
            caption: '销售数量',
            type: 'INTEGER',
            aggregation: 'sum'
        },
        {
            column: 'sales_amount',
            name: 'salesAmount',
            caption: '销售金额',
            type: 'MONEY',
            aggregation: 'sum'
        },
        {
            column: 'cost_amount',
            name: 'costAmount',
            caption: '成本金额',
            type: 'MONEY',
            aggregation: 'sum'
        },
        {
            column: 'profit_amount',
            name: 'profitAmount',
            caption: '利润金额',
            type: 'MONEY',
            aggregation: 'sum'
        }
    ]
};
```

### 6.2 维度表模型

```javascript
// DimProductModel.tm
/**
 * 商品维度模型定义
 *
 * @description 电商测试数据 - 商品维度表
 */
export const model = {
    name: 'DimProductModel',
    caption: '商品维度',
    tableName: 'dim_product',
    idColumn: 'product_key',

    dimensions: [],  // 维度表通常不关联其他维度

    properties: [
        {
            column: 'product_key',
            caption: '商品代理键',
            type: 'INTEGER'
        },
        {
            column: 'product_id',
            caption: '商品业务ID',
            type: 'STRING'
        },
        {
            column: 'product_name',
            caption: '商品名称',
            type: 'STRING'
        },
        {
            column: 'category_id',
            caption: '一级品类ID',
            type: 'STRING'
        },
        {
            column: 'category_name',
            caption: '一级品类名称',
            type: 'STRING'
        },
        {
            column: 'sub_category_id',
            caption: '二级品类ID',
            type: 'STRING'
        },
        {
            column: 'sub_category_name',
            caption: '二级品类名称',
            type: 'STRING'
        },
        {
            column: 'brand',
            caption: '品牌',
            type: 'STRING'
        },
        {
            column: 'unit_price',
            caption: '售价',
            type: 'MONEY'
        },
        {
            column: 'unit_cost',
            caption: '成本',
            type: 'MONEY'
        },
        {
            column: 'status',
            caption: '状态',
            type: 'STRING'
        },
        {
            column: 'created_at',
            caption: '创建时间',
            type: 'DATETIME'
        }
    ],

    measures: []  // 维度表通常没有度量
};
```

### 6.3 日期维度表模型

```javascript
// DimDateModel.tm
/**
 * 日期维度模型定义
 *
 * @description 电商测试数据 - 日期维度表
 */
export const model = {
    name: 'DimDateModel',
    caption: '日期维度',
    tableName: 'dim_date',
    idColumn: 'date_key',

    dimensions: [],

    properties: [
        {
            column: 'date_key',
            caption: '日期键',
            description: '日期主键，格式为yyyyMMdd的整数，如20240101',
            type: 'INTEGER'
        },
        {
            column: 'full_date',
            caption: '完整日期',
            description: '完整日期，格式为yyyy-MM-dd',
            type: 'DAY'
        },
        {
            column: 'year',
            caption: '年',
            description: '年份，如2024',
            type: 'INTEGER'
        },
        {
            column: 'quarter',
            caption: '季度',
            description: '季度数字，1-4表示第一到第四季度',
            type: 'INTEGER'
        },
        {
            column: 'month',
            caption: '月',
            description: '月份数字，1-12',
            type: 'INTEGER'
        },
        {
            column: 'month_name',
            caption: '月份名称',
            description: '月份中文名，如一月、二月、十二月',
            type: 'STRING'
        },
        {
            column: 'week_of_year',
            caption: '年度周数',
            description: '一年中的第几周，1-53',
            type: 'INTEGER'
        },
        {
            column: 'day_of_week',
            caption: '周几',
            description: '一周中的第几天，1=周一，7=周日',
            type: 'INTEGER'
        },
        {
            column: 'is_weekend',
            caption: '是否周末',
            description: '是否为周末（周六或周日）',
            type: 'BOOL'
        },
        {
            column: 'is_holiday',
            caption: '是否节假日',
            description: '是否为法定节假日',
            type: 'BOOL'
        }
    ],

    measures: []
};
```

---

## 7. 维度复用最佳实践

在实际项目中，同一个维度表（如日期维度、客户维度）往往被多个事实表引用。TM 文件使用 FSScript（类 JavaScript）语法，支持函数封装和模块导入，可以将通用维度配置抽取为工厂函数，实现维度定义的复用。

### 7.1 创建维度构建器

将常用维度封装为函数，存放在独立文件中：

```javascript
// dimensions/common-dims.fsscript
/**
 * 通用维度构建器
 * 提供可复用的维度定义工厂函数
 */

/**
 * 构建日期维度
 * @param {object} options - 配置选项
 * @param {string} options.name - 维度名称，默认 'salesDate'
 * @param {string} options.foreignKey - 外键列名，默认 'date_key'
 * @param {string} options.caption - 显示名称，默认 '日期'
 * @returns {object} 维度配置对象
 */
export function buildDateDim(options = {}) {
    const {
        name = 'salesDate',
        foreignKey = 'date_key',
        caption = '日期',
        description = '业务发生的日期'
    } = options;

    return {
        name,
        tableName: 'dim_date',
        foreignKey,
        primaryKey: 'date_key',
        captionColumn: 'full_date',
        caption,
        description,
        keyDescription: '日期主键，格式yyyyMMdd，如20240101',
        type: 'DATETIME',

        properties: [
            { column: 'year', caption: '年', type: 'INTEGER', description: '年份' },
            { column: 'quarter', caption: '季度', type: 'INTEGER', description: '季度（1-4）' },
            { column: 'month', caption: '月', type: 'INTEGER', description: '月份（1-12）' },
            { column: 'month_name', caption: '月份名称', type: 'STRING' },
            { column: 'week_of_year', caption: '年度周数', type: 'INTEGER' },
            { column: 'day_of_week', caption: '周几', type: 'INTEGER' },
            { column: 'is_weekend', caption: '是否周末', type: 'BOOL' },
            { column: 'is_holiday', caption: '是否节假日', type: 'BOOL' }
        ]
    };
}

/**
 * 构建客户维度
 * @param {object} options - 配置选项
 */
export function buildCustomerDim(options = {}) {
    const {
        name = 'customer',
        foreignKey = 'customer_key',
        caption = '客户',
        description = '客户信息'
    } = options;

    return {
        name,
        tableName: 'dim_customer',
        foreignKey,
        primaryKey: 'customer_key',
        captionColumn: 'customer_name',
        caption,
        description,
        keyDescription: '客户代理键，自增整数',

        properties: [
            { column: 'customer_id', caption: '客户ID', description: '客户唯一标识' },
            { column: 'customer_type', caption: '客户类型', description: '个人/企业' },
            { column: 'gender', caption: '性别' },
            { column: 'age_group', caption: '年龄段' },
            { column: 'province', caption: '省份' },
            { column: 'city', caption: '城市' },
            { column: 'member_level', caption: '会员等级' }
        ]
    };
}

/**
 * 构建商品维度
 * @param {object} options - 配置选项
 */
export function buildProductDim(options = {}) {
    const {
        name = 'product',
        foreignKey = 'product_key',
        caption = '商品',
        description = '商品信息'
    } = options;

    return {
        name,
        tableName: 'dim_product',
        foreignKey,
        primaryKey: 'product_key',
        captionColumn: 'product_name',
        caption,
        description,
        keyDescription: '商品代理键，自增整数',

        properties: [
            { column: 'product_id', caption: '商品ID' },
            { column: 'category_name', caption: '一级品类名称' },
            { column: 'sub_category_name', caption: '二级品类名称' },
            { column: 'brand', caption: '品牌' },
            { column: 'unit_price', caption: '商品售价', type: 'MONEY' },
            { column: 'unit_cost', caption: '商品成本', type: 'MONEY' }
        ]
    };
}

/**
 * 构建门店维度
 */
export function buildStoreDim(options = {}) {
    const {
        name = 'store',
        foreignKey = 'store_key',
        caption = '门店',
        description = '门店信息'
    } = options;

    return {
        name,
        tableName: 'dim_store',
        foreignKey,
        primaryKey: 'store_key',
        captionColumn: 'store_name',
        caption,
        description,

        properties: [
            { column: 'store_id', caption: '门店ID' },
            { column: 'store_type', caption: '门店类型' },
            { column: 'province', caption: '省份' },
            { column: 'city', caption: '城市' }
        ]
    };
}
```

### 7.2 在 TM 文件中使用维度构建器

```javascript
// model/FactSalesModel.tm
import { dicts } from '../dicts.fsscript';
import {
    buildDateDim,
    buildCustomerDim,
    buildProductDim,
    buildStoreDim
} from '../dimensions/common-dims.fsscript';

export const model = {
    name: 'FactSalesModel',
    caption: '销售事实表',
    tableName: 'fact_sales',
    idColumn: 'sales_key',

    dimensions: [
        // 使用构建器，自定义名称和描述
        buildDateDim({
            name: 'salesDate',
            caption: '销售日期',
            description: '订单发生的日期'
        }),

        // 使用默认配置
        buildCustomerDim(),
        buildProductDim(),
        buildStoreDim(),

        // 混合使用：构建器 + 内联维度
        {
            name: 'channel',
            tableName: 'dim_channel',
            foreignKey: 'channel_key',
            primaryKey: 'channel_key',
            captionColumn: 'channel_name',
            caption: '渠道',
            properties: [
                { column: 'channel_id', caption: '渠道ID' },
                { column: 'channel_type', caption: '渠道类型' }
            ]
        }
    ],

    properties: [
        // ... 属性定义
    ],

    measures: [
        // ... 度量定义
    ]
};
```

```javascript
// model/FactOrderModel.tm
import { buildDateDim, buildCustomerDim } from '../dimensions/common-dims.fsscript';

export const model = {
    name: 'FactOrderModel',
    caption: '订单事实表',
    tableName: 'fact_order',
    idColumn: 'order_key',

    dimensions: [
        // 订单模型使用不同的维度名称
        buildDateDim({
            name: 'orderDate',
            foreignKey: 'order_date_key',
            caption: '订单日期'
        }),
        buildCustomerDim(),

        // 订单可能有多个日期维度
        buildDateDim({
            name: 'shipDate',
            foreignKey: 'ship_date_key',
            caption: '发货日期',
            description: '订单发货的日期'
        })
    ],

    properties: [...],
    measures: [...]
};
```

### 7.3 高级技巧：属性扩展与覆盖

构建器返回的对象可以通过展开运算符进行扩展或覆盖：

```javascript
import { buildCustomerDim } from '../dimensions/common-dims.fsscript';

export const model = {
    name: 'FactVIPSalesModel',
    caption: 'VIP销售事实表',
    tableName: 'fact_vip_sales',

    dimensions: [
        // 扩展客户维度，添加额外属性
        {
            ...buildCustomerDim({ caption: 'VIP客户' }),
            // 添加 VIP 特有属性
            properties: [
                ...buildCustomerDim().properties,
                { column: 'vip_level', caption: 'VIP等级' },
                { column: 'vip_points', caption: '积分余额', type: 'INTEGER' },
                { column: 'vip_expire_date', caption: '会员到期日', type: 'DAY' }
            ]
        }
    ],

    // ...
};
```

### 7.4 嵌套维度的复用

对于雪花模型的嵌套维度，可以构建包含子维度的完整层级：

```javascript
// dimensions/product-hierarchy.fsscript
/**
 * 构建带品类层级的商品维度（雪花模型）
 */
export function buildProductWithCategoryDim(options = {}) {
    const {
        name = 'product',
        foreignKey = 'product_key',
        caption = '商品'
    } = options;

    return {
        name,
        tableName: 'dim_product',
        foreignKey,
        primaryKey: 'product_key',
        captionColumn: 'product_name',
        caption,

        properties: [
            { column: 'product_id', caption: '商品ID' },
            { column: 'brand', caption: '品牌' },
            { column: 'unit_price', caption: '售价', type: 'MONEY' }
        ],

        // 嵌套品类维度
        dimensions: [
            {
                name: 'category',
                alias: 'productCategory',
                tableName: 'dim_category',
                foreignKey: 'category_key',
                primaryKey: 'category_key',
                captionColumn: 'category_name',
                caption: '品类',

                properties: [
                    { column: 'category_id', caption: '品类ID' },
                    { column: 'category_level', caption: '品类层级' }
                ],

                // 继续嵌套品类组
                dimensions: [
                    {
                        name: 'group',
                        alias: 'categoryGroup',
                        tableName: 'dim_category_group',
                        foreignKey: 'group_key',
                        primaryKey: 'group_key',
                        captionColumn: 'group_name',
                        caption: '品类组',

                        properties: [
                            { column: 'group_id', caption: '品类组ID' },
                            { column: 'group_type', caption: '组类型' }
                        ]
                    }
                ]
            }
        ]
    };
}
```

### 7.5 父子维度的复用

对于组织架构等父子维度，可以参数化闭包表配置：

```javascript
// dimensions/hierarchy-dims.fsscript
/**
 * 构建组织/团队父子维度
 */
export function buildOrgDim(options = {}) {
    const {
        name = 'team',
        tableName = 'dim_team',
        foreignKey = 'team_id',
        closureTableName = 'team_closure',
        caption = '团队',
        description = '组织团队'
    } = options;

    return {
        name,
        tableName,
        foreignKey,
        primaryKey: 'team_id',
        captionColumn: 'team_name',
        caption,
        description,

        // 父子维度配置
        closureTableName,
        parentKey: 'parent_id',
        childKey: 'team_id',

        properties: [
            { column: 'team_id', caption: '团队ID', type: 'STRING' },
            { column: 'team_name', caption: '团队名称', type: 'STRING' },
            { column: 'parent_id', caption: '上级团队', type: 'STRING' },
            { column: 'team_level', caption: '层级', type: 'INTEGER' },
            { column: 'manager_name', caption: '负责人', type: 'STRING' }
        ]
    };
}

/**
 * 构建区域父子维度
 */
export function buildRegionDim(options = {}) {
    const {
        name = 'region',
        foreignKey = 'region_id',
        caption = '区域'
    } = options;

    return {
        name,
        tableName: 'dim_region',
        foreignKey,
        primaryKey: 'region_id',
        captionColumn: 'region_name',
        caption,

        closureTableName: 'region_closure',
        parentKey: 'parent_id',
        childKey: 'region_id',

        properties: [
            { column: 'region_id', caption: '区域ID', type: 'STRING' },
            { column: 'region_name', caption: '区域名称', type: 'STRING' },
            { column: 'region_type', caption: '区域类型', type: 'STRING' },
            { column: 'region_level', caption: '层级', type: 'INTEGER' }
        ]
    };
}
```

### 7.6 项目推荐结构

```
templates/
├── dimensions/                    # 维度构建器目录
│   ├── common-dims.fsscript       # 通用维度（日期、客户、商品等）
│   ├── hierarchy-dims.fsscript    # 父子维度（组织、区域等）
│   └── product-hierarchy.fsscript # 商品雪花维度
├── model/                         # TM 模型目录
│   ├── FactSalesModel.tm
│   ├── FactOrderModel.tm
│   └── ...
├── query/                         # QM 查询模型目录
│   └── ...
└── dicts.fsscript                 # 字典定义
```

### 7.7 维度复用最佳实践总结

| 实践 | 说明 |
|------|------|
| **函数封装** | 将重复使用的维度定义封装为工厂函数 |
| **参数化配置** | 通过参数支持不同场景的定制需求 |
| **默认值** | 为常用参数提供合理的默认值 |
| **模块化组织** | 按维度类型组织到不同的 `.fsscript` 文件 |
| **属性扩展** | 使用展开运算符 `...` 扩展或覆盖属性 |
| **统一维护** | 修改维度定义时只需改一处，全局生效 |
| **文档注释** | 为构建器函数添加 JSDoc 注释，便于使用 |

---

## 8. 命名约定

### 8.1 文件命名

- TM 文件：`{模型名}Model.tm`
- 事实表：`Fact{业务名}Model.tm`，如 `FactSalesModel.tm`
- 维度表：`Dim{业务名}Model.tm`，如 `DimCustomerModel.tm`
- 字典文件：`dicts.fsscript`

### 8.2 字段命名

| 位置 | 规范 | 示例 |
|------|------|------|
| 模型 `name` | 大驼峰 PascalCase | `FactSalesModel`, `DimCustomerModel` |
| 字段 `name` | 小驼峰 camelCase | `orderId`, `salesAmount`, `customerType` |
| 数据库 `column` | 蛇形 snake_case | `order_id`, `sales_amount`, `customer_type` |
| 维度属性引用 | `$` 分隔 | `customer$caption`, `salesDate$year` |

### 8.3 模型设计建议

1. **事实表**：
   - 包含业务事实度量（销售额、数量等）
   - 包含指向维度表的外键
   - 粒度要明确（如订单行级、订单级）

2. **维度表**：
   - 包含描述性属性
   - 使用代理键（surrogate key）作为主键
   - 维度表一般不定义度量

3. **星型模型 vs 雪花模型**：
   - 星型模型：维度表不嵌套，查询性能更好（推荐）
   - 雪花模型：维度表嵌套，节省存储空间，需要时使用

---

## 9. 高级特性

### 9.1 扩展数据

使用 `extData` 存储自定义元数据：

```javascript
{
    name: 'FactSalesModel',
    caption: '销售事实表',
    extData: {
        businessOwner: '销售部',
        updateFrequency: 'daily',
        customTag: 'core-metric'
    }
}
```

### 9.2 废弃标记

标记过时的模型或字段：

```javascript
{
    name: 'oldSalesAmount',
    caption: '旧版销售金额',
    column: 'old_sales_amt',
    type: 'MONEY',
    deprecated: true  // 前端配置时会显示废弃提示
}
```

---

## 10. 向量模型

向量模型用于与 Milvus 等向量数据库集成，支持语义相似度检索。

### 10.1 基本结构

```javascript
export const model = {
    name: 'DocumentSearchModel',
    caption: '文档检索模型',
    type: 'vector',                    // 指定为向量模型
    tableName: 'documents',            // Milvus 集合名称

    properties: [
        { column: 'doc_id', caption: '文档ID', type: 'BIGINT' },
        { column: 'title', caption: '标题', type: 'STRING' },
        { column: 'content', caption: '内容', type: 'STRING' },
        { column: 'category', caption: '分类', type: 'STRING' },
        { column: 'embedding', caption: '向量', type: 'VECTOR' }  // 向量字段
    ],

    measures: []
};
```

### 10.2 关键配置

| 字段 | 说明 |
|------|------|
| `type: 'vector'` | 指定模型类型为向量模型 |
| `tableName` | Milvus 集合名称 |
| `type: 'VECTOR'` | 属性类型为向量字段 |

### 10.3 向量字段元数据

向量字段的维度（dimension）、索引类型（indexType）、度量类型（metricType）会自动从 Milvus 获取，无需在 TM 中手动配置。

### 10.4 向量模型限制

- 不支持维度关联（dimensions）
- 不支持 JOIN 操作
- 仅支持 `similar` 和 `hybrid` 操作符进行检索
- 查询结果包含 `_score` 字段表示相似度

---

## 11. 预聚合 (preAggregations)

预聚合通过预先计算和存储聚合结果，显著提升大数据量场景下的查询性能。

### 11.1 基本结构

```javascript
export const model = {
    name: 'FactSalesModel',
    tableName: 'fact_sales',

    preAggregations: [
        {
            name: 'daily_product_sales',
            caption: '按日-产品预聚合',
            tableName: 'preagg_daily_product_sales',
            priority: 80,
            enabled: true,

            dimensions: ['product', 'salesDate'],
            granularity: {
                salesDate: 'day'            // 时间维度粒度
            },

            measures: [
                { name: 'salesAmount', aggregation: 'SUM' },
                { name: 'salesQuantity', aggregation: 'SUM' },
                { name: 'salesAmount', aggregation: 'COUNT', columnName: 'sales_count' }
            ],

            filters: [
                { field: 'orderStatus', op: '=', value: 'COMPLETED' }
            ],

            refresh: {
                strategy: 'INCREMENTAL',
                schedule: '0 2 * * *',      // 每天凌晨2点
                watermarkColumn: 'salesDate$caption',
                lookbackDays: 3
            }
        }
    ],

    dimensions: [...],
    measures: [...]
};
```

### 11.2 预聚合字段

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | string | 是 | 预聚合名称（模型内唯一） |
| `caption` | string | 否 | 显示名称 |
| `tableName` | string | 是 | 预聚合表名 |
| `schema` | string | 否 | 数据库 schema（默认使用主表 schema） |
| `priority` | number | 否 | 优先级 1-100（默认 50，越高越优先匹配） |
| `enabled` | boolean | 否 | 是否启用（默认 true） |
| `dimensions` | string[] | 是 | 包含的维度名称列表 |
| `granularity` | object | 否 | 时间维度粒度配置 |
| `measures` | array | 是 | 度量定义列表 |
| `filters` | array | 否 | 永久过滤条件 |
| `refresh` | object | 否 | 刷新配置 |

### 11.3 时间粒度 (granularity)

| 粒度值 | 说明 |
|--------|------|
| `minute` | 分钟 |
| `hour` | 小时 |
| `day` | 天 |
| `week` | 周 |
| `month` | 月 |
| `quarter` | 季度 |
| `year` | 年 |

### 11.4 度量定义 (measures)

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | string | 是 | 引用 TM 中的度量名称 |
| `aggregation` | string | 是 | 聚合方式（SUM/COUNT/MIN/MAX/AVG） |
| `columnName` | string | 否 | 预聚合表中的列名（默认 `name_aggregation`） |

### 11.5 刷新配置 (refresh)

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `strategy` | string | 否 | `FULL`（全量）/ `INCREMENTAL`（增量），默认 FULL |
| `schedule` | string | 否 | Cron 表达式 |
| `watermarkColumn` | string | 否 | 水位线列名（增量刷新用） |
| `lookbackDays` | number | 否 | 回溯天数（处理迟到数据） |

### 11.6 匹配规则

查询时系统自动匹配预聚合表，匹配条件：
1. 查询涉及的维度是预聚合维度的子集
2. 查询的时间粒度 >= 预聚合的时间粒度
3. 查询的度量和聚合方式与预聚合兼容
4. 查询条件不与预聚合的 filters 冲突

多个预聚合匹配时，优先使用 `priority` 较高的。

---

## 12. 访问控制 (accesses)

访问控制在 QM 中配置，但依赖 TM 中的维度定义实现行级/列级数据安全。

### 12.1 维度数据 SQL

在维度中定义 `dimensionDataSql`，用于动态生成数据权限 SQL：

```javascript
{
    name: 'store',
    tableName: 'dim_store',
    foreignKey: 'store_key',
    primaryKey: 'store_key',
    captionColumn: 'store_name',

    // 根据当前用户返回可访问的维度数据SQL
    dimensionDataSql: (context) => {
        return `SELECT store_key FROM user_store_permission WHERE user_id = '${context.userId}'`;
    }
}
```

### 12.2 命名数据源

当不同模型需要连接不同数据库时，使用 `dataSourceName`：

```javascript
export const model = {
    name: 'FactOrderModel',
    tableName: 'fact_orders',
    dataSourceName: 'orderDb',  // 引用配置中的命名数据源

    dimensions: [...],
    measures: [...]
};
```

> `dataSourceName` 的优先级高于全局默认数据源。需在应用配置中注册对应的命名数据源。

---

## 下一步

- [QM 语法手册](./qm-syntax.md) - 查询模型定义
- [JSON 查询 DSL](./query-dsl.md) - 查询 DSL 完整语法
- [父子维度](./parent-child.md) - 层级结构维度详解
- [计算字段](./calculated-fields.md) - 复杂计算逻辑
- [预聚合](../advanced/pre-aggregation.md) - 预聚合详细配置
- [查询 API](../api/query-api.md) - HTTP API 接口
