Skip to content

fix(core): restore content bylines table left staged by an interrupted rebuild - #2612

Merged
ascorbic merged 1 commit into
emdash-cms:mainfrom
danielmlr:fix/content-bylines-rename-repair
Aug 25, 2026
Merged

fix(core): restore content bylines table left staged by an interrupted rebuild#2612
ascorbic merged 1 commit into
emdash-cms:mainfrom
danielmlr:fix/content-bylines-rename-repair

Conversation

@danielmlr

Copy link
Copy Markdown
Contributor

What does this PR do?

A SQLite or D1 site can report no entries while every row is intact. Public pages render the empty state, /rss.xml lists nothing, and the admin reports the collection as not found. In that state the byline credits table exists only as _emdash_content_bylines_new, left behind by an interrupted run of migration 040, and _emdash_content_bylines is missing. Every loader query (loadEntry, loadCollection) then fails on its bylines subquery. Running the migrations again does not repair it.

The cause: 040 rebuilds the table on SQLite in four steps. It creates a staged copy, copies the rows, drops the old table, and renames the copy. If the run stops between the drop and the rename, the retry reads PRAGMA foreign_key_list on a table that no longer exists. SQLite returns no rows for a missing table, so the retry treats the table as already rebuilt and skips the rename. The runner records 040 as applied and the later migrations run. The staged copy stays behind.

Migration 071 renames _emdash_content_bylines_new back to _emdash_content_bylines when the live table is missing and the staged copy exists. It then re-creates the two indexes that the drop removed. On a healthy database it changes nothing. On Postgres it changes nothing either, because the 040 path there alters the table in place. It can restart after any of its statements. 040 itself stays unchanged because published migrations are immutable. Covered by integration tests in both dialects.

Closes #2241

Details

Numbering: #2584 also adds a migration numbered 071. Whichever of the two merges second needs a renumber. Nothing else in this change depends on the number.

Stale copy: if both tables exist, 071 leaves _emdash_content_bylines_new alone. The live table is authoritative in that state, and the migration deletes nothing.

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
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • 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 (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main. (n/a: no admin strings)
  • I have added and reviewed the user-facing changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/... (n/a: bug fix)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Fable 5

Screenshots / test output

packages/core/tests/integration/database/restore-content-bylines-table-migration.test.ts
  — healthy table and credits untouched (both dialects; Postgres only in CI); staged copy renamed back and indexes restored;
    second run after the rename completes; runner retry from 040 recovers the table; stale copy next to a live table left alone
  with up() stubbed to a no-op: 3 of 5 fail (rename, re-run, runner retry)

packages/core  vitest run tests/integration/database tests/database tests/unit/database tests/unit/astro
               105 files, 1080 tests passed
packages/cloudflare  vitest run tests/db   19 files, 249 tests passed
pnpm typecheck / lint / format             clean

…d rebuild

Migration 040 rebuilds `_emdash_content_bylines` on SQLite by staging a copy,
dropping the old table and renaming the copy. When the run stops between the
drop and the rename, a retry skips the rebuild: its guard reads
`PRAGMA foreign_key_list` on a table that no longer exists and treats the empty
result as "already rebuilt". The migration is then recorded as applied with
only `_emdash_content_bylines_new` present, every byline subquery fails, and
the site reports no entries although the rows are intact.

Add migration 071, which renames the staged copy back when the live table is
missing and re-creates the two indexes the drop removed. It is restart-safe,
leaves a healthy table untouched, and is a no-op on Postgres, whose 040 path
alters the table in place.
@changeset-bot

changeset-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a19f8b2

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

This PR includes changesets to release 17 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-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

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet area/core size/M labels Aug 23, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2612

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2612

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2612

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2612

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2612

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2612

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2612

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: a19f8b2

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(core): restore content bylines table left staged by an interrupted rebuild

Approach

This is the right fix for the right problem. Migration 040’s SQLite rebuild can fail between dropping _emdash_content_bylines and renaming _emdash_content_bylines_new; on retry, its PRAGMA foreign_key_list(_emdash_content_bylines) guard returns no rows for the missing table, so the runner records 040 as applied and leaves the credits orphaned. Since published migrations are immutable per AGENTS.md, adding a focused, idempotent corrective migration is the correct pattern, and leaving 040 untouched is the right call.

The migration is SQLite-only (Postgres alters in place), checks for both the live and staged tables, renames only when safe, and re-creates the dropped indexes with ifNotExists(). The no-op down() is acceptable because the post-071 state (live table properly named with indexes) is the canonical state.

What I checked

  • Read the diff and the full changed files.
  • Traced 040’s SQLite rebuild path to confirm the stated failure mode: rebuildContentBylines returns early when the live table is missing and PRAGMA foreign_key_list returns no rows, so the staged copy is never renamed.
  • Verified migrations 041–070 do not reference _emdash_content_bylines, so a failed 040 does not cause later migrations to fail before 071 can repair the table.
  • Confirmed 071’s index names and column lists match those created by 040 (idx_content_bylines_content on collection_slug, content_id, sort_order and idx_content_bylines_byline on byline_id).
  • Reviewed runner.ts registration and the migrations test’s trailing list; both correctly include 071.
  • Reviewed the new integration tests. They cover the healthy DB, the partial-run repair, re-run idempotency, runner retry from 040, and the stale-staged-copy-left-alone case. They exercise observable behavior (table/index existence and hydrated credits), not implementation trivia.
  • Checked dialect-helpers.ts for isSqlite/tableExists behavior; D1 inherits SQLite detection.
  • Reviewed the changeset against .changeset/README.md: it leads with “Fixes,” identifies affected users (SQLite/D1), names the observable problem (empty pages/feeds/admin), and states the outcome (next migration run restores the table and credits).
  • Checked for AGENTS.md convention violations: no unsafe SQL interpolation, no new user-facing strings, no route changes, no comment referencing issues/PRs. The migration’s module comment refers to migration 040 by number, which is appropriate for migration context.

Conclusion

The change is minimal, well-scoped, and the tests are capable of catching a real regression. I found no bugs, regressions, or convention violations. LGTM.

@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-review No maintainer or bot review yet labels Aug 24, 2026

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

@ascorbic
ascorbic merged commit 628630a into emdash-cms:main Aug 25, 2026
49 checks passed
@emdashbot emdashbot Bot mentioned this pull request Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core review/approved Approved; no new commits since size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migration 040 can leave _emdash_content_bylines_new behind on D1

2 participants