# Schema Attribute 使用與 SQL 產生器設計

## 1. 文件目的

本文件整理第二階段 MySQL TaskStore PoC 中，`Schema Attribute` 的使用方式，以及後續 `SQL Generator` 要如何從 C# Class 產出 MySQL 5.6.2 相容 DDL。

目前程式 repo 已完成第一版：

- `DbTableAttribute`
- `DbColumnAttribute`
- `DbIndexAttribute`
- `SchemaDefinition`
- `TableDefinition`
- `ColumnDefinition`
- `IndexDefinition`
- `SchemaDefinitionParser`

本文件是「下一步 SQL 產生器與自動建表執行器」的設計依據，不代表已經同意實際連線 MySQL、執行 DDL、安裝套件、修改啟動方式或新增 WebApi / ServiceHost。

## 2. 目前已完成與尚未完成

| 區塊 | 狀態 | 說明 |
|---|---|---|
| Schema Attribute | 已完成第一版 | 可在 C# Class / Property 上描述 table、column、index |
| Schema Parser | 已完成第一版 | 可將 Attribute 解析成中立 `SchemaDefinition` |
| SQL Generator | 尚未完成 | 尚未把 `SchemaDefinition` 轉成 MySQL DDL |
| Schema Initializer | 尚未完成 | 尚未連線 DB，也尚未檢查 `information_schema` |
| TaskStore | 尚未完成 | 尚未把任務、節點、命令、錯誤寫入 MySQL |

## 3. 使用邊界

### 3.1 Schema Attribute 負責什麼

Schema Attribute 只負責描述資料結構意圖：

- Table 名稱與描述。
- Column 名稱、MySQL 型別、主鍵、必填與描述。
- Index 名稱、欄位與唯一性。

Schema Attribute 不負責：

- 連線 MySQL。
- 建立 database。
- 執行 SQL。
- 判斷 schema 是否需要升級。
- 做資料搬移。
- 管理查詢或交易。

### 3.2 SQL Generator 負責什麼

SQL Generator 只負責把 `SchemaDefinition` 轉成 SQL 字串：

- `CREATE TABLE IF NOT EXISTS`
- `ALTER TABLE ADD COLUMN`
- `ALTER TABLE ADD INDEX`
- `ALTER TABLE ADD UNIQUE INDEX`

SQL Generator 不直接執行 SQL，也不持有連線字串。

### 3.3 Schema Initializer 負責什麼

Schema Initializer 是後續才會建立的執行器，負責：

1. 讀取已註冊的 schema class。
2. 呼叫 `SchemaDefinitionParser` 取得 `SchemaDefinition`。
3. 讀取 MySQL `information_schema`。
4. 比對既有 table / column / index。
5. 呼叫 SQL Generator 產出需要執行的 SQL。
6. 依設定決定 Dry Run 或實際執行。
7. 將結果寫入 Log。

## 4. C# Class 使用方式

### 4.1 Table 與 Index

```csharp
[DbTable("task_executions", Description = "任務執行紀錄")]
[DbIndex("idx_task_executions_status", "status")]
[DbIndex("idx_task_executions_started_at", "started_at")]
public class TaskExecutionRecord
{
}
```

規則：

- 每個 schema class 必須有一個 `DbTableAttribute`。
- Table name 採用 snake_case。
- Index name 建議包含 table name，避免跨表重名難以追蹤。
- 第一版不在 Attribute 上設定 Engine / Charset，由 SQL Generator options 統一決定。

### 4.2 Column

```csharp
[DbColumn("task_id", "varchar(64)", IsPrimaryKey = true, IsRequired = true, Description = "任務識別碼")]
public string TaskId { get; set; }

[DbColumn("status", "varchar(32)", IsRequired = true, Description = "任務狀態")]
public string Status { get; set; }

[DbColumn("raw_context", "longtext", Description = "任務原始上下文 JSON 字串")]
public string RawContext { get; set; }
```

規則：

- `Name` 必填。
- `Type` 必填，直接使用 MySQL 5.6.2 相容型別字串。
- `IsPrimaryKey = true` 時，SQL Generator 應視為 `NOT NULL`。
- 第一版不支援 `DefaultValueSql`，避免預設值 SQL 寫法先擴散。
- 原始 JSON / request / response 建議用 `text` 或 `longtext`，不要使用 MySQL 5.6.2 不支援的 `json` 型別。

## 5. Schema 轉換流程

```mermaid
flowchart LR
    A["C# Record Class"] --> B["DbTable / DbColumn / DbIndex"]
    B --> C["SchemaDefinitionParser"]
    C --> D["SchemaDefinition"]
    D --> E["MySqlSchemaSqlGenerator"]
    E --> F["DDL SQL 字串"]
    F --> G["Schema Initializer 後續執行"]
```

第一版已完成 `A` 到 `D`。

本文件規劃下一步 `D` 到 `F`。

## 6. SQL Generator 建議介面

建議先建立純字串產生器，不連線 DB。

```csharp
public interface ISchemaSqlGenerator
{
    IReadOnlyList<string> GenerateCreateTableSql(SchemaDefinition schema);
    IReadOnlyList<string> GenerateAddColumnSql(SchemaDefinition schema, SchemaDiff diff);
    IReadOnlyList<string> GenerateAddIndexSql(SchemaDefinition schema, SchemaDiff diff);
}
```

若第一版尚未建立 `SchemaDiff`，可以先只做：

```csharp
public interface ICreateTableSqlGenerator
{
    string GenerateCreateTableSql(TableDefinition table);
}
```

建議第一批先實作 `GenerateCreateTableSql`，等可產出完整建表 SQL 後，再補 `ALTER TABLE`。

## 7. MySQL 5.6.2 DDL 規則

### 7.1 CREATE TABLE

產出範例：

```sql
CREATE TABLE IF NOT EXISTS `task_executions` (
  `task_id` varchar(64) NOT NULL COMMENT '任務識別碼',
  `workflow_id` varchar(64) NOT NULL COMMENT 'Workflow 識別碼',
  `status` varchar(32) NOT NULL COMMENT '任務狀態',
  `started_at` datetime NULL COMMENT '開始時間',
  `ended_at` datetime NULL COMMENT '結束時間',
  `raw_context` longtext NULL COMMENT '任務原始上下文 JSON 字串',
  PRIMARY KEY (`task_id`),
  INDEX `idx_task_executions_status` (`status`),
  INDEX `idx_task_executions_started_at` (`started_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='任務執行紀錄';
```

### 7.2 ADD COLUMN

產出範例：

```sql
ALTER TABLE `task_executions`
  ADD COLUMN `raw_context` longtext NULL COMMENT '任務原始上下文 JSON 字串';
```

### 7.3 ADD INDEX

產出範例：

```sql
ALTER TABLE `task_executions`
  ADD INDEX `idx_task_executions_status` (`status`);
```

唯一索引產出範例：

```sql
ALTER TABLE `task_executions`
  ADD UNIQUE INDEX `uk_task_executions_task_id` (`task_id`);
```

## 8. Identifier 與字串安全規則

SQL Generator 必須集中處理命名與字串 escaping。

| 類型 | 規則 |
|---|---|
| Table / Column / Index 名稱 | 只允許英數字與底線，建議 regex：`^[A-Za-z][A-Za-z0-9_]*$` |
| Identifier 包裝 | 使用反引號，例如 `` `task_executions` `` |
| COMMENT 字串 | 單引號需轉成兩個單引號，例如 `patient's` → `patient''s` |
| Type 字串 | 第一版只允許白名單型別或安全格式，不接受任意 SQL 片段 |

不建議讓 `Type` 可放入完整 SQL，例如：

```text
varchar(64) NOT NULL DEFAULT 'x'
```

原因是第一版的 `NOT NULL`、主鍵與描述已由 Attribute 欄位控制，若讓 `Type` 混入約束，後續 Generator 很難穩定判斷。

## 9. MySQL 型別白名單建議

第一版建議允許：

| 類型 | 範例 |
|---|---|
| 字串 | `varchar(32)`、`varchar(64)`、`varchar(128)`、`varchar(255)` |
| 長文字 | `text`、`longtext` |
| 整數 | `int`、`bigint` |
| 小數 | `decimal(18,4)` |
| 時間 | `datetime` |
| 布林 | `tinyint(1)` |

第一版不建議允許：

- `json`
- `generated column`
- `foreign key`
- `trigger`
- `stored procedure`
- 任意 `default` SQL

## 10. 差異比對設計

後續 Schema Initializer 需要建立 `SchemaDiff`，但第一批 SQL Generator 可以先不做。

建議差異分類：

| 差異 | 第一版處理方式 |
|---|---|
| Table 不存在 | 產出 `CREATE TABLE IF NOT EXISTS` |
| Column 不存在 | 產出 `ALTER TABLE ADD COLUMN` |
| Index 不存在 | 產出 `ALTER TABLE ADD INDEX` |
| Column 型別不同 | 回報 mismatch，不產出修改 SQL |
| Column 必填不同 | 回報 mismatch，不產出修改 SQL |
| Column 描述不同 | 第一版只警告 |
| DB 多出欄位 | 忽略或警告，不刪除 |
| DB 多出索引 | 忽略或警告，不刪除 |

## 11. Log 與錯誤回報

SQL Generator 與 Initializer 的錯誤需可追蹤：

| 情境 | 建議錯誤 |
|---|---|
| Table name 無效 | `SchemaDefinitionInvalid` |
| Column name 無效 | `SchemaDefinitionInvalid` |
| Column type 不在白名單 | `SchemaDefinitionInvalid` |
| Primary key 缺失 | `SchemaDefinitionInvalid` |
| Index 指向不存在欄位 | `SchemaDefinitionInvalid` |
| DB 權限不足 | 後續 DB 錯誤碼 |
| schema mismatch | 後續 DB / Schema mismatch 錯誤碼 |

Log 至少需包含：

- Table name
- Column name
- Index name
- SQL 動作類型
- Dry Run 或實際執行
- 錯誤代碼
- 錯誤訊息
- 耗時

## 12. 測試規劃

### 12.1 SQL Generator 單元測試

| 測試項目 | 預期結果 |
|---|---|
| 單一 table + primary key | 產出 `CREATE TABLE IF NOT EXISTS` |
| 多欄位 + 多索引 | 產出完整欄位與 index |
| unique index | 產出 `UNIQUE INDEX` |
| description 含單引號 | COMMENT 正確 escape |
| 無效 table name | 回傳錯誤 |
| 無效 column type | 回傳錯誤 |
| index 指向不存在欄位 | 回傳錯誤 |

### 12.2 Initializer 驗證節點

| 驗證項目 | 預期結果 |
|---|---|
| Dry Run | 只輸出 SQL，不修改 DB |
| 空 database | 建立 TaskStore table |
| 重複啟動 | 不重複建立、不報錯 |
| 缺少欄位 | 補欄位 |
| 缺少索引 | 補索引 |
| 欄位型別不一致 | 停止或警告，不自動改型別 |

## 13. 建議實作順序

1. 建立 `MySqlSchemaSqlGeneratorOptions`。
2. 建立 identifier / comment / type 驗證 helper。
3. 實作 `GenerateCreateTableSql`。
4. 補 SQL Generator 單元測試。
5. 實作 `GenerateAddColumnSql`。
6. 實作 `GenerateAddIndexSql`。
7. 設計 `SchemaDiff`。
8. 設計 `SchemaInitializer` Dry Run。
9. 等使用者確認 MySQL 連線資訊後，再做實際連線驗證。

## 14. 與現有文件關係

| 文件 | 關係 |
|---|---|
| [MySQL Schema 自動建表設計](./mysql-schema-auto-create-design.md) | 上層設計，定義整體自動建表策略 |
| [MySQL schema Class 初稿](./mysql-schema-class-draft.md) | 定義 TaskStore 四張表的欄位與索引 |
| [MySQL Schema Attribute 實作前確認](./mysql-schema-attribute-preimplementation-checklist.md) | 實作 Attribute 前的範圍確認 |
| 本文件 | Attribute 已完成後，銜接 SQL Generator / Initializer 的設計 |

## 15. 建議下一步

建議下一步先做：

> SQL Generator 實作前確認

確認項目：

- SQL Generator 是否先只做 `CREATE TABLE`。
- MySQL type 白名單是否足夠。
- Identifier 命名規則是否接受。
- 第一版是否先做 Dry Run，不實際連線。

確認後再進入程式 repo 實作 `MySqlSchemaSqlGenerator` 第一版。
