Skip to content

Fix migration 016 partial-reapply crash#976

Merged
ascorbic merged 4 commits intomainfrom
opencode/issue954-20260510105333
May 10, 2026
Merged

Fix migration 016 partial-reapply crash#976
ascorbic merged 4 commits intomainfrom
opencode/issue954-20260510105333

Conversation

@ask-bonk
Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot commented May 10, 2026

Now I'll craft the final response as the PR body. The workflow will use this as the PR body when opening the PR.

What does this PR do?

Closes #954.

Migration 016_api_tokens was not safe to re-run against a partially-applied schema. The user-reported failure on a fresh Cloudflare Workers + D1 deploy is the canonical instance: if up() crashes mid-way -- D1 subrequest limit, isolate cancellation, transient connection error -- the first createTable("_emdash_api_tokens") succeeds but the migration record never gets inserted into _emdash_migrations. On the next request, Kysely sees 016 still pending and re-runs up() from the top, which crashes with table "_emdash_api_tokens" already exists and wedges every subsequent boot.

The fix is the same shape as PR #803 (which made 034 and 035 idempotent for the same bug class): every CREATE TABLE and CREATE INDEX in 016's up() is guarded with .ifNotExists(), and the symmetric dropTable calls in down() use .ifExists(). A retry now treats already-applied steps as no-ops and creates the remaining tables.

Residual risk worth flagging: migrations 017+ have the same structural vulnerability (no .ifNotExists() on their CREATE TABLE / CREATE INDEX calls; 017 also has a bare ALTER TABLE ... ADD COLUMN that will hard-fail on retry with duplicate column name). Per AGENTS.md scope discipline and the narrow precedent set by #803, I have not preemptively fixed those here -- only the migration named in the report. They should be addressed when (a) a user hits one of them, or (b) a maintainer decides to do a deliberate sweep.

Reproduction & TDD shape

  1. 6447fd9 -- adds the failing unit test. At that commit the new tests fail with the exact error from Migration 016_api_tokens fails with "table already exists" on fresh D1 database #954: table "_emdash_api_tokens" already exists.
  2. 839eb31 -- adds .ifNotExists() / .ifExists() to 016. Tests pass.
  3. eea15a0 -- changeset.
  4. 91fb5c0 -- strengthens the partial-reapply test in response to adversarial review. The original version hand-rolled a partial-state schema; because CREATE TABLE IF NOT EXISTS is a no-op against a pre-existing table even when the new definition differs, a future schema drift in 016 would have left the test silently passing. The strengthened version reuses up() itself to set up the partial state, drops only the trailing tables, retries, and asserts the recovered table's columns and 016's owned indexes are intact.

I verified the test catches the regression by reverting just the migration source while keeping the strengthened test: both nested re-run-safety tests fail with table "_emdash_api_tokens" already exists -- the exact symptom from the issue.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes (verified pnpm --filter emdash typecheck clean after pnpm build; pre-existing failure in @emdash-cms/registry-client reproduces on 136d348 and is unrelated to this PR)
  • pnpm lint passes (no new diagnostics; pnpm --silent lint:json | jq '.diagnostics | length' returns the same 73 pre-existing warnings on this branch as on 136d348, none on the changed files)
  • pnpm test passes (pnpm --filter emdash test -- 204 files, 3214 tests pass)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation -- n/a (no admin UI changes)
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion -- n/a (bug fix)

AI-generated code disclosure

  • This PR includes AI-generated code -- model/tool: Claude Opus 4.7 (via /bonk workflow)

Screenshots / test output

$ pnpm --filter emdash test tests/integration/database/ tests/unit/database/migrations/
 Test Files  8 passed (8)
      Tests  61 passed (61)
   Duration  2.27s

Regression check (revert only the migration source, keep the test):

× is a no-op when every table and index has already been created
× recovers when only the first tables were created before the crash

Caused by: SqliteError: table "_emdash_api_tokens" already exists
 ❯ up src/database/migrations/016_api_tokens.ts:15:2

With the migration fix in place: 4/4 pass.

Closes #954

github run

ask-bonk Bot added 4 commits May 10, 2026 10:57
Reproduces the symptom from #954: when migration 016's up() partially
completes — for example, the first createTable succeeds but a later
statement crashes due to a D1 subrequest limit, isolate cancellation,
or transient connection error — the migration record never gets
inserted into _emdash_migrations. On the next request, Kysely sees 016
still pending and re-runs up() from the top, which currently crashes
with 'table _emdash_api_tokens already exists'.

The new tests directly invoke up() twice, and also simulate the
partial-apply state by pre-creating just the first table 016 owns.
Both currently fail; the next commit makes 016 idempotent so they
pass.
)

When migration 016's up() crashes mid-way -- D1 subrequest limit,
isolate cancellation, transient connection error -- the migration
record never gets inserted into _emdash_migrations. Kysely's next
attempt re-runs up() from the top and crashes with 'table
_emdash_api_tokens already exists', blocking every subsequent boot
of the Worker. This is the failure path the user hit on a fresh D1
database: setup wizard runs, the first table creates, something
later in 016 fails, and the deploy is wedged.

Adds .ifNotExists() to every CREATE TABLE and CREATE INDEX in 016's
up() and .ifExists() to the symmetric drops in down(). The retry now
treats already-applied steps as no-ops and proceeds to create the
remaining tables, matching the recovery pattern from #803 (migrations
034 and 035).
Addresses adversarial review feedback: the original partial-apply test
hand-rolled an _emdash_api_tokens schema that was meant to mimic the
state left by a crashed up(). Because CREATE TABLE IF NOT EXISTS over
an existing table is a no-op even when the new definition differs, a
schema drift in the migration would leave the test silently passing
while the recovery path no longer matched what we shipped.

Reuses the migration's own up() to set up the partial state, then
drops only the trailing tables before retrying. Adds explicit
assertions on the recovered table's columns and 016's indexes, so a
future change to the migration that drops a column or index gets
caught here rather than slipping through.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 10, 2026

🦋 Changeset detected

Latest commit: 91fb5c0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 10, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-perf-coordinator 91fb5c0 May 10 2026, 11:11 AM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 10, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs 91fb5c0 May 10 2026, 11:11 AM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 10, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-i18n 91fb5c0 May 10 2026, 11:11 AM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 10, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground 91fb5c0 May 10 2026, 11:12 AM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 10, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache 91fb5c0 May 10 2026, 11:12 AM

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 10, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@976

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@976

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@976

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@976

emdash

npm i https://pkg.pr.new/emdash@976

create-emdash

npm i https://pkg.pr.new/create-emdash@976

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@976

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@976

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@976

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@976

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@976

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@976

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@976

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@976

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@976

commit: 91fb5c0

@ascorbic ascorbic merged commit 4c11017 into main May 10, 2026
36 checks passed
@ascorbic ascorbic deleted the opencode/issue954-20260510105333 branch May 10, 2026 13:57
@emdashbot emdashbot Bot mentioned this pull request May 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migration 016_api_tokens fails with "table already exists" on fresh D1 database

1 participant