Windmill 中的 DuckDB 脚本开发指南:参数、DuckLake、外部数据源与 S3 实战
【免费下载链接】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
导读
DuckDB 是 Windmill 中一类一等公民的脚本语言:你可以在脚本编辑器中直接用纯 SQL 编写可复用的数据任务,通过注释声明参数、通过ATTACH挂接 DuckLake 数据湖、连接外部 Postgres/MySQL 等数据库资源,并直接读写工作区 S3 存储上的 CSV、Parquet、JSON 文件。本文以 system_prompts/languages/duckdb.md 为基础,结合仓库源码,系统讲解在 Windmill 中编写 DuckDB 脚本的参数体系、DuckLake 集成、外部数据库连接与 S3 文件操作的完整姿势,读完即可上手写出可参数化、可调度、可回放的 DuckDB 数据脚本。
一、参数定义:注释声明 +$name引用
Windmill 的 DuckDB 脚本遵循一套“注释即签名”的参数协议:参数用注释声明,在 SQL 中用$name语法引用。脚本头部的一组--注释会被解析成脚本的输入参数签名,并在前端 UI 中自动渲染为表单。
-- $name (text) = default -- $age (integer) SELECT * FROM users WHERE name = $name AND age > $age;要点说明:
- 类型标注:括号内的
text、integer等即参数类型,Windmill 的类型体系(text、integer、boolean、datetime 等)均可使用;参数声明了类型后,前端会按类型渲染对应的输入控件并做校验。 - 默认值:用
= default给出。当调用方未显式传值时,执行器会回落到默认值。从 backend/windmill-worker/src/duckdb_executor.rs 的执行逻辑看,每个签名参数在运行时都会按“调用方传入值 → 参数默认值 → null”的优先级解析(job_args.remove(&sig_arg.name).or_else(|| sig_arg.default)),因此带默认值的参数可以安全地省略。 $name引用:声明后即可在 SQL 中直接以$name使用,执行器在运行前会对脚本做参数插值(源码中对应sanitize_and_interpolate_unsafe_sql_args),把$age这类占位符替换为实际的参数值。- 与其它语言一致:这套注释签名语法与 Windmill 的 Python、TypeScript 等脚本语言完全一致(都通过注释声明
name (type) = default),DuckDB 脚本因此可以与其它语言的脚本共享同一套参数传递、调度触发和 API 调用机制。
从源码结构看,DuckDB 脚本的签名解析由
windmill_parser_sql::parse_duckdb_sig(见 backend/windmill-worker/src/duckdb_executor.rs 第 1361 行)负责,脚本在每次运行时都会被重新解析签名——这意味着修改头部注释后无需重新发布 SDK,保存脚本即可生效。
二、DuckLake 集成:把数据湖挂进 DuckDB
DuckLake 是 Windmill 内置的、基于 DuckDB 引擎的数据湖能力,是脚本“写库”与“版本化”的承载层(详见 docs/ducklake-materialization.md)。在 DuckDB 脚本中通过ATTACH即可挂接:
-- 默认 DuckLake ATTACH 'ducklake' AS dl; -- 命名 DuckLake ATTACH 'ducklake://my_lake' AS dl; -- 挂接后即可查询 SELECT * FROM dl.schema.table;这里有几层需要理解:
别名即命名空间:
AS dl之后,dl就成为一个 catalog 别名,后续所有查询都通过dl.schema.table三段式引用,与 DuckDB 原生的多 catalog 语义完全一致。底层是真实连接的重写:你写的
ATTACH 'ducklake://name' AS dl并不是原样执行的。从 backend/windmill-worker/src/duckdb_executor.rs 的transform_attach_ducklake实现(约第 2451、2550 行)可以看到,执行器会把ducklake://形式的 ATTACH 重写为携带真实凭据与数据路径的完整 ATTACH,例如:ATTACH 'ducklake:postgres:...' AS dl (DATA_PATH 's3://...', OVERRIDE_DATA_PATH TRUE, AUTOMATIC_MIGRATION TRUE);即 DuckLake 的目录元数据存放在 Postgres、数据文件存放在工作区 S3 存储上,这些细节对脚本作者完全透明——你只需要写
ATTACH 'ducklake://...'。写库的正式姿势:普通脚本对 DuckLake 的常规写入并不是裸
INSERT,而是通过// materialize ducklake://<name>/<table>注释声明托管写入策略(replace / merge / append / SCD2),由 Windmill 生成幂等、带快照的写入 DDL。这一部分属于“管道”能力,详见 docs/ducklake-materialization.md 的注解语法一节。
三、外部数据库连接:通过资源ATTACH
除了 DuckLake,DuckDB 脚本还能借助 Windmill 的**资源(Resource)**体系连接外部数据库。资源是 Windmill 中集中管理、带权限控制的连接凭据载体;在 SQL 中用$res:前缀引用资源路径即可:
ATTACH '$res:path/to/resource' AS db (TYPE postgres); SELECT * FROM db.schema.table;关键点:
- 资源路径:
path/to/resource是工作区内资源的路径(可以带文件夹层级),$res:前缀告诉执行器“这是一个资源引用”,脚本作者永远不需要把主机名、用户名、密码写进 SQL。 (TYPE ...)指定方言:TYPE postgres指明连接目标类型。从执行器的 ATTACH 转换逻辑(parse_attach_db_resource/transform_attach_db_resource_query,位于 backend/windmill-worker/src/duckdb_executor.rs)可以确认,这一机制会读取对应资源中的连接配置(主机、端口、凭据等)并展开为 DuckDB 可执行的连接串;BigQuery 类型的资源还会有额外的凭据文件处理路径(源码中UseBigQueryCredentialsFile分支)。- 密码脱敏:执行器在转换资源 ATTACH 时会收集真实凭据,并在日志与错误信息中对密码做脱敏处理(
hidden_passwords机制),避免敏感信息泄漏到运行日志。 - 别名的后续使用:
AS db之后,db.schema.table即该外部库的表引用,可参与 JOIN、子查询等任意 SQL 运算。
四、S3 文件操作:读 CSV / Parquet / JSON
DuckDB 的原生 httpfs 扩展允许直接从对象存储读取数据。在 Windmill 中,s3://URI 有统一的映射规则,读写工作区 S3 存储:
-- 默认存储 SELECT * FROM read_csv('s3:///path/to/file.csv'); -- 命名存储 SELECT * FROM read_csv('s3://storage_name/path/to/file.csv'); -- Parquet 文件 SELECT * FROM read_parquet('s3:///path/to/file.parquet'); -- JSON 文件 SELECT * FROM read_json('s3:///path/to/file.json');理解这套 URI 的规则很重要:
- 默认存储用双斜杠:
s3:///path/to/file.csv中“主机”位置为空,表示使用工作区的默认 S3 存储。执行器的transform_s3_uris(backend/windmill-worker/src/duckdb_executor.rs 第 2686 行起)会把s3:///xxx规范化为s3://_default_/xxx(_default_即默认存储的内部名,对应源码中的DEFAULT_STORAGE),随后统一经工作区的 S3 代理完成读写。仓库自带的单元测试(同文件第 4027 行起)明确断言了这一行为:read_parquet('s3:///path/to/file.parquet')会被改写成read_parquet('s3://_default_/path/to/file.parquet')。 - 命名存储:
s3://storage_name/path中的storage_name是工作区中配置的具名存储(对应 Windmill 的存储资源),URI 会原样保留并路由到对应存储。 - 读取函数全家桶:
read_csv、read_parquet、read_json是 DuckDB 原生的表函数,因此它们可以出现在SELECT * FROM位置,与普通表无差别地参与过滤、聚合与多表 JOIN。
五、以(s3object)参数接收 S3 文件
当脚本需要由调用方指定要读取哪个文件时,把参数类型声明为(s3object)即可获得完整的文件选择体验:
-- $file (s3object) SELECT * FROM read_parquet($file);这套机制是这样运作的:
- 前端渲染文件选择器:声明为
(s3object)后,Windmill 的脚本编辑器会在参数表单中渲染一个S3 文件选择器,运行时可从工作区存储中挑选文件或目录。 - 参数以裸 URI 绑定:运行时,执行器把这个参数绑定为不带引号包装的
s3://storage/keyURI(见 backend/windmill-worker/src/duckdb_executor.rs 第 1426–1447 行的s3object分支:s3://{storage}/{s3},未指定存储时回落到DEFAULT_STORAGE),DuckDB 的读取函数可以直接消费该 URI。 - 适用于任何 DuckDB 读取函数:
read_csv($file)、read_json($file)等一律适用,不止read_parquet——因为它绑定的是裸 URI 字符串,凡是能接受s3://路径的 DuckDB 函数都能直接用。
这样,脚本作者既不需要硬编码文件路径,调用方也能在界面上可视化地选择文件,同时脚本本身保持“输入即路径、读取即分析”的纯粹函数形态。
六、写回 S3:用 DuckDB 原生的COPY ... TO
把查询结果写回 S3,DuckDB 提供了原生的导出语句:
COPY (SELECT * FROM users) TO 's3:///exports/users.parquet' (FORMAT PARQUET);- 目标路径同样遵循上文提到的
s3://规则:s3:///...双斜杠为默认存储,s3://storage_name/...为具名存储,执行器会做同样的 URI 规范化。 (FORMAT PARQUET)指定导出格式;DuckDB 原生支持 Parquet、CSV、JSON 等多种COPY ... TO目标格式,并可带PARTITION_BY、COMPRESSION等导出选项,配合SELECT子查询可以完成“查询 → 落盘”一步到位。
为什么这里不用-- s3流式指令
Windmill 的其它 SQL 方言(如 Postgres 脚本)提供-- s3流式指令,用于把查询结果以流式方式写回 S3。DuckDB 脚本不提供该指令,原因在于 DuckDB 本身就内置了完整的 S3 写入能力(COPY ... TO 's3://...'),无需再叠加一层流式导出——直接用原生语法即可,这也是文档明确建议的替代方案。写作时请以COPY ... TO为准,不要尝试在 DuckDB 脚本中使用-- s3指令。
七、底层原理速览:一条 DuckDB 脚本的运行时旅程
为了更可靠地使用上面的语法,了解执行器做了什么会很有帮助(全部逻辑集中在 backend/windmill-worker/src/duckdb_executor.rs 的do_duckdb中):
- 解析签名与注解:先用
parse_duckdb_sig解析头部注释中的-- $name (type)参数声明;同时解析// materialize、// macros、// use等管道注解。 - 参数插值:将调用方传入的参数(或默认值)按签名顺序绑定为
$name引用的值,其中(s3object)类型被特判翻译为裸s3://URI。 - ATTACH 转换:逐块扫描脚本语句,把
ducklake://、datatable://、$res:资源形式的ATTACH重写为携带真实凭据与数据路径的连接指令,并完成 S3 URI 规范化(transform_s3_uris)。 - 进程内执行:DuckDB 以**进程内(in-process)**方式运行在 worker 中(通过 FFI 动态库,见 backend/windmill-duckdb-ffi-internal 所在的 worker 目录及 docs/duckdb-isolation.md),因此它不能像其它语言那样被 nsjail 子进程隔离;在启用隔离的 worker 上,隔离策略会在连接层面施加
SET disabled_filesystems='LocalFileSystem'等设置——这也解释了为什么脚本中的外部访问(S3、DuckLake、资源连接)全部要走 Windmill 的托管通道,而不是脚本自行访问本地文件系统。 - 宏库注入:若脚本调用工作区宏库(
// use声明的CREATE MACRO库),执行器会从注册表拉取被调用的宏,以CREATE OR REPLACE TEMP MACRO形式按依赖顺序注入到语句流中(见 backend/parsers/windmill-parser/src/duckdb_macros.rs)。
理解这条链路后,就能明白为什么“参数声明要写注释、外部访问要走资源/DuckLake/S3 托管通道”——它们不是约定俗成的风格,而是执行器真实处理的分界点。
八、这份指南从哪来:Language System Prompts
本文内容是 Windmill 系统提示词中 DuckDB 语言专属指令(system_prompts/languages/duckdb.md)的完整展开。该目录是前端 Copilot 与 CLI 引导共用的“单一事实来源”(见 system_prompts/README.md),languages/下的语言指令会被打包进脚本生成的系统提示词,指导 AI 助手按照上述语法编写符合规范的 DuckDB 脚本。因此,本文所述语法也正是 Windmill 的 AI 助手生成 DuckDB 脚本时所遵循的规范——你完全可以把它当作与 Copilot 协作时的共同语言。
小结
- 参数:
-- $name (type) = default声明,$name引用,(s3object)获得文件选择器并绑定裸s3://URI。 - DuckLake:
ATTACH 'ducklake' AS dl或ATTACH 'ducklake://my_lake' AS dl,随后以dl.schema.table查询;底层由执行器重写为真实连接。 - 外部数据库:
ATTACH '$res:path/to/resource' AS db (TYPE postgres),凭据集中管理、自动脱敏。 - S3 读取:
read_csv/read_parquet/read_json消费s3:///...(默认存储)或s3://storage/...(具名存储)URI。 - S3 写入:使用 DuckDB 原生
COPY ... TO 's3:///...' (FORMAT PARQUET),不要使用其它方言的-- s3指令。
【免费下载链接】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),仅供参考