Skip to content

feat: run on drizzle databases and their transactions - #23

Merged
colinhacks merged 4 commits into
mainfrom
drizzle-clients
Oct 22, 2026
Merged

colinhacks merged 4 commits into
mainfrom
drizzle-clients

Conversation

@colinhacks

Copy link
Copy Markdown
Owner

.run() now takes a Drizzle database, and the transactions and savepoints Drizzle hands out, on every dialect. Reylo runs its statement on the driver client Drizzle's session holds, so the values are Reylo's own for that driver, exactly as when the client is passed directly.

import { drizzle } from "drizzle-orm/node-postgres";
import getUser from "./get_user.sql";

const db = drizzle(process.env.DATABASE_URL!);

const user = await getUser.run(db, { id: 1 });
await db.transaction((tx) => getUser.run(tx, { id: 1 }));
  • Which client: session.client (the pool for a database, the transaction's own connection inside one), or session.tx inside a libSQL transaction, whose client stays the base client. Run on the base client there, a libSQL transaction failed 13 driver-matrix rows with SQLITE_BUSY.
  • Types: a Drizzle database is accepted by its $client, so drizzle(pool) takes the dialects pool takes. A transaction is told apart by what only its dialect's has: setTransaction() on Postgres, get() and all() on SQLite, neither on MySQL. .runSync() takes a Drizzle database over a synchronous SQLite driver and its transaction (whose all() returns rows), and refuses libSQL's.
  • postgres.js JSON parameters: Drizzle replaces a postgres.js client's json and jsonb serializers with ones that pass the value through, and a transaction shows neither. So Reylo now gives every postgres.js client a JSON parameter as its JSON text declared unknown (sql.typed(text, 705)), which the server reads as the parameter's type.
  • Kysely and Prisma stay unsupported: both decode every value of a raw query themselves (Kysely through the pool's parsers, so a timestamp comes back as a local Date; Prisma by its own rules) before Reylo could see it. Their error now says which client to pass, and inside their transactions that the connection is not handed out.
  • Docs: a Drizzle tab on each dialect of Supported drivers (type-checked by twoslash against drizzle-orm 0.45.4), and a line in each dialect page's drivers list and the README.

Tests:

  • tests/execute.test.ts: a Drizzle database and transaction on each dialect, libSQL's session.tx, and the Kysely and Prisma refusals.
  • Driver matrix: five Drizzle jobs (over pg, postgres.js, mysql2, better-sqlite3 and libSQL), 310 rows, all pass; every other job's rows are as recorded. Type check: 666 cases over 113 client types, all as expected.
  • Run locally: typecheck, loader tests with none skipped, package build, docs site, every lab and the driver matrix (containers included), and every example project.

Copilot AI balanced review requested due to automatic review settings October 22, 2026 19:53
@vercel

vercel Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
reylo Ready Ready Preview Oct 22, 2026 8:11pm UTC

Request Review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues. Two small suggestions: one inline, one below.

Reviewed changes

This review covers how Reylo now runs on Drizzle databases, transactions and savepoints, plus the postgres.js JSON change that came with it. I checked underneath() against the published drizzle-orm@0.45.4 source. Only libsql/session.js keeps a tx field. Every other session, PlanetScale's and TiDB's included, puts the transaction's connection on client, so session.tx ?? session.client picks the right one everywhere.

  • Unwrapping: underneath() swaps a Drizzle database, transaction or savepoint for the driver client its session holds. Both run and runSync call it first, before they pick a path.
  • Types: DrizzleDatabase<C> is matched by $client. Each dialect's transaction is matched by a method only it has. runSync takes Drizzle over a synchronous SQLite driver.
  • postgres.js JSON: every client with typed() (all but Bun.sql) now gets a JSON parameter as typed(JSON.stringify(v), 705). Drizzle replaces the client's json/jsonb serializers with ones that pass the value through unchanged.
  • Error hints: the old "pass db.$client" hint is gone. New hints tell Kysely and Prisma users which client to pass, including from inside a transaction.
  • Lab and docs: five Drizzle driver-matrix jobs, Drizzle type-check cases, and Drizzle tabs on the Supported drivers page (checked by twoslash). The dialect pages and the README mention Drizzle.

ℹ️ The transaction types accept a Drizzle transaction from any driver of the dialect

DrizzleMysqlTransaction and the other transaction types match by dialect, not by driver. So .run(tx) also type-checks for a transaction from a Drizzle driver Reylo doesn't run on, even though $client rejects that driver's database. Most such drivers fail loudly at run time. A drizzle-orm/tidb-serverless transaction fails silently: its session.client is TiDB's native transaction, which has execute() but no query(), so the call goes to planetscale(). TiDB returns a plain array of rows, and planetscale() returns [] whenever the result has no .rows. This affects only a niche driver, so you may decide it's not worth handling.

Technical details
# A Drizzle tidb-serverless transaction type-checks and returns no rows

## Affected sites
- src/execute.ts:118 — `DrizzleMysqlTransaction` matches every Drizzle `MySqlTransaction`, `TiDBServerlessTransaction` included.
- src/decode-mysql.ts:284 — `if (!Array.isArray(result?.rows)) return [];` turns TiDB's row array into no rows.

## Required outcome
- A client whose `execute()` answers in a shape `planetscale()` doesn't recognize should fail with an error, not return no rows. Alternatively, document that only the Drizzle drivers listed in the docs are supported.

## Open questions for the human
- Is a run-time refusal enough, or should the types try to narrow the transaction to its driver (probably not feasible structurally)?

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread src/execute.ts Resolved
@colinhacks

Copy link
Copy Markdown
Owner Author

On the review's note that a Drizzle transaction from any driver of the dialect type-checks: the run-time refusal is fixed in c0ef9bf (fix(mysql): refuse an execute() result without rows).

Before, planetscale() in src/decode-mysql.ts returned [] whenever the result had no rows array. It now throws a TypeError instead: "reylo ran this statement through execute(), as on PlanetScale's @planetscale/database, and the client answered with an array rather than a result with rows. It takes execute(text, values) as on mysql2 and PlanetScale, or a Drizzle database over one of them." This is safe for PlanetScale: in both 1.x and 2.x, execute() builds rows as result ? parse(...) : [], so the field is always an array, empty for a statement that returns nothing. The driver matrix's PlanetScale jobs still match RESULTS.md row for row (gate lab-driver-matrix-containers passes). The same refusal also covers a bare @tidbcloud/serverless connection passed directly, which used to fail the same silent way.

New test in tests/execute.test.ts ("a Drizzle database or transaction runs on the driver client its session keeps"): a tidb-serverless-shaped transaction, whose session's client.execute() resolves to a plain row array, now rejects with that message. A unit-test fake that stood in for mysql2 with execute() alone now has query() beside it, as every real mysql2 client does.

The types are left as they are. A transaction's type carries no mark of its driver, so narrowing it would mean naming every driver's transaction class.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found. Both notes from the last review are addressed.

Reviewed changes

This run covers what changed since the last review at 814a2d9. The branch was rebased onto main, and the new work is in the postgres.js JSON tests and in the MySQL planetscale() path. tests/execute.test.ts passes locally (42 of 42).

  • postgres.js JSON coverage: added a { unsafe, typed } fake to the array/JSON test and to the jsonQuery test. They assert that each JSON value, jsonb[] included, arrives as { value: <JSON text>, type: 705 } and that null is still bound as null. Updated the comment above json() and the test title to match.
  • planetscale() refuses results without rows: an execute() answer with no rows array now throws a TypeError that describes what came back. Before, it was read as no rows. So a drizzle-orm/tidb-serverless transaction now fails loudly instead of returning [], and a new test covers this. Existing MySQL fakes that meant mysql2 now have query beside execute, so they still take the mysql2 path.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@colinhacks
colinhacks merged commit f96c614 into main Oct 22, 2026
4 checks passed
@colinhacks
colinhacks deleted the drizzle-clients branch October 22, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants