# SpacetimeDB JSON 字符串迁移 procedure 设计 ## 背景 `spacetime sql` 只能稳定读取 public 表或数据库 owner 可见表。当前 `ai_result_reference` 等运行真相表保持 private,直接 SQL 导出会遇到 `no such table` 或 private table 提示,不能作为跨服务器迁移的稳定方案。 SpacetimeDB reducer 必须保持确定性,不能访问文件系统和网络。procedure 可以返回数据,也可以在事务中读取 private 表,因此迁移改为: 1. `spacetime-module` 内的导出 procedure 读取迁移白名单表,并直接返回迁移 JSON 字符串。 2. Node 运维脚本默认通过 `spacetime call` 调用导出 procedure,把返回的 JSON 字符串写入本地文件。 3. Node 运维脚本读取本地 JSON 文件内容。导入时默认先通过 `POST /v1/identity` 创建临时 Web API identity/token,再用当前 CLI 登录态把该 identity 授权为迁移操作员;小文件直接通过 HTTP request body 传给导入 procedure,大文件自动切成分片上传后再提交。 4. 导入 procedure 校验 JSON 与表白名单后,在事务中写入目标数据库。 procedure 不再访问 HTTP 文件桥,也不接收部署机本地文件路径。这样可以避开 SpacetimeDB 对 private/special-purpose 地址的 HTTP 访问限制,并避免把 private 表内容通过临时 HTTP 服务转发。 SpacetimeDB Wasm 运行环境不支持 `std::time::SystemTime::now()`,procedure 或 reducer 内需要当前时间时必须使用 `ctx.timestamp`。如果共享 crate 同时服务前端/本地纯逻辑与 SpacetimeDB 模块,应提供 `*_at(now_ms)` 或显式时间参数版本,SpacetimeDB 模块只调用注入时间的函数,避免发布后触发 `time not implemented on this platform` panic。 `spacetime login show --token` 输出的是 CLI 登录 token,不是 HTTP `/v1/database/.../call` 所需的数据库连接 token。导入脚本如果没有显式传 `--token`,会自动调用 `POST /v1/identity` 获取 Web API token;迁移时不要把 CLI token 传给 `--token`。 ## 接口 ### 迁移操作员授权 迁移 procedure 会读取并写入 private 表,不能对任意登录身份开放。模块内新增私有表 `database_migration_operator` 作为迁移操作员白名单: - `operator_identity`: 被授权调用迁移 procedure 的 SpacetimeDB identity。 - `created_at`: 授权写入时间。 - `created_by`: 发起授权的 identity。 - `note`: 运维备注,只用于区分来源、环境或临时用途。 `database_migration_operator` 只控制迁移 procedure 调用权限,不会被导出或导入,避免把源库的运维权限复制到目标库。 大文件分片导入额外使用私有临时表 `database_migration_import_chunk` 暂存上传片段。这张表只保存当前导入过程的中间数据,提交成功后自动删除,失败时由脚本尽量调用清理 procedure;它不在迁移白名单内,也不会被导出到业务迁移 JSON。 首次授权时,操作员表为空,必须通过编译进模块的 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET` 引导密钥授权第一位操作员。发布脚本会在构建或发布 SpacetimeDB 模块时自动生成一份强随机引导密钥、注入 wasm 编译环境,并在控制台显示;运维人员必须记录对应数据库本次发布输出的密钥。表内已经存在操作员后,后续授权与撤销只能由已有操作员发起;此时不再接受引导密钥越权扩权。 新增 procedure: - `authorize_database_migration_operator`: 授权或更新迁移操作员备注。 - `revoke_database_migration_operator`: 撤销迁移操作员。 运维流程: 本地开发发布时,`npm run dev:rust` 会在发布模块前输出本次随机生成的迁移引导密钥。发布包部署时,`npm run deploy:rust:remote` 会把同一份密钥写入发布包根目录的 `migration-bootstrap-secret.txt`,目标服务器执行 `./start.sh` 发布 wasm 时也会再次显示该密钥。 发布完成后,在同一台机器上用当前 `spacetime login` 身份授权操作员: ```bash node scripts/spacetime-authorize-migration-operator.mjs \ --server dev \ --database xushi-p4wfr \ --bootstrap-secret <本次发布随机密钥> \ --operator-identity \ --note "2026-04-27 migration" ``` 迁移完成后可以撤销临时操作员: ```bash node scripts/spacetime-revoke-migration-operator.mjs \ --server dev \ --database xushi-p4wfr \ --operator-identity ``` 生产环境建议迁移完成后用 `--no-migration-bootstrap-secret` 重新发布一个未设置 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET` 的模块版本,避免引导密钥长期留在 wasm 中。 ### 发布脚本密钥行为 当前会构建或发布 `spacetime-module` 的脚本默认都会生成并显示迁移引导密钥: - `npm run dev:rust`:在本地 `spacetime publish --module-path` 前生成密钥,控制台输出 `[dev:rust] 迁移引导密钥: ...`。 - `npm run deploy:rust:remote`:在构建发布包 wasm 前生成密钥,控制台输出 `[deploy:rust] 迁移引导密钥: ...`,并把同一份密钥写入发布包根目录的 `migration-bootstrap-secret.txt`。服务器执行 `./start.sh` 发布 wasm 时也会再次显示该文件里的密钥。 - `npm run build:production-release -- --component spacetime-module`:在生产 Stdb module 构建前默认生成或复用 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET`,注入 `spacetime_module.wasm`,并写入 `build//migration-bootstrap-secret.txt`。生产构建日志只显示密钥来源和长度,不打印明文;该文件应保存为 Jenkins Secret Text,供 `Genarrative-Database-Export` / `Genarrative-Database-Import` 的 `BOOTSTRAP_SECRET_CREDENTIAL_ID` 使用。 如果迁移完成后不希望 wasm 继续携带引导密钥,重新发布时传 `--no-migration-bootstrap-secret`。远端发布包若使用 `--skip-spacetime-build`,必须同时传 `--no-migration-bootstrap-secret`,否则脚本会拒绝生成一个无法注入旧 wasm 的新密钥。 ### 导出 `export_database_migration_to_file(ctx, input)` 输入字段: - `include_tables`: 可选表名白名单。为空时导出当前实现支持的全部迁移表。 返回字段: - `ok`: 是否成功。 - `schema_version`: 迁移 JSON 结构版本。 - `migration_json`: 成功时包含完整迁移 JSON 字符串,失败时为空。 - `table_stats`: 表级导出统计。 - `error_message`: 失败原因。 ### 导入 `import_database_migration_from_file(ctx, input)` `import_database_migration_incremental_from_file(ctx, input)` `put_database_migration_import_chunk(ctx, input)` `import_database_migration_from_chunks(ctx, input)` `import_database_migration_incremental_from_chunks(ctx, input)` `clear_database_migration_import_chunks(ctx, input)` 输入字段: - `migration_json`: 导出 procedure 生成的完整迁移 JSON 字符串。 - `include_tables`: 可选表名白名单。为空时导入文件内所有支持表。 - `replace_existing`: 是否先清空本次迁移文件内实际导入的目标表。不会清空迁移文件未包含的表;分批迁移时只覆盖当前批次。 - `dry_run`: 只解析和统计,不写表。 分片导入字段: - `upload_id`: 本次分片上传的唯一 ID,只允许 ASCII 字母、数字、短横线或下划线。 - `chunk_index`: 当前分片序号,从 `0` 开始。 - `chunk_count`: 本次上传总分片数。 - `chunk`: 当前迁移 JSON 片段,单片最多 `1048576` bytes。 Node 导入脚本默认在文件超过 `524288` bytes 时使用分片导入;如果小文件直接导入仍遇到 `SpacetimeDB HTTP 413: Failed to buffer the request body: length limit exceeded`,也会自动退回分片流程。可通过 `--chunk-size ` 或环境变量 `GENARRATIVE_SPACETIME_MIGRATION_CHUNK_SIZE` 调小单片大小。 导入模式: - 默认严格追加:不清空目标表,逐行插入;遇到主键或唯一约束冲突时失败并回滚,适合确认目标库没有同表旧数据时使用。 - 增量追加:调用 `import_database_migration_incremental_from_file`,不清空目标表;遇到已存在或唯一约束冲突的行会跳过并计入 `skipped_row_count`,只插入目标库缺失的行。该模式不会更新目标库已有行。 - 覆盖导入:`replace_existing = true` 时先删除覆盖范围内的目标表旧数据,再插入迁移文件中的数据;只适合迁移文件是这些表完整快照的场景。 返回字段: - `ok`: 是否成功。 - `schema_version`: 迁移 JSON 结构版本。 - `migration_json`: 导入场景恒为空,避免重复回传大 JSON。 - `table_stats`: 表级导入或跳过统计。 - `error_message`: 失败原因。 保留 `export_database_migration_to_file` / `import_database_migration_from_file` 名称,是为了减少已经记住的 procedure 名变更;语义上不再代表 module 直接读写文件。 ## Node 脚本 ### 发布冲突自动迁移 Ubuntu 发布包的 `start.sh` 默认采用冲突感知发布: 1. 先不清库发布新 wasm。 2. 如果发布成功,流程结束。 3. 如果发布失败且输出可判定为 schema 冲突,脚本自动导出旧库迁移 JSON 到 `database-migrations//.json` 或 `GENARRATIVE_SPACETIME_MIGRATION_DIR` 指定目录。 4. 导出成功后执行清库发布新 wasm。 5. 新 wasm 发布成功后,把第 3 步导出的 JSON 自动导入回灌。 SpacetimeDB 2.1 对 schema 冲突的报错文案可能不再包含 `schema conflict`,而是直接提示 `manual migration`、`default value annotation` 或 `--delete-data`。发布脚本必须把这些文案同样识别为可迁移冲突,否则会停在原始失败而不进入导出回灌流程。 新增字段优先采用低风险热升级策略:旧字段顺序保持不变,新字段追加到表尾,并用 `#[default(...)]` 提供旧行默认值。只有仍无法通过发布器检查时,才执行清库发布与 JSON 回灌。 任一阶段失败都会中止流程,并保留已经导出的迁移 JSON。非 schema 冲突的发布失败不会进入迁移流程。 可选参数: - `GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT=false`:禁用冲突自动迁移,只保留原始发布失败。 - `GENARRATIVE_SPACETIME_MIGRATION_DIR=`:指定迁移 JSON 输出目录。 - `./start.sh --clear-database`:显式清库发布;该模式代表人工确认清库,不触发自动迁移。 冲突自动迁移需要发布脚本本次生成的 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET`。因此不要和 `--no-migration-bootstrap-secret` 同时使用。 ### 部署流水线自动迁移 Ubuntu 发布包的 `start.sh` 与 Jenkins `Genarrative-Deploy` 也采用同一套迁移 procedure,但迁移触发点在部署目录内: 1. Jenkins 覆盖部署前,如果旧部署目录存在 `migration-bootstrap-secret.txt`,先保存到 `deploy-state/migration-bootstrap-secret.previous.txt`。旧密钥快照属于部署状态,不再写入 `run/`,避免 `sudo` 启停脚本生成的 root 私有运行目录阻断后续覆盖部署。 2. Jenkins 复制新发布包,包含新 wasm、新 `migration-bootstrap-secret.txt` 和 `scripts/spacetime-*.mjs` 迁移脚本。 3. 新 `start.sh` 先不清库发布当前包内 `spacetime_module.wasm`。 4. 如果发布成功,流程结束。 5. 如果发布失败且输出可判定为 schema 冲突,`start.sh` 用旧密钥授权导出旧库 JSON。 6. 导出成功后,`start.sh` 清库发布新 wasm。 7. 新 wasm 发布成功后,`start.sh` 用新密钥授权导入,并以 `--replace-existing` 回灌迁移 JSON。 自动迁移 JSON 默认保存到部署目录的 `database-migrations//.json`。可通过 Jenkins 参数 `MIGRATION_DIRECTORY` 或环境变量 `GENARRATIVE_SPACETIME_MIGRATION_DIR` 覆盖。该目录属于运行态数据,部署脚本不会删除。 Jenkins 参数 `MIGRATE_ON_CONFLICT` 默认 `true`。如果设为 `false`,普通发布遇到 schema 冲突时会保留原始失败,不执行导出、清库发布和导入回灌。 Jenkins 参数 `CLEAR_DATABASE=true` 或手工执行 `./start.sh --clear-database` 时,语义是人工确认清库发布;此时 `spacetime publish` 追加 `-c=on-conflict`,不执行自动导出和导入回灌。 自动迁移依赖两个引导密钥: - 导出旧库:优先使用 `deploy-state/migration-bootstrap-secret.previous.txt`,也就是旧模块编译时注入的密钥。 - 导入新库:使用当前发布包 `migration-bootstrap-secret.txt`,也就是新模块编译时注入的密钥。 如果旧库或新库的 `database_migration_operator` 表已经不为空,bootstrap secret 不能再越权授权新的操作员;此时必须由已有迁移操作员发起授权,或在部署目录 `.env.local` 中配置已授权操作员的连接 token: ```text GENARRATIVE_SPACETIME_MIGRATION_EXPORT_TOKEN=<旧库迁移操作员 token> GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN=<新库迁移操作员 token> ``` `GENARRATIVE_SPACETIME_MIGRATION_EXPORT_TOKEN` 只用于 schema 冲突时导出旧库;`GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN` 只用于清库发布新 wasm 后导入回灌。Jenkins 覆盖部署会尽量保留部署目录现有 `.env.local` 中的这两个 token,除非新发布包已经显式提供同名变量。 如果不是通过 Jenkins 部署脚本覆盖发布包,而是手工替换文件,必须在覆盖前保留旧 `migration-bootstrap-secret.txt`;否则旧库迁移 procedure 可能无法授权导出。 ### 删除表和删除字段 迁移文件来自旧模块时,可能包含新模块已经删除的表或字段。导入阶段按以下规则处理: - 迁移文件包含新模块已删除或不在白名单内的表时,不中断迁移;该表全部行计入 `skipped_row_count`,并在导入结束后统一展示 `dropped_table` 告警。 - 迁移行包含新模块已删除的旧字段时,导入 procedure 会尝试丢弃旧字段后继续反序列化;恢复成功则导入该行,并在导入结束后统一展示 `dropped_field` 告警。 - 新模块新增必填字段、字段类型变化、枚举不兼容等无法通过“丢弃旧字段”恢复的情况仍会失败并回滚,避免写入不完整数据。 本机导出时,先确保本机 SpacetimeDB 服务和源数据库可访问,然后授权本机调用身份: ```bash node scripts/spacetime-authorize-migration-operator.mjs \ --server dev \ --database xushi-p4wfr \ --bootstrap-secret <本机源库发布时输出的随机密钥> \ --operator-identity <本机 spacetime login show 中的 identity> \ --note "local export" ``` 导出脚本负责调用本机源库 procedure 并保存返回 JSON: ```bash node scripts/spacetime-export-migration-json.mjs \ --server dev \ --database xushi-p4wfr \ --out tmp/spacetime-migrations/source-2026-04-27.json ``` 把 `tmp/spacetime-migrations/source-2026-04-27.json` 复制到服务器后,在服务器上登录目标 SpacetimeDB,并授权服务器侧调用身份: ```bash node scripts/spacetime-authorize-migration-operator.mjs \ --server dev \ --database xushi-p4wfr \ --bootstrap-secret <服务器目标库发布时输出的随机密钥> \ --operator-identity <服务器 spacetime login show 中的 identity> \ --note "server import" ``` 导入脚本负责读取服务器本地文件并把 JSON 字符串通过 Web API request body 传入目标库 procedure。因为 JSON 不再放进 `spacetime call` 命令行参数,所以不会触发 Linux `spawn E2BIG`: ```bash node scripts/spacetime-import-migration-json.mjs \ --server dev \ --database xushi-p4wfr \ --bootstrap-secret <服务器目标库发布时输出的随机密钥> \ --in tmp/spacetime-migrations/source-2026-04-27.json ``` 如果目标库已有部分数据,且只想补充缺失行,使用增量模式: ```bash node scripts/spacetime-import-migration-json.mjs \ --server dev \ --database xushi-p4wfr \ --bootstrap-secret <服务器目标库发布时输出的随机密钥> \ --in tmp/spacetime-migrations/source-2026-04-27.json \ --incremental ``` 如果目标库对应表已有数据,并且本次文件应作为这些表的覆盖来源,再显式追加 `--replace-existing`。脚本会把覆盖范围限定为迁移文件内实际包含且本次会导入的表,避免分批导入时清空文件外的其它表。 默认情况下,脚本会自动完成三步: 1. `POST /v1/identity` 创建临时 Web API identity/token。 2. 使用当前机器 `spacetime` CLI 登录态调用 `authorize_database_migration_operator`,授权这个临时 identity。 3. 使用 `Authorization: Bearer <临时 token>` 导入迁移 JSON。文件不超过 `--chunk-size` 时直接调用 `import_database_migration_from_file`;超过阈值或直接导入触发 HTTP 413 时,先逐片调用 `put_database_migration_import_chunk`,再调用 `import_database_migration_from_chunks` 或 `import_database_migration_incremental_from_chunks`。 4. 分片上传或提交失败时,脚本会尽量调用 `clear_database_migration_import_chunks` 清理临时分片。 5. 导入请求结束后,脚本会用同一个临时 Web API token 调用 `revoke_database_migration_operator`,撤销该临时 identity。 所有直接访问 SpacetimeDB Web API 的 POST 请求必须显式发送 `Content-Type: application/json`。部分 SpacetimeDB 版本不会接受省略 content type 或附带非预期 media type 的请求,即使 body 本身是合法 JSON,也会返回 `HTTP 415`。 如果你已经有可用的数据库连接 token,也可以显式传 `--token `。这种情况下脚本不会自动授权;该 token 对应的 identity 必须已经是迁移操作员。 如果 `authorize_database_migration_operator` 返回 `当前 identity 未被授权执行数据库迁移`,说明当前机器 `spacetime` CLI 登录身份不是既有迁移操作员。表内已经存在操作员时,即使提供了正确 bootstrap secret,也不会允许非操作员继续扩权;需要先让既有操作员授权当前部署机 identity,或直接使用既有操作员 token 执行导出/导入。 正式导入前建议先加 `--dry-run`,确认 JSON 可解析、版本匹配、表名都在迁移白名单内。 `--dry-run` 不会模拟目标库主键或唯一约束冲突,因此增量模式的 `skipped_row_count` 只有真实导入时才准确。 如果 Jenkins 或 SpacetimeDB 返回 `HTTP 413`,优先降低导入流水线的 `CHUNK_SIZE`,例如 `262144`。该参数只影响上传到 procedure 的单片 request body,不改变迁移 JSON 的表范围和导入语义。 不要在只想追加数据时使用 `--replace-existing`。该参数会先删除覆盖范围内的目标表旧数据,再插入迁移文件中的数据;如果源文件不是完整快照,会造成目标表数据丢失。 如需分批迁移,可用逗号分隔表名: ```bash node scripts/spacetime-export-migration-json.mjs \ --database xushi-p4wfr \ --out tmp/spacetime-migrations/ai.json \ --include ai_task,ai_task_stage,ai_text_chunk,ai_result_reference ``` `--server` 支持 `dev`、`local`,也可以直接传 SpacetimeDB 服务器 URL。导出、授权、撤销默认走 `spacetime call`,使用当前机器的 CLI 登录态;导入默认走 Web API request body,避免大 JSON 触发命令行长度限制。数据库名可通过 `--database` 或 `GENARRATIVE_SPACETIME_DATABASE` 提供。 授权脚本额外支持: - `--bootstrap-secret` 或 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET` - `--operator-identity` 或 `GENARRATIVE_SPACETIME_MIGRATION_OPERATOR_IDENTITY` - `--note` ## 表范围 首版覆盖当前 private table 报错相关与主运行真相表: - 认证:`auth_store_snapshot`、`user_account`、`auth_identity`、`refresh_session` - AI:`ai_task`、`ai_task_stage`、`ai_text_chunk`、`ai_result_reference` - 运行存档与账户投影:`runtime_snapshot`、`runtime_setting`、`user_browse_history`、`profile_dashboard_state`、`profile_wallet_ledger`、`profile_invite_code`、`profile_referral_relation`、`profile_played_world`、`profile_membership`、`profile_recharge_order`、`profile_save_archive` - RPG 运行真相:`player_progression`、`chapter_progression`、`npc_state`、`story_session`、`story_event`、`inventory_slot`、`battle_state`、`treasure_record`、`quest_record`、`quest_log` - 自定义世界:`custom_world_profile`、`custom_world_session`、`custom_world_agent_session`、`custom_world_agent_message`、`custom_world_agent_operation`、`custom_world_draft_card`、`custom_world_gallery_entry` - 资产索引:`asset_object`、`asset_entity_binding` - 拼图:`puzzle_agent_session`、`puzzle_agent_message`、`puzzle_work_profile`、`puzzle_runtime_run` - 大鱼:`big_fish_creation_session`、`big_fish_agent_message`、`big_fish_asset_slot` `big_fish_runtime_run` 当前运行态已由前端本地运行服务承接,不再加入迁移白名单;但旧库仍可能存在该表。为避免热升级被 “Removing the table big_fish_runtime_run requires a manual migration” 阻断,模块发布期可以保留兼容空壳表,后续确认旧数据可丢弃后再走正式删除表迁移。 后续新增 SpacetimeDB 表时,必须同步把表加入迁移白名单与本文档。 ## 风险与限制 迁移 JSON 作为 procedure 返回值和 HTTP request body 传递,会受 SpacetimeDB 调用响应体、请求体以及中间代理大小限制。导入端已经内置分片上传来规避 `HTTP 413` 请求体限制;如果导出响应本身过大,仍需先按 `include_tables` 分批导出。 `spacetime call` 在 PowerShell 中手写 JSON 容易被剥掉双引号。导入大文件时也不能把完整 JSON 放进命令行参数,否则 Linux 会在启动子进程时返回 `spawn E2BIG`。推荐使用仓库里的 Node 脚本,由脚本直接走 Web API request body,避免 shell 二次处理和命令行长度限制。