开源工具
github.com ·
@colinhacks
Reylo v0.3.0 发布:支持 Durable Object SQL 存储并修正类型推断
Reylo 0.3.0 的官方发布说明称,该版本可在 Durable Object 的 SQL 存储上运行查询,并会按 package.json 中列出的 SQLite 驱动(better-sqlite3、@libsql/client、sqlite3、@tursodatabase/database)校验语句;同时为 SQLite 全文检索函数补全类型,并修正 Postgres 和 MySQL 上若干过窄的类型推断。说明还提示,依赖旧类型的代码可能需要增加 null 检查。
AI推荐理由 官方发布说明,信息密度较高:新增对 Durable Object SQL 存储的支持、按项目实际 SQLite 驱动校验语句、为 FTS5/FTS4 函数补全类型,并修正 Postgres/MySQL 若干过窄类型(可能要求调用方补 null 检查)。局限是面向 TypeScript SQL 查询工具的细分开发者,影响面有限,且未提供性能或采用数据。
62
正文
Official release notes (https://github.com/colinhacks/reylo/releases/tag/v0.3.0)
Reylo 0.3.0 runs on a Durable Object's SQL storage, checks SQLite statements against your app's own drivers, types SQLite's full-text search, names the query's file in its errors, and fixes several types that were wrong on Postgres and MySQL. Install `reylo` and your database's engine at 0.3.0 together.
> [!NOTE]
> Three of the fixes below widen a type that was too narrow, where the database could return a value the type ruled out: a `select *` over a left-joined `with` query on Postgres, a JSON `coalesce` with a default other than `'[]'` on Postgres, and a scalar subquery whose `with` shadows a table on MySQL. Code that relied on the old types may need a null check.
### Running queries
- Durable Objects: `.run()` and `.runSync()` take a SQLite-backed Durable Object's `ctx.storage.sql`, as they take D1's binding, inside `ctx.storage.transactionSync()` too. An integer past 2^53, which the storage returns rounded, is refused, as on D1, and a BLOB comes back as a `Uint8Array`.
- Every query exposes the file it came from as `.source`, relative to the project's root, so an app can tag its logs and traces with it: `getUser.source` is `"queries/get_user.sql"`.
- Reylo's errors about a parameter's value, a column's value or the row count now start with the query's file, as in `queries/get_user.sql: Expected exactly one row, got 0`. The driver's own errors pass through unchanged.
### Type check
- On SQLite, the type check also prepares each statement on the SQLite drivers your `package.json` lists (`better-sqlite3`, `@libsql/client`, `sqlite3`, `@tursodatabase/database`), whose SQLite may be older than Reylo's. A statement one of them refuses is an error at the place SQLite names, such as `json_pretty` on libSQL, which runs SQLite 3.45:
```
queries/pretty.sql(1,12): error reylo6: @libsql/client runs SQLite 3.45.1, which refuses this statement: no such function: json_pretty
```
### Inference on SQLite
- The full-text search functions are typed: FTS5's `bm25` is a `number`, `highlight` and `snippet` are `string | null`, and FTS4's `offsets` is a `string` and `matchinfo` a `Uint8Array`. They were `unknown`.
### Inference on Postgres
- A `select *` over a `with` query on the nullable side of an outer join types that query's columns as nullable. They were typed from the table inside the query, non-null where every row can be null.
- A JSON `coalesce(x, default)` keeps the shape of `x` only where `x` is never null, or where it is an array and the default is `'[]'`. Under any other default it is JSON of unknown shape: over no rows, `coalesce(jsonb_agg(id), '{}')` returns `{}`, which the old `number[]` ruled out.
### Inference on MySQL
- A derived table's columns keep what Reylo infers about its query alone, so a count or a non-null column read through a derived table stays non-null. A column computed on the null-extended side of an outer join, such as `d.next` in `left join (select user_id, id + 1 as next from posts) d`, is now nullable, where the server's flag had it non-null.
- A scalar subquery with a `with` of its own is typed as it would be with the `with` outside it, so `(with r as (select id from posts) select count(id) from r)` is non-null and the JSON it builds is typed. Where that `with` shadows a table, its columns are typed from the `with` query rather than from the table.
### For agents
- An agent skill, `using-reylo`, carries the rules an agent needs in a Reylo project once it is set up. Install it with `npx skills add colinhacks/reylo --skill using-reylo`.
Repository README (current repository documentation, not release changes)
Import .sql files into your TypeScript code with full type inference
Read the docs →
If you have a file like this:
```sql
-- 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
```
You can import it directly from a TypeScript file like this:
```ts
// 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
```
Reylo is powered by TypeScript [content mappers](https://github.com/microsoft/typescript-go/pull/4712), introduced in TypeScript 7.1 (currently [in beta](https://devblogs.microsoft.com/typescript/announcing-typescript-7-1-beta/)), which let you dynamically define static types for external file types and integrate them directly into TypeScript compilation.
Reylo is the perfect option for the agent era:
- 💪 **The full power of SQL** — any statement, function, operator or syntax your database supports, in its own dialect.
- 🌵 **DRY** — the types come from the SQL itself: no hand-written row types, no schema file to keep in sync.
- 🎯 **Checked against your database** — a misspelled column, a wrong parameter or a dropped table is a type error, in `tsc` and in your editor.
- 🤖 **Agent-friendly** — each fact lives in one place, so an agent cannot leave a stale type behind, and the type check finds every caller it missed.
- 🧩 **Postgres, MySQL and SQLite** — bring your own driver: `pg`, postgres.js, `mysql2`, `better-sqlite3`, `Bun.sql`, Cloudflare D1 and [more](#supported-drivers).
- ⚡ **No codegen** — the type checker asks Reylo directly: no generated files, no watcher.
## Setup
To install Reylo, you'll need:
- **TypeScript 7.1 (currently in beta).** The first release with content mappers.
- **Node 24 or later.**
- **A running dev database.** Postgres, MySQL 8.0.22 or later, or a SQLite file: Reylo reads your current schema from it.
Install Reylo, the engine for your database and TypeScript 7.1:
```sh
npm install reylo
npm install -D @reylosql/postgres # or mysql, sqlite
npm install -D typescript@beta
```
Add Reylo to `tsconfig.json`, for `.sql` files and for its project file, `.reylo.yaml` or `.reylo.yml`:
```jsonc
// tsconfig.json
{
"compilerOptions": { /* ... */ },
"contentMappers": [
{ "package": "reylo", "extensions": [".sql", ".reylo.yaml", ".reylo.yml"] }
]
}
```
Then type-check with `--runExternalCode`, which lets `tsc` run content mappers:
```jsonc
// package.json
{
"scripts": {
"typecheck": "tsc --noEmit --runExternalCode"
}
}
```
Point Reylo at your dev database with `DATABASE_URL`, in your environment or in `.env`. Its scheme tells Reylo which database you use:
```sh
# .env
DATABASE_URL=postgres://localhost:5432/app # or mysql://…, or file:./app.db for SQLite
```
To make `.sql` imports work at run time too, load Reylo where your code runs. On Node:
```sh
node --env-file=.env --import reylo/register main.ts
```
For Bun, Nub, Vite, Next.js, esbuild, webpack, Rspack, Cloudflare Workers and Jest, add [the plugin for your setup](https://reylo.dev/#plugins), or copy its complete [example project](https://github.com/colinhacks/reylo/tree/main/examples). To see the types as you type, set up [your editor](https://reylo.dev/#editors).
## Usage
Write a query in a `.sql` file, import it, and run it with your database driver:
```sql
-- get_user.sql
select id, name, email from users where id = :id::int
```
```ts
import getUser from "./get_user.sql";
const user = await getUser.run(db, { id: 1 });
// { id: number; name: string; email: string | null } | null
```
Name each parameter with a colon, and give it a type with a cast where the query doesn't pin one down. Pass `.run()` your driver's client: a pool, a client, or a connection or transaction taken from one.
It resolves to the rows, shaped by how many the query can return:
| The query | Resolves to |
| --- | --- |
| A match on a primary or unique key, or `limit 1` | `Row \| null` |
| An aggregate without `group by`, a one-row `insert … returning` | `Row` |
| An `insert`, `update` or `delete` without `returning` | `void` |
| Anything else | `Row[]` |
An imported query also has `.runSync()`, `.with()`, `.sql` and `.raw` ([Usage](https://reylo.dev/#usage)).
## What Reylo infers
To type a query, Reylo has your dev database prepare it, without running it, and reads back the type of every column and parameter. On top of that, it infers:
- **Nulls** — a column is nullable unless its schema, a join or a `where` test rules null out.
- **Cardinality** — a match on a unique key is one row or `null`, and an aggregate is exactly one row.
- **Unions** — an enum, or a column whose check constraint lists its values, is a union of them.
- **JSON** — JSON the query builds, and a JSON column with a JSON Schema check, keep their shape.
Each has examples in [Inference](https://reylo.dev/#inference).
## Supported drivers
Reylo is bring-your-own-driver. Pass `.run()` a client from any of these, or a Drizzle database over one:
- **Postgres** — `pg`, postgres.js, `Bun.sql`, PGlite, Neon, `pg-promise`, `@vercel/postgres`
- **MySQL** — `mysql2`, PlanetScale, `Bun.sql`
- **SQLite** — `better-sqlite3`, `node:sqlite`, `Bun.sql`, libSQL, Turso, Cloudflare D1, `sqlite3`, `sqlite`
## Documentation
[reylo.dev](https://reylo.dev) covers the rest: every plugin and editor, configuration, errors, each database's data types and edge cases, and advanced examples. Agents can read it all as Markdown at [reylo.dev/llms.txt](https://reylo.dev/llms.txt).
## License
[MIT](LICENSE)