From b0556e36d6dc89ed5f646a8c29df735803b892ff Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Tue, 20 Oct 2026 10:40:57 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A6=86=E7=9B=96=E6=A3=80=E6=9F=A5=E7=8E=B0?= =?UTF-8?q?=E6=9F=A5=E5=88=B0=E7=9A=84=E8=A1=A8=E7=9A=84=E8=8C=83=E5=9B=B4?= =?UTF-8?q?=E5=9C=A8=E8=BF=9B=E7=A8=8B=E9=87=8C=E8=AE=B0=205=20=E5=88=86?= =?UTF-8?q?=E9=92=9F=EF=BC=88=E6=8C=89=E6=95=B0=E6=8D=AE=E6=BA=90=E3=80=81?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E8=A1=A8=E6=A0=BC=E7=9A=84=E5=BF=AB=E7=85=A7?= =?UTF-8?q?=E3=80=81=E8=A1=A8=E3=80=81=E5=88=97=E8=AE=A4=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E4=B8=80=E5=BC=A0=E5=9B=BE=E9=87=8C=E5=87=A0=E4=B8=AA=E5=8F=96?= =?UTF-8?q?=E6=95=B0=E8=8A=82=E7=82=B9=E3=80=81=E5=8A=A9=E6=89=8B=E8=87=AA?= =?UTF-8?q?=E6=9F=A5=E5=87=A0=E8=BD=AE=E6=9F=A5=E5=90=8C=E4=B8=80=E5=BC=A0?= =?UTF-8?q?=E8=A1=A8=EF=BC=8C=E4=B8=8D=E5=86=8D=E6=AF=8F=E6=AC=A1=E5=85=A8?= =?UTF-8?q?=E8=A1=A8=20MIN=20/=20MAX=EF=BC=9B=E6=9F=A5=E4=B8=8D=E6=88=90?= =?UTF-8?q?=E7=9A=84=E3=80=81=E9=80=80=E5=9B=9E=E5=89=96=E6=9E=90=E7=9A=84?= =?UTF-8?q?=E4=B8=8D=E8=AE=B0=EF=BC=8C=E8=BF=87=E6=9C=9F=E4=BA=86=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E7=8E=B0=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/data/coverage.py | 13 +++++++++++++ backend/tests/test_query_coverage.py | 25 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/backend/app/data/coverage.py b/backend/app/data/coverage.py index eeb147b6..6e2960d9 100644 --- a/backend/app/data/coverage.py +++ b/backend/app/data/coverage.py @@ -48,6 +48,10 @@ MAX_BOUNDS = 24 MAX_SQL_CHARS = 20_000 #: 一条 no_data 里最多列几个结果列(计数照实) MAX_COLUMNS = 12 +#: 现查到的表的范围在进程里记多久(秒):同一张表一会儿内查好几次(一张图里几个取数节点、助手自查几轮)不再全表 MIN / +#: MAX。滚动表一天才换一次数据,几分钟的旧不影响判断;查不成的、退回剖析的不记 +LIVE_TTL_S = 300.0 +_LIVE: dict[tuple[Any, ...], tuple[float, Any]] = {} #: 给人和模型看的说法 NO_DATA_MESSAGE = "{columns}查的是 {window},但 {table} 的 {column} {data}:这些 0 不代表这段时间真的是 0,是这段时间表里没有数据{basis}" @@ -392,6 +396,11 @@ async def _range(source: Any, flt: _Filter, read: str | None, cache: Cache, fact key = (str(getattr(source, "name", "")), name_key(flt.table_name), name_key(flt.column_name)) if key in cache.ranges: return cache.ranges[key] + live = (getattr(source, "id", None), getattr(source, "snapshot_id", None), *key) + held = _LIVE.get(live) + if held is not None and held[0] > time.monotonic(): + cache.ranges[key] = held[1] + return held[1] table = flt.table.copy() table.set("alias", None) ident = flt.column.this.copy() if isinstance(flt.column.this, exp.Identifier) \ @@ -413,6 +422,10 @@ async def _range(source: Any, flt: _Filter, read: str | None, cache: Cache, fact elif facts is not None: out = await _fallback(flt, facts) cache.ranges[key] = out + if out is not None and out.basis == "live": + if len(_LIVE) > 512: + _LIVE.clear() + _LIVE[live] = (time.monotonic() + LIVE_TTL_S, out) return out diff --git a/backend/tests/test_query_coverage.py b/backend/tests/test_query_coverage.py index b8300d4f..c8ce0941 100644 --- a/backend/tests/test_query_coverage.py +++ b/backend/tests/test_query_coverage.py @@ -232,3 +232,28 @@ async def test_the_weekly_report_shape_on_mysql(monkeypatch): # 表的范围同一张表、同一列只查一次;按运行当天现算的界一条 SELECT 算出来 assert sum("MIN(" in t for t in seen) == 1 assert all(t.startswith("SELECT ") for t in seen) + + +async def test_a_live_range_is_kept_for_a_while(monkeypatch): + """现查到的范围在进程里记几分钟:一张图里几个取数节点、助手自查几轮查同一张表,不再每次全表 MIN / MAX。""" + from types import SimpleNamespace + + seen: list[str] = [] + + async def probe(source, text): + seen.append(text) + return ["20200101", "20200101"], ["text", "text"] + + monkeypatch.setattr(coverage, "_probe", probe) + monkeypatch.setattr(coverage, "_LIVE", {}) + source = SimpleNamespace(name=f"plant_{uuid.uuid4().hex[:6]}", kind="sqlite", schema_cache=None, id="s1") + payload = {"columns": ["n"], "rows": [[0]], "row_count": 1, "column_types": {"n": "number"}} + sql = "SELECT COUNT(*) AS n FROM gate_events WHERE event_date >= '20261012'" + for _ in range(2): + assert len(await coverage.result_gaps(source, sql, payload)) == 1 + assert sum("MIN(" in t for t in seen) == 1 + monkeypatch.setattr(coverage, "LIVE_TTL_S", 0.0) + coverage._LIVE.clear() + await coverage.result_gaps(source, sql, payload) + await coverage.result_gaps(source, sql, payload) + assert sum("MIN(" in t for t in seen) == 3, "过期了就重新现查"