TSts-grm

原生 SQL

DSL 覆盖了大量场景,但窗口函数、方言专属函数(jsonb_setposition 等)仍需要直达 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'))

要点:

  • 后缀决定类型.numNumExpression.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 无法移植。

使用纪律

  1. 值一律传列表达式或参数,不要手工拼用户输入(模板内 ${...} 是受控展开,裸字面量按原文写入——只放数字/常量);
  2. native 生成的列会影响排序/取形,但对批加载优化(预取合并)的相容性有限——窗口列等场景避免与嵌套集合取形同查,必要时拆两条查询;
  3. 跨数据库的 SQL 用 native 无法移植——方言差异建议收敛到「SQL 后端」层。

下一步

单查询能力齐了。最后一节看关联预取:嵌套 .with 背后的 SQL 策略、双语句批加载与 join fetch 的取舍——ORM 性能的真相区。