Commit 03eb8d63 authored by Data Governance Dev's avatar Data Governance Dev

feat(web3): 结果表全部已配置列展示实际值 + 表头 tooltip 显示字段注释

后端 queries.py:
- SQL 改成 SELECT 全部已配置字段(早期只 SELECT「rules ∪ show_default」,
  导致结果表表头 14 列、数据只有 1 列;列与数据错位让非规则列全部 NULL)
- 打开连接后调一次 list_columns,从 info_schema 拉字段注释写进
  field_list[i].field_comment,供前端表头 tooltip
- field_list 顺带回每条规则的 rule_list({id,desc,regex}),
  供「⚠ N规则」badge hover 出规则明细

前端:
- DataQualityView.vue: resultFields 映射补 comment / ruleList
- ResultTable.vue: 表头 cn 包 el-tooltip(无注释则退化 plain span),
  已有「⚠ N规则」tooltip 不动

测试:
- test_run_query_only_bad_rows.py 加 _FakeDBConnection.list_columns + 3 条用例
  (show_default=false + 无规则的字段也要 SELECT;field_comment 从数据源补回;
  rule_list 带回规则明细),共 12 条全过
parent 8013322d
......@@ -95,7 +95,8 @@ async def run_query(req: RunQueryRequest, db: Session = Depends(get_session)):
(f, rules_by_field[f.id]) for f in fields if rules_by_field[f.id]
]
# 顺带回一份字段列表(表头用);与 FieldOut 结构对齐
# 顺带回一份字段列表(表头用);包含规则明细(rule_list),前端可渲染「规则明细」tooltip
# 不含 field_comment —— 表头注释从数据源补,见下方「打开连接后从 info_schema 拉」
field_list_out = [
{
"id": f.id,
......@@ -103,6 +104,10 @@ async def run_query(req: RunQueryRequest, db: Session = Depends(get_session)):
"show_default": bool(f.show_default),
"ord": f.ord,
"rules": len(rules_by_field[f.id]),
"rule_list": [
{"id": r.id, "desc": r.desc, "regex": r.regex}
for r in rules_by_field[f.id]
],
}
for f in fields
]
......@@ -149,15 +154,19 @@ async def run_query(req: RunQueryRequest, db: Session = Depends(get_session)):
compiled.append((f, rule_patterns))
# 6) 连库 + 拉数据
# SELECT 的列 = 有规则的字段 ∪ show_default 的字段
# (show_default 但没规则的列不校验,只是结果表要展示出来当上下文)
# SELECT 的列 = 任务已配置的**全部**字段(plan §3 决策:结果表展示全部已配置列作为上下文,
# 规则只决定哪些列要标红,不决定哪些列要展示;早期版本限制为「rules ∪ show_default」,
# 但结果表表头用的是 field_list(全部已配置),列与数据错位 → 非规则列一片 NULL。
# 仍不做 SELECT * —— 只 SELECT 任务显式配置的列,防止暴露未配置的敏感列)
started = time.monotonic()
table = task.source_table
schema = (conn.get("schema") or conn.get("db") or "").strip()
col_names: list[str] = []
seen: set[str] = set()
for f in fields:
if rules_by_field[f.id] or f.show_default:
if f.field_key not in col_names:
col_names.append(f.field_key)
if f.field_key and f.field_key not in seen:
col_names.append(f.field_key)
seen.add(f.field_key)
col_sql = ", ".join(quote_ident(c, cfg.db_type) for c in col_names)
# 全表扫描:不带 LIMIT。靠 iter_rows 流式读,整表不进内存
sql = f"SELECT {col_sql} FROM {quote_ident(table, cfg.db_type)}"
......@@ -178,6 +187,25 @@ async def run_query(req: RunQueryRequest, db: Session = Depends(get_session)):
out_rows: list[dict[str, Any]] = []
try:
with DBConnection(cfg) as dbc:
# 7.0) 从数据源补全字段注释(仅展示用,失败也不阻塞主查询)
# 字段的 column_comment 没有落库(plan §4 决策),这里直接从 info_schema 拉一次
# 失败时 field_comment 留空字符串,前端 tooltip 退化到「暂无注释」
if schema and table:
try:
col_meta = dbc.list_columns(schema, table) or []
comments_by_key: dict[str, str] = {
(m.get("column_name") or "").lower(): (m.get("column_comment") or "")
for m in col_meta
if m.get("column_name")
}
for f_out in field_list_out:
c = comments_by_key.get(f_out["field_key"].lower(), "")
if c:
f_out["field_comment"] = c
except Exception as e:
logger.warning(f"[queries] 获取字段注释失败,跳过:{type(e).__name__}: {e}")
# 7.1) 流式扫全表 + 逐行校验
for raw in dbc.iter_rows(sql):
scanned += 1
# 列名归一为小写(达梦默认大写,MySQL 视 collation)
......
......@@ -32,7 +32,15 @@
<span class="flag">!</span><span class="cn">备注</span>
</template>
<template v-else>
<span class="cn">{{ col.cn }}</span>
<el-tooltip
v-if="col.comment"
:content="col.comment"
placement="top"
effect="dark"
>
<span class="cn">{{ col.cn }}</span>
</el-tooltip>
<span v-else class="cn">{{ col.cn }}</span>
<span class="en">{{ col.en }}</span>
<el-tooltip
v-if="col.rules > 0"
......
......@@ -170,7 +170,9 @@ async function onQuery() {
key: f.field_key,
cn: f.field_key, // 后端没存中文名,先用英文 key 占位
en: '',
comment: f.field_comment || '', // 来自数据源 COLUMN_COMMENT → 表头 tooltip
rules: f.rules || 0, // 数字 → 表头显示「⚠ N规则」
ruleList: f.rule_list || [], // 规则明细(含 desc)→ 表头「⚠ N规则」hover 出规则列表
showDefault: f.show_default !== false, // 任务配置「默认展示」未勾选 → 结果表不展示该列
}))
lastResult.value = r
......
......@@ -52,6 +52,13 @@ class _FakeDBConnection:
last_sql = None
rows = FAKE_TABLE_ROWS
column_meta = [ # 假装数据源 info_schema 返回的元数据
{"column_name": "id_card", "column_comment": "身份证号"},
{"column_name": "phone", "column_comment": "手机号"},
{"column_name": "person_name", "column_comment": "姓名"},
{"column_name": "nickname", "column_comment": "昵称"},
]
list_columns_called_with = None # 用来断言「打开连接后去拉过字段注释」
def __init__(self, cfg):
self.cfg = cfg
......@@ -67,6 +74,13 @@ class _FakeDBConnection:
for r in _FakeDBConnection.rows:
yield dict(r)
def list_columns(self, schema, table_name):
"""新接口:从 info_schema 拉字段元数据(含 column_comment)
queries.py 在打开连接后会调一次,把 column_comment 写进 field_list
"""
_FakeDBConnection.list_columns_called_with = (schema, table_name)
return list(_FakeDBConnection.column_meta)
@pytest.fixture()
def client(tmp_path, monkeypatch):
......@@ -97,6 +111,9 @@ def client(tmp_path, monkeypatch):
db.add(Rule(field_id=11, desc="手机号 11 位", regex=r"^1\d{10}$", ord=0))
# person_name:无规则,但默认展示 → 应被 SELECT 出来当上下文
db.add(Field(id=12, task_id=1, field_key="person_name", show_default=1, ord=2))
# nickname:无规则 + show_default=false → 旧逻辑不会 SELECT 这列,结果表全 NULL
# 新逻辑(2026-08-21)SELECT 全部已配置字段,这列也要被拉
db.add(Field(id=13, task_id=1, field_key="nickname", show_default=0, ord=3))
db.commit()
monkeypatch.setattr(queries_mod, "DBConnection", _FakeDBConnection)
......@@ -206,3 +223,61 @@ def test_row_index_points_at_position_in_full_scan(client):
body = _run(client, max_rows=1)
# 李四是全表第 2 行(0-based = 1)
assert body["rows"][0]["__row_index"] == 1
# ── 2026-08-21 追加:结果表非规则列显示实际值 / 表头字段注释 ─────────
def test_show_default_false_no_rules_field_is_selected(client):
"""没规则 + show_default=false 的字段也要进 SELECT(2026-08-21 行为变更)。
早期版本限制「rules ∪ show_default」,结果是结果表表头有这列、值全 NULL。
现在改成:SELECT 全部已配置字段;规则只决定哪些列要标红。
"""
_run(client)
sql = _FakeDBConnection.last_sql
assert "`nickname`" in sql, (
f"show_default=false + 无规则的字段也要 SELECT,否则结果表这列全 NULL:{sql}"
)
def test_field_comment_returned_for_header_tooltip(client):
"""field_list 每项应带回 field_comment(来自数据源 COLUMN_COMMENT),
供前端表头 tooltip 展示字段注释。
"""
_FakeDBConnection.list_columns_called_with = None # 复位
body = _run(client)
# 1) 后端确实打开连接去拉过字段注释
assert _FakeDBConnection.list_columns_called_with == ("d", "t_user_info"), (
"应打开连接后从 info_schema 拉一次字段注释"
)
# 2) field_list 里每个有注释的字段都填上了 field_comment
fl = {f["field_key"]: f for f in body["field_list"]}
assert fl["id_card"]["field_comment"] == "身份证号"
assert fl["phone"]["field_comment"] == "手机号"
assert fl["person_name"]["field_comment"] == "姓名"
assert fl["nickname"]["field_comment"] == "昵称"
def test_rule_list_included_for_tooltip(client):
"""field_list 每项应带回 rule_list(rule 的 desc + regex),
供前端「⚠ N规则」badge hover 出规则明细。
"""
body = _run(client)
fl = {f["field_key"]: f for f in body["field_list"]}
# id_card 两条规则都要列出来
id_card_rules = fl["id_card"]["rule_list"]
assert len(id_card_rules) == 2
descs = [r["desc"] for r in id_card_rules]
assert "必须 18 位" in descs
assert "必须全是数字或以 X 结尾" in descs
# phone 一条规则
assert len(fl["phone"]["rule_list"]) == 1
assert fl["phone"]["rule_list"][0]["desc"] == "手机号 11 位"
# 无规则的字段:rule_list 是空 list(不是 None,前端可以放心 .map)
assert fl["person_name"]["rule_list"] == []
assert fl["nickname"]["rule_list"] == []
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment