使用 prql-php:通过 PHP FFI 调用 PRQL 编译器将 PRQL 查询编译为 SQL
2026/9/23 11:11:33 网站建设 项目流程
  • 后端

【免费下载链接】prql

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

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

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任务,它完成三件事:

  1. cargo build --package prqlc-c --release—— 以 release 模式编译prqlc-ccrate;
  2. 把生成的libprqlc_c.*(Linux 下为libprqlc_c.so)与prqlc.h头文件复制到prqlc/bindings/php/lib/目录;
  3. 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。在底层,它并不是一个独立的编译流程,而是prqlToPLplToRQrqToSQL三段式管线的"打包版本"。

对照 prqlc-c 的 C FFI 层,compile的 Rust 实现正是依次调用prqlc::prql_to_plprqlc::pl_to_rqprqlc::rq_to_sql,且省去了中间两次 JSON 序列化开销。这解释了为何 PHP 层提供两种使用方式:日常编译用compile,需要调试中间表示或做二次处理时用后三个方法。

分步调试:prqlToPL / plToRQ / rqToSQL

PRQL 的编译是一条清晰的三段式流水线,对应编译器架构中的三层 IR:

方法输入输出对应 IR 阶段
prqlToPLPRQL 源码字符串PL 序列化为 JSON解析与语义分析后的管道语言(PL)
plToRQPL JSONRQ 序列化为 JSON解析变量引用、校验函数调用、确定 frame 后的关系查询(RQ)
rqToSQLRQ JSONSQL 字符串面向具体方言的 SQL 生成

从 prqlc-c 的 C FFI 层 可以看到三者对应的 C 符号分别为prql_to_plpl_to_rqrq_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 结构体 一一对应:

属性类型默认值作用
formatbooltrue是否对生成的 SQL 进行美化:拆分成多行、优化缩进与空格
target?stringnull(等价于sql.any编译目标方言,如sql.mssqlsql.duckdbsql.postgres
signature_commentbooltrue是否在生成的 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导出compileprql_to_plpl_to_rqrq_to_sqlresult_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-php

build-php负责编译 Rust 动态库并把产物与头文件复制到lib/test-php则在prqlc/bindings/php目录下运行vendor/bin/phpunit tests

测试套件(CompilerTest.php)覆盖了四类断言:

  1. 环境自检extension_loaded("ffi")为真,且lib/下存在动态库与prqlc.h头文件(testFfiExtensionIsLoadedtestPrqlLibraryFileExiststestPrqlHeaderFileExists);
  2. 错误路径:非法查询返回非空messages
  3. 正确性:指定sql.mssql方言编译from employees | take 10得到预期 SQL;
  4. 管线一致性:分步调用prqlToPL → plToRQ → rqToSQL的结果与compile一步调用完全相等。

代码规范

prql-php的 PHP 代码遵循 PSR-12 编码标准,通过 PHP_CodeSniffer 检查:

./vendor/bin/phpcs --standard=PSR12 src tests

从 composer.json 的require-dev可以看到开发依赖为phpunit/phpunit: ^10squizlabs/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

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询