# 动态表对象虚拟动作设计

## 1. 背景

动态表对象本质上是可读写的远端 SQL 表对象。当前 datacloud-data 已经具备 DB 对象的明细查询和聚合统计能力：

- `query_{object_code}` 路由到 `QueryExecutor`；
- `compute_{object_code}` 路由到 `ComputeExecutor`；
- 对象表名来自 `OntologyClass.table_name`；
- 字段物理列来自 `OntologyField.source_column` 或 `field_code`；
- 返回结构统一为 `records`、`total`、`meta`。

动态表与普通 DB 对象的关键区别是：它是可写对象，并且实际执行通道通常由业务系统提供。SDK 仍然通过现有 `DataSourceManager` connector 抽象执行 SQL；connector 可以是本地数据库连接器，也可以是业务方实现的远程 HTTP SQL 连接器。

业务方 connector 可以适配不同业务系统 API。下面是一个业务系统远程 SQL 接口示例：

```bash
curl -s http://10.10.168.203:51919/plugins/byclaw-sqlite/sqlExecute \
  -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {{OPENCLAW_GATEWAY_TOKEN}}" \
  -d '{"sql":"SELECT 1 as test, '\''hello'\'' as msg", "user_code": "0027024630"}'
```

本设计在现有 DB 查询和统计的协议、校验、SQL 生成能力基础上，为动态表对象提供以下虚拟动作：

- `insert_{object_code}`：新增一条或多条记录；
- `update_{object_code}`：按条件修改记录；
- `delete_{object_code}`：按条件删除记录；
- `query_{object_code}`：复用现有 DB 明细查询；
- `compute_{object_code}`：复用现有 DB 聚合统计。

## 2. 设计目标

1. 动态表不在 `ext_property` 中保存表编码，继续使用对象定义中的表编码。
2. `query_{object_code}` 和 `compute_{object_code}` 复用当前 DB 类型的入参协议、校验规则和 SQL 生成规则，最终通过对象 `datasource_alias` 对应的 connector 执行。
3. 新增、修改、删除也通过同一个 datasource connector 执行，避免把不同业务系统 HTTP API 逻辑散落在 action 层。
4. 所有虚拟动作返回现有 action 结构：`records`、`total`、`meta`。
5. 写操作只允许访问对象定义中声明的字段，禁止透传任意列名。
6. SQL 构造必须集中处理标识符和字面量转义；用户输入值不得直接拼进 SQL。

## 3. 非目标

1. 不设计跨表写入。
2. 不支持对 linked 字段、derived 字段、formula 字段直接写入。
3. 不绕过数据库约束实现业务级唯一性校验。
4. 不在本阶段设计批量导入文件能力。
5. 不改变现有 query/compute 协议。

## 4. 动态表对象识别

动态表对象使用独立来源类型：

```python
cls.source_type == "DYNAMIC_TABLE"
```

这样动态表不需要通过普通 DB 对象的 `db_type`、datasource alias 或 `source_config` 再做二次推断，动作注入和执行路由都可以直接判断 `source_type`。但 SQL 方言仍然必须从数据源配置的 `db_type` 获取，不能把 `source_type == "DYNAMIC_TABLE"` 当成 SQL 方言。

推荐识别方式：

1. `cls.source_type == "DYNAMIC_TABLE"`；
2. `cls.table_name` 非空；
3. `cls.datasource_alias` 指向 dbsource 配置；
4. dbsource 配置中声明真实数据库方言 `db_type`，例如 `SQLITE`、`MYSQL`、`POSTGRES`、`OPENGAUSS`。

```json
{
  "source_type": "DYNAMIC_TABLE",
  "datasource_alias": "dynamic_table",
  "table_name": "sales_analysis_result"
}
```

对应 dbsource 配置。业务方 connector 由业务代码实现并注册或注入，endpoint、token、`user_code`、重试策略等连接细节封装在 connector 实现内部；SQL 方言从对象 `datasource_alias` 对应的 dbsource 配置读取。

`connector_type` 放在 dbsource 的 `dbParams` 中，用于表达“执行通道/业务系统 API”。它不写入 OWL 对象 `ext_property`，也不替代 `dbType`：

```xml
<dbCode>dynamic_table</dbCode>
<dbType>SQLITE</dbType>
<dbParams>{"connector_type":"BYCLAW_SQL_EXECUTE"}</dbParams>
```

解析后等价于：

```python
DataSourceConfig(
    alias="dynamic_table",
    db_type="SQLITE",
    connector_type="BYCLAW_SQL_EXECUTE",
)
```

兼容策略：如果历史 OWL 或 JSON 已经把动态表写成 `source_type == "DB"` 且 `source_config.db_type == "DYNAMIC_TABLE"`，可以在 loader 解析阶段归一化为 `source_type == "DYNAMIC_TABLE"`。运行时逻辑不再长期保留两套判断。

只要 `source_type == "DYNAMIC_TABLE"`，就注入 `insert/update/delete`。不再额外使用开关控制动态表写动作暴露。

运行时判断函数保持简单：

```python
def is_dynamic_table(cls: Any) -> bool:
    return str(getattr(cls, "source_type", "") or "").upper() == "DYNAMIC_TABLE"
```

### 4.1 Connector 选择规则

动态表对象不通过 `ext_property.backend` 选择执行实现。动态表只声明 `datasource_alias`，执行实现由 `DataSourceManager.get_connector(datasource_alias)` 决定。

各业务系统需要适配不同 HTTP API 时，由业务方实现自己的 connector，并在应用启动阶段注册或注入。SDK 不内置各业务系统的 endpoint、token、`user_code` 等配置，也不在 OWL 中保存这些运行时连接信息。

优先级：

1. 通过 `cls.datasource_alias` 找到 dbsource 配置；
2. 如果 dbsource 配置声明 `connector_type`，按 `connector_type` 从 `ConnectorRegistry` 获取业务 connector；
3. 如果未声明 `connector_type`，沿用现有 `DataSourceManager` 兼容逻辑，例如 `datasource_id` / `sql_execute_url` 强制走 HTTP connector，否则按 `db_type` 选择本地 connector；
4. 如果业务方已手动向 `DataSourceManager._connectors` 或等价注入点放入 connector 实例，则直接复用该实例。

示例：

```python
ConnectorRegistry.register("BYCLAW_SQL_EXECUTE", ByclawSqlExecuteConnector)
```

约束：

1. connector 类型是运行时配置，不写入对象 `ext_property`。
2. connector 实现类、endpoint、token、`user_code` 获取策略、重试策略等由业务方实现和配置。
3. OWL 只表达对象绑定哪个 datasource，不保存 endpoint、token、`user_code` 等运行时敏感配置。
4. `db_type` 仍然表示 SQL 方言，从 `datasource_configs[datasource_alias]` 读取；不要把 `connector_type` 当成 SQL 方言。
5. 如果 `connector_type` 未注册，应返回明确配置错误，不静默降级到其他 connector。
6. 业务方也可以在应用启动时直接向 `DataSourceManager` 注入已构建好的 connector 实例，动态表 executor 不感知差异。

解析后的 DataSourceConfig 形态：

```python
DataSourceConfig(
    alias="dynamic_table",
    db_type="SQLITE",
    connector_type="BYCLAW_SQL_EXECUTE",
)
```

解析规则：

```python
connector = datasource_manager.get_connector(cls.datasource_alias)
db_type = datasource_configs[cls.datasource_alias].db_type
```

这样 OWL 只表达“这个对象属于哪个数据源”，具体 connector 怎么连、怎么鉴权、怎么调用业务系统 API，全部由业务方在代码或运行时配置中实现。

## 5. 动作清单

| 动作 | action_family | virtual_backend | 说明 |
| --- | --- | --- | --- |
| `query_{object_code}` | `query` | `dynamic_table_connector` | 明细查询，复用 DB 查询协议和 SQL 生成规则，执行走 datasource connector |
| `compute_{object_code}` | `compute` | `dynamic_table_connector` | 聚合统计，复用 DB 统计协议和 SQL 生成规则，执行走 datasource connector |
| `insert_{object_code}` | `insert` | `dynamic_table_connector` | 新增动态表记录，执行走 datasource connector |
| `update_{object_code}` | `update` | `dynamic_table_connector` | 修改动态表记录，执行走 datasource connector |
| `delete_{object_code}` | `delete` | `dynamic_table_connector` | 删除动态表记录，执行走 datasource connector |

如果产品侧希望“新增”工具名使用 `create_{object_code}`，可以作为 `insert_{object_code}` 的 `legacy_aliases` 或额外注册别名，不建议同时实现两套执行逻辑。

## 6. query/compute 复用方案

### 6.1 query 动作

`query_{object_code}` 保持现有 DB 查询协议。动态表对象复用其协议、字段校验、WHERE/ORDER/LIMIT SQL 生成规则，最终通过对象 `datasource_alias` 对应的 connector 执行 SQL。

```python
if action_family == "query" and is_dynamic_table(cls):
    from datacloud_data_sdk.executor.dynamic_table_executor import DynamicTableExecutor

    return await DynamicTableExecutor(self._loader).query(object_code, params)

if action_family == "query":
    return await QueryExecutor(self._loader).execute(object_code, params)
```

查询协议继续使用：

```json
{
  "select": ["customer_name", "amount"],
  "filters": [
    {"field": "status", "op": "eq", "value": "active"}
  ],
  "filter_relation": "AND",
  "order_by": [
    {"field": "created_at", "direction": "desc"}
  ],
  "limit": 100,
  "offset": 0
}
```

返回：

```json
{
  "records": [
    {
      "customer_name": "白银有色",
      "amount": 1000
    }
  ],
  "total": 1,
  "meta": {
    "columns": [
      {"name": "customer_name", "label": "客户名称", "type": "string"},
      {"name": "amount", "label": "金额", "type": "number"}
    ],
    "object_code": "sales_dynamic_table"
  }
}
```

### 6.2 compute 动作

`compute_{object_code}` 保持现有 DB 聚合统计协议。动态表对象复用当前 `ComputeExecutor` 的协议、字段校验、维度/指标 SQL 生成规则，最终通过对象 `datasource_alias` 对应的 connector 执行 SQL。

```python
if action_family == "compute" and is_dynamic_table(cls):
    from datacloud_data_sdk.executor.dynamic_table_executor import DynamicTableExecutor

    return await DynamicTableExecutor(self._loader).compute(object_code, params)

if action_family == "compute":
    return await ComputeExecutor(self._loader).execute(object_code, params)
```

统计协议继续使用：

```json
{
  "dimensions": [
    {"field": "region", "group_op": "direct"}
  ],
  "metrics": [
    {"field": "amount", "agg": "sum", "as": "amount_sum"}
  ],
  "filters": [],
  "order_by": [
    {"field": "amount_sum", "direction": "desc"}
  ],
  "limit": 100
}
```

返回：

```json
{
  "records": [
    {
      "region": "西北",
      "amount_sum": 1000
    }
  ],
  "total": 1,
  "meta": {
    "columns": [
      {"name": "region", "label": "区域", "type": "string"},
      {"name": "amount_sum", "label": "amount_sum", "type": "number"}
    ],
    "object_code": "sales_dynamic_table"
  }
}
```

## 7. 写操作协议

### 7.1 字段范围

写操作只允许写入可持久化字段：

```python
field.property_kind not in ("derived", "linked")
```

字段到物理列映射：

```python
column_name = field.source_column or field.field_code
```

以下字段默认不允许由用户直接写入：

1. `property_kind == "derived"`；
2. `property_kind == "linked"`；
3. `formula` 非空字段；
4. 由配置声明为只读的字段；
5. 主键字段默认视为自动生成，不出现在新增 schema 中，除非对象定义显式允许外部传入。

### 7.2 insert 动作

动作名：

```text
insert_{object_code}
```

入参：

```json
{
  "records": [
    {
      "customer_name": "白银有色",
      "amount": 1000,
      "status": "active"
    }
  ]
}
```

schema 生成规则：

1. `records` 为必填数组；
2. `records.items.properties` 根据对象可写字段生成；
3. 每个字段类型根据 `OntologyField.field_type` 映射；
4. `records.items.additionalProperties = false`；
5. 对象字段 `required == true` 且非自动生成时，加入 `records.items.required`。
6. `is_primary_key == true` 的字段默认从 `records.items.properties` 中排除，避免 Agent 传入自动生成主键。

如果某个动态表确实要求外部传主键，需要在对象或字段扩展配置中显式声明，例如：

```json
{
  "source_config": {
    "allow_write_primary_key": true
  }
}
```

返回：

```json
{
  "records": [
    {
      "customer_name": "白银有色",
      "amount": 1000,
      "status": "active"
    }
  ],
  "total": 1,
  "meta": {
    "columns": [
      {"name": "customer_name", "label": "客户名称", "type": "string"},
      {"name": "amount", "label": "金额", "type": "number"},
      {"name": "status", "label": "状态", "type": "string"}
    ],
    "operation": "insert"
  }
}
```

SQL 生成：

```sql
INSERT INTO table_name (customer_name, amount, status)
VALUES ('白银有色', 1000, 'active')
```

新增动作返回对应新增行数据。若 `sqlExecute` 不支持 `RETURNING`，执行器应按以下策略返回：

1. 单条 insert 且输入字段足以代表新增行时，返回输入记录中可写字段值；
2. 如果存在服务端生成字段且需要返回完整行，必须通过后续查询能力按主键、唯一键或后端返回标识查询；
3. 无法可靠定位新增行时，只返回输入字段，不伪造主键值。

批量新增可以按数据库能力选择：

1. 单条多次执行；
2. `executemany`；
3. 多 values SQL。

第一版推荐单条循环；如果业务方 connector 支持批量执行能力，可以在 connector 内部优化，动态表 executor 不感知具体业务系统 API。

### 7.3 update 动作

动作名：

```text
update_{object_code}
```

入参：

```json
{
  "values": {
    "status": "inactive",
    "updated_by": "system"
  },
  "filters": [
    {"field": "customer_name", "op": "eq", "value": "白银有色"}
  ],
  "filter_relation": "AND",
  "returning": ["customer_name", "status", "updated_by"]
}
```

字段说明：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `values` | object | 是 | 待修改字段和值，只允许可写字段 |
| `filters` | array[object] | 是 | 修改条件，协议复用 query filters |
| `filter_relation` | string | 否 | `AND` 或 `OR`，默认 `AND` |
| `returning` | array[string] | 否 | 希望返回的字段；为空时默认返回全部非 linked 字段 |

安全约束：

1. `filters` 必须非空；
2. `values` 必须非空；
3. 禁止修改主键，除非对象定义显式允许；
4. 默认禁止 `filter_relation == "OR"` 搭配强制过滤字段缺失的请求；
5. 如果预计影响行数超过配置阈值，应拒绝或要求显式确认参数。

返回：

```json
{
  "records": [
    {
      "customer_name": "白银有色",
      "status": "inactive",
      "updated_by": "system"
    }
  ],
  "total": 1,
  "meta": {
    "columns": [
      {"name": "customer_name", "label": "客户名称", "type": "string"},
      {"name": "status", "label": "状态", "type": "string"},
      {"name": "updated_by", "label": "更新人", "type": "string"}
    ],
    "operation": "update"
  }
}
```

SQL 生成：

```sql
UPDATE table_name
SET status = 'inactive', updated_by = 'system'
WHERE customer_name = '白银有色'
```

修改动作必须返回被修改后的对应行数据。推荐执行流程：

1. 先根据 `filters` 查询待修改行的主键或完整返回字段；
2. 执行 `UPDATE`；
3. 如果存在主键，按主键集合重新查询修改后的行并返回；
4. 如果不存在主键但 `filters` 修改后仍能匹配，则按原 filters 查询；
5. 如果无法可靠查询修改后的行，应拒绝请求或要求对象定义主键。

### 7.4 delete 动作

动作名：

```text
delete_{object_code}
```

入参：

```json
{
  "filters": [
    {"field": "customer_name", "op": "eq", "value": "白银有色"}
  ],
  "filter_relation": "AND"
}
```

字段说明：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `filters` | array[object] | 是 | 删除条件，协议复用 query filters |
| `filter_relation` | string | 否 | `AND` 或 `OR`，默认 `AND` |

安全约束：

1. `filters` 必须非空；
2. 禁止无条件删除；
3. 删除固定为物理删除，生成 `DELETE FROM ... WHERE ...`；
4. 如果预计影响行数超过配置阈值，应拒绝或要求显式确认参数。

返回：

```json
{
  "records": [
    {
      "customer_name": "白银有色",
      "amount": 1000,
      "status": "inactive"
    }
  ],
  "total": 1,
  "meta": {
    "columns": [
      {"name": "customer_name", "label": "客户名称", "type": "string"},
      {"name": "amount", "label": "金额", "type": "number"},
      {"name": "status", "label": "状态", "type": "string"}
    ],
    "operation": "delete"
  }
}
```

物理删除 SQL：

```sql
DELETE FROM table_name
WHERE customer_name = '白银有色'
```

删除动作必须返回被删除的对应行数据。由于物理删除后记录已不存在，执行器必须先执行 SELECT 查出待删除行，再执行 DELETE，最后返回删除前查询到的行数据。

## 8. Schema 生成器设计

在 `virtual_action/generator.py` 中新增：

```python
def build_insert_schema(scope_name: str, fields: list[Any]) -> dict[str, Any]:
    ...


def build_update_schema(scope_name: str, fields: list[Any]) -> dict[str, Any]:
    ...


def build_delete_schema(scope_name: str, fields: list[Any]) -> dict[str, Any]:
    ...
```

字段类型映射：

| `OntologyField.field_type` | JSON Schema |
| --- | --- |
| `STRING` / `TEXT` | `{"type": "string"}` |
| `DATE` / `DATETIME` / `TIMESTAMP` | `{"type": "string"}` |
| `NUMBER` / `DECIMAL` / `DOUBLE` / `FLOAT` | `{"type": "number"}` |
| `INTEGER` / `BIGINT` | `{"type": "integer"}` |
| `BOOLEAN` | `{"type": "boolean"}` |
| `ARRAY` | `{"type": "array"}` |
| `OBJECT` | `{"type": "object"}` |
| 未识别类型 | `{"type": "string"}` |

可写字段 schema 示例：

```json
{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "customer_name": {
      "type": "string",
      "description": "客户名称"
    },
    "amount": {
      "type": "number",
      "description": "金额"
    },
    "status": {
      "type": "string",
      "description": "状态"
    }
  }
}
```

`update` 和 `delete` 的 `filters` schema 应直接复用 query filters 生成逻辑，即 `_build_filters_schema(fields, strict_field_code=True)`。

## 9. SQL Connector 执行规范

动态表所有动作最终都通过 datasource connector 执行。SDK 层只负责生成受控 SQL、调用 `BaseSourceConnector.execute()`、归一化 action 返回结构。不同业务系统的远程 HTTP API 由业务方实现不同 connector，SDK 不内置业务系统适配逻辑。

### 9.1 Connector 协议

```python
class BaseSourceConnector(ABC):
    config: DataSourceConfig

    async def execute(
        self,
        sql: str,
        params: dict[str, Any] | None = None,
    ) -> list[dict[str, Any]]:
        ...
```

`DynamicTableExecutor` 只依赖 `DataSourceManager.get_connector(cls.datasource_alias)` 返回的 connector，不直接依赖具体 HTTP 地址、token、`user_code` 或业务系统协议。

### 9.2 业务方 connector 实现示例

下面是一个面向业务系统 `sqlExecute` 接口的 connector 示例。该类由业务方实现并注册，SDK 只要求它满足 `BaseSourceConnector.execute(sql, params)` 协议。

```python
class ByclawSqlExecuteConnector(BaseSourceConnector):
    @classmethod
    def supported_type(cls) -> str:
        return "BYCLAW_SQL_EXECUTE"

    async def execute(
        self,
        sql: str,
        params: dict[str, Any] | None = None,
    ) -> list[dict[str, Any]]:
        ...
```

注册示例：

```python
ConnectorRegistry.register("BYCLAW_SQL_EXECUTE", ByclawSqlExecuteConnector)
```

业务方 connector 内部可以调用如下接口：

```text
POST /plugins/byclaw-sqlite/sqlExecute
Content-Type: application/json
Authorization: Bearer {OPENCLAW_GATEWAY_TOKEN}
```

请求体示例：

```json
{
  "sql": "SELECT 1 as test, 'hello' as msg",
  "user_code": "0027024630"
}
```

对应 dbsource 配置建议。endpoint、token provider、`user_code` provider、timeout 等运行时参数由业务方 connector 自己读取或在构造时注入；这些不放入 OWL。SQL 方言不放到 connector 类型中，而是复用 dbsource 的 `dbType`，connector 类型放在 `dbParams.connector_type`：

```json
{
  "connector_type": "BYCLAW_SQL_EXECUTE"
}
```

`user_code` 的来源由业务方 connector 实现决定。可选策略：

1. 从 `InvocationContext` 读取当前用户；
2. 从 connector 构造参数中的 provider 读取；
3. 使用固定 user code provider；
4. 未配置时报错。

动态表 query、compute、insert、update、delete 都必须走同一个 datasource connector。区别只在 SDK 生成的 SQL。

SQL 生成必须读取对象 `datasource_alias` 对应 dbsource 的 `db_type`：

```python
db_type = datasource_configs[datasource_alias].db_type
```

`db_type` 影响：

| 能力 | 说明 |
| --- | --- |
| 标识符引用 | SQLite/MySQL 使用反引号或按现有 DB 工具规则，Postgres/OpenGauss 使用双引号 |
| 分页语法 | 不同数据库的 `LIMIT/OFFSET`、`FETCH` 语法差异 |
| 日期时间分组 | `strftime`、`DATE_FORMAT`、`date_trunc` 等函数差异 |
| 布尔值渲染 | `1/0`、`TRUE/FALSE` 差异 |
| RETURNING | insert/update 是否可直接返回变更行 |
| UPSERT 扩展 | 后续如支持 upsert，需要按方言生成 |

响应归一化规则：

1. 业务方 connector 对外必须返回 `list[dict[str, Any]]`。
2. 业务系统原始响应中的 `records`、`data`、`resultObject.data` 等结构，应在 connector 内部归一化。
3. insert/update/delete 的 action 主返回必须由 `DynamicTableExecutor` 组装为对应行数据，不能只返回影响行数。
4. 如果业务系统只返回影响行数，connector 可以返回空列表，executor 需要通过写前/写后查询补齐对应行数据。
5. 原始响应如果需要调试，应由 connector 记录日志或放入明确的诊断通道，不直接暴露到 action 主返回。

### 9.3 多业务 connector 扩展

业务系统差异通过 connector 扩展，而不是通过动态表专用 backend 扩展：

| connector 实现 | 用途 |
| --- | --- |
| `ByclawSqlExecuteConnector` | 调用业务系统 `/plugins/byclaw-sqlite/sqlExecute`，连接信息由业务方实现管理 |
| `ChatbiHttpSqlConnector` | 调用 ChatBI SQL HTTP 服务 |
| `HttpSqlConnector` | 复用现有通用 HTTP SQL connector |
| `SQLiteConnector` | 测试或本地开发时复用本地 SQLite |
| `MockConnector` | 单元测试 |

选择逻辑：

```python
connector = datasource_manager.get_connector(cls.datasource_alias)
records = await connector.execute(sql, params)
```

其中 connector 实例由 `DataSourceManager` 创建、缓存或由测试直接注入。业务方新增系统时，只需要新增 connector 实现和注册项，不需要改动态表 action 或 executor。

### 9.4 SQL 字符串规范

如果目标 connector 不支持参数绑定，只接收完整 SQL 字符串，则 SDK 需要集中渲染 SQL 字面量：

1. 表名、列名只能来自 ontology，禁止来自用户入参；
2. 字符串值必须转义单引号，例如 `O'Reilly` 渲染为 `'O''Reilly'`；
3. `None` 渲染为 `NULL`；
4. boolean 按目标数据库方言渲染；
5. 日期时间按字符串渲染；
6. 数值必须校验类型后直接渲染；
7. `IN` 列表逐项渲染；
8. 所有 SQL 渲染逻辑集中在一个 helper 中，禁止各执行函数自行拼接用户值。

如果目标 connector 支持参数化请求体，应优先传递 `params`：

```json
{
  "sql": "SELECT * FROM t WHERE name = :name",
  "params": {"name": "白银有色"}
}
```

## 10. 执行器设计

新增执行器：

```text
packages/datacloud-data/src/datacloud_data_sdk/executor/dynamic_table_executor.py
```

类设计：

```python
class DynamicTableExecutor:
    def __init__(self, loader: OntologyLoader) -> None:
        ...

    async def query(self, object_code: str, arguments: dict[str, Any]) -> dict[str, Any]:
        ...

    async def compute(self, object_code: str, arguments: dict[str, Any]) -> dict[str, Any]:
        ...

    async def insert(self, object_code: str, arguments: dict[str, Any]) -> dict[str, Any]:
        ...

    async def update(self, object_code: str, arguments: dict[str, Any]) -> dict[str, Any]:
        ...

    async def delete(self, object_code: str, arguments: dict[str, Any]) -> dict[str, Any]:
        ...
```

公共内部能力：

1. `_get_class(object_code)`：读取并校验 `DYNAMIC_TABLE` 对象；
2. `_writable_fields(cls)`：返回可写字段；
3. `_field_to_column(field)`：解析物理列；
4. `_validate_insert_records(records, fields)`；
5. `_validate_update_values(values, fields)`；
6. `_get_db_type(datasource_alias)`：从 dbsource 的 `datasource_configs` 读取真实数据库方言；
7. `_build_query_sql(cls, arguments, db_type)`：复用 DB query SQL 生成规则，并按 `db_type` 渲染方言；
8. `_build_compute_sql(cls, arguments, db_type)`：复用 DB compute SQL 生成规则，并按 `db_type` 渲染方言；
9. `_build_where_sql(filters, field_map, db_type, relation)`：复用 query filters 规则，但输出符合 `db_type` 的完整 SQL 字面量；
10. `_columns_meta(fields)`：生成返回 `meta.columns`。

## 11. Action 路由设计

在 `Action._execute_virtual()` 增加对象级写操作路由：

```python
if action_family in {"insert", "update", "delete"}:
    from datacloud_data_sdk.executor.dynamic_table_executor import DynamicTableExecutor

    executor = DynamicTableExecutor(self._loader)
    if action_family == "insert":
        return await executor.insert(object_code, params)
    if action_family == "update":
        return await executor.update(object_code, params)
    return await executor.delete(object_code, params)
```

动态表对象的 `query` 和 `compute` 也走 `DynamicTableExecutor`；普通 DB 对象保持现状：

```python
if action_family == "query" and is_dynamic_table(cls):
    return await DynamicTableExecutor(self._loader).query(object_code, params)

if action_family == "compute" and is_dynamic_table(cls):
    return await DynamicTableExecutor(self._loader).compute(object_code, params)

if action_family == "query":
    return await QueryExecutor(self._loader).execute(object_code, params)

if action_family == "compute":
    return await ComputeExecutor(self._loader).execute(object_code, params)
```

## 12. 虚拟动作注入设计

动态表对象注入动作：

```python
actions = [
    query_action,
    compute_action,
    insert_action,
    update_action,
    delete_action,
]
```

动作字段：

```python
OntologyAction(
    action_code=f"insert_{object_code}",
    action_name=f"新增{object_name}",
    action_family="insert",
    virtual_backend="dynamic_table_connector",
    action_type="operation",
    is_virtual=True,
    input_schema=build_insert_schema(object_name, fields),
    scope_type="object",
    scope_code=object_code,
)
```

`update` 和 `delete` 同理。

注入策略：

1. `query/compute` 对 DB 和 DYNAMIC_TABLE 对象继续默认注入；
2. `insert/update/delete` 只对动态表对象注入；
3. 如果对象没有可写字段，不注入 `insert/update`；
4. 如果对象没有可过滤字段，不注入 `update/delete`，或注入但 schema 无法通过校验。

## 13. 返回结构约定

所有动作统一返回：

```json
{
  "records": [],
  "total": 0,
  "meta": {
    "columns": []
  }
}
```

`meta.columns` 是对 `records` 字段的描述：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `name` | string | record 字段名 |
| `label` | string | 字段显示名 |
| `type` | string | 字段类型 |
| `description` | string | 可选字段说明 |

写操作可以额外返回 `meta.operation` 标识操作类型，但 `meta.columns` 必须稳定存在。

## 14. 术语转换

动态表对象的字段属性如果绑定了术语，需要和现有 DB 查询保持一致做术语转换。术语转换是对象字段语义的一部分，不属于 connector 的职责。

适用范围：

1. `query_{object_code}` 返回记录；
2. `compute_{object_code}` 返回维度字段；
3. `insert_{object_code}` 入参和返回记录；
4. `update_{object_code}` 的 `values`、`filters` 和返回记录；
5. `delete_{object_code}` 的 `filters` 和返回的删除前记录。

转换规则：

1. 输入侧：当字段属性配置了 `term_set` / `term_type` / `term_field` 等术语绑定信息时，用户可传术语名称或编码；执行器在生成 SQL 前转换为物理存储值。
2. 过滤侧：`filters.value` 进入 SQL 前需要完成术语解析，避免把展示名直接写入 SQL。
3. 写入侧：`insert.records[*]` 和 `update.values` 中的术语字段需要先转换为存储值，再生成 SQL。
4. 输出侧：connector 返回记录后，使用 `ResultTermConverter` 或等价能力把存储值转换为业务可读术语值。
5. `meta.columns` 保持字段描述，不承载术语映射明细；如需要调试术语转换，可放入可选 `meta.runtime`。

推荐执行顺序：

```text
action arguments
  -> schema 校验
  -> 术语输入解析
  -> SQL 生成
  -> datasource connector.execute
  -> records 术语输出转换
  -> records / total / meta.columns
```

测试覆盖：

1. insert 传入术语名称时，实际 SQL 使用存储编码。
2. update.values 传入术语名称时，实际 SQL 使用存储编码。
3. query/filter 传入术语名称时，WHERE 使用存储编码。
4. 返回 records 中术语字段转换为业务可读值。

## 15. 安全与事务

1. SQL 值必须通过统一 SQL 字面量渲染 helper 处理；如果后端支持 params，则必须使用参数绑定。
2. 表名和列名只能来自 ontology，不允许用户输入。
3. SQL 方言必须由数据源 `db_type` 决定，禁止把 `DYNAMIC_TABLE` 当作方言名。
4. `update/delete` 必须带过滤条件。
5. 默认不允许写 derived/linked/formula 字段。
6. 批量 insert 应在事务内执行；任一行失败则整体回滚。
7. update/delete 需要配置最大影响行数阈值，例如 `dynamic_table_max_affected_rows`。
8. delete 固定为物理删除，不支持软删除分支。
9. 写操作错误应抛出明确异常或返回空 records，并在调用层统一格式化。

## 16. 测试方案

### 16.1 Schema 测试

1. `insert` schema 只包含可写字段。
2. `insert.records.items.additionalProperties == false`。
3. `insert` schema 默认不包含 `is_primary_key == true` 的自动生成主键字段。
4. `update.values.properties` 根据可写字段生成。
5. `update/delete.filters` 复用 query filters schema。
6. derived/linked/formula 字段不出现在写 schema 中。

### 16.2 注入测试

1. 动态表对象注入 `query_*`、`compute_*`、`insert_*`、`update_*`、`delete_*`。
2. 普通 DB 对象只注入 `query_*`、`compute_*`，不注入动态表写动作。
3. API/KNOWLEDGE_BASE 对象不注入动态表写动作。
4. 动作 `action_family` 和 `virtual_backend` 正确。

### 16.3 Executor 测试

使用注入到 `DataSourceManager` 的 `MockConnector` 覆盖：

1. insert 单条成功；
2. insert 批量成功；
3. insert 未声明字段失败；
4. insert 自动生成主键字段被 schema 屏蔽；
5. update 带 filters 成功，并返回修改后的行数据；
6. update 无 filters 失败；
7. delete 带 filters 成功，并返回删除前的行数据；
8. delete 无 filters 失败；
9. delete 生成物理 `DELETE`，不生成软删除 `UPDATE`；
10. derived/linked 字段写入失败；
11. 返回 `records/total/meta.columns` 结构正确。

### 16.4 Connector 测试

1. 业务方 connector 能通过 `ConnectorRegistry` 注册并被 `DataSourceManager` 获取。
2. connector 请求 URL、请求头、请求体由业务方实现测试覆盖。
3. connector 对外统一返回 `list[dict[str, Any]]`。
4. HTTP 非 2xx、业务失败、非法 JSON 都能转换为明确错误。
5. SQL 生成使用 dbsource 配置中的 `db_type`，不同 `db_type` 下标识符引用、分页、时间函数符合对应方言。
6. `connector_type` 只影响执行通道，不影响 SQL 方言。

### 16.5 复用测试

1. 动态表 `query_*` 复用 DB query 协议和 SQL 生成规则。
2. 动态表 `compute_*` 复用 DB compute 协议和 SQL 生成规则。
3. 普通 DB 对象仍走 `QueryExecutor` 和 `ComputeExecutor`。
4. 动态表动作不影响现有普通 DB query/compute 测试。

## 17. 实施步骤

1. 增加 `build_insert_schema()`、`build_update_schema()`、`build_delete_schema()`。
2. 增加动态表动作注入逻辑。
3. 复用现有 `BaseSourceConnector`、`ConnectorRegistry`、`DataSourceManager` 执行体系。
4. 如需要支持新的业务系统 HTTP API，由业务方新增 connector 实现并注册。
5. 新增 `DynamicTableExecutor`，覆盖 query/compute/insert/update/delete。
6. 在 `Action._execute_virtual()` 中将动态表对象的 query/compute/insert/update/delete 路由到 `DynamicTableExecutor`。
7. 增加 LoaderConfig 配置项，例如：

```python
dynamic_table_max_affected_rows: int = 1000
```

8. 补充术语输入解析和输出转换。
9. 补充 schema、注入、executor、connector、术语转换、路由测试。
10. 运行格式化、lint、mypy 和相关 pytest。

## 18. 待确认问题

1. 新增动作名称是否确定使用 `insert_{object_code}`，还是产品侧要求 `create_{object_code}`。
2. update/delete 是否需要强制按主键或唯一键过滤。
3. 是否所有动态表对象都必须定义主键；如果 update 返回修改后行数据，建议要求定义主键。
4. insert 是否需要返回服务端生成主键；如果需要，需要明确 `sqlExecute` 是否支持 RETURNING 或返回 last_insert_id。
5. 业务方 connector 的 `user_code` 是否必须优先读取当前登录用户上下文，还是允许使用固定 user code provider。
6. 业务方 connector 是否需要额外的运行时参数注入规范，例如 endpoint、token provider、`user_code` provider。
