- 后端
【免费下载链接】prql
PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement
导读
PRQL(Pipelined Relational Query Language)是一门面向数据转换的现代语言,它最终会被编译为 SQL。为了让同一份 PRQL 查询能够适配不同的数据库,PRQL 在查询头部提供了prql target:指令用于指定 SQL 方言,同时通过prql version:指令声明查询所使用的语言版本。本文将基于官方文档 web/book/src/project/target.md 并结合 prqlc 编译器源码,系统讲解 Target(编译目标)与 Version(版本)两大机制:如何声明方言、哪些方言受支持、编译器参数与查询头部的优先级关系,以及版本声明如何保护你的查询免受编译器升级的影响。
Target:查询头部的方言声明
PRQL 允许在查询的最顶部声明目标方言(target dialect),编译器会据此生成对应数据库特有的 SQL 风味。声明语法是prql target:sql.<dialect>,位于查询第一行,例如:
prql target:sql.postgres from employees sort age take 10上述查询会被编译为 PostgreSQL 风格的 SQL。同样的查询,只需把方言换成 SQL Server:
prql target:sql.mssql from employees sort age take 10编译器就会输出适配 MSSQL 的SELECT TOP 10之类的写法。
从源码看 Target 的解析链路
从源码层面看,target:是查询头部(query header)的一部分,由 prqlc-parser/src/parser/stmt.rs 中的keyword("prql")解析得到,随后在语义分析阶段被解析进 PL AST 的def节点。在 semantic/mod.rs 的测试用例中可以看到,prql target:sql.mssql version:"0"会被解析为:
def: version: ^0 other: target: sql.mssqltarget与version属于同一头部(header)的两个字段,二者语法结构相同,都形如prql <字段>:<值>。
编译器的Target类型定义在 prqlc/src/lib.rs:
pub enum Target { /// 当为 None 时,方言从查询头部的 target 字段中提取 Sql(Option<sql::Dialect>), }Target的FromStr实现(lib.rs)会识别sql.前缀,sql.any被转换为Sql(None),其余字符串则尝试匹配Dialect枚举,匹配失败会抛出Reason::NotFound错误(命名空间为target)。这也意味着:方言名拼写错误会在编译期直接报错,而不是静默地回退到通用 SQL。
方言支持矩阵
根据 target.md 的说明,方言分为"受支持"(Supported)与"不受支持"(Unsupported)两个等级,这一分级在 prqlc/src/sql/dialect.rs 的Dialect::support_level()方法中有直接对应。
受支持的方言(Supported)
受支持方言尽可能支持全部 PRQL 语言特性,每次提交都会运行测试,项目组会尽力修复 bug。当前列表为:
sql.clickhousesql.duckdbsql.generic—— 文档脚注特别说明:虽然不存在一个名为 "generic" 的真实数据库来测试它,但它仍被视作受支持方言。它对应 dialect.rs 中与Ansi共用一个GenericDialecthandler 的Generic枚举项,也是Dialect的#[default]项。sql.mysqlsql.postgressql.redshiftsql.sqlite
从Dialect枚举(dialect.rs)可以看到,Generic被标注为默认值,这与Options::default()中target: Target::Sql(None)(lib.rs)一致——当查询头部和编译器参数都没有指定方言时,PRQL 默认走通用 SQL 路径。
不受支持的方言(Unsupported)
不受支持方言在编译器中已有实现,但测试覆盖很少甚至没有,部分功能可能存在缺口,项目欢迎社区贡献补齐测试或新增方言:
sql.mssqlsql.ansisql.bigquerysql.snowflakesql.oracle—— 非常早期;目前只保证标识符加引号以适应 Oracle 的大小写折叠规则,将take编译为OFFSET ... FETCH FIRST而非LIMIT,text.contains使用||而非CONCAT。其余大多数语言特性回退到通用 SQL,在 Oracle 上可能无法正确执行。
上述源码注释中关于 Oracle 的描述与文档一致:OracleDialect实现了use_fetch(用OFFSET n ROWS FETCH FIRST n ROWS ONLY做行数限制)、IdentQuotingStyle::AlwaysQuoted(始终加引号)以及table_alias_uses_as() -> false(Oracle 的表别名不允许AS关键字),见 dialect.rs。
方言底层实现:DialectHandler 特性
方言差异在编译器中通过DialectHandlertrait 来抽象(dialect.rs)。Dialect枚举通过handler()方法(dialect.rs)分派到具体的 handler 结构体,例如MsSqlDialect、PostgresDialect、OracleDialect等,Ansi与Generic共用GenericDialect。
该 trait 暴露了一系列可覆写的行为钩子,决定了每个方言的 SQL 生成差异,例如:
| 钩子方法 | 默认行为 | 典型覆写 |
|---|---|---|
use_fetch() | false(使用LIMIT) | MSSQL、Oracle 返回true,改用FETCH子句 |
ident_quote() | "(双引号) | MySQL、ClickHouse 用反引号` |
ident_quoting_style() | 条件引号 | Snowflake、Oracle 用AlwaysQuoted(始终引号) |
has_concat_function() | true(使用CONCAT) | Redshift、SQLite 返回false,回退到||运算符 |
set_ops_distinct() | true | SQLite、MSSQL、Snowflake 返回false |
except_all()/intersect_all() | true | SQLite、MSSQL、DuckDB 返回false |
supports_distinct_on() | false | Postgres、ClickHouse、DuckDB 返回true |
stars_in_group() | true | SQLite 返回false |
translate_chrono_item() | 默认报错(日期格式化需要方言支持) | Postgres/MySQL/MSSQL/ClickHouse/DuckDB/BigQuery/Redshift 各自实现 |
table_alias_uses_as() | true | Oracle 返回false |
requires_order_by_in_window_function() | false | Snowflake 返回true(ROW_NUMBER()等排名函数要求 ORDER BY) |
文件头注释(dialect.rs)说明了设计原则:优先面向通用方言生成,只有通用方言不支持(如 MSSQL 没有LIMIT)或方言专属实现性能更优时才引入方言差异;相应地,生成的 SQL 可能偏冗长,但换来了更简单的翻译器。例如chrono_item_to_strftime(dialect.rs)负责把 chrono 日期格式串转回 strftime 表示,Postgres 的translate_chrono_item(dialect.rs)则把 PRQL 的%Y、%m等规格映射到 PostgreSQL 的YYYY、MM等格式,并处理字面量转义。每个方言 handler 的具体差异都可以在上述源码中找到对应实现,是理解"为什么同一 PRQL 在不同数据库输出不同 SQL"的最佳入口。
Target 优先级:编译器参数 > 查询头部
一个查询的编译目标由两个来源决定:查询头部的prql target:声明,以及传给编译器的 target 参数。编译器参数优先于查询头部声明。
例如下面的 shell 命令,查询内部声明了sql.generic,但prqlc compile命令通过--target选项指定了sql.duckdb,此时sql.duckdb胜出,输出的 SQL 基于 DuckDB 方言:
echo 'prql target:sql.generic from foo' | prqlc compile --target sql.duckdb在 prqlc/src/cli/mod.rs 中,compile子命令的--target参数定义如下:
/// Target to compile to #[arg(short, long, default_value = "sql.any", env = "PRQLC_TARGET")] target: String,默认值正是文档中提到的特殊 targetsql.any,同时支持通过环境变量PRQLC_TARGET注入(对 CI 场景很实用)。
sql.any:让查询头部说了算
如果希望编译器尊重查询头部声明的方言,就需要在编译器选项中显式传入特殊值sql.any:
echo 'prql target:sql.generic from foo' | prqlc compile --target sql.any从 lib.rs 的FromStr实现可以看到,sql.any会被解析为Target::Sql(None),而Sql(None)的语义正是"方言从查询头部提取"。换言之:sql.any不是某种"任意数据库",而是"由查询决定"的占位符。由于--target的默认值就是sql.any,所以默认情况下查询头部总是生效的;只有当你显式传入某个具体方言时,它才会覆盖查询头部。
Target::names()(lib.rs)会生成sql.any加上所有sql.<dialect>组成的列表,CLI 的prqlc list-targets命令即用于展示所有可用编译目标名(见 cli/mod.rs),可以通过它确认当前版本支持的全部方言。
各语言绑定中的 Target 设置
除 CLI 外,各语言绑定也把 Target 作为公开 API 暴露。例如 Python 绑定 prqlc/bindings/prqlc-python/src/lib.rs 与 C 绑定 prqlc/bindings/prqlc-c/src/lib.rs 中都出现sql.any/ 方言字符串的处理;Rust 侧则可以直接用类型安全的枚举,参考 lib.rs 的 doctest:
use prqlc::{compile, Options, Target, sql::Dialect}; let prql = "from employees | select {name,age}"; let opts = Options::default() .with_target(Target::Sql(Some(Dialect::SQLite))) .with_signature_comment(false) .with_format(false); let sql = compile(&prql, &opts).unwrap();Options的默认值为format: true、target: Target::Sql(None)、signature_comment: true(lib.rs),with_target链式方法可覆盖目标方言。
Version:查询版本声明
PRQL 允许在查询头部声明语言版本:
prql version:"0.13.14" from employees这个版本声明有两个作用,其中第一个已经实现,第二个是 PRQL 1.0 的门槛特性:
- 编译器版本下限检查(已实现):如果编译器版本比查询声明的版本更旧,编译器会直接报错。这避免了"查询用了语言新特性、而编译器尚未升级"时产生的令人困惑的错误——与其等编译结果莫名其妙地出错,不如一开始就明确提示版本不匹配。
- 按主版本编译(规划中,未实现):编译器将为查询的主版本编译。这允许语言持续演进而不破坏存量查询,也无需用户同时安装多个版本的编译器。这是 PRQL 1.0 的 gating 特性。
关于版本检查,从语义分析层的测试可以印证:在 semantic/mod.rs 中,version:foo(非合法版本字符串)和version:"25"这类会被拒绝的用例,以及未知方言target:sql.yah的用例,都会在parse_resolve_and_lower阶段直接返回错误,说明 header 中的非法值与非法方言一样会触发编译期报错。
在 PRQL 中查询编译器版本
当前正在使用的编译器版本,可以通过 PRQL 标准库中的特殊函数std.prql.version获取。将函数结果放进一个数组字面量即可查询:
[{version = prql.version}]这个用法在集成测试 prqlc/tests/integration/sql.rs 中有直接验证,测试还展示了它的派生用法derive y = std.prql.version。注意std.prql.version返回的是编译器版本,与查询头部声明的version:是两回事:前者是运行时读取当前编译器,后者是查询作者声明的"最低可用版本"契约。
实战:完整使用组合
综合以上机制,一个典型的跨数据库工作流是:在查询头部写清楚它依赖的方言与版本(作为查询的"元数据"),然后在编译时通过 CLI 参数覆盖目标数据库。例如:
# 查询头部声明 generic,但实际编译给 DuckDB 执行 cat <<'EOF' | prqlc compile --target sql.duckdb prql target:sql.generic version:"0.13.14" from employees sort age take 10 EOF # 列出当前编译器支持的所有 target prqlc list-targets # 通过环境变量指定 target(适合脚本/CI) PRQLC_TARGET=sql.postgres prqlc compile query.prql注意事项:
- 若
--target未指定(默认sql.any),以查询头部的prql target:为准;若指定了具体方言,则以参数为准。 - 方言名必须与 dialect.rs 中的
Dialect枚举(ansi、bigquery、clickhouse、duckdb、generic、mssql、mysql、oracle、postgres、redshift、sqlite、snowflake)一致,拼写错误会在编译期报错。 - 使用
std.prql.version可以读取当前编译器版本,用于诊断版本不匹配问题。 - 生成 SQL 时会附带一行包含 PRQL 编译器版本信息的签名注释(
Options::signature_comment默认为true,可用--hide-signature-comment关闭,见 cli/mod.rs)。
相关资源
- 本文主题的官方文档:web/book/src/project/target.md
- 方言枚举与
DialectHandler特性定义:prqlc/prqlc/src/sql/dialect.rs Target、Options与compile入口:prqlc/prqlc/src/lib.rs- CLI
compile子命令与--target参数:prqlc/prqlc/src/cli/mod.rs - 查询头部解析(
prql关键字):prqlc/prqlc-parser/src/parser/stmt.rs - Header 解析与版本校验测试:prqlc/prqlc/src/semantic/mod.rs
std.prql.version使用示例:prqlc/prqlc/tests/integration/sql.rs- 各语言绑定对 target 的封装:prqlc/bindings
- 后端
【免费下载链接】prql
PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement
相关推荐
phar-io/version版本解析机制详解
phar io/version版本解析机制详解 本文深入解析了phar io/version库的版本解析机制,重点介绍了Version类的结构与设计、版本字符串
开发工具终极Mermaid在线编辑器:如何零代码创建专业图表可视化
终极Mermaid在线编辑器:如何零代码创建专业图表可视化 Mermaid Live Editor是一款功能强大的在线图表编辑器,让您无需编写复杂代码就能创建流
前端开发者工具数据可视化RedisInsight Windows 安装指南:装完就连上库
RedisInsight Windows 安装指南:装完就连上库 如果你不想逐条敲命令去查 Redis 数据,RedisInsight 是官方推出的可视化 Re
数据库客户端桌面应用后端前端数据可视化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考