ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SpacetimeDB C++ 查询构建器视图测试模块 sdk-test-view-pk-cpp 深度解析:主键保留视图与右半连接实战

SpacetimeDB C++ 查询构建器视图测试模块 sdk-test-view-pk-cpp 深度解析:主键保留视图与右半连接实战 SpacetimeDB C 查询构建器视图测试模块 sdk-test-view-pk-cpp 深度解析主键保留视图与右半连接实战【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB导读本文以 SpacetimeDB 仓库中的sdk-test-view-pk-cpp模块为研究对象系统讲解如何在 C 模块中通过查询构建器query builder定义查询返回型视图query-returning views如何在视图中保留表的主键Primary Key语义以及如何利用**右半连接right semijoin**从带索引的成员表关联到玩家表行。读完本文你将掌握SPACETIMEDB_VIEW宏的完整用法、QueryT与right_semijoin的调用模式以及通过 Emscripten 将 C 模块编译为 WebAssembly 的完整构建链路并能够对照 Rust 与 C# 的同构实现理解跨语言一致性的设计思想。一、模块定位验证 C 查询构建器视图能力的标准测试件sdk-test-view-pk-cpp是 SpacetimeDB 中用于验证C 查询构建器query-builder视图View功能的测试模块位于 modules/sdk-test-view-pk-cpp/。它的设计目标非常明确——作为一套可运行、可断言的标准测试件覆盖以下三个核心能力点README 中## Coverage一节列出的原始范围query-returning views over base tables视图以查询而非物化结果集的形式返回对基础表的引用PK-preserving query views查询返回型视图必须保留底层表的主键信息使客户端能够基于主键做行级更新/删除跟踪right semijoins from indexed membership tables to player rows从带有索引btree index的成员表出发通过右半连接回到玩家表得到在成员表中出现过的玩家集合。该模块刻意保持与另外两个语言实现逐表、逐 reducer、逐视图同构Rust 版modules/sdk-test-view-pk/src/lib.rsC# 版modules/sdk-test-view-pk-cs/Lib.cs这意味着同样的player_id关联逻辑在三种 SDK 中拥有完全一致的表结构、数据操作和查询语义是跨语言一致性验证的关键样本。二、表结构设计一个玩家表 两个带索引的成员表视图要关联的数据基础是三个公开表全部在 modules/sdk-test-view-pk-cpp/src/lib.cpp 中定义表访问器名字段约束ViewPkPlayerview_pk_playerid: uint64_t、name: std::stringid为主键ViewPkMembershipview_pk_membershipid: uint64_t、player_id: uint64_tid为主键player_id建 btree 索引ViewPkMembershipSecondaryview_pk_membership_secondaryid: uint64_t、player_id: uint64_tid为主键player_id建 btree 索引2.1 表定义宏的写法struct ViewPkPlayer { uint64_t id; std::string name; }; SPACETIMEDB_STRUCT(ViewPkPlayer, id, name) SPACETIMEDB_TABLE(ViewPkPlayer, view_pk_player, Public) FIELD_PrimaryKey(view_pk_player, id)struct ViewPkMembership { uint64_t id; uint64_t player_id; }; SPACETIMEDB_STRUCT(ViewPkMembership, id, player_id) SPACETIMEDB_TABLE(ViewPkMembership, view_pk_membership, Public) FIELD_PrimaryKey(view_pk_membership, id) FIELD_Index(view_pk_membership, player_id)要点解读SPACETIMEDB_STRUCT声明普通 C struct 为可序列化的 SpacetimeDB 行类型SPACETIMEDB_TABLE将其注册为公开表FIELD_PrimaryKey声明主键字段这里的主键是id类型为uint64_tFIELD_Index为player_id建立 btree 索引——右半连接两侧之一必须带索引这是 SpacetimeDB 查询优化器能够高效执行 semijoin 的前提两个成员表结构完全相同仅访问器名不同其目的是验证两张不同的索引表分别与同一张玩家表做右半连接这一场景对应视图sender_view_pk_players_a与sender_view_pk_players_b。与 Rust 版对照modules/sdk-test-view-pk/src/lib.rsC 的FIELD_Index对应 Rust 的#[index(btree)]属性表定义语义完全对齐。三、Reducer 层为视图准备测试数据视图本身是只读的因此模块通过四个 reducer 来写入与更新数据全部定义在 modules/sdk-test-view-pk-cpp/src/lib.cppReducer作用insert_view_pk_player(id, name)插入一名玩家update_view_pk_player(id, name)按主键id更新玩家姓名insert_view_pk_membership(id, player_id)向主成员表插入一条成员记录insert_view_pk_membership_secondary(id, player_id)向次成员表插入一条成员记录3.1 插入与按主键更新SPACETIMEDB_REDUCER(insert_view_pk_player, ReducerContext ctx, uint64_t id, std::string name) { ctx.db[view_pk_player].insert(ViewPkPlayer{id, std::move(name)}); return Ok(); } SPACETIMEDB_REDUCER(update_view_pk_player, ReducerContext ctx, uint64_t id, std::string name) { ctx.db[view_pk_player_id].update(ViewPkPlayer{id, std::move(name)}); return Ok(); }这里有一个值得注意的细节更新操作直接使用由FIELD_PrimaryKey(view_pk_player, id)生成的索引访问器view_pk_player_id进行主键寻址更新。这印证了主键声明在表访问层面生成的能力——view_pk_player_id即按主键id索引的表访问器update会定位到id匹配的行并整体覆盖为新值。同样的能力在查询构建器中体现为视图的PK-preserving性质见第四节。Rust 版对应的写法是ctx.db.view_pk_player().id().update(...)modules/sdk-test-view-pk/src/lib.rsC# 版是ctx.Db.view_pk_player.id.Update(...)modules/sdk-test-view-pk-cs/Lib.cs三者语义一一对应。四、视图层三种视图与主键保留PK-Preserving机制4.1 三种视图一览模块通过SPACETIMEDB_VIEW宏定义三个公开视图modules/sdk-test-view-pk-cpp/src/lib.cppusing ViewPkPlayerQuery QueryViewPkPlayer; // 视图 1直接覆盖玩家表全部行 SPACETIMEDB_VIEW(ViewPkPlayerQuery, all_view_pk_players, Public, ViewContext ctx) { return ctx.from[view_pk_player]; } // 视图 2主成员表 → 玩家表 右半连接 SPACETIMEDB_VIEW(ViewPkPlayerQuery, sender_view_pk_players_a, Public, ViewContext ctx) { return ctx.from[view_pk_membership].right_semijoin( ctx.from[view_pk_player], [](const auto membership, const auto player) { return membership.player_id.eq(player.id); }); } // 视图 3次成员表 → 玩家表 右半连接 SPACETIMEDB_VIEW(ViewPkPlayerQuery, sender_view_pk_players_b, Public, ViewContext ctx) { return ctx.from[view_pk_membership_secondary].right_semijoin( ctx.from[view_pk_player], [](const auto membership, const auto player) { return membership.player_id.eq(player.id); }); }三个视图的访问器名客户端可见名称为all_view_pk_playerssender_view_pk_players_asender_view_pk_players_b4.2 视图的返回类型约束源码级验证SPACETIMEDB_VIEW宏对返回类型有严格的编译期校验见 crates/bindings-cpp/include/spacetimedb/view_macros.h返回类型必须是以下之一std::vectorT、std::optionalT、QueryT或其它查询构建器返回值其中T必须是可序列化的 SpacetimeType视图目前必须声明为Publicstatic_assert(access_enum Public, Views must be Public...)会在编译期拦截私有视图视图当前只接受唯一的ctx_param参数ViewContext ctx或AnonymousViewContext ctx带参视图参数化视图仍处于 TODO 状态视图通过__preinit__40_view_name导出符号在优先级 40 阶段注册保证表与 reducer 已先行注册主键信息由VIEW_PrimaryKey派生的__preinit__41_*符号在优先级 41 注册。也就是说QueryViewPkPlayer是一种惰性查询返回值它不立即物化结果而是把对view_pk_player表及其主键元数据的引用交给运行时由订阅/查询子系统按需求值。这正是 query-returning views 与返回std::vector的物化视图的本质区别。4.3 PK-Preserving为什么视图能保留主键视图all_view_pk_players直接返回ctx.from[view_pk_player]其结果行的行类型仍是ViewPkPlayer——而该类型的id字段早已通过FIELD_PrimaryKey(view_pk_player, id)登记为主键。因此客户端订阅视图后可以按id精确追踪行的插入/更新/删除增量更新 reducerupdate_view_pk_player引发的行变更会以主键定位的方式下发客户端无需全表比对即可完成本地一致性维护两个右半连接视图虽然改变了行的来源集合只包含匹配玩家但输出行类型不变主键信息随之保留——这就是 PK-preserving query views 的全部含义。对照 Rust 实现#[view(accessor all_view_pk_players, public)]返回impl QueryViewPkPlayermodules/sdk-test-view-pk/src/lib.rs类型层面的主键元数据由#[table]派生宏自动携带语义一致。五、右半连接Right Semijoin原理与源码印证5.1 语义解读left.right_semijoin(right, predicate)的结果是满足 predicate 的右表行集合此处为view_pk_player中所有player_id id匹配上的行左表仅作为过滤条件来源其行不会出现在结果中。对sender_view_pk_players_a而言左表view_pk_membership成员表player_id带 btree 索引右表view_pk_player玩家表id为主键谓词membership.player_id.eq(player.id)结果 被至少一条成员记录引用的所有玩家行。同一个玩家可能被多条成员记录引用但结果集中该玩家只会出现一次semijoin 是集合语义等价于 SQL 的WHERE EXISTS这正是半连接区别于普通 join 的关键。5.2 源码级印证right_semijoin的实现位置right_semijoin的声明与实现位于crates/bindings-cpp/include/spacetimedb/query_builder/table.hTable与FromWhere两种调用方的成员方法crates/bindings-cpp/include/spacetimedb/query_builder/join.h内部实现right_semijoin_impl。从签名可见TableTLeftRow,...对应ctx.from[view_pk_membership]与FromWhereTRightRow,...对应带条件/索引的右表引用均支持right_semijoin(right, predicate)谓词通过TFn predicate泛型捕获——这正是视图代码中 lambda[](const auto membership, const auto player) { ... }的落点。索引FIELD_Index(view_pk_membership, player_id)与主键FIELD_PrimaryKey(view_pk_player, id)在此构成连接两侧的索引化匹配路径。5.3 与 Rust / C# 的逐行对照环节C本模块RustC#右表引用ctx.from[view_pk_player]ctx.from.view_pk_player()ctx.From.view_pk_player()调用.right_semijoin(...).right_semijoin(...).RightSemijoin(...)谓词membership.player_id.eq(player.id)\|membership, player\| membership.player_id.eq(player.id)(membership, player) membership.player_id.Eq(player.id)源码lib.cpplib.rsLib.cs三套 SDK 的 API 名称、参数顺序与谓词语义完全一致这使得该模块成为验证 SpacetimeDB 查询构建器跨语言一致性的最小可执行样本。六、构建链路从 C 到lib.wasm6.1 一键构建README 中给出的构建命令是 PowerShell 脚本Windows 环境.\modules\sdk-test-view-pk-cpp\compile.bat构建产物为modules/sdk-test-view-pk-cpp/build/lib.wasm。6.2compile.bat做了什么modules/sdk-test-view-pk-cpp/compile.bat 的核心只有两步call emcmake cmake -B build . call cmake --build buildemcmake是 Emscripten 提供的 CMake 包装器它会为 CMake 注入 Emscripten 工具链使生成的构建系统把目标编译为 WebAssembly 而非本地可执行文件。6.3CMakeLists.txt的关键配置modules/sdk-test-view-pk-cpp/CMakeLists.txt 中值得展开的要点C 标准CMAKE_CXX_STANDARD 20lib.cpp中大量使用结构化绑定风格的 lambda 与std::move均依赖 C17可覆盖变量MODULE_SOURCE默认src/lib.cpp与OUTPUT_NAME默认lib便于复用同一套构建文件生成不同模块SDK 头文件路径SPACETIMEDB_CPP_LIBRARY_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../crates/bindings-cpp即仓库内的 C 绑定库 crates/bindings-cpp/并通过add_subdirectory直接引用其源码参与构建Emscripten 专用编译选项-fno-exceptions -O2 -g0导出符号ABI 契约EXPORTED_FUNCS [_malloc,_free,___describe_module__,___call_reducer__,___call_view__,___call_view_anon__]即模块必须导出描述模块、调用 reducer、调用视图/匿名视图这组运行时入口链接选项-sSTANDALONE_WASM1、-sWASM1、--no-entry、-sERROR_ON_UNDEFINED_SYMBOLS1、-sFILESYSTEM0、-sDISABLE_EXCEPTION_CATCHING1、-sINITIAL_MEMORY16MB等最终输出lib.wasm。编译完成后即可用 SpacetimeDB CLIspacetime publish将该模块发布到数据库实例再通过spacetime call调用 reducer 灌入数据、通过订阅系统验证三个视图的返回内容。七、验证思路与预期行为以该模块做端到端验证时典型的操作序列与预期如下insert_view_pk_player(1, alice)、insert_view_pk_player(2, bob)、insert_view_pk_player(3, carol)insert_view_pk_membership(100, 1)、insert_view_pk_membership(101, 2)订阅三个视图all_view_pk_players→ 3 行全部玩家sender_view_pk_players_a→ 2 行alice、bob因为只有player_id ∈ {1,2}的成员记录sender_view_pk_players_b→ 0 行次成员表未插入数据再执行insert_view_pk_membership_secondary(200, 3)后sender_view_pk_players_b增量出现carolupdate_view_pk_player(1, alice-x)后all_view_pk_players中id1的行以主键形式下发更新——验证 PK-preserving 行为。上述验证过程与模块同构的 Rust 版sdk-test-view-pk、C# 版sdk-test-view-pk-cs保持一致三个语言实现可作为彼此的交叉测试基准。八、小结sdk-test-view-pk-cpp虽是一个测试模块却是理解 SpacetimeDB C SDK 查询构建器的绝佳最小范例视图 对基础表的惰性查询引用QueryT客户端以增量订阅方式消费主键保留 视图输出行类型携带底层表主键元数据保证行级更新追踪能力右半连接right_semijoin借助两侧索引FIELD_Index的 btree 索引与FIELD_PrimaryKey的主键索引高效实现EXISTS语义构建emcmake CMake Emscripten 生成导出标准 ABI 的lib.wasm可直接发布到 SpacetimeDB 实例。读者若想深入源码建议依次阅读 modules/sdk-test-view-pk-cpp/src/lib.cpp、crates/bindings-cpp/include/spacetimedb/view_macros.h、crates/bindings-cpp/include/spacetimedb/query_builder/join.h并对照 Rust 版 modules/sdk-test-view-pk/src/lib.rs 与 C# 版 modules/sdk-test-view-pk-cs/Lib.cs 体会跨语言 API 的一致性设计。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表