目录接口对探查范围外的表照实说「在探查范围外」,不再说「可能已被删除或尚未探查结构」

验收:GET /api/datasources/{id}/catalog/{表} 对「对象索引里有、但在探查范围外」的表回 404「数据源中没有表 …,可能
已被删除或尚未探查结构」,而 /schema 和 /catalog/search 都说对了。_table_key 一律写死这一句,没看 absent_reasons。

改法:source_objects 加 ABSENT_MESSAGE 和 absent_message(),给人看的五种「不在」(表已不在、全库对象清单里也没有、
在探查范围外、在范围里却读不到结构、老缓存说不清)一句一句,和界面的 CATALOG_ABSENT_TEXT、/schema 用的
schema_view.absent_detail 同一套原因。上传表格、记录表按版本走,没有对象索引又没截断过的老缓存认不出名字是不是
从来就没有过:这两种只说「数据源中没有表 …」,不说「已删除」。用到的三处都换成它:_table_key(表详情、审阅、
使用记录等十来个接口共用)、表助手开始前的 404、批量审阅逐条的 table_missing 错误。

测试:test_catalog_schema_truncated 新增一条(205 张表、规则只收 200 张的合成库):范围外的表在表详情、表助手、
批量审阅里都说「探查范围外」,对象索引里也没有的说「没有表 …」。去掉修复回跑挂。
This commit is contained in:
ThearchyHelios
2026-10-17 08:28:54 +08:00
parent 7db9760121
commit c64c7a1062
4 changed files with 62 additions and 6 deletions
+10 -3
View File
@@ -226,14 +226,21 @@ async def _detail(session: AsyncSession, source: Any, table: str, entry: catalog
async def _table_key(session: AsyncSession, source: Any, table: str) -> tuple[str, catalog.CatalogEntry | None]:
"""路径里的表名 → 目录的键。表结构里有的按表结构的写法;表结构里没有、但还留着目录的也认(便于清理)。"""
"""路径里的表名 → 目录的键。表结构里有的按表结构的写法;表结构里没有、但还留着目录的也认(便于清理)。都没有时 404,
说清是哪一种「不在」(_missing)。"""
key = catalog.resolve_table_name(source.schema_cache, table)
entry = await catalog.read_entry(session, source.id, key or table)
if key is None and entry is None:
raise HTTPException(404, f"数据源中没有表 {table},可能已被删除或尚未探查结构")
raise await _missing(session, source, table)
return key or table, entry
async def _missing(session: AsyncSession, source: Any, table: str) -> HTTPException:
"""表结构里没有这张表的 404:表已不在、数据源里没有、在探查范围外、读不到结构、说不清,和 /schema、/catalog/search、
目录清单的 absent_reason 同一套(source_objects.absent_message)。"""
return HTTPException(404, await source_objects.absent_message(session, source, table))
def _conflict(e: catalog.CatalogConflict) -> HTTPException:
return HTTPException(409, str(e))
@@ -906,7 +913,7 @@ async def catalog_table_assistant(source_id: str, table: str, payload: Assistant
row, source = await _resolved(session, source_id)
key = catalog.resolve_table_name(source.schema_cache, table)
if key is None:
raise HTTPException(404, f"数据源中没有表 {table},可能已被删除或尚未探查结构")
raise await _missing(session, source, table)
messages = [m.model_dump() for m in payload.messages]
if problem := catalog_assistant.conversation_problem(messages):
raise HTTPException(422, problem)
+2 -2
View File
@@ -41,7 +41,7 @@ from typing import Any
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.data import catalog
from app.data import catalog, source_objects
from app.db.models import Run
#: 队列的筛选:全部、影响数字的项、名称和说明
@@ -346,7 +346,7 @@ async def review_batch(session: AsyncSession, source: Any, items: Iterable[Batch
key = known[item.table_name]
if key is None:
results[i] = _result(item, ok=False, code=CODE_TABLE_MISSING,
error=f"数据源中没有表 {item.table_name},可能已被删除或尚未探查结构")
error=await source_objects.absent_message(session, source, item.table_name))
continue
groups.setdefault(key, []).append(i)
at = catalog.now_iso()
+29 -1
View File
@@ -825,6 +825,33 @@ async def absent_reasons(session: AsyncSession, row: Any, names: Iterable[str])
return out
#: 表结构里没有的表给人看的一句(接口的 404、批量审阅逐条的错误):和界面的 CATALOG_ABSENT_TEXT、/schema 的说法
#: (schema_view.absent_detail,给模型看)同一套「不在」。{table} 是点名的表
ABSENT_MESSAGE = {
ABSENT_REMOVED: "数据源中已经没有表 {table}(已删除或改名)",
ABSENT_MISSING: "数据源中没有表 {table}(全库的对象清单里也没有)",
ABSENT_OUT_OF_SCOPE: ("表 {table} 在探查范围外:对象索引里有它,但没有读取结构。在数据源卡片的「探查范围」里加入它,"
"或在数据目录的搜索框搜索它并「加入范围」"),
ABSENT_REFLECT_FAILED: "表 {table} 在探查范围内,但上次探查读不到它的结构(权限不够或对象有问题),修好后重新探查即可",
ABSENT_UNKNOWN: ("表 {table} 不在已探查的表里:升级前的探查只取了一部分表,无法判断它是否还在,请回到数据源卡片重新"
"「探查结构」"),
}
#: 说不出是哪一种「不在」时(上传表格、记录表按版本走;没有对象索引、又没截断过的老缓存认不出名字是不是从来就没有)
ABSENT_PLAIN = "数据源中没有表 {table}"
async def absent_message(session: AsyncSession, row: Any, table: str) -> str:
"""点名的表不在表结构里时给人看的一句(ABSENT_MESSAGE)。手工接入的源按 absent_reasons 说是哪一种;上传表格、记录表
的结构随版本走,只说没有(目录清单把它们记成 removed,是说「那一版里没有」,点名一个从来没有过的名字时说不得「已删除」);
没有对象索引的老缓存同理:absent_reasons 对没截断的一律记 removed,名字写错了也会这样,只说没有。"""
if (getattr(row, "origin", None) or "manual") != "manual":
return ABSENT_PLAIN.format(table=table)
reason = (await absent_reasons(session, row, [table])).get(table)
if reason == ABSENT_REMOVED and not await has_index(session, row.id, index_schema(row)):
reason = None
return ABSENT_MESSAGE.get(reason or "", ABSENT_PLAIN).format(table=table)
@dataclass(frozen=True)
class OutsideObject:
"""对象索引里在、范围外的一个对象(助手检索、目录搜索用)。"""
@@ -1039,7 +1066,8 @@ async def delete_index(session: AsyncSession, source_id: str) -> None:
__all__ = [
"ABSENT_MISSING", "ABSENT_OUT_OF_SCOPE", "ABSENT_REFLECT_FAILED", "ABSENT_REMOVED", "ABSENT_UNKNOWN", "ADD_LIMIT", "AddOutcome",
"ABSENT_MESSAGE", "ABSENT_MISSING", "ABSENT_OUT_OF_SCOPE", "ABSENT_PLAIN", "ABSENT_REFLECT_FAILED", "ABSENT_REMOVED",
"ABSENT_UNKNOWN", "ADD_LIMIT", "AddOutcome",
"INDEX_LIMIT", "IndexResult", "IndexedObject", "OutsideObject", "REFLECT_BATCH", "RULES_MAX", "Reflected",
"RefreshStats", "SCOPE_DEFAULT", "SCOPE_MAX", "SCOPE_OPTION", "SCOPE_REASONS", "ScopePlan", "ScopeRules", "Trail",
"VIA_CATALOG", "VIA_LOOK", "VIA_SCHEMA_TOOL", "absent_reasons", "add_to_scope", "apply_scope",
@@ -157,3 +157,24 @@ def test_legacy_truncated_cache_still_does_not_deny():
"truncated": True, "total": 205})
text = describe_table(row, "t300")
assert "中没有" not in text and "只探查了" in text and "information_schema" in text
async def test_catalog_detail_of_an_out_of_scope_table_says_so(client, wide):
"""对象索引里有、在探查范围外的表:GET /catalog/{表} 的 404 说「在探查范围外」,和 /schema、/catalog/search 同一套「不在」
(下一轮验收:以前说「可能已被删除或尚未探查结构」);表助手、批量审阅的同一个错误也一样。对象索引里也没有的说「没有
这张表」。"""
sid, _ = wide
r = await client.get(f"/api/datasources/{sid}/catalog/t200")
assert r.status_code == 404 and "探查范围外" in r.json()["detail"], r.text
assert "可能已被删除" not in r.json()["detail"]
found = (await client.get(f"/api/datasources/{sid}/catalog/search", params={"q": "t200"})).json()["items"]
assert [i["absent_reason"] for i in found if i["name"] == "t200"] in (["out_of_scope"], [None]), found
r = await client.post(f"/api/datasources/{sid}/catalog/t201/assistant", json={"messages": [
{"role": "user", "content": "这张表是干什么的"}]})
assert r.status_code == 404 and "探查范围外" in r.json()["detail"], r.text
r = await client.post(f"/api/datasources/{sid}/catalog/review/batch", json={"items": [
{"table_name": "t202", "path": "label", "action": "confirm", "if_version": 0}]})
[result] = r.json()["results"]
assert result["ok"] is False and "探查范围外" in result["error"], result
r = await client.get(f"/api/datasources/{sid}/catalog/no_such_table")
assert r.status_code == 404 and "没有表 no_such_table" in r.json()["detail"] and "探查范围外" not in r.json()["detail"]