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 最新 commit0a3b121b07d7(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 各给一个对应机制:
- 手写行类型会腐烂。手写
interface User { id: number; ... }必然和 schema 漂移。→ Reylo 让类型从 SQL 自己长出来,SQL 改了类型跟着改。 - SQL 里的错要等到运行时才发现。列名拼错、参数漏传、表已 drop,都得到线上才炸。→ Reylo 把查询交给你的开发数据库 prepare(不执行),让数据库自己报:
Postgres: get_user.sql:1:12 - error reylo6: column "nmae" does not exist (42703),错误定位到行列。 - 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)。
坑与注意
- 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这种飘的写法。 - 类型检查必须有活着的数据库,且类型检查和构建都跑不了。文档 FAQ 明说「Can I build without a database? No, not yet」。现象:CI 里没起库就
tsc,直接连库失败;影响:本地开发、CI、Docker 构建都得额外起一个库;修复:CI 里固定「起库 → 跑迁移 → typecheck → test」四步,别把 typecheck 放在没有库的 job 里。 - 必须指向直连,不能指连接池。文档明确写:指向 pooler(尤其事务模式的 PgBouncer)描述查询会失败。现象:
direct_url填了 PgBouncer 地址,准备查询报错;影响:类型检查随机失败;修复:direct_url用真实数据库地址,DATABASE_URL可以继续指池地址。 .sql有错时,tsc会压住 TypeScript 侧的报错。文档原话:While a.sqlfile has an error,tscholds back the TypeScript errors。现象:一次跑出几十个 TS 错误却只有一个 SQL 错;影响:噪音大、定位偏;修复:先修 SQL,TS 错误会跟着消。- Vite 下
.sql会被打进浏览器包。文档警告:Vite 把查询打包到它被 import 的任何地方,包括浏览器。现象:SQL 源码(含表名、逻辑)泄漏到前端 bundle;影响:信息泄漏 + 包体膨胀;修复:只在 server 目录(如.server/)里 import.sql。 - 同名列是硬错误。
select u.id, p.id, p.title会报reylo14: Columns 1 and 2 are both named "id",因为一行对象只保留最后一个。现象:SQL 在数据库里能跑,在 Reylo 里报错;影响:改动影响面比预期大;修复:给重名列加as别名。 - 参数推断不出来就是错误(
strict_params: true默认)。裸select :x或:x is null里的参数类型是unknown,默认报错要求你 cast。现象:随手写个参数就报类型错;影响:初期磨合成本高;修复:给参数加::int之类的 cast;确实想放宽就设strict_params: false。 - MySQL 的错误没有行列定位,整条语句一起报(SQLite 走
:1:1,Postgres 走:1:12)。现象:MySQL 下错误定位很粗;影响:定位成本高;修复:MySQL 项目优先用较短的语句,或自己先在数据库里跑一遍确认。 - SQLite 有一批明确的能力边界(文档「Limits」段):非
STRICT表的声明类型不管用(所以表要写strict);你自己注册的函数或扩展函数查不了(no such function: slugify),得在 TS 里对.run()的结果后处理;ATTACH的附加库与TEMP表查不了(类型检查只读一个数据库文件);超过 350 层嵌套表达式 SQLite 自己会报「Expression tree is too large」;一个.sql文件里写多条语句(迁移脚本)是错误。 - 大整数跨驱动行为不一致。Reylo 自己解码结果,但驱动对超过 2^53 的整数处理不同:
sqlite3、sqlite、D1、Durable Objects、Bun.sql(未开 safeIntegers)会四舍五入,此时 Reylo 直接抛错;libSQL 默认intMode下驱动自己就抛RangeError。现象:某些驱动下bigint列取数报错;影响:换驱动就炸;修复:libSQL 显式设intMode: "bigint"或"string",或在 SQL 里::int收窄。 - Bun 1.4.3 上
Bun.sql有 bug 级差异:含单引号字符串内双引号的查询(如json_extract('{"a":1}', '$.a'))返回零行。现象:同样的 SQL 在别的驱动正常,在 Bun 上静默返回空;影响:极难排查的静默错误;修复:文档建议把 JSON 文档作为参数传(json_extract(:doc::text, '$.a'))。 - 值不是 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转换。 - Cloudflare D1 的
direct_url不能写进.env。文档明确:Wrangler 会把.env加载成 Worker secrets。现象:D1 项目类型检查拿不到库;影响:配置看似正确但不生效;修复:把direct_url写进.reylo.yaml,指向wrangler d1 migrations apply --local生成的本地文件。 .reylo目录是缓存。它是 Reylo 对每条查询分析结果的缓存,自带.gitignore,不要提交,也不要手动改。- 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。