feat: gate the automatic ALTER behind an explicit mode

两个 recorder 的 `__init__` 增 keyword-only 必填 `auto_migrate`(设计 D-c:
缺省规则只写在 config 一处,不与类签名漂移),并把写入语句从模块级常量改为
实例级: manual 档探测到旧表缺列时一条 ALTER 都不发,改按现有列裁剪 INSERT,
准备期发一次 warning(逐列点名 + "以下维度不会被记录" + 可直接执行的补列 SQL)。

裁剪是关掉 ALTER 的前提而非增强: 旧表缺列时若既不 ALTER 又不裁剪,每一行
INSERT 都撞 `no column named tenant_id` 被整行丢弃,比自动 ALTER 更严重地
违反"遥测必录"。auto 档行为逐字不变(先探测后 ALTER、duplicate column 视为
成功、失败只 warning 不判死、写入沿用全量列)。

探测失败、或探测结果与 COLUMNS 毫无交集,两档都保守回落全量列——空列集会让
`insert_sql` 产出 `INSERT INTO llm_calls () VALUES ()`(它不拒空列表,空集
技术上是子集)。PG 侧 `_columns`/`_insert` 与 `_schema_ready` 在同一处一起
赋值,不留"已就绪但语句还是旧的"窗口。

同批改 `GatewaySettings.telemetry_auto_migrate`(按后端派生: PG False、
SQLite True)与 `client._build_telemetry` 透传: 签名变更与其唯一调用点必须
落在同一次提交,否则该提交点整条装配路 TypeError。env 键留给下一步。
This commit is contained in:
2026-08-19 11:57:41 -04:00
parent ecc22b34fc
commit e949edb62a
8 changed files with 466 additions and 73 deletions
+8
View File
@@ -131,6 +131,9 @@ class GatewaySettings:
telemetry_backend: str
telemetry_sqlite_path: str | None
telemetry_pg_dsn: str | None
# 是否允许 recorder 给已存在的旧表自动 ALTER 补列(issue #13);
# 派生规则只写在 `_load_pgw` 一处,不与 recorder 的类签名漂移
telemetry_auto_migrate: bool
redis_url: str | None
pricing_path: str | None
structured_max_retries: int
@@ -444,6 +447,11 @@ def _load_pgw(env: Mapping[str, str]) -> dict[str, object]:
if telemetry_backend == "sqlite"
else None,
"telemetry_pg_dsn": _load_pg_dsn(env) if telemetry_backend == "postgres" else None,
# 按后端不对称派生(issue #13): SQLite 是下游自己的本地文件(无 DBA、无迁移
# 工具),补列是毫秒级元数据操作;PG 是共享生产表,ALTER 取 ACCESS EXCLUSIVE
# 锁会阻塞该表其后所有查询,而遥测是业务路径上的内联 await。
# backend=none 时无 recorder 消费该值,派生结果恒 False。
"telemetry_auto_migrate": telemetry_backend == "sqlite",
"redis_url": redis_url,
"pricing_path": env.get("PGW_PRICING_PATH") or None,
"structured_max_retries": _load_structured_retries(env),