- 后端
【免费下载链接】prql
PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement
PRQL(Pipelined Relational Query Language)是一种面向数据转换的现代化语言,旨在以管道式、可读性强的语法替代复杂 SQL。prql-php是 PRQL 项目为 PHP 提供的官方绑定(bindings),它通过 PHP FFI(Foreign Function Interface)将 Rust 编写的prqlc编译器暴露给 PHP 应用。本文将以 prqlc/bindings/php/README.md 为骨架,结合 PHP 绑定源码 与底层 C FFI 实现,完整讲解prql-php的环境安装、Compiler类四大编译方法、编译选项与错误消息结构、底层 FFI 调用链,以及面向开发者的构建、测试与代码规范流程。读完本文,你将能够在自己的 PHP 项目中直接编译 PRQL 查询并获得对应的 SQL,并能深入理解这条从 PHP 到 Rust 再到 SQL 的完整链路。
prql-php 是什么
prql-php提供的是prqlcRust crate 的 PHP 绑定。它没有用纯 PHP 重新实现 PRQL 编译器,而是通过 PHP 8 内置的 FFI 扩展直接加载prqlc-c编译产物(一个共享库),从而在 PHP 进程中完成 PRQL → SQL 的编译。
从 php.md 的入口(该文档通过 mdBook 的{{#include}}指令直接引用了 prqlc/bindings/php/README.md)可以看到,prql-php对外暴露的核心是一个Compiler类,包含四个主要方法:
compile:把 PRQL 字符串一步编译为 SQL 字符串;prqlToPL:把 PRQL 编译为 PL(Pipeline Language)中间表示,输出 JSON;plToRQ:把 PL JSON 转换为 RQ(Relational Query)中间表示,输出 JSON;rqToSQL:把 RQ JSON 转换为最终的 SQL 字符串。
需要说明的是,该绑定仍处于早期阶段(README 明确标注 "It's still at an early stage"),尚未发布到 Composer 官方仓库,且项目欢迎社区贡献。因此在实际使用时,需要从本仓库源码自行构建。
环境要求与安装
启用 PHP FFI 扩展
prql-php依赖 PHP 的 FFI 扩展。需要在php.ini配置文件中开启:
ffi.enable = "true"从 composer.json 可以看到库的运行时依赖为"php": "^8.1"和"ext-ffi": "*",即要求 PHP 8.1 及以上版本,并且必须安装ext-ffi扩展。
构建并准备共享库
由于prql-php通过 FFI 加载 Rust 编译产物,使用前必须先在本地构建prqlc-c动态库。项目根目录的 Taskfile.yaml 中定义了build-php任务,它完成三件事:
cargo build --package prqlc-c --release—— 以 release 模式编译prqlc-ccrate;- 把生成的
libprqlc_c.*(Linux 下为libprqlc_c.so)与prqlc.h头文件复制到prqlc/bindings/php/lib/目录; - 在
prqlc/bindings/php下执行composer install安装 PHP 依赖。
在 Compiler.php 的构造函数中可以看到库的加载逻辑:默认从__DIR__ . '/../lib'(即prqlc/bindings/php/lib)查找头文件prqlc.h与共享库,并按操作系统区分动态库文件名:
- Windows:
libprqlc_c.dll - macOS(Darwin):
libprqlc_c.dylib - 其他(Linux 等):
libprqlc_c.so
构造函数支持传入?string $lib_path自定义库所在目录,方便将库部署到其他位置。
快速上手:编译第一条 PRQL 查询
README 给出了最简用法。在项目内安装依赖并构建好共享库后,编写如下 PHP 代码:
<?php use Prql\Compiler\Compiler; $prql = new Compiler(); $result = $prql->compile("from employees"); echo $result->output;compile("from employees")会把这条 PRQL 管道查询编译为 SQL,结果对象的output属性保存生成的 SQL 字符串,直接输出即可。这是最常用的入口:一条 PRQL 查询、一步拿到 SQL。
更完整一点,配合Options使用:
<?php use Prql\Compiler\Compiler; use Prql\Compiler\Options; $options = new Options(); $options->format = false; // 不美化 SQL 格式 $options->signature_comment = false; // 不在 SQL 后追加编译器签名注释 $options->target = "sql.mssql"; // 指定 SQL Server 方言 $prql = new Compiler(); $result = $prql->compile("from employees | take 10", $options); if (count($result->messages) === 0) { echo $result->output; }这条示例并非凭空捏造——它正是 CompilerTest.php 中testCompileWorks测试用例的复刻,测试断言其输出为:
SELECT * FROM employees ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH FIRST 10 ROWS ONLY可以看到take 10在 SQL Server 方言下被翻译为FETCH FIRST 10 ROWS ONLY。
Compiler 类:四个编译方法与分层管线
一步到位的 compile
compile(string $prql_query, ?Options $options = null): Result是最常用的 API。在底层,它并不是一个独立的编译流程,而是prqlToPL、plToRQ、rqToSQL三段式管线的"打包版本"。
对照 prqlc-c 的 C FFI 层,compile的 Rust 实现正是依次调用prqlc::prql_to_pl→prqlc::pl_to_rq→prqlc::rq_to_sql,且省去了中间两次 JSON 序列化开销。这解释了为何 PHP 层提供两种使用方式:日常编译用compile,需要调试中间表示或做二次处理时用后三个方法。
分步调试:prqlToPL / plToRQ / rqToSQL
PRQL 的编译是一条清晰的三段式流水线,对应编译器架构中的三层 IR:
| 方法 | 输入 | 输出 | 对应 IR 阶段 |
|---|---|---|---|
prqlToPL | PRQL 源码字符串 | PL 序列化为 JSON | 解析与语义分析后的管道语言(PL) |
plToRQ | PL JSON | RQ 序列化为 JSON | 解析变量引用、校验函数调用、确定 frame 后的关系查询(RQ) |
rqToSQL | RQ JSON | SQL 字符串 | 面向具体方言的 SQL 生成 |
从 prqlc-c 的 C FFI 层 可以看到三者对应的 C 符号分别为prql_to_pl、pl_to_rq、rq_to_sql,Compiler.php 中的同名方法正是通过 FFI 对这些符号的封装。
CompilerTest.php 中的testOtherFunctions给出了完整的分步用法:先用prqlToPL得到 PL JSON,再用plToRQ得到 RQ JSON,接着用rqToSQL得到 SQL,并断言该结果与直接用compile得到的结果完全一致(assertEquals($via_json, $direct)),从测试层面印证了"分步调用 = 一步调用"的等价性。
Result 与错误消息
四个方法统一返回Result对象,其结构在 Result.php 中定义,包含两个公开属性:
string $output:编译产物(SQL 字符串,或在 PL/RQ 阶段为 JSON 字符串);array $messages:编译过程中产生的消息数组,元素类型为Message。
Message.php 定义了Message的字段:kind(消息类型)、code(机器可读的错误标识)、reason(纯文本错误说明)、hint(修复建议)、span(源码中的字符偏移区间)、display(带注解的代码片段)、location(行列号)。消息类型为 MessageKind.php 中的枚举Error/Warning/Lint,其中Error是目前唯一被实际实现并产出的类型(见 C 层注释 "Currently only Error is implemented")。
在 Compiler.php 的convertResult/convertMessage中可以看到,PHP 层会把 C 结构体逐字段转换为 PHP 对象;Span(Span.php,字符偏移起止)与SourceLocation(SourceLocation.php,起止行列)在指针为空时映射为null。
一个典型的错误处理写法:
<?php use Prql\Compiler\Compiler; $prql = new Compiler(); $result = $prql->compile("invalid"); foreach ($result->messages as $message) { echo $message->reason, PHP_EOL; if ($message->location !== null) { echo "at line {$message->location->start_line}, col {$message->location->start_col}", PHP_EOL; } }对应测试 testInvalidQuery 正是断言对"invalid"这种非法查询会返回恰好 1 条消息。
Options 编译选项详解
Options类(Options.php)控制 SQL 生成阶段的三个行为,与 C FFI 层 Options 结构体 一一对应:
| 属性 | 类型 | 默认值 | 作用 |
|---|---|---|---|
format | bool | true | 是否对生成的 SQL 进行美化:拆分成多行、优化缩进与空格 |
target | ?string | null(等价于sql.any) | 编译目标方言,如sql.mssql、sql.duckdb、sql.postgres等 |
signature_comment | bool | true | 是否在生成的 SQL 末尾追加编译器签名注释 |
target的语义在 C 层有明确说明:默认值sql.any表示"根据查询头(query header)中的target参数自动决定 SQL 方言"。从 prqlc-c 的 convert_options 可以看到,PHP 传入的target字符串最终会通过Target::from_str解析为 Rust 侧的方言枚举,解析失败会直接产生编译错误。
PHP 侧的optionsInit方法(Compiler.php)负责把Options对象拷贝为 C 结构体,并在optionsDestroy中释放target字符串占用的 FFI 内存——这是使用 FFI 时需要注意的 C 侧内存管理细节,库已经帮你处理妥当。
底层原理:PHP 到 Rust 的 FFI 调用链
prql-php之所以能"零成本"复用 Rust 编译器,靠的是prqlc-c这个 C ABI 中间层。整条链路可以概括为:
PHP 调用 Compiler::compile() → PHP FFI::cdef() 绑定 libprqlc_c 动态库 → C 符号 compile()(Rust #[no_mangle] 导出) → prqlc::prql_to_pl() → prqlc::pl_to_rq() → prqlc::rq_to_sql() → CompileResult 结构体(output + messages 数组) → PHP convertResult() 转换为 Result 对象几个关键设计点:
- C ABI 导出:
prqlc-c使用#[no_mangle] pub unsafe extern "C" fn导出compile、prql_to_pl、pl_to_rq、rq_to_sql、result_destroy等符号,见 prqlc-c/src/lib.rs; - 头文件驱动绑定:PHP 侧通过
\FFI::cdef($header_source, $library)解析 prqlc.h 头文件来声明函数与结构体布局,因此库目录中必须同时存在头文件与动态库; - 内存所有权:Rust 侧分配的内存由
result_destroy统一释放(lib.rs#L199-L245),PHP 侧在每次调用后立即调用它,避免泄漏。文档注释明确要求"每个返回CompileResult的函数调用后,result_destroy必须恰好被调用一次"; - 字符串传递:Rust 假设输入是零终止字符串(
*const c_char),PHP 侧由 FFI 自动处理转换。
开发指南:环境、构建、测试与代码规范
搭建开发环境
README 推荐使用 nix flake 快速获得带 PHP、ext-ffi与 Composer 的开发环境。首先在 nix 中启用实验性的 flakes 特性:
mkdir -p ~/.config/nix echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf然后在prqlc/bindings/php/目录内进入开发 shell:
nix shell github:loophp/nix-shell#env-php81 --impure该 flake 会自动带入ext-ffi扩展——正如 composer.json 中require声明的"ext-ffi": "*"所要求的那样。当然,如果你已经有配置好ext-ffi的 PHP 8.1+ 环境,也可以跳过 nix,直接在本地执行composer install。
构建与测试
项目的 Taskfile.yaml 提供了两个与 PHP 绑定相关的任务:
task build-php task test-phpbuild-php负责编译 Rust 动态库并把产物与头文件复制到lib/,test-php则在prqlc/bindings/php目录下运行vendor/bin/phpunit tests。
测试套件(CompilerTest.php)覆盖了四类断言:
- 环境自检:
extension_loaded("ffi")为真,且lib/下存在动态库与prqlc.h头文件(testFfiExtensionIsLoaded、testPrqlLibraryFileExists、testPrqlHeaderFileExists); - 错误路径:非法查询返回非空
messages; - 正确性:指定
sql.mssql方言编译from employees | take 10得到预期 SQL; - 管线一致性:分步调用
prqlToPL → plToRQ → rqToSQL的结果与compile一步调用完全相等。
代码规范
prql-php的 PHP 代码遵循 PSR-12 编码标准,通过 PHP_CodeSniffer 检查:
./vendor/bin/phpcs --standard=PSR12 src tests从 composer.json 的require-dev可以看到开发依赖为phpunit/phpunit: ^10与squizlabs/php_codesniffer: ^3.7。库的自动加载遵循 PSR-4:命名空间Prql\Compiler\映射到src/目录。
现状、限制与适用场景
- 成熟度:README 明确说明绑定仍处于早期阶段,尚未发布到 Composer 官方仓库,因此不能直接
composer require prql/compiler,需要基于本仓库自行构建并引入; - 平台限制:需要 PHP 8.1+、
ext-ffi扩展,以及对应平台的libprqlc_c动态库(Linux.so/ macOS.dylib/ Windows.dll); - 典型场景:在 PHP 应用中动态生成 SQL(例如把业务上易于表达、维护的 PRQL 管道编译成 SQL 交给数据库执行),或借助
prqlToPL/plToRQ调试、检查 PRQL 的中间表示; - 进一步阅读:绑定目录的总体说明见 web/book/src/project/bindings/README.md,其他语言的绑定实现(C、Python、JS、Java、.NET、Elixir)位于 prqlc/bindings 下,可作为横向对比参考。
总而言之,prql-php用不到两百行 PHP 代码,通过 FFI 把完整的prqlc编译器带进了 PHP 生态:一条 PRQL 管道、一个Compiler实例,就能在你的 PHP 应用中产出跨方言的 SQL。
- 后端
【免费下载链接】prql
PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement
相关推荐
深入PRQL编译器架构:从源码到SQL的魔法转换
深入PRQL编译器架构:从源码到SQL的魔法转换 PRQL编译器采用分层架构设计,将查询转换过程分解为解析、语义分析和SQL生成三个阶段。本文详细解析了PRQL
后端PRQL 语言与编译器深度解析:基于 prql 仓库的管道式 SQL 替代方案实战指南
PRQL 语言与编译器深度解析:基于 prql 仓库的管道式 SQL 替代方案实战指南 PRQL( P ipelined R elational Q uery
后端PRQL性能优化技巧:10个方法提升你的查询编译和执行效率
PRQL性能优化技巧:10个方法提升你的查询编译和执行效率 PRQL(Pipelined Relational Query Language)是一个现代化的数据
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考