CTE 与 Window 函数(v1.1)
v1.1.0 引入了对 **CTE(Common Table Expressions)** 和 **Window 函数** 的支持,通过 `linq!` 宏提供类型安全的语法糖,同时保留 raw SQL 模式以应对复杂场景。
CTE 语法糖(推荐)
使用 `linq!` 宏的 `with ... as |e: T| ...; from name` 语法,以闭包表达式定义 CTE 主体:
let sql = linq!(
ctx.set::<CteEmployee>();
with high_earners as |e: CteEmployee| e.salary > 85_000;
from high_earners
)
.to_sql();
生成的 SQL:
WITH high_earners AS (SELECT * FROM "cte_employees" WHERE "salary" > ?)
SELECT * FROM high_earners
复合 WHERE 条件
CTE 闭包体支持任意 `BoolExpr`,包括 `&&`、`||`、`!` 等:
let sql = linq!(
ctx.set::<CteEmployee>();
with eng_high as |e: CteEmployee| e.salary > 85_000 && e.dept == "Engineering";
from eng_high
)
.to_sql();
多 CTE 组合
可定义多个 CTE 并在主查询中引用:
let sql = linq!(
ctx.set::<CteEmployee>();
with high_eng as |e: CteEmployee| e.dept == "Engineering" && e.salary > 85_000;
with sales_low as |e: CteEmployee| e.dept == "Sales" && e.salary < 85_000;
from high_eng
)
.to_sql();
CTE 执行
CTE 语法糖生成的查询可直接执行:
let rows = linq!(
ctx.set::<CteEmployee>();
with high_earners as |e: CteEmployee| e.salary > 85_000;
from high_earners
)
.to_list()
.await?;
> **PostgreSQL 注意**:typed 模式自动使用 provider 原生占位符(`$N`),确保 PostgreSQL 占位符连续正确。raw 模式使用 `?` 占位符,在 PostgreSQL 上可能不兼容——推荐使用 typed 模式。
CTE Raw 模式
对于无法用闭包表达的复杂 CTE,可使用 raw 模式:
use rust_ef::provider::DbValue;
let rows = ctx
.set::<CteEmployee>()
.query()
.with_cte_internal(
"high_earners",
"SELECT * FROM cte_employees WHERE salary > ?",
vec![DbValue::Integer(85_000)],
vec!["emp_id", "name", "dept", "salary"],
)
.from_cte("high_earners")
.to_list()
.await?;
> **限制**:raw 模式的预编译 SQL 使用 `?` 占位符,在 PostgreSQL 上不会转换为 `$N`。生产环境推荐使用 typed 模式(`linq!(with ...)` 语法糖)。
Window 函数
`linq!` 宏的 `window` 子句支持 10 种窗口函数:
| 函数 | 说明 | SQL | |------|------|-----| | `row_number` | 行号 | `ROW_NUMBER()` | | `rank` | 排名(同值跳号) | `RANK()` | | `dense_rank` | 稠密排名(同值不跳号) | `DENSE_RANK()` | | `lag` | 前一行值 | `LAG(...)` | | `lead` | 后一行值 | `LEAD(...)` | | `sum` | 累计求和 | `SUM(...)` | | `count` | 计数 | `COUNT(...)` | | `avg` | 平均值 | `AVG(...)` | | `min` | 最小值 | `MIN(...)` | | `max` | 最大值 | `MAX(...)` |
基本语法
window <func> [<field>] partition_by <expr> order_by <expr> [asc|desc] as <alias>
ROW_NUMBER 示例
按部门分区,按薪资降序排名:
let sql = linq!(
ctx.set::<WinEmployee>(),
|e: WinEmployee| e.emp_id > 0;
window row_number partition_by e.dept order_by e.salary desc as rn
)
.to_sql();
生成的 SQL:
SELECT *, ROW_NUMBER() OVER (PARTITION BY "dept" ORDER BY "salary" DESC) AS "rn"
FROM "win_employees"
WHERE "emp_id" > ?
SUM 窗口聚合
计算每个部门的薪资总额:
let sql = linq!(
ctx.set::<WinEmployee>();
window sum e.salary partition_by e.dept as dept_total
)
.to_sql();
执行 Window 查询
let rows = linq!(
ctx.set::<WinEmployee>();
window row_number partition_by e.dept order_by e.salary desc as rn
)
.to_list()
.await?;
> **注意**:Window 函数投影列通过 `SELECT *` 附加在结果集中,`from_row` 仅读取实体字段,窗口函数列被忽略。如需读取窗口函数结果,使用 `select` 子句获取原始行。
IN / NOT IN 子查询
v1.1.0 同时支持 `IN` / `NOT IN` 标量子查询,通过 `in_subquery` 语法表达:
// 查询有文章的 Blog
let blogs = linq!(
ctx.set::<Blog>(),
|b: Blog| b.blog_id.in_subquery(|p: Post| p.blog_id)
)
.to_list()
.await?;
生成的 SQL:
SELECT * FROM "blogs"
WHERE "blog_id" IN (SELECT "blog_id" FROM "posts")
NOT IN 子查询
// 查询没有文章的 Blog
let blogs = linq!(
ctx.set::<Blog>(),
|b: Blog| !b.blog_id.in_subquery(|p: Post| p.blog_id)
)
.to_list()
.await?;
与其他条件组合
let blogs = linq!(
ctx.set::<Blog>(),
|b: Blog| b.rating > 3 && b.blog_id.in_subquery(|p: Post| p.blog_id)
)
.to_list()
.await?;
设计要点
| 实践 | 说明 | |------|------| | **优先使用 typed 模式** | `linq!(with ...)` 语法糖自动处理占位符方言,三库通用 | | **raw 模式仅用于复杂场景** | 闭包无法表达的 CTE 才用 `with_cte_internal` | | **Window 列不被 from_row 读取** | 需要窗口函数结果时用 `select` 获取原始行 | | **PostgreSQL 占位符** | typed 模式自动用 `$N`,raw 模式用 `?`(PG 不兼容) | | **SQLite 版本要求** | CTE 需 3.8.3+,Window 函数需 3.25+ |
限制
- **raw CTE PostgreSQL 兼容性**:`with_cte_internal()` 的 SQL 使用 `?` 占位符,在 PostgreSQL 上不转换为 `$N`。推荐 typed 模式 - **Window 投影列读取**:`from_row` 忽略窗口函数列,需用 `select` 子句获取原始行 - **CTE 嵌套**:当前不支持 CTE 内嵌套 CTE,每个 `with` 子句独立编译
上一节:[JOIN 查询](join-queries.md)
下一节:[全局查询过滤器](global-query-filters.md)