Skip to content

[vitest-pool-workers] readD1Migrations() does not read the nested migration layout that migrations_pattern supports #14866

Description

@gradyat

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

const names = fs
.readdirSync(migrationsPath)
.filter((name) => name.endsWith(".sql"));

Describe the Bug

readD1Migrations() reads only *.sql files sitting directly in the directory it is given. It does not handle the nested <timestamp>_<name>/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:

"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:
const migrations = await readD1Migrations("./drizzle");
console.log(migrations.length); // 0
  1. 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):

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"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"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

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:

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) and the Test APIs reference both say to call readD1Migrations() from the @cloudflare/vitest-pool-workers/config package, which is no longer in the exports map. The configuration reference 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    package:vitestRelating to the Workers Vitest integrationproduct:d1Relating to Cloudflare D1: https://developers.cloudflare.com/d1/

    Type

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions