colinhacks/reylo · 上手攻略

  • 仓库:colinhacks/reylo
  • 链接:https://github.com/colinhacks/reylo
  • 分类:llm-infra
  • 作者:Tom
  • 更新:2026-10-23

三证 checklist(写作前自检)

  • ✅ GitHub URL 已 fetch:README(raw)、package.json、packages/postgres/package.json、官网 reylo.dev/llms.txt(约 137KB Markdown 全量文档)均已实际抓取(fetch 时间 2026-10-23)
  • ✅ 技术锚点已查证:核心技术是 TypeScript content mappers(typescript-go PR #4712),随 TypeScript 7.1 引入,目前 7.1 处于 beta(官方 devblog 公告)
  • ✅ 子模块 anchor:engine/(Rust 引擎,产出 engine.wasm)、packages/postgres|mysql|sqlite(各带一个 engine.wasm)、src/(TS 侧 content mapper + runtime loader)、examples/(11 个可复制工程)
  • ⚠️ 版本口径:npm reylo 最新 0.3.0,发布于 2026-10-22T22:42:37Z(距本文写作不到 12 小时);@reylosql/postgres / mysql / sqlite 同版本 0.3.0,三者是 peerDependency 且必须与 reylo 主包版本严格一致。仓库 main 最新 commit 0a3b121b07d7(2026-10-23,修复 sqlite 多类型值推断)。本文所有命令按 0.3.0 核对
  • ⚠️ overhead 数字缺失:文档未给出类型检查的耗时量级(每个 query prepare + catalog 读取的延迟),属于待补硬字段

这是什么

Reylo 是一个 TypeScript content mapper,让你在 TS 代码里直接 import 一个 .sql 文件,把这个查询的行类型、参数类型、可空性、返回行数全部从你真实的开发数据库推出来。

// get_user.sql
select id, name, email from users where id = :id::int
import getUser from "./get_user.sql";

const user = await getUser.run(db, { id: 1 });
//    ^? const user: { id: number; name: string; email: string | null } | null

关键在于它不做 codegen:没有生成文件、没有 watcher。类型检查时 tsc 直接调用 Reylo 去问数据库「这条查询编译出来是什么形状」。作者把这点包装成「agent 时代的理想形态」——事实只存在于 .sql 一处,agent 改不动类型、也留不下过期的手写行类型,漏改的调用方会被类型检查抓出来。

引擎侧是 Rust:仓库根目录有 Cargo.toml/Cargo.lock,engine/ 目录产出 engine.wasm,随 @reylosql/postgres 等包分发;TS 侧 src/mapper.ts 是 content mapper 入口,src/register.ts 是运行时加载器,src/cli.ts 是 CLI。MIT 许可,作者 Colin McDonnell。

解决什么问题

传统 TS + SQL 的痛点有三个,Reylo 各给一个对应机制:

  1. 手写行类型会腐烂。手写 interface User { id: number; ... } 必然和 schema 漂移。→ Reylo 让类型从 SQL 自己长出来,SQL 改了类型跟着改。
  2. SQL 里的错要等到运行时才发现。列名拼错、参数漏传、表已 drop,都得到线上才炸。→ Reylo 把查询交给你的开发数据库 prepare(不执行),让数据库自己报:Postgres: get_user.sql:1:12 - error reylo6: column "nmae" does not exist (42703),错误定位到行列。
  3. codegen 流程要 watcher、要 commit 生成文件、CI 要额外一步。→ content mapper 把这一步做进 tsc 本身。

它还有一层比较硬的能力:从 catalog 推可空性与基数,而不只是列类型。

快速安装

前置条件三项,都不轻松:

  • TypeScript 7.1(当前 beta)——首个支持 content mappers 的版本
  • Node 24 或更高(package.json 的 engines.node = ">=24")
  • 一个跑着的开发数据库:Postgres、MySQL 8.0.22+,或一个 SQLite 文件
npm install reylo
npm install -D @reylosql/postgres   # 或 @reylosql/mysql / @reylosql/sqlite
npm install -D typescript@beta

⚠️ 三个包的版本必须对齐:主包 0.3.0 的 peerDependencies 写死了 @reylosql/postgres: "0.3.0"、@reylosql/mysql: "0.3.0"、@reylosql/sqlite: "0.3.0",另可选 pg >=8 / postgres >=3 / mysql2 >=3。

核心用法

1. 注册 content mapper(tsconfig.json)

{
  "compilerOptions": { /* ... */ },
  "contentMappers": [
    { "package": "reylo", "extensions": [".sql", ".reylo.yaml", ".reylo.yml"] }
  ]
}

2. 打开外部代码执行(package.json)

{
  "scripts": {
    "typecheck": "tsc --noEmit --runExternalCode"
  }
}

--runExternalCode 是让 tsc 去跑 content mapper 的开关,漏了它 .sql 导入就没有类型。

3. 指向开发数据库(.env)

# .env
DATABASE_URL=postgres://localhost:5432/app   # 或 mysql://root@localhost:3306/app

SQLite 用 file:./app.db,路径相对 package.json。URL 的 scheme 决定方言,不用额外配置。

覆盖与插值用项目根目录的 .reylo.yaml:

direct_url: postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:5432/app
env: [.env.local, .env]     # 默认值 [.env.local, .env],先设值的文件优先,环境变量压过所有文件
strict_params: true          # 默认 true

strict_params: false 会把「推断不出类型的参数」从报错降级为 unknown。

4. 运行时加载(Node)

node --env-file=.env --import reylo/register main.ts

各运行时/框架插件(package.json 的 exports 字段可查全):

目标 接法
Node node --import reylo/register
Bun bunfig.toml 里 preload = ["reylo/register"]
Nub nub.jsonc 里 preload = ["reylo/register"]
Vite vite.config.ts 里 plugins: [reylo()](来自 reylo/vite)
Next.js next.config.ts 里 export default withReylo(nextConfig)(来自 reylo/next)
esbuild / Rspack reylo/esbuild 插件
webpack module.rules 里 { test: /\.sql$/, use: "reylo/webpack" }
Jest transform: { "\\.sql$": "reylo/jest" } + NODE_OPTIONS=--experimental-vm-modules
Cloudflare Workers Vite 插件 + reylo/vite,D1 binding 直接当 client 传

Next.js 的 typecheck 脚本要写成 next typegen && tsc --noEmit --runExternalCode(先跑 next typegen 生成路由类型)。

5. 写查询并调用

-- get_user.sql
select id, name, email from users where id = :id::int
import getUser, { type Row, type Params } from "./get_user.sql";

const user = await getUser.run(db, { id: 1 });

导入的查询对象还提供:

  • .sql —— 编译后发给数据库的 SQL(末尾带 sqlcommenter 风格的 /*file='get_user.sql'*/,Datadog、PlanetScale 之类的监控能按文件分组)
  • .raw —— 未编译的原始文件内容,.source 是它的项目相对路径
  • .runSync() —— 仅 better-sqlite3、node:sqlite、@tursodatabase/database/compat 的 Database、Durable Object 的 ctx.storage.sql 支持
  • .with(db) —— 绑定 client,返回接受参数的函数

6. 返回值的形状由查询本身决定

查询形态 .run() 的返回类型
命中主键/唯一键,或 limit 1 Row \| null
无 group by 的聚合、单行 insert … returning Row
不带 returning 的 insert / update / delete void
其他 Row[]

7. 额外推断能力

  • 可空性:列默认可空,除非 schema 的 NOT NULL、join 语义或 where 判定(如 where nick is not null、where lower(nick) = :nick)把它排除
  • 基数:唯一键匹配 → 一行或 null;聚合 → 恰好一行
  • 联合类型:enum、或 check 约束列出取值的列 → 联合类型
  • JSON:查询自己构造的 JSON、带 JSON Schema CHECK 的 JSON 列,都保留形状

典型适用场景

  • TS 后端 + SQL 查询密集:省掉所有手写行类型与 zod schema 的一层重复。
  • AI agent 写代码的代码库:类型随 SQL 变,agent 改完 SQL 不用去同步类型定义;漏改的调用方 tsc 直接报错。作者自己把这称为「agent-friendly」,还提供了官方 agent skill:npx skills add colinhacks/reylo --skill using-reylo。
  • CI 门禁:起库 → 跑迁移 → DATABASE_URL=... tsc --noEmit --runExternalCode,SQL 写错在 CI 就挂。
  • 多方言团队:同一套写法覆盖 Postgres / MySQL / SQLite,方言差异由引擎和驱动消化。
  • Next.js / Cloudflare Workers / D1:官方 examples 目录里有 11 个可直接复制改造的完整工程(node、bun、nub、vite、nextjs、esbuild、webpack、rspack、jest、cloudflare-workers)。

坑与注意

  1. TypeScript 7.1 还在 beta,这是最大的依赖风险。现象:npm i -D typescript@beta 装到的版本会随 beta 迭代漂移,content mapper 的行为可能变;影响:升级 beta 后类型结果可能悄悄变化,或 API 改名;修复:把 typescript 版本在 package.json 里钉死一个具体 beta 版本(仓库自己用的是 typescript: ^7.1.0-dev.20260918.1),别用 ^beta 这种飘的写法。
  2. 类型检查必须有活着的数据库,且类型检查和构建都跑不了。文档 FAQ 明说「Can I build without a database? No, not yet」。现象:CI 里没起库就 tsc,直接连库失败;影响:本地开发、CI、Docker 构建都得额外起一个库;修复:CI 里固定「起库 → 跑迁移 → typecheck → test」四步,别把 typecheck 放在没有库的 job 里。
  3. 必须指向直连,不能指连接池。文档明确写:指向 pooler(尤其事务模式的 PgBouncer)描述查询会失败。现象:direct_url 填了 PgBouncer 地址,准备查询报错;影响:类型检查随机失败;修复:direct_url 用真实数据库地址,DATABASE_URL 可以继续指池地址。
  4. .sql 有错时,tsc 会压住 TypeScript 侧的报错。文档原话:While a .sql file has an error, tsc holds back the TypeScript errors。现象:一次跑出几十个 TS 错误却只有一个 SQL 错;影响:噪音大、定位偏;修复:先修 SQL,TS 错误会跟着消。
  5. Vite 下 .sql 会被打进浏览器包。文档警告:Vite 把查询打包到它被 import 的任何地方,包括浏览器。现象:SQL 源码(含表名、逻辑)泄漏到前端 bundle;影响:信息泄漏 + 包体膨胀;修复:只在 server 目录(如 .server/)里 import .sql。
  6. 同名列是硬错误。select u.id, p.id, p.title 会报 reylo14: Columns 1 and 2 are both named "id",因为一行对象只保留最后一个。现象:SQL 在数据库里能跑,在 Reylo 里报错;影响:改动影响面比预期大;修复:给重名列加 as 别名。
  7. 参数推断不出来就是错误(strict_params: true 默认)。裸 select :x 或 :x is null 里的参数类型是 unknown,默认报错要求你 cast。现象:随手写个参数就报类型错;影响:初期磨合成本高;修复:给参数加 ::int 之类的 cast;确实想放宽就设 strict_params: false。
  8. MySQL 的错误没有行列定位,整条语句一起报(SQLite 走 :1:1,Postgres 走 :1:12)。现象:MySQL 下错误定位很粗;影响:定位成本高;修复:MySQL 项目优先用较短的语句,或自己先在数据库里跑一遍确认。
  9. SQLite 有一批明确的能力边界(文档「Limits」段):非 STRICT 表的声明类型不管用(所以表要写 strict);你自己注册的函数或扩展函数查不了(no such function: slugify),得在 TS 里对 .run() 的结果后处理;ATTACH 的附加库与 TEMP 表查不了(类型检查只读一个数据库文件);超过 350 层嵌套表达式 SQLite 自己会报「Expression tree is too large」;一个 .sql 文件里写多条语句(迁移脚本)是错误。
  10. 大整数跨驱动行为不一致。Reylo 自己解码结果,但驱动对超过 2^53 的整数处理不同:sqlite3、sqlite、D1、Durable Objects、Bun.sql(未开 safeIntegers)会四舍五入,此时 Reylo 直接抛错;libSQL 默认 intMode 下驱动自己就抛 RangeError。现象:某些驱动下 bigint 列取数报错;影响:换驱动就炸;修复:libSQL 显式设 intMode: "bigint" 或 "string",或在 SQL 里 ::int 收窄。
  11. Bun 1.4.3 上 Bun.sql 有 bug 级差异:含单引号字符串内双引号的查询(如 json_extract('{"a":1}', '$.a'))返回零行。现象:同样的 SQL 在别的驱动正常,在 Bun 上静默返回空;影响:极难排查的静默错误;修复:文档建议把 JSON 文档作为参数传(json_extract(:doc::text, '$.a'))。
  12. 值不是 Date,是 string。日期时间全部以 ISO 8601 文本返回(timestamptz → "2026-10-20T08:00:00.123456Z"),numeric/money → string,json/jsonb → unknown(除非有 JSON Schema CHECK 约束)。现象:原以为拿到 Date 对象,实际是 string;影响:日期比较、算术、序列化都要自己处理;修复:在应用层统一用 Date.parse / Temporal 转换。
  13. Cloudflare D1 的 direct_url 不能写进 .env。文档明确:Wrangler 会把 .env 加载成 Worker secrets。现象:D1 项目类型检查拿不到库;影响:配置看似正确但不生效;修复:把 direct_url 写进 .reylo.yaml,指向 wrangler d1 migrations apply --local 生成的本地文件。
  14. .reylo 目录是缓存。它是 Reylo 对每条查询分析结果的缓存,自带 .gitignore,不要提交,也不要手动改。
  15. TypeScript 7 没有给 ts-loader 提供 JS API。文档在 webpack 一节直接写明:要用 swc-loader 或 babel-loader。现象:webpack 项目沿用 ts-loader 会失败;影响:老配置要改;修复:换 loader 或干脆用 esbuild/Rspack 路线。

与同类对比

  • vs. Kysely / Drizzle:这两个是 TS 侧的查询构造器 + 类型层,SQL 本身还是由你写字符串或用 builder 拼;Drizzle 的类型来自它自己的 schema 定义,不是数据库实时 catalog。Reylo 反过来:SQL 是一等公民,类型由 SQL + 数据库推导。代价是必须连活库、且必须用 TS 7.1 beta。
  • vs. Prisma / TypeORM 的 codegen 路线:它们是「生成文件 + 保持同步」,典型痛点是生成物与 schema 漂移、要 watcher、CI 要额外跑 generate。Reylo 明确以「no codegen」为卖点,代价是把类型检查与数据库可用性绑死了。
  • vs. sqlc / pgtyped:这是最接近的一类。sqlc 从 SQL + schema 生成 Go/Rust/TS 类型,属于 codegen 派;Reylo 用 content mapper 把这件事变成编译期按需询问,不落文件。sqlc 的优势是成熟稳定、不需要 beta 编译器。
  • vs. 手写 zod + 运行时校验:手写类型无成本、无需活库,但会漂移;Reylo 反过来。中间路线是「zod schema + 数据库生成 zod」,但那仍是 codegen。
  • vs. Prisma Client:Prisma 抽象掉了 SQL,你写不出方言特有语法;Reylo 反其道,数据库支持的语法全都能用,代价是方言差异自己扛(文档对 Postgres/MySQL/SQLite 各有一整节差异说明)。

一句话推荐结论

推荐给已经在用 TypeScript 7.1 beta、且愿意为类型检查挂一个活数据库的团队——它把「SQL 里的错在 tsc 阶段暴露」这件事做得相当彻底(方言级错误定位 + 可空性/基数/联合类型推断),但请把三个前置条件当成硬门槛评估:TypeScript 7.1 beta、Node 24+、CI 里必须有库;不满足这三条的团队应该继续用 sqlc 或 Drizzle。