drizzle-team / drizzle-team/drizzle-orm
[BUG]: drizzle-kit migrate fails on Cloudflare D1 (d1-http) — up-migrations reads proxy rows positionally, breaking any non-empty __drizzle_migrations
- Dominant language
- TypeScript
- Stars
- 35.8k
- Forks
- 1.6k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 4
Description
## What version of `drizzle-orm` are you using?
1.0.0-rc.4
## What version of `drizzle-kit` are you using?
1.0.0-rc.4
## Describe the Bug
`drizzle-kit migrate` is broken for the Cloudflare **D1 HTTP** driver (`driver: 'd1-http'`) whenever the `__drizzle_migrations` table already contains rows (i.e. every deploy after the first). It aborts with:
```
[✗] Error during migration: While upgrading your database migrations table we found 35
([id: undefined, created_at: undefined], [id: undefined, created_at: undefined], … ×35)
migrations in the database that do not match any local migration. This means that some
migrations were applied to the database but are missing from the local environment
```
The migrations in the database are **not** missing or corrupt — they are a complete, correct v1 journal. Querying the same table directly shows fully-populated rows:
```
id | hash | created_at | name | applied_at
1 | 6286989f0b… | 1779652433000 | 20260524195353_mean_santa_claus | NULL
2 | 68568f233c… | 1781291587000 | 20260612191307_curvy_steel_serp… | NULL
… (35 rows, all with real id / hash / created_at / name)
```
Note the error prints `id: undefined, created_at: undefined` for rows whose `id`/`created_at` are demonstrably non-null — the values are being read as `undefined`, not actually absent.
### Root cause
The new migrations-table version-upgrade step added in rc.4 reads rows **positionally**, but the sqlite-proxy / d1-http driver returns rows as **objects**, so every positional access is `undefined`.
`drizzle-orm/up-migrations/sqlite-proxy.js` — `upgradeAsyncIfNeeded()`:
```js
const rows = await db.session.arrays(sql`SELECT name as column_name FROM pragma_table_info(${migrationsTable})`);
const version = GET_VERSION_FOR.sqlite(rows.map((r) => r[0])); // r[0] → undefined for object rows
```
Because `r[0]` is `undefined`, `GET_VERSION_FOR.sqlite()` never sees a `name` column and returns version **0** — even though the table is already v1 (it has `name` + `applied_at`). The `for (let v = version; v < MIGRATIONS_TABLE_VERSIONS.sqlite; v++)` loop then runs the v0→v1 `upgradeAsyncFunctions[0]`, which reads existing rows the same way:
```js
const dbRows = (await db.session.arrays(sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`))
.map((row) => ({ id: row[0], hash: row[1], created_at: row[2] })); // all undefined
```
With `id`/`hash`/`created_at` all `undefined`, every row fails to match any local migration and the function throws.
The reason `arrays()` returns objects is `drizzle-orm/sqlite-proxy/session.js` — `SQLiteRemoteSession.prepareQuery()` implements **both** `all` and `values` (array mode) identically, always requesting `"all"` from the proxy client and returning its rows verbatim:
```js
all: (params) => this.client(query.sql, params, "all").then(({ rows }) => rows),
values: (params) => this.client(query.sql, params, "all").then(({ rows }) => rows), // not array-mode
```
The d1-http proxy client returns D1 HTTP rows as objects, so `arrays()` (which goes through the `values`/array path) yields objects, and the positional reads in `up-migrations/sqlite-proxy.js` break.
### Impact
`drizzle-kit migrate` over `d1-http` is unusable for any non-empty journal — it fails on every CI deploy after the first. It only succeeds against a brand-new/empty database (where the upgrade step is skipped via the `newDb` path), which is not viable for production. `drizzle-orm` 1.0.0-rc.3 (which had no migrations-table upgrade step and matched migrations by `name`) works correctly against the same journals.
## Expected behavior
`drizzle-kit migrate` over d1-http should correctly detect an existing v1 `__drizzle_migrations` table (skip the upgrade) and apply only genuinely-pending migrations by name, as in rc.3 — not misread proxy rows as `undefined`.
## Suggested fix
Either:
- Make `SQLiteRemoteSession.prepareQuery`'s `values` execute method return positional arrays (true array-mode) rather than aliasing `all`, or
- Make `up-migrations/sqlite-proxy.js` read by column name (`r.column_name`, `row.id`, …) since the proxy yields object rows.
## Environment
- Driver: `d1-http` (Cloudflare D1 via HTTP API) in `drizzle.config.ts`
- Command: `drizzle-kit migrate`
- Existing `__drizzle_migrations` is a correct v1 table (`id, hash, created_at, name, applied_at`) with applied rows.
Contributor guide
Research direction
Start with upgradeAsyncIfNeeded() and the v0→v1 upgrade in drizzle-orm/up-migrations/sqlite-proxy.js, then inspect SQLiteRemoteSession.prepareQuery() in drizzle-orm/sqlite-proxy/session.js and reproduce with drizzle-kit migrate over an existing d1-http journal. Done means an existing v1 table is detected without upgrading, and only genuinely pending migrations are applied by name.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- sqlite, typescript
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100