news 2026/9/23 23:22:22

PRQL 的 Elixir 绑定:使用 Rustler NIF 在 Elixir 中编译 PRQL 查询

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PRQL 的 Elixir 绑定:使用 Rustler NIF 在 Elixir 中编译 PRQL 查询
  • 后端

【免费下载链接】prql

PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement

项目地址:https://gitcode.com/gh_mirrors/pr/prql
点击查看免费下载

本指南围绕 PRQL 仓库中的 Elixir 语言绑定(位于 prqlc/bindings/elixir/README.md,并被文档站点收录于 web/book/src/project/bindings/elixir.md)展开,介绍如何在 Elixir 项目中安装、调用PRQL.compile/2将 PRQL 管道式查询编译为 SQL,并结合仓库源码剖析其 Rustler NIF 桥接层的实现原理、参数语义与开发测试流程。读完本文,你将能够在自己的 Elixir 应用中直接驱动prqlc编译器,并理解其编译管线的每一层(PRQL → PL AST → RQ → SQL)对应的 Elixir API。

一、什么是 PRQL 的 Elixir 绑定

PRQL(Pipelined Relational Query Language)是一种面向数据转换的现代查询语言,目标是成为 SQL 的管道式替代品。PRQL 编译器的核心是 Rust crateprqlc,位于本仓库的 prqlc/prqlc 目录。为了让 Elixir 生态也能使用这一编译器,仓库维护了一套独立的 Elixir 绑定工程,其入口即 prqlc/bindings/elixir/README.md。

该绑定基于Rustler实现:Rustler 是 Elixir 社区中编写 Rust NIF(Native Implemented Function,原生函数)的标准库,它把prqlc的编译能力以原生函数的形式暴露给 Erlang VM,Elixir 侧则通过一个PRQL模块封装调用。从绑定分级来看,Elixir 目前被归类为Unsupported(非官方支持)层级——功能可用,但尚未达到需要维护者、完整测试覆盖、发布到官方包仓库等"Supported"标准(详见 web/book/src/project/bindings/README.md 中的分级说明)。

二、安装与依赖

2.1 在 mix 项目中引入依赖

mix.exsdeps/0中添加依赖:

def deps do [ {:prql, "~> 0.1.0"} ] end

随后执行mix deps.get拉取依赖。当前绑定工程的版本为0.1.0,对 Elixir 的要求是~> 1.15(见 prqlc/bindings/elixir/mix.exs 中project/0的声明)。

2.2 需要 Rust 工具链

需要特别注意的是:在写本文时,该绑定尚未发布预编译产物。README 明确说明,当前在 Elixir 项目中使用该绑定,需要从本仓库源码编译 Rust crate(prqlc/bindings/elixir/README.md的 "Development" 一节)。也就是说,开发/运行环境必须具备 Rust 工具链,且mix compile时会触发对 NIF 的本地编译。

绑定侧的依赖声明位于 prqlc/bindings/elixir/native/prql/Cargo.toml:

  • prqlc:以path = "../../../../prqlc"指向仓库内的编译器本体(版本对应0.13.15),并关闭默认特性(default-features = false);
  • rustler = "0.38.0":NIF 桥接库;
  • crate 类型为cdylib,即编译为可供 Erlang VM 动态加载的共享库。

Elixir 侧的构建依赖同样由 mix.exs 声明:{:rustler, "~> 0.38.0"}与仅开发期使用的{:ex_doc, "~> 0.21"}

三、基本用法:把 PRQL 编译为 SQL

绑定对外暴露的入口是PRQL.compile/2。以下两个示例直接取自绑定 README,也是PRQL模块的 doctest 用例(见 prqlc/bindings/elixir/lib/prql.ex 中的@doc与 prqlc/bindings/elixir/test/prql_test.exs):

iex> PRQL.compile("from customers", signature_comment: false) {:ok, "SELECT\n *\nFROM\n customers\n"} iex> PRQL.compile("from customers\ntake 10", target: :mssql, signature_comment: false) {:ok, "SELECT\n *\nFROM\n customers\nORDER BY\n (\n SELECT\n NULL\n ) OFFSET 0 ROWS\nFETCH FIRST\n 10 ROWS ONLY\n"}

可以观察到两点:

  1. 返回值是{:ok, sql_string}元组:成功时第二个元素就是可直接交给 SQL 驱动执行的 SQL 文本;
  2. 方言参数影响输出take 10在默认的:generic方言与:mssql方言下会生成不同的分页语法——MSSQL 使用OFFSET 0 ROWS ... FETCH FIRST 10 ROWS ONLY表达取前 10 行,这正体现了 PRQL 面向多种 SQL 方言编译的能力。

四、compile/2 的完整参数语义

PRQL.compile(prql_query, opts \\ [])接受两个参数:待编译的 PRQL 查询字符串,以及关键字选项列表。其类型约束为when is_binary(prql_query) and is_list(opts)(见 lib/prql.ex),并通过struct(CompileOptions, opts)将选项结构化为 NIF 侧期望的 struct。

4.1 target:目标 SQL 方言

target决定生成的 SQL 面向哪种数据库方言,支持以下 12 个原子值:

原子值对应 prqlc 方言典型场景
:genericGeneric(默认)通用 SQL
:mssqlMsSqlMicrosoft SQL Server
:mysqlMySqlMySQL
:postgresPostgresPostgreSQL
:ansiAnsiANSI 标准 SQL
:bigqueryBigQueryGoogle BigQuery
:clickhouseClickHouseClickHouse
:duckdbDuckDbDuckDB
:oracleOracleOracle
:redshiftRedshiftAmazon Redshift
:sqliteSQLiteSQLite
:snowflakeSnowflakeSnowflake

该参数默认值为:generic。有两个值得注意的语义:

  • 未知原子回退到:generic:Rust 侧的target_from_atom(见 native/prql/src/lib.rs)会逐一分发各方言原子,落入else分支时统一使用Generic
  • 方言始终优先于查询头@doc中明确说明"这里的方言总是胜过查询头中的target:sql.…参数,且没有可以回退到查询头的值"——即无法通过查询内声明覆盖compile/2传入的target

4.2 format:SQL 格式化开关

format为布尔值,控制是否将生成的 SQL 交给格式化器进行多行拆分与缩进美化,默认值为true。README 与 doctest 中大量使用signature_comment: false正是为了让输出更简洁、便于断言。

4.3 signature_comment:签名注释开关

signature_comment为布尔值,控制是否在生成的 SQL 后附加编译器签名注释,默认值为true

以上三个选项的默认值可以在 NIF 侧的 struct 定义中确认(native/prql/src/lib.rs):

defstruct target: :generic, format: true, signature_comment: true

而在 Elixir 侧,对应的类型声明位于 lib/prql/native.ex 的PRQL.Native.CompileOptions,其t/0类型包含target: target(), format: boolean(), signature_comment: boolean()三个字段。

Rust 侧将CompileOptions转换为prqlc::Options的实现(impl From<CompileOptions> for prqlc::Options)进一步印证了选项的传递链路:formattargetsignature_comment直接映射到prqlc::Options对应字段,同时固定设置display: prqlc::DisplayOptions::Plain(即不做额外的展示层格式化)。

五、错误处理与 compile!/2

5.1 错误返回结构

当查询无法编译时,compile/2返回{:error, reason},其中reason是一个JSON 字符串。测试用例 prql_test.exs 给出了完整的错误 JSON 结构示例——对于查询invalid,返回的错误 JSON 包含inner数组,每个元素含kindcodereasonhintsspandisplay(带源码标注的可读错误信息)与location(起始/结束行列坐标)等字段。这得益于 Rust 侧to_result_tuple的实现:prqlc::ErrorMessages会被序列化为 JSON(e.to_json())作为{:error, json}元组的第二个元素返回。

在应用侧,你可以用任意 JSON 解码库(如Jason)解析该错误字符串,从中提取span(如1:0-7)与display字段做进一步展示或定位。

5.2 断言式 API:compile!/2

如果希望出错时直接抛出异常而非返回元组,可以使用compile!/2

@spec compile!(binary(), [compile_opts()]) :: binary() def compile!(prql_query, opts \\ []) do case compile(prql_query, opts) do {:ok, result} -> result {:error, reason} -> raise PRQL.PRQLError, reason end end

compile!/2成功时直接返回 SQL 字符串,失败时抛出PRQL.PRQLError异常。异常模块定义在 lib/prql/errors.ex:它基于defexception [:message, :error]exception/1回调将message固定为"Error compiling PRQL query",原始错误 JSON 存放在:error字段中。

六、编译器管线的逐层 API

除了端到端的compile/2,绑定还暴露了 PRQL 编译器内部三条管线的逐步调用函数(对应prqlc库的核心编译流程):

Elixir 函数阶段输入输出
prql_to_pl/1PRQL → PL AST(JSON)PRQL 查询字符串{:ok, pl_json}{:error, json}
pl_to_rq/1PL AST(JSON) → RQ(JSON)PL 的 JSON 表示{:ok, rq_json}{:error, json}
rq_to_sql/1RQ(JSON) → SQLRQ 的 JSON 表示{:ok, sql}{:error, json}

每个函数都有对应的!版本(prql_to_pl!/1pl_to_rq!/1rq_to_sql!/1),语义与compile!/2一致:成功返回字符串,失败抛出PRQL.PRQLError

Rust 侧实现(native/prql/src/lib.rs)揭示了这些函数与prqlc库的对应关系:

  • prql_to_plprqlc::prql_to_pl解析出 PL AST 后,再经prqlc::json::from_pl序列化为 JSON;
  • pl_to_rq:先prqlc::json::to_pl反序列化 PL JSON,经prqlc::pl_to_rq生成 RQ,再由prqlc::json::from_rq输出 JSON;
  • rq_to_sql:先prqlc::json::to_rq反序列化 RQ JSON,再调用prqlc::rq_to_sql生成 SQL——代码注释也指出此处"目前只使用默认 Options"。

这套分层 API 对需要自定义编译管线(例如在 PL/RQ 层做分析或转换)的 Elixir 开发者非常有用,可以嵌入自己的中间处理步骤。

七、架构原理:Elixir 如何调用 Rust 编译器

绑定采用标准的 Rustler NIF 架构,整体数据流如下:

  1. Elixir 调用层:lib/prql.ex 的PRQL模块将用户参数结构化为PRQL.Native.CompileOptions,调用PRQL.Native.compile/2等 NIF 函数;
  2. NIF 占位层:lib/prql/native.ex 使用use Rustler, otp_app: :prql声明 NIF 宿主模块。未加载 NIF 时,各函数返回:erlang.nif_error(:nif_not_loaded),保证模块在无原生库环境下仍可加载;
  3. Rust 实现层:native/prql/src/lib.rs 通过#[rustler::nif]标注四个导出函数,并以rustler::init!("Elixir.PRQL.Native")注册到名为Elixir.PRQL.Native的模块;
  4. 返回值编码:Rust 侧把Result<String, prqlc::ErrorMessages>统一转换为Response(一个NifTuple,包含:ok/:error原子与结果字符串),映射为 Elixir 的{:ok, binary()} | {:error, binary()}元组。

值得注意的实现细节:Rust 侧CompileOptions通过#[derive(NifStruct)]#[module = "PRQL.Native.CompileOptions"]声明与 Elixir struct 的映射关系,因此 Elixir 侧struct(CompileOptions, opts)生成的 struct 可以直接作为 NIF 参数传递。另外,该 crate 在wasm目标下被显式排除(#![cfg(not(target_family = "wasm"))]),表明它面向原生环境运行。

八、开发与测试流程

README 的 "Development" 一节给出了本地开发的三个标准步骤:

mix deps.get # 安装 Elixir 依赖 mix compile # 编译工程(含 Rust NIF) mix test # 运行测试

测试工程包含 test/prql_test.exs 与 test/test_helper.exs。其中测试覆盖了两类核心场景:

  • 成功路径PRQL.compile("from customers", signature_comment: false)断言其输出与预期 SQL 完全一致;
  • 错误路径PRQL.compile("invalid", ...)断言返回的{:error, json}Jason.decode后与预期的错误结构逐字段相等。

此外,prql_test.exs中的doctest PRQL会直接执行PRQL模块@doc中的 doctest 示例,确保文档中的用法示例持续可验证。NIF 的构建说明见 native/prql/README.md:NIF 会随项目一起编译,并自动加载进PRQL.Native模块。

九、当前状态与未来规划

绑定 README 明确标注"我们正处于 Elixir 绑定的早期开发阶段"("We are in the early stages of developing Elixir bindings")。从 web/book/src/project/bindings/README.md 的分级看,Elixir 绑定当前属于Unsupported层级:功能可正常工作,但尚未满足 Supported 所需的条件(有维护者、覆盖核心编译函数及对应测试、发布到语言官方包仓库、在Taskfile.yaml提供开发环境引导脚本等)。

README 提及的未来工作包括发布预编译产物,届时 Elixir 项目将无需本地 Rust 工具链即可直接使用 PRQL 编译能力。在此之前,任何使用该绑定的 Elixir 项目都依赖 Rust 工具链与仓库内prqlccrate 的本地编译。

十、小结

本文围绕 PRQL 的 Elixir 绑定,完整覆盖了从依赖安装、PRQL.compile/2基本用法、三大编译选项(target/format/signature_comment)语义、错误处理与compile!/2、编译器逐层 API,到 Rustler NIF 桥接层源码剖析与开发测试流程的全部内容。你可以基于 prqlc/bindings/elixir/lib/prql.ex 的公开 API 直接集成,也可以参考 native/prql/src/lib.rs 理解底层调用链,从而在 Elixir 项目中获得完整的 PRQL 编译能力。

  • 后端

【免费下载链接】prql

PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement

项目地址:https://gitcode.com/gh_mirrors/pr/prql
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 23:17:04

黑翅鸢算法优化CNN-BiLSTM-Attention的客流量预测实战

简介&#xff1a;这是一份基于黑翅鸢算法BKA-CNN-BiLSTM-Attention的客流量预测Matlab实现&#xff0c;面向计算机、电子信息工程、数学等专业的学生&#xff0c;可用于课程设计、期末大作业与毕业设计。代码采用参数化编程&#xff0c;注释清晰&#xff0c;附赠可直接运行的案…

作者头像 李华
网站建设 2026/9/23 23:16:05

佛山壁挂炉维修电话|不点火不供暖就近上门检修|欧米到家客服电话

&#x1f4dd; 文章简介佛山家庭使用壁挂炉时&#xff0c;常见问题包括不点火、不出热水、地暖或暖气片不热、故障代码、水压下降、漏水、风机异响、频繁启停等。欧米到家提供壁挂炉检测、维修、清洗保养、采暖调试及配件更换建议服务&#xff0c;覆盖佛山各区&#xff1a;禅城…

作者头像 李华
网站建设 2026/9/23 23:04:10

SegGIS v2.0视频教程:GIS实战技巧与场景化学习指南

1. 项目概述&#xff1a;SegGIS v2.0视频教程的价值定位SegGIS作为地理信息系统&#xff08;GIS&#xff09;领域的重要工具&#xff0c;其2.0版本在功能扩展和用户体验上都有显著提升。这套视频教程的诞生&#xff0c;源于我观察到大量GIS从业者在面对新版软件时普遍存在的三个…

作者头像 李华
网站建设 2026/9/23 22:58:48

Java实现RTP/RTCP协议栈:国标GB28181与低延迟音视频传输实战

简介&#xff1a;本资源是一套基于Java实现RTP实时音视频传输的完整开发实践包&#xff0c;面向Java中级开发者及多媒体通信学习者&#xff0c;聚焦RTP协议原理落地与jlibrtp库实战应用。压缩包含45个文件&#xff0c;主体为39个Java源码&#xff08;涵盖RTPSession、RTCP报文处…

作者头像 李华