管理核心数据库迁移

本页内容

EmDash 核心迁移会更新 EmDash 自身的表,以及内容表上的标准列。它们不会创建、删除或重命名你的集合和字段;内容模型的变更请参阅演进已部署站点的模式。

运行时迁移模式默认为 auto,因此现有部署会继续在启动时应用待处理的核心迁移。由部署管理的迁移,可以让构建在新的应用代码接收流量之前先迁移它的数据库,然后让运行时验证或信任这一部署步骤。

核心迁移只能向前执行。它们的编写方式保证:在确定已完成的语句之后,命令可以重试;但被中断的远程命令可能留下含糊不清的结果。安全的做法是用 emdash migrate --status 检查同一个数据库,而不是假定整个迁移或完全没有迁移已经运行。

构建、迁移、部署、检查

Astro 的构建或同步会写出 .emdash/migrations.json。这份不含密钥的清单记录了该次构建所使用的确切 EmDash 版本、有序的迁移集合、locale 配置,以及适配器的迁移执行器。

请在生成该清单的依赖所在的项目中运行这些命令。先构建,并检查目标。

pnpm build
pnpm emdash migrate --status

确认所报告的目标就是预期的数据库后,开始交互式迁移。在确认之前,请在提示处再次检查目标。然后部署同一份构建,并检查已部署的 schema。

pnpm emdash migrate
pnpm wrangler deploy
pnpm emdash migrate --check

emdash migrate --status 会报告已应用、待处理和未知的迁移,而不会更改数据库。普通的 emdash migrate 命令会显示目标,并在应用待处理迁移之前要求确认。

--check 从不应用迁移,并且当存在已知的待处理迁移,或数据库中包含构建不认识的迁移记录时,会以非零状态退出。如果你想在不触发 check 那种表示“需要处理”的非零退出状态的情况下检查同样的迁移集合,请使用 --status。CLI 参考区分了待处理、未知、确认、中断和运行错误这几种退出码。

非交互式的应用,以及每一次带 --json 的应用,都要求提供 --expected-target-fingerprint;如果解析出的目标与之不符,命令会失败。请在自动化部署作业中使用这些选项,而不要用于上面的交互式工作流。

如果清单存放在其他位置,请使用 --manifest path/to/migrations.json。进行本地排查时,--from-config [--config astro.config.mjs] 会显式地对受信任的项目配置求值,而不会运行 Astro 钩子或启动服务器。部署流水线应当使用构建清单。

显式选择数据库

所配置的适配器会向清单提供不含密钥的目标信息。凭据保留在环境变量中,并且只由迁移命令读取。

适配器清单中的目标默认凭据变量常用覆盖项
SQLite数据库路径或 file: URL—--database <path>
libSQL公开 URLTURSO_AUTH_TOKEN配置 migrationAuthTokenEnv
PostgreSQL连接变量名称DATABASE_URL--database-url-env <name>
Cloudflare D1Wrangler 绑定名称CLOUDFLARE_API_TOKEN--d1、--account-id、--wrangler-config、--wrangler-env
Hyperdrive主绑定和源变量名称与绑定对应的直连源变量配置 migrationConnectionStringEnv

相对的 SQLite 路径从项目根目录解析,而不是从已安装的 EmDash 包或 shell 当前所在的子目录解析。PostgreSQL、libSQL 和 Hyperdrive 的目标标签会省略凭据和 URL 参数。

迁移前预置 D1

创建 D1 数据库和迁移它的 schema 是两个独立的操作。emdash migrate 从不创建缺失的数据库。

  1. 预置数据库,并记录它的生产环境 UUID。

    pnpm wrangler d1 create my-site-production
  2. 把该 UUID 添加到 wrangler.jsonc 中预期的绑定和环境。

  3. 构建站点,使 D1 绑定被记录到 .emdash/migrations.json 中。

  4. 设置账户 ID,以及具有 D1 Edit 权限的限定范围 API 令牌。检查所选的目标,然后运行交互式迁移。只有当账户和数据库与预期的生产数据库一致时,才在提示处确认。

    export CLOUDFLARE_ACCOUNT_ID="..."
    export CLOUDFLARE_API_TOKEN="..."
    pnpm emdash migrate \
      --status \
      --wrangler-config wrangler.jsonc \
      --wrangler-env production
    pnpm emdash migrate \
      --wrangler-config wrangler.jsonc \
      --wrangler-env production

你也可以改为同时提供 --account-id 和 --d1 <database-uuid-or-name>。按名称查找必须恰好解析到一个数据库。预览 ID、占位符 ID、相互冲突的账户和含糊不清的绑定都会以失败告终(fail closed)。

在 CI 中配置 D1 迁移

EmDash 在应用迁移期间会在 D1 数据库中持有一把迁移锁,emdash migrate 和 auto 模式下的运行时迁移都是如此。当锁被持有时,如果又启动了第二次运行,它最多会等待 10 秒。如果第一次运行在这段时间内完成,第二次运行会成功,但不应用任何内容;否则,它会失败,并且不应用任何迁移。请针对同一个账户和数据库 UUID,每次只运行一个迁移作业,这样第二个作业会在 CI 队列中等待,而不是失败。

请在 CI 环境中设置以下密钥和变量:

  • 密钥 CLOUDFLARE_API_TOKEN:具有 D1 Edit 权限的限定范围令牌。
  • 变量 CLOUDFLARE_ACCOUNT_ID:拥有该数据库的 Cloudflare 账户 ID。
  • 变量 D1_DATABASE_ID:生产环境 D1 数据库的 UUID。
  • 变量 EMDASH_TARGET_FINGERPRINT:你在本地审核过账户和数据库之后,由 emdash migrate --status 打印的指纹。

下面这个 GitHub Actions 工作流使用了这些值,并同时用这两个不可变的 D1 标识符为并发组命名。它的应用步骤是非交互式的,因此会显式提供已审核过的目标指纹。

name: Deploy

on:
  workflow_dispatch:

concurrency:
  group: emdash-migrations-${{ vars.CLOUDFLARE_ACCOUNT_ID }}-${{ vars.D1_DATABASE_ID }}
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build
      - name: Inspect EmDash migration target
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
        run: |
          pnpm emdash migrate --status --json \
            --account-id "${{ vars.CLOUDFLARE_ACCOUNT_ID }}" \
            --d1 "${{ vars.D1_DATABASE_ID }}"
      - name: Apply EmDash migrations
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          EMDASH_TARGET_FINGERPRINT: ${{ vars.EMDASH_TARGET_FINGERPRINT }}
        run: |
          pnpm emdash migrate \
            --account-id "${{ vars.CLOUDFLARE_ACCOUNT_ID }}" \
            --d1 "${{ vars.D1_DATABASE_ID }}" \
            --expected-target-fingerprint "$EMDASH_TARGET_FINGERPRINT"
      - run: pnpm wrangler deploy
      - name: Check EmDash migrations
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
        run: |
          pnpm emdash migrate --check \
            --account-id "${{ vars.CLOUDFLARE_ACCOUNT_ID }}" \
            --d1 "${{ vars.D1_DATABASE_ID }}"

只有在本地审核过变化后的目标之后,才更新 EMDASH_TARGET_FINGERPRINT。指纹中不含任何凭据,但如果不检查账户和数据库就修改它,就会失去防止迁移错数据库的保护。

释放卡住的迁移锁

如果某次 D1 迁移运行在释放迁移锁之前就停止了,锁就会一直被持有。在以下情况会发生这种事:CI 作业在 emdash migrate 期间被取消,auto 模式下的 Worker 在运行时迁移期间停止,或者开发服务器在应用迁移时被停止。因迁移错误而失败的运行会释放锁。EmDash 不会释放不是由它持有的锁,因为持有者可能仍在应用迁移,也可能在迁移中途停止了。在锁被释放之前,站点无法应用它待处理的迁移,而在 auto 模式下,EmDash 会初始化失败。

锁被持有超过一分钟之后,emdash migrate 和运行时迁移会停止等待它,并报告下面的错误:

The migration lock has been held since 2026-09-01T12:00:00.000Z (lock 1788264000000). A migration may still be running; if none is, check the database and release the lock: https://docs.emdashcms.com/deployment/core-migrations/#release-a-stuck-migration-lock

释放远程 D1 数据库的锁要用到 emdash migrate,因此需要一份写出了 .emdash/migrations.json 的构建,以及具有 D1 Edit 权限的 API 令牌,如迁移前预置 D1中所述。

  1. 确认没有任何迁移作业、部署或其他 emdash migrate 命令正在针对该数据库运行。

  2. 使用与该迁移相同的目标选项,检查锁和迁移集合。

    pnpm emdash migrate --status

    报告以锁及其 id 开头。如果停止的那次运行正在应用某个迁移,那么它就是第一个待处理的迁移,并且可能只应用了一部分。

    Migration lock: held since 2026-09-01T12:00:00.000Z (id 1788264000000)

    auto 模式下的 Worker 在应用迁移时也可能持有该锁。请在一分钟或更长时间之后再次运行该命令,并且当已知的已应用迁移仍在变化时,应当等待,而不要释放锁。

  3. 使用该 id 释放锁。命令会要求你确认目标,并且只有当锁仍然是该 id 时才会释放它。在非交互式 shell 中,请添加 --expected-target-fingerprint,并使用 --status 打印的目标指纹。

    pnpm emdash migrate --release-lock 1788264000000
  4. 再次应用待处理的迁移。

    pnpm emdash migrate

    如果应用在第一个待处理的迁移中失败,请把该迁移视为执行到一半,并按照故障排除中关于含糊不清的 D1 写入的条目处理。

emdash migrate 只能访问远程 D1 数据库。当锁被持有在开发服务器的本地 D1 数据库中时,请停止服务器,并用 Wrangler 清除该锁,把 DB 替换为绑定名称,把其中的数字替换为错误信息中的锁 id。

pnpm wrangler d1 execute DB --local --command "UPDATE _emdash_migrations_lock SET is_locked = 0 WHERE is_locked = 1788264000000"

Hyperdrive 连接到源

Hyperdrive 的迁移执行器会直接打开一条到源的 PostgreSQL 连接。它不会让迁移流量经过 Hyperdrive,不会使用可选的缓存绑定,也不会从 Worker 继承私有网络的可达性。

部署运行器必须能够访问该源。当默认的、与绑定对应的变量不合适时,请在 hyperdrive() 上设置 migrationConnectionStringEnv,并且只把该变量提供给迁移作业。请把运行时的 Hyperdrive 凭据和直连源的部署凭据分开保管。

逐步采用运行时强制

下面的 EmDash 集成配置启用了运行时强制,同时在开发环境中保留自动迁移。

emdash({
	database,
	migrations: {
		runtime: "check",
		dev: "auto",
	},
});
  • auto 是向后兼容的默认值。运行时启动时会检查并应用待处理的迁移。
  • check 执行一次有方向的状态查询,并在存在已知的待处理迁移时,于处理请求之前返回 503。在滚动部署期间,它能容忍来自更新的兼容构建的记录。对 EmDash 所安装的图片端点的请求仍然会被处理,因为它们会跳过执行这项检查的运行时启动过程。
  • manual 不执行任何运行时迁移或状态查询。只有在部署流水线能够可靠地为每个构建执行应用和检查之后,才使用它。

当同一个产物被提升到多个环境时,可以用 EMDASH_MIGRATIONS_MODE 覆盖运行时模式。设置和开发的绕过路由遵循实际生效的模式;它们不能在 check 或 manual 之下悄悄地执行迁移。

一种稳妥的推广方式是:在引入部署作业时先使用 auto,在作业可靠之后改用 check,在每次部署都强制执行外部检查时再改用 manual。

滚动部署期间的兼容性

核心迁移遵循扩展/部署/收缩(expand/deploy/contract)的顺序。部署期间,旧的和新的应用 isolate 可能会暂时同时针对已扩展的数据库运行,并且回填可能仍在进行。在每个已部署的版本都停止使用某个 schema 之前,请不要收缩它。

对于未知的已应用迁移记录,运行时的 check 只在这种滚动部署的方向上容忍它们。CLI 的精确检查会报告它们,而应用会拒绝修改,因为数据库可能更新,或者可能有分叉的迁移历史。

回滚边界

部署之前的应用产物并不会撤销核心迁移。在应用待处理迁移之前,请先做一份可恢复的数据库备份,并记录与之匹配的应用产物。如果之前的应用无法针对已迁移的 schema 运行,请把迁移前的数据库和应用一起恢复。不要从 _emdash_migrations 中删除行,也不要把迁移内部的 down() 函数当作运维层面的回滚来运行。

修复混合的 PostgreSQL 所有权

当已有的 PostgreSQL 站点创建的 EmDash 对象有多个所有者,并且之后的迁移因 must be owner of table 之类的错误而失败时,请使用本手册。请选择 EmDash 主连接今后继续使用的标准角色。在更改所有权之前,请先做一份可恢复的数据库备份,并停止应用流量和 schema 变更。

检查当前 schema 中的每个表:

SELECT
  n.nspname AS schema_name,
  c.relname AS table_name,
  pg_get_userbyid(c.relowner) AS owner
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE n.nspname = current_schema()
  AND c.relkind IN ('r', 'p')
ORDER BY c.relname;

EmDash 的对象包括 _emdash_* 和 _plugin_* 系统表、ec_* 集合表,以及 content_taxonomies、media、options、revisions 和 taxonomies 之类不带前缀的表。在专用的 EmDash schema 中,每个应用表都应当由标准角色拥有。

EmDash 还会创建供媒体使用情况触发器使用的 PostgreSQL 函数。请检查函数的所有权,并保留每个函数的参数签名,供修复命令使用:

SELECT
  n.nspname AS schema_name,
  p.proname AS function_name,
  pg_get_function_identity_arguments(p.oid) AS arguments,
  pg_get_userbyid(p.proowner) AS owner
FROM pg_proc AS p
JOIN pg_namespace AS n ON n.oid = p.pronamespace
WHERE n.nspname = current_schema()
ORDER BY p.proname, arguments;

使用超级用户,或使用能够更改所有权的服务商角色,转移每个不一致的对象。请使用清单中真实的 schema、对象、角色和函数签名,而不要原样照搬示例中的名称:

ALTER TABLE emdash.content_taxonomies OWNER TO emdash_app;
ALTER TABLE emdash.ec_posts OWNER TO emdash_app;
ALTER FUNCTION emdash.emdash_media_usage_capture_work() OWNER TO emdash_app;

更改表的所有者同时也会覆盖它所附带的索引、约束和触发器,但不包括独立的触发器函数。请重复执行这两个清单查询,直到每个 EmDash 表和函数报告的所有者都是标准角色。然后以该角色连接,并在重新放入流量之前验证 current_database()、current_schema() 和迁移状态。

对于非超级用户,要转移所有权,它必须拥有或继承该对象的所有权,能够 SET ROLE 到新的所有者,并且新的所有者必须在该 schema 上拥有 CREATE 权限。托管的 PostgreSQL 服务商可能要求使用它们的管理员角色来执行转移。

故障排除

  • 找不到迁移清单。 请先构建或同步项目。对于非标准的产物位置,请使用 --manifest,或者在本地排查时显式选择 --from-config。
  • 产物与项目的 EmDash 不匹配。 请把应用和清单一起重新构建并部署。请运行项目自带的 CLI,而不是全局安装的版本。
  • 目标缺失或含糊不清。 请先预置它,然后提供明确的数据库路径、连接变量名称、D1 选择器,或所选的 Wrangler 配置和环境。EmDash 不会根据无关的环境变量或绑定去猜测。
  • 目标指纹发生了变化。 请停下来,审核所显示的账户、环境、数据库名称、UUID 或路径。只有在确认了预期的目标之后,才更新期望的指纹。
  • 存在未知的迁移记录。 不要删除这些记录,也不要重新运行应用。请确认应用产物是预期的版本,并调查是否有更新的或分叉的构建迁移过该数据库。
  • D1 写入的结果含糊不清。 不要重放迁移命令。请针对同一个账户和数据库 UUID 运行 emdash migrate --status,检查结果,并在迁移中途停止时升级处理。
  • 日期时间规范化需要人工审核。 某个旧版日期时间落在了站点所配置时区中重复或被跳过的夏令时小时内。错误信息会列出每个受影响的内容行或修订版本。请用明确的 UTC 偏移量更正这些值,然后重试迁移。在每个已存储的值都能被解析之前,迁移的预检不会写入任何日期时间。
  • Hyperdrive 无法连接。 请测试从部署运行器到 PostgreSQL 源的可达性,并验证直连源变量。Worker 到 Hyperdrive 的连通性并不能证明运行器能够访问该源。