mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:31:58 +00:00
「需要剖析」多认一种:上次剖析之后多出了码值、日期检查(new_checks);停止排队任务时不再读过期的 job(起草、剖析两处)
- 以前只认从没剖析过、上次没做完、又冒出了待核实关系三种:上次剖析时没有可剖析的项(指定了表、记为跳过),后来有人把一列 记成状态类,或者日期列变了,「需要剖析的表」永远漏掉它。现在按当前要做的检查对上次剖析的检查项(kind、target),多出了 码值、日期检查就要剖析,原因 new_checks;上次是空表而跳过的仍不算(不发查询不知道有没有数据),界面和 409 的说法写明。 due_reason 改收 plan_checks 列出的检查,没人用的 profile_checks 去掉。 - 停止一个排队中的任务时,排队那一步没停成(刚被后台认领)要回滚,回滚让读到的 job 属性全部过期,再读 job.id 就在异步会话里 同步加载(MissingGreenlet),接口 500。起草的 cancel 在 main 上就是这样写的,剖析照抄了;两处都先把 id、状态取出来。 新测试在旧代码上复现了这个 500。
This commit is contained in:
@@ -175,8 +175,9 @@ def _summary_row(name: str, meta: dict[str, Any] | None, entry: catalog.CatalogE
|
||||
def _profiling(name: str, meta: dict[str, Any] | None, entry: catalog.CatalogEntry | None, *,
|
||||
tables: dict[str, Any], masked: set[str], last: dict[str, Any] | None) -> dict[str, Any]:
|
||||
notes = entry.notes if entry else {}
|
||||
checks = catalog_profile.profile_checks(name, meta, notes, tables=tables, masked=masked)
|
||||
reason = catalog_profile_jobs.due_reason(meta, notes, last, checks)
|
||||
planned = catalog_profile.plan_checks(name, meta, notes, tables=tables, masked=masked)
|
||||
checks = sum(1 for c in planned if c.skip is None)
|
||||
reason = catalog_profile_jobs.due_reason(meta, notes, last, planned)
|
||||
return {"last_profile": catalog_profile_jobs.public_last(last), "profile_due": reason is not None,
|
||||
"profile_due_reason": reason, "profile_checks": checks}
|
||||
|
||||
|
||||
@@ -491,17 +491,20 @@ async def retry_tables(session: AsyncSession, job: CatalogDraftJob) -> list[str]
|
||||
async def cancel(session: AsyncSession, job: CatalogDraftJob) -> None:
|
||||
"""停止一个任务。排队的直接 cancelled(在队里等着的后台执行跟着停掉、出队);进行中的交给 manager 停(停掉模型调用、
|
||||
没写完的表记 cancelled)。已经结束的抛 DraftFinished。"""
|
||||
if job.status in FINISHED:
|
||||
# 先取出来:排队的没停成(刚被后台认领)时 cancel_queued 会回滚,回滚让 job 的属性全部过期,再读 job.id 就要在
|
||||
# 异步会话里同步地重新加载(MissingGreenlet)
|
||||
job_id, status = job.id, job.status
|
||||
if status in FINISHED:
|
||||
raise DraftFinished("这次起草已经结束了,不需要停止")
|
||||
if job.status == "queued":
|
||||
if await runtime.cancel_queued(session, CatalogDraftJob, CatalogDraftTask, job.id):
|
||||
await manager.stop(job.id, "user", grace=STOP_GRACE_S)
|
||||
if status == "queued":
|
||||
if await runtime.cancel_queued(session, CatalogDraftJob, CatalogDraftTask, job_id):
|
||||
await manager.stop(job_id, "user", grace=STOP_GRACE_S)
|
||||
return
|
||||
# 刚被后台认领、开始了:按进行中的停
|
||||
if await manager.stop(job.id, "user", grace=STOP_GRACE_S):
|
||||
if await manager.stop(job_id, "user", grace=STOP_GRACE_S):
|
||||
return
|
||||
# 库里说在进行,这个进程里却没有在跑它(记录对不上):直接记停止
|
||||
await _close(session, job.id, "cancelled", None, "cancelled", None)
|
||||
await _close(session, job_id, "cancelled", None, "cancelled", None)
|
||||
|
||||
|
||||
async def _close(session: AsyncSession, job_id: str, job_status: str, job_error: str | None, task_status: str,
|
||||
|
||||
@@ -1476,12 +1476,6 @@ def plan_checks(table: str, meta: dict[str, Any] | None, notes: dict[str, Any] |
|
||||
return checks
|
||||
|
||||
|
||||
def profile_checks(table: str, meta: dict[str, Any] | None, notes: dict[str, Any] | None, *, tables: dict[str, Any],
|
||||
masked: set[str]) -> int:
|
||||
"""这张表要做几项检查(按规则不查的不算):0 表示没有可剖析的项;视图、表结构里没有的表是 0。"""
|
||||
return sum(1 for c in plan_checks(table, meta, notes, tables=tables, masked=masked) if c.skip is None)
|
||||
|
||||
|
||||
def has_pending_relations(notes: dict[str, Any] | None) -> bool:
|
||||
"""这张表还有没有待核实的关系:要核对的关系里有还是推断(proposed)的。挑表时这样的表排在前面。"""
|
||||
return any(r.get("status") == "proposed" for r in _relation_candidates(notes))
|
||||
@@ -1867,5 +1861,5 @@ __all__ = ["CODES_MAX", "EMPTY_ERROR", "FILTER_CHECKED", "FILTER_NOT_ENABLED", "
|
||||
"QueryFailed", "SourceRunner", "SqlDialect", "TableBudget", "TableHooks", "TableResult", "TableSize",
|
||||
"UNFINISHED_REASONS", "VIEW_ERROR", "check_valid_filter", "ensure_enabled", "estimate_size", "excluded_share",
|
||||
"has_pending_relations", "make_context", "masked_of", "migrate_settings", "pending_relation_paths",
|
||||
"plan_checks", "profile_checks", "profile_settings", "profile_settings_problem", "profile_settings_rejection",
|
||||
"plan_checks", "profile_settings", "profile_settings_problem", "profile_settings_rejection",
|
||||
"profile_table", "run_single_query", "sql_literal", "summarize", "without_legacy"]
|
||||
|
||||
@@ -319,7 +319,8 @@ def _ranked(source_id: str, *columns: Any) -> Any:
|
||||
|
||||
async def last_profiles(session: AsyncSession, source_id: str) -> dict[str, dict[str, Any]]:
|
||||
"""每张表的「上次剖析」(见 _ranked 的 shown):{表名: CatalogLastProfile 加上内部用的 paths(那一项检查项的路径,判断
|
||||
有没有新的待核实关系用)、queued(最近一项还排着或在做:这张表已经在进行中的任务里了)}。在做的就是进行中的状态。
|
||||
有没有新的待核实关系用)、keys(那一项检查项的 (kind, target),判断有没有新的码值、日期检查用)、queued(最近一项还排着
|
||||
或在做:这张表已经在进行中的任务里了)}。在做的就是进行中的状态。
|
||||
每张表取哪一项在库里挑好。"""
|
||||
inner = _ranked(source_id, CatalogProfileTask.table_name, CatalogProfileTask.job_id,
|
||||
CatalogProfileTask.seq, CatalogProfileTask.status, CatalogProfileTask.started_at,
|
||||
@@ -339,17 +340,18 @@ async def last_profiles(session: AsyncSession, source_id: str) -> dict[str, dict
|
||||
"summary": {**_empty_summary(), **(row["summary"] or {})}, "skipped_count": row["skipped_count"] or 0,
|
||||
"unfinished": row["unfinished"] or 0, "error": row["error"], "row_estimate": row["row_estimate"],
|
||||
"paths": {c.get("path") for c in row["checks"] or [] if isinstance(c, dict) and c.get("path")},
|
||||
"keys": {(c.get("kind"), c.get("target")) for c in row["checks"] or [] if isinstance(c, dict)},
|
||||
"queued": row["table_name"] in queued,
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
#: last_profiles 里只给内部用的键
|
||||
_INTERNAL = ("paths", "queued")
|
||||
_INTERNAL = ("paths", "keys", "queued")
|
||||
|
||||
|
||||
def public_last(last: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
"""接口里的 CatalogLastProfile:去掉内部用的 paths、queued。"""
|
||||
"""接口里的 CatalogLastProfile:去掉内部用的 paths、keys、queued。"""
|
||||
return None if last is None else {k: v for k, v in last.items() if k not in _INTERNAL}
|
||||
|
||||
|
||||
@@ -365,14 +367,18 @@ async def last_detail(session: AsyncSession, source_id: str, table: str) -> dict
|
||||
|
||||
|
||||
def due_reason(meta: dict[str, Any] | None, notes: dict[str, Any] | None, last: dict[str, Any] | None,
|
||||
checks: int) -> str | None:
|
||||
planned: list[catalog_profile.PlannedCheck]) -> str | None:
|
||||
"""这张表要不要剖析、为什么:never 从没剖析过;unfinished 上次剖析是 partial / failed / cancelled;new_relations
|
||||
目录里有待核实的关系不在上次剖析的检查项里。表结构里没有、视图、没有可剖析的项(checks 为 0)、已经在进行中的任务里
|
||||
(排着或在做)的不要。
|
||||
目录里有待核实的关系不在上次剖析的检查项里;new_checks 上次剖析之后多出了要做的码值、日期检查(上次剖析时没有可剖析的
|
||||
项、后来起草或人工把某一列记成了状态类,或者日期列变了)。表结构里没有、视图、没有可剖析的项、已经在进行中的任务里
|
||||
(排着或在做)的不要。planned 是按当前目录和表结构列出的检查(catalog_profile.plan_checks)。
|
||||
|
||||
「上次剖析」和表清单显示的是同一项(last_profiles,真正开始过的最近一项):上次真正剖析完整了,后来某个任务没轮到它
|
||||
就停了,不算没做完——不然表清单一边显示「上次剖析:完成」,一边说「上次剖析没做完」。一次都没开始过的,按最近一项算。"""
|
||||
if not isinstance(meta, dict) or meta.get("is_view") or checks <= 0:
|
||||
就停了,不算没做完——不然表清单一边显示「上次剖析:完成」,一边说「上次剖析没做完」。一次都没开始过的,按最近一项算。
|
||||
|
||||
上次是空表而跳过的不算要剖析:表里有没有数据,不发查询就不知道;空表等有了数据,选「全部表」或单独剖析它。"""
|
||||
runnable = [c for c in planned if c.skip is None]
|
||||
if not isinstance(meta, dict) or meta.get("is_view") or not runnable:
|
||||
return None
|
||||
if last is None:
|
||||
return "never"
|
||||
@@ -382,6 +388,9 @@ def due_reason(meta: dict[str, Any] | None, notes: dict[str, Any] | None, last:
|
||||
return "unfinished"
|
||||
if catalog_profile.pending_relation_paths(notes) - set(last.get("paths") or ()):
|
||||
return "new_relations"
|
||||
seen = last.get("keys") or set()
|
||||
if any(c.kind != "relation" and (c.kind, c.target) not in seen for c in runnable):
|
||||
return "new_checks"
|
||||
return None
|
||||
|
||||
|
||||
@@ -429,8 +438,8 @@ async def ensure_free(session: AsyncSession, row: Any, tables: list[str] | None
|
||||
|
||||
|
||||
#: 挑出 0 张表时的说法
|
||||
NOTHING_NEEDED = ("没有需要剖析的表:能剖析的表都剖析过了,上次剖析也都做完了,之后没有新的待核实关系。"
|
||||
"要重新剖析,请选「全部表」或指定要剖析的表")
|
||||
NOTHING_NEEDED = ("没有需要剖析的表:能剖析的表都剖析过了,上次剖析也都做完了,之后没有新的待核实关系,也没有新的码值、"
|
||||
"日期列(上次是空表的不算)。要重新剖析,请选「全部表」或指定要剖析的表")
|
||||
NOTHING_AT_ALL = ("没有可以剖析的表:表结构里的表都是视图,或者没有可剖析的项(要核对的关联关系、像码值的列、日期列都没有,"
|
||||
"或者都被遮罩了)。请先起草数据目录,或指定要剖析的表")
|
||||
|
||||
@@ -487,10 +496,9 @@ async def _pick(session: AsyncSession, row: Any, source: Any, *, tables: list[st
|
||||
if not isinstance(meta, dict) or meta.get("is_view"):
|
||||
continue
|
||||
checks = plan(name)
|
||||
runnable = sum(1 for c in checks if c.skip is None)
|
||||
if runnable == 0:
|
||||
if not any(c.skip is None for c in checks):
|
||||
continue
|
||||
if scope == "needed" and due_reason(meta, notes(name), last.get(name), runnable) is None:
|
||||
if scope == "needed" and due_reason(meta, notes(name), last.get(name), checks) is None:
|
||||
continue
|
||||
candidates.append((name, checks))
|
||||
if not candidates:
|
||||
@@ -627,22 +635,25 @@ async def _close(session: AsyncSession, job_id: str, job_status: str, job_error:
|
||||
async def cancel(session: AsyncSession, job: CatalogProfileJob) -> None:
|
||||
"""停止一个剖析任务。排队的直接 cancelled(在队里等着的后台执行跟着停掉、出队);进行中的交给 manager 停:正在等的那条
|
||||
查询不再等(库那边按单条时限截止),那张表做完的项照常写进目录,后面的表 cancelled。已经结束的抛 ProfileFinished。"""
|
||||
if job.status in FINISHED:
|
||||
# 先取出来:排队的没停成(刚被后台认领)时要回滚,回滚让 job 的属性全部过期,再读 job.id 就要在异步会话里同步地
|
||||
# 重新加载(MissingGreenlet)
|
||||
job_id, status = job.id, job.status
|
||||
if status in FINISHED:
|
||||
raise ProfileFinished("这次剖析已经结束了,不需要停止")
|
||||
if job.status == "queued":
|
||||
if status == "queued":
|
||||
claimed = await session.execute(
|
||||
update(CatalogProfileJob).where(CatalogProfileJob.id == job.id, CatalogProfileJob.status == "queued")
|
||||
update(CatalogProfileJob).where(CatalogProfileJob.id == job_id, CatalogProfileJob.status == "queued")
|
||||
.values(status="cancelled", finished_at=utcnow()).execution_options(synchronize_session=False))
|
||||
if claimed.rowcount == 1:
|
||||
await _close_tasks(session, job.id, "cancelled", None, why=STOPPED_ERROR)
|
||||
await _close_tasks(session, job_id, "cancelled", None, why=STOPPED_ERROR)
|
||||
await session.commit()
|
||||
await manager.stop(job.id, "user", grace=STOP_GRACE_S)
|
||||
await manager.stop(job_id, "user", grace=STOP_GRACE_S)
|
||||
return
|
||||
await session.rollback() # 刚被后台认领、开始了:按进行中的停
|
||||
if await manager.stop(job.id, "user", grace=STOP_GRACE_S):
|
||||
if await manager.stop(job_id, "user", grace=STOP_GRACE_S):
|
||||
return
|
||||
# 库里说在进行,这个进程里却没有在跑它(记录对不上):直接记停止
|
||||
await _close(session, job.id, "cancelled", None, "cancelled", None)
|
||||
await _close(session, job_id, "cancelled", None, "cancelled", None)
|
||||
|
||||
|
||||
async def startup() -> int:
|
||||
|
||||
@@ -597,6 +597,41 @@ async def test_profile_due_never_unfinished_and_new_relations(client, make_sourc
|
||||
await catalog_profile_jobs.manager.wait(job["id"], timeout=30)
|
||||
|
||||
|
||||
async def test_profile_due_new_checks_after_a_column_becomes_a_status(client, make_source, scenic_db):
|
||||
"""上次剖析时没有可剖析的项(指定了表,记为跳过),后来有人把一列记成状态类:多出了码值检查,又要剖析了,原因是
|
||||
new_checks;剖析过(这一次估过行数发现是空表)之后不再要。"""
|
||||
sid = await make_source(scenic_db, options=_on(), draft=["suppliers"])
|
||||
await _profile(client, sid, ["suppliers"])
|
||||
rows = await _rows(client, sid)
|
||||
assert rows["suppliers"]["last_profile"]["status"] == "skipped"
|
||||
assert (rows["suppliers"]["profile_checks"], rows["suppliers"]["profile_due"]) == (0, False)
|
||||
|
||||
async with SessionLocal() as session:
|
||||
entry = await catalog.read_entry(session, sid, "suppliers") # 起草没推断出什么时还没有目录
|
||||
notes = dict(entry.notes) if entry else {}
|
||||
columns = dict(notes.get("columns") or {})
|
||||
columns["name"] = {**(columns.get("name") or {}),
|
||||
"measure": {"value": "status", "source": "human", "status": "confirmed"}}
|
||||
notes["columns"] = columns
|
||||
await catalog.write_entry(session, sid, "suppliers", notes, if_version=entry.version if entry else 0,
|
||||
actor=None)
|
||||
rows = await _rows(client, sid)
|
||||
assert rows["suppliers"]["profile_checks"] == 1
|
||||
assert (rows["suppliers"]["profile_due"], rows["suppliers"]["profile_due_reason"]) == (True, "new_checks")
|
||||
assert "keys" not in rows["suppliers"]["last_profile"]
|
||||
job = (await _start(client, sid)).json()["job"]
|
||||
assert "suppliers" in [t["table_name"] for t in job["tasks"]], "「需要剖析的表」里有它"
|
||||
await client.post(f"{_url(sid)}/{job['id']}/cancel")
|
||||
await catalog_profile_jobs.manager.wait(job["id"], timeout=30)
|
||||
# 景区库的 suppliers 是空表:这次剖析估过行数就跳过了。空表不算要剖析(不发查询不知道有没有数据),等有了数据选
|
||||
# 「全部表」或单独剖析它
|
||||
await _profile(client, sid, ["suppliers"])
|
||||
rows = await _rows(client, sid)
|
||||
last = rows["suppliers"]["last_profile"]
|
||||
assert (last["status"], last["error"]) == ("skipped", "空表,没有可剖析的数据")
|
||||
assert (rows["suppliers"]["profile_due"], rows["suppliers"]["profile_due_reason"]) == (False, None)
|
||||
|
||||
|
||||
async def test_nothing_to_profile_is_409(client, make_source, tmp_path):
|
||||
path = _db(tmp_path / "plain.db", "CREATE TABLE suppliers (id INTEGER PRIMARY KEY, name TEXT);",
|
||||
{"suppliers": [(1, "甲供应商")]})
|
||||
@@ -763,3 +798,43 @@ async def test_retention_keeps_recent_jobs_and_each_tables_last_profile(client,
|
||||
left = set((await session.execute(select(CatalogProfileJob.id)
|
||||
.where(CatalogProfileJob.source_id == sid))).scalars())
|
||||
assert ids[0] in left and job["id"] in left and ids[3] not in left
|
||||
|
||||
|
||||
async def test_cancel_a_job_claimed_between_load_and_cancel(client, make_source, scenic_db, monkeypatch):
|
||||
"""停止时读到的是排队中,可在停之前后台刚认领了它:排队的那一步没停成、回滚,接着按进行中的停。回滚让读到的 job
|
||||
属性全部过期,以前再读 job.id 会在异步会话里同步加载(MissingGreenlet),接口 500。起草任务同一个写法,一起钉住。"""
|
||||
sid = await make_source(scenic_db, options=_on(), draft=["visits"])
|
||||
stopped: list[str] = []
|
||||
|
||||
async def fake_stop(job_id, reason, *, grace=0):
|
||||
stopped.append(job_id)
|
||||
return True
|
||||
|
||||
for module, model in ((catalog_profile_jobs, CatalogProfileJob), (catalog_jobs, None)):
|
||||
monkeypatch.setattr(module.manager, "start", lambda job_id: None) # 不在后台跑:认领由下面手工做
|
||||
monkeypatch.setattr(module.manager, "stop", fake_stop)
|
||||
job_id = (await _start(client, sid, ["visits"])).json()["job"]["id"]
|
||||
async with SessionLocal() as session:
|
||||
job = await session.get(CatalogProfileJob, job_id)
|
||||
assert job.status == "queued"
|
||||
async with SessionLocal() as other: # 后台刚认领
|
||||
await other.execute(catalog_profile_jobs.update(CatalogProfileJob).where(CatalogProfileJob.id == job_id)
|
||||
.values(status="running"))
|
||||
await other.commit()
|
||||
await catalog_profile_jobs.cancel(session, job)
|
||||
assert stopped == [job_id]
|
||||
|
||||
from app.db.models import CatalogDraftJob
|
||||
|
||||
r = await client.post(f"/api/datasources/{sid}/catalog/draft-jobs", json={"tables": ["visits"], "use_model": False})
|
||||
assert r.status_code == 202, r.text
|
||||
draft_id = r.json()["job"]["id"]
|
||||
async with SessionLocal() as session:
|
||||
job = await session.get(CatalogDraftJob, draft_id)
|
||||
assert job.status == "queued"
|
||||
async with SessionLocal() as other:
|
||||
await other.execute(catalog_profile_jobs.update(CatalogDraftJob).where(CatalogDraftJob.id == draft_id)
|
||||
.values(status="running"))
|
||||
await other.commit()
|
||||
await catalog_jobs.cancel(session, job)
|
||||
assert stopped == [job_id, draft_id]
|
||||
|
||||
@@ -2850,8 +2850,8 @@ export const PROFILE_TEXT = {
|
||||
scopeSelected: (n: number) => `已选的 ${formatNumber(n)} 张表`,
|
||||
scopeFiltered: (n: number) => `当前筛选结果中的 ${formatNumber(n)} 张表`,
|
||||
scopeNeeded: (n: number) => `需要剖析的 ${formatNumber(n)} 张表`,
|
||||
scopeNeededHint: '从没剖析过、上次没做完,或剖析之后又出现了待核实关联关系的表',
|
||||
scopeNeededNone: '没有需要剖析的表:都剖析过、上次都做完了,也没有新的待核实关联关系',
|
||||
scopeNeededHint: '从没剖析过、上次没做完,或剖析之后又出现了待核实关联关系、要取码值或日期范围的列的表。上次是空表的不算,有了数据请选全部表或单独剖析它',
|
||||
scopeNeededNone: '没有需要剖析的表:都剖析过、上次都做完了,也没有新的待核实关联关系和新的码值、日期列',
|
||||
scopeAll: (n: number) => `全部 ${formatNumber(n)} 张表`,
|
||||
scopeAllHint: '有可剖析项的表,不含视图',
|
||||
/** 已选、筛选结果里有剖析不了的表:不交给服务端,写明有几张 */
|
||||
@@ -3073,9 +3073,10 @@ export const PROFILE_TEXT = {
|
||||
never: '从没剖析过',
|
||||
unfinished: '上次剖析没做完',
|
||||
new_relations: '剖析之后目录里出现了新的待核实关联关系',
|
||||
new_checks: '剖析之后多出了要取码值或日期范围的列',
|
||||
} as Record<string, string>,
|
||||
dueFilter: '需要剖析',
|
||||
dueFilterHint: '从没剖析过、上次没做完,或剖析之后又出现了待核实关联关系的表',
|
||||
dueFilterHint: '从没剖析过、上次没做完,或剖析之后又出现了待核实关联关系、要取码值或日期范围的列的表',
|
||||
|
||||
// ---- 表详情的「数据剖析」一节
|
||||
detailHint: '对业务库发只读查询得到的观测:行数、日期范围、核实了什么、跳过了什么',
|
||||
|
||||
@@ -2845,7 +2845,8 @@ export interface CatalogTableRow {
|
||||
profile_due?: boolean
|
||||
/**
|
||||
* 为什么需要剖析:never 从没剖析过;unfinished 最近一次剖析没做完(部分完成、失败、已停止);new_relations 目录里有待核实的
|
||||
* 关系不在最近一次剖析的检查项里。不需要时为 null
|
||||
* 关系不在最近一次剖析的检查项里;new_checks 最近一次剖析之后多出了码值、日期检查(某一列被记成了状态类、日期列变了)。
|
||||
* 不需要时为 null
|
||||
*/
|
||||
profile_due_reason?: CatalogProfileDueReason | null
|
||||
/** 按当前目录和表结构这张表要做几项检查;0 表示没有可剖析的项(视图为 0) */
|
||||
@@ -3350,7 +3351,7 @@ export interface CatalogProfileSettings {
|
||||
}
|
||||
|
||||
/** 为什么需要剖析(GET /catalog 每行的 profile_due_reason) */
|
||||
export type CatalogProfileDueReason = 'never' | 'unfinished' | 'new_relations'
|
||||
export type CatalogProfileDueReason = 'never' | 'unfinished' | 'new_relations' | 'new_checks'
|
||||
|
||||
/** 剖析的范围:selected 指定的表;needed 需要剖析的表;all 全部可剖析的表(不含视图) */
|
||||
export type CatalogProfileScope = 'selected' | 'needed' | 'all'
|
||||
|
||||
@@ -658,7 +658,7 @@ async function openDialog(page) {
|
||||
await box(page).waitFor()
|
||||
}
|
||||
// 界面不该露的机读码:状态、跳过原因、检查类别、查询结果的英文值
|
||||
const CODE_RE = /\b(?:queued|running|succeeded|partial|failed|cancelled|interrupted|pending|skipped|budget|deadline|timeout|rejected|masked|too_large|slow|nothing|row_estimate|relation|codes|verified|proposed|unfinished|new_relations|never)\b/
|
||||
const CODE_RE = /\b(?:queued|running|succeeded|partial|failed|cancelled|interrupted|pending|skipped|budget|deadline|timeout|rejected|masked|too_large|slow|nothing|row_estimate|relation|codes|verified|proposed|unfinished|new_relations|new_checks|never)\b/
|
||||
async function cleanCopy(page, where) {
|
||||
let t = await box(page).innerText().catch(() => '')
|
||||
// 表名、列名、SQL 是库里的标识,照原样显示,先去掉再查
|
||||
|
||||
Reference in New Issue
Block a user