Files
AgentLab/backend/app/data/catalog_profile_observe.py
T
ThearchyHelios ed72ee68d6 W3-a 关联覆盖率下降、清理目录跨表、扩容器集成测试到对象索引和取值观察
- 剖析核对关联关系时,覆盖率从够有确证的线(95%)掉到线以下的记一条 coverage_drop(取值变化,warning):剖析本身照旧
  按规则降回推断(人工确认的只补上新的覆盖率),这里只把「覆盖率下降」留进变化记录。人点的剖析也记(变化记录全站一份)。
  关系的发现里多带 previous_coverage。
- 处理动作「清理目录」逐表清:这张表上说到被删的列的项,加上别的表指向它的关联关系(表删了时尤其如此);记这张表的
  目录版本,别的表也动了的另记 catalog_versions。
- 剖析任务的逐表进度多一类 observe(取值观察的四类合成一组),前端「取值观察 N/M」用它。
- 定时维护补一条测试:AGENTLAB_SCHEDULER 关着时调度循环不起,到点的维护不触发、不建任务。
- scripts/dialect-it.sh / dialect_it.py 扩到第 2 波的对象索引字典查询(走字典查询不退回 Inspector、类型、表注释、行数
  估算、Oracle 小写名的 quote)和这一波的取值观察(经剖析引擎只观察:行数、小表码值整表、大表 PG / Oracle 块采样、MySQL
  前 N 行、最新业务日期排除无效日期,max_date_sql 直接跑)。夹具的订单表加了表注释。本机跑了一次四种库的容器
  (postgres:16、mysql:8.4、mariadb:11、gvenzl/oracle-free:23-slim):121 项全过、0 项不通过,跑完容器已删、镜像留着。

验证:后端全量 14046 通过(11 跳过、8 预期失败)。
2026-10-16 17:28:07 +08:00

484 lines
27 KiB
Python
Raw 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.
"""取值观察:剖析引擎的四类观察检查(docs/design/next-round/03-数据接入与目录维护.md B2)。
**为什么要有它。** 码值出现新取值、比例剧变、业务日期断档、某天没数据,以前都看不到:确认过的码值列不再取分布,大表只看
物理顺序最老的前 10 万行,没有最新业务日期、按日行数、总行数的观察和历史;「已列全」过期以后 SQL 检查还照它提示。
**四类检查**(和关系、码值、日期检查一样是剖析的检查项:计入每张表的预算、止损,每条查询都留痕):
- rows:这张表的行数(剖析开头估行数那一条的结果,不另发查询);口径 stats(数据库统计的估算值)/ count(数到上限);
- codes_observe:码值列的取值分布——**所有**码值列都观察,包括人工确认过、标了已列全的(目录-D6);只读,不改含义;
- freshness:业务日期列(已确认或已验证)的最新日期;
- daily_volume:业务日期最近 35 天每天的行数。
**大表的口径**(目录-D7):小表(行数不超过整表统计的上限)看整表(full)。大表:业务日期已确认或已验证、而且这一列打头
有索引(探查记下的 index_first_columns)时看最近 35 天的窗口(window,走索引的范围扫描);PostgreSQL / Oracle 没有就块采样
(sample,按数据块随机取约 p%,p 按表的大小取一档,不跨档比);MySQL / SQLite 没有块采样,只看按存储顺序的前 N 行
(first_rows),照实写口径。最新业务日期、按日行数在大表上只在业务日期列打头有索引时才查,否则记「表太大」跳过。每条观察
都记口径(basis),只和口径相同的历史比(data/value_drift)。
**遮罩**:遮罩列不出现在任何一条观察查询里(被遮罩的码值列、业务日期列记「被遮罩」跳过;窗口要用的业务日期列被遮罩时
不用窗口)。
**落库与比较**(save,和这张表的剖析结果、目录在同一个事务里):每条观察写进 table_observations,每张表每种观察(码值按
列)留最近 26 条;拿同口径的历史比出取值变化写进 source_changes。机器唯一能写进目录的事:数据证明码值表不再列全时撤销
「已列全」(目录版本 +1,现有的目录漂移、冻结快照、SQL 检查自然跟上);新取值本身、含义、状态都不改,交给人在变化里处理。
**什么时候观察**:剖析任务的范围 observe(只观察)和 maintenance(需要剖析的表 + 观察到期的表,定时维护用)才带观察检查;
人点的「需要剖析 / 全部 / 指定的表」照旧不观察。观察到期(observe_due):有数据目录、不是视图的表,从没观察过,或者上一次
观察在 OBSERVE_EVERY_H 小时以前。
"""
from __future__ import annotations
import logging
from collections.abc import Iterable, Mapping
from datetime import date, datetime, timedelta, timezone
from typing import Any
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.data import catalog
from app.db.base import utcnow
from app.db.models_sources import TableObservation
logger = logging.getLogger(__name__)
OBSERVE_KINDS = ("rows", "codes_observe", "freshness", "daily_volume")
#: 按日行数、窗口观察看最近几天
WINDOW_DAYS = 35
#: 码值观察最多取几个取值(多出来的记 truncated,不判新码值、不算比例)
OBSERVE_CODES_MAX = 50
#: 块采样的比例按表的大小取一档:目标约 max_scan_rows 行,向下取到这几档里的一档(同一张表大小不变时口径不变)
SAMPLE_LADDER = (10.0, 5.0, 2.0, 1.0, 0.5, 0.2, 0.1, 0.05, 0.02, 0.01)
#: 上一次观察多久以前算「观察到期」(每天跑一次的定时维护每次都会观察;每小时跑一次的一天观察两次)
OBSERVE_EVERY_H = 12
#: 撤销「已列全」时目录的署名
OBSERVER = "取值观察"
KIND_LABEL = {"rows": "行数", "codes_observe": "码值观察", "freshness": "最新业务日期", "daily_volume": "按日行数"}
TOO_LARGE_DATE = ("{table} 的行数超过整表统计的行数上限,业务日期列 {column} 打头没有索引,不看{what}"
"(大表只在业务日期列打头有索引时才查)")
NO_DATE = "{table} 没有已确认或已验证的业务日期,不看{what}"
# ==========================================================================
# 列出检查(纯函数)
# ==========================================================================
def _business_date(meta: Mapping[str, Any], notes: Mapping[str, Any]) -> str | None:
"""已确认或已验证的业务日期列(表结构里的写法);没有为 None。"""
from app.data.catalog_profile import _column
item = notes.get("business_date") if isinstance(notes.get("business_date"), Mapping) else None
if not item or item.get("status") not in ("confirmed", "verified"):
return None
column = str((item.get("value") or {}).get("column") or "")
return _column(dict(meta), column) if column else None
def code_columns(meta: Mapping[str, Any], notes: Mapping[str, Any]) -> list[str]:
"""要观察的码值列:目录里有码值项(没被驳回)的列,加上 ENUM / SET 列;按表结构的顺序。人工确认过、标了已列全的照样
观察(目录-D6)。"""
from app.data.catalog_profile import _is_enum
described = notes.get("columns") if isinstance(notes.get("columns"), Mapping) else {}
out: list[str] = []
for col in meta.get("columns") or []:
if not isinstance(col, Mapping):
continue
name = str(col.get("name") or "")
items = described.get(name) if isinstance(described.get(name), Mapping) else {}
codes = items.get("codes") if isinstance(items, Mapping) else None
listed = isinstance(codes, Mapping) and codes.get("status") != "rejected"
if listed or (_is_enum(col.get("type"), col.get("enum_values")) and not isinstance(codes, Mapping)):
out.append(name)
return out
def plan_observations(table: str, meta: Mapping[str, Any] | None, notes: Mapping[str, Any] | None, *,
masked: set[str]) -> list[Any]:
"""这张表要做的观察检查(catalog_profile.PlannedCheck),接在关系、码值、日期检查后面:行数、各码值列、最新业务日期、
按日行数。被遮罩的列照样列出来、带 skip(不查,让人看得见为什么没有)。视图、表结构里没有的返回空列表。"""
from app.data.catalog_profile import PlannedCheck
if not isinstance(meta, Mapping) or meta.get("is_view"):
return []
notes = notes or {}
out = [PlannedCheck("rows", table, None)]
for column in code_columns(meta, notes):
skip = (("masked", f"{table}.{column} 在数据源设置中被遮罩,不观察取值", False)
if column.lower() in masked else None)
out.append(PlannedCheck("codes_observe", column, f"columns.{column}.codes", skip, column=column))
day = _business_date(meta, notes)
for kind, what in (("freshness", "最新业务日期"), ("daily_volume", "按日行数")):
if day is None:
continue
skip = (("masked", f"{table}.{day} 在数据源设置中被遮罩,不看{what}", False) if day.lower() in masked else None)
out.append(PlannedCheck(kind, day, "business_date", skip, column=day))
return out
# ==========================================================================
# 执行(在 catalog_profile._TableCx 上发查询:预算、止损、留痕都在那里)
# ==========================================================================
def sample_percent(rows: int | None, target: int) -> float | None:
"""块采样取几档:约 target 行、向下取到 SAMPLE_LADDER 的一档;表不大于 target 时不采样(None)。"""
if not rows or rows <= target:
return None
want = 100.0 * target / rows
return next((p for p in SAMPLE_LADDER if p <= want), SAMPLE_LADDER[-1])
def _today() -> date:
return datetime.now(timezone.utc).date()
def _since() -> date:
return _today() - timedelta(days=WINDOW_DAYS)
def _window_ok(cx: Any, meta: Mapping[str, Any], day: str | None) -> bool:
"""大表能不能看窗口:业务日期列已确认或已验证、打头有索引、没被遮罩。"""
if not day or cx.is_masked(day):
return False
return day.lower() in {str(c).lower() for c in meta.get("index_first_columns") or []}
def _observation(kind: str, column: str, basis: dict[str, Any], data: dict[str, Any], refs: list[int]) -> dict[str, Any]:
key = basis_key(basis)
return {"kind": kind, "column": column, "basis": {**basis, "query_refs": refs}, "basis_key": key, "data": data}
def basis_key(basis: Mapping[str, Any]) -> str:
"""口径的规范写法:只和它相同的历史比。"""
method = basis.get("method")
if method == "window":
return f"window:{basis.get('column')}:{basis.get('days')}"
if method == "sample":
return f"sample:{basis.get('percent'):g}"
if method == "first_rows":
return f"first_rows:{basis.get('rows')}"
return str(method or "")
def basis_text(basis: Mapping[str, Any]) -> str:
"""口径的说法(跳过、留痕的说明里用)。"""
method = basis.get("method")
if method == "window":
return f"业务日期 {basis.get('column')} 最近 {basis.get('days')} 天"
if method == "sample":
return f"块采样约 {basis.get('percent'):g}%"
if method == "first_rows":
return f"按存储顺序取前 {basis.get('rows')} 行"
if method == "stats":
return "数据库统计的估算值"
if method == "count":
return "逐行数出"
return "整表"
async def execute(cx: Any, table: str, check: Any, result: Any) -> None:
"""执行一项观察检查,观察到的追加进 result.observations。按规则不查的抛 catalog_profile._Skip。"""
from app.data.catalog_profile import _Skip
meta = cx.tables[table]
start = len(result.query_log)
size = await cx.size_with_data(table, result) if check.kind != "rows" else await cx.size(table, result)
if check.kind == "rows":
basis: dict[str, Any] = {"method": size.method}
data = {"rows": size.rows, "at_least": size.at_least, "method": size.method}
if size.method == "unknown":
raise _Skip("no_data", f"{table} 的行数没能得到(统计信息和计数都没查成),不记行数观察")
result.observations.append(_observation("rows", "", basis, data, []))
return
before = len(result.observations)
if check.kind == "codes_observe":
await _codes(cx, table, str(check.column), meta, size, result)
else:
await _dates(cx, table, check.kind, str(check.column), meta, size, result)
if len(result.observations) > before:
# 留痕:这条观察是哪几条查询得出的(这张表 query_log 里的下标)
result.observations[-1]["basis"]["query_refs"] = list(range(start, len(result.query_log)))
async def _codes(cx: Any, table: str, column: str, meta: Mapping[str, Any], size: Any, result: Any) -> None:
from app.data import catalog_profile as cp
if cx.is_masked(column):
raise cp._Skip("masked", f"{table}.{column} 在数据源设置中被遮罩,不观察取值", carry=False)
d = cx.dialect
col_meta = cp._column_meta(dict(meta), column) or {}
t, c = d.table(dict(meta), table), d.column(dict(meta), column)
n = OBSERVE_CODES_MAX + 1
day = _business_date(meta, getattr(cx, "table_notes", None) or {})
if size.within(cx.settings.max_scan_rows):
basis: dict[str, Any] = {"method": "full"}
sql = d.value_counts_sql(t, c, n, scan_cap=None)
elif _window_ok(cx, meta, day):
since = _since()
basis = {"method": "window", "column": day, "days": WINDOW_DAYS, "since": since.isoformat()}
text = catalog.column_family(cp._column_meta(dict(meta), str(day))) != "time"
sql = d.window_value_counts_sql(t, c, d.column(dict(meta), str(day)), since, n, text=text)
else:
# 数到上限、只知道「至少多少行」的按下限取档(取到的档偏大,多看一点,不会少看)
percent = sample_percent(size.rows if size.rows is not None else size.at_least,
max(cx.settings.max_scan_rows, cx.settings.sample_size))
sampled = d.sample_value_counts_sql(t, c, percent, n) if percent else None
if sampled is not None:
basis, sql = {"method": "sample", "percent": percent}, sampled
else:
cap = cx.scan_cap(size) or max(cx.settings.sample_size, cx.settings.max_scan_rows)
basis, sql = {"method": "first_rows", "rows": cap}, d.value_counts_sql(t, c, n, scan_cap=cap)
res = await cx.run(sql, max_rows=n)
fixed = cp.is_fixed_char(col_meta.get("type"))
counts: dict[str, int] = {}
for row in res.rows[:OBSERVE_CODES_MAX]:
key = cp._code_key(row[0], fixed=fixed)
if key is None:
raise cp._Skip("unsupported", f"{table}.{column} 的取值不是整数或短文字,不观察", carry=False)
counts[key] = counts.get(key, 0) + (cp._as_int(row[1]) or 0)
data = {"values": [[k, v] for k, v in counts.items()], "rows": sum(counts.values()),
"truncated": len(res.rows) > OBSERVE_CODES_MAX}
result.observations.append(_observation("codes_observe", column, basis, data, []))
async def _dates(cx: Any, table: str, kind: str, column: str, meta: Mapping[str, Any], size: Any, result: Any) -> None:
from app.data import catalog_profile as cp
what = KIND_LABEL[kind]
if cx.is_masked(column):
raise cp._Skip("masked", f"{table}.{column} 在数据源设置中被遮罩,不看{what}", carry=False)
if not size.within(cx.settings.max_scan_rows) and not _window_ok(cx, meta, column):
raise cp._Skip("too_large", TOO_LARGE_DATE.format(table=table, column=column, what=what))
d = cx.dialect
t, c = d.table(dict(meta), table), d.column(dict(meta), column)
text = catalog.column_family(cp._column_meta(dict(meta), column)) != "time"
if kind == "freshness":
res = await cx.run(d.max_date_sql(t, c, text=text))
value = res.rows[0][0] if res.rows and res.rows[0] else None
if value is None or not cp._DATE_TEXT.match(str(value)):
raise cp._Skip("no_data", f"{table}.{column} 没有可识别的日期值")
result.observations.append(_observation("freshness", column, {"method": "full"},
{"max": str(value)[:19]}, []))
return
since = _since()
res = await cx.run(d.day_volume_sql(t, c, since, text=text), max_rows=WINDOW_DAYS + 2)
days: dict[str, int] = {}
for row in res.rows:
day = str(row[0])[:10] if row and row[0] is not None else ""
if cp._ISO_DAY.fullmatch(day):
days[day] = days.get(day, 0) + (cp._as_int(row[1]) or 0)
basis = {"method": "window", "column": column, "days": WINDOW_DAYS, "since": since.isoformat()}
result.observations.append(_observation("daily_volume", column, basis,
{"since": since.isoformat(), "until": _today().isoformat(),
"days": days}, []))
# ==========================================================================
# 落库、比较、撤销「已列全」
# ==========================================================================
async def _history(session: AsyncSession, source_id: str, table: str, kind: str, column: str) -> list[TableObservation]:
return list((await session.execute(select(TableObservation).where(
TableObservation.source_id == source_id, TableObservation.table_name == table, TableObservation.kind == kind,
TableObservation.column_name == column).order_by(TableObservation.at.desc(), TableObservation.id.desc())
.limit(TableObservation.KEEP))).scalars())
async def _prune(session: AsyncSession, source_id: str, table: str, kind: str, column: str) -> None:
keep = [r.id for r in await _history(session, source_id, table, kind, column)]
if keep:
await session.execute(delete(TableObservation).where(
TableObservation.source_id == source_id, TableObservation.table_name == table,
TableObservation.kind == kind, TableObservation.column_name == column,
TableObservation.id.not_in(keep)).execution_options(synchronize_session=False))
def _rules_columns(notes: Mapping[str, Any], meta: Mapping[str, Any] | None, table: str, kind: str | None) -> set[str]:
"""有效记录条件用到的列(小写)。"""
from app.data import sqlcheck
item = notes.get("valid_filter") if isinstance(notes.get("valid_filter"), Mapping) else None
if not item or item.get("status") == "rejected":
return set()
parsed = sqlcheck.parse_valid_filter(item.get("value"), meta, kind=kind, table=table)
return {c.lower() for c in parsed.columns}
async def _revoke_complete(session: AsyncSession, source_id: str, table: str, column: str) -> int | None:
"""撤销码值项上的「已列全」(目录版本 +1,不提交):值、含义、状态、来源都不动。撞上别人刚改过重读重来,最多三次。
已经不是已列全了(别人刚改过)返回 None。"""
for _attempt in range(3):
entry = await catalog.read_entry(session, source_id, table)
if entry is None:
return None
notes = entry.notes
item = ((notes.get("columns") or {}).get(column) or {}).get("codes")
if not catalog.codes_complete(item):
return None
item.pop("complete", None)
item["updated_at"] = catalog.now_iso()
try:
written = await catalog.write_entry(session, source_id, table, notes, if_version=entry.version,
actor=OBSERVER, commit=False)
except catalog.CatalogConflict:
continue
return written.version
return None
async def save(session: AsyncSession, source: Any, table: str, result: Any, *, job_id: str | None,
seq: int | None, detected: Any) -> int:
"""这张表这一次的观察写进 table_observations、和同口径的历史比,比出来的取值变化写进 source_changes(不提交:和剖析
结果、目录在同一个事务里)。返回记了几条新变化。出错只记日志、不挡剖析(观察是辅助步骤)。"""
from app.data import source_changes as sc
from app.data import value_drift as vd
observations = list(getattr(result, "observations", None) or [])
if not observations:
return 0
fresh = 0
try:
entry = await catalog.read_entry(session, source.id, table)
notes = entry.notes if entry else {}
meta = ((getattr(source, "schema_cache", None) or {}).get("tables") or {}).get(table)
rules = _rules_columns(notes, meta, table, getattr(source, "kind", None))
now = utcnow()
today = now.date()
for obs in observations:
kind, column = obs["kind"], obs["column"]
history = await _history(session, source.id, table, kind, column)
session.add(TableObservation(source_id=source.id, table_name=table, column_name=column, kind=kind,
basis=obs["basis"], basis_key=obs["basis_key"], data=obs["data"],
job_id=job_id, task_seq=seq, at=now))
await session.flush()
await _prune(session, source.id, table, kind, column)
item = ((notes.get("columns") or {}).get(column) or {}).get("codes") if column else None
known = list((item or {}).get("value") or {}) if isinstance(item, Mapping) and \
item.get("status") != "rejected" else []
current = vd.Obs(kind, obs["basis_key"], obs["data"], now)
past = [vd.Obs(h.kind, h.basis_key, h.data or {}, h.at) for h in history]
findings = vd.compare(current, past, today=today, known=known,
complete=catalog.codes_complete(item))
for f in findings:
after = dict(f.after) if isinstance(f.after, Mapping) else {"value": f.after}
if f.revoke_complete:
version = await _revoke_complete(session, source.id, table, column)
after.update(complete_revoked=version is not None, catalog_version=version)
touched = bool(column) and column.lower() in rules
change = sc.NewChange(kind=f.kind, table=table, column=column or None, severity=f.severity,
before=f.before, after=after,
basis={"method": obs["basis"].get("method"), "key": obs["basis_key"],
"query_refs": obs["basis"].get("query_refs") or []},
affects={"catalog": [], "templates": [], "rules_touched": touched},
key=(obs["basis_key"], f.key))
_row, new = await sc.record(session, source, change, detected)
fresh += int(new)
except Exception: # noqa: BLE001 - 观察是辅助步骤:落库、比较出错不挡剖析结果
logger.exception("保存 %s 的取值观察时出错", table)
return fresh
def coverage_drops(findings: Iterable[Mapping[str, Any]]) -> list[dict[str, Any]]:
"""关系核对的结论里覆盖率掉到有确证的线以下的(以前够、这次不够):[{path, target, before, after}]。剖析本身照旧按
规则降回推断(或只给人工确认的补上新的覆盖率),这里只把「覆盖率下降」记成一条取值变化。"""
out: list[dict[str, Any]] = []
line = catalog.PROFILE_VERIFY_COVERAGE
for f in findings:
if not isinstance(f, Mapping) or f.get("kind") != "relation":
continue
before, after = f.get("previous_coverage"), f.get("coverage")
if isinstance(before, (int, float)) and isinstance(after, (int, float)) and before >= line > after:
out.append({"path": f.get("path"), "target": f.get("target"), "before": before, "after": after,
"confirmed": bool(f.get("confirmed"))})
return out
async def record_coverage_drops(session: AsyncSession, source: Any, table: str, result: Any, *, detected: Any) -> int:
"""这张表这次剖析里覆盖率掉下来的关系记成 coverage_drop(不提交,和剖析结果同一个事务)。人点的剖析也记:变化记录是
全站一份。出错只记日志。"""
from app.data import source_changes as sc
try:
fresh = 0
for drop in coverage_drops(getattr(result, "findings", None) or []):
_row, new = await sc.record(session, source, sc.NewChange(
kind=sc.COVERAGE_DROP, table=table, severity="warning",
before={"coverage": drop["before"], "path": drop["path"], "target": drop["target"]},
after={"coverage": drop["after"], "confirmed": drop["confirmed"]},
basis={"method": "relation_sample", "key": "relation"},
affects={"catalog": [{"table": table, "path": drop["path"], "label": drop["target"],
"status": "confirmed" if drop["confirmed"] else "proposed",
"source": None, "needs_action": False}], "templates": [], "rules_touched": False},
key=(drop["path"], round(float(drop["after"]), 3))), detected)
fresh += int(new)
return fresh
except Exception: # noqa: BLE001
logger.exception("记录 %s 的关联覆盖率下降时出错", table)
return 0
# ==========================================================================
# 观察到期、给助手的观察结论
# ==========================================================================
async def last_observed(session: AsyncSession, source_id: str,
tables: Iterable[str] | None = None) -> dict[str, datetime]:
"""每张表最近一次观察的时刻。"""
query = select(TableObservation.table_name, func.max(TableObservation.at)).where(
TableObservation.source_id == source_id).group_by(TableObservation.table_name)
if tables is not None:
wanted = list(tables)
if not wanted:
return {}
query = query.where(TableObservation.table_name.in_(wanted))
return {name: at for name, at in (await session.execute(query)).all() if at is not None}
def observe_reason(last: datetime | None, *, now: datetime | None = None) -> str | None:
"""观察到期没有:never 从没观察过;observe_due 上一次在 OBSERVE_EVERY_H 小时以前;没到期为 None。"""
if last is None:
return "never"
now = now or utcnow()
if last.tzinfo is None:
last = last.replace(tzinfo=timezone.utc)
return "observe_due" if now - last >= timedelta(hours=OBSERVE_EVERY_H) else None
async def observed_facts(session: AsyncSession, source_id: str,
tables: Iterable[str] | None = None) -> dict[str, dict[str, Any]]:
"""给助手看的观察结论(经 schema_view.profile_lines 和剖析事实同一节,next-round X7):{表名: {freshness: [{column, max,
at}], rows: {rows, method, at_least, at}}},每种取最近一次。遮罩在渲染那一刻再核(不在这里滤)。"""
query = select(TableObservation).where(TableObservation.source_id == source_id,
TableObservation.kind.in_(("freshness", "rows")))
if tables is not None:
wanted = list(tables)
if not wanted:
return {}
query = query.where(TableObservation.table_name.in_(wanted))
out: dict[str, dict[str, Any]] = {}
seen: set[tuple[str, str, str]] = set()
for r in (await session.execute(query.order_by(TableObservation.at.desc()))).scalars():
key = (r.table_name, r.kind, r.column_name)
if key in seen:
continue
seen.add(key)
facts = out.setdefault(r.table_name, {})
at = r.at.isoformat() if r.at else None
if r.kind == "freshness" and (r.data or {}).get("max"):
facts.setdefault("freshness", []).append({"column": r.column_name, "max": r.data["max"], "at": at})
elif r.kind == "rows":
facts["rows"] = {**{k: (r.data or {}).get(k) for k in ("rows", "method", "at_least")}, "at": at}
return out
__all__ = ["KIND_LABEL", "OBSERVE_CODES_MAX", "OBSERVE_EVERY_H", "OBSERVE_KINDS", "OBSERVER", "SAMPLE_LADDER",
"WINDOW_DAYS", "basis_key", "basis_text", "code_columns", "coverage_drops", "execute", "last_observed", "observe_reason",
"observed_facts", "plan_observations", "record_coverage_drops", "sample_percent", "save"]