Windmill 数据指标层(Pipeline Metrics Layer):在 DuckLake 物化脚本中声明度量和维度
Windmill 数据指标层Pipeline Metrics Layer在 DuckLake 物化脚本中声明度量和维度【免费下载链接】windmillOpen-source developer platform to power your entire infra and turn scripts into webhooks, workflows and UIs. Fastest workflow engine (13x vs Airflow). Open-source alternative to Retool and Temporal.项目地址: https://gitcode.com/GitHub_Trending/wi/windmill导读data_metric度量目录是 Windmill 中为 DuckLake 物化表声明权威聚合口径的一种轻量机制直接在物化脚本的注释头里写下// measure与// dimension部署时将其收录进目录表再交由脚本编辑器抽屉与 AI Agent 读取让写查询的人无论人还是模型在动手聚合之前先看到这张表的标准答案。本指南以 docs/pipeline-metrics-layer.md 为主线结合 data_metrics.rs、迁移脚本、读取端点 等源码完整讲解声明语法、目录存储、访问模式、授权模型、两个消费端编辑器抽屉与 MCP Agent 工具以及部署期的安全校验帮助你在自己的流水线中落地一套先声明、后查询的指标口径实践。这个功能是什么在 Windmill 中DuckLake 表由 DuckDB 脚本通过// materialize注释物化参见 ducklake-materialization.md。指标层允许你在同一段注释头里把这张表规范的聚合方式声明出来-- materialize ducklake://sales/orders -- measure revenue sum(amount) where not is_refund -- measure order_count count(*) -- dimension region region -- dimension month date_trunc(month, ordered_at) SELECT ...这些声明在部署deploy时被解析、收录进data_metric目录随后被两类消费方读取脚本编辑器抽屉drawer根据度量/维度选择在客户端拼装出一条普通SELECT供用户复制、在 REPL 中执行或插入当前脚本Agent 工具MCP tool列出某张表或某个文件夹下声明了什么让 Agent 直接使用声明的expr/filter而不是自己臆造聚合。关键在于——该功能只负责记录定义并把定义交给写查询的人它不重写、不编译、也不执行任何 SQL。正如 data_metrics.rs 模块注释所写的Nothing here rewrites or executes SQL: the catalog is read by the script editor and by agents, which compose their own queries.它刻意不做什么文档明确划出了三条边界理解它们才能正确使用这个功能它不强制任何东西。Agent 或人可以无视已声明的 measure直接写不带退款过滤的sum(amount)没有任何机制能拦住。设计赌注是大多数错误数字源于不知道规则而非故意违反规则让写查询的人知情就已经实现了绝大部分价值。它不是语义层semantic layer。声明是单表的没有 join planner也没有跨粒度grain的扇出安全聚合。要跨表组合开发者需要用流水线步骤物化一张 mart 表并在该表上声明度量。没有查询时间接层。部署什么就跑什么。若在部署时把{{ metric(...) }}这类 token 编译成冻结 SQL、再由 worker 替换会导致编辑器中的脚本不再是实际执行的脚本任务日志里也会出现源码中不存在的 SQL——这正是该设计坚决回避的。为什么没有编译器文档给出了一个务实的权衡分析编译器相比把定义交给调用方多买到两样东西——无 LLM 参与的逐字节一致 SQL以及强制力。而这两者只有在一个前提下才有价值多个相互独立的消费方依赖同一个数字可复现。当前代码库中还没有出现这种情况的证据。在那之前编译器意味着 token 语法、按脚本哈希做部署期冻结、worker 替换路径以及编辑器与运行时不一致的第二个来源纯属额外成本。如果未来证据出现目录catalog正是建造它的正确地基声明已经是结构化且经过校验的。编写声明注释头语法两条注解都写在脚本开头的注释头里紧挨着// materialize// measure name aggregate [where predicate]// dimension name expression其中expr和filter是作者自己的 SQL原样存储verbatim。谓词刻意与聚合分开存放、而不是折叠进聚合里是因为读取方会把它渲染成expr FILTER (WHERE filter)——这正是让两个带不同谓词的 measure 能共存于同一个GROUP BY之下的原因共享的WHERE表达不了这一点。声明挂在脚本所物化的表上因此没有 ducklake// materialize目标的脚本不产生任何目录行——没有表可以挂载它们。这一点在 sync_metric_catalog 中实现解析注解后若materialize目标缺失或不是AssetKind::Ducklake直接返回。目录The Catalog表结构与维护策略data_metric每行存放一条声明部署时按脚本路径整体替换先 DELETE 该路径旧行、再 INSERT 新行因此它永远描述已部署状态与asset表的维护方式一致。迁移脚本 20260720050649_data_metric_catalog.up.sql 定义了表结构CREATE TABLE data_metric ( workspace_id VARCHAR(50) NOT NULL REFERENCES workspace (id) ON UPDATE CASCADE ON DELETE CASCADE, script_path VARCHAR(510) NOT NULL, -- 声明方脚本也是权限锚点 table_path VARCHAR(510) NOT NULL, -- 规范化后的 ducklake 路径 lake/schema.table kind VARCHAR(16) NOT NULL CHECK (kind IN (measure, dimension)), name VARCHAR(255) NOT NULL, expr TEXT NOT NULL, filter TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (workspace_id, script_path, kind, name) );持久化而非按需解析是文件夹级列表查询便宜的关键读单张表的声明无论如何都是点查但列出f/analytics下声明的所有指标若按需解析则每次 Agent 调用都要抓取并解析文件夹内每个脚本的正文。持久化后两种访问模式都是单次索引扫描靠三个索引支撑idx_data_metric_table (workspace_id, table_path)——编辑器抽屉的这张表声明了什么idx_data_metric_page (workspace_id, table_path, kind, name, script_path)——列表端点的排序键让 keyset 分页每页都是有序索引范围扫描而不是每次请求都重新排序整个目录idx_data_metric_folder (workspace_id, script_path text_pattern_ops)——Agent 工具的文件夹前缀查询。这里特意使用text_pattern_ops数据库的 collation 不是C在 locale collation 下规划器不会为前缀匹配使用默认 opclass 的索引。文档记录了一个 50k 行的实测默认 opclass 下是 seq scan改用text_pattern_ops后才是索引范围扫描。部署时sync_metric_catalog会同时清理old_path与script_path两处的旧行确保脚本重命名不会在其不再占用的路径上留下孤儿声明见 data_metrics.rs。规范化表路径Canonical table paths生产者与消费者对同一张表的写法不同生产者写// materialize ducklake://sales/orders省略 schema而消费者读到的资产名可能是sales.main.orders。两者必须归一化到同一个目录键否则消费者永远解析不到它读的那张表的声明。canonical_table_path统一转换为lake/schema.table、默认 schema 为maindata_metrics.rscanonical_table_path(ducklake://sales/orders) sales/main.orders canonical_table_path(sales/orders) sales/main.orders canonical_table_path(sales/analytics.orders) sales/analytics.orders -- 显式 schema 保留测试 data_metrics.rs 测试模块 验证了这三条路径规则。授权模型data_metric表没有 RLS。读取时通过 authed 连接上的EXISTS子查询过滤script表从而由script表既有的文件夹folder、组group、用户user策略决定调用方能看到什么见 list_metrics 的 SQL 片段。因此声明方脚本路径是目录键的一部分DuckLake 路径没有可供授权的文件夹。此外带 path scope 的 token 会被单独过滤build_scope_path_filter(authed, data_metrics, read)使用独立的data_metricsscope 域而非scripts的别名否则会放行所有/scripts路由并把 scope 过滤推进 SQL 里而不是取回后再过滤——这样 keyset 游标最后一行的位置永远不会指向调用方看不到的行且每页大小不会泄露越权声明的数量。消费方一脚本编辑器抽屉每个 DuckDB 脚本的编辑器栏都有一个Metrics trigger在窄宽度下它收纳进紧凑的 Helpers 菜单。抽屉的功能流程实现在 MetricsDrawer.svelte列出声明了指标的 DuckLake 表供浏览根据度量/维度选择在客户端拼装一条普通SELECT提供三种去向复制一条完整的独立查询自带ATTACH把 lake 挂到dl别名下在嵌入式 REPL 里运行直接对着 lake 执行预览追加到脚本复用脚本已挂的别名——因为重复执行ATTACH不会生效所以插入版会去掉自己的ATTACH源码第 92/107-115 行的注释说明了这一逻辑。输出是普通的可编辑 SQL与目录没有任何回链——用户改完即为己有这正是不重写、不编译、不执行哲学在前端的体现。抽屉的 REPL 每次执行都会跑一个 preview job所以它是验证一个指标的地方而不是仪表盘没有图表、没有过滤器构造器。消费方二Agent 工具MCP同一个列表端点以x-mcp-tool: true暴露为 MCP 工具listDataMetrics见 openapi.yaml 与 auto_generated_endpoints.rs 中的注册。Agent 可以问这张表声明了什么或这个文件夹下声明了什么并使用声明的expr/filter而不是自己发明聚合。OpenAPI 的 tool 描述本身就是给 Agent 的使用手册Call this before writing any aggregate query over a DuckLake table. A declared measure is the canonical definition of that number… Use each returnedexprverbatim, and when a measure has afilterwrite it asexpr FILTER (WHERE filter)so measures with different predicates can share one GROUP BY. If a number you need has no declared measure, write your own aggregate as usual.端点的分页是 keyset游标分页排序键为(table_path, kind, name, script_path)四个cursor_*参数必须一起传部分游标会被拒绝返回next_cursor表示还有更多。总工作量对全目录是线性的——offset 分页会重复读取之前所有页。per_page上限 1000。path_prefix匹配路径本身及其所有后代但对%、_做了转义、并按/边界锚定因此f/analytics不会误匹配f/analytics2见 descendants_of。部署期的校验硬拒绝 vs 建议性告警因为读取方REPL、Agent 拼装的查询会执行存储的文本部署时有三类输入被硬性拒绝都在 sync_metric_catalog 中实现不安全的 lake/table 路径lake 名会被插进读取方执行的ATTACH ducklake://lake …字符串引号或分号就是存储型 SQL 注入。is_safe_table_path只允许字母、数字与_ - . /。不是单一 SQL 表达式的 measure/dimension 正文count(*) FROM t; DELETE …若被存下会在任何打开抽屉的人身份下执行。校验用windmill_parser_sql_asset::is_single_sql_expression实现见 lib.rs。注意尾随注释也会被拒绝——解析器跳过注释后sum(amount) --rest无法被确认是单表达式。带where过滤的 measure 不是单一聚合调用sum(a)/count(b) where …编译为FILTER (WHERE …)而 SQL 会把FILTER绑定到其中一个聚合上只过滤表达式的一部分、静默产生错误数字。因此带过滤的 measure 必须是sum(amount)这类单一聚合调用is_single_function_call。这三类拒绝的理由在单元测试里都有对应用例例如 data_metrics.rs 测试模块assert!(!is_single_sql_expression(1; DROP TABLE t)); assert!(!is_single_sql_expression(sum(amount) --rest)); assert!(!is_single_sql_expression(sum(amount) /* c */)); assert!(!is_single_function_call(sum(amount) / count(*))); assert!(!is_safe_table_path(dl;SELECT(1);--/main.orders));其余一切都是建议性的缺列missing-column与非聚合 measure 的警告来自独立的check_schema_contracts端点scripts.rs编辑器在保存后 fire-and-forget 调用它警告永不阻塞部署。CLI 或直接 API 部署会跳过这些警告——一条 SQL 本身错误但过了语法关的声明只有真正运行它时才会被发现。另外两处边界值得注意列宽超限name最长 255、table_path最长 510 字符也会在部署时以清晰消息拒绝而不是抛 Postgres 的 value too longsync_metric_catalog对每个语言都会执行不只是 DuckDB因为脚本删掉了声明、改了语言、或在同路径被替换都必须清掉旧行。局限与后续方向声明只存在于物化后的 DuckLake 表上目前无法为 Postgres 表或 API 结果声明 measure。抽屉 REPL 每次执行一个 preview job是验证指标的场所而非仪表盘没有图表或过滤器构造器。抽屉在 TypeScript 里拼 SQLAgent 自己拼 SQL二者因读取同一份声明而语义一致但不是逐字节一致只有未来要求完全相同的 SQL即上文说的编译器场景时才会成为问题。硬拒绝针对三类存储型风险其余质量问题靠check_schema_contracts的建议性警告兜底而 CLI/直连 API 部署会跳过这些警告。需要深入代码的读者可继续阅读目录写路径 sync_metric_catalog、目录读路径 list_metrics、建表迁移、部署集成点、OpenAPI 端点定义、前端抽屉实现。指标层的声明与缺列告警也属于 schema_contracts.rs 负责的资产契约检查的一部分可与 pipelines-vs-dbt.md 对照阅读。【免费下载链接】windmillOpen-source developer platform to power your entire infra and turn scripts into webhooks, workflows and UIs. Fastest workflow engine (13x vs Airflow). Open-source alternative to Retool and Temporal.项目地址: https://gitcode.com/GitHub_Trending/wi/windmill创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →