新增旧玩法表阶段一清空迁移能力
增加受 migration operator 保护的固定 63 张旧表清空 procedure 生成 spacetime-client procedure、输入和结果绑定 更新后端数据契约与两阶段退役决策记录
This commit is contained in:
@@ -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::*;
|
||||
|
||||
+59
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+15
@@ -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;
|
||||
}
|
||||
+20
@@ -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;
|
||||
}
|
||||
+17
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user