原生 SQL
DSL 覆盖了大量场景,但窗口函数、方言专属函数(jsonb_set、position 等)仍需要直达 SQL。dsl.native 用模板字面量把 SQL 片段嵌进查询,且保持类型(.num/.str/.dt…)。
基本形态:native.num 模板
// ① 模型(完整可运行版见工作区)
import { model, prop, dto, dsl } from '@ts-grm/core'
import { sqlClient } from './infra/sql-client'
const Book = model("Book", "id", class {
id = prop.i64()
name = prop.str(50)
edition = prop.i32()
price = prop.num(10, 2)
})
const BookView = dto.view(Book, c => [c.id, c.name, c.price])
// ② 窗口函数:价格降序排名(row_number)作为派生列
const rows = await sqlClient.createQuery(Book, (q, book) => {
return q.select({
book: book.fetch(BookView),
rank: dsl.native.num `row_number() over(order by ${book.price} desc)`,
})
}).fetchList()
console.log(rows.map(r => `${r.rank}: ${r.book.name}`).join('\n'))
要点:
- 后缀决定类型:
.num→NumExpression,.str/.dt/.pred(谓词)等一族; - 模板内嵌列表达式:
${book.price}就地展开为 SQL 列(BOOK.PRICE),保持参数化与类型检查; - 窗口函数、方言函数、CASE 分支都能这么写。
边界:native 只有表达式族,没有谓词
当前版本 dsl.native 提供 num / str 两种类型的原生表达式(模板返回值类型由后缀决定;.dt 等其余留待扩展)。没有 native.pred——需要原生布尔条件时,用子查询或先把原生片段算成列再比较:
// 用原生列比较:把 CASE/窗口/表达式算成列,再回到既有条件体系
q.where(
dsl.native.num `char_length(${book.name})`.gt(8) // 原生片段 + 列表达式方法
)
模板内嵌 ${} 列卓展已实现表达式节点(生成 SQL 列表达式),模板中裸字面量会按原文写入(数字/字符串标量安全;用户输入务必走参数或先验净),跨数据库的 SQL 无法移植。
使用纪律
- 值一律传列表达式或参数,不要手工拼用户输入(模板内
${...}是受控展开,裸字面量按原文写入——只放数字/常量); native生成的列会影响排序/取形,但对批加载优化(预取合并)的相容性有限——窗口列等场景避免与嵌套集合取形同查,必要时拆两条查询;- 跨数据库的 SQL 用
native无法移植——方言差异建议收敛到「SQL 后端」层。
下一步
单查询能力齐了。最后一节看关联预取:嵌套 .with 背后的 SQL 策略、双语句批加载与 join fetch 的取舍——ORM 性能的真相区。