Files
AgentLab/backend/tests/test_dual_check_fixture.py
ThearchyHelios caeab0fad6 SQL 差异说明认得同一列上的条件改写:说清「这次对 X 的条件是 …,口径是 …」
验收:口径 status = 'paid'、这次 status IN ('paid', 'refunded'),explain_sql_diff 只给兜底「两条 SQL 的写法不同,
未能归纳成具体差异」。原因是差异说明只说四类,同一列上两边各写一套条件(等号对 IN、取值集合不同、范围端点不同)
不在其中:_extra_filters 看到另一边也用了这一列就放弃。

改法:_Explainer 加 _rewrites,排在 _times 之后、_extra_filters 之前。同一张实表的同一列两边 WHERE 里都有条件、
至少一边有没配上的,就说「A 对 X 的条件是 …,B 是 …」;两边都只是取值集合、又有共同的取值时补「(这次多了
'refunded')」。两边都是时间条件时 aspect 是 time_range(FacetDiff 说明里早有这个方面、engine/compare 也按它
排序),否则是 filter 且 a、b 两边都有——dual_check_facts 对两边都有的 filter 本来就说「两边各有不同的筛选条件」
不写目录,ledger/stats、评测页的方面叫法都已认得这两个名字。

为了不说出不存在的不同,只在这些条件都满足时说:两边用到这一列的条件都只涉及这一列、比的是那一列本身;
按取值逐段算过两边留下的取值确实不同(_Range.compare 加 never_null:表结构里确定不为空的列不拿空值比;
x >= 100 AND x >= 50 对 x >= 100 这类冗余写法算出来一样,不说);不在 LEFT JOIN 可空一侧;WHERE 之外(ON、
HAVING、CASE、聚合参数)都没用到这一列(SUM(CASE WHEN status = 'paid' …) 配上 IN 可能和只筛 paid 一样);不是
认不准的时间窗口;没换表;有认不出列的条件时整段不说。

只改说明、不碰等价判定:说明只在已经判了不等价之后才跑。新测试把 _rewrites 换成什么都不做,逐对(语料全部
加几对改写、两种先后、给不给表结构)比等价与否和判定的级别,一字不差;IN 只有一个值、IN 列表去重排序照样判等价。
语料执行守门(意思一样的对不许说出具体不同)全过。

测试:test_sql_semantics 新增三组(各方言说两边、可能不影响结果的不说、等价判定不变);原来断言这两类只给
兜底的用例改成「算出来的值上换了值」(LOWER(status))仍只给兜底;test_sql_text_diff、test_dual_check_facts 跟上
(同一列上的时间范围现在是 time_range,照样不写目录;换了筛选的值不写目录)。去掉 _rewrites 的调用回跑,新测试
挂 5 条。

双盲复算的前端夹具 dual_check.json 里「说不清的」那个变体原来就是同一列上的时间范围,现在说得出了:换成套着
DATE() 的时间范围(认不出取值范围,仍只给兜底),test_dual_check_fixture 重新生成,check-verify 对应的说法和
高亮那一行的断言跟上。
2026-10-17 08:17:12 +08:00

392 lines
24 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""双盲复算审批卡的前端夹具(frontend/src/dev/fixtures/dual_check.json)要和服务端的真实输出对得上。
审批卡上「对照两条 SQL」的高亮、「只差一个方面会记入数据目录」的提示,都是照 explain_sql_diff 的输出画的。它的
text_diff 是规范化之后的行(sqlglot pretty:列名带表名、每个条件一行),和原文的行对不上;夹具要是照原文手写,
check-verify 在夹具上能过,真实数据上却一处都不高亮(2026-10 复核时发现)。所以这里按夹具里的两条 SQL 重新算一遍
差异说明,逐个指标、逐个变体和夹具里写的比。sql_semantics 改了说法或规范形式,这里先变红,按下面的办法重新生成。
审批 payload 的摘要照双盲复算节点的写法核对:diff 是前 3 条差异的原话,fallback 是「除了兜底和解析失败,一条具体
差异都没有」,delta_pct 是 (盲算值 − 原流程值) ÷ |原流程值| × 100 保留两位。夹具的数据源当 SQLite 比(方言 sqlite),
两边的叫法是「原流程」「盲算」,同节点调用时一样。
**变体。** variants 换掉「销售额」的那一对 SQL,挑的是收尾版差异说明(只说时间列、多出来的筛选、同一列上的条件改写、
聚合与去重、关联五类,别的只给兜底)下各有代表性的几种:按哪一列筛时间不同(time_column,两列都声明为日期类型,预告写业务日期)、
只有一边按时间筛、一边多出来的筛选(排除无效记录的 filter_extra 预告写有效记录条件,指标自己范围的 filter_scope 不写)、
差两个筛选条件、关联方式、只差一个关联条件(join_condition:LEFT JOIN 的 ON 里多一个条件,只给兜底,逐行对照里那一行
条件前面带着它所在的 JOIN)、聚合只差去重(dedup,预告写去重规则)、说不清的(fallback:套着 DATE() 的时间范围不同——
光一列上的范围不同如今说得出「对 paid_at 的条件是 …」,套着函数的认不出取值范围,只给兜底,审批卡默认展开逐行对照)、
一边解析失败。
**写目录的预告。** 服务端在摘要里另带每个指标的 catalog(采用原流程、采用盲算各会往数据目录里写什么,
dual_check_facts.preview,约定之外另加的),审批卡照它说;老服务端没有时卡上按差异的形状有条件地说,两种说法都按
「服务端到底写不写」核对(2026-10 复核:卡上说会记入、服务端却不写的有好几种)。夹具里的预告(catalog_preview 按指标、
variants 和 peers 各自的 catalog)也是服务端的真实输出:按下面的 SCHEMA、空目录、指标自己的范围留空算。SCHEMA 里
paid_at、created_at 声明为 TIMESTAMP:业务日期只在两边的时间列都声明为带日期的类型时才写。双盲复算的服务端合入之前
没有这个模块,逐项比对那几项跳过,形状和门槛照样查。
**同一次裁决里几个指标落到同一项上。** 预告是每个指标单独算的,服务端写目录前却要把同一次裁决里落到同一张表同一项上的
说法归拢(业务日期、去重规则说法不同的一项都不写,有效记录条件合成一句写,驳回过的拿出来不写)。服务端把每种组合按
真写的办法算好放进对照工件(dual_check_facts.preview_joint → catalog_joint),卡上按各指标当前的选择取;工件里没有时
卡上按其余指标当前的选择有条件地推。peers 是换掉「退款率」对不上的那个输入的几对 SQL,和「销售额」(或换上的变体)
落到数据目录的同一项上;merges 是检查脚本看的几种组合:notes 是这一组合发起审批时的目录(不写就是空目录),catalog
是两个指标按它各自的预告,joint 是 preview_joint 的真实输出(这两项 regenerate 重新生成),written 是服务端按这几种
选择真写一遍(record_choices,临时数据源)的结果——它要建数据源,regenerate 不生成:测试报不一致时,照报出的实际结果
改 written,再看审批卡的说法要不要跟着改。joint 里按这一组合的选择取出的那一条,和 written 一字不差(会写的值、不写的
原因)。
**挡下的一边。** catalog_blocked 是「销售额」那一对 SQL 在目录里已有人工确认的业务日期时的预告(notes 是那时的目录):
两边都认出了事实、都被目录挡下,照样带着写到哪和原因,审批卡逐条说「不会记入:原因」。
重新生成夹具里的差异说明和预告(只改 diff、摘要的 diff 和 fallback、预告,其余原样;没有预告的模块时预告不动):
cd backend && PYTHONPATH=. python tests/test_dual_check_fixture.py
"""
from __future__ import annotations
import importlib
import json
import uuid
from collections.abc import Iterator
from pathlib import Path
from typing import Any
import pytest
from app.data import catalog
from app.data.catalog import TABLE_FIELD_LABEL
from app.data.sql_semantics import explain_sql_diff
from app.db.base import SessionLocal
from app.db.models import DataSource
from tests.fixtures.sources import drop_source
FIXTURE = Path(__file__).resolve().parents[2] / "frontend" / "src" / "dev" / "fixtures" / "dual_check.json"
DIALECT = "sqlite"
LABELS = ("原流程", "盲算")
#: 摘要里每个指标最多列几条差异(同双盲复算节点的 SUMMARY_DIFFS)
SUMMARY_DIFFS = 3
#: 预告用的数据源名和表结构(虚构):服务端要认得出表、认得出聚合的列属于哪张表,才会预告写哪一项;时间列声明为
#: TIMESTAMP,业务日期才会预告写(文本、整数、只有时刻的时间列不记)
SOURCE = "shop"
SCHEMA: dict[str, Any] = {"tables": {
"orders": {"name": "orders", "columns": [
{"name": "id", "type": "INTEGER"}, {"name": "user_id", "type": "INTEGER"}, {"name": "amount", "type": "REAL"},
{"name": "status", "type": "TEXT"}, {"name": "channel", "type": "TEXT"}, {"name": "paid_at", "type": "TIMESTAMP"},
{"name": "created_at", "type": "TIMESTAMP"}], "primary_key": ["id"]},
"refunds": {"name": "refunds", "columns": [
{"name": "id", "type": "INTEGER"}, {"name": "order_id", "type": "INTEGER"}, {"name": "amount", "type": "REAL"},
{"name": "created_at", "type": "TIMESTAMP"}], "primary_key": ["id"]},
}}
#: 写目录时认得出的三项(dual_check_facts.FACT_ITEMS)
FACT_ITEMS = ("business_date", "valid_filter", "dedup")
#: 差异里能对上这三项的方面:时间列、筛选条件、聚合
FACT_ASPECTS = ("time", "filter", "aggregate")
#: 变体换掉的是口径卡里的「销售额」,peers 换掉的是「退款率」:预告里的指标名照它
VARIANT_METRIC = "销售额"
PEER_METRIC = "退款率"
#: 一份夹具里换掉 SQL 的两组:变体换「销售额」,peers 换「退款率」
SWAPS = {"variants": VARIANT_METRIC, "peers": PEER_METRIC}
def _diff(original: str, blind: str) -> dict[str, Any]:
"""工件里的 diff 只留界面用得到的几项:条目只有 aspect、text,不带两边的写法和 detail(同节点写工件时)。"""
out = explain_sql_diff(original, blind, DIALECT, labels=LABELS).as_dict()
return {"equivalent": out["equivalent"], "items": [{"aspect": i["aspect"], "text": i["text"]} for i in out["items"]],
"fallback": out["fallback"], "text_diff": out["text_diff"]}
def _differing(metric: dict[str, Any]) -> list[tuple[dict[str, Any], dict[str, Any]]]:
"""两边取值不同的输入(按 path 配对):节点只比这些输入的 SQL。"""
blind = {i["path"]: i for i in metric["blind"]["inputs"]}
return [(i, blind[i["path"]]) for i in metric["original"]["inputs"] if i["value"] != blind[i["path"]]["value"]]
def _summary_of(metric: dict[str, Any]) -> dict[str, Any]:
items = metric["diff"]["items"]
original, blind = metric["original"]["value"], metric["blind"]["value"]
return {
"diff": [i["text"] for i in items[:SUMMARY_DIFFS]],
"fallback": not any(i["aspect"] not in ("other", "parse") for i in items),
"delta_pct": None if original == 0 else round((blind - original) / abs(original) * 100, 2),
"original_rendered": metric["original"]["rendered"],
"blind_rendered": metric["blind"]["rendered"],
}
def _load() -> dict[str, Any]:
return json.loads(FIXTURE.read_text(encoding="utf-8"))
def _candidate(metric: str, original: str, blind: str) -> dict[str, Any]:
"""双盲复算节点交给写目录那一步的候选(少一个 choice):只有一个输入对不上,差异条目带两边的写法和 detail(工件里
只留 aspect、text,写目录要看完整的)。指标自己的范围留空:夹具里没有口径说明、分组和运行输入。"""
diff = explain_sql_diff(original, blind, DIALECT, schema=SCHEMA, labels=LABELS).as_dict()
return {"metric": metric, "source": SOURCE, "dialect": DIALECT, "sql": {"a": original, "b": blind},
"diff": {"equivalent": diff["equivalent"], "fallback": diff["fallback"], "items": diff["items"]},
"scope": {"texts": [], "columns": []}}
def _preview_cases(fx: dict[str, Any]) -> Iterator[tuple[str, str, str, str, dict[str, Any], dict[str, Any]]]:
"""夹具里每一份预告:(名字, 指标名, 原流程 SQL, 盲算 SQL, 工件里的差异, 预告)。口径卡的指标按对不上的那个输入,
变体和 peers 按各自的一对 SQL(检查脚本拿变体换掉「销售额」、拿 peers 换掉「退款率」)。"""
for metric in fx["card"]["metrics"]:
(a, b), = _differing(metric)
yield (metric["metric_id"], metric["name"], a["sql"], b["sql"], metric["diff"],
fx["catalog_preview"][metric["metric_id"]])
for group, metric_name in SWAPS.items():
for name, swap in fx[group].items():
original, blind = swap["sql"]
yield f"{group}.{name}", metric_name, original, blind, swap["diff"], swap["catalog"]
def _merge_metrics(fx: dict[str, Any], case: dict[str, Any]) -> list[tuple[str, str, tuple[str, str], dict[str, Any]]]:
"""merges 里的一种组合 → 两个指标各自的 (指标 id, 指标名, 两条 SQL, 预告):「销售额」是口径卡原样(gmv 为空)或换上
的变体,「退款率」是换上的 peers。"""
gmv = fx["card"]["metrics"][0]
if case["gmv"]:
variant = fx["variants"][case["gmv"]]
gmv_sql, gmv_preview = tuple(variant["sql"]), variant["catalog"]
else:
(a, b), = _differing(gmv)
gmv_sql, gmv_preview = (a["sql"], b["sql"]), fx["catalog_preview"]["gmv"]
peer = fx["peers"][case["refund_rate"]]
return [("gmv", VARIANT_METRIC, gmv_sql, gmv_preview),
("refund_rate", PEER_METRIC, tuple(peer["sql"]), peer["catalog"])]
def _merge_candidates(fx: dict[str, Any], case: dict[str, Any]) -> list[dict[str, Any]]:
"""merges 里一种组合交给服务端的候选(不带 choice):两个指标各自的那一对 SQL,带上指标 id(preview_joint 按它说结果)。"""
return [{**_candidate(metric, *sql), "metric_id": metric_id} for metric_id, metric, sql, _ in _merge_metrics(fx, case)]
def _server(name: str) -> Any:
"""服务端写目录模块里的 preview(预告)或 record_choices(真写);双盲复算的服务端还没合入时返回 None。"""
try:
return getattr(importlib.import_module("app.engine.dual_check_facts"), name, None)
except ImportError:
return None
def test_card_metrics_carry_the_real_diff():
fx = _load()
for metric in fx["card"]["metrics"]:
pairs = _differing(metric)
# 夹具里每个指标只有一个输入对不上:节点这时直接用这一处的差异说明,几处时才会合并、加前缀
assert len(pairs) == 1, metric["metric_id"]
(a, b), = pairs
assert metric["diff"] == _diff(a["sql"], b["sql"]), metric["metric_id"]
def test_summary_follows_the_card():
fx = _load()
summary = {s["metric_id"]: s for s in fx["approval"]["payload"]["summary"]}
assert list(summary) == [m["metric_id"] for m in fx["card"]["metrics"]]
for metric in fx["card"]["metrics"]:
got = summary[metric["metric_id"]]
assert {k: got[k] for k in _summary_of(metric)} == _summary_of(metric), metric["metric_id"]
def test_variants_carry_the_real_diff():
fx = _load()
for group in SWAPS:
assert fx[group], group
for name, swap in fx[group].items():
original, blind = swap["sql"]
assert swap["diff"] == _diff(original, blind), f"{group}.{name}"
#: 变体里各有代表性的几种(见模块说明「变体」):名字 → (差异条目的方面, 预告会写的边和项)
REPRESENTATIVE = {
"time_column": (["time"], {"a": "business_date", "b": "business_date"}),
"filter_extra": (["filter"], {"a": "valid_filter"}),
"dedup": (["aggregate"], {"a": "dedup"}),
"fallback": (["other"], {}),
"join_condition": (["other"], {}),
}
def test_variants_keep_their_representative_shapes():
"""检查脚本拿这几种变体看卡上怎么说。差异说明或写目录的规则一改,变体可能悄悄变成别的形状(换了时间列却只给兜底、
预告不再写业务日期),那几项检查就什么也没测:这里守住每种的方面和预告会写的项,说不清的那种还得有逐行对照。不依赖
服务端的模块,一直查。"""
fx = _load()
for name, (aspects, writes) in REPRESENTATIVE.items():
swap = fx["variants"][name]
assert [i["aspect"] for i in swap["diff"]["items"]] == aspects, name
assert {side: said["item"] for side, said in swap["catalog"].items() if said["write"]} == writes, name
fallback = fx["variants"]["fallback"]["diff"]
assert fallback["fallback"] is True and fallback["text_diff"], fallback
# 只差一个关联条件:那一行条件前面带着它所在的 JOIN(两边相同的上级行,审批卡淡色画),检查脚本拿它看上级行怎么画
on = fx["variants"]["join_condition"]["diff"]["text_diff"]
assert [line[:2] for line in on] == [" ", "- "] and on[0].startswith(" LEFT JOIN ") and on[0].endswith(" ON"), on
def test_text_diff_is_not_the_raw_text():
"""守住这次的教训:至少有一处规范化之后的行和原文的任何一行都不一样(真实输出就是这样),夹具不再照原文手写。"""
fx = _load()
gmv = next(m for m in fx["card"]["metrics"] if m["metric_id"] == "gmv")
raw = {" ".join(line.split()).lower() for side in ("original", "blind") for line in gmv[side]["inputs"][0]["sql"].split("\n")}
shown = [" ".join(line[2:].split()).lower() for line in gmv["diff"]["text_diff"]]
assert shown and not any(line in raw for line in shown)
def test_catalog_previews_have_the_server_shape():
"""每个指标、每个变体和 peers 都有预告,两边齐全;写的只能是服务端写目录的门槛之内的(两条 SQL 不等价、没有兜底、
恰好一条差异,而且是时间列、筛选条件或聚合),写的是三项之一、有表有取值;不写的有原因。不依赖服务端的模块,一直查。"""
fx = _load()
assert set(fx["catalog_preview"]) == {m["metric_id"] for m in fx["card"]["metrics"]}
cases = list(_preview_cases(fx))
assert len(cases) == len(fx["card"]["metrics"]) + sum(len(fx[group]) for group in SWAPS)
for name, _, _, _, diff, preview in cases:
assert set(preview) == {"a", "b"}, name
single = not diff["equivalent"] and not diff["fallback"] and len(diff["items"]) == 1
for side in ("a", "b"):
said = preview[side]
if said["write"] is True:
assert single and diff["items"][0]["aspect"] in FACT_ASPECTS, (name, side)
assert said["item"] in FACT_ITEMS and said["item"] in TABLE_FIELD_LABEL, (name, side)
assert said["source"] == SOURCE and said["table"] in SCHEMA["tables"] and said["value"], (name, side)
else:
assert said["write"] is False and said["reason"], (name, side)
def test_catalog_previews_follow_the_server():
"""预告和服务端(dual_check_facts.preview)一字不差:写不写、写到哪、写成什么,不写的原因(审批卡逐条照原话说「不会
记入:原因」)。服务端改了写目录的规则或说法,这里先变红——审批卡上照预告说的、检查脚本按预告核对的说法都要跟着看一遍。"""
preview = _server("preview")
if preview is None:
pytest.skip("双盲复算的服务端(dual_check_facts.preview)还没合入")
for name, metric, original, blind, _, expected in _preview_cases(_load()):
assert preview(_candidate(metric, original, blind), SCHEMA, {}) == expected, name
def test_a_blocked_side_still_says_where_it_would_write():
"""目录里已有人工确认的业务日期:两边都认出了事实、都被挡下——照样带着写到哪、本来要写什么和原因,和服务端一字不差"""
preview = _server("preview")
if preview is None:
pytest.skip("双盲复算的服务端(dual_check_facts.preview)还没合入")
fx = _load()
blocked = fx["catalog_blocked"]
gmv = fx["card"]["metrics"][0]
(a, b), = _differing(gmv)
assert preview(_candidate(gmv["name"], a["sql"], b["sql"]), SCHEMA, blocked["notes"]) == blocked["catalog"]
for side in ("a", "b"):
said = blocked["catalog"][side]
assert said["write"] is False and said["reason"] and said["table"] == "orders" and said["item"] == "business_date"
def test_merges_carry_the_server_joint_preview():
"""merges 里每一种组合:两个指标各自的预告(catalog)和合起来的预告(joint)都是服务端按这一组合的目录算出来的"""
preview, preview_joint = _server("preview"), _server("preview_joint")
if preview is None or preview_joint is None:
pytest.skip("双盲复算的服务端(dual_check_facts.preview_joint)还没合入")
fx = _load()
for name, case in fx["merges"].items():
notes = case.get("notes") or {}
candidates = _merge_candidates(fx, case)
assert case["catalog"] == {c["metric_id"]: preview(c, SCHEMA, notes) for c in candidates}, name
assert case["joint"] == preview_joint(candidates, {SOURCE: (SCHEMA, notes)}), name
def test_merges_put_two_metrics_on_the_same_item():
"""merges 里每一种组合:两个指标按各自的选择,预告都写、写的是同一个数据源同一张表的同一项。检查脚本拿这几种组合看
「同一次裁决里几个指标落到同一项上」卡上怎么说,换了 SQL 落不到同一项上,那几项检查就什么也没测。服务端写了的,
written 里只有这一项。不依赖服务端的模块,一直查。"""
fx = _load()
assert fx["merges"]
for name, case in fx["merges"].items():
sides = [case["catalog"][metric_id][case["choices"][metric_id]] for metric_id, _, _, _ in _merge_metrics(fx, case)]
# 落到同一项上的:会写的、被目录挡下的都算(都带着写到哪),服务端照样把它们归拢成一项
assert all("table" in s for s in sides), name
targets = {(s["source"], s["table"], s["item"]) for s in sides}
assert len(targets) == 1, name
assert all((SOURCE, w["table"], w["item"]) in targets for w in case["written"]), name
@pytest.fixture
async def make_source():
"""建一个只有表结构(SCHEMA)、目录为空的临时数据源,返回它的名字和 id;用完连同目录一起删掉。写目录只读表结构,
不连库,所以不用建库文件。"""
made: list[str] = []
async def make() -> tuple[str, str]:
name = f"dc_fixture_{uuid.uuid4().hex[:8]}"
async with SessionLocal() as session:
row = DataSource(name=name, kind="sqlite", readonly=True, enabled=True, schema_cache=SCHEMA)
session.add(row)
await session.commit()
made.append(row.id)
return name, row.id
yield make
for source_id in made:
await drop_source(source_id)
async def test_merged_choices_follow_the_server(make_source):
"""merges 里每一种组合按服务端真写一遍(dual_check_facts.record_choices,空目录):写了哪几项、写成什么,和夹具的
written 一致。审批卡按同一套规矩说——同一张表的同一项,业务日期、去重规则说法不同的一项都不写,有效记录条件用 AND
合成一句写——服务端改了归拢的规矩,这里先变红,卡上的说法(DualCheckApproval 的 peersOf)要跟着看一遍。"""
record = _server("record_choices")
if record is None:
pytest.skip("双盲复算的服务端(dual_check_facts.record_choices)还没合入")
fx = _load()
for name, case in fx["merges"].items():
source, source_id = await make_source()
for table, notes in (case.get("notes") or {}).items():
async with SessionLocal() as session:
await catalog.write_entry(session, source_id, table, notes, if_version=0, actor="数据组")
candidates = [{**c, "source": source, "choice": case["choices"][c["metric_id"]]}
for c in _merge_candidates(fx, case)]
out = await record(candidates, run_id="run-dc-0001", node_title="复核周报口径", actor=None)
written = []
async with SessionLocal() as session:
for w in out["written"]:
entry = await catalog.read_entry(session, source_id, w["table"])
assert w["value"] == entry.notes[w["item"]]["value"], name
written.append({"table": w["table"], "item": w["item"], "value": entry.notes[w["item"]]["value"]})
assert written == case["written"], (name, out["skipped"])
# 审批卡按这一组合的选择从 joint 里取的那一条,就是真写的结果:会写的值、不写的原因一字不差
chosen = {c["metric_id"]: case["choices"][c["metric_id"]] for c in candidates}
[entry] = [e for e in case["joint"] if {m["metric_id"]: m["choice"] for m in e["members"]} == chosen]
for metric_id, said in entry["outcomes"].items():
if said["write"]:
assert {"table": entry["table"], "item": entry["item"], "value": said["value"]} in written, (name, metric_id)
else:
assert said["reason"] in [s["reason"] for s in out["skipped"]], (name, metric_id, out["skipped"])
def regenerate() -> None:
fx = _load()
for metric in fx["card"]["metrics"]:
(a, b), = _differing(metric)
metric["diff"] = _diff(a["sql"], b["sql"])
summary = {s["metric_id"]: s for s in fx["approval"]["payload"]["summary"]}
for metric in fx["card"]["metrics"]:
summary[metric["metric_id"]].update(_summary_of(metric))
for group in SWAPS:
for swap in fx[group].values():
swap["diff"] = _diff(*swap["sql"])
if (preview := _server("preview")) is not None:
fx["catalog_preview"] = {}
for metric in fx["card"]["metrics"]:
(a, b), = _differing(metric)
said = preview(_candidate(metric["name"], a["sql"], b["sql"]), SCHEMA, {})
fx["catalog_preview"][metric["metric_id"]] = said
for group, metric_name in SWAPS.items():
for swap in fx[group].values():
swap["catalog"] = preview(_candidate(metric_name, *swap["sql"]), SCHEMA, {})
gmv = fx["card"]["metrics"][0]
(a, b), = _differing(gmv)
blocked = fx["catalog_blocked"]
blocked["catalog"] = preview(_candidate(gmv["name"], a["sql"], b["sql"]), SCHEMA, blocked["notes"])
if (preview_joint := _server("preview_joint")) is not None:
for case in fx["merges"].values():
notes = case.get("notes") or {}
candidates = _merge_candidates(fx, case)
case["catalog"] = {c["metric_id"]: preview(c, SCHEMA, notes) for c in candidates}
case["joint"] = preview_joint(candidates, {SOURCE: (SCHEMA, notes)})
FIXTURE.write_text(json.dumps(fx, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
if __name__ == "__main__":
regenerate()