From 414abad9e6bf88de2a9821624a2bcef90568c70b Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 5 Mar 2026 10:04:03 -0800 Subject: [PATCH 1/4] Add subscription smoketests that join on a view's primary key --- .../smoketests/modules/views-query/src/lib.rs | 20 ++- .../modules/views-subscribe/src/lib.rs | 15 +- crates/smoketests/tests/smoketests/views.rs | 162 ++++++++++++------ 3 files changed, 138 insertions(+), 59 deletions(-) diff --git a/crates/smoketests/modules/views-query/src/lib.rs b/crates/smoketests/modules/views-query/src/lib.rs index 0bf4bf60636..85ec00026e7 100644 --- a/crates/smoketests/modules/views-query/src/lib.rs +++ b/crates/smoketests/modules/views-query/src/lib.rs @@ -1,4 +1,4 @@ -use spacetimedb::{Query, ReducerContext, Table, ViewContext}; +use spacetimedb::{AnonymousViewContext, Query, ReducerContext, Table, ViewContext}; #[spacetimedb::table(accessor = user, public)] pub struct User { @@ -88,3 +88,21 @@ fn users_who_are_above_20_and_below_30(ctx: &ViewContext) -> impl Query fn users_who_are_above_eq_20_and_below_eq_30(ctx: &ViewContext) -> impl Query { ctx.from.person().r#where(|p| p.age.gte(20).and(p.age.lte(30))) } + +#[spacetimedb::view(accessor = anonymous_adult_people, public)] +fn anonymous_adult_people(ctx: &AnonymousViewContext) -> impl Query { + ctx.from.person().r#where(|p| p.age.gte(20)) +} + +#[spacetimedb::view(accessor = online_users_identity_1, public)] +fn online_users_identity_1(ctx: &ViewContext) -> impl Query { + ctx.from.user().r#where(|u| u.online).filter(|u| u.identity.eq(1)) +} + +#[spacetimedb::view(accessor = users_whos_age_is_known_identity_1, public)] +fn users_whos_age_is_known_identity_1(ctx: &ViewContext) -> impl Query { + ctx.from + .user() + .left_semijoin(ctx.from.person(), |p, u| p.identity.eq(u.identity)) + .filter(|u| u.identity.eq(1)) +} diff --git a/crates/smoketests/modules/views-subscribe/src/lib.rs b/crates/smoketests/modules/views-subscribe/src/lib.rs index eaefece9acf..9357a9cfc9c 100644 --- a/crates/smoketests/modules/views-subscribe/src/lib.rs +++ b/crates/smoketests/modules/views-subscribe/src/lib.rs @@ -1,4 +1,4 @@ -use spacetimedb::{Identity, ProcedureContext, ReducerContext, Table, ViewContext}; +use spacetimedb::{Identity, ProcedureContext, Query, ReducerContext, Table, ViewContext}; #[spacetimedb::table(accessor = player_state)] pub struct PlayerState { @@ -6,6 +6,7 @@ pub struct PlayerState { identity: Identity, #[unique] name: String, + online: bool, } #[spacetimedb::view(accessor = my_player, public)] @@ -13,11 +14,22 @@ pub fn my_player(ctx: &ViewContext) -> Option { ctx.db.player_state().identity().find(ctx.sender()) } +#[spacetimedb::view(accessor = all_players, public)] +pub fn all_players(ctx: &ViewContext) -> impl Query { + ctx.from.player_state() +} + +#[spacetimedb::view(accessor = online_players, public)] +pub fn online_players(ctx: &ViewContext) -> impl Query { + ctx.from.player_state().r#where(|row| row.online) +} + #[spacetimedb::reducer] pub fn insert_player(ctx: &ReducerContext, name: String) { ctx.db.player_state().insert(PlayerState { name, identity: ctx.sender(), + online: true, }); } @@ -28,6 +40,7 @@ pub fn insert_player_proc(ctx: &mut ProcedureContext, name: String) { tx.db.player_state().insert(PlayerState { name: name.clone(), identity: sender, + online: true, }); }); } diff --git a/crates/smoketests/tests/smoketests/views.rs b/crates/smoketests/tests/smoketests/views.rs index 654c551625b..677e6b0ddff 100644 --- a/crates/smoketests/tests/smoketests/views.rs +++ b/crates/smoketests/tests/smoketests/views.rs @@ -1,4 +1,4 @@ -use serde_json::json; +use serde_json::{json, Value}; use spacetimedb_smoketests::{require_dotnet, require_pnpm, Smoketest}; const TS_VIEWS_SUBSCRIBE_MODULE: &str = r#"import { schema, t, table } from "spacetimedb/server"; @@ -77,6 +77,30 @@ public static partial class Module } "#; +fn project_inserts_and_deletes_for_view(events: Vec, view_name: &str) -> Vec { + events + .into_iter() + .map(|event| { + json!({ + view_name: { + "deletes": event[view_name]["deletes"] + .as_array() + .unwrap() + .iter() + .map(|row| json!({"name": row["name"]})) + .collect::>(), + "inserts": event[view_name]["inserts"] + .as_array() + .unwrap() + .iter() + .map(|row| json!({"name": row["name"]})) + .collect::>() + } + }) + }) + .collect() +} + /// Tests that views populate the st_view_* system tables #[test] fn test_st_view_tables() { @@ -404,24 +428,7 @@ fn test_subscribing_with_different_identities() { test.call("insert_player", &["Bob"]).unwrap(); let events = sub.collect().unwrap(); - let projection: Vec = events - .into_iter() - .map(|event| { - let deletes = event["my_player"]["deletes"] - .as_array() - .unwrap() - .iter() - .map(|row| json!({"name": row["name"]})) - .collect::>(); - let inserts = event["my_player"]["inserts"] - .as_array() - .unwrap() - .iter() - .map(|row| json!({"name": row["name"]})) - .collect::>(); - json!({"my_player": {"deletes": deletes, "inserts": inserts}}) - }) - .collect(); + let projection = project_inserts_and_deletes_for_view(events, "my_player"); assert_eq!( serde_json::json!(projection), @@ -500,26 +507,7 @@ fn test_procedure_triggers_subscription_updates() { let sub = test.subscribe_background(&["select * from my_player"], 1).unwrap(); test.call("insert_player_proc", &["Alice"]).unwrap(); let events = sub.collect().unwrap(); - - let projection: Vec = events - .into_iter() - .map(|event| { - let deletes = event["my_player"]["deletes"] - .as_array() - .unwrap() - .iter() - .map(|row| json!({"name": row["name"]})) - .collect::>(); - let inserts = event["my_player"]["inserts"] - .as_array() - .unwrap() - .iter() - .map(|row| json!({"name": row["name"]})) - .collect::>(); - json!({"my_player": {"deletes": deletes, "inserts": inserts}}) - }) - .collect(); - + let projection = project_inserts_and_deletes_for_view(events, "my_player"); assert_eq!( serde_json::json!(projection), serde_json::json!([ @@ -543,24 +531,7 @@ fn test_typescript_procedure_triggers_subscription_updates() { test.call("insert_player_proc", &["Alice"]).unwrap(); let events = sub.collect().unwrap(); - let projection: Vec = events - .into_iter() - .map(|event| { - let deletes = event["my_player"]["deletes"] - .as_array() - .unwrap() - .iter() - .map(|row| json!({"name": row["name"]})) - .collect::>(); - let inserts = event["my_player"]["inserts"] - .as_array() - .unwrap() - .iter() - .map(|row| json!({"name": row["name"]})) - .collect::>(); - json!({"my_player": {"deletes": deletes, "inserts": inserts}}) - }) - .collect(); + let projection = project_inserts_and_deletes_for_view(events, "my_player"); assert_eq!( serde_json::json!(projection), @@ -609,3 +580,80 @@ fn test_csharp_query_builder_view_query() { 1 | true"#, ); } + +#[test] +fn test_subscribe_join_with_view_on_primary_key_col() { + let test = Smoketest::builder().precompiled_module("views-subscribe").build(); + + test.call("insert_player_proc", &["Alice"]).unwrap(); + + let query = + "SELECT all_players.* FROM player_state JOIN all_players ON player_state.identity = all_players.identity"; + let events = test.subscribe(&[query], 0).unwrap(); + let projection = project_inserts_and_deletes_for_view(events, "all_players"); + + assert_eq!( + serde_json::json!(projection), + serde_json::json!([ + {"all_players": {"deletes": [], "inserts": [{"name": "Alice"}]}} + ]) + ); +} + +#[test] +fn test_subscribe_join_two_views_on_primary_key_col() { + let test = Smoketest::builder().precompiled_module("views-query").build(); + + let query = "SELECT online_users.* \ + FROM online_users \ + JOIN users_whos_age_is_known \ + ON online_users.identity = users_whos_age_is_known.identity"; + let events = test.subscribe(&[query], 0).unwrap(); + let projection = project_inserts_and_deletes_for_view(events, "online_users"); + + assert_eq!( + serde_json::json!(projection), + serde_json::json!([ + {"online_users": {"deletes": [], "inserts": [{"name": "Alice"}]}} + ]) + ); +} + +#[test] +fn test_subscribe_join_with_anonymous_view_on_primary_key_col() { + let test = Smoketest::builder().precompiled_module("views-query").build(); + + let query = "SELECT anonymous_adult_people.* \ + FROM person \ + JOIN anonymous_adult_people \ + ON person.identity = anonymous_adult_people.identity \ + WHERE anonymous_adult_people.identity = 1"; + let events = test.subscribe(&[query], 0).unwrap(); + let projection = project_inserts_and_deletes_for_view(events, "anonymous_adult_people"); + + assert_eq!( + serde_json::json!(projection), + serde_json::json!([ + {"anonymous_adult_people": {"deletes": [], "inserts": [{"name": "Alice"}]}} + ]) + ); +} + +#[test] +fn test_subscribe_join_two_sender_views_with_filters_on_both_sides() { + let test = Smoketest::builder().precompiled_module("views-query").build(); + + let query = "SELECT online_users_identity_1.* \ + FROM online_users_identity_1 \ + JOIN users_whos_age_is_known_identity_1 \ + ON online_users_identity_1.identity = users_whos_age_is_known_identity_1.identity"; + let events = test.subscribe(&[query], 0).unwrap(); + let projection = project_inserts_and_deletes_for_view(events, "online_users_identity_1"); + + assert_eq!( + serde_json::json!(projection), + serde_json::json!([ + {"online_users_identity_1": {"deletes": [], "inserts": [{"name": "Alice"}]}} + ]) + ); +} From 3a89e9a9ba6c99dc59d23d48da8f1a7ae17875bb Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 5 Mar 2026 10:26:49 -0800 Subject: [PATCH 2/4] Add rust sdk tests exercising on_update for views --- Cargo.lock | 17 + Cargo.toml | 2 + modules/sdk-test-view-pk/Cargo.toml | 14 + modules/sdk-test-view-pk/src/lib.rs | 69 ++ modules/sdk-test-view/src/lib.rs | 37 +- sdks/rust/tests/test.rs | 40 + sdks/rust/tests/view-pk-client/Cargo.toml | 14 + sdks/rust/tests/view-pk-client/LICENSE | 1 + sdks/rust/tests/view-pk-client/README.md | 10 + sdks/rust/tests/view-pk-client/src/main.rs | 296 ++++++ .../all_view_pk_players_table.rs | 111 +++ .../insert_view_pk_membership_reducer.rs | 72 ++ ...rt_view_pk_membership_secondary_reducer.rs | 72 ++ .../insert_view_pk_player_reducer.rs | 72 ++ .../view-pk-client/src/module_bindings/mod.rs | 931 ++++++++++++++++++ .../sender_view_pk_players_a_table.rs | 159 +++ .../sender_view_pk_players_b_table.rs | 159 +++ .../update_view_pk_player_reducer.rs | 72 ++ .../view_pk_membership_secondary_table.rs | 161 +++ .../view_pk_membership_secondary_type.rs | 54 + .../view_pk_membership_table.rs | 159 +++ .../view_pk_membership_type.rs | 54 + .../module_bindings/view_pk_player_table.rs | 159 +++ .../module_bindings/view_pk_player_type.rs | 52 + 24 files changed, 2786 insertions(+), 1 deletion(-) create mode 100644 modules/sdk-test-view-pk/Cargo.toml create mode 100644 modules/sdk-test-view-pk/src/lib.rs create mode 100644 sdks/rust/tests/view-pk-client/Cargo.toml create mode 120000 sdks/rust/tests/view-pk-client/LICENSE create mode 100644 sdks/rust/tests/view-pk-client/README.md create mode 100644 sdks/rust/tests/view-pk-client/src/main.rs create mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/all_view_pk_players_table.rs create mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_membership_reducer.rs create mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_membership_secondary_reducer.rs create mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_player_reducer.rs create mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/mod.rs create mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/sender_view_pk_players_a_table.rs create mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/sender_view_pk_players_b_table.rs create mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/update_view_pk_player_reducer.rs create mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_secondary_table.rs create mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_secondary_type.rs create mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_table.rs create mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_type.rs create mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_player_table.rs create mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_player_type.rs diff --git a/Cargo.lock b/Cargo.lock index a3dfa2fcac4..ff75c6e7b06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6994,6 +6994,13 @@ dependencies = [ "spacetimedb 2.0.3", ] +[[package]] +name = "sdk-test-view-pk" +version = "0.1.0" +dependencies = [ + "spacetimedb 2.0.3", +] + [[package]] name = "sdk-unreal-test-harness" version = "2.0.3" @@ -10094,6 +10101,16 @@ dependencies = [ "test-counter", ] +[[package]] +name = "view-pk-client" +version = "2.0.3" +dependencies = [ + "anyhow", + "env_logger 0.10.2", + "spacetimedb-sdk", + "test-counter", +] + [[package]] name = "vsimd" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 42cf0b85632..566d3170002 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,12 +50,14 @@ members = [ "modules/sdk-test-connect-disconnect", "modules/sdk-test-procedure", "modules/sdk-test-view", + "modules/sdk-test-view-pk", "modules/sdk-test-event-table", "sdks/rust/tests/test-client", "sdks/rust/tests/test-counter", "sdks/rust/tests/connect_disconnect_client", "sdks/rust/tests/procedure-client", "sdks/rust/tests/view-client", + "sdks/rust/tests/view-pk-client", "sdks/rust/tests/event-table-client", "tools/ci", "tools/upgrade-version", diff --git a/modules/sdk-test-view-pk/Cargo.toml b/modules/sdk-test-view-pk/Cargo.toml new file mode 100644 index 00000000000..4dbb0ecdc0c --- /dev/null +++ b/modules/sdk-test-view-pk/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "sdk-test-view-pk" +version = "0.1.0" +edition.workspace = true +license-file = "LICENSE" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +spacetimedb.workspace = true + +[lints] +workspace = true diff --git a/modules/sdk-test-view-pk/src/lib.rs b/modules/sdk-test-view-pk/src/lib.rs new file mode 100644 index 00000000000..f9e886f3b3a --- /dev/null +++ b/modules/sdk-test-view-pk/src/lib.rs @@ -0,0 +1,69 @@ +use spacetimedb::{reducer, table, view, Query, ReducerContext, Table, ViewContext}; + +#[table(accessor = view_pk_player, public)] +pub struct ViewPkPlayer { + #[primary_key] + pub id: u64, + pub name: String, +} + +#[table(accessor = view_pk_membership, public)] +pub struct ViewPkMembership { + #[primary_key] + pub id: u64, + #[index(btree)] + pub player_id: u64, +} + +#[table(accessor = view_pk_membership_secondary, public)] +pub struct ViewPkMembershipSecondary { + #[primary_key] + pub id: u64, + #[index(btree)] + pub player_id: u64, +} + +#[reducer] +pub fn insert_view_pk_player(ctx: &ReducerContext, id: u64, name: String) { + ctx.db.view_pk_player().insert(ViewPkPlayer { id, name }); +} + +#[reducer] +pub fn update_view_pk_player(ctx: &ReducerContext, id: u64, name: String) { + ctx.db.view_pk_player().id().update(ViewPkPlayer { id, name }); +} + +#[reducer] +pub fn insert_view_pk_membership(ctx: &ReducerContext, id: u64, player_id: u64) { + ctx.db.view_pk_membership().insert(ViewPkMembership { id, player_id }); +} + +#[reducer] +pub fn insert_view_pk_membership_secondary(ctx: &ReducerContext, id: u64, player_id: u64) { + ctx.db + .view_pk_membership_secondary() + .insert(ViewPkMembershipSecondary { id, player_id }); +} + +#[view(accessor = all_view_pk_players, public)] +pub fn all_view_pk_players(ctx: &ViewContext) -> impl Query { + ctx.from.view_pk_player() +} + +#[view(accessor = sender_view_pk_players_a, public)] +pub fn sender_view_pk_players_a(ctx: &ViewContext) -> impl Query { + ctx.from + .view_pk_membership() + .right_semijoin(ctx.from.view_pk_player(), |membership, player| { + membership.player_id.eq(player.id) + }) +} + +#[view(accessor = sender_view_pk_players_b, public)] +pub fn sender_view_pk_players_b(ctx: &ViewContext) -> impl Query { + ctx.from + .view_pk_membership_secondary() + .right_semijoin(ctx.from.view_pk_player(), |membership, player| { + membership.player_id.eq(player.id) + }) +} diff --git a/modules/sdk-test-view/src/lib.rs b/modules/sdk-test-view/src/lib.rs index c6e01161210..d33addc0c5d 100644 --- a/modules/sdk-test-view/src/lib.rs +++ b/modules/sdk-test-view/src/lib.rs @@ -1,5 +1,5 @@ use spacetimedb::{ - reducer, table, view, AnonymousViewContext, Identity, ReducerContext, SpacetimeType, Table, ViewContext, + reducer, table, view, AnonymousViewContext, Identity, Query, ReducerContext, SpacetimeType, Table, ViewContext, }; #[table(accessor = player, public)] @@ -36,6 +36,41 @@ struct PlayerAndLevel { level: u64, } +#[table(accessor = view_pk_player, public)] +pub struct ViewPkPlayer { + #[primary_key] + pub id: u64, + pub name: String, +} + +#[table(accessor = view_pk_membership, public)] +pub struct ViewPkMembership { + #[primary_key] + pub id: u64, + #[index(btree)] + pub player_id: u64, +} + +#[reducer] +pub fn insert_view_pk_player(ctx: &ReducerContext, id: u64, name: String) { + ctx.db.view_pk_player().insert(ViewPkPlayer { id, name }); +} + +#[reducer] +pub fn update_view_pk_player(ctx: &ReducerContext, id: u64, name: String) { + ctx.db.view_pk_player().id().update(ViewPkPlayer { id, name }); +} + +#[reducer] +pub fn insert_view_pk_membership(ctx: &ReducerContext, id: u64, player_id: u64) { + ctx.db.view_pk_membership().insert(ViewPkMembership { id, player_id }); +} + +#[view(accessor = all_view_pk_players, public)] +pub fn all_view_pk_players(ctx: &ViewContext) -> impl Query { + ctx.from.view_pk_player() +} + #[reducer] fn insert_player(ctx: &ReducerContext, identity: Identity, level: u64) { let Player { entity_id, .. } = ctx.db.player().insert(Player { entity_id: 0, identity }); diff --git a/sdks/rust/tests/test.rs b/sdks/rust/tests/test.rs index d6c5cac38ae..ed623c7d36f 100644 --- a/sdks/rust/tests/test.rs +++ b/sdks/rust/tests/test.rs @@ -488,3 +488,43 @@ macro_rules! view_tests { view_tests!(rust_view, ""); view_tests!(cpp_view, "-cpp"); + +macro_rules! view_pk_tests { + ($mod_name:ident, $suffix:literal) => { + mod $mod_name { + use spacetimedb_testing::sdk::Test; + + const MODULE: &str = concat!("sdk-test-view-pk", $suffix); + const CLIENT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/view-pk-client"); + + fn make_test(subcommand: &str) -> Test { + Test::builder() + .with_name(subcommand) + .with_module(MODULE) + .with_client(CLIENT) + .with_language("rust") + .with_bindings_dir("src/module_bindings") + .with_compile_command("cargo build") + .with_run_command(format!("cargo run -- {}", subcommand)) + .build() + } + + #[test] + fn query_builder_view_with_pk_on_update_callback() { + make_test("view-pk-on-update").run() + } + + #[test] + fn query_builder_join_table_with_view_pk() { + make_test("view-pk-join-query-builder").run() + } + + #[test] + fn query_builder_semijoin_two_sender_views_with_pk() { + make_test("view-pk-semijoin-two-sender-views-query-builder").run() + } + } + }; +} + +view_pk_tests!(rust_view_pk, ""); diff --git a/sdks/rust/tests/view-pk-client/Cargo.toml b/sdks/rust/tests/view-pk-client/Cargo.toml new file mode 100644 index 00000000000..f872b1e7f17 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "view-pk-client" +version.workspace = true +edition.workspace = true +license-file = "LICENSE" + +[dependencies] +spacetimedb-sdk = { path = "../.." } +test-counter = { path = "../test-counter" } +anyhow.workspace = true +env_logger.workspace = true + +[lints] +workspace = true diff --git a/sdks/rust/tests/view-pk-client/LICENSE b/sdks/rust/tests/view-pk-client/LICENSE new file mode 120000 index 00000000000..424c4c33df2 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/LICENSE @@ -0,0 +1 @@ +../../../../licenses/BSL.txt \ No newline at end of file diff --git a/sdks/rust/tests/view-pk-client/README.md b/sdks/rust/tests/view-pk-client/README.md new file mode 100644 index 00000000000..ba3c9b4fc5f --- /dev/null +++ b/sdks/rust/tests/view-pk-client/README.md @@ -0,0 +1,10 @@ +This test client is used with the module: + +- [`sdk-test-view-pk`](/modules/sdk-test-view-pk) + +To (re-)generate the `module_bindings`, from this directory, run: + +```sh +mkdir -p src/module_bindings +spacetime generate --lang rust --out-dir src/module_bindings --module-path ../../../../modules/sdk-test-view-pk +``` diff --git a/sdks/rust/tests/view-pk-client/src/main.rs b/sdks/rust/tests/view-pk-client/src/main.rs new file mode 100644 index 00000000000..38f73e5f91e --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/main.rs @@ -0,0 +1,296 @@ +mod module_bindings; + +use module_bindings::*; +use spacetimedb_sdk::TableWithPrimaryKey; +use spacetimedb_sdk::{error::InternalError, DbContext}; +use test_counter::TestCounter; + +const LOCALHOST: &str = "http://localhost:3000"; + +type ResultRecorder = Box)>; + +fn exit_on_panic() { + let default_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + default_hook(panic_info); + std::process::exit(1); + })); +} + +fn db_name_or_panic() -> String { + std::env::var("SPACETIME_SDK_TEST_DB_NAME").expect("Failed to read db name from env") +} + +fn put_result(result: &mut Option, res: Result<(), anyhow::Error>) { + (result.take().unwrap())(res); +} + +fn reducer_callback_assert_committed( + reducer_name: &'static str, +) -> impl FnOnce(&ReducerEventContext, Result, InternalError>) + Send + 'static { + move |_ctx, outcome| match outcome { + Ok(Ok(())) => (), + Ok(Err(msg)) => panic!("`{reducer_name}` reducer returned error: {msg}"), + Err(internal_error) => panic!("`{reducer_name}` reducer panicked: {internal_error:?}"), + } +} + +fn connect_then( + test_counter: &std::sync::Arc, + callback: impl FnOnce(&DbConnection) + Send + 'static, +) -> DbConnection { + let connected_result = test_counter.add_test("on_connect"); + let name = db_name_or_panic(); + let conn = DbConnection::builder() + .with_database_name(name) + .with_uri(LOCALHOST) + .on_connect(|ctx, _, _| { + callback(ctx); + connected_result(Ok(())); + }) + .on_connect_error(|_ctx, error| panic!("Connect errored: {error:?}")) + .build() + .unwrap(); + conn.run_threaded(); + conn +} + +fn subscribe_these_then( + ctx: &impl RemoteDbContext, + queries: &[&str], + callback: impl FnOnce(&SubscriptionEventContext) + Send + 'static, +) { + ctx.subscription_builder() + .on_applied(callback) + .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) + .subscribe(queries); +} + +/// Subscribe to a query builder view whose underlying table has a primary key. +/// Ensures the rust sdk emits an `on_update` callback and that the client receives the correct old and new rows. +/// +/// Test: +/// 1. Subscribe to: SELECT * FROM all_view_pk_players +/// 2. Insert row: (id=1, name="before") +/// 3. Update row: (id=1, name="after") +/// +/// Expect: +/// - `on_update` is called for PK=1 +/// - `old_row` should be the "before" value +/// - `new_row` should be the "after" value +fn exec_view_pk_on_update() { + let test_counter = TestCounter::new(); + let mut on_update = Some(test_counter.add_test("on_update")); + + connect_then(&test_counter, move |ctx| { + subscribe_these_then(ctx, &["SELECT * FROM all_view_pk_players"], move |ctx| { + ctx.db.all_view_pk_players().on_update(move |_, old_row, new_row| { + assert_eq!(old_row.id, 1); + assert_eq!(old_row.name, "before"); + assert_eq!(new_row.id, 1); + assert_eq!(new_row.name, "after"); + put_result(&mut on_update, Ok(())); + }); + + // Seed the row that the view will expose. + ctx.reducers() + .insert_view_pk_player_then( + 1, + "before".to_string(), + reducer_callback_assert_committed("insert_view_pk_player"), + ) + .unwrap(); + + // Mutate same PK so subscription emits an update(old,new) pair. + ctx.reducers() + .update_view_pk_player_then( + 1, + "after".to_string(), + reducer_callback_assert_committed("update_view_pk_player"), + ) + .unwrap(); + }); + }); + + test_counter.wait_for_all(); +} + +/// Subscribe to a right semijoin whose rhs is a view with primary key. +/// +/// Ensures: +/// 1. A semijoin subscription involving a view is valid +/// 2. The rust sdk emits an `on_update` callback and that the client receives the correct old and new rows +/// +/// Query: +/// SELECT player.* +/// FROM view_pk_membership membership +/// JOIN all_view_pk_players player ON membership.player_id = player.id +/// +/// Test: +/// 1. Insert player row (id=1, "before"). +/// 2. Insert membership row referencing player_id=1, allowing the semijoin match. +/// 3. Update player row to (id=1, "after"). +/// +/// Expect: +/// - `on_update` is called for player PK=1 +/// - `old_row` should be the "before" value +/// - `new_row` should be the "after" value +fn exec_view_pk_join_query_builder() { + let test_counter = TestCounter::new(); + let mut joined_update = Some(test_counter.add_test("join_update")); + + connect_then(&test_counter, move |ctx| { + ctx.subscription_builder() + .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) + .on_applied(move |ctx| { + ctx.db.all_view_pk_players().on_update(move |_, old_row, new_row| { + assert_eq!(old_row.id, 1); + assert_eq!(old_row.name, "before"); + assert_eq!(new_row.id, 1); + assert_eq!(new_row.name, "after"); + put_result(&mut joined_update, Ok(())); + }); + + // Base player row. + ctx.reducers() + .insert_view_pk_player_then( + 1, + "before".to_string(), + reducer_callback_assert_committed("insert_view_pk_player"), + ) + .unwrap(); + + // Membership row that causes semijoin inclusion. + ctx.reducers() + .insert_view_pk_membership_then( + 1, + 1, + reducer_callback_assert_committed("insert_view_pk_membership"), + ) + .unwrap(); + + // Update same PK to force joined-stream update event. + ctx.reducers() + .update_view_pk_player_then( + 1, + "after".to_string(), + reducer_callback_assert_committed("update_view_pk_player"), + ) + .unwrap(); + }) + .add_query(|q| { + q.from + .view_pk_membership() + .right_semijoin(q.from.all_view_pk_players(), |membership, player| { + membership.player_id.eq(player.id) + }) + .build() + }) + .subscribe(); + }); + + test_counter.wait_for_all(); +} + +/// Subscribe to a semijoin between two views with primary keys. +/// +/// Ensures: +/// 1. A semijoin subscription involving a view is valid +/// 2. The rust sdk emits an `on_update` callback and that the client receives the correct old and new rows +/// +/// Query: +/// SELECT b.* +/// FROM sender_view_pk_players_a a +/// JOIN sender_view_pk_players_b b ON a.id = b.id +/// +/// Test: +/// 1. Insert player row (id=1, "before"). +/// 2. Insert membership for sender view A. +/// 3. Insert membership for sender view B. +/// 4. Update player row to (id=1, "after"). +/// +/// Expect: +/// - `on_update` is called for player PK=1 +/// - `old_row` should be the "before" value +/// - `new_row` should be the "after" value +fn exec_view_pk_semijoin_two_sender_views_query_builder() { + let test_counter = TestCounter::new(); + let mut joined_update = Some(test_counter.add_test("join_update")); + + connect_then(&test_counter, move |ctx| { + ctx.subscription_builder() + .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) + .on_applied(move |ctx| { + ctx.db.sender_view_pk_players_b().on_update(move |_, old_row, new_row| { + assert_eq!(old_row.id, 1); + assert_eq!(old_row.name, "before"); + assert_eq!(new_row.id, 1); + assert_eq!(new_row.name, "after"); + put_result(&mut joined_update, Ok(())); + }); + + // Base player row used by both sender-scoped views. + ctx.reducers() + .insert_view_pk_player_then( + 1, + "before".to_string(), + reducer_callback_assert_committed("insert_view_pk_player"), + ) + .unwrap(); + + // Membership edge that enables sender_view_pk_players_a. + ctx.reducers() + .insert_view_pk_membership_then( + 1, + 1, + reducer_callback_assert_committed("insert_view_pk_membership"), + ) + .unwrap(); + + // Membership edge that enables sender_view_pk_players_b. + ctx.reducers() + .insert_view_pk_membership_secondary_then( + 1, + 1, + reducer_callback_assert_committed("insert_view_pk_membership_secondary"), + ) + .unwrap(); + + // Update same PK to verify sender-view join emits an on_update event. + ctx.reducers() + .update_view_pk_player_then( + 1, + "after".to_string(), + reducer_callback_assert_committed("update_view_pk_player"), + ) + .unwrap(); + }) + .add_query(|q| { + q.from + .sender_view_pk_players_a() + .right_semijoin(q.from.sender_view_pk_players_b(), |lhs_view, rhs_view| { + lhs_view.id.eq(rhs_view.id) + }) + .build() + }) + .subscribe(); + }); + + test_counter.wait_for_all(); +} + +fn main() { + env_logger::init(); + exit_on_panic(); + + let test = std::env::args() + .nth(1) + .expect("Pass a test name as a command-line argument to the test client"); + + match &*test { + "view-pk-on-update" => exec_view_pk_on_update(), + "view-pk-join-query-builder" => exec_view_pk_join_query_builder(), + "view-pk-semijoin-two-sender-views-query-builder" => exec_view_pk_semijoin_two_sender_views_query_builder(), + _ => panic!("Unknown test: {test}"), + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/all_view_pk_players_table.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/all_view_pk_players_table.rs new file mode 100644 index 00000000000..8b96750dae6 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/all_view_pk_players_table.rs @@ -0,0 +1,111 @@ +// 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 super::view_pk_player_type::ViewPkPlayer; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `all_view_pk_players`. +/// +/// Obtain a handle from the [`AllViewPkPlayersTableAccess::all_view_pk_players`] method on [`super::RemoteTables`], +/// like `ctx.db.all_view_pk_players()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.all_view_pk_players().on_insert(...)`. +pub struct AllViewPkPlayersTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `all_view_pk_players`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait AllViewPkPlayersTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`AllViewPkPlayersTableHandle`], which mediates access to the table `all_view_pk_players`. + fn all_view_pk_players(&self) -> AllViewPkPlayersTableHandle<'_>; +} + +impl AllViewPkPlayersTableAccess for super::RemoteTables { + fn all_view_pk_players(&self) -> AllViewPkPlayersTableHandle<'_> { + AllViewPkPlayersTableHandle { + imp: self.imp.get_table::("all_view_pk_players"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct AllViewPkPlayersInsertCallbackId(__sdk::CallbackId); +pub struct AllViewPkPlayersDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for AllViewPkPlayersTableHandle<'ctx> { + type Row = ViewPkPlayer; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = AllViewPkPlayersInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> AllViewPkPlayersInsertCallbackId { + AllViewPkPlayersInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: AllViewPkPlayersInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = AllViewPkPlayersDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> AllViewPkPlayersDeleteCallbackId { + AllViewPkPlayersDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: AllViewPkPlayersDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("all_view_pk_players"); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `ViewPkPlayer`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait all_view_pk_playersQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `ViewPkPlayer`. + fn all_view_pk_players(&self) -> __sdk::__query_builder::Table; +} + +impl all_view_pk_playersQueryTableAccess for __sdk::QueryTableAccessor { + fn all_view_pk_players(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("all_view_pk_players") + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_membership_reducer.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_membership_reducer.rs new file mode 100644 index 00000000000..ab960530ef5 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_membership_reducer.rs @@ -0,0 +1,72 @@ +// 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(super) struct InsertViewPkMembershipArgs { + pub id: u64, + pub player_id: u64, +} + +impl From for super::Reducer { + fn from(args: InsertViewPkMembershipArgs) -> Self { + Self::InsertViewPkMembership { + id: args.id, + player_id: args.player_id, + } + } +} + +impl __sdk::InModule for InsertViewPkMembershipArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `insert_view_pk_membership`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait insert_view_pk_membership { + /// Request that the remote module invoke the reducer `insert_view_pk_membership` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and this method provides no way to listen for its completion status. + /// /// Use [`insert_view_pk_membership:insert_view_pk_membership_then`] to run a callback after the reducer completes. + fn insert_view_pk_membership(&self, id: u64, player_id: u64) -> __sdk::Result<()> { + self.insert_view_pk_membership_then(id, player_id, |_, _| {}) + } + + /// Request that the remote module invoke the reducer `insert_view_pk_membership` to run as soon as possible, + /// registering `callback` to run when we are notified that the reducer completed. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed with the `callback`. + fn insert_view_pk_membership_then( + &self, + id: u64, + player_id: u64, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()>; +} + +impl insert_view_pk_membership for super::RemoteReducers { + fn insert_view_pk_membership_then( + &self, + id: u64, + player_id: u64, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()> { + self.imp + .invoke_reducer_with_callback(InsertViewPkMembershipArgs { id, player_id }, callback) + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_membership_secondary_reducer.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_membership_secondary_reducer.rs new file mode 100644 index 00000000000..1e1962019f1 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_membership_secondary_reducer.rs @@ -0,0 +1,72 @@ +// 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(super) struct InsertViewPkMembershipSecondaryArgs { + pub id: u64, + pub player_id: u64, +} + +impl From for super::Reducer { + fn from(args: InsertViewPkMembershipSecondaryArgs) -> Self { + Self::InsertViewPkMembershipSecondary { + id: args.id, + player_id: args.player_id, + } + } +} + +impl __sdk::InModule for InsertViewPkMembershipSecondaryArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `insert_view_pk_membership_secondary`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait insert_view_pk_membership_secondary { + /// Request that the remote module invoke the reducer `insert_view_pk_membership_secondary` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and this method provides no way to listen for its completion status. + /// /// Use [`insert_view_pk_membership_secondary:insert_view_pk_membership_secondary_then`] to run a callback after the reducer completes. + fn insert_view_pk_membership_secondary(&self, id: u64, player_id: u64) -> __sdk::Result<()> { + self.insert_view_pk_membership_secondary_then(id, player_id, |_, _| {}) + } + + /// Request that the remote module invoke the reducer `insert_view_pk_membership_secondary` to run as soon as possible, + /// registering `callback` to run when we are notified that the reducer completed. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed with the `callback`. + fn insert_view_pk_membership_secondary_then( + &self, + id: u64, + player_id: u64, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()>; +} + +impl insert_view_pk_membership_secondary for super::RemoteReducers { + fn insert_view_pk_membership_secondary_then( + &self, + id: u64, + player_id: u64, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()> { + self.imp + .invoke_reducer_with_callback(InsertViewPkMembershipSecondaryArgs { id, player_id }, callback) + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_player_reducer.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_player_reducer.rs new file mode 100644 index 00000000000..d7a53da45d7 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_player_reducer.rs @@ -0,0 +1,72 @@ +// 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(super) struct InsertViewPkPlayerArgs { + pub id: u64, + pub name: String, +} + +impl From for super::Reducer { + fn from(args: InsertViewPkPlayerArgs) -> Self { + Self::InsertViewPkPlayer { + id: args.id, + name: args.name, + } + } +} + +impl __sdk::InModule for InsertViewPkPlayerArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `insert_view_pk_player`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait insert_view_pk_player { + /// Request that the remote module invoke the reducer `insert_view_pk_player` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and this method provides no way to listen for its completion status. + /// /// Use [`insert_view_pk_player:insert_view_pk_player_then`] to run a callback after the reducer completes. + fn insert_view_pk_player(&self, id: u64, name: String) -> __sdk::Result<()> { + self.insert_view_pk_player_then(id, name, |_, _| {}) + } + + /// Request that the remote module invoke the reducer `insert_view_pk_player` to run as soon as possible, + /// registering `callback` to run when we are notified that the reducer completed. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed with the `callback`. + fn insert_view_pk_player_then( + &self, + id: u64, + name: String, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()>; +} + +impl insert_view_pk_player for super::RemoteReducers { + fn insert_view_pk_player_then( + &self, + id: u64, + name: String, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()> { + self.imp + .invoke_reducer_with_callback(InsertViewPkPlayerArgs { id, name }, callback) + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/mod.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/mod.rs new file mode 100644 index 00000000000..9db3ef815bc --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/mod.rs @@ -0,0 +1,931 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +// This was generated using spacetimedb cli version 2.0.3 (commit cea9b6a2ddb9a3fbf5a9d3307fbae8efe29e26dc). + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +pub mod all_view_pk_players_table; +pub mod insert_view_pk_membership_reducer; +pub mod insert_view_pk_membership_secondary_reducer; +pub mod insert_view_pk_player_reducer; +pub mod sender_view_pk_players_a_table; +pub mod sender_view_pk_players_b_table; +pub mod update_view_pk_player_reducer; +pub mod view_pk_membership_secondary_table; +pub mod view_pk_membership_secondary_type; +pub mod view_pk_membership_table; +pub mod view_pk_membership_type; +pub mod view_pk_player_table; +pub mod view_pk_player_type; + +pub use all_view_pk_players_table::*; +pub use insert_view_pk_membership_reducer::insert_view_pk_membership; +pub use insert_view_pk_membership_secondary_reducer::insert_view_pk_membership_secondary; +pub use insert_view_pk_player_reducer::insert_view_pk_player; +pub use sender_view_pk_players_a_table::*; +pub use sender_view_pk_players_b_table::*; +pub use update_view_pk_player_reducer::update_view_pk_player; +pub use view_pk_membership_secondary_table::*; +pub use view_pk_membership_secondary_type::ViewPkMembershipSecondary; +pub use view_pk_membership_table::*; +pub use view_pk_membership_type::ViewPkMembership; +pub use view_pk_player_table::*; +pub use view_pk_player_type::ViewPkPlayer; + +#[derive(Clone, PartialEq, Debug)] + +/// One of the reducers defined by this module. +/// +/// Contained within a [`__sdk::ReducerEvent`] in [`EventContext`]s for reducer events +/// to indicate which reducer caused the event. + +pub enum Reducer { + InsertViewPkMembership { id: u64, player_id: u64 }, + InsertViewPkMembershipSecondary { id: u64, player_id: u64 }, + InsertViewPkPlayer { id: u64, name: String }, + UpdateViewPkPlayer { id: u64, name: String }, +} + +impl __sdk::InModule for Reducer { + type Module = RemoteModule; +} + +impl __sdk::Reducer for Reducer { + fn reducer_name(&self) -> &'static str { + match self { + Reducer::InsertViewPkMembership { .. } => "insert_view_pk_membership", + Reducer::InsertViewPkMembershipSecondary { .. } => "insert_view_pk_membership_secondary", + Reducer::InsertViewPkPlayer { .. } => "insert_view_pk_player", + Reducer::UpdateViewPkPlayer { .. } => "update_view_pk_player", + _ => unreachable!(), + } + } + #[allow(clippy::clone_on_copy)] + fn args_bsatn(&self) -> Result, __sats::bsatn::EncodeError> { + match self { + Reducer::InsertViewPkMembership { id, player_id } => { + __sats::bsatn::to_vec(&insert_view_pk_membership_reducer::InsertViewPkMembershipArgs { + id: id.clone(), + player_id: player_id.clone(), + }) + } + Reducer::InsertViewPkMembershipSecondary { id, player_id } => __sats::bsatn::to_vec( + &insert_view_pk_membership_secondary_reducer::InsertViewPkMembershipSecondaryArgs { + id: id.clone(), + player_id: player_id.clone(), + }, + ), + Reducer::InsertViewPkPlayer { id, name } => { + __sats::bsatn::to_vec(&insert_view_pk_player_reducer::InsertViewPkPlayerArgs { + id: id.clone(), + name: name.clone(), + }) + } + Reducer::UpdateViewPkPlayer { id, name } => { + __sats::bsatn::to_vec(&update_view_pk_player_reducer::UpdateViewPkPlayerArgs { + id: id.clone(), + name: name.clone(), + }) + } + _ => unreachable!(), + } + } +} + +#[derive(Default)] +#[allow(non_snake_case)] +#[doc(hidden)] +pub struct DbUpdate { + all_view_pk_players: __sdk::TableUpdate, + sender_view_pk_players_a: __sdk::TableUpdate, + sender_view_pk_players_b: __sdk::TableUpdate, + view_pk_membership: __sdk::TableUpdate, + view_pk_membership_secondary: __sdk::TableUpdate, + view_pk_player: __sdk::TableUpdate, +} + +impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { + type Error = __sdk::Error; + fn try_from(raw: __ws::v2::TransactionUpdate) -> Result { + let mut db_update = DbUpdate::default(); + for table_update in __sdk::transaction_update_iter_table_updates(raw) { + match &table_update.table_name[..] { + "all_view_pk_players" => db_update + .all_view_pk_players + .append(all_view_pk_players_table::parse_table_update(table_update)?), + "sender_view_pk_players_a" => db_update + .sender_view_pk_players_a + .append(sender_view_pk_players_a_table::parse_table_update(table_update)?), + "sender_view_pk_players_b" => db_update + .sender_view_pk_players_b + .append(sender_view_pk_players_b_table::parse_table_update(table_update)?), + "view_pk_membership" => db_update + .view_pk_membership + .append(view_pk_membership_table::parse_table_update(table_update)?), + "view_pk_membership_secondary" => db_update + .view_pk_membership_secondary + .append(view_pk_membership_secondary_table::parse_table_update(table_update)?), + "view_pk_player" => db_update + .view_pk_player + .append(view_pk_player_table::parse_table_update(table_update)?), + + unknown => { + return Err(__sdk::InternalError::unknown_name("table", unknown, "DatabaseUpdate").into()); + } + } + } + Ok(db_update) + } +} + +impl __sdk::InModule for DbUpdate { + type Module = RemoteModule; +} + +impl __sdk::DbUpdate for DbUpdate { + fn apply_to_client_cache(&self, cache: &mut __sdk::ClientCache) -> AppliedDiff<'_> { + let mut diff = AppliedDiff::default(); + + diff.view_pk_membership = cache + .apply_diff_to_table::("view_pk_membership", &self.view_pk_membership) + .with_updates_by_pk(|row| &row.id); + diff.view_pk_membership_secondary = cache + .apply_diff_to_table::( + "view_pk_membership_secondary", + &self.view_pk_membership_secondary, + ) + .with_updates_by_pk(|row| &row.id); + diff.view_pk_player = cache + .apply_diff_to_table::("view_pk_player", &self.view_pk_player) + .with_updates_by_pk(|row| &row.id); + diff.all_view_pk_players = cache + .apply_diff_to_table::("all_view_pk_players", &self.all_view_pk_players) + .with_updates_by_pk(|row| &row.id); + diff.sender_view_pk_players_a = cache + .apply_diff_to_table::("sender_view_pk_players_a", &self.sender_view_pk_players_a) + .with_updates_by_pk(|row| &row.id); + diff.sender_view_pk_players_b = cache + .apply_diff_to_table::("sender_view_pk_players_b", &self.sender_view_pk_players_b) + .with_updates_by_pk(|row| &row.id); + + diff + } + fn parse_initial_rows(raw: __ws::v2::QueryRows) -> __sdk::Result { + let mut db_update = DbUpdate::default(); + for table_rows in raw.tables { + match &table_rows.table[..] { + "all_view_pk_players" => db_update + .all_view_pk_players + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "sender_view_pk_players_a" => db_update + .sender_view_pk_players_a + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "sender_view_pk_players_b" => db_update + .sender_view_pk_players_b + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "view_pk_membership" => db_update + .view_pk_membership + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "view_pk_membership_secondary" => db_update + .view_pk_membership_secondary + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "view_pk_player" => db_update + .view_pk_player + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + unknown => { + return Err(__sdk::InternalError::unknown_name("table", unknown, "QueryRows").into()); + } + } + } + Ok(db_update) + } + fn parse_unsubscribe_rows(raw: __ws::v2::QueryRows) -> __sdk::Result { + let mut db_update = DbUpdate::default(); + for table_rows in raw.tables { + match &table_rows.table[..] { + "all_view_pk_players" => db_update + .all_view_pk_players + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "sender_view_pk_players_a" => db_update + .sender_view_pk_players_a + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "sender_view_pk_players_b" => db_update + .sender_view_pk_players_b + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "view_pk_membership" => db_update + .view_pk_membership + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "view_pk_membership_secondary" => db_update + .view_pk_membership_secondary + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "view_pk_player" => db_update + .view_pk_player + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + unknown => { + return Err(__sdk::InternalError::unknown_name("table", unknown, "QueryRows").into()); + } + } + } + Ok(db_update) + } +} + +#[derive(Default)] +#[allow(non_snake_case)] +#[doc(hidden)] +pub struct AppliedDiff<'r> { + all_view_pk_players: __sdk::TableAppliedDiff<'r, ViewPkPlayer>, + sender_view_pk_players_a: __sdk::TableAppliedDiff<'r, ViewPkPlayer>, + sender_view_pk_players_b: __sdk::TableAppliedDiff<'r, ViewPkPlayer>, + view_pk_membership: __sdk::TableAppliedDiff<'r, ViewPkMembership>, + view_pk_membership_secondary: __sdk::TableAppliedDiff<'r, ViewPkMembershipSecondary>, + view_pk_player: __sdk::TableAppliedDiff<'r, ViewPkPlayer>, + __unused: std::marker::PhantomData<&'r ()>, +} + +impl __sdk::InModule for AppliedDiff<'_> { + type Module = RemoteModule; +} + +impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { + fn invoke_row_callbacks(&self, event: &EventContext, callbacks: &mut __sdk::DbCallbacks) { + callbacks.invoke_table_row_callbacks::("all_view_pk_players", &self.all_view_pk_players, event); + callbacks.invoke_table_row_callbacks::( + "sender_view_pk_players_a", + &self.sender_view_pk_players_a, + event, + ); + callbacks.invoke_table_row_callbacks::( + "sender_view_pk_players_b", + &self.sender_view_pk_players_b, + event, + ); + callbacks.invoke_table_row_callbacks::("view_pk_membership", &self.view_pk_membership, event); + callbacks.invoke_table_row_callbacks::( + "view_pk_membership_secondary", + &self.view_pk_membership_secondary, + event, + ); + callbacks.invoke_table_row_callbacks::("view_pk_player", &self.view_pk_player, event); + } +} + +#[doc(hidden)] +pub struct RemoteModule; + +impl __sdk::InModule for RemoteModule { + type Module = Self; +} + +/// The `reducers` field of [`EventContext`] and [`DbConnection`], +/// with methods provided by extension traits for each reducer defined by the module. +pub struct RemoteReducers { + imp: __sdk::DbContextImpl, +} + +impl __sdk::InModule for RemoteReducers { + type Module = RemoteModule; +} + +/// The `procedures` field of [`DbConnection`] and other [`DbContext`] types, +/// with methods provided by extension traits for each procedure defined by the module. +pub struct RemoteProcedures { + imp: __sdk::DbContextImpl, +} + +impl __sdk::InModule for RemoteProcedures { + type Module = RemoteModule; +} + +/// The `db` field of [`EventContext`] and [`DbConnection`], +/// with methods provided by extension traits for each table defined by the module. +pub struct RemoteTables { + imp: __sdk::DbContextImpl, +} + +impl __sdk::InModule for RemoteTables { + type Module = RemoteModule; +} + +/// A connection to a remote module, including a materialized view of a subset of the database. +/// +/// Connect to a remote module by calling [`DbConnection::builder`] +/// and using the [`__sdk::DbConnectionBuilder`] builder-pattern constructor. +/// +/// You must explicitly advance the connection by calling any one of: +/// +/// - [`DbConnection::frame_tick`]. +/// - [`DbConnection::run_threaded`]. +/// - [`DbConnection::run_async`]. +/// - [`DbConnection::advance_one_message`]. +/// - [`DbConnection::advance_one_message_blocking`]. +/// - [`DbConnection::advance_one_message_async`]. +/// +/// Which of these methods you should call depends on the specific needs of your application, +/// but you must call one of them, or else the connection will never progress. +pub struct DbConnection { + /// Access to tables defined by the module via extension traits implemented for [`RemoteTables`]. + pub db: RemoteTables, + /// Access to reducers defined by the module via extension traits implemented for [`RemoteReducers`]. + pub reducers: RemoteReducers, + #[doc(hidden)] + + /// Access to procedures defined by the module via extension traits implemented for [`RemoteProcedures`]. + pub procedures: RemoteProcedures, + + imp: __sdk::DbContextImpl, +} + +impl __sdk::InModule for DbConnection { + type Module = RemoteModule; +} + +impl __sdk::DbContext for DbConnection { + type DbView = RemoteTables; + type Reducers = RemoteReducers; + type Procedures = RemoteProcedures; + + fn db(&self) -> &Self::DbView { + &self.db + } + fn reducers(&self) -> &Self::Reducers { + &self.reducers + } + fn procedures(&self) -> &Self::Procedures { + &self.procedures + } + + fn is_active(&self) -> bool { + self.imp.is_active() + } + + fn disconnect(&self) -> __sdk::Result<()> { + self.imp.disconnect() + } + + type SubscriptionBuilder = __sdk::SubscriptionBuilder; + + fn subscription_builder(&self) -> Self::SubscriptionBuilder { + __sdk::SubscriptionBuilder::new(&self.imp) + } + + fn try_identity(&self) -> Option<__sdk::Identity> { + self.imp.try_identity() + } + fn connection_id(&self) -> __sdk::ConnectionId { + self.imp.connection_id() + } + fn try_connection_id(&self) -> Option<__sdk::ConnectionId> { + self.imp.try_connection_id() + } +} + +impl DbConnection { + /// Builder-pattern constructor for a connection to a remote module. + /// + /// See [`__sdk::DbConnectionBuilder`] for required and optional configuration for the new connection. + pub fn builder() -> __sdk::DbConnectionBuilder { + __sdk::DbConnectionBuilder::new() + } + + /// If any WebSocket messages are waiting, process one of them. + /// + /// Returns `true` if a message was processed, or `false` if the queue is empty. + /// Callers should invoke this message in a loop until it returns `false` + /// or for as much time is available to process messages. + /// + /// Returns an error if the connection is disconnected. + /// If the disconnection in question was normal, + /// i.e. the result of a call to [`__sdk::DbContext::disconnect`], + /// the returned error will be downcastable to [`__sdk::DisconnectedError`]. + /// + /// This is a low-level primitive exposed for power users who need significant control over scheduling. + /// Most applications should call [`Self::frame_tick`] each frame + /// to fully exhaust the queue whenever time is available. + pub fn advance_one_message(&self) -> __sdk::Result { + self.imp.advance_one_message() + } + + /// Process one WebSocket message, potentially blocking the current thread until one is received. + /// + /// Returns an error if the connection is disconnected. + /// If the disconnection in question was normal, + /// i.e. the result of a call to [`__sdk::DbContext::disconnect`], + /// the returned error will be downcastable to [`__sdk::DisconnectedError`]. + /// + /// This is a low-level primitive exposed for power users who need significant control over scheduling. + /// Most applications should call [`Self::run_threaded`] to spawn a thread + /// which advances the connection automatically. + pub fn advance_one_message_blocking(&self) -> __sdk::Result<()> { + self.imp.advance_one_message_blocking() + } + + /// Process one WebSocket message, `await`ing until one is received. + /// + /// Returns an error if the connection is disconnected. + /// If the disconnection in question was normal, + /// i.e. the result of a call to [`__sdk::DbContext::disconnect`], + /// the returned error will be downcastable to [`__sdk::DisconnectedError`]. + /// + /// This is a low-level primitive exposed for power users who need significant control over scheduling. + /// Most applications should call [`Self::run_async`] to run an `async` loop + /// which advances the connection when polled. + pub async fn advance_one_message_async(&self) -> __sdk::Result<()> { + self.imp.advance_one_message_async().await + } + + /// Process all WebSocket messages waiting in the queue, + /// then return without `await`ing or blocking the current thread. + pub fn frame_tick(&self) -> __sdk::Result<()> { + self.imp.frame_tick() + } + + /// Spawn a thread which processes WebSocket messages as they are received. + pub fn run_threaded(&self) -> std::thread::JoinHandle<()> { + self.imp.run_threaded() + } + + /// Run an `async` loop which processes WebSocket messages when polled. + pub async fn run_async(&self) -> __sdk::Result<()> { + self.imp.run_async().await + } +} + +impl __sdk::DbConnection for DbConnection { + fn new(imp: __sdk::DbContextImpl) -> Self { + Self { + db: RemoteTables { imp: imp.clone() }, + reducers: RemoteReducers { imp: imp.clone() }, + procedures: RemoteProcedures { imp: imp.clone() }, + imp, + } + } +} + +/// A handle on a subscribed query. +// TODO: Document this better after implementing the new subscription API. +#[derive(Clone)] +pub struct SubscriptionHandle { + imp: __sdk::SubscriptionHandleImpl, +} + +impl __sdk::InModule for SubscriptionHandle { + type Module = RemoteModule; +} + +impl __sdk::SubscriptionHandle for SubscriptionHandle { + fn new(imp: __sdk::SubscriptionHandleImpl) -> Self { + Self { imp } + } + + /// Returns true if this subscription has been terminated due to an unsubscribe call or an error. + fn is_ended(&self) -> bool { + self.imp.is_ended() + } + + /// Returns true if this subscription has been applied and has not yet been unsubscribed. + fn is_active(&self) -> bool { + self.imp.is_active() + } + + /// Unsubscribe from the query controlled by this `SubscriptionHandle`, + /// then run `on_end` when its rows are removed from the client cache. + fn unsubscribe_then(self, on_end: __sdk::OnEndedCallback) -> __sdk::Result<()> { + self.imp.unsubscribe_then(Some(on_end)) + } + + fn unsubscribe(self) -> __sdk::Result<()> { + self.imp.unsubscribe_then(None) + } +} + +/// Alias trait for a [`__sdk::DbContext`] connected to this module, +/// with that trait's associated types bounded to this module's concrete types. +/// +/// Users can use this trait as a boundary on definitions which should accept +/// either a [`DbConnection`] or an [`EventContext`] and operate on either. +pub trait RemoteDbContext: + __sdk::DbContext< + DbView = RemoteTables, + Reducers = RemoteReducers, + SubscriptionBuilder = __sdk::SubscriptionBuilder, +> +{ +} +impl< + Ctx: __sdk::DbContext< + DbView = RemoteTables, + Reducers = RemoteReducers, + SubscriptionBuilder = __sdk::SubscriptionBuilder, + >, + > RemoteDbContext for Ctx +{ +} + +/// An [`__sdk::DbContext`] augmented with a [`__sdk::Event`], +/// passed to [`__sdk::Table::on_insert`], [`__sdk::Table::on_delete`] and [`__sdk::TableWithPrimaryKey::on_update`] callbacks. +pub struct EventContext { + /// Access to tables defined by the module via extension traits implemented for [`RemoteTables`]. + pub db: RemoteTables, + /// Access to reducers defined by the module via extension traits implemented for [`RemoteReducers`]. + pub reducers: RemoteReducers, + /// Access to procedures defined by the module via extension traits implemented for [`RemoteProcedures`]. + pub procedures: RemoteProcedures, + /// The event which caused these callbacks to run. + pub event: __sdk::Event, + imp: __sdk::DbContextImpl, +} + +impl __sdk::AbstractEventContext for EventContext { + type Event = __sdk::Event; + fn event(&self) -> &Self::Event { + &self.event + } + fn new(imp: __sdk::DbContextImpl, event: Self::Event) -> Self { + Self { + db: RemoteTables { imp: imp.clone() }, + reducers: RemoteReducers { imp: imp.clone() }, + procedures: RemoteProcedures { imp: imp.clone() }, + event, + imp, + } + } +} + +impl __sdk::InModule for EventContext { + type Module = RemoteModule; +} + +impl __sdk::DbContext for EventContext { + type DbView = RemoteTables; + type Reducers = RemoteReducers; + type Procedures = RemoteProcedures; + + fn db(&self) -> &Self::DbView { + &self.db + } + fn reducers(&self) -> &Self::Reducers { + &self.reducers + } + fn procedures(&self) -> &Self::Procedures { + &self.procedures + } + + fn is_active(&self) -> bool { + self.imp.is_active() + } + + fn disconnect(&self) -> __sdk::Result<()> { + self.imp.disconnect() + } + + type SubscriptionBuilder = __sdk::SubscriptionBuilder; + + fn subscription_builder(&self) -> Self::SubscriptionBuilder { + __sdk::SubscriptionBuilder::new(&self.imp) + } + + fn try_identity(&self) -> Option<__sdk::Identity> { + self.imp.try_identity() + } + fn connection_id(&self) -> __sdk::ConnectionId { + self.imp.connection_id() + } + fn try_connection_id(&self) -> Option<__sdk::ConnectionId> { + self.imp.try_connection_id() + } +} + +impl __sdk::EventContext for EventContext {} + +/// An [`__sdk::DbContext`] augmented with a [`__sdk::ReducerEvent`], +/// passed to on-reducer callbacks. +pub struct ReducerEventContext { + /// Access to tables defined by the module via extension traits implemented for [`RemoteTables`]. + pub db: RemoteTables, + /// Access to reducers defined by the module via extension traits implemented for [`RemoteReducers`]. + pub reducers: RemoteReducers, + /// Access to procedures defined by the module via extension traits implemented for [`RemoteProcedures`]. + pub procedures: RemoteProcedures, + /// The event which caused these callbacks to run. + pub event: __sdk::ReducerEvent, + imp: __sdk::DbContextImpl, +} + +impl __sdk::AbstractEventContext for ReducerEventContext { + type Event = __sdk::ReducerEvent; + fn event(&self) -> &Self::Event { + &self.event + } + fn new(imp: __sdk::DbContextImpl, event: Self::Event) -> Self { + Self { + db: RemoteTables { imp: imp.clone() }, + reducers: RemoteReducers { imp: imp.clone() }, + procedures: RemoteProcedures { imp: imp.clone() }, + event, + imp, + } + } +} + +impl __sdk::InModule for ReducerEventContext { + type Module = RemoteModule; +} + +impl __sdk::DbContext for ReducerEventContext { + type DbView = RemoteTables; + type Reducers = RemoteReducers; + type Procedures = RemoteProcedures; + + fn db(&self) -> &Self::DbView { + &self.db + } + fn reducers(&self) -> &Self::Reducers { + &self.reducers + } + fn procedures(&self) -> &Self::Procedures { + &self.procedures + } + + fn is_active(&self) -> bool { + self.imp.is_active() + } + + fn disconnect(&self) -> __sdk::Result<()> { + self.imp.disconnect() + } + + type SubscriptionBuilder = __sdk::SubscriptionBuilder; + + fn subscription_builder(&self) -> Self::SubscriptionBuilder { + __sdk::SubscriptionBuilder::new(&self.imp) + } + + fn try_identity(&self) -> Option<__sdk::Identity> { + self.imp.try_identity() + } + fn connection_id(&self) -> __sdk::ConnectionId { + self.imp.connection_id() + } + fn try_connection_id(&self) -> Option<__sdk::ConnectionId> { + self.imp.try_connection_id() + } +} + +impl __sdk::ReducerEventContext for ReducerEventContext {} + +/// An [`__sdk::DbContext`] passed to procedure callbacks. +pub struct ProcedureEventContext { + /// Access to tables defined by the module via extension traits implemented for [`RemoteTables`]. + pub db: RemoteTables, + /// Access to reducers defined by the module via extension traits implemented for [`RemoteReducers`]. + pub reducers: RemoteReducers, + /// Access to procedures defined by the module via extension traits implemented for [`RemoteProcedures`]. + pub procedures: RemoteProcedures, + imp: __sdk::DbContextImpl, +} + +impl __sdk::AbstractEventContext for ProcedureEventContext { + type Event = (); + fn event(&self) -> &Self::Event { + &() + } + fn new(imp: __sdk::DbContextImpl, _event: Self::Event) -> Self { + Self { + db: RemoteTables { imp: imp.clone() }, + reducers: RemoteReducers { imp: imp.clone() }, + procedures: RemoteProcedures { imp: imp.clone() }, + imp, + } + } +} + +impl __sdk::InModule for ProcedureEventContext { + type Module = RemoteModule; +} + +impl __sdk::DbContext for ProcedureEventContext { + type DbView = RemoteTables; + type Reducers = RemoteReducers; + type Procedures = RemoteProcedures; + + fn db(&self) -> &Self::DbView { + &self.db + } + fn reducers(&self) -> &Self::Reducers { + &self.reducers + } + fn procedures(&self) -> &Self::Procedures { + &self.procedures + } + + fn is_active(&self) -> bool { + self.imp.is_active() + } + + fn disconnect(&self) -> __sdk::Result<()> { + self.imp.disconnect() + } + + type SubscriptionBuilder = __sdk::SubscriptionBuilder; + + fn subscription_builder(&self) -> Self::SubscriptionBuilder { + __sdk::SubscriptionBuilder::new(&self.imp) + } + + fn try_identity(&self) -> Option<__sdk::Identity> { + self.imp.try_identity() + } + fn connection_id(&self) -> __sdk::ConnectionId { + self.imp.connection_id() + } + fn try_connection_id(&self) -> Option<__sdk::ConnectionId> { + self.imp.try_connection_id() + } +} + +impl __sdk::ProcedureEventContext for ProcedureEventContext {} + +/// An [`__sdk::DbContext`] passed to [`__sdk::SubscriptionBuilder::on_applied`] and [`SubscriptionHandle::unsubscribe_then`] callbacks. +pub struct SubscriptionEventContext { + /// Access to tables defined by the module via extension traits implemented for [`RemoteTables`]. + pub db: RemoteTables, + /// Access to reducers defined by the module via extension traits implemented for [`RemoteReducers`]. + pub reducers: RemoteReducers, + /// Access to procedures defined by the module via extension traits implemented for [`RemoteProcedures`]. + pub procedures: RemoteProcedures, + imp: __sdk::DbContextImpl, +} + +impl __sdk::AbstractEventContext for SubscriptionEventContext { + type Event = (); + fn event(&self) -> &Self::Event { + &() + } + fn new(imp: __sdk::DbContextImpl, _event: Self::Event) -> Self { + Self { + db: RemoteTables { imp: imp.clone() }, + reducers: RemoteReducers { imp: imp.clone() }, + procedures: RemoteProcedures { imp: imp.clone() }, + imp, + } + } +} + +impl __sdk::InModule for SubscriptionEventContext { + type Module = RemoteModule; +} + +impl __sdk::DbContext for SubscriptionEventContext { + type DbView = RemoteTables; + type Reducers = RemoteReducers; + type Procedures = RemoteProcedures; + + fn db(&self) -> &Self::DbView { + &self.db + } + fn reducers(&self) -> &Self::Reducers { + &self.reducers + } + fn procedures(&self) -> &Self::Procedures { + &self.procedures + } + + fn is_active(&self) -> bool { + self.imp.is_active() + } + + fn disconnect(&self) -> __sdk::Result<()> { + self.imp.disconnect() + } + + type SubscriptionBuilder = __sdk::SubscriptionBuilder; + + fn subscription_builder(&self) -> Self::SubscriptionBuilder { + __sdk::SubscriptionBuilder::new(&self.imp) + } + + fn try_identity(&self) -> Option<__sdk::Identity> { + self.imp.try_identity() + } + fn connection_id(&self) -> __sdk::ConnectionId { + self.imp.connection_id() + } + fn try_connection_id(&self) -> Option<__sdk::ConnectionId> { + self.imp.try_connection_id() + } +} + +impl __sdk::SubscriptionEventContext for SubscriptionEventContext {} + +/// An [`__sdk::DbContext`] augmented with a [`__sdk::Error`], +/// passed to [`__sdk::DbConnectionBuilder::on_disconnect`], [`__sdk::DbConnectionBuilder::on_connect_error`] and [`__sdk::SubscriptionBuilder::on_error`] callbacks. +pub struct ErrorContext { + /// Access to tables defined by the module via extension traits implemented for [`RemoteTables`]. + pub db: RemoteTables, + /// Access to reducers defined by the module via extension traits implemented for [`RemoteReducers`]. + pub reducers: RemoteReducers, + /// Access to procedures defined by the module via extension traits implemented for [`RemoteProcedures`]. + pub procedures: RemoteProcedures, + /// The event which caused these callbacks to run. + pub event: Option<__sdk::Error>, + imp: __sdk::DbContextImpl, +} + +impl __sdk::AbstractEventContext for ErrorContext { + type Event = Option<__sdk::Error>; + fn event(&self) -> &Self::Event { + &self.event + } + fn new(imp: __sdk::DbContextImpl, event: Self::Event) -> Self { + Self { + db: RemoteTables { imp: imp.clone() }, + reducers: RemoteReducers { imp: imp.clone() }, + procedures: RemoteProcedures { imp: imp.clone() }, + event, + imp, + } + } +} + +impl __sdk::InModule for ErrorContext { + type Module = RemoteModule; +} + +impl __sdk::DbContext for ErrorContext { + type DbView = RemoteTables; + type Reducers = RemoteReducers; + type Procedures = RemoteProcedures; + + fn db(&self) -> &Self::DbView { + &self.db + } + fn reducers(&self) -> &Self::Reducers { + &self.reducers + } + fn procedures(&self) -> &Self::Procedures { + &self.procedures + } + + fn is_active(&self) -> bool { + self.imp.is_active() + } + + fn disconnect(&self) -> __sdk::Result<()> { + self.imp.disconnect() + } + + type SubscriptionBuilder = __sdk::SubscriptionBuilder; + + fn subscription_builder(&self) -> Self::SubscriptionBuilder { + __sdk::SubscriptionBuilder::new(&self.imp) + } + + fn try_identity(&self) -> Option<__sdk::Identity> { + self.imp.try_identity() + } + fn connection_id(&self) -> __sdk::ConnectionId { + self.imp.connection_id() + } + fn try_connection_id(&self) -> Option<__sdk::ConnectionId> { + self.imp.try_connection_id() + } +} + +impl __sdk::ErrorContext for ErrorContext {} + +impl __sdk::SpacetimeModule for RemoteModule { + type DbConnection = DbConnection; + type EventContext = EventContext; + type ReducerEventContext = ReducerEventContext; + type ProcedureEventContext = ProcedureEventContext; + type SubscriptionEventContext = SubscriptionEventContext; + type ErrorContext = ErrorContext; + type Reducer = Reducer; + type DbView = RemoteTables; + type Reducers = RemoteReducers; + type Procedures = RemoteProcedures; + type DbUpdate = DbUpdate; + type AppliedDiff<'r> = AppliedDiff<'r>; + type SubscriptionHandle = SubscriptionHandle; + type QueryBuilder = __sdk::QueryBuilder; + + fn register_tables(client_cache: &mut __sdk::ClientCache) { + all_view_pk_players_table::register_table(client_cache); + sender_view_pk_players_a_table::register_table(client_cache); + sender_view_pk_players_b_table::register_table(client_cache); + view_pk_membership_table::register_table(client_cache); + view_pk_membership_secondary_table::register_table(client_cache); + view_pk_player_table::register_table(client_cache); + } + const ALL_TABLE_NAMES: &'static [&'static str] = &[ + "all_view_pk_players", + "sender_view_pk_players_a", + "sender_view_pk_players_b", + "view_pk_membership", + "view_pk_membership_secondary", + "view_pk_player", + ]; +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/sender_view_pk_players_a_table.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/sender_view_pk_players_a_table.rs new file mode 100644 index 00000000000..a052c0291d2 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/sender_view_pk_players_a_table.rs @@ -0,0 +1,159 @@ +// 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 super::view_pk_player_type::ViewPkPlayer; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `sender_view_pk_players_a`. +/// +/// Obtain a handle from the [`SenderViewPkPlayersATableAccess::sender_view_pk_players_a`] method on [`super::RemoteTables`], +/// like `ctx.db.sender_view_pk_players_a()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.sender_view_pk_players_a().on_insert(...)`. +pub struct SenderViewPkPlayersATableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `sender_view_pk_players_a`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait SenderViewPkPlayersATableAccess { + #[allow(non_snake_case)] + /// Obtain a [`SenderViewPkPlayersATableHandle`], which mediates access to the table `sender_view_pk_players_a`. + fn sender_view_pk_players_a(&self) -> SenderViewPkPlayersATableHandle<'_>; +} + +impl SenderViewPkPlayersATableAccess for super::RemoteTables { + fn sender_view_pk_players_a(&self) -> SenderViewPkPlayersATableHandle<'_> { + SenderViewPkPlayersATableHandle { + imp: self.imp.get_table::("sender_view_pk_players_a"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct SenderViewPkPlayersAInsertCallbackId(__sdk::CallbackId); +pub struct SenderViewPkPlayersADeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for SenderViewPkPlayersATableHandle<'ctx> { + type Row = ViewPkPlayer; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = SenderViewPkPlayersAInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> SenderViewPkPlayersAInsertCallbackId { + SenderViewPkPlayersAInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: SenderViewPkPlayersAInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = SenderViewPkPlayersADeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> SenderViewPkPlayersADeleteCallbackId { + SenderViewPkPlayersADeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: SenderViewPkPlayersADeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct SenderViewPkPlayersAUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for SenderViewPkPlayersATableHandle<'ctx> { + type UpdateCallbackId = SenderViewPkPlayersAUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> SenderViewPkPlayersAUpdateCallbackId { + SenderViewPkPlayersAUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: SenderViewPkPlayersAUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `id` unique index on the table `sender_view_pk_players_a`, +/// which allows point queries on the field of the same name +/// via the [`SenderViewPkPlayersAIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.sender_view_pk_players_a().id().find(...)`. +pub struct SenderViewPkPlayersAIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> SenderViewPkPlayersATableHandle<'ctx> { + /// Get a handle on the `id` unique index on the table `sender_view_pk_players_a`. + pub fn id(&self) -> SenderViewPkPlayersAIdUnique<'ctx> { + SenderViewPkPlayersAIdUnique { + imp: self.imp.get_unique_constraint::("id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> SenderViewPkPlayersAIdUnique<'ctx> { + /// Find the subscribed row whose `id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &u64) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("sender_view_pk_players_a"); + _table.add_unique_constraint::("id", |row| &row.id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `ViewPkPlayer`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait sender_view_pk_players_aQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `ViewPkPlayer`. + fn sender_view_pk_players_a(&self) -> __sdk::__query_builder::Table; +} + +impl sender_view_pk_players_aQueryTableAccess for __sdk::QueryTableAccessor { + fn sender_view_pk_players_a(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("sender_view_pk_players_a") + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/sender_view_pk_players_b_table.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/sender_view_pk_players_b_table.rs new file mode 100644 index 00000000000..cdb82a39446 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/sender_view_pk_players_b_table.rs @@ -0,0 +1,159 @@ +// 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 super::view_pk_player_type::ViewPkPlayer; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `sender_view_pk_players_b`. +/// +/// Obtain a handle from the [`SenderViewPkPlayersBTableAccess::sender_view_pk_players_b`] method on [`super::RemoteTables`], +/// like `ctx.db.sender_view_pk_players_b()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.sender_view_pk_players_b().on_insert(...)`. +pub struct SenderViewPkPlayersBTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `sender_view_pk_players_b`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait SenderViewPkPlayersBTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`SenderViewPkPlayersBTableHandle`], which mediates access to the table `sender_view_pk_players_b`. + fn sender_view_pk_players_b(&self) -> SenderViewPkPlayersBTableHandle<'_>; +} + +impl SenderViewPkPlayersBTableAccess for super::RemoteTables { + fn sender_view_pk_players_b(&self) -> SenderViewPkPlayersBTableHandle<'_> { + SenderViewPkPlayersBTableHandle { + imp: self.imp.get_table::("sender_view_pk_players_b"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct SenderViewPkPlayersBInsertCallbackId(__sdk::CallbackId); +pub struct SenderViewPkPlayersBDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for SenderViewPkPlayersBTableHandle<'ctx> { + type Row = ViewPkPlayer; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = SenderViewPkPlayersBInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> SenderViewPkPlayersBInsertCallbackId { + SenderViewPkPlayersBInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: SenderViewPkPlayersBInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = SenderViewPkPlayersBDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> SenderViewPkPlayersBDeleteCallbackId { + SenderViewPkPlayersBDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: SenderViewPkPlayersBDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct SenderViewPkPlayersBUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for SenderViewPkPlayersBTableHandle<'ctx> { + type UpdateCallbackId = SenderViewPkPlayersBUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> SenderViewPkPlayersBUpdateCallbackId { + SenderViewPkPlayersBUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: SenderViewPkPlayersBUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `id` unique index on the table `sender_view_pk_players_b`, +/// which allows point queries on the field of the same name +/// via the [`SenderViewPkPlayersBIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.sender_view_pk_players_b().id().find(...)`. +pub struct SenderViewPkPlayersBIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> SenderViewPkPlayersBTableHandle<'ctx> { + /// Get a handle on the `id` unique index on the table `sender_view_pk_players_b`. + pub fn id(&self) -> SenderViewPkPlayersBIdUnique<'ctx> { + SenderViewPkPlayersBIdUnique { + imp: self.imp.get_unique_constraint::("id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> SenderViewPkPlayersBIdUnique<'ctx> { + /// Find the subscribed row whose `id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &u64) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("sender_view_pk_players_b"); + _table.add_unique_constraint::("id", |row| &row.id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `ViewPkPlayer`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait sender_view_pk_players_bQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `ViewPkPlayer`. + fn sender_view_pk_players_b(&self) -> __sdk::__query_builder::Table; +} + +impl sender_view_pk_players_bQueryTableAccess for __sdk::QueryTableAccessor { + fn sender_view_pk_players_b(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("sender_view_pk_players_b") + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/update_view_pk_player_reducer.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/update_view_pk_player_reducer.rs new file mode 100644 index 00000000000..f3c06006ba6 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/update_view_pk_player_reducer.rs @@ -0,0 +1,72 @@ +// 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(super) struct UpdateViewPkPlayerArgs { + pub id: u64, + pub name: String, +} + +impl From for super::Reducer { + fn from(args: UpdateViewPkPlayerArgs) -> Self { + Self::UpdateViewPkPlayer { + id: args.id, + name: args.name, + } + } +} + +impl __sdk::InModule for UpdateViewPkPlayerArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `update_view_pk_player`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait update_view_pk_player { + /// Request that the remote module invoke the reducer `update_view_pk_player` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and this method provides no way to listen for its completion status. + /// /// Use [`update_view_pk_player:update_view_pk_player_then`] to run a callback after the reducer completes. + fn update_view_pk_player(&self, id: u64, name: String) -> __sdk::Result<()> { + self.update_view_pk_player_then(id, name, |_, _| {}) + } + + /// Request that the remote module invoke the reducer `update_view_pk_player` to run as soon as possible, + /// registering `callback` to run when we are notified that the reducer completed. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed with the `callback`. + fn update_view_pk_player_then( + &self, + id: u64, + name: String, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()>; +} + +impl update_view_pk_player for super::RemoteReducers { + fn update_view_pk_player_then( + &self, + id: u64, + name: String, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()> { + self.imp + .invoke_reducer_with_callback(UpdateViewPkPlayerArgs { id, name }, callback) + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_secondary_table.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_secondary_table.rs new file mode 100644 index 00000000000..f7f49d58b73 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_secondary_table.rs @@ -0,0 +1,161 @@ +// 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 super::view_pk_membership_secondary_type::ViewPkMembershipSecondary; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `view_pk_membership_secondary`. +/// +/// Obtain a handle from the [`ViewPkMembershipSecondaryTableAccess::view_pk_membership_secondary`] method on [`super::RemoteTables`], +/// like `ctx.db.view_pk_membership_secondary()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.view_pk_membership_secondary().on_insert(...)`. +pub struct ViewPkMembershipSecondaryTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `view_pk_membership_secondary`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait ViewPkMembershipSecondaryTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`ViewPkMembershipSecondaryTableHandle`], which mediates access to the table `view_pk_membership_secondary`. + fn view_pk_membership_secondary(&self) -> ViewPkMembershipSecondaryTableHandle<'_>; +} + +impl ViewPkMembershipSecondaryTableAccess for super::RemoteTables { + fn view_pk_membership_secondary(&self) -> ViewPkMembershipSecondaryTableHandle<'_> { + ViewPkMembershipSecondaryTableHandle { + imp: self + .imp + .get_table::("view_pk_membership_secondary"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct ViewPkMembershipSecondaryInsertCallbackId(__sdk::CallbackId); +pub struct ViewPkMembershipSecondaryDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for ViewPkMembershipSecondaryTableHandle<'ctx> { + type Row = ViewPkMembershipSecondary; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = ViewPkMembershipSecondaryInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ViewPkMembershipSecondaryInsertCallbackId { + ViewPkMembershipSecondaryInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: ViewPkMembershipSecondaryInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = ViewPkMembershipSecondaryDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ViewPkMembershipSecondaryDeleteCallbackId { + ViewPkMembershipSecondaryDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: ViewPkMembershipSecondaryDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct ViewPkMembershipSecondaryUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for ViewPkMembershipSecondaryTableHandle<'ctx> { + type UpdateCallbackId = ViewPkMembershipSecondaryUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> ViewPkMembershipSecondaryUpdateCallbackId { + ViewPkMembershipSecondaryUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: ViewPkMembershipSecondaryUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `id` unique index on the table `view_pk_membership_secondary`, +/// which allows point queries on the field of the same name +/// via the [`ViewPkMembershipSecondaryIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.view_pk_membership_secondary().id().find(...)`. +pub struct ViewPkMembershipSecondaryIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> ViewPkMembershipSecondaryTableHandle<'ctx> { + /// Get a handle on the `id` unique index on the table `view_pk_membership_secondary`. + pub fn id(&self) -> ViewPkMembershipSecondaryIdUnique<'ctx> { + ViewPkMembershipSecondaryIdUnique { + imp: self.imp.get_unique_constraint::("id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> ViewPkMembershipSecondaryIdUnique<'ctx> { + /// Find the subscribed row whose `id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &u64) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("view_pk_membership_secondary"); + _table.add_unique_constraint::("id", |row| &row.id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `ViewPkMembershipSecondary`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait view_pk_membership_secondaryQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `ViewPkMembershipSecondary`. + fn view_pk_membership_secondary(&self) -> __sdk::__query_builder::Table; +} + +impl view_pk_membership_secondaryQueryTableAccess for __sdk::QueryTableAccessor { + fn view_pk_membership_secondary(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("view_pk_membership_secondary") + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_secondary_type.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_secondary_type.rs new file mode 100644 index 00000000000..7c39e65a6fe --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_secondary_type.rs @@ -0,0 +1,54 @@ +// 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 ViewPkMembershipSecondary { + pub id: u64, + pub player_id: u64, +} + +impl __sdk::InModule for ViewPkMembershipSecondary { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `ViewPkMembershipSecondary`. +/// +/// Provides typed access to columns for query building. +pub struct ViewPkMembershipSecondaryCols { + pub id: __sdk::__query_builder::Col, + pub player_id: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for ViewPkMembershipSecondary { + type Cols = ViewPkMembershipSecondaryCols; + fn cols(table_name: &'static str) -> Self::Cols { + ViewPkMembershipSecondaryCols { + id: __sdk::__query_builder::Col::new(table_name, "id"), + player_id: __sdk::__query_builder::Col::new(table_name, "player_id"), + } + } +} + +/// Indexed column accessor struct for the table `ViewPkMembershipSecondary`. +/// +/// Provides typed access to indexed columns for query building. +pub struct ViewPkMembershipSecondaryIxCols { + pub id: __sdk::__query_builder::IxCol, + pub player_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for ViewPkMembershipSecondary { + type IxCols = ViewPkMembershipSecondaryIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + ViewPkMembershipSecondaryIxCols { + id: __sdk::__query_builder::IxCol::new(table_name, "id"), + player_id: __sdk::__query_builder::IxCol::new(table_name, "player_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for ViewPkMembershipSecondary {} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_table.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_table.rs new file mode 100644 index 00000000000..bc491cad1bf --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_table.rs @@ -0,0 +1,159 @@ +// 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 super::view_pk_membership_type::ViewPkMembership; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `view_pk_membership`. +/// +/// Obtain a handle from the [`ViewPkMembershipTableAccess::view_pk_membership`] method on [`super::RemoteTables`], +/// like `ctx.db.view_pk_membership()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.view_pk_membership().on_insert(...)`. +pub struct ViewPkMembershipTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `view_pk_membership`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait ViewPkMembershipTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`ViewPkMembershipTableHandle`], which mediates access to the table `view_pk_membership`. + fn view_pk_membership(&self) -> ViewPkMembershipTableHandle<'_>; +} + +impl ViewPkMembershipTableAccess for super::RemoteTables { + fn view_pk_membership(&self) -> ViewPkMembershipTableHandle<'_> { + ViewPkMembershipTableHandle { + imp: self.imp.get_table::("view_pk_membership"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct ViewPkMembershipInsertCallbackId(__sdk::CallbackId); +pub struct ViewPkMembershipDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for ViewPkMembershipTableHandle<'ctx> { + type Row = ViewPkMembership; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = ViewPkMembershipInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ViewPkMembershipInsertCallbackId { + ViewPkMembershipInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: ViewPkMembershipInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = ViewPkMembershipDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ViewPkMembershipDeleteCallbackId { + ViewPkMembershipDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: ViewPkMembershipDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct ViewPkMembershipUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for ViewPkMembershipTableHandle<'ctx> { + type UpdateCallbackId = ViewPkMembershipUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> ViewPkMembershipUpdateCallbackId { + ViewPkMembershipUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: ViewPkMembershipUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `id` unique index on the table `view_pk_membership`, +/// which allows point queries on the field of the same name +/// via the [`ViewPkMembershipIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.view_pk_membership().id().find(...)`. +pub struct ViewPkMembershipIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> ViewPkMembershipTableHandle<'ctx> { + /// Get a handle on the `id` unique index on the table `view_pk_membership`. + pub fn id(&self) -> ViewPkMembershipIdUnique<'ctx> { + ViewPkMembershipIdUnique { + imp: self.imp.get_unique_constraint::("id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> ViewPkMembershipIdUnique<'ctx> { + /// Find the subscribed row whose `id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &u64) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("view_pk_membership"); + _table.add_unique_constraint::("id", |row| &row.id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `ViewPkMembership`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait view_pk_membershipQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `ViewPkMembership`. + fn view_pk_membership(&self) -> __sdk::__query_builder::Table; +} + +impl view_pk_membershipQueryTableAccess for __sdk::QueryTableAccessor { + fn view_pk_membership(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("view_pk_membership") + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_type.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_type.rs new file mode 100644 index 00000000000..001b4eacc4b --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_membership_type.rs @@ -0,0 +1,54 @@ +// 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 ViewPkMembership { + pub id: u64, + pub player_id: u64, +} + +impl __sdk::InModule for ViewPkMembership { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `ViewPkMembership`. +/// +/// Provides typed access to columns for query building. +pub struct ViewPkMembershipCols { + pub id: __sdk::__query_builder::Col, + pub player_id: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for ViewPkMembership { + type Cols = ViewPkMembershipCols; + fn cols(table_name: &'static str) -> Self::Cols { + ViewPkMembershipCols { + id: __sdk::__query_builder::Col::new(table_name, "id"), + player_id: __sdk::__query_builder::Col::new(table_name, "player_id"), + } + } +} + +/// Indexed column accessor struct for the table `ViewPkMembership`. +/// +/// Provides typed access to indexed columns for query building. +pub struct ViewPkMembershipIxCols { + pub id: __sdk::__query_builder::IxCol, + pub player_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for ViewPkMembership { + type IxCols = ViewPkMembershipIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + ViewPkMembershipIxCols { + id: __sdk::__query_builder::IxCol::new(table_name, "id"), + player_id: __sdk::__query_builder::IxCol::new(table_name, "player_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for ViewPkMembership {} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_player_table.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_player_table.rs new file mode 100644 index 00000000000..6a5db875df9 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_player_table.rs @@ -0,0 +1,159 @@ +// 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 super::view_pk_player_type::ViewPkPlayer; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `view_pk_player`. +/// +/// Obtain a handle from the [`ViewPkPlayerTableAccess::view_pk_player`] method on [`super::RemoteTables`], +/// like `ctx.db.view_pk_player()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.view_pk_player().on_insert(...)`. +pub struct ViewPkPlayerTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `view_pk_player`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait ViewPkPlayerTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`ViewPkPlayerTableHandle`], which mediates access to the table `view_pk_player`. + fn view_pk_player(&self) -> ViewPkPlayerTableHandle<'_>; +} + +impl ViewPkPlayerTableAccess for super::RemoteTables { + fn view_pk_player(&self) -> ViewPkPlayerTableHandle<'_> { + ViewPkPlayerTableHandle { + imp: self.imp.get_table::("view_pk_player"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct ViewPkPlayerInsertCallbackId(__sdk::CallbackId); +pub struct ViewPkPlayerDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for ViewPkPlayerTableHandle<'ctx> { + type Row = ViewPkPlayer; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = ViewPkPlayerInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ViewPkPlayerInsertCallbackId { + ViewPkPlayerInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: ViewPkPlayerInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = ViewPkPlayerDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ViewPkPlayerDeleteCallbackId { + ViewPkPlayerDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: ViewPkPlayerDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct ViewPkPlayerUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for ViewPkPlayerTableHandle<'ctx> { + type UpdateCallbackId = ViewPkPlayerUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> ViewPkPlayerUpdateCallbackId { + ViewPkPlayerUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: ViewPkPlayerUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `id` unique index on the table `view_pk_player`, +/// which allows point queries on the field of the same name +/// via the [`ViewPkPlayerIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.view_pk_player().id().find(...)`. +pub struct ViewPkPlayerIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> ViewPkPlayerTableHandle<'ctx> { + /// Get a handle on the `id` unique index on the table `view_pk_player`. + pub fn id(&self) -> ViewPkPlayerIdUnique<'ctx> { + ViewPkPlayerIdUnique { + imp: self.imp.get_unique_constraint::("id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> ViewPkPlayerIdUnique<'ctx> { + /// Find the subscribed row whose `id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &u64) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("view_pk_player"); + _table.add_unique_constraint::("id", |row| &row.id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `ViewPkPlayer`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait view_pk_playerQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `ViewPkPlayer`. + fn view_pk_player(&self) -> __sdk::__query_builder::Table; +} + +impl view_pk_playerQueryTableAccess for __sdk::QueryTableAccessor { + fn view_pk_player(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("view_pk_player") + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_player_type.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_player_type.rs new file mode 100644 index 00000000000..5efbca0f434 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/view_pk_player_type.rs @@ -0,0 +1,52 @@ +// 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 ViewPkPlayer { + pub id: u64, + pub name: String, +} + +impl __sdk::InModule for ViewPkPlayer { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `ViewPkPlayer`. +/// +/// Provides typed access to columns for query building. +pub struct ViewPkPlayerCols { + pub id: __sdk::__query_builder::Col, + pub name: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for ViewPkPlayer { + type Cols = ViewPkPlayerCols; + fn cols(table_name: &'static str) -> Self::Cols { + ViewPkPlayerCols { + id: __sdk::__query_builder::Col::new(table_name, "id"), + name: __sdk::__query_builder::Col::new(table_name, "name"), + } + } +} + +/// Indexed column accessor struct for the table `ViewPkPlayer`. +/// +/// Provides typed access to indexed columns for query building. +pub struct ViewPkPlayerIxCols { + pub id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for ViewPkPlayer { + type IxCols = ViewPkPlayerIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + ViewPkPlayerIxCols { + id: __sdk::__query_builder::IxCol::new(table_name, "id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for ViewPkPlayer {} From ab19a381dd6feaaf50f050dbcbbbafaaeac60033 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 5 Mar 2026 11:31:30 -0800 Subject: [PATCH 3/4] Update client codegen for query builder views with primary keys --- crates/codegen/src/rust.rs | 12 +- crates/execution/src/pipelined.rs | 74 ++-- crates/lib/src/db/mod.rs | 1 + crates/lib/src/db/raw_def/v9.rs | 12 +- crates/lib/src/db/view.rs | 32 ++ crates/physical-plan/src/plan.rs | 165 ++++++++- crates/physical-plan/src/rules.rs | 337 +++++++++++++++--- crates/query-builder/src/lib.rs | 7 +- crates/schema/src/def.rs | 20 +- crates/schema/src/def/validate/v10.rs | 58 ++- crates/schema/src/def/validate/v9.rs | 26 +- crates/schema/src/schema.rs | 86 ++++- .../all_view_pk_players_table.rs | 48 +++ .../view-pk-client/src/module_bindings/mod.rs | 2 +- 14 files changed, 731 insertions(+), 149 deletions(-) create mode 100644 crates/lib/src/db/view.rs diff --git a/crates/codegen/src/rust.rs b/crates/codegen/src/rust.rs index 086d467af97..c6ed504f714 100644 --- a/crates/codegen/src/rust.rs +++ b/crates/codegen/src/rust.rs @@ -1411,9 +1411,19 @@ impl __sdk::InModule for DbUpdate {{ } for view in iter_views(module) { let field_name = table_method_name(&view.accessor_name); + let with_updates = view + .primary_key + .map(|col| { + let pk_field = view.return_columns[col.idx()] + .accessor_name + .deref() + .to_case(Case::Snake); + format!(".with_updates_by_pk(|row| &row.{pk_field})") + }) + .unwrap_or_default(); writeln!( out, - "diff.{field_name} = cache.apply_diff_to_table::<{}>({:?}, &self.{field_name});", + "diff.{field_name} = cache.apply_diff_to_table::<{}>({:?}, &self.{field_name}){with_updates};", type_ref_name(module, view.product_type_ref), view.name.deref(), ); diff --git a/crates/execution/src/pipelined.rs b/crates/execution/src/pipelined.rs index 99b815c75d3..c9462fc8cad 100644 --- a/crates/execution/src/pipelined.rs +++ b/crates/execution/src/pipelined.rs @@ -341,6 +341,7 @@ impl From for PipelinedExecutor { lhs, rhs, rhs_index, + rhs_prefix, rhs_field, unique, lhs_field, @@ -352,6 +353,7 @@ impl From for PipelinedExecutor { lhs: Box::new(Self::from(*lhs)), rhs_table: rhs.table_id, rhs_index, + rhs_prefix, rhs_field, lhs_field, unique, @@ -362,6 +364,7 @@ impl From for PipelinedExecutor { lhs, rhs, rhs_index, + rhs_prefix, rhs_field, unique, lhs_field, @@ -373,6 +376,7 @@ impl From for PipelinedExecutor { lhs: Box::new(Self::from(*lhs)), rhs_table: rhs.table_id, rhs_index, + rhs_prefix, rhs_field, rhs_delta, lhs_field, @@ -923,6 +927,16 @@ fn combine_prefix_and_last(prefix: Vec<(ColId, AlgebraicValue)>, last: Algebraic } } +fn combine_probe_prefix_and_last(prefix: &[AlgebraicValue], last: AlgebraicValue) -> AlgebraicValue { + if prefix.is_empty() { + last + } else { + AlgebraicValue::product(ProductValue::from_iter( + prefix.iter().cloned().chain(std::iter::once(last)), + )) + } +} + impl PipelinedIxScanEq { /// We don't know statically if an index scan will return rows pub fn is_empty(&self, _: &impl DeltaStore) -> bool { @@ -969,6 +983,8 @@ pub struct PipelinedIxJoin { pub rhs_table: TableId, /// The rhs index pub rhs_index: IndexId, + /// Constant prefix values for multi-column index probes. + pub rhs_prefix: Vec, /// The rhs join field pub rhs_field: ColId, /// The lhs join field @@ -996,7 +1012,7 @@ impl PipelinedIxJoin { let mut bytes_scanned = 0; let iter_rhs = |u: &Tuple, lhs_field: &TupleField, bytes_scanned: &mut usize| -> Result<_> { - let key = project(u, lhs_field, bytes_scanned); + let key = combine_probe_prefix_and_last(&self.rhs_prefix, project(u, lhs_field, bytes_scanned)); Ok(tx .index_scan_point(self.rhs_table, self.rhs_index, &key)? .map(Row::Ptr) @@ -1135,6 +1151,8 @@ pub struct PipelinedIxDeltaJoin { pub rhs_delta: Delta, /// The rhs index pub rhs_index: IndexId, + /// Constant prefix values for multi-column index probes. + pub rhs_prefix: Vec, /// The rhs join field pub rhs_field: ColId, /// The lhs join field @@ -1177,13 +1195,10 @@ impl PipelinedIxDeltaJoin { lhs.execute(tx, metrics, &mut |u| { n += 1; index_seeks += 1; + let key = + combine_probe_prefix_and_last(&self.rhs_prefix, project(&u, lhs_field, &mut bytes_scanned)); if tx - .index_scan_point_for_delta( - self.rhs_table, - self.rhs_index, - self.rhs_delta, - &project(&u, lhs_field, &mut bytes_scanned), - ) + .index_scan_point_for_delta(self.rhs_table, self.rhs_index, self.rhs_delta, &key) .next() .is_some() { @@ -1203,13 +1218,10 @@ impl PipelinedIxDeltaJoin { lhs.execute(tx, metrics, &mut |u| { n += 1; index_seeks += 1; + let key = + combine_probe_prefix_and_last(&self.rhs_prefix, project(&u, lhs_field, &mut bytes_scanned)); if let Some(v) = tx - .index_scan_point_for_delta( - self.rhs_table, - self.rhs_index, - self.rhs_delta, - &project(&u, lhs_field, &mut bytes_scanned), - ) + .index_scan_point_for_delta(self.rhs_table, self.rhs_index, self.rhs_delta, &key) .next() .map(Tuple::Row) { @@ -1229,13 +1241,10 @@ impl PipelinedIxDeltaJoin { lhs.execute(tx, metrics, &mut |u| { n += 1; index_seeks += 1; + let key = + combine_probe_prefix_and_last(&self.rhs_prefix, project(&u, lhs_field, &mut bytes_scanned)); if let Some(v) = tx - .index_scan_point_for_delta( - self.rhs_table, - self.rhs_index, - self.rhs_delta, - &project(&u, lhs_field, &mut bytes_scanned), - ) + .index_scan_point_for_delta(self.rhs_table, self.rhs_index, self.rhs_delta, &key) .next() .map(Tuple::Row) { @@ -1256,13 +1265,10 @@ impl PipelinedIxDeltaJoin { lhs.execute(tx, metrics, &mut |u| { n += 1; index_seeks += 1; + let key = + combine_probe_prefix_and_last(&self.rhs_prefix, project(&u, lhs_field, &mut bytes_scanned)); for _ in 0..tx - .index_scan_point_for_delta( - self.rhs_table, - self.rhs_index, - self.rhs_delta, - &project(&u, lhs_field, &mut bytes_scanned), - ) + .index_scan_point_for_delta(self.rhs_table, self.rhs_index, self.rhs_delta, &key) .count() { f(u.clone())?; @@ -1281,13 +1287,10 @@ impl PipelinedIxDeltaJoin { lhs.execute(tx, metrics, &mut |u| { n += 1; index_seeks += 1; + let key = + combine_probe_prefix_and_last(&self.rhs_prefix, project(&u, lhs_field, &mut bytes_scanned)); for v in tx - .index_scan_point_for_delta( - self.rhs_table, - self.rhs_index, - self.rhs_delta, - &project(&u, lhs_field, &mut bytes_scanned), - ) + .index_scan_point_for_delta(self.rhs_table, self.rhs_index, self.rhs_delta, &key) .map(Tuple::Row) { f(v)?; @@ -1306,13 +1309,10 @@ impl PipelinedIxDeltaJoin { lhs.execute(tx, metrics, &mut |u| { n += 1; index_seeks += 1; + let key = + combine_probe_prefix_and_last(&self.rhs_prefix, project(&u, lhs_field, &mut bytes_scanned)); for v in tx - .index_scan_point_for_delta( - self.rhs_table, - self.rhs_index, - self.rhs_delta, - &project(&u, lhs_field, &mut bytes_scanned), - ) + .index_scan_point_for_delta(self.rhs_table, self.rhs_index, self.rhs_delta, &key) .map(Tuple::Row) { f(u.clone().join(v.clone()))?; diff --git a/crates/lib/src/db/mod.rs b/crates/lib/src/db/mod.rs index 6bdbca93d62..e287f56f438 100644 --- a/crates/lib/src/db/mod.rs +++ b/crates/lib/src/db/mod.rs @@ -4,3 +4,4 @@ pub mod attr; pub mod auth; pub mod default_element_ordering; pub mod raw_def; +pub mod view; diff --git a/crates/lib/src/db/raw_def/v9.rs b/crates/lib/src/db/raw_def/v9.rs index 7d5a03905fa..ba5361c6cdb 100644 --- a/crates/lib/src/db/raw_def/v9.rs +++ b/crates/lib/src/db/raw_def/v9.rs @@ -26,6 +26,7 @@ use crate::db::raw_def::v10::RawConstraintDefV10; use crate::db::raw_def::v10::RawScopedTypeNameV10; use crate::db::raw_def::v10::RawSequenceDefV10; use crate::db::raw_def::v10::RawTypeDefV10; +use crate::db::view::extract_view_return_product_type_ref; /// A not-yet-validated `sql`. pub type RawSql = Box; @@ -124,16 +125,7 @@ impl RawModuleDefV9 { fn type_ref_for_view(&self, view_name: &str) -> Option { self.find_view_def(view_name) .map(|view_def| &view_def.return_type) - .and_then(|return_type| { - return_type - .as_option() - .and_then(|inner| inner.clone().into_ref().ok()) - .or_else(|| { - return_type - .as_array() - .and_then(|inner| inner.elem_ty.clone().into_ref().ok()) - }) - }) + .and_then(|return_type| extract_view_return_product_type_ref(return_type).map(|(ref_, _)| ref_)) } /// Find and return the product type ref for a table or view in this module def diff --git a/crates/lib/src/db/view.rs b/crates/lib/src/db/view.rs new file mode 100644 index 00000000000..b3a79a1c781 --- /dev/null +++ b/crates/lib/src/db/view.rs @@ -0,0 +1,32 @@ +use spacetimedb_sats::{AlgebraicType, AlgebraicTypeRef}; + +pub const QUERY_VIEW_RETURN_TAG: &str = "__query__"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ViewKind { + Procedural, + Query, +} + +pub fn extract_view_return_product_type_ref(return_type: &AlgebraicType) -> Option<(AlgebraicTypeRef, ViewKind)> { + // Query-builder views (`Query`) are encoded as: { __query__: T }. + if let Some(product) = return_type.as_product() + && product.elements.len() == 1 + && product.elements[0].name.as_deref() == Some(QUERY_VIEW_RETURN_TAG) + && let Some(product_type_ref) = product.elements[0].algebraic_type.as_ref().copied() + { + return Some((product_type_ref, ViewKind::Query)); + } + + return_type + .as_option() + .and_then(AlgebraicType::as_ref) + .or_else(|| { + return_type + .as_array() + .map(|array_type| array_type.elem_ty.as_ref()) + .and_then(AlgebraicType::as_ref) + }) + .copied() + .map(|product_type_ref| (product_type_ref, ViewKind::Procedural)) +} diff --git a/crates/physical-plan/src/plan.rs b/crates/physical-plan/src/plan.rs index e80512d32ab..10c81656b92 100644 --- a/crates/physical-plan/src/plan.rs +++ b/crates/physical-plan/src/plan.rs @@ -575,6 +575,7 @@ impl PhysicalPlan { rhs, rhs_label, rhs_index, + rhs_prefix, rhs_field, unique, lhs_field, @@ -587,6 +588,7 @@ impl PhysicalPlan { rhs, rhs_label, rhs_index, + rhs_prefix, rhs_field, unique, lhs_field, @@ -1252,6 +1254,8 @@ pub struct IxJoin { pub rhs_label: Label, /// The index id pub rhs_index: IndexId, + /// Optional constant prefix values for multi-column index probes. + pub rhs_prefix: Vec, /// The index field pub rhs_field: ColId, /// Is the index a unique constraint index? @@ -1451,7 +1455,7 @@ mod tests { identity::AuthCtx, AlgebraicType, AlgebraicValue, }; - use spacetimedb_primitives::{ColId, ColList, ColSet, TableId}; + use spacetimedb_primitives::{ColId, ColList, ColSet, IndexId, TableId}; use spacetimedb_schema::{ def::{BTreeAlgorithm, ConstraintData, IndexAlgorithm, UniqueConstraintData}, identifier::Identifier, @@ -1462,7 +1466,8 @@ mod tests { use crate::{ compile::{compile_select, compile_select_list}, - plan::{HashJoin, IxJoin, IxScan, PhysicalPlan, ProjectListPlan, Sarg, Semi, TupleField}, + plan::{HashJoin, IxJoin, IxScan, Label, PhysicalPlan, ProjectListPlan, Sarg, Semi, TupleField}, + rules::HashToIxJoin, }; use super::{PhysicalExpr, ProjectPlan, TableScan}; @@ -1498,7 +1503,7 @@ mod tests { ) -> TableOrViewSchema { TableOrViewSchema::from(Arc::new(TableSchema::new( table_id, - TableName::for_test(table_name), + TableName::new(Identifier::for_test(table_name)), None, columns .iter() @@ -2310,4 +2315,158 @@ mod tests { assert!(plan.plan_iter().any(|plan| plan.has_filter())); assert!(plan.plan_iter().any(|plan| plan.has_table_scan(None))); } + + #[test] + fn hash_join_to_index_join_with_constant_prefix() { + let a_id = TableId(1); + let b_id = TableId(2); + + let a = Arc::new(schema(a_id, "a", &[("id", AlgebraicType::U64)], &[], &[], None)); + + // Multi-column index on (owner, id). + let b = Arc::new(schema( + b_id, + "b", + &[("owner", AlgebraicType::U64), ("id", AlgebraicType::U64)], + &[&[0, 1]], + &[], + None, + )); + + let plan = PhysicalPlan::Filter( + Box::new(PhysicalPlan::HashJoin( + HashJoin { + lhs: Box::new(PhysicalPlan::TableScan( + TableScan { + schema: a.inner(), + limit: None, + delta: None, + }, + Label(1), + )), + rhs: Box::new(PhysicalPlan::TableScan( + TableScan { + schema: b.inner(), + limit: None, + delta: None, + }, + Label(2), + )), + lhs_field: TupleField { + label: Label(1), + label_pos: None, + field_pos: 0, + }, + rhs_field: TupleField { + label: Label(2), + label_pos: None, + field_pos: 1, + }, + unique: false, + }, + Semi::All, + )), + PhysicalExpr::BinOp( + BinOp::Eq, + Box::new(PhysicalExpr::Field(TupleField { + label: Label(2), + label_pos: None, + field_pos: 0, + })), + Box::new(PhysicalExpr::Value(AlgebraicValue::U64(5))), + ), + ); + + let plan = plan.apply_rec::().unwrap(); + + match plan { + PhysicalPlan::IxJoin( + IxJoin { + rhs, + rhs_field, + rhs_prefix, + .. + }, + Semi::All, + ) => { + assert_eq!(rhs.table_id, b_id); + assert_eq!(rhs_field, ColId(1)); + assert_eq!(rhs_prefix, vec![AlgebraicValue::U64(5)]); + } + plan => panic!("unexpected plan: {plan:#?}"), + } + } + + #[test] + fn hash_join_to_index_join_with_rhs_ixscan_prefix() { + let a_id = TableId(1); + let b_id = TableId(2); + + let a = Arc::new(schema(a_id, "a", &[("id", AlgebraicType::U64)], &[], &[], None)); + + // Multi-column index on (owner, id), plus a single-column index on owner. + let b = Arc::new(schema( + b_id, + "b", + &[("owner", AlgebraicType::U64), ("id", AlgebraicType::U64)], + &[&[0], &[0, 1]], + &[], + None, + )); + + let plan = PhysicalPlan::HashJoin( + HashJoin { + lhs: Box::new(PhysicalPlan::TableScan( + TableScan { + schema: a.inner(), + limit: None, + delta: None, + }, + Label(1), + )), + rhs: Box::new(PhysicalPlan::IxScan( + IxScan { + schema: b.inner(), + limit: None, + delta: None, + index_id: IndexId(0), + prefix: vec![], + arg: Sarg::Eq(ColId(0), AlgebraicValue::U64(7)), + }, + Label(2), + )), + lhs_field: TupleField { + label: Label(1), + label_pos: None, + field_pos: 0, + }, + rhs_field: TupleField { + label: Label(2), + label_pos: None, + field_pos: 1, + }, + unique: false, + }, + Semi::All, + ); + + let plan = plan.apply_rec::().unwrap(); + + match plan { + PhysicalPlan::IxJoin( + IxJoin { + rhs, + rhs_field, + rhs_prefix, + .. + }, + Semi::All, + ) => { + assert_eq!(rhs.table_id, b_id); + assert_eq!(rhs_field, ColId(1)); + assert_eq!(rhs_prefix, vec![AlgebraicValue::U64(7)]); + } + plan => panic!("unexpected plan: {plan:#?}"), + } + } } diff --git a/crates/physical-plan/src/rules.rs b/crates/physical-plan/src/rules.rs index 2fd9f58a92e..e02520b7fdf 100644 --- a/crates/physical-plan/src/rules.rs +++ b/crates/physical-plan/src/rules.rs @@ -27,8 +27,9 @@ //! * [UniqueHashJoinRule] //! Mark hash join as unique use anyhow::{bail, Result}; +use spacetimedb_lib::AlgebraicValue; use spacetimedb_primitives::{ColId, ColSet, IndexId}; -use spacetimedb_schema::schema::IndexSchema; +use spacetimedb_schema::schema::{IndexSchema, TableSchema}; use spacetimedb_sql_parser::ast::{BinOp, LogOp}; use crate::plan::{ @@ -1061,21 +1062,163 @@ impl RewriteRule for PullFilterAboveHashJoin { /// Always prefer an index join to a hash join pub(crate) struct HashToIxJoin; +/// A filter term of the form `rhs_col = constant`. +type EqConstFilterTerm = (ColId, AlgebraicValue); + +/// Planning metadata derived while proving `HashJoin -> IxJoin` is valid. +#[derive(Clone)] +pub(crate) struct HashToIxJoinInfo { + /// The index to probe on the RHS table. + rhs_index: IndexId, + /// The join column on the RHS. + /// This must be the final probe component for the chosen index. + rhs_field: ColId, + /// Constant leading key components for a multi-column index probe. + /// For an index `(a, b, c)` and join on `c`, this stores `[a_const, b_const]`. + rhs_prefix: Vec, + /// RHS filter terms consumed into `rhs_prefix` and therefore removable + /// from the residual filter. + consumed_filter_terms: Vec, +} + +/// Collect RHS predicates of the form `rhs.col = constant`. +/// +/// This is intentionally narrow: only equality predicates over RHS fields are +/// useful for building exact prefix probes into multi-column indexes. +fn rhs_eq_constants(expr: &PhysicalExpr, rhs_label: Label) -> Vec { + match expr { + PhysicalExpr::BinOp(BinOp::Eq, lhs, rhs) + if matches!( + (&**lhs, &**rhs), + (PhysicalExpr::Field(TupleField { label, .. }), PhysicalExpr::Value(_)) if *label == rhs_label + ) => + { + match (&**lhs, &**rhs) { + (PhysicalExpr::Field(TupleField { field_pos, .. }), PhysicalExpr::Value(value)) => { + vec![(ColId(*field_pos as u16), value.clone())] + } + _ => vec![], + } + } + PhysicalExpr::LogOp(LogOp::And, exprs) => exprs + .iter() + .flat_map(|expr| rhs_eq_constants(expr, rhs_label)) + .collect(), + _ => vec![], + } +} + +fn ix_join_candidate( + schema: &TableSchema, + rhs_field_pos: usize, + constants: &[EqConstFilterTerm], +) -> Option { + let rhs_field = ColId(rhs_field_pos as u16); + schema.indexes.iter().find_map(|ix| { + let cols = ix.index_algorithm.columns(); + // For point-probe index joins we require: + // 1) join key is the last index column, + // 2) every leading index column is fixed by an RHS equality constant. + if cols.iter().last()? != rhs_field { + return None; + }; + + let mut rhs_prefix: Vec = vec![]; + let mut consumed_filter_terms: Vec = vec![]; + for col in cols.iter().take(cols.len().saturating_sub(1).into()) { + let (_, value) = constants.iter().find(|(candidate, _)| *candidate == col)?; + rhs_prefix.push(value.clone()); + consumed_filter_terms.push((col, value.clone())); + } + + Some(HashToIxJoinInfo { + rhs_index: ix.index_id, + rhs_field, + rhs_prefix, + consumed_filter_terms, + }) + }) +} + +/// Extract equality constants encoded directly in an `IxScan`. +/// +/// We only support `Eq` SARGs here because index joins require exact probe +/// values for all leading index columns. +fn rhs_eq_constants_from_ixscan(scan: &IxScan) -> Option> { + let mut constants = scan + .prefix + .iter() + .map(|(col, value)| (*col, value.clone())) + .collect::>(); + + let Sarg::Eq(col, value) = &scan.arg else { + return None; + }; + constants.push((*col, value.clone())); + Some(constants) +} + +/// Ensure we do not drop predicates that were previously represented by an +/// `IxScan` when rewriting to an `IxJoin`. +fn all_terms_consumed(required: &[EqConstFilterTerm], consumed: &[EqConstFilterTerm]) -> bool { + required + .iter() + .all(|term| consumed.iter().any(|consumed_term| consumed_term == term)) +} + +fn remove_consumed_filter_terms( + expr: PhysicalExpr, + rhs_label: Label, + consumed_filter_terms: &[EqConstFilterTerm], +) -> Option { + // These terms are now represented by `rhs_prefix`; keeping them in a + // residual filter is redundant work. + let is_consumed = |col_id: ColId, value: &AlgebraicValue| { + consumed_filter_terms + .iter() + .any(|(consumed_col, consumed_val)| consumed_col == &col_id && consumed_val == value) + }; + + match expr { + PhysicalExpr::BinOp(BinOp::Eq, lhs, rhs) + if matches!( + (&*lhs, &*rhs), + (PhysicalExpr::Field(TupleField { label, field_pos, .. }), PhysicalExpr::Value(value)) + if *label == rhs_label && is_consumed(ColId(*field_pos as u16), value) + ) => + { + None + } + PhysicalExpr::LogOp(LogOp::And, exprs) => { + let mut kept: Vec<_> = exprs + .into_iter() + .filter_map(|expr| remove_consumed_filter_terms(expr, rhs_label, consumed_filter_terms)) + .collect(); + + match kept.len() { + 0 => None, + 1 => Some(kept.swap_remove(0)), + _ => Some(PhysicalExpr::LogOp(LogOp::And, kept)), + } + } + expr => Some(expr), + } +} + impl RewriteRule for HashToIxJoin { type Plan = PhysicalPlan; - type Info = (IndexId, ColId); + type Info = HashToIxJoinInfo; fn matches(plan: &PhysicalPlan) -> Option { - if let PhysicalPlan::HashJoin( - HashJoin { - rhs, - rhs_field: TupleField { field_pos, .. }, - .. - }, - _, - ) = plan - { - return match &**rhs { + match plan { + PhysicalPlan::HashJoin( + HashJoin { + rhs, + rhs_field: TupleField { field_pos, .. }, + .. + }, + _, + ) => match &**rhs { PhysicalPlan::TableScan( TableScan { schema, @@ -1083,48 +1226,152 @@ impl RewriteRule for HashToIxJoin { delta: _, }, _, + ) => ix_join_candidate(schema, *field_pos, &[]), + PhysicalPlan::IxScan( + scan @ IxScan { + schema, + limit: None, + delta: _, + .. + }, + _, ) => { - // Is there a single column index on this field? - schema.indexes.iter().find_map(|ix| { - ix.index_algorithm - .columns() - .as_singleton() - .filter(|col_id| col_id.idx() == *field_pos) - .map(|col_id| (ix.index_id, col_id)) - }) + // New behavior: if earlier rules already lowered an RHS filter to + // `IxScan(sender = const)`, reuse those constants when proving that + // a multi-column join index can be probed. + let constants = rhs_eq_constants_from_ixscan(scan)?; + let info = ix_join_candidate(schema, *field_pos, &constants)?; + // Guardrail: do not rewrite unless every scan-encoded predicate is + // represented in the chosen `rhs_prefix` + join key. + all_terms_consumed(&constants, &info.consumed_filter_terms).then_some(info) } _ => None, - }; + }, + PhysicalPlan::Filter(input, expr) => { + let PhysicalPlan::HashJoin( + HashJoin { + rhs, + rhs_field: TupleField { field_pos, .. }, + .. + }, + _, + ) = &**input + else { + return None; + }; + + let PhysicalPlan::TableScan( + TableScan { + schema, + limit: None, + delta: _, + }, + rhs_label, + ) = &**rhs + else { + return None; + }; + + let constants = rhs_eq_constants(expr, *rhs_label); + ix_join_candidate(schema, *field_pos, &constants) + } + _ => None, } - None } - fn rewrite(plan: PhysicalPlan, (rhs_index, rhs_field): Self::Info) -> Result { - if let PhysicalPlan::HashJoin(join, semi) = plan - && let PhysicalPlan::TableScan( - TableScan { - schema: rhs, - limit: None, - delta: rhs_delta, - }, - rhs_label, - ) = *join.rhs - { - return Ok(PhysicalPlan::IxJoin( - IxJoin { - lhs: join.lhs, - rhs, - rhs_label, + fn rewrite(plan: PhysicalPlan, info: Self::Info) -> Result { + match plan { + PhysicalPlan::HashJoin(join, semi) => { + let HashToIxJoinInfo { rhs_index, rhs_field, - rhs_delta, - unique: false, - lhs_field: join.lhs_field, - }, - semi, - )); + rhs_prefix, + .. + } = info; + + let (rhs, rhs_label, rhs_delta) = match *join.rhs { + PhysicalPlan::TableScan( + TableScan { + schema: rhs, + limit: None, + delta: rhs_delta, + }, + rhs_label, + ) => (rhs, rhs_label, rhs_delta), + PhysicalPlan::IxScan( + IxScan { + schema: rhs, + limit: None, + delta: rhs_delta, + .. + }, + rhs_label, + ) => (rhs, rhs_label, rhs_delta), + _ => bail!("{INVARIANT_VIOLATION}: Failed to rewrite hash join as index join"), + }; + + Ok(PhysicalPlan::IxJoin( + IxJoin { + lhs: join.lhs, + rhs, + rhs_label, + rhs_index, + rhs_prefix, + rhs_field, + rhs_delta, + unique: false, + lhs_field: join.lhs_field, + }, + semi, + )) + } + PhysicalPlan::Filter(input, expr) => { + let HashToIxJoinInfo { + rhs_index, + rhs_field, + rhs_prefix, + consumed_filter_terms, + } = info; + + let PhysicalPlan::HashJoin(join, semi) = *input else { + bail!("{INVARIANT_VIOLATION}: Failed to rewrite filtered hash join as index join"); + }; + + let PhysicalPlan::TableScan( + TableScan { + schema: rhs, + limit: None, + delta: rhs_delta, + }, + rhs_label, + ) = *join.rhs + else { + bail!("{INVARIANT_VIOLATION}: Failed to rewrite filtered hash join as index join"); + }; + + let ix_join = PhysicalPlan::IxJoin( + IxJoin { + lhs: join.lhs, + rhs, + rhs_label, + rhs_index, + rhs_prefix, + rhs_field, + rhs_delta, + unique: false, + lhs_field: join.lhs_field, + }, + semi, + ); + + if let Some(expr) = remove_consumed_filter_terms(expr, rhs_label, &consumed_filter_terms) { + Ok(PhysicalPlan::Filter(Box::new(ix_join), expr)) + } else { + Ok(ix_join) + } + } + _ => bail!("{INVARIANT_VIOLATION}: Failed to rewrite hash join as index join"), } - bail!("{INVARIANT_VIOLATION}: Failed to rewrite hash join as index join") } } diff --git a/crates/query-builder/src/lib.rs b/crates/query-builder/src/lib.rs index b8dd794d20b..eb34d58b6bc 100644 --- a/crates/query-builder/src/lib.rs +++ b/crates/query-builder/src/lib.rs @@ -7,6 +7,8 @@ pub use join::*; use spacetimedb_lib::{sats::impl_st, AlgebraicType, SpacetimeType}; pub use table::*; +const QUERY_VIEW_RETURN_TAG: &str = "__query__"; + /// Trait implemented by all query builder types. Use `impl Query` as a /// return type for view functions and helpers. pub trait Query { @@ -38,7 +40,10 @@ impl Query for RawQuery { } } -impl_st!([T: SpacetimeType] RawQuery, ts => AlgebraicType::option(T::make_type(ts))); +impl_st!( + [T: SpacetimeType] RawQuery, + ts => AlgebraicType::product([(QUERY_VIEW_RETURN_TAG, T::make_type(ts))]) +); #[cfg(test)] mod tests { diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index 2162a9c9abb..89c201e3f85 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -775,6 +775,7 @@ impl From for TableDef { name, is_public, product_type_ref, + primary_key, return_columns, accessor_name, .. @@ -782,7 +783,7 @@ impl From for TableDef { Self { name, product_type_ref, - primary_key: None, + primary_key, columns: return_columns.into_iter().map(ColumnDef::from).collect(), indexes: <_>::default(), constraints: <_>::default(), @@ -1507,12 +1508,13 @@ pub struct ViewDef { pub params_for_generate: ProductTypeDef, /// The return type of the view. - /// Either `Option` or `Vec` where: + /// Either `Option`, `Vec`, or `Query` where: /// - /// 1. `T` is a [`ProductType`] containing the columns of the view, - /// 2. `T` is registered in the module's typespace, - /// 3. `Option` refers to [`AlgebraicType::option()`], and + /// 1. `T` is a [`ProductType`] containing the columns of the view + /// 2. `T` is registered in the module's typespace + /// 3. `Option` refers to [`AlgebraicType::option()`] /// 4. `Vec` refers to [`AlgebraicType::array()`] + /// 5. `Query` is a special [`ProductType`] `{ __query__: T }` pub return_type: AlgebraicType, /// The return type of the view, formatted for client codegen. @@ -1520,11 +1522,17 @@ pub struct ViewDef { /// The single source of truth for the view's columns. /// - /// If a view can return only `Option` or `Vec`, + /// If a view can return only `Option`, `Vec`, or `Query`, /// this is a reference to the inner product type `T`. /// All elements of `T` must have names. pub product_type_ref: AlgebraicTypeRef, + /// The primary key of the view. + /// + /// This is set for query-builder views when the underlying table has a primary key. + /// The database engine does not actually care about this, but client code generation does. + pub primary_key: Option, + /// The return columns of this view. /// The same information is stored in `product_type_ref`. /// This is just a more convenient-to-access format. diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index a8e8a13cb10..7ebbaae06d4 100644 --- a/crates/schema/src/def/validate/v10.rs +++ b/crates/schema/src/def/validate/v10.rs @@ -1,6 +1,7 @@ use spacetimedb_data_structures::map::HashMap; use spacetimedb_lib::bsatn::Deserializer; use spacetimedb_lib::db::raw_def::v10::*; +use spacetimedb_lib::db::view::{extract_view_return_product_type_ref, ViewKind}; use spacetimedb_lib::de::DeserializeSeed as _; use spacetimedb_sats::{Typespace, WithTypespace}; @@ -233,7 +234,7 @@ pub fn validate(def: RawModuleDefV10) -> Result { .combine_errors() .and_then( |(mut tables, types, reducers, procedures, views, schedules, lifecycles)| { - let (mut reducers, mut procedures, views) = + let (mut reducers, mut procedures, mut views) = check_function_names_are_unique(reducers, procedures, views)?; // Attach lifecycles to their respective reducers attach_lifecycles_to_reducers(&mut reducers, lifecycles)?; @@ -243,6 +244,7 @@ pub fn validate(def: RawModuleDefV10) -> Result { check_scheduled_functions_exist(&mut tables, &reducers, &procedures)?; change_scheduled_functions_and_lifetimes_visibility(&tables, &mut reducers, &mut procedures)?; + assign_query_view_primary_keys(&tables, &mut views); Ok((tables, types, reducers, procedures, views)) }, @@ -684,17 +686,8 @@ impl<'a> ModuleValidatorV10<'a> { }) }; - let product_type_ref = return_type - .as_option() - .and_then(AlgebraicType::as_ref) - .or_else(|| { - return_type - .as_array() - .map(|array_type| array_type.elem_ty.as_ref()) - .and_then(AlgebraicType::as_ref) - }) - .cloned() - .ok_or_else(invalid_return_type)?; + let (product_type_ref, return_kind) = + extract_view_return_product_type_ref(&return_type).ok_or_else(invalid_return_type)?; let product_type = self .core @@ -716,11 +709,18 @@ impl<'a> ModuleValidatorV10<'a> { arg_name, })?; + let return_type_for_generate_input = match return_kind { + ViewKind::Procedural => return_type.clone(), + // Query-builder views still return rows from the client's perspective. + // For codegen purposes we model this as `Vec`. + ViewKind::Query => AlgebraicType::array(product_type_ref.into()), + }; + let return_type_for_generate = self.core.validate_for_type_use( || TypeLocation::ViewReturn { view_name: accessor_name.clone(), }, - &return_type, + &return_type_for_generate_input, ); let name = self.core.resolve_function_ident(accessor_name.clone())?; @@ -774,6 +774,7 @@ impl<'a> ModuleValidatorV10<'a> { return_type, return_type_for_generate, product_type_ref, + primary_key: None, return_columns, param_columns, }) @@ -829,6 +830,37 @@ fn attach_schedules_to_tables( Ok(()) } +fn assign_query_view_primary_keys(tables: &IdentifierMap, views: &mut IndexMap) { + let primary_key_for_product_type_ref = |product_type_ref: AlgebraicTypeRef| { + let mut primary_key = None; + for table in tables + .values() + .filter(|table| table.product_type_ref == product_type_ref) + { + let Some(table_primary_key) = table.primary_key else { + continue; + }; + match primary_key { + Some(existing) if existing != table_primary_key => { + // Ambiguous source table: keep the view without a primary key. + return None; + } + None => primary_key = Some(table_primary_key), + Some(_) => {} + } + } + primary_key + }; + + for view in views.values_mut() { + view.primary_key = match extract_view_return_product_type_ref(&view.return_type) { + Some((_, ViewKind::Procedural)) => None, + Some((product_type_ref, ViewKind::Query)) => primary_key_for_product_type_ref(product_type_ref), + None => None, + }; + } +} + #[cfg(test)] mod tests { use crate::def::validate::tests::{ diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index 37a40469dcc..d040435afe5 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -10,6 +10,7 @@ use spacetimedb_data_structures::map::{HashMap, HashSet}; use spacetimedb_lib::db::default_element_ordering::{product_type_has_default_ordering, sum_type_has_default_ordering}; use spacetimedb_lib::db::raw_def::v10::{reducer_default_err_return_type, reducer_default_ok_return_type}; use spacetimedb_lib::db::raw_def::v9::RawViewDefV9; +use spacetimedb_lib::db::view::{extract_view_return_product_type_ref, ViewKind}; use spacetimedb_lib::ProductType; use spacetimedb_primitives::col_list; use spacetimedb_sats::{bsatn::de::Deserializer, de::DeserializeSeed, WithTypespace}; @@ -438,21 +439,12 @@ impl ModuleValidatorV9<'_> { }) }; - // The possible return types of a view are `Vec` or `Option`, + // The possible return types of a view are `Vec`, `Option`, or `Query`, // where `T` is a `ProductType` in the `Typespace`. // Here we extract the inner product type ref `T`. // We exit early for errors since this breaks all the other checks. - let product_type_ref = return_type - .as_option() - .and_then(AlgebraicType::as_ref) - .or_else(|| { - return_type - .as_array() - .map(|array_type| array_type.elem_ty.as_ref()) - .and_then(AlgebraicType::as_ref) - }) - .cloned() - .ok_or_else(invalid_return_type)?; + let (product_type_ref, return_kind) = + extract_view_return_product_type_ref(&return_type).ok_or_else(invalid_return_type)?; let product_type = self .core @@ -474,11 +466,18 @@ impl ModuleValidatorV9<'_> { arg_name, })?; + let return_type_for_generate_input = match return_kind { + ViewKind::Procedural => return_type.clone(), + // Query-builder views still return rows from the client's perspective. + // For codegen purposes we model this as `Vec`. + ViewKind::Query => AlgebraicType::array(product_type_ref.into()), + }; + let return_type_for_generate = self.core.validate_for_type_use( || TypeLocation::ViewReturn { view_name: name.clone(), }, - &return_type, + &return_type_for_generate_input, ); let mut view_in_progress = ViewValidator::new( @@ -525,6 +524,7 @@ impl ModuleValidatorV9<'_> { return_type, return_type_for_generate, product_type_ref, + primary_key: None, return_columns, param_columns, accessor_name: name, diff --git a/crates/schema/src/schema.rs b/crates/schema/src/schema.rs index 3a64a66aff7..e62b9dc6963 100644 --- a/crates/schema/src/schema.rs +++ b/crates/schema/src/schema.rs @@ -22,8 +22,8 @@ use std::collections::BTreeMap; use std::sync::Arc; use crate::def::{ - ColumnDef, ConstraintData, ConstraintDef, IndexAlgorithm, IndexDef, ModuleDef, ModuleDefLookup, ScheduleDef, - SequenceDef, TableDef, UniqueConstraintData, ViewColumnDef, ViewDef, + ColumnDef, ConstraintData, ConstraintDef, IndexAlgorithm, IndexDef, ModuleDef, ModuleDefLookup, + RawModuleDefVersion, ScheduleDef, SequenceDef, TableDef, UniqueConstraintData, ViewColumnDef, ViewDef, }; use crate::identifier::Identifier; @@ -760,6 +760,7 @@ impl TableSchema { name, is_public, is_anonymous, + primary_key, param_columns, return_columns, .. @@ -772,6 +773,9 @@ impl TableSchema { .map(|(i, schema)| (ColId::from(i), schema)) .map(|(col_pos, schema)| ColumnSchema { col_pos, ..schema }) .collect(); + let view_primary_key = (module_def.raw_module_def_version() == RawModuleDefVersion::V10) + .then_some(*primary_key) + .flatten(); let table_access = if *is_public { StAccess::Public @@ -796,7 +800,7 @@ impl TableSchema { StTableType::User, table_access, None, - None, + view_primary_key, false, None, ) @@ -842,6 +846,7 @@ impl TableSchema { name, is_public, is_anonymous, + primary_key, param_columns, return_columns, accessor_name, @@ -851,12 +856,9 @@ impl TableSchema { let n = return_columns.len() + 2; let mut columns = Vec::with_capacity(n); let mut meta_cols = 0; - let mut index_name = name.as_raw().clone().into_inner(); let mut push_column = |name: &'static str, col_type| { meta_cols += 1; - index_name += "_"; - index_name += name; columns.push(ColumnSchema { table_id: TableId::SENTINEL, col_pos: columns.len().into(), @@ -883,23 +885,69 @@ impl TableSchema { .map(|(col_pos, schema)| ColumnSchema { col_pos, ..schema }), ); - let index_schema = |col_list: ColList| { - index_name += "idx_btree"; - IndexSchema { + let make_index_name = |col_list: &ColList| { + let cols_name = generate_cols_name(col_list, |col| columns.get(col.idx()).map(|col| &*col.col_name)); + RawIdentifier::new(format!("{name}_{cols_name}_idx_btree")) + }; + + let make_constraint_name = |col_list: &ColList| { + let cols_name = generate_cols_name(col_list, |col| columns.get(col.idx()).map(|col| &*col.col_name)); + RawIdentifier::new(format!("{name}_{cols_name}_key")) + }; + + let mut indexes = match meta_cols { + 1 => vec![IndexSchema { index_id: IndexId::SENTINEL, table_id: TableId::SENTINEL, - index_name: RawIdentifier::new(index_name), - index_algorithm: IndexAlgorithm::BTree(col_list.into()), + index_name: make_index_name(&col_list![0]), + index_algorithm: IndexAlgorithm::BTree(col_list![0].into()), alias: None, - } - }; - - let indexes = match meta_cols { - 1 => vec![index_schema(col_list![0])], - 2 => vec![index_schema(col_list![0, 1])], + }], + 2 => vec![IndexSchema { + index_id: IndexId::SENTINEL, + table_id: TableId::SENTINEL, + index_name: make_index_name(&col_list![0, 1]), + index_algorithm: IndexAlgorithm::BTree(col_list![0, 1].into()), + alias: None, + }], _ => vec![], }; + let mut constraints = vec![]; + let view_primary_key = (module_def.raw_module_def_version() == RawModuleDefVersion::V10) + .then_some(primary_key.map(|pk| ColId::from(meta_cols + pk.idx()))) + .flatten(); + + if *is_anonymous { + if let Some(pk_col) = view_primary_key { + let cols = col_list![pk_col]; + constraints.push(ConstraintSchema { + table_id: TableId::SENTINEL, + constraint_id: ConstraintId::SENTINEL, + constraint_name: make_constraint_name(&cols), + data: ConstraintData::Unique(UniqueConstraintData { + columns: ColSet::from(cols.clone()), + }), + }); + indexes.push(IndexSchema { + index_id: IndexId::SENTINEL, + table_id: TableId::SENTINEL, + index_name: make_index_name(&cols), + index_algorithm: IndexAlgorithm::BTree(cols.into()), + alias: None, + }); + } + } else if let Some(pk_col) = view_primary_key { + let cols = col_list![ColId(0), pk_col]; + indexes.push(IndexSchema { + index_id: IndexId::SENTINEL, + table_id: TableId::SENTINEL, + index_name: make_index_name(&cols), + index_algorithm: IndexAlgorithm::BTree(cols.into()), + alias: None, + }); + } + let table_access = if *is_public { StAccess::Public } else { @@ -918,12 +966,12 @@ impl TableSchema { Some(view_info), columns, indexes, - vec![], + constraints, vec![], StTableType::User, table_access, None, - None, + if *is_anonymous { view_primary_key } else { None }, false, Some(accessor_name.clone()), ) diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/all_view_pk_players_table.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/all_view_pk_players_table.rs index 8b96750dae6..4a64417bb6a 100644 --- a/sdks/rust/tests/view-pk-client/src/module_bindings/all_view_pk_players_table.rs +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/all_view_pk_players_table.rs @@ -78,9 +78,57 @@ impl<'ctx> __sdk::Table for AllViewPkPlayersTableHandle<'ctx> { } } +pub struct AllViewPkPlayersUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for AllViewPkPlayersTableHandle<'ctx> { + type UpdateCallbackId = AllViewPkPlayersUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> AllViewPkPlayersUpdateCallbackId { + AllViewPkPlayersUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: AllViewPkPlayersUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `id` unique index on the table `all_view_pk_players`, +/// which allows point queries on the field of the same name +/// via the [`AllViewPkPlayersIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.all_view_pk_players().id().find(...)`. +pub struct AllViewPkPlayersIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> AllViewPkPlayersTableHandle<'ctx> { + /// Get a handle on the `id` unique index on the table `all_view_pk_players`. + pub fn id(&self) -> AllViewPkPlayersIdUnique<'ctx> { + AllViewPkPlayersIdUnique { + imp: self.imp.get_unique_constraint::("id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> AllViewPkPlayersIdUnique<'ctx> { + /// Find the subscribed row whose `id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &u64) -> Option { + self.imp.find(col_val) + } +} + #[doc(hidden)] pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { let _table = client_cache.get_or_make_table::("all_view_pk_players"); + _table.add_unique_constraint::("id", |row| &row.id); } #[doc(hidden)] diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/mod.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/mod.rs index 9db3ef815bc..3057c6eda03 100644 --- a/sdks/rust/tests/view-pk-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/mod.rs @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.0.3 (commit cea9b6a2ddb9a3fbf5a9d3307fbae8efe29e26dc). +// This was generated using spacetimedb cli version 2.0.3 (commit 995798d29d314301cb475e2cd499f32a1691ea90). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; From 2088fbc35f3ed22c96ad915526301359a3744e75 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 5 Mar 2026 19:58:12 -0800 Subject: [PATCH 4/4] update tests --- crates/bindings/tests/ui/views.stderr | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bindings/tests/ui/views.stderr b/crates/bindings/tests/ui/views.stderr index 8a91ab089e7..d7b7c1f8a91 100644 --- a/crates/bindings/tests/ui/views.stderr +++ b/crates/bindings/tests/ui/views.stderr @@ -77,27 +77,27 @@ error[E0425]: cannot find type `T` in this scope 202 | fn view_nonexistent_table(ctx: &ViewContext) -> impl Query { | ^ not found in this scope -error[E0277]: the trait bound `ViewKind: ViewKindTrait` is not satisfied +error[E0277]: the trait bound `spacetimedb::rt::ViewKind: ViewKindTrait` is not satisfied --> tests/ui/views.rs:106:1 | 106 | #[view(accessor = view_def_wrong_context, public)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `ViewKindTrait` is not implemented for `ViewKind` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `ViewKindTrait` is not implemented for `spacetimedb::rt::ViewKind` | help: the following other types implement trait `ViewKindTrait` --> src/rt.rs | | impl ViewKindTrait for ViewKind { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `ViewKind` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `spacetimedb::rt::ViewKind` ... | impl ViewKindTrait for ViewKind { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `ViewKind` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `spacetimedb::rt::ViewKind` = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0276]: impl has stricter requirements than trait --> tests/ui/views.rs:106:1 | 106 | #[view(accessor = view_def_wrong_context, public)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ impl has extra requirement `ViewKind: ViewKindTrait` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ impl has extra requirement `spacetimedb::rt::ViewKind: ViewKindTrait` | = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info)