资讯动态

两天 691★:这个新项目让 .sql 文件直接变成 TypeScript 类型

发布时间:2026/10/24 7:01:45 来源:尧图企业网站定制
两天 691★这个新项目让 .sql 文件直接变成 TypeScript 类型【免费下载链接】reyloImport .sql files into TypeScript with full type inference and zero codegen.项目地址: https://gitcode.com/gh_mirrors/re/reylo在 GitHub 日榜 2026-10-23 的快照里colinhacks/reylo被记下 648 颗星、24 小时 616。把时间轴拉回一天标题里的 691 是两天累计的口径48 小时折合约 14.4★/h而只看最后 24 小时则是约 25.7★/h——增速明显在往上走而不是一条平线不同快照的采样时刻不同总数会差几个属于正常现象。更值得琢磨的是它为什么能起量。它没有引入新数据库也没有发明新 ORM只做了一件在 TypeScript 生态里被反复尝试、又反复妥协的事让import q from ./q.sql拿到的不是一个字符串而是一个带完整类型签名的查询对象。而这一次它终于有了不需要 codegen 的落地方式TypeScript 7.1 引入的 content mappers。它到底解决什么列名、参数、表名写错立刻报错先从 README 里那段最小的例子说起-- get_author.sql select u.id, u.name, count(p.id)::int as post_count from users u left join posts p on p.user_id u.id where u.id :id::int group by u.id// main.ts import postgres from postgres; import getAuthor from ./get_author.sql; const db postgres(process.env.DATABASE_URL); const author await getAuthor.run(db, { id: 1 }); // { id: number; name: string; post_count: number } | null三个细节藏在这几行里参数是:id这种具名占位符不是$1。Reylo 会把它重写成各数据库自己的形式同时把参数类型推出来——这里靠::int和where u.id的上下文。返回类型是| null不是Row[]。因为id命中主键最多一行。post_count是number而不是string。count()在 Postgres 里是bigint作者显式::int转换了。这些都不是运行期才知道的。写错列名时错误直接落在.sql文件的那一行上get_user.sql:1:12 - error reylo6: column nmae does not exist (42703) 1 select id, nmae, email ~~~~错误码reylo6对应数据库拒绝了这条语句范围来自数据库自己报的位置。仓库里 src/mapper.ts 的diag()把这个位置换算成 UTF-8 字节区间写进诊断withinFile()还会把越界的区间裁到文件末尾——因为 TypeScript 会直接拒收一个越界的 transform 响应整个模块退化成any。这类细节很能说明项目已经踩过坑了。为什么可以不要 codegencontent mapper 的 152 行传统方案的路径是schema 文件 → 生成器 →types.ts→ 你的代码。中间任何一环不同步类型就是过期文档。Reylo 换了一条路类型检查器在遇到.sql导入时直接把文件交给一个外部进程拿回一个虚拟模块的 TypeScript 源码。src/rpc.ts 实现了一个带Content-Length帧的 JSON-RPC 服务因为tsc就是这么和 content mapper 说话的。src/mapper.ts 只做三件事初始化、打开项目、transform。transform 的核心输出是这样一段文本由emit()拼接import type { Query } from reylo; export type Params { id: number }; export type Row { id: number; name: string; post_count: number }; export declare const sql: select ...; export declare const raw: select ...; export declare const params: readonly [id]; declare const query: Query { id: number }, { id: number; name: string; post_count: number }, maybeOne, postgres { readonly sql: typeof sql; readonly raw: typeof raw; readonly source: string }; export default query;几个取舍很讲究Query的四个类型参数是Params、Row、Cardinality、Dialect。行数被编码进类型于是.run()的返回类型可以被条件类型算出来见 src/index.ts 的Materialize。sql和raw用字面量类型保存。悬停时能看到真正发出去的语句而不是string。mappings把整个虚拟模块映射回 SQL 语句的字节区间。所以对 import 做转到定义会打开.sql文件而 hover / rename 被刻意放弃否则会落到整条语句上或者把 TypeScript 写进 SQL 里。推断本身不在 TypeScript 侧。src/module-types.ts 只是把引擎的分析结果渲染成对象类型文本列注释顺带变成字段的 JSDoc——Postgres 的comment on column因此能在编辑器里悬停看到。引擎是一份 Rust编译成三个 WebAssembly这是这个项目最不像周末项目的地方。整个分析能力在 engine/ 下是一个 Cargo workspace包位置职责reylo-engine-coreengine/core/协议、Databasetrait、占位符词法、类型模型、JSON Schema、结构化参数、新鲜度事实reylo-engine-postgresengine/postgres/libpg_query打过补丁、类型注册表、分析、基数证明、JSON 形状reylo-engine-sqliteengine/sqlite/打了 hook 的 SQLite amalgamation以及解析树上的分析reylo-engine-mysqlengine/mysql/自带的sqlparser0.63 精简副本 服务端 prepare 元数据设计上最关键的一条引擎永远不打开连接。它只回放分析——每一轮它要么给出答案要么列出还需要什么要 describe 的语句、要跑的目录查询由宿主通过项目自己的驱动去满足再把结果喂回去。宿主是 src/engine.ts通过 WASI 加载 packages/postgres/engine.wasm 这样的产物提交进仓库所以没有 Rust 工具链也能跑。代码量对比很能说明重心TypeScript 侧src/*.ts约 6.7k 行Rust 侧含 oracle约 91k 行。三个 wasm 分别是 3.85 MB / 1.71 MB / 2.45 MB。它推断的东西比列类型多得多如果只把prepare的元数据翻译成 TS那这个项目不值得 691 颗星。真正的工作量在元数据之外site/content/sections/03-inference.mdx 列了四类每一类都有独立的实现文件空值性。left join右侧的列、coalesce、where ... is not null都会改变| null。这不是启发式猜测Postgres 侧有专门的一轮分析。基数。这是最有产品感的一处。命中主键或唯一键的等值查询、limit 1→Row | null无group by的聚合、单行insert ... returning→Row没有returning的写 →void其余 →Row[]。类型系统因此能表达这里不可能返回两行materialize()在运行期还会真的检查行数并抛错。枚举与 CHECK 约束。枚举类型直接变成标签的联合类型CHECK 里列出的取值同理。examples/node/src/main.ts里那句role: admin | member就是这么来的。JSON 的形状。这条最有意思select u.name, jsonb_agg(jsonb_build_object(id, p.id, title, p.title)) as posts from users u join posts p on p.user_id u.id group by u.idposts的类型是{ id: number; title: string }[]。MySQL 的json_arrayagg(json_object(...))、SQLite 的json_group_array(json_object(...))各有对应实现。再进一步如果一个jsonb列上挂了 JSON Schema 的 CHECK 约束列类型就从 schema 推。还有一个更狠的用法——把 JSON Schema 写进 cast让参数获得类型并在运行期校验insert into events (kind, payload) values (click, :payload::json({ type: object, properties: { x: { type: integer }, y: { type: integer }, button: { type: string, enum: [left, right] } }, required: [x, y] }))payload的类型就是那个对象.run()还会在发出去之前跑一遍校验src/json-schema.ts。同一个机制让批量插入一千行变成单个 JSON 参数而不是拼 SQL。为什么现在值得关注类型工具正在往薄的方向走过去几年数据库类型工具的演进方向是把 schema 变成中心一份 schema 描述世界类型从它生成运行时也围绕它组织。代价是那份描述必须活着且必须和真实数据库保持一致。Reylo 的答案是把中心换成数据库自己对语句的解释。README 里那句 no generated files, no watcher 不只是少几个文件而是删掉了一整条同步链路没有生成物 → 没有忘了重新生成这种失败模式没有 watcher → 迁移后不需要重启什么下一次类型检查自然读到新 schema没有手写行类型 → 想改类型只能改 SQL 或改库表两个都是真东西。仓库里还专门写了一条面向 agent 的论据每个事实只存在一处agent 没法留下一个过期的类型而类型检查会找出它漏改的每一个调用点。这话有点营销味但机理是成立的——这正是生成物方案的固有缺陷。缓存策略也服务于这个目标。src/analyze.ts 把每条语句的分析结果写进.reylo/queries/语句哈希-服务器指纹.json条目里记录它是对哪个 schema 分析的。schema 指纹 5 秒过期所以一次迁移在开发服务器或编辑器里几秒内就能反映出来。连不上数据库时例如容器构建、CI它退回使用已有条目——reylo generate因此成了离线构建的入口。运行期还有一道新鲜度闸门。src/fresh.ts 把语句依赖的目录事实列类型、是否 NOT NULL、是否有默认值写进模块第一次运行以及之后每 60 秒用调用方自己的 client 跑一条目录查询比对。不一致就直接失败并指名哪个表的哪个列变了而不是让新代码跑在旧 schema 上。这一段注释里甚至记着一个真实踩坑用performance.now()而不是Date.now()因为 Next.js 的 Cache Components 在预渲染时拒绝Date.now()。工程上的代价别被 demo 骗了热度之外有几条边界必须先说清楚它们都写在 site/content/sections/09-faq.mdx 和 site/content/sections/01-setup.mdx 里必须有活的开发数据库。类型检查、构建、CI 都要能连上。这不是可选项也不是以后会支持——当前架构就是拿数据库当类型预言机。本地没有库就跑不起来。依赖 TypeScript 7.1 beta 与 Node 24。7.1 的第一个带 content mappers 的版本tsc还要加--runExternalCode才允许跑外部代码。这决定了它现在是尝鲜而非上生产。编辑器要显式指向项目里的 TypeScript。VS Code / Cursor 扩展自带的是 7.0没有 content mappers必须js/ts.tsdk.path: ./node_modules/typescript并信任工作区。Zed 更进一步它不把.sql文件送给 language server所以 SQL 内部的错误只在tsc里出现。一条语句一个文件。迁移文件会被类型检查当成多语句报错需要exclude掉。参数在一条语句里只能出现一次想要同一个值用两次得用with先选出来这条规则由引擎的诊断码 3 强制。参数没有类型就是错误。自 2026-10-20 起strict_params默认开启诊断码 12 会提示你补 cast或者在.reylo.yaml里关掉。这是个正确的默认值但迁就旧代码时会有摩擦。不改 schema、不做迁移。它读你现有的库Prisma Migrate / Drizzle Kit / Atlas 继续用你的。驱动是自带的。.run()接收你自己的 clientpg、postgres.js、Bun.sql、PGlite、Neon、pg-promise、vercel/postgresmysql2、PlanetScalebetter-sqlite3、node:sqlite、libSQL、Turso、Cloudflare D1、sqlite3……以及 Drizzle 包出来的 database 和事务。src/execute.ts 把每个驱动的最小结构切片写成类型所以 Reylo 对驱动包零依赖——连类型依赖都没有。一个被低估的信号它有 1969 条 SQL 的回归集判断这类类型推断项目能不能长期活着看 demo 没用看测试集。仓库里有一个叫 oracle 的东西oracle/cases/下有 103 个 case 目录、1969 个.sql文件每个 case 目录带setup.sql、一条语句一个文件以及一份expected.txt。格式是这样的primary_key.sql: maybeOne (id: int4) - { id: int4 } deferrable.sql: many (k: int4) - { id: int4 } lossy_cast_bigint.sql: many (b: float8) - { id: int4 }第一列是语句第二列是基数括号里是参数类型箭头后是列类型?表示可空。注意deferrable.sql和lossy_cast_bigint.sql可延迟唯一约束不算唯一键bigint和float8比较会丢精度所以基数退化成many。这类判断没法靠直觉写对只能靠一条条 case 攒。更关键的是 oracle 不只比对文本——它会在回滚事务里真的执行语句用极端值跑两遍检查返回值是否违反推断的类型、返回行数是否超出基数。也就是说类型推断这件事在这里是被当作可靠性属性在验证而不是当作编辑器体验。对一篇文章来说这是最有说服力的部分它解释了为什么 Rust 侧有 91k 行。收尾691 颗星买的是什么如果只看SQL 变类型这个想法至少有五年历史了各种sqlc式的、模板式的、ORM 式的方案都试过。Reylo 能被日榜推上来我觉得是三件事在同一时间点凑齐了一是平台能力到位。content mappers 让外部文件类型成为 TypeScript 编译器的一等公民不需要 ESLint 插件、不需要d.ts生成、不需要守护进程。二是不用假装自己在建模。类型不是对世界的声明而是数据库对一条具体语句的回答。这让类型过期这个问题从根上消失了——它压根没有第二份数据。三是作者的方法论。package.json里的 author 是 Colin McDonnellZod 的作者而这个仓库把大量精力放在了看不见的地方UTF-8 字节偏移的诊断裁剪、按 schema 指纹失效的缓存、运行期的新鲜度校验、103 个 case 的 soundness 回归。这些东西不会出现在 GIF 里但它们决定了这个东西在真实项目里是玩具还是工具。现在的它还有明显的门槛TS 7.1 beta、Node 24、必须有活的开发库。但也正因为门槛高它展示的方向才更有信息量——类型工具的下一轮竞争可能不是谁能生成更多代码而是谁能把中间产物删得更干净。【免费下载链接】reyloImport .sql files into TypeScript with full type inference and zero codegen.项目地址: https://gitcode.com/gh_mirrors/re/reylo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑