# Delta Lake Schema 演进修复方案

## Summary

本改动解决 Delta Lake 字段新增或删除后，云查询引擎仍持有旧表
schema 的问题：

- AWS Athena：首次查询明确返回 `INVALID_GLUE_SCHEMA` 时，对本次查询涉及的
  Clinic 托管 Delta 表通过 Glue `DeleteTable` 删除 Catalog 定义、按原模板
  重新 `CREATE TABLE`，然后重试原查询一次。
- GCP BigQuery：已有 Delta Lake 外部表达到配置的刷新间隔后，通过
  `tables.patch` 和 `autodetect_schema=true` 重新推断 schema。刷新失败
  fail-open，不阻断原查询。
- Azure Synapse 当前直接读取 Delta，不需要本次修复。
- 本方案支持字段新增、字段删除后的表元数据同步；字段重命名和字段类型变化
  不承诺自动兼容，仍需按查询引擎实际兼容性单独验证。

## Athena 失败后自愈

### 触发条件

只有同时满足以下条件才会进入修复：

- 查询引擎为 AWS Athena。
- 首次查询错误是结构化的 `AthenaQueryError`。
- `StateChangeReason` 包含 `INVALID_GLUE_SCHEMA:`。
- 请求没有设置 `skipTableManagement=true`。
- Athena Table Manager 已启用，且 `enabled`、`auto_create` 均为 true。
- SQL 引用的目标可以解析为 Clinic Table Manager 配置中的 Delta 物理表。

普通 SQL 错误、权限错误、超时、非 AWS 请求和非托管表都不会触发重建。

### 修复流程

1. Athena 查询失败时保留 query ID、state、reason 和 Athena error metadata。
2. 使用本次查询已经提取出的表引用确定修复范围。
3. 如果引用的是 View，根据模板的 `DependsOn` 展开为物理表。
4. 只保留 Table Manager 中 `IsDeltaTable()` 为真的托管表。
5. 从 Glue 读取现有表 metadata，确认：
   - `table_type=DELTA` 或 `spark.sql.sources.provider=delta`；
   - Glue `LOCATION` 与本次请求的 data source 一致。
6. 清除该表的存在性缓存。
7. 通过 Glue `DeleteTable` API 删除表定义。Athena 对 Delta Lake 的 DDL
   支持有限，不能通过 Athena SQL 执行 `DROP TABLE`。
8. 复用现有 Table Template 和本次请求的 data source，通过 Athena
   `CREATE EXTERNAL TABLE` 创建表。
9. 恢复表存在性缓存，并记录最近成功修复时间。
10. 使用原 context 和剩余 timeout 重试原 SQL 一次。

第二次查询失败后直接返回错误，不再触发下一轮修复。

### 并发和安全边界

- 同一个 Clinic 进程内，相同 `database/table/region` 的并发修复通过
  `singleflight` 合并。
- 已在首次查询失败时间之后修复成功的表会复用该修复结果。
- 当前并发合并是进程内的，不是跨 Pod 分布式锁；多副本同时收到相同错误时，
  仍可能分别执行重建。真实并发验收需要覆盖多副本场景。
- LOCATION 不一致、现有表不是 Delta、Glue DeleteTable 失败或 CREATE 失败时立即停止，
  不执行查询重试。
- 日志记录首次 query ID、region、修复表和重试结果，不记录凭证或完整 SQL。

## BigQuery 周期性 Schema Refresh

### 配置

GCP Table Manager 新增：

```yaml
gcp:
  table_manager:
    schema_refresh_seconds: 300
```

- `0`：关闭刷新，保持升级兼容。
- 大于 `0`：已有表距离本进程上次检查超过该秒数时，在下一次访问该表时触发
  检查。

这不是后台定时任务：没有查询流量时不会刷新。刷新时间保存在进程内存中，
因此每个 Pod 会独立检查和刷新。

当前分支示例配置为 `300`。考虑到生产 Schema 变更约每月一次，生产配置更建议：

- 设置为 `0`，在 Delta Schema 发布流程后显式执行
  `bq update --autodetect_schema PROJECT:DATASET.TABLE`；或者
- 暂时使用 `86400` 作为每日一次的低频兜底。

是否从 `300` 调整为 `0` 或 `86400` 应在部署前明确；本改动目前只提供刷新能力，
尚未实现 Schema 变更事件驱动。

### 刷新流程

1. `EnsureTableExists` 确认表已存在。
2. 独立检查 `schema_refresh_seconds`，不依赖存在性缓存 TTL。
3. 获取 BigQuery table metadata。
4. 仅当下列条件全部满足时刷新：
   - 表含 `ExternalDataConfig`；
   - `SourceFormat` 为 `DELTA_LAKE`；
   - 只有一个 Source URI；
   - Source URI 与本次 data source 指向同一个 GCS Delta 根目录。
5. 复制完整的 `ExternalDataConfig`，保留 URI、connection 和其他外表设置，
   清除旧的推断 Schema。
6. 使用当前 ETag 调用 `Table.Update`，并设置
   `bigquery.WithAutoDetectSchema(true)`。
7. 同一进程内同一 `dataset/table` 的并发刷新通过 `singleflight` 合并。
8. 成功或安全跳过后更新时间；失败不更新时间，并继续原查询，使后续请求可以
   再次尝试。
9. 新建表时记录刷新时间，避免创建后立即重复刷新。

刷新结果记录到：

```text
clinic_data_proxy_schema_refresh_total{provider="gcp",outcome="success|skipped|failure"}
```

## Code Changes

- `pkg/engine/athena.go`
  - 增加结构化 `AthenaQueryError`。
  - 保留 Athena error metadata。
  - 增加 Glue 表类型和 LOCATION 查询。
  - 增加 `ExecuteDDL`，DDL 成功后直接返回，不再下载并解析 S3 空结果文件。
- `pkg/engine/table_manager.go`
  - 解析托管 Delta 修复目标和 View 依赖。
  - 校验 Glue metadata。
  - 实现 Glue DeleteTable、CREATE、缓存恢复和进程内并发合并。
  - CREATE/DROP/ALTER/VIEW 等表管理 DDL 统一使用 `ExecuteDDL`。
- `pkg/service/dataproxy_service.go`
  - 保存查询表引用。
  - 首次命中目标错误后修复并只重试一次。
- `pkg/engine/bigquery.go`
  - 使用 ETag 和完整外表配置刷新 Delta Schema。
- `pkg/engine/bigquery_table_manager.go`
  - 实现请求触发的周期检查、fail-open 和进程内并发合并。
- `pkg/config/dataproxy_config.go`
  - 增加 `schema_refresh_seconds`。
- `pkg/observability/dataproxy_metrics.go`
  - 增加 GCP Schema 刷新结果指标。

## Automated Validation

### 快速验证

无需 AWS/GCP 凭证：

```bash
go test ./pkg/engine ./pkg/service \
  -run 'InvalidGlueSchema|DeltaRepair|ExecuteQuery|BigQuerySchemaRefresh|ExternalDataConfig|SameGCSDeltaRoot' \
  -count=1
```

重点覆盖：

- Athena 只识别结构化 `INVALID_GLUE_SCHEMA`。
- View 依赖解析到托管 Delta 物理表。
- 非 Delta 和 LOCATION 不匹配不会删除 Glue 表。
- Glue DeleteTable、CREATE 顺序以及缓存恢复。
- 同进程并发修复只执行一次。
- 原查询最多重试一次，且复用原 context。
- BigQuery 配置为 0 时关闭刷新。
- 刷新间隔、并发合并和失败 fail-open。
- GCS URI/format 检查以及外表配置保留。

### 包级回归

```bash
go test ./pkg/engine ./pkg/service -count=1
go test -race ./pkg/engine ./pkg/service \
  -run 'DeltaRepair|SchemaRefresh|ExecuteQuery' \
  -count=1
```

### 全仓库回归

```bash
go test ./... -count=1
```

全仓库中包含需要云凭证、外部服务或特定本地环境的测试。如果失败，需要区分：

- 本次涉及的 `pkg/engine`、`pkg/service` 回归失败；
- 与本次改动无关的既有集成测试或环境失败。

### 当前分支执行结果

2026-07-31 本地验证结果：

- 目标测试通过。
- `go test ./pkg/engine ./pkg/service -count=1` 通过。
- 目标 `-race` 测试通过。
- `go test ./... -count=1` 未全通过，但失败不在本次 Schema 修复代码：
  - 根包 `cluster_compare_test.go` 调用 `handlers.ClusterSqlDiff` 时仍使用旧函数签名；
  - `test/` 中部分用例要求本地 `127.0.0.1:8087` 服务；
  - 部分用例要求 Azure、AWS、飞书等外部凭证或测试资源；
  - `test/bitable_test.go` 使用的飞书测试文档不存在。

合并验收至少应保证前两组包级测试通过；全仓库既有失败应单独修复或在具备完整
集成环境的 CI 中执行。

## AWS Integration Validation

必须使用隔离的 S3 Delta 根目录、Glue database/table 和 Athena workgroup，
禁止直接对生产表做 Schema 破坏测试。

### 前置权限

测试身份至少需要：

- Athena 查询和 DDL 执行权限。
- Glue `GetTable`、`DeleteTable` 以及 Athena CREATE 对应的 Glue 表操作权限。
- 隔离 S3 Delta 数据和 Athena query result 路径的读写权限。

### 验证步骤

1. 在隔离 Delta 表写入初始 Schema，例如：

   ```text
   id BIGINT, old_field STRING, date STRING
   ```

2. 通过 Clinic Data Proxy 查询 `id`、`old_field`，确认初始查询成功并已创建
   Glue 表。
3. 在 Delta 表新增 `new_field` 并写入一条新数据，但不要手工更新 Glue 表。
4. 通过 Data Proxy 查询 `new_field`。
5. 验证：
   - 首次 Athena query history 中失败原因为 `INVALID_GLUE_SCHEMA`；
   - 服务日志包含 `Athena Delta schema repaired; retrying query once`；
   - Glue 表定义被删除，随后 Athena history 中出现 CREATE；
   - 只有一次原查询重试；
   - 重试查询成功并返回 `new_field`。
6. 删除一个没有被查询引用的字段，重复步骤 4、5，剩余字段应查询成功。
7. 删除一个仍被 SQL 引用的字段：
   - Schema 修复应执行；
   - 第二次查询应明确失败；
   - 不应出现循环 DeleteTable/CREATE 或第三次查询。
8. 验证安全拒绝：
   - 使用与 Glue LOCATION 不一致的 data source；
   - 使用非 Delta Glue 表；
   - 设置 `skipTableManagement=true`。

上述三种情况均不得调用 Glue DeleteTable。

### 并发验证

Schema 变更后，同时发起 10 个完全相同的查询：

```bash
seq 1 10 | xargs -P10 -I{} curl -sS \
  -X POST "${DATA_PROXY_URL}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  --data-binary "@${REQUEST_JSON}"
```

单 Pod 环境预期只出现一组 Glue DeleteTable、CREATE；多 Pod 环境需要额外观察是否存在跨
Pod 重复重建。若多 Pod 重复重建造成查询不稳定，需要在上线前增加分布式锁，
或将 Schema 修复请求固定路由到单实例。

### 2026-07-31 本地真实 AWS 验证

使用本地 8087 服务和 AWS SSO 临时身份，对 `ap-south-1` 的现有 Delta 表完成
真实验证：

- 首次 SELECT 返回 `INVALID_GLUE_SCHEMA`。
- Glue DeleteTable 成功，Athena CREATE 成功。
- 服务日志出现 `Athena Delta schema repaired; retrying query once`。
- retry once 成功，聚合查询返回 `cnt=89`。
- 紧接着重复相同请求直接成功，只执行一次 SELECT，没有再次重建表。

首次验证 trace ID 为 `2daf1a4b3976a0b167828b5e1418ae79`，最终重试 Athena
query ID 为 `aa34743b-b7cc-4023-9597-dcef89cc1ad9`。稳定状态复查 trace ID 为
`bc7a1ba109f05c8363c8a0f294fda4cc`。

## GCP Integration Validation

必须使用隔离的 GCS Delta 根目录、BigQuery dataset 和外部表。

### 前置权限

测试身份至少需要：

- `bigquery.tables.get`
- `bigquery.tables.update`
- GCS Delta 根目录读取权限

### 验证步骤

1. 测试环境临时设置较短周期，例如：

   ```yaml
   schema_refresh_seconds: 10
   ```

2. 创建初始 Delta 表和 BigQuery Delta Lake 外部表。
3. 记录当前 Schema：

   ```bash
   bq show --schema --format=prettyjson PROJECT:DATASET.TABLE
   ```

4. 在 Delta 表新增 `new_field` 并写入数据。
5. 等待超过测试刷新周期，然后通过 Data Proxy 查询该表，触发
   `EnsureTableExists`。
6. 再次执行 `bq show`，确认 `new_field` 已进入 BigQuery 表 Schema。
7. 查询 `new_field`，确认数据可读。
8. 检查指标：

   ```promql
   clinic_data_proxy_schema_refresh_total{provider="gcp"}
   ```

   `outcome="success"` 应增加。
9. 连续多次查询，确认刷新周期内计数不再增加。
10. 将 `schema_refresh_seconds` 设置为 `0` 并重启，执行新的 Schema 变更，
    确认查询不会触发自动刷新。

### Fail-open 验证

在隔离测试身份中临时撤销 `bigquery.tables.update`，保留 get/query 权限：

- 查询旧字段仍应成功；
- 日志应出现 `BigQuery Delta schema refresh failed`；
- `outcome="failure"` 指标增加；
- 恢复权限后，下一次请求应再次尝试并刷新成功。

权限调整会改变云端状态，若不方便操作，保留单元测试
`TestBigQuerySchemaRefreshFailureIsFailOpenAndRetried` 作为该场景的自动化验收。

## Acceptance Checklist

- [ ] 快速验证和包级回归通过。
- [ ] race 测试通过。
- [ ] Athena 新增字段能够自动执行 Glue DeleteTable、CREATE 并重试成功。
- [ ] Athena 普通错误、非托管表和 LOCATION 不匹配不会删除 Glue 表。
- [ ] Athena 第二次查询失败后停止，不循环修复。
- [ ] BigQuery 新增字段在刷新后可查询。
- [ ] BigQuery 刷新失败不影响旧字段查询。
- [ ] 观察日志和 GCP Schema refresh metric 符合预期。
- [ ] 多 Pod Athena 并发重建风险完成验证或给出上线处置。
- [ ] 上线前明确 GCP 生产刷新策略：`0`、`86400` 或事件驱动。
