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:
@@ -214,6 +214,19 @@ class TestSchemaModule:
|
||||
with pytest.raises(ValueError, match="mysql"):
|
||||
insert_sql("mysql", COLUMNS)
|
||||
|
||||
def test_insert_sql_rejects_an_empty_column_set(self):
|
||||
"""空列集合两端都拼出语法非法的 SQL,构造器自己拒,不押在调用方的不变量上。
|
||||
|
||||
入参来自数据库探测结果: 探测到一张与本库毫无共同列的同名表,`effective`
|
||||
就是空的。真放行会产出 `INSERT OR IGNORE INTO llm_calls () VALUES ()`,
|
||||
错误要到执行时才由数据库报,离真因很远。
|
||||
"""
|
||||
from polygateway.telemetry.schema import insert_sql
|
||||
|
||||
for backend in ("sqlite", "postgres"):
|
||||
with pytest.raises(ValueError, match="至少需要一列"):
|
||||
insert_sql(backend, [])
|
||||
|
||||
def test_schema_sql_is_paste_ready_and_same_source(self):
|
||||
"""打印给下游的脚本与库执行的 DDL 同源,且对人可重复执行。"""
|
||||
from polygateway.telemetry.schema import PG_BACKFILL, SQLITE_BACKFILL, telemetry_schema_sql
|
||||
@@ -628,8 +641,9 @@ class TestSQLiteSchemaMode:
|
||||
):
|
||||
"""探测结果与 COLUMNS 毫无交集视同探测异常: 保守回落全量列。
|
||||
|
||||
空列集会构造出 `INSERT INTO llm_calls () VALUES ()` 这种语法非法的语句
|
||||
(`insert_sql` 不拒空列表——空集技术上是子集),故必须在交给它之前拦住。
|
||||
`insert_sql` 自己拒空列集合(见 `test_insert_sql_rejects_an_empty_column_set`),
|
||||
故这里回落不发生就不是"拼出空语句",而是 ValueError 逃出 `__init__` ——
|
||||
遥测初始化失败必须静默降级,崩溃比丢维度严重得多。
|
||||
"""
|
||||
from polygateway.telemetry.schema import COLUMNS
|
||||
|
||||
@@ -645,6 +659,30 @@ class TestSQLiteSchemaMode:
|
||||
recorder.close()
|
||||
assert captured_warnings # 沉默地退化成空语句是最坏结果,必须有声
|
||||
|
||||
async def test_empty_probe_result_degrades_instead_of_raising(
|
||||
self, tmp_path, captured_warnings
|
||||
):
|
||||
"""探测返回空集合时走回落,绝不让 `insert_sql` 的 ValueError 逃出去。
|
||||
|
||||
SQLite 建不出零列的表,故直接喂空探测结果调那条分支——它正是
|
||||
`insert_sql` 拒空之后唯一可能把"静默降级"变成崩溃的入口。
|
||||
"""
|
||||
from polygateway.telemetry.schema import COLUMNS
|
||||
|
||||
db = tmp_path / "empty_probe.db"
|
||||
recorder = SQLiteRecorder(db, auto_migrate=False)
|
||||
recorder._adopt_existing_columns(set()) # 不得抛
|
||||
assert recorder._columns == COLUMNS
|
||||
await _record_minimal(recorder, call_id="c-after") # 写入照常
|
||||
recorder.close()
|
||||
|
||||
conn = sqlite3.connect(db)
|
||||
assert conn.execute(
|
||||
"SELECT response FROM llm_calls WHERE call_id = 'c-after'"
|
||||
).fetchone() == ("ok",)
|
||||
conn.close()
|
||||
assert [m for m in captured_warnings if "没有任何本库认识的列" in m] # 只有 warning
|
||||
|
||||
async def test_auto_migrate_is_required_keyword_only(self, tmp_path):
|
||||
"""关键行为参数不给默认值(P4): 缺省规则只写在 config 一处,不与类签名漂移。"""
|
||||
with pytest.raises(TypeError):
|
||||
@@ -877,6 +915,25 @@ class TestPostgresSchemaMode:
|
||||
assert [s for s in conn.statements if s.lstrip().startswith("CREATE TABLE")]
|
||||
assert recorder._columns == COLUMNS
|
||||
|
||||
async def test_no_recognizable_column_falls_back_to_the_full_column_set(
|
||||
self, captured_warnings
|
||||
):
|
||||
"""PG 侧同款回落(SQLite 侧对称用例见 TestSQLiteSchemaMode)。
|
||||
|
||||
表存在(`to_regclass` 非空)但列与 `COLUMNS` 毫无交集: 裁剪结果为空,
|
||||
必须回落全量而不是把空列集交给 `insert_sql`——`_prepare_schema` 里那次
|
||||
调用在 try 之外,ValueError 会顺着 `record_llm_call` 冒给业务调用方。
|
||||
"""
|
||||
from polygateway.telemetry.schema import COLUMNS
|
||||
|
||||
conn = _FakePgConn(["foo", "bar"])
|
||||
recorder = self._recorder(conn, auto_migrate=False)
|
||||
await _record_minimal(recorder) # 不得抛
|
||||
|
||||
assert recorder._columns == COLUMNS
|
||||
assert not [s for s in conn.statements if s.startswith("ALTER TABLE")]
|
||||
assert [m for m in captured_warnings if "没有任何本库认识的列" in m]
|
||||
|
||||
async def test_auto_mode_still_backfills(self):
|
||||
"""auto 档现状回归: 缺列照补,补完写全量列。"""
|
||||
from polygateway.telemetry.schema import COLUMNS, PG_BACKFILL
|
||||
|
||||
Reference in New Issue
Block a user