新增旧玩法表阶段一清空迁移能力
Project CI / Repository checks (pull_request) Successful in 3m18s
Project CI / Frontend tests (pull_request) Successful in 4m30s
Project CI / Backend tests (pull_request) Successful in 8m1s
Project CI / Native shell tests (pull_request) Successful in 14m55s

增加受 migration operator 保护的固定 63 张旧表清空 procedure

生成 spacetime-client procedure、输入和结果绑定

更新后端数据契约与两阶段退役决策记录
This commit is contained in:
2026-09-02 16:28:14 +08:00
parent fe35fcf264
commit 617898d0f9
8 changed files with 304 additions and 1 deletions
@@ -15,6 +15,15 @@
- 关联文档:相关 PRD、技术文档、提交或 Issue
```
## 2026-09-02 旧玩法表采用两阶段退役清理
- 背景:旧创作模板的业务代码已退出现役编译链,但 SpacetimeDB 中的历史表仍需先完成数据清理;直接删除表定义会扩大 schema 迁移和客户端兼容风险。
- 决策:阶段一只在 `spacetime-module/src/migration.rs` 增加受 `database_migration_operator` 保护的 `clear_retired_database_tables` procedure。procedure 使用固定的 63 张旧玩法表清单,不接受动态表名;`dry_run=true` 只返回逐表行数统计,`dry_run=false` 在同一事务内逐表清空,任一失败整体回滚。阶段一不删除表定义、不修改 `legacy_schema/**`、migration 导入导出白名单或生成 bindings。
- 阶段边界:清理清单包含旧 gameplay、`custom_world`、Puzzle / Puzzle Clear、Bark Battle、Match3D、Jump Hop、Wooden Fish、Square Hole、Visual Novel 和 Big Fish 表;`runtime_setting``runtime_snapshot``user_browse_history``creation_entry_config` 等现役表明确排除。阶段二只有在备份、客户端兼容性和运行态确认完成后,才评估从 module 定义与 migration 白名单移除空表,并按 schema / bindings 流程发布;固定清单旁保留 TODO。
- 影响范围:SpacetimeDB migration procedure、`spacetime-client` 生成 bindings、后端数据契约和本决策记录;禁止新增 SQL `DROP TABLE``--delete-data=always` 或直接写系统表的实现。
- 验证方式:固定清单测试确认数量为 63 且不含现役表;本地数据库以已授权 operator 执行 dry-run,确认 63 张表均返回 0 行且未写入;apply 的单事务回滚由 procedure 实现,实际 apply 仅在另行授权的维护窗口执行。另运行 bindings 生成、SpacetimeDB schema / runtime 检查、编码和 diff 门禁。
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md``server-rs/crates/spacetime-module/src/migration.rs`
## 2026-08-31 DirectProject 客户端扩展按独立 Skill/MCP 导入
- 背景:DirectProject 需要使用用户在 AGC 客户端导入的市面原生 Skill、MCP 和 Plugin 内容,但第三方内容不应直接安装到运行时 Codex,也不应要求用户转换为 AGC 自定义格式。
@@ -186,7 +186,8 @@ npm run check:server-rs-ddd
8. 多列索引按 SpacetimeDB 绑定生成的元组参数直接传入,例如 `.filter((source_type, profile_id, played_day))`;前缀查询只传前缀元组,例如 `.filter((scope_kind, scope_id.as_str()))`。不要为了绕过类型问题退回整表遍历。
9. procedure result 必须返回 typed snapshot / typed value。`spacetime-client` mapper 不得再通过 `row_json/session_json/work_json/items_json/run_json/event_json/feedback_json: Option<String>` 做跨层 JSON 字符串传输,也不得在 mapper 里反序列化旧 `*JsonRecord` 兼容结构。业务内部持久化字段如 `profile_payload_json``levels_json` 等不属于 procedure result 载荷例外,仍按各自表契约处理。
10. procedure 需要按调用者 identity 鉴权时,必须先从外层 `ProcedureContext::sender()` 捕获 caller,再把 caller 显式传入 `try_with_tx` 闭包内的事务函数。当前 workspace 锁定 SpacetimeDB `2.8.3`;鉴权边界不得依赖事务上下文的隐式 sender 语义,即使 SDK 升级也继续保持显式 caller 参数。
11. 修改后运行:
11. 旧创作模板历史表采用两阶段退役:阶段一只允许 migration operator 调用固定范围的 `clear_retired_database_tables` procedure 清空 63 张旧玩法表,输入仅有 `dry_run``dry_run=true` 只返回逐表行数统计,`dry_run=false` 在单一事务内逐表删除全部行,任一失败整体回滚。清单必须在 `spacetime-module/src/migration.rs` 中静态维护,不接受调用方传入任意表名;`runtime_setting``runtime_snapshot``user_browse_history``creation_entry_config` 等现役表不属于清理范围。阶段一保留所有旧表定义、`legacy_schema/**`、migration 导入导出白名单和生成 bindings,不执行 `DROP TABLE``--delete-data=always` 或系统表写入。阶段二待备份、客户端兼容性和运行态确认完成后,另行评估从 module 定义与 migration 白名单移除空表,并同步 schema / bindings;当前在固定清单旁保留 TODO,不提前改动表定义。
12. 修改后运行:
```bash
npm run spacetime:generate
@@ -178,6 +178,7 @@ pub mod claim_profile_recharge_order_expiration_schedule_and_return_procedure;
pub mod claim_profile_task_reward_and_return_procedure;
pub mod clean_editor_image_asset_kind_and_return_procedure;
pub mod clear_database_migration_import_chunks_procedure;
pub mod clear_retired_database_tables_procedure;
pub mod close_profile_recharge_order_and_return_procedure;
pub mod combat_outcome_type;
pub mod compact_external_generation_job_payloads_and_return_procedure;
@@ -223,6 +224,9 @@ pub mod custom_world_session_table;
pub mod custom_world_session_type;
pub mod custom_world_theme_mode_type;
pub mod database_migration_authorize_operator_input_type;
pub mod database_migration_clear_retired_tables_input_type;
pub mod database_migration_clear_retired_tables_result_type;
pub mod database_migration_clear_table_stat_type;
pub mod database_migration_export_input_type;
pub mod database_migration_import_chunk_input_type;
pub mod database_migration_import_chunk_table;
@@ -1052,6 +1056,7 @@ pub use claim_profile_recharge_order_expiration_schedule_and_return_procedure::c
pub use claim_profile_task_reward_and_return_procedure::claim_profile_task_reward_and_return;
pub use clean_editor_image_asset_kind_and_return_procedure::clean_editor_image_asset_kind_and_return;
pub use clear_database_migration_import_chunks_procedure::clear_database_migration_import_chunks;
pub use clear_retired_database_tables_procedure::clear_retired_database_tables;
pub use close_profile_recharge_order_and_return_procedure::close_profile_recharge_order_and_return;
pub use combat_outcome_type::CombatOutcome;
pub use compact_external_generation_job_payloads_and_return_procedure::compact_external_generation_job_payloads_and_return;
@@ -1097,6 +1102,9 @@ pub use custom_world_session_table::*;
pub use custom_world_session_type::CustomWorldSession;
pub use custom_world_theme_mode_type::CustomWorldThemeMode;
pub use database_migration_authorize_operator_input_type::DatabaseMigrationAuthorizeOperatorInput;
pub use database_migration_clear_retired_tables_input_type::DatabaseMigrationClearRetiredTablesInput;
pub use database_migration_clear_retired_tables_result_type::DatabaseMigrationClearRetiredTablesResult;
pub use database_migration_clear_table_stat_type::DatabaseMigrationClearTableStat;
pub use database_migration_export_input_type::DatabaseMigrationExportInput;
pub use database_migration_import_chunk_input_type::DatabaseMigrationImportChunkInput;
pub use database_migration_import_chunk_table::*;
@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::database_migration_clear_retired_tables_input_type::DatabaseMigrationClearRetiredTablesInput;
use super::database_migration_clear_retired_tables_result_type::DatabaseMigrationClearRetiredTablesResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct ClearRetiredDatabaseTablesArgs {
pub input: DatabaseMigrationClearRetiredTablesInput,
}
impl __sdk::InModule for ClearRetiredDatabaseTablesArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `clear_retired_database_tables`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait clear_retired_database_tables {
fn clear_retired_database_tables(&self, input: DatabaseMigrationClearRetiredTablesInput) {
self.clear_retired_database_tables_then(input, |_, _| {});
}
fn clear_retired_database_tables_then(
&self,
input: DatabaseMigrationClearRetiredTablesInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<DatabaseMigrationClearRetiredTablesResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl clear_retired_database_tables for super::RemoteProcedures {
fn clear_retired_database_tables_then(
&self,
input: DatabaseMigrationClearRetiredTablesInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<DatabaseMigrationClearRetiredTablesResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, DatabaseMigrationClearRetiredTablesResult>(
"clear_retired_database_tables",
ClearRetiredDatabaseTablesArgs { input },
__callback,
);
}
}
@@ -0,0 +1,15 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct DatabaseMigrationClearRetiredTablesInput {
pub dry_run: bool,
}
impl __sdk::InModule for DatabaseMigrationClearRetiredTablesInput {
type Module = super::RemoteModule;
}
@@ -0,0 +1,20 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::database_migration_clear_table_stat_type::DatabaseMigrationClearTableStat;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct DatabaseMigrationClearRetiredTablesResult {
pub ok: bool,
pub dry_run: bool,
pub table_stats: Vec<DatabaseMigrationClearTableStat>,
pub error_message: Option<String>,
}
impl __sdk::InModule for DatabaseMigrationClearRetiredTablesResult {
type Module = super::RemoteModule;
}
@@ -0,0 +1,17 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct DatabaseMigrationClearTableStat {
pub table_name: String,
pub row_count_before: u64,
pub cleared_row_count: u64,
}
impl __sdk::InModule for DatabaseMigrationClearTableStat {
type Module = super::RemoteModule;
}
@@ -361,6 +361,163 @@ macro_rules! clear_migration_table {
};
}
// 阶段一固定清空清单:只覆盖已经退役的玩法表,不接受调用方动态指定表名。
// TODO(phase 2): 完成数据备份、客户端兼容性和运行态确认后,再从 active.rs、legacy_schema、
// migration 白名单和生成绑定中移除这些表定义;本阶段只清空行,不改变 schema。
macro_rules! retired_migration_tables {
($macro_name:ident $(, $arg:expr)*) => {
$macro_name! {
$($arg,)*
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,
puzzle_agent_session,
puzzle_background_compile_task,
puzzle_agent_message,
puzzle_work_profile,
puzzle_event,
puzzle_runtime_run,
puzzle_leaderboard_entry,
puzzle_clear_agent_session,
puzzle_clear_work_profile,
puzzle_clear_runtime_run,
puzzle_clear_event,
bark_battle_draft_config,
bark_battle_published_config,
bark_battle_runtime_run,
bark_battle_score_record,
bark_battle_leaderboard_entry,
bark_battle_work_stats_projection,
bark_battle_personal_best_projection,
match3d_agent_session,
match3d_agent_message,
match_3_d_work_profile,
match3d_runtime_run,
jump_hop_agent_session,
jump_hop_work_profile,
jump_hop_runtime_run,
jump_hop_event,
jump_hop_leaderboard_entry,
wooden_fish_agent_session,
wooden_fish_work_profile,
wooden_fish_runtime_run,
wooden_fish_event,
square_hole_agent_session,
square_hole_agent_message,
square_hole_work_profile,
square_hole_runtime_run,
visual_novel_agent_session,
visual_novel_agent_message,
visual_novel_work_profile,
visual_novel_runtime_run,
visual_novel_runtime_history_entry,
visual_novel_runtime_event,
big_fish_creation_session,
big_fish_agent_message,
big_fish_asset_slot,
big_fish_runtime_run,
big_fish_event,
}
};
}
macro_rules! retired_migration_table_name_list {
($($table:ident),+ $(,)?) => {
&[$(stringify!($table)),+]
};
}
const RETIRED_MIGRATION_TABLE_NAMES: &[&str] =
retired_migration_tables!(retired_migration_table_name_list);
macro_rules! clear_retired_migration_table {
($ctx:expr, $dry_run:expr, $stats:expr, $($table:ident),+ $(,)?) => {
$(
let rows = $ctx.db.$table().iter().collect::<Vec<_>>();
let row_count_before = rows.len() as u64;
if !$dry_run {
for row in rows {
$ctx.db.$table().delete(row);
}
}
$stats.push(DatabaseMigrationClearTableStat {
table_name: stringify!($table).to_string(),
row_count_before,
cleared_row_count: if $dry_run { 0 } else { row_count_before },
});
)+
};
}
fn clear_retired_database_tables_tx(
ctx: &ReducerContext,
dry_run: bool,
) -> Vec<DatabaseMigrationClearTableStat> {
let mut stats = Vec::with_capacity(RETIRED_MIGRATION_TABLE_NAMES.len());
retired_migration_tables!(clear_retired_migration_table, ctx, dry_run, stats);
stats
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
pub struct DatabaseMigrationClearRetiredTablesInput {
pub dry_run: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
pub struct DatabaseMigrationClearTableStat {
pub table_name: String,
pub row_count_before: u64,
pub cleared_row_count: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
pub struct DatabaseMigrationClearRetiredTablesResult {
pub ok: bool,
pub dry_run: bool,
pub table_stats: Vec<DatabaseMigrationClearTableStat>,
pub error_message: Option<String>,
}
#[spacetimedb::procedure]
pub fn clear_retired_database_tables(
ctx: &mut ProcedureContext,
input: DatabaseMigrationClearRetiredTablesInput,
) -> DatabaseMigrationClearRetiredTablesResult {
let dry_run = input.dry_run;
let caller = ctx.sender();
match ctx.try_with_tx(|tx| {
require_migration_operator(tx, caller)?;
Ok::<_, String>(clear_retired_database_tables_tx(tx, dry_run))
}) {
Ok(table_stats) => DatabaseMigrationClearRetiredTablesResult {
ok: true,
dry_run,
table_stats,
error_message: None,
},
Err(error) => DatabaseMigrationClearRetiredTablesResult {
ok: false,
dry_run,
table_stats: Vec::new(),
error_message: Some(error),
},
}
}
// 迁移权限独立存表,避免把 private 表导出能力开放给任意登录身份。
#[spacetimedb::procedure]
pub fn authorize_database_migration_operator(
@@ -1713,6 +1870,23 @@ fn is_supported_migration_table(table_name: &str) -> bool {
mod migration_bootstrap_secret_tests {
use super::*;
#[test]
fn retired_migration_table_list_is_fixed_and_excludes_active_tables() {
assert_eq!(RETIRED_MIGRATION_TABLE_NAMES.len(), 63);
let mut unique_names = RETIRED_MIGRATION_TABLE_NAMES.to_vec();
unique_names.sort_unstable();
unique_names.dedup();
assert_eq!(unique_names.len(), RETIRED_MIGRATION_TABLE_NAMES.len());
for active_table in [
"runtime_setting",
"runtime_snapshot",
"user_browse_history",
"creation_entry_config",
] {
assert!(!RETIRED_MIGRATION_TABLE_NAMES.contains(&active_table));
}
}
#[test]
fn old_profile_redeem_code_rows_default_to_open_validity_window() {
let normalized = normalize_migration_row(