18 KiB
SpacetimeDB JSON 字符串迁移 procedure 设计
背景
spacetime sql 只能稳定读取 public 表或数据库 owner 可见表。当前 ai_result_reference 等运行真相表保持 private,直接 SQL 导出会遇到 no such table 或 private table 提示,不能作为跨服务器迁移的稳定方案。
SpacetimeDB reducer 必须保持确定性,不能访问文件系统和网络。procedure 可以返回数据,也可以在事务中读取 private 表,因此迁移改为:
spacetime-module内的导出 procedure 读取迁移白名单表,并直接返回迁移 JSON 字符串。- Node 运维脚本默认通过
spacetime call调用导出 procedure,把返回的 JSON 字符串写入本地文件。 - Node 运维脚本读取本地 JSON 文件内容。导入时默认先通过
POST /v1/identity创建临时 Web API identity/token,再用当前 CLI 登录态把该 identity 授权为迁移操作员,最后通过 HTTP request body 把 JSON 字符串传给导入 procedure。 - 导入 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 模块只调用注入时间的函数,避免发布后在 maincloud 触发 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 调用权限,不会被导出或导入,避免把源库的运维权限复制到目标库。
首次授权时,操作员表为空,必须通过编译进模块的 GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET 引导密钥授权第一位操作员。发布脚本会在构建或发布 SpacetimeDB 模块时自动生成一份强随机引导密钥、注入 wasm 编译环境,并在控制台显示;运维人员必须记录对应数据库本次发布输出的密钥。表内已经存在操作员后,后续授权与撤销只能由已有操作员发起;此时不再接受引导密钥越权扩权。
新增 procedure:
authorize_database_migration_operator: 授权或更新迁移操作员备注。revoke_database_migration_operator: 撤销迁移操作员。
运维流程:
npm run spacetime:publish:maincloud -- --database <database>
# 控制台会输出:
# [spacetime:maincloud] 迁移引导密钥: <本次发布随机密钥>
发布完成后,在同一台机器上用当前 spacetime login 身份授权操作员:
node scripts/spacetime-authorize-migration-operator.mjs \
--server maincloud \
--database xushi-p4wfr \
--bootstrap-secret <本次发布随机密钥> \
--operator-identity <identity-hex> \
--note "2026-04-27 migration"
迁移完成后可以撤销临时操作员:
node scripts/spacetime-revoke-migration-operator.mjs \
--server maincloud \
--database xushi-p4wfr \
--operator-identity <identity-hex>
生产环境建议迁移完成后用 --no-migration-bootstrap-secret 重新发布一个未设置 GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET 的模块版本,避免引导密钥长期留在 wasm 中。
发布脚本密钥行为
当前所有会构建或发布 spacetime-module 的脚本默认都会生成并显示迁移引导密钥:
npm run spacetime:publish:maincloud:在本机cargo build前生成密钥,控制台输出[spacetime:maincloud] 迁移引导密钥: ...。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 时也会再次显示该文件里的密钥。
如果迁移完成后不希望 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)
输入字段:
migration_json: 导出 procedure 生成的完整迁移 JSON 字符串。include_tables: 可选表名白名单。为空时导入文件内所有支持表。replace_existing: 是否先清空本次迁移文件内实际导入的目标表。不会清空迁移文件未包含的表;分批迁移时只覆盖当前批次。dry_run: 只解析和统计,不写表。
导入模式:
- 默认严格追加:不清空目标表,逐行插入;遇到主键或唯一约束冲突时失败并回滚,适合确认目标库没有同表旧数据时使用。
- 增量追加:调用
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 脚本
发布冲突自动迁移
npm run spacetime:publish:maincloud 默认采用冲突感知发布:
- 先不清库发布新 wasm。
- 如果发布成功,流程结束。
- 如果发布失败且输出可判定为 schema 冲突,脚本自动导出旧库迁移 JSON 到
tmp/spacetime-migrations/maincloud/<database>/<timestamp>.json。 - 导出成功后执行清库发布新 wasm。
- 新 wasm 发布成功后,把第 3 步导出的 JSON 自动导入回灌。
SpacetimeDB 2.1 对 schema 冲突的报错文案可能不再包含 schema conflict,而是直接提示 manual migration、default value annotation 或 --delete-data。发布脚本必须把这些文案同样识别为可迁移冲突,否则会停在原始失败而不进入导出回灌流程。
新增字段优先采用低风险热升级策略:旧字段顺序保持不变,新字段追加到表尾,并用 #[default(...)] 提供旧行默认值。只有仍无法通过发布器检查时,才执行清库发布与 JSON 回灌。
任一阶段失败都会中止流程,并保留已经导出的迁移 JSON。非 schema 冲突的发布失败不会进入迁移流程。
npm run spacetime:publish:maincloud -- --database xushi-p4wfr
可选参数:
--no-migrate-on-conflict:禁用冲突自动迁移,只保留原始发布失败。--migration-dir <dir>:指定迁移 JSON 输出目录。--clear-database:显式清库发布;该模式代表人工确认清库,不触发自动迁移。
冲突自动迁移需要发布脚本本次生成的 GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET。因此不要和 --no-migration-bootstrap-secret 同时使用。
部署流水线自动迁移
Ubuntu 发布包的 start.sh 与 Jenkins Genarrative-Deploy 也采用同一套迁移 procedure,但迁移触发点在部署目录内:
- Jenkins 覆盖部署前,如果旧部署目录存在
migration-bootstrap-secret.txt,先保存到deploy-state/migration-bootstrap-secret.previous.txt。旧密钥快照属于部署状态,不再写入run/,避免sudo启停脚本生成的 root 私有运行目录阻断后续覆盖部署。 - Jenkins 复制新发布包,包含新 wasm、新
migration-bootstrap-secret.txt和scripts/spacetime-*.mjs迁移脚本。 - 新
start.sh先不清库发布当前包内spacetime_module.wasm。 - 如果发布成功,流程结束。
- 如果发布失败且输出可判定为 schema 冲突,
start.sh用旧密钥授权导出旧库 JSON。 - 导出成功后,
start.sh清库发布新 wasm。 - 新 wasm 发布成功后,
start.sh用新密钥授权导入,并以--replace-existing回灌迁移 JSON。
自动迁移 JSON 默认保存到部署目录的 database-migrations/<database>/<timestamp>.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,也就是新模块编译时注入的密钥。
如果不是通过 Jenkins 部署脚本覆盖发布包,而是手工替换文件,必须在覆盖前保留旧 migration-bootstrap-secret.txt;否则旧库迁移 procedure 可能无法授权导出。
删除表和删除字段
迁移文件来自旧模块时,可能包含新模块已经删除的表或字段。导入阶段按以下规则处理:
- 迁移文件包含新模块已删除或不在白名单内的表时,不中断迁移;该表全部行计入
skipped_row_count,并在导入结束后统一展示dropped_table告警。 - 迁移行包含新模块已删除的旧字段时,导入 procedure 会尝试丢弃旧字段后继续反序列化;恢复成功则导入该行,并在导入结束后统一展示
dropped_field告警。 - 新模块新增必填字段、字段类型变化、枚举不兼容等无法通过“丢弃旧字段”恢复的情况仍会失败并回滚,避免写入不完整数据。
本机导出时,先确保本机 SpacetimeDB 服务和源数据库可访问,然后授权本机调用身份:
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:
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,并授权服务器侧调用身份:
node scripts/spacetime-authorize-migration-operator.mjs \
--server maincloud \
--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:
node scripts/spacetime-import-migration-json.mjs \
--server maincloud \
--database xushi-p4wfr \
--bootstrap-secret <服务器目标库发布时输出的随机密钥> \
--in tmp/spacetime-migrations/source-2026-04-27.json
如果目标库已有部分数据,且只想补充缺失行,使用增量模式:
node scripts/spacetime-import-migration-json.mjs \
--server maincloud \
--database xushi-p4wfr \
--bootstrap-secret <服务器目标库发布时输出的随机密钥> \
--in tmp/spacetime-migrations/source-2026-04-27.json \
--incremental
如果目标库对应表已有数据,并且本次文件应作为这些表的覆盖来源,再显式追加 --replace-existing。脚本会把覆盖范围限定为迁移文件内实际包含且本次会导入的表,避免分批导入时清空文件外的其它表。
默认情况下,脚本会自动完成三步:
POST /v1/identity创建临时 Web API identity/token。- 使用当前机器
spacetimeCLI 登录态调用authorize_database_migration_operator,授权这个临时 identity。 - 使用
Authorization: Bearer <临时 token>调用import_database_migration_from_file,把完整迁移 JSON 放在 HTTP body 中。 - 导入请求结束后,脚本会用同一个临时 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 <web-api-token>。这种情况下脚本不会自动授权;该 token 对应的 identity 必须已经是迁移操作员。
正式导入前建议先加 --dry-run,确认 JSON 可解析、版本匹配、表名都在迁移白名单内。
--dry-run 不会模拟目标库主键或唯一约束冲突,因此增量模式的 skipped_row_count 只有真实导入时才准确。
不要在只想追加数据时使用 --replace-existing。该参数会先删除覆盖范围内的目标表旧数据,再插入迁移文件中的数据;如果源文件不是完整快照,会造成目标表数据丢失。
如需分批迁移,可用逗号分隔表名:
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、maincloud,也可以直接传 SpacetimeDB 服务器 URL。导出、授权、撤销默认走 spacetime call,使用当前机器的 CLI 登录态;导入默认走 Web API request body,避免大 JSON 触发命令行长度限制。数据库名可通过 --database、GENARRATIVE_SPACETIME_MAINCLOUD_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 当前运行态已由前端本地运行服务承接,不再加入迁移白名单;但 maincloud 旧库仍可能存在该表。为避免热升级被 “Removing the table big_fish_runtime_run requires a manual migration” 阻断,模块发布期可以保留兼容空壳表,后续确认旧数据可丢弃后再走正式删除表迁移。
后续新增 SpacetimeDB 表时,必须同步把表加入迁移白名单与本文档。
风险与限制
迁移 JSON 作为 procedure 返回值和 HTTP request body 传递,会受 SpacetimeDB 调用响应体、请求体以及中间代理大小限制。数据量较大时,先按 include_tables 分批迁移;若单表本身过大,再补充分片 procedure,而不是恢复 HTTP 文件桥。
spacetime call 在 PowerShell 中手写 JSON 容易被剥掉双引号。导入大文件时也不能把完整 JSON 放进命令行参数,否则 Linux 会在启动子进程时返回 spawn E2BIG。推荐使用仓库里的 Node 脚本,由脚本直接走 Web API request body,避免 shell 二次处理和命令行长度限制。