- 后端
【免费下载链接】prql
PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement
prql-dotnet(包内命名prql-net)是 PRQL 官方仓库中为 .NET 生态提供的编译器绑定,它通过一个静态的PrqlCompiler类把 PRQL 查询转译为 SQL,并以net10.0目标框架交付。本文将围绕该绑定的官方文档(见 dotnet.md 与 README.md),完整讲解其安装部署、四个核心编译 API、编译选项与结果模型,并结合仓库源码剖析其背后的 P/Invoke 调用链与内存管理机制,帮助你直接在 .NET 应用中集成 PRQL 编译能力。
一、prql-dotnet 是什么:项目定位与当前状态
根据官方文档,prql-net以net10.0为目标框架提供 PRQL 的 .NET 绑定,核心入口是静态类PrqlCompiler,它提供四个方法:
| 方法 | 作用 | 输出 |
|---|---|---|
Compile | 将 PRQL 字符串一次性编译为 SQL 字符串 | SQL 文本 |
PrqlToPl | 将 PRQL 解析为 PL(管道语言层)AST 的 JSON | PL AST(JSON) |
PlToRq | 解析变量引用、校验函数调用、确定 frame,将 PL 转为 RQ(关系查询层)AST | RQ AST(JSON) |
RqToSql | 将 RQ AST 转译为 SQL 字符串 | SQL 文本 |
这 4 个方法共同覆盖了 PRQL 编译管线的完整阶段(PL → RQ → SQL),每个方法都返回一个携带Output字符串与Messages消息集合的Result对象。
需要特别注意的是其成熟度:官方文档明确说明它"仍处于早期阶段(early stage),尚未发布到 NuGet",当前版本号为0.1.0。在 bindings 总览文档 中,.NET 绑定与 PHP 一起被归类为Nascent(萌芽期)层级——即"正在开发中,可能尚未完全可用",区别于 Java、Elixir、prqlc-c 的 Unsupported 层级,以及 JavaScript、Python、R、Rust 的 Supported 层级。因此,在生产环境采用前应充分评估其成熟度,并欢迎通过贡献来完善它。
二、安装与运行环境准备
2.1 目标框架要求
项目工程文件 PrqlCompiler.csproj 中明确配置:
<TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>即该绑定面向 .NET 10(net10.0),要求你的项目同样基于 .NET 10 或以上版本,并开启隐式 using 与可空引用类型。
2.2 原生库部署:libprqlc_c 的动态加载
这是整个绑定最关键的部署步骤。文档明确指出,编译时libprqlc_c库是**在运行时被动态导入(dynamically imported)**的,因此你需要根据操作系统把对应的原生动态库放到项目的bin输出目录中,与PrqlCompiler.dll及其余编译产物放在一起:
- Linux:
libprqlc_c.so - macOS:
libprqlc_c.dylib - Windows:
libprqlc_c.dll
典型路径为{your_project}/bin/Debug/net10.0/(Release 构建则对应bin/Release/net10.0/)。
从源码看,库名常量定义在 PrqlCompiler.cs:
private const string LibraryName = "libprqlc_c";而在 csproj 中,三个平台的动态库均被配置为"最新时复制到输出目录":
<None Update="libprqlc_c.dll"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> <None Update="libprqlc_c.dylib"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> <None Update="libprqlc_c.so"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None>这意味着:如果你通过源码构建本项目,把对应平台的libprqlc_c动态库文件放进项目目录(与 csproj 同级的约定位置),构建时便会自动复制到输出目录;如果你以手工方式集成,则需要自行完成上述文件放置。缺少该动态库时,运行时LibraryImport将无法解析符号,调用编译方法会直接失败。
2.3 包元数据
虽然尚未发布到 NuGet,csproj 中已经预留了完整的打包信息(PackageId=Prql.Compiler、PackageVersion=0.1.0、PackageLicenseExpression=Apache-2.0、标签prql;sql等),说明发布通道的准备工作已在推进中。当前阶段集成方式以源码引用或本地构建为主。
三、快速上手:第一个编译调用
官方文档给出的最小可用示例如下,它演示了如何编译一条最简单的 PRQL 查询并打印结果:
using Prql.Compiler; var options = new PrqlCompilerOptions { Format = false, SignatureComment = false, }; var result = PrqlCompiler.Compile("from employees", options); Console.WriteLine(result.Output);在测试代码 CompilerTest.cs 中可以验证其输出:当Format = false、SignatureComment = false时,Compile("from employees", ...)恰好得到SELECT * FROM employees。也就是说,PRQL 的from employees会被转译为对employees表的全量选择,输出直接可被数据库执行。
如果你不需要自定义选项,也可以使用单参数重载PrqlCompiler.Compile("from employees"),此时内部会以默认选项(Format=true、SignatureComment=true)编译。
四、编译 API 全景:单步编译与分段管线
PrqlCompiler的四个方法分别对应 PRQL 编译器管线的不同阶段。理解它们的关系,是进阶使用的前提。
4.1 Compile:一步到位
public static Result Compile(string prqlQuery); public static Result Compile(string prqlQuery, PrqlCompilerOptions options);这是最常用的入口:输入 PRQL 源字符串,输出 SQL。从 C 层实现看,prqlc-c 的 compile 函数 本质上是prql_to_pl→pl_to_rq→rq_to_sql三个阶段的串联封装("without converting to JSON between each of the functions"),因此Compile与手动分段调用的结果在语义上完全一致。
4.2 分段 API:PrqlToPl / PlToRq / RqToSql
当需要调试、二次加工或观测中间表示时,可以逐段调用:
// 1) PRQL -> PL AST (JSON) var pl = PrqlCompiler.PrqlToPl("let a = (from employees | take 10)\n\nfrom a | select {first_name}"); // 2) PL AST -> RQ AST (JSON) var rq = PrqlCompiler.PlToRq(pl.Output); // 3) RQ AST -> SQL var sql = PrqlCompiler.RqToSql(rq.Output, new PrqlCompilerOptions());每段输出均为 JSON 文本(PL/RQ AST)或 SQL 文本,输入输出皆为字符串,链路清晰。测试 TestOtherFunctions 使用包含let变量定义与take、select管道的查询验证了这条链路:分段调用(PrqlToPl→PlToRq→RqToSql)与直接Compile得到的Output与Messages完全一致,证明分段 API 与整体 API 结果等价。
4.3 参数与异常约定
从 PrqlCompiler.cs 的 XML 注释与参数校验代码可以看出统一的约定:
prqlQuery为 null 或空字符串时抛出ArgumentException(ParamName为"prqlQuery");options为 null 时抛出ArgumentNullException;PlToRq的空参数名为"plJson",RqToSql为"rqJson";- 编译错误不会以异常形式抛出,而是进入
Result.Messages(详见下文)。
对应测试 CompilerTest.cs 对以上每种异常场景都有回归覆盖,包括Compile("")、PrqlToPl("")、PlToRq("")、RqToSql("", ...)等。
五、编译选项 PrqlCompilerOptions 详解
PrqlCompilerOptions定义在 PrqlCompilerOptions.cs,是一个 C# record,共三个属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Format | bool | true | 是否将生成的 SQL 字符串交给格式化器处理:拆分多行、美化缩进与空格 |
Target | string? | null | 编译目标与方言(dialect) |
SignatureComment | bool | true | 是否在生成的 SQL 之后追加编译器签名注释 |
5.1 Format(SQL 格式化)
默认为true。测试中为了获得紧凑可断言的输出,通常显式关闭:
var options = new PrqlCompilerOptions { Format = false, SignatureComment = false };C 层 Options 结构体 中对应的format字段注释为"Pass generated SQL string through a formatter that splits it into multiple lines and prettifies indentation and spacing",即格式化只影响 SQL 的外观排版,不影响语义。
5.2 Target(方言目标)
默认null。C 层注释说明其默认行为:Default to sql.any, which uses target argument from the query header to determine the SQL dialect——即null时由查询头部的target参数决定方言。显式指定时可使用类似"sql.mssql"的值,测试中正是通过Target = "sql.mssql"将 PRQL 编译为 SQL Server 方言的 SQL(SELECT * FROM employees)。如果你面向其他数据库(如 PostgreSQL、DuckDB、ClickHouse 等),可参考仓库中 dialect 相关实现 所支持的方言命名,并通过Target指定。
5.3 SignatureComment(签名注释)
默认为true,即在生成的 SQL 末尾追加一行形如-- Generated by PRQL compiler version x.y.z的注释,便于追踪 SQL 来源。若需要将输出原样交给下游(如做快照对比或逐字执行),可关闭此项。
5.4 选项如何跨越 FFI 边界
在 NativePrqlCompilerOptions.cs 中,托管选项会被转换为与 C 结构体Options内存布局一致的原生结构体(LayoutKind.Sequential),其中bool转为byte,Target字符串通过Marshal.StringToCoTaskMemUTF8转成 UTF-8 指针,并在finally块中用Marshal.FreeCoTaskMem释放(见 PrqlCompiler.cs),避免内存泄漏。
六、结果模型:Output 与 Messages
所有方法都返回Result(见 Result.cs),它由两个公开成员组成:
Output(string):编译器输出。成功时为 SQL(Compile/RqToSql)或 AST 的 JSON(PrqlToPl/PlToRq);失败时通常为空字符串。Messages(IReadOnlyCollection<Message>):错误、警告与 lint 消息集合。编译失败的信息都在这里,而不是抛出异常。
6.1 Message 结构
每个Message(见 Message.cs)包含六个字段:
| 字段 | 类型 | 说明 |
|---|---|---|
Kind | MessageKind | 消息类型(Error/Warning/Lint),当前仅Error被实现 |
Code | string? | 机器可读的错误标识符,可能为 null |
Reason | string | 错误的纯文本说明 |
Hint | string? | 修复建议,可能为 null |
Span | Span? | 错误在源文件中的字符偏移区间,可能为 null |
Display | string? | 带标注的代码片段(含原因与提示),可能为 null |
Location | SourceLocation? | 错误在源文件中的行列号,可能为 null |
其中:
Span是readonly record struct Span(ulong Start, ulong End)(见 Span.cs),以字符为单位表示偏移;SourceLocation是readonly record struct SourceLocation(ulong StartLine, ulong StartCol, ulong EndLine, ulong EndCol)(见 SourceLocation.cs),表示行列范围。
这两个结构体与 C 层 Span / SourceLocation 一一对应,且源注释注明"Make sure to keep in sync with prqlc::Span"。
6.2 错误处理实战
测试 Compile_ReportsErrorMessages 展示了错误场景:查询from employees | unknown_function col中unknown_function未定义,编译后:
var result = PrqlCompiler.Compile(query); // result.Messages 非空 var message = result.Messages.First(); Assert.Equal(MessageKind.Error, message.Kind); Assert.False(string.IsNullOrEmpty(message.Reason)); Assert.NotNull(message.Span); Assert.NotNull(message.Location); Assert.False(string.IsNullOrEmpty(message.Display));可见错误消息会携带Span、Location、Display(标注了出处的代码片段)等丰富诊断信息,可直接用于在编辑器或日志中定位问题。
6.3 原生内存的自动释放
值得注意的实现细节:Result的构造函数在读完Output与所有Message后,会调用ResultDestroyExtern(P/Invoke 到 C 层的result_destroy,见 Result.cs)释放原生层分配的内存,且这一调用位于finally块中,保证异常路径也不会泄漏。C 层 result_destroy 的文档要求"每个返回CompileResult的调用恰好调用一次result_destroy,不得手动释放任何字段",.NET 端正是按此契约实现的。
七、源码级原理:P/Invoke 与 FFI 调用链
从 PrqlCompiler.cs 可以看到四个原生导出函数通过LibraryImport(源码生成的高性能 P/Invoke)绑定:
[LibraryImport(LibraryName, EntryPoint = "compile", StringMarshalling = StringMarshalling.Utf8)] private static partial NativeResult CompileExtern(string prqlQuery, ref NativePrqlCompilerOptions options); [LibraryImport(LibraryName, EntryPoint = "prql_to_pl", StringMarshalling = StringMarshalling.Utf8)] private static partial NativeResult PrqlToPlExtern(string prqlQuery); [LibraryImport(LibraryName, EntryPoint = "pl_to_rq", StringMarshalling = StringMarshalling.Utf8)] private static partial NativeResult PlToRqExtern(string plJson); [LibraryImport(LibraryName, EntryPoint = "rq_to_sql", StringMarshalling = StringMarshalling.Utf8)] private static partial NativeResult RqToSqlExtern(string rqJson, ref NativePrqlCompilerOptions options);几个关键设计点:
统一采用
StringMarshalling.Utf8。测试 Compile_HandlesNonAsciiInput 专门验证了这一点:查询from employees | filter name == 'Café'编译后输出中仍保留Café。测试注释指出,默认的DllImportANSI 封送会静默损坏非 ASCII 字节,而LibraryImport+ UTF-8 封送可以正确往返——因此在查询中直接使用中文、法文等非 ASCII 文本是安全的。调用链与 C 层一一对应。
compile对应 C 层的 compile 函数,它内部串联了prqlc::prql_to_pl、prqlc::pl_to_rq、prqlc::rq_to_sql三个 Rust 函数;prql_to_pl在 C 层还会经过json::from_pl序列化;pl_to_rq先json::to_pl反序列化再json::from_rq序列化;rq_to_sql先json::to_rq反序列化。整个调用链最终落到prqlccrate 的编译器核心。返回值的内存布局。
NativeResult(见 NativeResult.cs)与 C 层 CompileResult 结构一致:Output指针、Messages指针与MessagesLen长度。Result构造函数遍历消息数组,逐个将NativeMessage反序列化为托管Message,并处理了可空字符串(PtrToUtf8StringIndirect解引用二级指针)与可空结构体指针(IntPtr.Zero判定)等边界情况。
八、版本状态与路线图
官方 README 的 TODO 部分交代了版本策略:当前版本停在0.1.0,是因为 prqlc-c 尚未更新到最新的编译器 API;一旦 prqlc-c 跟进最新 API,.NET 绑定的版本号就可以与整个 PRQL 项目的主版本对齐。
因此使用本绑定时有两点预期管理:
- 绑定版本(0.1.0)与 PRQL 编译器版本目前不同步,两者 API 的对应关系以 prqlc-c 的导出为准;
- 由于处于 Nascent 阶段,未来 API 可能存在破坏性变更(例如
Message字段、选项结构、方法签名等),升级时建议对照 CHANGELOG.md 与绑定源码确认差异。
九、深入阅读:相关文件索引
- 官方绑定文档:dotnet.md(即本文主题文档,内容由 README.md 引入)
- 绑定层级总览:bindings/README.md
- 托管 API 实现:PrqlCompiler.cs、PrqlCompilerOptions.cs、Result.cs、Message.cs
- 数据模型:Span.cs、SourceLocation.cs、MessageKind.cs
- 工程与打包配置:PrqlCompiler.csproj、解决方案 prql-net.sln
- 测试用例:CompilerTest.cs
- C FFI 层(
libprqlc_c的来源):prqlc-c/src/lib.rs,配套头文件 prqlc.h 与 prqlc.hpp - PRQL 编译器核心(PL/RQ/方言):prqlc/src 下的
ir/pl、ir/rq与sql模块
结语
prql-dotnet 为 .NET 开发者打开了一条通往 PRQL 编译能力的通道:无论你是想"一步编译"得到 SQL,还是想通过PrqlToPl→PlToRq→RqToSql的分段管线观测和加工中间 AST,PrqlCompiler都提供了简洁一致的 API 与结构化的诊断消息。部署时只需牢记"把对应平台的libprqlc_c动态库放到输出目录"这一关键前提,并理性看待其 0.1.0 版本与 Nascent 阶段的状态。随着 prqlc-c 跟上最新 API,这个绑定将很快与 PRQL 主版本对齐,值得持续关注与贡献。
- 后端
【免费下载链接】prql
PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement
相关推荐
PRQL PHP 绑定指南:使用 prql-php 通过 FFI 将 PRQL 编译为 SQL
PRQL PHP 绑定指南:使用 prql php 通过 FFI 将 PRQL 编译为 SQL prql php 是 PRQL 编译器在 PHP 生态中的官方绑
后端使用 prql-php:通过 PHP FFI 调用 PRQL 编译器将 PRQL 查询编译为 SQL
使用 prql php:通过 PHP FFI 调用 PRQL 编译器将 PRQL 查询编译为 SQL PRQL(Pipelined Relational Que
后端PRQL Python 绑定(prqlc)完整指南:安装、编译 API 与调试用法
PRQL Python 绑定(prqlc)完整指南:安装、编译 API 与调试用法 PRQL(Pipelined Relational Query Langua
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考