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

Open
#5,952 6 comments 2 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.