fix: reject an empty column set in insert_sql

`insert_sql(backend, [])` 此前返回 `INSERT OR IGNORE INTO llm_calls () VALUES ()`
与 `INSERT INTO llm_calls () VALUES () ON CONFLICT DO NOTHING`,两条都语法非法。
入参正来自数据库列探测(遇到一张与本库毫无共同列的同名表,裁剪结果就是空),
把"非空"押在调用方的不变量上不成立——共享构造器自己拒,与它既有的"未知
backend""非 COLUMNS 子集"两道校验同款。

连带风险已实测确认: 两个 recorder 的空集回落都发生在调用 `insert_sql` **之前**,
故新增的 raise 不会逃出 SQLite 的 `__init__`(遥测初始化失败必须静默降级)或
PG 的准备期(`_prepare_schema` 里那次调用在 try 之外,异常会一路冒给业务调用方)。
新增 SQLite 空探测结果用例: 构造成功、写入照常、只有 warning。

同时补 PG 侧"探测结果与 COLUMNS 无交集"的回落用例(此前只有 SQLite 侧有),
并把承认缺口的那段测试注释改成断言拒绝。
This commit is contained in:
2026-08-19 13:18:21 -04:00
parent ba4a138692
commit 0721cf60aa
4 changed files with 73 additions and 11 deletions
+4 -3
View File
@@ -183,9 +183,10 @@ class PostgresRecorder:
裁剪是关掉 ALTER 的**前提**而非增强: 旧表缺列时仍发全量 INSERT,每一行
都会因未知列被拒 → 遥测彻底丢失,比自动 ALTER 更严重地违反"遥测必录"
探测结果与 `COLUMNS` 毫无交集时视同探测异常保守回落全量: 空列集会构造出
`INSERT INTO llm_calls () VALUES ()` 这种语法非法的语句(`insert_sql` 拦
不住——空集技术上是子集),必须在交给它之前拦下。
探测结果与 `COLUMNS` 毫无交集时视同探测异常保守回落全量: 空列集拼不出合法
INSERT,`insert_sql` 会 ValueError,而 `_prepare_schema` 里那次调用在 try
**之外**,异常会顺着 `record_llm_call` 一路冒给业务调用方(遥测绝不冒泡)
——回落必须发生在把空列集交给它之前。
"""
effective = tuple(column for column in COLUMNS if column in existing)
if not effective:
+7 -3
View File
@@ -149,10 +149,12 @@ _COLUMN_SET = frozenset(COLUMNS)
def insert_sql(backend: str, columns: Sequence[str]) -> str:
"""按给定列构造 INSERT;列必须是 `COLUMNS` 的子集,否则 ValueError。
"""按给定列构造 INSERT;列必须是 `COLUMNS` 的非空子集,否则 ValueError。
子集校验是**注入面的闸**: 列名来自数据库探测结果,不是常量,不校验就等于把外部
字符串拼进 SQL(占位符只保护值,保护不了列名)。sqlite 用 `?`、postgres 用 `$n`,
字符串拼进 SQL(占位符只保护值,保护不了列名)。空集同样来自探测结果,而
`INSERT INTO llm_calls () VALUES ()` 两端都语法非法——本函数自己拒,不把这个
不变量押在调用方身上。sqlite 用 `?`、postgres 用 `$n`,
两端的重复键处理都不绑定具体约束名(`INSERT OR IGNORE` / `ON CONFLICT`)。
**PG 的 `ON CONFLICT` 一律不带冲突目标,不得"顺手"补回 `(call_id)`**: PG 要求
@@ -170,11 +172,13 @@ def insert_sql(backend: str, columns: Sequence[str]) -> str:
完整的 INSERT 语句。
Raises:
ValueError: backend 不在取值域内,columns 含 `COLUMNS` 之外的列名。
ValueError: backend 不在取值域内,columns 为空,或含 `COLUMNS` 之外的列名。
"""
if backend not in _BACKENDS:
raise ValueError(f"未知遥测后端 {backend!r}: 只支持 {list(_BACKENDS)}")
selected = tuple(columns)
if not selected:
raise ValueError("遥测 INSERT 至少需要一列: 空列集合会拼出语法非法的 SQL")
unknown = [column for column in selected if column not in _COLUMN_SET]
if unknown:
raise ValueError(f"列名不在遥测 schema 内(拒绝拼进 SQL): {unknown}")
+3 -3
View File
@@ -87,9 +87,9 @@ class SQLiteRecorder:
裁剪是关掉 ALTER 的**前提**而非增强: 旧表缺列时仍发全量 INSERT,每一行
都会因未知列被拒 → 遥测彻底丢失,比自动 ALTER 更严重地违反"遥测必录"
探测结果与 `COLUMNS` 毫无交集时视同探测异常保守回落全量: 空列集会构造出
`INSERT INTO llm_calls () VALUES ()` 这种语法非法的语句(`insert_sql` 拦
不住——空集技术上是子集),必须在交给它之前拦下
探测结果与 `COLUMNS` 毫无交集时视同探测异常保守回落全量: 空列集拼不出合法
INSERT,`insert_sql` 会 ValueError,而遥测构造期抛异常就是把"初始化失败静默
降级"的铁律破成崩溃——回落必须发生在把空列集交给它之前。
"""
effective = tuple(column for column in COLUMNS if column in existing)
if not effective: