- 后端
【免费下载链接】prql
PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement
PRQL(Pipelined Relational Query Language)是一种现代化的数据转换语言,旨在成为 SQL 的简单、强大的流水线式替代品。本指南以仓库中 minimal-cpp 示例 为核心,完整讲解如何在 C++ 程序中通过prqlc-c这一 C/C++ FFI 绑定,把 PRQL 查询编译为 SQL。读完本文,你将掌握从环境准备、Makefile 链接配置到compile/result_destroy等关键 API 调用的完整实战链路,并理解其底层编译流水线与内存管理约定。
一、示例概述:一条命令跑通 C++ 调 PRQL
minimal-cpp 示例 是 PRQL 仓库中面向 C++ 开发者的最小可用示例。它本身只有三部分内容:一个 C++ 源文件 main.cpp、一份 Makefile 以及一段极其精简的使用说明:
A minimal example for using prqlc-c with `gcc` and `make`. ## How to run make run也就是说,官方给出的运行方式只有一行命令make run。其背后的完整工作流是:Makefile 先通过 Cargo 构建 Rust 侧静态库libprqlc_c.a,再用g++编译链接示例程序并直接执行。下面各节将逐层拆解这条命令背后发生的每一件事。
二、main.cpp 代码逐行解读
完整源码位于 main.cpp,全文仅 24 行,核心逻辑如下:
#include <cstring> #include <iostream> #include "prqlc.hpp" using namespace prqlc; void print_result(CompileResult res) { if (strcmp(res.output, "") == 0) { std::cout << "Output: <empty>\n\n"; } else { std::cout << "Output:\n\n" << res.output; } } int main() { const auto prql_query = "from albums | select {album_id, title} | take 3"; CompileResult res = compile(prql_query, nullptr); print_result(res); result_destroy(res); return 0; }可以提炼出调用 FFI 的四个标准步骤:
- 引入头文件:通过
#include "prqlc.hpp"引入由 cbindgen 自动生成的 C++ 头文件(using namespace prqlc;使其所有类型可直接使用)。 - 构造 PRQL 查询字符串:示例使用
"from albums | select {album_id, title} | take 3",即从albums表选取album_id与title两列并取前 3 行——一个典型的 PRQL 流水线。 - 调用
compile:CompileResult res = compile(prql_query, nullptr);。第二个参数传nullptr表示使用编译器的默认选项。 - 释放结果:
result_destroy(res);归还 FFI 侧分配的堆内存。
输出判断用strcmp(res.output, "") == 0来识别"空输出"(编译失败时output为空字符串,详细错误在messages中)。注意最小示例只打印输出、不打印错误消息,更完整的错误处理写法见后文第六节。
三、Makefile 深度拆解:链接静态库的完整参数
Makefile 是这个示例中信息量最大的部分,它展示了"C++ + prqlc-c"的标准构建与链接方式:
PRQL_PROJECT=../../../../.. run: build ./main.out build-prql: cargo build --package prqlc-c --release UNAME_S := $(shell uname -s) LD_FLAGS = -L${PRQL_PROJECT}/target/release \ ${PRQL_PROJECT}/target/release/libprqlc_c.a ifeq ($(UNAME_S),Darwin) LD_FLAGS := $(LD_FLAGS) -framework CoreFoundation endif build: main.cpp build-prql g++ main.cpp -o main.out \ -I${PRQL_PROJECT}/prqlc/bindings/prqlc-c \ $(LD_FLAGS) valgrind: build valgrind ./main.out逐项说明:
PRQL_PROJECT=../../../../..:从prqlc/bindings/prqlc-c/examples/minimal-cpp/向上回溯 5 级到仓库根目录,作为定位 Rust 构建产物和头文件的基准路径。cargo build --package prqlc-c --release:以 release 模式构建prqlc-ccrate。根据 prqlc-c/Cargo.toml 中的crate-type = ["staticlib", "cdylib"],一次构建会同时产出静态库libprqlc_c.a与动态库libprqlc_c.so(macOS 上为.dylib),产物位于仓库根目录的target/release/下。-L${PRQL_PROJECT}/target/release:把静态库所在目录加入链接器搜索路径。${PRQL_PROJECT}/target/release/libprqlc_c.a:直接以完整路径显式链接静态库,绕开-l的命名查找规则。-I${PRQL_PROJECT}/prqlc/bindings/prqlc-c:头文件搜索路径,使#include "prqlc.hpp"能够命中 prqlc.hpp。- macOS 特殊处理:
ifeq ($(UNAME_S),Darwin)时追加-framework CoreFoundation。这是因为 prqlc 依赖 Rust 的core-foundation系系统库,macOS 上链接静态库必须显式带上该 framework。 valgrind目标:在构建完成后用 Valgrind 运行./main.out,用于检测内存泄漏——这与下文第五节的内存管理要求直接呼应。
在 Linux 上,完整的等价命令可还原为(假定仓库根目录为$PRQL_PROJECT):
cargo build --package prqlc-c --release g++ main.cpp -o main.out \ -I${PRQL_PROJECT}/prqlc/bindings/prqlc-c \ -L${PRQL_PROJECT}/target/release \ ${PRQL_PROJECT}/target/release/libprqlc_c.a ./main.out四、FFI 核心 API:五个导出函数与三个数据结构
prqlc-c的全部 FFI 表面由 src/lib.rs 定义,并通过 cbindgen 同步生成 C/C++ 两个头文件 prqlc.h 与 prqlc.hpp。C++ 侧可见的 API 全部位于namespace prqlc中,共有 5 个导出函数:
| 函数 | 签名(C++ 侧) | 作用 |
|---|---|---|
compile | CompileResult compile(const char *prql_query, const Options *options) | 一步完成 PRQL → SQL 的完整编译,options 可传nullptr |
prql_to_pl | CompileResult prql_to_pl(const char *prql_query) | PRQL 源码 → PL AST(JSON 序列化输出) |
pl_to_rq | CompileResult pl_to_rq(const char *pl_json) | PL JSON → RQ AST(JSON 序列化输出) |
rq_to_sql | CompileResult rq_to_sql(const char *rq_json, const Options *options) | RQ JSON → SQL 字符串 |
result_destroy | void result_destroy(CompileResult res) | 释放CompileResult占用的全部堆内存 |
从 lib.rs 中compile的实现 可以看到,它本质上是后三个阶段的无 JSON 中转封装:
let result = options .and_then(|opts| { Ok(prql_query.as_str()) .and_then(prqlc::prql_to_pl) .and_then(prqlc::pl_to_rq) .and_then(|rq| prqlc::rq_to_sql(rq, &opts.unwrap_or_default())) }) .map_err(|e| e.composed(&prql_query.into()));即一条调用链:prql_to_pl → pl_to_rq → rq_to_sql,对应 PRQL 编译器经典的 PL(流水线 AST)→ RQ(关系代数 AST)→ SQL 三阶段设计。
4.1 Options:编译选项结构体
Options是唯一需要 C++ 侧手动填充的结构体,其三个字段及默认值(prqlc.hpp 中的文档注释)为:
struct Options { bool format; // 是否对生成的 SQL 做美化格式化(多行、缩进、间距),默认 true char *target; // 目标 SQL 方言,默认 "sql.any"(由查询头决定方言) bool signature_comment; // 是否在生成的 SQL 尾部附加编译器签名注释,默认 true };对照 lib.rs 中convert_options的实现可以发现两个细节:
target传nullptr或空字符串时会被归一化为"sql.any";Options各字段会透传给prqlc::Options,因此传入nullptr与传入"全默认字段"的Options行为等价。
target可取如sql.mssql、sql.duckdb、sql.postgres等方言标识,可用于跨数据库方言的编译输出。
4.2 CompileResult:编译结果结构体
struct CompileResult { const char *output; // 编译输出:成功时为 SQL(或 PL/RQ 的 JSON),失败时为空字符串 const Message *messages; // 错误/警告消息数组,无消息时为 nullptr size_t messages_len; // 消息条数 };编译成功时output持有结果、messages_len为 0;编译失败时output为空、messages指向Message数组(见 result_into_c_str 实现)。Message结构体还包含机器可读错误码code、纯文本reason、修复建议hint、带上下文的display、字符偏移span以及行列位置location,可用于构造友好的错误报告。
五、内存管理约定:为什么必须调用 result_destroy
这是使用 prqlc-c 最容易踩坑的地方,官方在头文件与源码中反复强调。所有返回CompileResult的函数(compile、prql_to_pl、pl_to_rq、rq_to_sql)都会在 Rust 侧堆上分配内存(字符串、消息数组、Options 等),因此:
- 每个
CompileResult必须且只能调用一次result_destroy,且不得手动free其任何字段(见 prqlc.hpp 的 Safety 说明); - 输入字符串要求是0 结尾的 C 字符串,Rust 侧通过
CStr::from_ptr读取(lib.rs); - 从 result_destroy 的实现 可见,它会递归释放每条消息的
code、reason、hint、span、display、location,再释放消息数组和output字符串。因此print_result必须在result_destroy之前完成对res.output的读取——这正是最小示例的调用顺序。
示例 Makefile 中内置的valgrind目标(valgrind ./main.out)即用于验证这类内存生命周期是否正确。在编写自己的代码时,建议像 C++ 的 RAII 习惯那样,把result_destroy放入析构/延迟释放逻辑(例如 Zig 示例中的defer prql.result_destroy(result),见 minimal-zig 的 main.zig),确保所有返回路径都会释放。
六、从最小示例到生产用法:错误处理与自定义选项
minimal-cpp 只演示了最简路径,同目录的 minimal-c/main.c(C 语言版)则补齐了另外三个关键用法,C++ 侧可完全照搬同样的模式:
1. 遍历并打印错误消息
for (size_t i = 0; i < res.messages_len; i++) { Message const *e = &res.messages[i]; if (e->display != NULL) { printf("%s", *e->display); } else if (e->code != NULL) { printf("[%s] Error: %s\n", *e->code, e->reason); } else { printf("Error: %s", e->reason); } }e->code与e->display是"指向指针的指针"(const char *const *),指向 C 字符串指针数组,因此需要解引用一层再按字符串打印。
2. 自定义 Options(以 SQL Server 方言为例)
Options opts; opts.format = false; // 关闭格式化 opts.signature_comment = false; // 关闭签名注释 opts.target = "sql.mssql"; // 输出 SQL Server 方言 res = compile(prql_query, &opts);注意 C++ 侧Options.target类型为char *,传字符串字面量时需要自行保证其生命周期覆盖compile调用期间。
3. 使用分阶段 API 查看中间 AST
res = prql_to_pl(prql_query); // 得到 PL JSON res2 = pl_to_rq(res.output); // 将 PL JSON 转 RQ JSON rq_to_sql(res2.output, NULL); // 最终转 SQL先result_destroy(res)再复用,可避免提前释放被下游消费的output缓冲区。这套分阶段接口非常适合调试编译管线或在 JSON 层做自定义处理。
七、prqlc.hpp 从哪来:cbindgen 与头文件再生成
prqlc.hpp 与 prqlc.h 均以/* This file is autogenerated. Do not modify this file manually. */开头,由 cbindgen 从 src/lib.rs 自动生成。若需在修改 FFI 后重新生成头文件,官方 README(prqlc-c/README.md)与根目录 Taskfile.yaml 给出了标准做法:
task build-prqlc-c-header对应 Taskfile 中的实际命令是:
cbindgen --crate prqlc-c --output prqlc.h cbindgen --crate prqlc-c --lang C++ --output prqlc.hpp日常开发中无需手动生成,直接使用仓库内已有的头文件即可。
八、迁移到其他构建系统与语言
prqlc-c不只服务于 C/C++。由于它编译为标准的 C ABI 静态/动态库,任何支持 FFI 的语言都可复用同一套 API。prqlc-c/README.md 给出了在其他构建系统中链接静态库的通用配方:
CGO_LDFLAGS="-L/path/to/target/release -lprqlc_c -pthread -ldl -lm" go build即链接时除-lprqlc_c外还需带上其系统依赖-pthread -ldl -lm(macOS 上再加-framework CoreFoundation)。仓库内还提供了另外两个对照实现:
- minimal-c:功能最全的 C 示例(错误处理、自定义 Options、分阶段 API 全覆盖);
- minimal-zig:通过 Zig 的
@cImport直接引用prqlc.h。
如果你的应用需要嵌入 PRQL 编译能力,推荐以 minimal-cpp 为起点跑通"构建 → 链接 → 调用 → 释放"全流程,再逐步引入第六节中的错误处理与自定义方言选项。
九、小结:掌握的最小可用链路
回顾整条链路:make run一次执行背后是Cargo 构建静态库 → g++ 编译链接 → 运行可执行文件三个步骤;程序内部则是compile一行调用 → 三阶段编译流水线 →print_result读取输出 →result_destroy释放内存。理解这四点,即可把 PRQL 的编译能力稳定嵌入到任意 C++ 项目中,并在此基础上平滑迁移到错误处理、自定义方言乃至分阶段 AST 调试等进阶用法。
- 后端
【免费下载链接】prql
PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement
相关推荐
使用 prql-php:通过 PHP FFI 调用 PRQL 编译器将 PRQL 查询编译为 SQL
使用 prql php:通过 PHP FFI 调用 PRQL 编译器将 PRQL 查询编译为 SQL PRQL(Pipelined Relational Que
后端在 PostgreSQL 中用 PRQL 编写查询:PL/PRQL 扩展与 prqlc 方言支持实战指南
在 PostgreSQL 中用 PRQL 编写查询:PL/PRQL 扩展与 prqlc 方言支持实战指南 本篇技术指南围绕 PRQL 项目官方文档中 Postg
后端PRQL Elixir Bindings:在 Elixir 中编译 PRQL 查询为 SQL 的完整指南
PRQL Elixir Bindings:在 Elixir 中编译 PRQL 查询为 SQL 的完整指南 PRQL(Pipelined Relational Q
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考