cloudflare / cloudflare/workers-sdk
[vitest-pool-workers] readD1Migrations() does not read the nested migration layout that migrations_pattern supports
- Dominant language
- TypeScript
- Stars
- 4.5k
- Forks
- 1.5k
- Avg merge
- 3d 8h
- Merged PRs (30d)
- 187
Description
### What versions & operating system are you using?
```
System:
OS: Windows 11 10.0.26200
CPU: (8) x64 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz
Memory: 1.06 GB / 15.71 GB
Binaries:
Node: 24.18.0 - C:\Users\Grady\.vite-plus\js_runtime\node\24.18.0\node.EXE
npm: 11.16.0 - C:\Users\Grady\.vite-plus\js_runtime\node\24.18.0\npm.CMD
pnpm: 10.32.1 - C:\Users\Grady\.vite-plus\package_manager\pnpm\10.32.1\pnpm\bin\pnpm.CMD
npmPackages:
@cloudflare/vite-plugin: ^1.47.0 => 1.47.0
@cloudflare/vitest-pool-workers: ^0.18.8 => 0.18.8
wrangler: ^4.114.0 => 4.114.0
```
### Please provide a link to a minimal reproduction
https://github.com/cloudflare/workers-sdk/blob/afaecee40d557f515c5bcb3256a8ca13327c6557/packages/vitest-pool-workers/src/pool/d1.ts#L20-L22
### Describe the Bug
`readD1Migrations()` reads only `*.sql` files sitting directly in the directory it is given. It does not handle the nested `_/migration.sql` layout that D1 added support for in May via `migrations_pattern`, so for a project using that layout it returns an empty array and `applyD1Migrations()` then applies nothing.
I've linked the source line rather than a repro repo, because the failure is entirely inside the reader — there's no interaction between components to reproduce, just a few lines of Wrangler config against one line of source, both inline below.
### Setup
Wrangler config using the nested layout:
```jsonc
"d1_databases": [
{
"binding": "DB",
"database_name": "example-db",
"migrations_dir": "drizzle",
"migrations_pattern": "drizzle/*/migration.sql"
}
]
```
On disk — this is drizzle-kit's default output shape:
```
drizzle/
20260101000000_init/
migration.sql
snapshot.json
```
### Steps to reproduce
1. `wrangler d1 migrations apply example-db --local` — reads and applies the migration.
2. In `vitest.config.ts`:
```ts
const migrations = await readD1Migrations("./drizzle");
console.log(migrations.length); // 0
```
3. `applyD1Migrations()` therefore applies nothing, and every test fails against an empty database.
Same directory, same Wrangler config, two different answers from the same toolchain.
### Cause
`packages/vitest-pool-workers/src/pool/d1.ts` (permalinked above):
```ts
const names = fs
.readdirSync(migrationsPath)
.filter((name) => name.endsWith(".sql"));
```
A single non-recursive read, so a directory of per-migration subdirectories matches nothing. This matches only `migrations_pattern`'s default value of `${migrations_dir}/*.sql`.
### Expected
`readD1Migrations()` reads the same migrations Wrangler applies.
Honouring `migrations_dir` / `migrations_pattern` from the Wrangler config the pool is usually already pointed at (via `wrangler.configPath`) would keep the test path and the deploy path from drifting. Globbing one level deeper would also fix the immediate problem.
### Context
`migrations_pattern` shipped on **May 29, 2026**, explicitly to support this layout:
- Changelog: [D1 migrations support nested layouts via `migrations_pattern`](https://developers.cloudflare.com/changelog/post/2026-06-04-migrations-pattern/) — *"You can now point `wrangler d1 migrations apply` at a nested migrations layout — such as the one produced by Drizzle (`migrations/0001_init/migration.sql`)"*
- D1 docs: [Nested migration layouts](https://developers.cloudflare.com/d1/reference/migrations/#nested-migration-layouts) — *"If you use an ORM such as Drizzle that writes each migration as its own subdirectory (for example, `migrations/0001_init/migration.sql`), set `migrations_pattern` to the glob that matches your layout"*
- Config reference: [`migrations_pattern`](https://developers.cloudflare.com/workers/wrangler/configuration/)
So Wrangler gained first-class support for the Drizzle layout two months ago, and the Workers Vitest integration's migration reader — the officially documented way to apply migrations in tests — cannot read it. Following the D1 docs to configure a Drizzle project leaves you with a test suite that runs against an empty database.
### Related, smaller
Finding zero migrations returns `[]` silently, which is what makes this expensive to diagnose — the first symptom is `no such table` pointing at your own query, several layers from the cause. `readD1Migrations()` already throws a `TypeError` when `migrationsPath` isn't a string, so throwing when it finds nothing to apply would be consistent with its existing behaviour, and an empty migration set is never what the caller wanted.
Also adjacent, and more directly tied to the fix — the sort immediately below the code above:
```ts
names.sort((a, b) => {
const aNumber = parseInt(a.split("_")[0]);
const bNumber = parseInt(b.split("_")[0]);
return aNumber - bNumber;
});
```
This is safe today, because `wrangler d1 migrations create` always produces `NNNN_name.sql` and `parseInt` always succeeds. If nested layouts are read and names become paths relative to `migrations_dir` — per the D1 docs, e.g. `0001_init/migration.sql` — that guarantee no longer holds: a directory without a numeric prefix gives `NaN` on both sides, and a comparator returning `NaN` leaves the ordering implementation-defined. Migration order isn't cosmetic, so it may be worth pinning explicitly as part of whatever the fix turns out to be.
Finally, unrelated to the bug itself but in the same area and cheap to fix: the JSDoc on `applyD1Migrations` ([`types/cloudflare-test.d.ts#L183-L184`](https://github.com/cloudflare/workers-sdk/blob/afaecee40d557f515c5bcb3256a8ca13327c6557/packages/vitest-pool-workers/types/cloudflare-test.d.ts#L183-L184)) and the [Test APIs reference](https://developers.cloudflare.com/workers/testing/vitest-integration/test-apis/) both say to call `readD1Migrations()` from the `@cloudflare/vitest-pool-workers/config` package, which is no longer in the exports map. The [configuration reference](https://developers.cloudflare.com/workers/testing/vitest-integration/configuration/) already shows the correct root-package import.
### Please provide any relevant error logs
```
Error: D1_ERROR: no such table: users: SQLITE_ERROR
❯ D1DatabaseSessionAlwaysPrimary._sendOrThrow cloudflare-internal:d1-api:146:19
❯ cloudflare-internal:d1-api:97:27
```
Every test in the project fails this way. Nothing in the output mentions migrations, the migrations
directory, or the fact that zero were applied.
Contributor guide
Research direction
Start in packages/vitest-pool-workers/src/pool/d1.ts at readD1Migrations(), then compare its flat-file handling with Wrangler’s migrations_pattern behavior and the nested layout described here. Verify that both flat and nested migration directories are read in migration order, and that applyD1Migrations() no longer leaves the test database empty.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- databases, testing
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100