From cfb7bae0fcb68ca43023e7e7290989ca5f098fbb Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Tue, 3 Mar 2026 16:05:58 -0800 Subject: [PATCH 1/9] tests for query builder views with primary keys --- Cargo.lock | 17 + Cargo.toml | 2 + crates/smoketests/tests/smoketests/views.rs | 111 +-- modules/sdk-test-view-pk-cs/.gitignore | 2 + modules/sdk-test-view-pk-cs/Lib.cs | 52 ++ .../sdk-test-view-pk-cs.csproj | 8 + modules/sdk-test-view-pk-ts/.gitignore | 1 + modules/sdk-test-view-pk-ts/package.json | 17 + modules/sdk-test-view-pk-ts/src/index.ts | 51 ++ modules/sdk-test-view-pk-ts/tsconfig.json | 14 + modules/sdk-test-view-pk/Cargo.toml | 14 + modules/sdk-test-view-pk/src/lib.rs | 36 + sdks/rust/tests/test.rs | 40 + sdks/rust/tests/view-pk-client/Cargo.toml | 17 + sdks/rust/tests/view-pk-client/src/main.rs | 169 ++++ .../all_view_pk_players_table.rs | 111 +++ .../all_view_pk_players_type.rs | 13 + .../insert_view_pk_membership_reducer.rs | 72 ++ .../insert_view_pk_membership_type.rs | 16 + .../insert_view_pk_player_reducer.rs | 72 ++ .../insert_view_pk_player_type.rs | 16 + .../view-pk-client/src/module_bindings/mod.rs | 850 ++++++++++++++++++ .../update_view_pk_player_reducer.rs | 72 ++ .../update_view_pk_player_type.rs | 16 + .../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 ++ 28 files changed, 2158 insertions(+), 55 deletions(-) create mode 100644 modules/sdk-test-view-pk-cs/.gitignore create mode 100644 modules/sdk-test-view-pk-cs/Lib.cs create mode 100644 modules/sdk-test-view-pk-cs/sdk-test-view-pk-cs.csproj create mode 100644 modules/sdk-test-view-pk-ts/.gitignore create mode 100644 modules/sdk-test-view-pk-ts/package.json create mode 100644 modules/sdk-test-view-pk-ts/src/index.ts create mode 100644 modules/sdk-test-view-pk-ts/tsconfig.json 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 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/all_view_pk_players_type.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_type.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/insert_view_pk_player_type.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/update_view_pk_player_reducer.rs create mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/update_view_pk_player_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 60fbaac65ab..589a00129b3 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" @@ -10091,6 +10098,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/crates/smoketests/tests/smoketests/views.rs b/crates/smoketests/tests/smoketests/views.rs index bae4d4f8fd8..b03864fb10c 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"; @@ -69,6 +69,31 @@ public static partial class Module } "#; +fn project_inserts_and_deletes(events: Vec, view_name: &str) -> Vec { + events + .into_iter() + .map(|event| { + let view_event = &event[view_name]; + let deletes = view_event["deletes"] + .as_array() + .unwrap() + .iter() + .map(|row| json!({"name": row["name"]})) + .collect::>(); + let inserts = view_event["inserts"] + .as_array() + .unwrap() + .iter() + .map(|row| json!({"name": row["name"]})) + .collect::>(); + + let mut projection = serde_json::Map::new(); + projection.insert(view_name.to_string(), json!({"deletes": deletes, "inserts": inserts})); + Value::Object(projection) + }) + .collect() +} + /// Tests that views populate the st_view_* system tables #[test] fn test_st_view_tables() { @@ -396,24 +421,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(events, "my_player"); assert_eq!( serde_json::json!(projection), @@ -457,6 +465,33 @@ fn test_query_left_semijoin_view() { ); } +#[test] +fn test_subscribe_join_with_view_on_primary_key_col() { + require_pnpm!(); + let mut test = Smoketest::builder().autopublish(false).build(); + test.publish_typescript_module_source( + "views-subscribe-typescript", + "views-subscribe-typescript", + TS_VIEWS_SUBSCRIBE_MODULE, + ) + .unwrap(); + + let query = + "SELECT all_players.* FROM player_state JOIN all_players ON player_state.identity = all_players.identity"; + let sub = test.subscribe_background(&[query], 1).unwrap(); + test.call("insert_player_proc", &["Alice"]).unwrap(); + let events = sub.collect().unwrap(); + + let projection = project_inserts_and_deletes(events, "all_players"); + + assert_eq!( + serde_json::json!(projection), + serde_json::json!([ + {"all_players": {"deletes": [], "inserts": [{"name": "Alice"}]}} + ]) + ); +} + #[test] fn test_query_complex_right_semijoin_view() { let test = Smoketest::builder().precompiled_module("views-query").build(); @@ -493,24 +528,7 @@ fn test_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(events, "my_player"); assert_eq!( serde_json::json!(projection), @@ -535,24 +553,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(events, "my_player"); assert_eq!( serde_json::json!(projection), diff --git a/modules/sdk-test-view-pk-cs/.gitignore b/modules/sdk-test-view-pk-cs/.gitignore new file mode 100644 index 00000000000..1746e3269ed --- /dev/null +++ b/modules/sdk-test-view-pk-cs/.gitignore @@ -0,0 +1,2 @@ +bin +obj diff --git a/modules/sdk-test-view-pk-cs/Lib.cs b/modules/sdk-test-view-pk-cs/Lib.cs new file mode 100644 index 00000000000..b5fd639f772 --- /dev/null +++ b/modules/sdk-test-view-pk-cs/Lib.cs @@ -0,0 +1,52 @@ +namespace SpacetimeDB.Sdk.Test.ViewPk; + +using SpacetimeDB; + +[Table(Accessor = "view_pk_player", Public = true)] +public partial struct ViewPkPlayer +{ + [PrimaryKey] + public ulong id; + public string name; +} + +[Table(Accessor = "view_pk_membership", Public = true)] +public partial struct ViewPkMembership +{ + [PrimaryKey] + public ulong id; + [Index.BTree] + public ulong player_id; +} + +public static partial class Module +{ + [Reducer] + public static void insert_view_pk_player(ReducerContext ctx, ulong id, string name) + { + ctx.Db.view_pk_player.Insert(new ViewPkPlayer { id = id, name = name }); + } + + [Reducer] + public static void update_view_pk_player(ReducerContext ctx, ulong id, string name) + { + var old = ctx.Db.view_pk_player.id.Find(id); + if (old != null) + { + ctx.Db.view_pk_player.id.Delete(id); + } + ctx.Db.view_pk_player.Insert(new ViewPkPlayer { id = id, name = name }); + } + + [Reducer] + public static void insert_view_pk_membership(ReducerContext ctx, ulong id, ulong player_id) + { + ctx.Db.view_pk_membership.Insert(new ViewPkMembership { id = id, player_id = player_id }); + } + + [View(Accessor = "all_view_pk_players", Public = true)] + public static IQuery all_view_pk_players(ViewContext ctx) + { + return ctx.From.view_pk_player(); + } +} diff --git a/modules/sdk-test-view-pk-cs/sdk-test-view-pk-cs.csproj b/modules/sdk-test-view-pk-cs/sdk-test-view-pk-cs.csproj new file mode 100644 index 00000000000..43eb99b6e86 --- /dev/null +++ b/modules/sdk-test-view-pk-cs/sdk-test-view-pk-cs.csproj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/modules/sdk-test-view-pk-ts/.gitignore b/modules/sdk-test-view-pk-ts/.gitignore new file mode 100644 index 00000000000..1521c8b7652 --- /dev/null +++ b/modules/sdk-test-view-pk-ts/.gitignore @@ -0,0 +1 @@ +dist diff --git a/modules/sdk-test-view-pk-ts/package.json b/modules/sdk-test-view-pk-ts/package.json new file mode 100644 index 00000000000..ae27725a5fa --- /dev/null +++ b/modules/sdk-test-view-pk-ts/package.json @@ -0,0 +1,17 @@ +{ + "name": "sdk-test-view-pk-ts", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "cargo build -p spacetimedb-standalone && cargo run -p spacetimedb-cli -- build", + "generate-ts": "cargo build -p spacetimedb-standalone && cargo run -p spacetimedb-cli -- generate --lang typescript --out-dir ts-codegen", + "publish": "cargo run -p spacetimedb-cli -- publish" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "spacetimedb": "workspace:^" + } +} diff --git a/modules/sdk-test-view-pk-ts/src/index.ts b/modules/sdk-test-view-pk-ts/src/index.ts new file mode 100644 index 00000000000..9bedf6d72d5 --- /dev/null +++ b/modules/sdk-test-view-pk-ts/src/index.ts @@ -0,0 +1,51 @@ +import { schema, t, table } from 'spacetimedb/server'; + +const viewPkPlayer = table( + { name: 'view_pk_player', public: true }, + { + id: t.u64().primaryKey(), + name: t.string(), + } +); + +const viewPkMembership = table( + { name: 'view_pk_membership', public: true }, + { + id: t.u64().primaryKey(), + player_id: t.u64().index('btree'), + } +); + +const spacetimedb = schema({ viewPkPlayer, viewPkMembership }); +export default spacetimedb; + +export const all_view_pk_players = spacetimedb.view( + { public: true }, + t.array(viewPkPlayer.rowType), + ctx => ctx.from.viewPkPlayer +); + +export const insert_view_pk_player = spacetimedb.reducer( + { id: t.u64(), name: t.string() }, + (ctx, { id, name }) => { + ctx.db.viewPkPlayer.insert({ id, name }); + } +); + +export const update_view_pk_player = spacetimedb.reducer( + { id: t.u64(), name: t.string() }, + (ctx, { id, name }) => { + const old = ctx.db.viewPkPlayer.id.find(id); + if (old !== undefined) { + ctx.db.viewPkPlayer.id.delete(id); + } + ctx.db.viewPkPlayer.insert({ id, name }); + } +); + +export const insert_view_pk_membership = spacetimedb.reducer( + { id: t.u64(), player_id: t.u64() }, + (ctx, { id, player_id }) => { + ctx.db.viewPkMembership.insert({ id, player_id }); + } +); diff --git a/modules/sdk-test-view-pk-ts/tsconfig.json b/modules/sdk-test-view-pk-ts/tsconfig.json new file mode 100644 index 00000000000..824514ec33a --- /dev/null +++ b/modules/sdk-test-view-pk-ts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "strict": true, + "skipLibCheck": true, + "moduleResolution": "bundler", + "jsx": "react-jsx", + "target": "ESNext", + "lib": ["ES2021", "dom"], + "module": "ESNext", + "isolatedModules": true, + "noEmit": true + }, + "include": ["./**/*"] +} 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..be8c2722a82 --- /dev/null +++ b/modules/sdk-test-view-pk/src/lib.rs @@ -0,0 +1,36 @@ +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, +} + +#[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() +} diff --git a/sdks/rust/tests/test.rs b/sdks/rust/tests/test.rs index d6c5cac38ae..64c9714556b 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 --features expect_view_pk_on_update") + .with_run_command(format!( + "cargo run --features expect_view_pk_on_update -- {}", + 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() + } + } + }; +} + +view_pk_tests!(rust_view_pk, ""); +view_pk_tests!(typescript_view_pk, "-ts"); +view_pk_tests!(csharp_view_pk, "-cs"); 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..19131e5fa7c --- /dev/null +++ b/sdks/rust/tests/view-pk-client/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "view-pk-client" +version.workspace = true +edition.workspace = true +license-file = "LICENSE" + +[features] +expect_view_pk_on_update = [] + +[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/src/main.rs b/sdks/rust/tests/view-pk-client/src/main.rs new file mode 100644 index 00000000000..9ef194c3033 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/main.rs @@ -0,0 +1,169 @@ +mod module_bindings; + +use module_bindings::*; +use spacetimedb_sdk::{error::InternalError, DbContext, Table}; +#[cfg(feature = "expect_view_pk_on_update")] +use spacetimedb_sdk::TableWithPrimaryKey; +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 +} + +#[cfg(feature = "expect_view_pk_on_update")] +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); +} + +#[cfg(feature = "expect_view_pk_on_update")] +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(())); + }); + + ctx.reducers() + .insert_view_pk_player_then( + 1, + "before".to_string(), + reducer_callback_assert_committed("insert_view_pk_player"), + ) + .unwrap(); + + 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(); +} + +#[cfg(not(feature = "expect_view_pk_on_update"))] +fn exec_view_pk_on_update() { + panic!("This test must be run with --features expect_view_pk_on_update"); +} + +fn exec_view_pk_join_query_builder() { + let test_counter = TestCounter::new(); + let mut joined_insert = Some(test_counter.add_test("join_insert")); + + 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_insert(move |_, row| { + assert_eq!(row.id, 1); + assert_eq!(row.name, "joined"); + put_result(&mut joined_insert, Ok(())); + }); + + ctx.reducers() + .insert_view_pk_player_then( + 1, + "joined".to_string(), + reducer_callback_assert_committed("insert_view_pk_player"), + ) + .unwrap(); + + ctx.reducers() + .insert_view_pk_membership_then( + 1, + 1, + reducer_callback_assert_committed("insert_view_pk_membership"), + ) + .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(); +} + +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(), + _ => 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/all_view_pk_players_type.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/all_view_pk_players_type.rs new file mode 100644 index 00000000000..8965a07fa67 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/all_view_pk_players_type.rs @@ -0,0 +1,13 @@ +// 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 AllViewPkPlayers {} + +impl __sdk::InModule for AllViewPkPlayers { + type Module = super::RemoteModule; +} 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_type.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_membership_type.rs new file mode 100644 index 00000000000..df07dbfea17 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_membership_type.rs @@ -0,0 +1,16 @@ +// 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 InsertViewPkMembership { + pub id: u64, + pub player_id: u64, +} + +impl __sdk::InModule for InsertViewPkMembership { + type Module = super::RemoteModule; +} 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/insert_view_pk_player_type.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_player_type.rs new file mode 100644 index 00000000000..64a50975905 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_player_type.rs @@ -0,0 +1,16 @@ +// 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 InsertViewPkPlayer { + pub id: u64, + pub name: String, +} + +impl __sdk::InModule for InsertViewPkPlayer { + type Module = super::RemoteModule; +} 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..c4213d6a729 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/mod.rs @@ -0,0 +1,850 @@ +// 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 8cb2038f8553b92cea512a8e697ad0525c56aecd). + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +pub mod all_view_pk_players_table; +pub mod all_view_pk_players_type; +pub mod insert_view_pk_membership_reducer; +pub mod insert_view_pk_membership_type; +pub mod insert_view_pk_player_reducer; +pub mod insert_view_pk_player_type; +pub mod update_view_pk_player_reducer; +pub mod update_view_pk_player_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 all_view_pk_players_type::AllViewPkPlayers; +pub use insert_view_pk_membership_reducer::insert_view_pk_membership; +pub use insert_view_pk_membership_type::InsertViewPkMembership; +pub use insert_view_pk_player_reducer::insert_view_pk_player; +pub use insert_view_pk_player_type::InsertViewPkPlayer; +pub use update_view_pk_player_reducer::update_view_pk_player; +pub use update_view_pk_player_type::UpdateViewPkPlayer; +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 }, + 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::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::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, + view_pk_membership: __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)?), + "view_pk_membership" => db_update + .view_pk_membership + .append(view_pk_membership_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_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); + + 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)?), + "view_pk_membership" => db_update + .view_pk_membership + .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)?), + "view_pk_membership" => db_update + .view_pk_membership + .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>, + view_pk_membership: __sdk::TableAppliedDiff<'r, ViewPkMembership>, + 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::("view_pk_membership", &self.view_pk_membership, 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); + view_pk_membership_table::register_table(client_cache); + view_pk_player_table::register_table(client_cache); + } + const ALL_TABLE_NAMES: &'static [&'static str] = &["all_view_pk_players", "view_pk_membership", "view_pk_player"]; +} 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/update_view_pk_player_type.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/update_view_pk_player_type.rs new file mode 100644 index 00000000000..ed00e519826 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/update_view_pk_player_type.rs @@ -0,0 +1,16 @@ +// 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 UpdateViewPkPlayer { + pub id: u64, + pub name: String, +} + +impl __sdk::InModule for UpdateViewPkPlayer { + type Module = super::RemoteModule; +} 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 c603df8bc05a19bf5cb89258b8f9781725be8d1c Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Tue, 3 Mar 2026 16:19:25 -0800 Subject: [PATCH 2/9] C# client codegen tests --- .../view-pk-client/Program.cs | 169 ++++++++++++++++++ .../view-pk-client/client.csproj | 15 ++ sdks/csharp/tools~/gen-regression-tests.sh | 1 + sdks/csharp/tools~/run-regression-tests.sh | 6 + 4 files changed, 191 insertions(+) create mode 100644 sdks/csharp/examples~/regression-tests/view-pk-client/Program.cs create mode 100644 sdks/csharp/examples~/regression-tests/view-pk-client/client.csproj diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/Program.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/Program.cs new file mode 100644 index 00000000000..43d9d8219fd --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/Program.cs @@ -0,0 +1,169 @@ +/// View primary key tests run with a live server. +/// To run these, run a local SpacetimeDB via `spacetime start`, +/// then in a separate terminal run `tools~/run-regression-tests.sh`. + +using System.Diagnostics; +using SpacetimeDB; +using SpacetimeDB.Types; + +const string HOST = "http://localhost:3000"; +const string DBNAME = "view-pk-tests"; +const int TIMEOUT_SECONDS = 20; + +DbConnection Connect(Action onConnect) +{ + return DbConnection.Builder() + .WithUri(HOST) + .WithDatabaseName(DBNAME) + .OnConnect(onConnect) + .OnConnectError((err) => + { + throw err; + }) + .OnDisconnect((_, err) => + { + if (err != null) + { + throw err; + } + + throw new Exception("Unexpected disconnect"); + }) + .Build(); +} + +void WaitFor(DbConnection db, Func isDone, string testName) +{ + var start = DateTime.Now; + while (!isDone()) + { + db.FrameTick(); + Thread.Sleep(100); + if ((DateTime.Now - start).TotalSeconds > TIMEOUT_SECONDS) + { + throw new Exception($"{testName} timed out after {TIMEOUT_SECONDS} seconds"); + } + } +} + +void RunOnUpdateTest() +{ + Log.Info("Running view-pk on-update test"); + + var applied = false; + var onUpdateCalled = false; + + var db = Connect((conn, _, _) => + { + conn.SubscriptionBuilder() + .OnApplied((ctx) => + { + if (applied) + { + return; + } + applied = true; + + ctx.Db.AllViewPkPlayers.OnUpdate += (_, oldRow, newRow) => + { + Debug.Assert(oldRow.Id == 1UL, $"Expected old id=1, got {oldRow.Id}"); + Debug.Assert(oldRow.Name == "before", $"Expected old name=before, got {oldRow.Name}"); + Debug.Assert(newRow.Id == 1UL, $"Expected new id=1, got {newRow.Id}"); + Debug.Assert(newRow.Name == "after", $"Expected new name=after, got {newRow.Name}"); + onUpdateCalled = true; + }; + + ctx.Procedures.InsertViewPkPlayer(1UL, "before", (_, result) => + { + if (!result.IsSuccess) + { + throw result.Error!; + } + }); + + ctx.Procedures.UpdateViewPkPlayer(1UL, "after", (_, result) => + { + if (!result.IsSuccess) + { + throw result.Error!; + } + }); + }) + .OnError((_, err) => + { + throw err; + }) + .AddQuery(qb => qb.From.AllViewPkPlayers()) + .Subscribe(); + }); + + WaitFor(db, () => applied && onUpdateCalled, "view-pk on-update test"); + db.Disconnect(); +} + +void RunJoinQueryBuilderTest() +{ + Log.Info("Running view-pk join query-builder test"); + + var applied = false; + var onInsertCalled = false; + + var db = Connect((conn, _, _) => + { + conn.SubscriptionBuilder() + .OnApplied((ctx) => + { + if (applied) + { + return; + } + applied = true; + + ctx.Db.AllViewPkPlayers.OnInsert += (_, row) => + { + Debug.Assert(row.Id == 2UL, $"Expected joined row id=2, got {row.Id}"); + Debug.Assert(row.Name == "joined", $"Expected joined row name=joined, got {row.Name}"); + onInsertCalled = true; + }; + + ctx.Procedures.InsertViewPkPlayer(2UL, "joined", (_, result) => + { + if (!result.IsSuccess) + { + throw result.Error!; + } + }); + + ctx.Procedures.InsertViewPkMembership(1UL, 2UL, (_, result) => + { + if (!result.IsSuccess) + { + throw result.Error!; + } + }); + }) + .OnError((_, err) => + { + throw err; + }) + .AddQuery(qb => + qb.From.ViewPkMembership().RightSemijoin(qb.From.AllViewPkPlayers(), (m, p) => m.PlayerId.Eq(p.Id)) + ) + .Subscribe(); + }); + + WaitFor(db, () => applied && onInsertCalled, "view-pk join query-builder test"); + db.Disconnect(); +} + +System.AppDomain.CurrentDomain.UnhandledException += (_, args) => +{ + Log.Exception($"Unhandled exception: {args}"); + Environment.Exit(1); +}; + +RunOnUpdateTest(); +RunJoinQueryBuilderTest(); + +Log.Info("Success"); +Environment.Exit(0); diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/client.csproj b/sdks/csharp/examples~/regression-tests/view-pk-client/client.csproj new file mode 100644 index 00000000000..c0e1682bcbc --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/client.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + true + + + + + + + diff --git a/sdks/csharp/tools~/gen-regression-tests.sh b/sdks/csharp/tools~/gen-regression-tests.sh index 8936971cd5a..451a2131a60 100755 --- a/sdks/csharp/tools~/gen-regression-tests.sh +++ b/sdks/csharp/tools~/gen-regression-tests.sh @@ -10,3 +10,4 @@ cargo build --manifest-path "$STDB_PATH/crates/standalone/Cargo.toml" cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- generate -y -l csharp -o "$SDK_PATH/examples~/regression-tests/client/module_bindings" --module-path "$SDK_PATH/examples~/regression-tests/server" cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- generate -y -l csharp -o "$SDK_PATH/examples~/regression-tests/republishing/client/module_bindings" --module-path "$SDK_PATH/examples~/regression-tests/republishing/server-republish" cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- generate -y -l csharp -o "$SDK_PATH/examples~/regression-tests/procedure-client/module_bindings" --module-path "$STDB_PATH/modules/sdk-test-procedure" +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- generate -y -l csharp -o "$SDK_PATH/examples~/regression-tests/view-pk-client/module_bindings" --module-path "$STDB_PATH/modules/sdk-test-view-pk-cs" diff --git a/sdks/csharp/tools~/run-regression-tests.sh b/sdks/csharp/tools~/run-regression-tests.sh index fdd7733bf65..1832979061b 100644 --- a/sdks/csharp/tools~/run-regression-tests.sh +++ b/sdks/csharp/tools~/run-regression-tests.sh @@ -31,6 +31,9 @@ rm -rf "$SDK_PATH/examples~/regression-tests/procedure-client/module_bindings"/* # Publish module for procedure tests cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server local -p "$STDB_PATH/modules/sdk-test-procedure" procedure-tests +# Publish module for view primary-key tests +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server local -p "$STDB_PATH/modules/sdk-test-view-pk-cs" view-pk-tests + # Run client for btree test cd "$SDK_PATH/examples~/regression-tests/client" && dotnet run -c Debug @@ -40,3 +43,6 @@ cd "$SDK_PATH/examples~/regression-tests/republishing/client" && dotnet run -c D # Run client for procedure test cd "$SDK_PATH/examples~/regression-tests/procedure-client" && dotnet run -c Debug + +# Run client for view primary-key tests +cd "$SDK_PATH/examples~/regression-tests/view-pk-client" && dotnet run -c Debug From 4ce95ba76db9d3d55f620c6ef71b199bbf5f4043 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Tue, 3 Mar 2026 16:55:02 -0800 Subject: [PATCH 3/9] typescript client codegen tests --- Cargo.lock | 269 ++++++------------ .../test-app/server/Cargo.toml | 2 +- .../test-app/server/src/lib.rs | 47 ++- .../all_view_pk_players_table.ts | 16 ++ .../test-app/src/module_bindings/index.ts | 71 ++++- .../insert_view_pk_membership_reducer.ts | 16 ++ .../insert_view_pk_player_reducer.ts | 16 ++ .../test-app/src/module_bindings/types.ts | 12 + .../src/module_bindings/types/reducers.ts | 12 + .../update_view_pk_player_reducer.ts | 16 ++ .../view_pk_membership_table.ts | 16 ++ .../module_bindings/view_pk_player_table.ts | 16 ++ .../tests/db_connection.test.ts | 117 +++++++- crates/bindings-typescript/tests/utils.ts | 9 + 14 files changed, 439 insertions(+), 196 deletions(-) create mode 100644 crates/bindings-typescript/test-app/src/module_bindings/all_view_pk_players_table.ts create mode 100644 crates/bindings-typescript/test-app/src/module_bindings/insert_view_pk_membership_reducer.ts create mode 100644 crates/bindings-typescript/test-app/src/module_bindings/insert_view_pk_player_reducer.ts create mode 100644 crates/bindings-typescript/test-app/src/module_bindings/update_view_pk_player_reducer.ts create mode 100644 crates/bindings-typescript/test-app/src/module_bindings/view_pk_membership_table.ts create mode 100644 crates/bindings-typescript/test-app/src/module_bindings/view_pk_player_table.ts diff --git a/Cargo.lock b/Cargo.lock index 589a00129b3..ae4f4093f0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -449,7 +449,7 @@ name = "basic-rs-template-module" version = "0.1.0" dependencies = [ "log", - "spacetimedb 2.0.3", + "spacetimedb", ] [[package]] @@ -457,7 +457,7 @@ name = "benchmarks-module" version = "0.1.0" dependencies = [ "anyhow", - "spacetimedb 2.0.3", + "spacetimedb", ] [[package]] @@ -3562,7 +3562,7 @@ name = "keynote-benchmarks" version = "0.1.0" dependencies = [ "log", - "spacetimedb 2.0.3", + "spacetimedb", ] [[package]] @@ -3968,7 +3968,7 @@ version = "0.0.0" dependencies = [ "anyhow", "log", - "spacetimedb 2.0.3", + "spacetimedb", ] [[package]] @@ -5156,7 +5156,7 @@ name = "perf-test-module" version = "0.1.0" dependencies = [ "log", - "spacetimedb 2.0.3", + "spacetimedb", ] [[package]] @@ -5612,7 +5612,7 @@ dependencies = [ "anyhow", "env_logger 0.10.2", "serde_json", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-sdk", "test-counter", ] @@ -5816,7 +5816,7 @@ name = "quickstart-chat-module" version = "0.1.0" dependencies = [ "log", - "spacetimedb 2.0.3", + "spacetimedb", ] [[package]] @@ -6961,7 +6961,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" name = "sdk-test-event-table-module" version = "0.1.0" dependencies = [ - "spacetimedb 2.0.3", + "spacetimedb", ] [[package]] @@ -6971,7 +6971,7 @@ dependencies = [ "anyhow", "log", "paste", - "spacetimedb 2.0.3", + "spacetimedb", ] [[package]] @@ -6981,7 +6981,7 @@ dependencies = [ "anyhow", "log", "paste", - "spacetimedb 2.0.3", + "spacetimedb", ] [[package]] @@ -6991,14 +6991,14 @@ dependencies = [ "anyhow", "log", "paste", - "spacetimedb 2.0.3", + "spacetimedb", ] [[package]] name = "sdk-test-view-pk" version = "0.1.0" dependencies = [ - "spacetimedb 2.0.3", + "spacetimedb", ] [[package]] @@ -7461,26 +7461,7 @@ name = "spacetime-module" version = "0.1.0" dependencies = [ "log", - "spacetimedb 2.0.3", -] - -[[package]] -name = "spacetimedb" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db18cb19c7499ba4a65b1504442179a7e4aba487dc35978d90966c5ca02ee16b" -dependencies = [ - "bytemuck", - "derive_more 0.99.20", - "getrandom 0.2.16", - "log", - "rand 0.8.5", - "scoped-tls", - "serde_json", - "spacetimedb-bindings-macro 1.9.0", - "spacetimedb-bindings-sys 1.9.0", - "spacetimedb-lib 1.9.0", - "spacetimedb-primitives 1.9.0", + "spacetimedb", ] [[package]] @@ -7498,10 +7479,10 @@ dependencies = [ "rand 0.8.5", "scoped-tls", "serde_json", - "spacetimedb-bindings-macro 2.0.3", - "spacetimedb-bindings-sys 2.0.3", - "spacetimedb-lib 2.0.3", - "spacetimedb-primitives 2.0.3", + "spacetimedb-bindings-macro", + "spacetimedb-bindings-sys", + "spacetimedb-lib", + "spacetimedb-primitives", "spacetimedb-query-builder", "trybuild", ] @@ -7516,7 +7497,7 @@ dependencies = [ "serde_with", "spacetimedb-data-structures", "spacetimedb-jsonwebtoken", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", ] [[package]] @@ -7553,11 +7534,11 @@ dependencies = [ "spacetimedb-data-structures", "spacetimedb-datastore", "spacetimedb-execution", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-paths", - "spacetimedb-primitives 2.0.3", + "spacetimedb-primitives", "spacetimedb-query", - "spacetimedb-sats 2.0.3", + "spacetimedb-sats", "spacetimedb-schema", "spacetimedb-standalone", "spacetimedb-table", @@ -7570,20 +7551,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "spacetimedb-bindings-macro" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47725515a53cf3344aa6bbb3f2063c7fbb5496c743f7a7c2150413acd1213c1d" -dependencies = [ - "heck 0.4.1", - "humantime", - "proc-macro2", - "quote", - "spacetimedb-primitives 1.9.0", - "syn 2.0.107", -] - [[package]] name = "spacetimedb-bindings-macro" version = "2.0.3" @@ -7592,24 +7559,15 @@ dependencies = [ "humantime", "proc-macro2", "quote", - "spacetimedb-primitives 2.0.3", + "spacetimedb-primitives", "syn 2.0.107", ] -[[package]] -name = "spacetimedb-bindings-sys" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08201dac3ce095645dfbf407e71aba7c784a6061dace21bb4a49dd0b80d3f007" -dependencies = [ - "spacetimedb-primitives 1.9.0", -] - [[package]] name = "spacetimedb-bindings-sys" version = "2.0.3" dependencies = [ - "spacetimedb-primitives 2.0.3", + "spacetimedb-primitives", ] [[package]] @@ -7665,9 +7623,9 @@ dependencies = [ "spacetimedb-data-structures", "spacetimedb-fs-utils", "spacetimedb-jsonwebtoken", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-paths", - "spacetimedb-primitives 2.0.3", + "spacetimedb-primitives", "spacetimedb-schema", "syntect", "tabled", @@ -7731,7 +7689,7 @@ dependencies = [ "spacetimedb-data-structures", "spacetimedb-datastore", "spacetimedb-jsonwebtoken", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-paths", "spacetimedb-schema", "tempfile", @@ -7763,9 +7721,9 @@ dependencies = [ "serde_json", "serde_with", "smallvec", - "spacetimedb-lib 2.0.3", - "spacetimedb-primitives 2.0.3", - "spacetimedb-sats 2.0.3", + "spacetimedb-lib", + "spacetimedb-primitives", + "spacetimedb-sats", "strum", "thiserror 1.0.69", ] @@ -7781,8 +7739,8 @@ dependencies = [ "itertools 0.12.1", "regex", "spacetimedb-data-structures", - "spacetimedb-lib 2.0.3", - "spacetimedb-primitives 2.0.3", + "spacetimedb-lib", + "spacetimedb-primitives", "spacetimedb-schema", "spacetimedb-testing", ] @@ -7810,8 +7768,8 @@ dependencies = [ "spacetimedb-commitlog", "spacetimedb-fs-utils", "spacetimedb-paths", - "spacetimedb-primitives 2.0.3", - "spacetimedb-sats 2.0.3", + "spacetimedb-primitives", + "spacetimedb-sats", "tempfile", "thiserror 1.0.69", "tokio", @@ -7906,14 +7864,14 @@ dependencies = [ "spacetimedb-fs-utils", "spacetimedb-jsonwebtoken", "spacetimedb-jwks", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-memory-usage", "spacetimedb-metrics", "spacetimedb-paths", "spacetimedb-physical-plan", - "spacetimedb-primitives 2.0.3", + "spacetimedb-primitives", "spacetimedb-query", - "spacetimedb-sats 2.0.3", + "spacetimedb-sats", "spacetimedb-schema", "spacetimedb-snapshot", "spacetimedb-subscription", @@ -7985,11 +7943,11 @@ dependencies = [ "spacetimedb-data-structures", "spacetimedb-durability", "spacetimedb-execution", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-metrics", "spacetimedb-paths", - "spacetimedb-primitives 2.0.3", - "spacetimedb-sats 2.0.3", + "spacetimedb-primitives", + "spacetimedb-sats", "spacetimedb-schema", "spacetimedb-snapshot", "spacetimedb-table", @@ -8010,7 +7968,7 @@ dependencies = [ "spacetimedb-commitlog", "spacetimedb-fs-utils", "spacetimedb-paths", - "spacetimedb-sats 2.0.3", + "spacetimedb-sats", "tempfile", "thiserror 1.0.69", "tokio", @@ -8024,10 +7982,10 @@ dependencies = [ "anyhow", "itertools 0.12.1", "spacetimedb-expr", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-physical-plan", - "spacetimedb-primitives 2.0.3", - "spacetimedb-sats 2.0.3", + "spacetimedb-primitives", + "spacetimedb-sats", "spacetimedb-sql-parser", "spacetimedb-table", ] @@ -8042,11 +8000,11 @@ dependencies = [ "derive_more 0.99.20", "ethnum", "pretty_assertions", - "spacetimedb 2.0.3", + "spacetimedb", "spacetimedb-data-structures", - "spacetimedb-lib 2.0.3", - "spacetimedb-primitives 2.0.3", - "spacetimedb-sats 2.0.3", + "spacetimedb-lib", + "spacetimedb-primitives", + "spacetimedb-sats", "spacetimedb-schema", "spacetimedb-sql-parser", "thiserror 1.0.69", @@ -8104,26 +8062,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "spacetimedb-lib" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702c08bfcd0426c45786e30f016e0a03d85f34dac3555e5b370291441297e266" -dependencies = [ - "anyhow", - "bitflags 2.10.0", - "blake3", - "chrono", - "derive_more 0.99.20", - "enum-as-inner", - "hex", - "itertools 0.12.1", - "spacetimedb-bindings-macro 1.9.0", - "spacetimedb-primitives 1.9.0", - "spacetimedb-sats 1.9.0", - "thiserror 1.0.69", -] - [[package]] name = "spacetimedb-lib" version = "2.0.3" @@ -8145,11 +8083,11 @@ dependencies = [ "ron", "serde", "serde_json", - "spacetimedb-bindings-macro 2.0.3", + "spacetimedb-bindings-macro", "spacetimedb-memory-usage", "spacetimedb-metrics", - "spacetimedb-primitives 2.0.3", - "spacetimedb-sats 2.0.3", + "spacetimedb-primitives", + "spacetimedb-sats", "thiserror 1.0.69", ] @@ -8202,7 +8140,7 @@ dependencies = [ "pgwire", "spacetimedb-client-api", "spacetimedb-client-api-messages", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "thiserror 1.0.69", "tokio", ] @@ -8217,26 +8155,13 @@ dependencies = [ "pretty_assertions", "spacetimedb-data-structures", "spacetimedb-expr", - "spacetimedb-lib 2.0.3", - "spacetimedb-primitives 2.0.3", + "spacetimedb-lib", + "spacetimedb-primitives", "spacetimedb-schema", "spacetimedb-sql-parser", "spacetimedb-table", ] -[[package]] -name = "spacetimedb-primitives" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55af71f2ccb753957ad47b19648481bd67ae458885f18df867a4d5b0a55c8c67" -dependencies = [ - "bitflags 2.10.0", - "either", - "enum-as-inner", - "itertools 0.12.1", - "nohash-hasher", -] - [[package]] name = "spacetimedb-primitives" version = "2.0.3" @@ -8260,9 +8185,9 @@ dependencies = [ "spacetimedb-client-api-messages", "spacetimedb-execution", "spacetimedb-expr", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-physical-plan", - "spacetimedb-primitives 2.0.3", + "spacetimedb-primitives", "spacetimedb-schema", "spacetimedb-sql-parser", "spacetimedb-table", @@ -8272,7 +8197,7 @@ dependencies = [ name = "spacetimedb-query-builder" version = "2.0.3" dependencies = [ - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", ] [[package]] @@ -8289,38 +8214,12 @@ dependencies = [ "rand 0.9.2", "rand_distr", "spacetimedb-client-api-messages", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "thiserror 1.0.69", "tokio", "tokio-tungstenite", ] -[[package]] -name = "spacetimedb-sats" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a89afd9f4eded852e7355102f66f8ff346d25fe903d38ef0b6a171d32d696a" -dependencies = [ - "anyhow", - "arrayvec", - "bitflags 2.10.0", - "bytemuck", - "bytes", - "chrono", - "decorum", - "derive_more 0.99.20", - "enum-as-inner", - "ethnum", - "hex", - "itertools 0.12.1", - "second-stack", - "sha3", - "smallvec", - "spacetimedb-bindings-macro 1.9.0", - "spacetimedb-primitives 1.9.0", - "thiserror 1.0.69", -] - [[package]] name = "spacetimedb-sats" version = "2.0.3" @@ -8349,10 +8248,10 @@ dependencies = [ "serde_json", "sha3", "smallvec", - "spacetimedb-bindings-macro 2.0.3", + "spacetimedb-bindings-macro", "spacetimedb-memory-usage", "spacetimedb-metrics", - "spacetimedb-primitives 2.0.3", + "spacetimedb-primitives", "thiserror 1.0.69", "uuid", ] @@ -8377,10 +8276,10 @@ dependencies = [ "serial_test", "smallvec", "spacetimedb-data-structures", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-memory-usage", - "spacetimedb-primitives 2.0.3", - "spacetimedb-sats 2.0.3", + "spacetimedb-primitives", + "spacetimedb-sats", "spacetimedb-sql-parser", "spacetimedb-testing", "termcolor", @@ -8411,10 +8310,10 @@ dependencies = [ "rand 0.9.2", "spacetimedb-client-api-messages", "spacetimedb-data-structures", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-metrics", "spacetimedb-query-builder", - "spacetimedb-sats 2.0.3", + "spacetimedb-sats", "spacetimedb-schema", "spacetimedb-testing", "thiserror 1.0.69", @@ -8460,10 +8359,10 @@ dependencies = [ "spacetimedb-datastore", "spacetimedb-durability", "spacetimedb-fs-utils", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-paths", - "spacetimedb-primitives 2.0.3", - "spacetimedb-sats 2.0.3", + "spacetimedb-primitives", + "spacetimedb-sats", "spacetimedb-schema", "spacetimedb-table", "tempfile", @@ -8479,7 +8378,7 @@ name = "spacetimedb-sql-parser" version = "2.0.3" dependencies = [ "derive_more 0.99.20", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "sqlparser", "thiserror 1.0.69", ] @@ -8511,7 +8410,7 @@ dependencies = [ "spacetimedb-client-api-messages", "spacetimedb-core", "spacetimedb-datastore", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-paths", "spacetimedb-pg", "spacetimedb-schema", @@ -8534,9 +8433,9 @@ dependencies = [ "spacetimedb-data-structures", "spacetimedb-execution", "spacetimedb-expr", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-physical-plan", - "spacetimedb-primitives 2.0.3", + "spacetimedb-primitives", "spacetimedb-query", "spacetimedb-schema", ] @@ -8561,10 +8460,10 @@ dependencies = [ "rand 0.9.2", "smallvec", "spacetimedb-data-structures", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-memory-usage", - "spacetimedb-primitives 2.0.3", - "spacetimedb-sats 2.0.3", + "spacetimedb-primitives", + "spacetimedb-sats", "spacetimedb-schema", "thiserror 1.0.69", ] @@ -8590,7 +8489,7 @@ dependencies = [ "spacetimedb-client-api-messages", "spacetimedb-core", "spacetimedb-data-structures", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-paths", "spacetimedb-schema", "spacetimedb-standalone", @@ -8636,9 +8535,9 @@ dependencies = [ "smallvec", "spacetimedb-data-structures", "spacetimedb-execution", - "spacetimedb-lib 2.0.3", - "spacetimedb-primitives 2.0.3", - "spacetimedb-sats 2.0.3", + "spacetimedb-lib", + "spacetimedb-primitives", + "spacetimedb-sats", "spacetimedb-schema", "spacetimedb-table", "tempfile", @@ -8733,8 +8632,8 @@ dependencies = [ "rust_decimal", "spacetimedb-core", "spacetimedb-datastore", - "spacetimedb-lib 2.0.3", - "spacetimedb-sats 2.0.3", + "spacetimedb-lib", + "spacetimedb-sats", "spacetimedb-vm", "sqllogictest", "sqllogictest-engines", @@ -9866,7 +9765,7 @@ version = "0.1.0" dependencies = [ "anyhow", "log", - "spacetimedb 1.9.0", + "spacetimedb", ] [[package]] @@ -9875,7 +9774,7 @@ version = "0.1.0" dependencies = [ "anyhow", "log", - "spacetimedb 2.0.3", + "spacetimedb", ] [[package]] @@ -10093,7 +9992,7 @@ version = "2.0.3" dependencies = [ "anyhow", "env_logger 0.10.2", - "spacetimedb-lib 2.0.3", + "spacetimedb-lib", "spacetimedb-sdk", "test-counter", ] @@ -11283,7 +11182,7 @@ dependencies = [ "reqwest 0.12.24", "serde", "serde_json", - "spacetimedb 2.0.3", + "spacetimedb", "spacetimedb-data-structures", "spacetimedb-guard", "tempfile", diff --git a/crates/bindings-typescript/test-app/server/Cargo.toml b/crates/bindings-typescript/test-app/server/Cargo.toml index c61fe890b87..acab3353dcc 100644 --- a/crates/bindings-typescript/test-app/server/Cargo.toml +++ b/crates/bindings-typescript/test-app/server/Cargo.toml @@ -9,6 +9,6 @@ edition = "2024" crate-type = ["cdylib"] [dependencies] -spacetimedb = "1.2.0" +spacetimedb = { path = "../../../bindings" } log = "0.4" anyhow = "1.0" diff --git a/crates/bindings-typescript/test-app/server/src/lib.rs b/crates/bindings-typescript/test-app/server/src/lib.rs index 853c91632e1..d37cff9af62 100644 --- a/crates/bindings-typescript/test-app/server/src/lib.rs +++ b/crates/bindings-typescript/test-app/server/src/lib.rs @@ -1,6 +1,6 @@ -use spacetimedb::{reducer, table, Identity, ReducerContext, SpacetimeType, Table}; +use spacetimedb::{reducer, table, view, Identity, Query, ReducerContext, SpacetimeType, Table, ViewContext}; -#[table(name = player, public)] +#[table(accessor = player, public)] pub struct Player { #[primary_key] #[auto_inc] @@ -16,14 +16,14 @@ pub struct Point { pub y: u16, } -#[table(name = user, public)] +#[table(accessor = user, public)] pub struct User { #[primary_key] pub identity: Identity, pub username: String, } -#[table(name = unindexed_player, public)] +#[table(accessor = unindexed_player, public)] pub struct UnindexedPlayer { #[primary_key] #[auto_inc] @@ -33,16 +33,51 @@ pub struct UnindexedPlayer { location: Point, } +#[table(accessor = view_pk_player, public)] +pub struct ViewPkPlayer { + #[primary_key] + id: u64, + name: String, +} + +#[table(accessor = view_pk_membership, public)] +pub struct ViewPkMembership { + #[primary_key] + id: u64, + #[index(btree)] + player_id: u64, +} + #[reducer] pub fn create_player(ctx: &ReducerContext, name: String, location: Point) { ctx.db.user().insert(User { - identity: ctx.sender, + identity: ctx.sender(), username: name.clone(), }); ctx.db.player().insert(Player { id: 0, - user_id: ctx.sender, + user_id: ctx.sender(), name, location, }); } + +#[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() +} diff --git a/crates/bindings-typescript/test-app/src/module_bindings/all_view_pk_players_table.ts b/crates/bindings-typescript/test-app/src/module_bindings/all_view_pk_players_table.ts new file mode 100644 index 00000000000..df99ddc1f3a --- /dev/null +++ b/crates/bindings-typescript/test-app/src/module_bindings/all_view_pk_players_table.ts @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from '../../../src/index'; + +export default __t.row({ + id: __t.u64(), + name: __t.string(), +}); diff --git a/crates/bindings-typescript/test-app/src/module_bindings/index.ts b/crates/bindings-typescript/test-app/src/module_bindings/index.ts index 852b010b300..f4956327115 100644 --- a/crates/bindings-typescript/test-app/src/module_bindings/index.ts +++ b/crates/bindings-typescript/test-app/src/module_bindings/index.ts @@ -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.0 (commit 098afaf1a5ed935bce5a32c88620e829506effe7). +// This was generated using spacetimedb cli version 2.0.3 (commit 8cb2038f8553b92cea512a8e697ad0525c56aecd). /* eslint-disable */ /* tslint:disable */ @@ -35,13 +35,19 @@ import { // Import all reducer arg schemas import CreatePlayerReducer from './create_player_reducer'; +import InsertViewPkMembershipReducer from './insert_view_pk_membership_reducer'; +import InsertViewPkPlayerReducer from './insert_view_pk_player_reducer'; +import UpdateViewPkPlayerReducer from './update_view_pk_player_reducer'; // Import all procedure arg schemas // Import all table schema definitions +import AllViewPkPlayersRow from './all_view_pk_players_table'; import PlayerRow from './player_table'; import UnindexedPlayerRow from './unindexed_player_table'; import UserRow from './user_table'; +import ViewPkMembershipRow from './view_pk_membership_table'; +import ViewPkPlayerRow from './view_pk_player_table'; /** Type-only namespace exports for generated type groups. */ @@ -106,11 +112,70 @@ const tablesSchema = __schema({ }, UserRow ), + view_pk_membership: __table( + { + name: 'view_pk_membership', + indexes: [ + { + accessor: 'id', + name: 'view_pk_membership_id_idx_btree', + algorithm: 'btree', + columns: ['id'], + }, + { + accessor: 'player_id', + name: 'view_pk_membership_player_id_idx_btree', + algorithm: 'btree', + columns: ['playerId'], + }, + ], + constraints: [ + { + name: 'view_pk_membership_id_key', + constraint: 'unique', + columns: ['id'], + }, + ], + }, + ViewPkMembershipRow + ), + view_pk_player: __table( + { + name: 'view_pk_player', + indexes: [ + { + accessor: 'id', + name: 'view_pk_player_id_idx_btree', + algorithm: 'btree', + columns: ['id'], + }, + ], + constraints: [ + { + name: 'view_pk_player_id_key', + constraint: 'unique', + columns: ['id'], + }, + ], + }, + ViewPkPlayerRow + ), + all_view_pk_players: __table( + { + name: 'all_view_pk_players', + indexes: [], + constraints: [], + }, + AllViewPkPlayersRow + ), }); /** The schema information for all reducers in this module. This is defined the same way as the reducers would have been defined in the server, except the body of the reducer is omitted in code generation. */ const reducersSchema = __reducers( - __reducerSchema('create_player', CreatePlayerReducer) + __reducerSchema('create_player', CreatePlayerReducer), + __reducerSchema('insert_view_pk_membership', InsertViewPkMembershipReducer), + __reducerSchema('insert_view_pk_player', InsertViewPkPlayerReducer), + __reducerSchema('update_view_pk_player', UpdateViewPkPlayerReducer) ); /** The schema information for all procedures in this module. This is defined the same way as the procedures would have been defined in the server. */ @@ -119,7 +184,7 @@ const proceduresSchema = __procedures(); /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { - cliVersion: '2.0.0' as const, + cliVersion: '2.0.3' as const, }, tables: tablesSchema.schemaType.tables, reducers: reducersSchema.reducersType.reducers, diff --git a/crates/bindings-typescript/test-app/src/module_bindings/insert_view_pk_membership_reducer.ts b/crates/bindings-typescript/test-app/src/module_bindings/insert_view_pk_membership_reducer.ts new file mode 100644 index 00000000000..53b3b92a193 --- /dev/null +++ b/crates/bindings-typescript/test-app/src/module_bindings/insert_view_pk_membership_reducer.ts @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from '../../../src/index'; + +export default { + id: __t.u64(), + playerId: __t.u64(), +}; diff --git a/crates/bindings-typescript/test-app/src/module_bindings/insert_view_pk_player_reducer.ts b/crates/bindings-typescript/test-app/src/module_bindings/insert_view_pk_player_reducer.ts new file mode 100644 index 00000000000..1d11cf14879 --- /dev/null +++ b/crates/bindings-typescript/test-app/src/module_bindings/insert_view_pk_player_reducer.ts @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from '../../../src/index'; + +export default { + id: __t.u64(), + name: __t.string(), +}; diff --git a/crates/bindings-typescript/test-app/src/module_bindings/types.ts b/crates/bindings-typescript/test-app/src/module_bindings/types.ts index dc09648d9e8..b9aced685f0 100644 --- a/crates/bindings-typescript/test-app/src/module_bindings/types.ts +++ b/crates/bindings-typescript/test-app/src/module_bindings/types.ts @@ -41,3 +41,15 @@ export const User = __t.object('User', { username: __t.string(), }); export type User = __Infer; + +export const ViewPkMembership = __t.object('ViewPkMembership', { + id: __t.u64(), + playerId: __t.u64(), +}); +export type ViewPkMembership = __Infer; + +export const ViewPkPlayer = __t.object('ViewPkPlayer', { + id: __t.u64(), + name: __t.string(), +}); +export type ViewPkPlayer = __Infer; diff --git a/crates/bindings-typescript/test-app/src/module_bindings/types/reducers.ts b/crates/bindings-typescript/test-app/src/module_bindings/types/reducers.ts index 1d7e61f99ef..4bdade2ab31 100644 --- a/crates/bindings-typescript/test-app/src/module_bindings/types/reducers.ts +++ b/crates/bindings-typescript/test-app/src/module_bindings/types/reducers.ts @@ -7,5 +7,17 @@ import { type Infer as __Infer } from '../../../../src/index'; // Import all reducer arg schemas import CreatePlayerReducer from '../create_player_reducer'; +import InsertViewPkMembershipReducer from '../insert_view_pk_membership_reducer'; +import InsertViewPkPlayerReducer from '../insert_view_pk_player_reducer'; +import UpdateViewPkPlayerReducer from '../update_view_pk_player_reducer'; export type CreatePlayerParams = __Infer; +export type InsertViewPkMembershipParams = __Infer< + typeof InsertViewPkMembershipReducer +>; +export type InsertViewPkPlayerParams = __Infer< + typeof InsertViewPkPlayerReducer +>; +export type UpdateViewPkPlayerParams = __Infer< + typeof UpdateViewPkPlayerReducer +>; diff --git a/crates/bindings-typescript/test-app/src/module_bindings/update_view_pk_player_reducer.ts b/crates/bindings-typescript/test-app/src/module_bindings/update_view_pk_player_reducer.ts new file mode 100644 index 00000000000..1d11cf14879 --- /dev/null +++ b/crates/bindings-typescript/test-app/src/module_bindings/update_view_pk_player_reducer.ts @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from '../../../src/index'; + +export default { + id: __t.u64(), + name: __t.string(), +}; diff --git a/crates/bindings-typescript/test-app/src/module_bindings/view_pk_membership_table.ts b/crates/bindings-typescript/test-app/src/module_bindings/view_pk_membership_table.ts new file mode 100644 index 00000000000..95b70888921 --- /dev/null +++ b/crates/bindings-typescript/test-app/src/module_bindings/view_pk_membership_table.ts @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from '../../../src/index'; + +export default __t.row({ + id: __t.u64().primaryKey(), + playerId: __t.u64().name('player_id'), +}); diff --git a/crates/bindings-typescript/test-app/src/module_bindings/view_pk_player_table.ts b/crates/bindings-typescript/test-app/src/module_bindings/view_pk_player_table.ts new file mode 100644 index 00000000000..64085ef60e7 --- /dev/null +++ b/crates/bindings-typescript/test-app/src/module_bindings/view_pk_player_table.ts @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from '../../../src/index'; + +export default __t.row({ + id: __t.u64().primaryKey(), + name: __t.string(), +}); diff --git a/crates/bindings-typescript/tests/db_connection.test.ts b/crates/bindings-typescript/tests/db_connection.test.ts index ec17430e41a..719422b698c 100644 --- a/crates/bindings-typescript/tests/db_connection.test.ts +++ b/crates/bindings-typescript/tests/db_connection.test.ts @@ -10,9 +10,10 @@ import { } from '../src'; import { ServerMessage } from '../src/sdk/client_api/types'; import WebsocketTestAdapter from '../src/sdk/websocket_test_adapter'; -import { DbConnection } from '../test-app/src/module_bindings'; +import { DbConnection, tables } from '../test-app/src/module_bindings'; import User from '../test-app/src/module_bindings/user_table'; import { + encodeAllViewPkPlayersRow, anIdentity, bobIdentity, encodePlayer, @@ -80,6 +81,7 @@ function getLastCallReducerRequestId(wsAdapter: WebsocketTestAdapter): number { function getLastSubscribeMessageInfo(wsAdapter: WebsocketTestAdapter): { requestId: number; querySetId: number; + queryStrings: string[]; } { for (let i = wsAdapter.outgoingMessages.length - 1; i >= 0; i--) { const message = wsAdapter.outgoingMessages[i]; @@ -87,6 +89,7 @@ function getLastSubscribeMessageInfo(wsAdapter: WebsocketTestAdapter): { return { requestId: message.value.requestId, querySetId: message.value.querySetId.id, + queryStrings: message.value.queryStrings, }; } } @@ -775,4 +778,116 @@ describe('DbConnection', () => { expect(foundUser!.username).toEqual('sally'); expect(client.db.user.count()).toEqual(2n); }); + + test('it calls onUpdate callback for a query-builder view once PK metadata is propagated', async () => { + const wsAdapter = new WebsocketTestAdapter(); + const client = DbConnection.builder() + .withUri('ws://127.0.0.1:1234') + .withDatabaseName('db') + .withWSFn(wsAdapter.createWebSocketFn.bind(wsAdapter) as any) + .build(); + await client['wsPromise']; + wsAdapter.acceptConnection(); + wsAdapter.sendToClient( + ServerMessage.InitialConnection({ + identity: anIdentity, + token: 'a-token', + connectionId: ConnectionId.random(), + }) + ); + + client.subscriptionBuilder().subscribe(tables.all_view_pk_players); + await Promise.resolve(); + const { querySetId } = getLastSubscribeMessageInfo(wsAdapter); + + const initial = { id: 1n, name: 'before' }; + const updated = { id: 1n, name: 'after' }; + + const viewUpdatedPromise = new Deferred(); + client.db.all_view_pk_players.onUpdate((_ctx, oldRow, newRow) => { + expect(oldRow).toEqual(initial); + expect(newRow).toEqual(updated); + viewUpdatedPromise.resolve(); + }); + + wsAdapter.sendToClient( + ServerMessage.TransactionUpdate({ + querySets: [ + makeQuerySetUpdate( + querySetId, + 'all_view_pk_players', + encodeAllViewPkPlayersRow(initial) + ), + ], + }) + ); + + wsAdapter.sendToClient( + ServerMessage.TransactionUpdate({ + querySets: [ + makeQuerySetUpdate( + querySetId, + 'all_view_pk_players', + encodeAllViewPkPlayersRow(updated), + encodeAllViewPkPlayersRow(initial) + ), + ], + }) + ); + + await viewUpdatedPromise.promise; + }); + + test('it subscribes with query-builder semijoin from table to view on view PK', async () => { + const wsAdapter = new WebsocketTestAdapter(); + const client = DbConnection.builder() + .withUri('ws://127.0.0.1:1234') + .withDatabaseName('db') + .withWSFn(wsAdapter.createWebSocketFn.bind(wsAdapter) as any) + .build(); + await client['wsPromise']; + wsAdapter.acceptConnection(); + wsAdapter.sendToClient( + ServerMessage.InitialConnection({ + identity: anIdentity, + token: 'a-token', + connectionId: ConnectionId.random(), + }) + ); + + client + .subscriptionBuilder() + .subscribe(qb => + qb.view_pk_membership.rightSemijoin( + qb.all_view_pk_players, + (membership, player) => membership.playerId.eq(player.id) + ) + ); + await Promise.resolve(); + + const { queryStrings, querySetId } = getLastSubscribeMessageInfo(wsAdapter); + expect(queryStrings).toEqual([ + 'SELECT "all_view_pk_players".* FROM "view_pk_membership" JOIN "all_view_pk_players" ON "view_pk_membership"."playerId" = "all_view_pk_players"."id"', + ]); + + const joinInsertPromise = new Deferred(); + client.db.all_view_pk_players.onInsert((_ctx, row) => { + expect(row).toEqual({ id: 2n, name: 'joined' }); + joinInsertPromise.resolve(); + }); + + wsAdapter.sendToClient( + ServerMessage.TransactionUpdate({ + querySets: [ + makeQuerySetUpdate( + querySetId, + 'all_view_pk_players', + encodeAllViewPkPlayersRow({ id: 2n, name: 'joined' }) + ), + ], + }) + ); + + await joinInsertPromise.promise; + }); }); diff --git a/crates/bindings-typescript/tests/utils.ts b/crates/bindings-typescript/tests/utils.ts index 4219aeb6013..d975ddddb3a 100644 --- a/crates/bindings-typescript/tests/utils.ts +++ b/crates/bindings-typescript/tests/utils.ts @@ -2,6 +2,7 @@ import BinaryWriter from '../src/lib/binary_writer'; import { Identity } from '../src/lib/identity'; import type { Infer } from '../src/lib/type_builders'; import { RowSizeHint, TableUpdateRows } from '../src/sdk/client_api/types'; +import AllViewPkPlayersRow from '../test-app/src/module_bindings/all_view_pk_players_table'; import PlayerRow from '../test-app/src/module_bindings/player_table'; import { Point } from '../test-app/src/module_bindings/types'; import UserRow from '../test-app/src/module_bindings/user_table'; @@ -28,6 +29,14 @@ export function encodeUser(value: Infer): Uint8Array { return writer.getBuffer(); } +export function encodeAllViewPkPlayersRow( + value: Infer +): Uint8Array { + const writer = new BinaryWriter(1024); + AllViewPkPlayersRow.serialize(writer, value); + return writer.getBuffer(); +} + export function encodeCreatePlayerArgs( name: string, location: Infer From ae2cd51bf4d4f5bdcd7cca9b3a81d9acd9612499 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Tue, 3 Mar 2026 23:08:06 -0800 Subject: [PATCH 4/9] primary keys for query builder views --- crates/bindings-csharp/Codegen/Module.cs | 18 ++- .../src/lib/type_builders.ts | 20 +++ .../src/server/view.test-d.ts | 30 +++-- .../bindings-typescript/src/server/views.ts | 55 +++++++-- .../all_view_pk_players_table.ts | 2 +- .../test-app/src/module_bindings/index.ts | 17 ++- crates/codegen/src/rust.rs | 12 +- crates/codegen/src/typescript.rs | 12 +- crates/core/src/db/relational_db.rs | 11 ++ crates/core/src/db/update.rs | 52 +++++++- .../locking_tx_datastore/committed_state.rs | 5 + .../src/locking_tx_datastore/datastore.rs | 11 +- .../src/locking_tx_datastore/mut_tx.rs | 24 ++++ .../src/locking_tx_datastore/tx_state.rs | 6 +- crates/lib/src/db/raw_def/v10.rs | 2 +- crates/lib/src/db/raw_def/v9.rs | 23 +++- crates/query-builder/src/lib.rs | 7 +- crates/schema/src/auto_migrate.rs | 8 +- crates/schema/src/def.rs | 73 ++++++++++- crates/schema/src/def/validate/v10.rs | 116 +++++++++++++++--- crates/schema/src/def/validate/v9.rs | 93 ++++++++++---- crates/schema/src/schema.rs | 109 +++++++++++++--- crates/smoketests/tests/smoketests/views.rs | 2 +- modules/sdk-test-view-pk-ts/src/index.ts | 2 +- 24 files changed, 607 insertions(+), 103 deletions(-) diff --git a/crates/bindings-csharp/Codegen/Module.cs b/crates/bindings-csharp/Codegen/Module.cs index 8c988c2d902..4e425c6f44e 100644 --- a/crates/bindings-csharp/Codegen/Module.cs +++ b/crates/bindings-csharp/Codegen/Module.cs @@ -1119,12 +1119,15 @@ string makeConstraintFn /// record ViewDeclaration { + private const string QueryViewReturnTag = "__query__"; + public readonly string Name; public readonly string? CanonicalName; public readonly string FullName; public readonly bool IsAnonymous; public readonly bool IsPublic; public readonly bool ReturnsQuery; + public readonly string? QueryRowTypeBSATNName; public readonly TypeUse ReturnType; public readonly EquatableArray Parameters; public readonly Scope Scope; @@ -1190,6 +1193,7 @@ method.ReturnType is INamedTypeSymbol { ReturnsQuery = true; var rowType = TypeUse.Parse(method, queryRowType, diag); + QueryRowTypeBSATNName = rowType.BSATNName; var optType = queryRowType.IsValueType ? "SpacetimeDB.BSATN.ValueOption" : "SpacetimeDB.BSATN.RefOption"; @@ -1199,6 +1203,7 @@ method.ReturnType is INamedTypeSymbol } else { + QueryRowTypeBSATNName = null; ReturnType = TypeUse.Parse(method, method.ReturnType, diag); } Scope = new Scope(methodSyntax.Parent as MemberDeclarationSyntax); @@ -1233,17 +1238,24 @@ method.ReturnType is INamedTypeSymbol ); } - public string GenerateViewDef(uint Index) => - $$$""" + public string GenerateViewDef(uint Index) + { + var returnTypeExpr = + ReturnsQuery && QueryRowTypeBSATNName is { } rowTypeBSATNName + ? $"new global::SpacetimeDB.AlgebraicType.Product([new(\"{QueryViewReturnTag}\", new {rowTypeBSATNName}().GetAlgebraicType(registrar))])" + : $"new {ReturnType.BSATNName}().GetAlgebraicType(registrar)"; + + return $$$""" new global::SpacetimeDB.Internal.RawViewDefV10( SourceName: "{{{Name}}}", Index: {{{Index}}}, IsPublic: {{{IsPublic.ToString().ToLower()}}}, IsAnonymous: {{{IsAnonymous.ToString().ToLower()}}}, Params: [{{{MemberDeclaration.GenerateDefs(Parameters)}}}], - ReturnType: new {{{ReturnType.BSATNName}}}().GetAlgebraicType(registrar) + ReturnType: {{{returnTypeExpr}}} ); """; + } /// /// Generates the class responsible for evaluating a view. diff --git a/crates/bindings-typescript/src/lib/type_builders.ts b/crates/bindings-typescript/src/lib/type_builders.ts index 6bf8ae1ec99..9f5852e50b1 100644 --- a/crates/bindings-typescript/src/lib/type_builders.ts +++ b/crates/bindings-typescript/src/lib/type_builders.ts @@ -1295,6 +1295,14 @@ export class ArrayBuilder> } } +export const queryViewReturnMarker = Symbol('queryViewReturnMarker'); + +export class QueryBuilderViewReturnBuilder< + Row extends TypeBuilder, +> extends ArrayBuilder { + readonly [queryViewReturnMarker] = true as const; +} + export class ByteArrayBuilder extends TypeBuilder< Uint8Array, @@ -3871,6 +3879,18 @@ export const t = { return new ArrayBuilder(e); }, + /** + * Declares that a view returns a query over rows of the given type. + * + * This emits a query-view return marker in the module definition while preserving + * list-valued runtime semantics for view execution. + */ + query>( + rowType: Row + ): QueryBuilderViewReturnBuilder { + return new QueryBuilderViewReturnBuilder(rowType); + }, + enum: enumImpl, /** diff --git a/crates/bindings-typescript/src/server/view.test-d.ts b/crates/bindings-typescript/src/server/view.test-d.ts index f2c220fd6d0..e1271179ff5 100644 --- a/crates/bindings-typescript/src/server/view.test-d.ts +++ b/crates/bindings-typescript/src/server/view.test-d.ts @@ -71,10 +71,11 @@ const spacetime = schema({ personWithMissing, }); +const queryRetValue = t.query(person.rowType); const arrayRetValue = t.array(person.rowType); const optionalPerson = t.option(person.rowType); -spacetime.anonymousView({ name: 'v1', public: true }, arrayRetValue, ctx => { +spacetime.anonymousView({ name: 'v1', public: true }, queryRetValue, ctx => { return ctx.from.person.build(); }); @@ -105,8 +106,23 @@ spacetime.anonymousView( ); spacetime.anonymousView( - { name: 'v2', public: true }, + { name: 'arrayProcedural', public: true }, arrayRetValue, + () => [] +); + +spacetime.anonymousView( + { name: 'arrayCannotReturnQuery', public: true }, + arrayRetValue, + // @ts-expect-error query-builder views must use t.query(...) + ctx => { + return ctx.from.person.build(); + } +); + +spacetime.anonymousView( + { name: 'v2', public: true }, + queryRetValue, // @ts-expect-error returns a query of the wrong type. ctx => { return ctx.from.order.build(); @@ -116,7 +132,7 @@ spacetime.anonymousView( // For queries, we can't return rows with extra fields. spacetime.anonymousView( { name: 'v3', public: true }, - arrayRetValue, + queryRetValue, // @ts-expect-error returns a query of the wrong type. ctx => { return ctx.from.personWithExtra.build(); @@ -126,7 +142,7 @@ spacetime.anonymousView( // Ideally this would fail, since we depend on the field ordering for serialization. spacetime.anonymousView( { name: 'reorderedPerson', public: true }, - arrayRetValue, + queryRetValue, // Comment this out if we can fix the types. // // @ts-expect-error returns a query of the wrong type. ctx => { @@ -137,21 +153,21 @@ spacetime.anonymousView( // Fails because it is missing a field. spacetime.anonymousView( { name: 'missingField', public: true }, - arrayRetValue, + queryRetValue, // @ts-expect-error returns a query of the wrong type. ctx => { return ctx.from.personWithMissing.build(); } ); -spacetime.anonymousView({ name: 'v4', public: true }, arrayRetValue, ctx => { +spacetime.anonymousView({ name: 'v4', public: true }, queryRetValue, ctx => { // @ts-expect-error returns a query of the wrong type. const _invalid = ctx.from.person.where(row => row.id.eq('string')).build(); const _columnEqs = ctx.from.person.where(row => row.id.eq(row.id)).build(); return ctx.from.person.where(row => row.id.eq(5)).build(); }); -spacetime.anonymousView({ name: 'v5', public: true }, arrayRetValue, ctx => { +spacetime.anonymousView({ name: 'v5', public: true }, queryRetValue, ctx => { const _nonIndexedSemijoin = ctx.from.person .where(row => row.id.eq(5)) // @ts-expect-error person_id is not indexed. diff --git a/crates/bindings-typescript/src/server/views.ts b/crates/bindings-typescript/src/server/views.ts index accd0c92563..0a49a6e744c 100644 --- a/crates/bindings-typescript/src/server/views.ts +++ b/crates/bindings-typescript/src/server/views.ts @@ -10,11 +10,13 @@ import type { OptionAlgebraicType } from '../lib/option'; import type { ParamsObj } from '../lib/reducers'; import { type UntypedSchemaDef } from '../lib/schema'; import { + QueryBuilderViewReturnBuilder, RowBuilder, type Infer, type InferSpacetimeTypeOfTypeBuilder, type InferTypeOfRow, type TypeBuilder, + queryViewReturnMarker, } from '../lib/type_builders'; import { bsatnBaseSize, toPascalCase } from '../lib/util'; import type { ReadonlyDbView } from './db_view'; @@ -99,25 +101,32 @@ export type ViewFn< S extends UntypedSchemaDef, Params extends ParamsObj, Ret extends ViewReturnTypeBuilder, -> = - | ((ctx: ViewCtx, params: InferTypeOfRow) => Infer) - | (( +> = Ret extends QueryViewReturnTypeBuilder + ? ( ctx: ViewCtx, params: InferTypeOfRow - ) => RowTypedQuery>, ExtractArrayProduct>); + ) => RowTypedQuery>, ExtractArrayProduct> + : (ctx: ViewCtx, params: InferTypeOfRow) => Infer; export type AnonymousViewFn< S extends UntypedSchemaDef, Params extends ParamsObj, Ret extends ViewReturnTypeBuilder, -> = - | ((ctx: AnonymousViewCtx, params: InferTypeOfRow) => Infer) - | (( +> = Ret extends QueryViewReturnTypeBuilder + ? ( ctx: AnonymousViewCtx, params: InferTypeOfRow - ) => RowTypedQuery>, ExtractArrayProduct>); + ) => RowTypedQuery>, ExtractArrayProduct> + : ( + ctx: AnonymousViewCtx, + params: InferTypeOfRow + ) => Infer; -export type ViewReturnTypeBuilder = +type QueryViewReturnTypeBuilder = QueryBuilderViewReturnBuilder< + TypeBuilder +>; + +type ProceduralViewReturnTypeBuilder = | TypeBuilder< readonly object[], { tag: 'Array'; value: AlgebraicTypeVariants.Product } @@ -127,6 +136,10 @@ export type ViewReturnTypeBuilder = OptionAlgebraicType >; +export type ViewReturnTypeBuilder = + | ProceduralViewReturnTypeBuilder + | QueryViewReturnTypeBuilder; + export function registerView< S extends UntypedSchemaDef, const Anonymous extends boolean, @@ -154,13 +167,33 @@ export function registerView< ctx.registerTypesRecursively(paramsBuilder) ); + const returnTypeDescriptor = returnType as AlgebraicType; + const isQueryBuilderReturn = (ret as any)[queryViewReturnMarker] === true; + const queryRowTypeIsProduct = + returnTypeDescriptor.tag === 'Array' && + (returnTypeDescriptor.value.tag === 'Product' || + (returnTypeDescriptor.value.tag === 'Ref' && + typespace.types[returnTypeDescriptor.value.value]?.tag === 'Product')); + + const moduleReturnType = + isQueryBuilderReturn && queryRowTypeIsProduct + ? AlgebraicType.Product({ + elements: [ + { + name: '__query__', + algebraicType: returnTypeDescriptor.value, + }, + ], + }) + : returnType; + ctx.moduleDef.views.push({ sourceName: exportName, index: (anon ? ctx.anonViews : ctx.views).length, isPublic: opts.public, isAnonymous: anon, params: paramType, - returnType, + returnType: moduleReturnType, }); if (opts.name != null) { @@ -186,7 +219,7 @@ export function registerView< } (anon ? ctx.anonViews : ctx.views).push({ - fn, + fn: fn as any, deserializeParams: ProductType.makeDeserializer(paramType, typespace), serializeReturn: AlgebraicType.makeSerializer(returnType, typespace), returnTypeBaseSize: bsatnBaseSize(typespace, returnType), diff --git a/crates/bindings-typescript/test-app/src/module_bindings/all_view_pk_players_table.ts b/crates/bindings-typescript/test-app/src/module_bindings/all_view_pk_players_table.ts index df99ddc1f3a..64085ef60e7 100644 --- a/crates/bindings-typescript/test-app/src/module_bindings/all_view_pk_players_table.ts +++ b/crates/bindings-typescript/test-app/src/module_bindings/all_view_pk_players_table.ts @@ -11,6 +11,6 @@ import { } from '../../../src/index'; export default __t.row({ - id: __t.u64(), + id: __t.u64().primaryKey(), name: __t.string(), }); diff --git a/crates/bindings-typescript/test-app/src/module_bindings/index.ts b/crates/bindings-typescript/test-app/src/module_bindings/index.ts index f4956327115..8ea1c690d2b 100644 --- a/crates/bindings-typescript/test-app/src/module_bindings/index.ts +++ b/crates/bindings-typescript/test-app/src/module_bindings/index.ts @@ -163,8 +163,21 @@ const tablesSchema = __schema({ all_view_pk_players: __table( { name: 'all_view_pk_players', - indexes: [], - constraints: [], + indexes: [ + { + accessor: 'id', + name: 'all_view_pk_players_id_idx_btree', + algorithm: 'btree', + columns: ['id'], + }, + ], + constraints: [ + { + name: 'all_view_pk_players_id_key', + constraint: 'unique', + columns: ['id'], + }, + ], }, AllViewPkPlayersRow ), 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/codegen/src/typescript.rs b/crates/codegen/src/typescript.rs index 549530451df..810ba802914 100644 --- a/crates/codegen/src/typescript.rs +++ b/crates/codegen/src/typescript.rs @@ -8,7 +8,6 @@ use super::util::{collect_case, print_auto_generated_file_comment, type_ref_name use std::collections::BTreeSet; use std::fmt::{self, Write}; -use std::iter; use std::ops::Deref; use convert_case::{Case, Casing}; @@ -216,9 +215,18 @@ impl Lang for TypeScript { for view in iter_views(module) { let type_ref = view.product_type_ref; let view_name_pascalcase = view.accessor_name.deref().to_case(Case::Pascal); + let view_table = TableDef::from(view.clone()); writeln!(out, "{}: __table({{", view.accessor_name); out.indent(1); - write_table_opts(module, out, type_ref, &view.name, iter::empty(), iter::empty(), false); + write_table_opts( + module, + out, + type_ref, + &view.name, + iter_indexes(&view_table), + iter_constraints(&view_table), + false, + ); out.dedent(1); writeln!(out, "}}, {}Row),", view_name_pascalcase); } diff --git a/crates/core/src/db/relational_db.rs b/crates/core/src/db/relational_db.rs index 4cf13096c96..20d3132d57e 100644 --- a/crates/core/src/db/relational_db.rs +++ b/crates/core/src/db/relational_db.rs @@ -991,6 +991,17 @@ impl RelationalDB { Ok(self.inner.alter_table_access_mut_tx(tx, name, access)?) } + pub(crate) fn alter_table_primary_key( + &self, + tx: &mut MutTx, + table_id: TableId, + primary_key: Option, + ) -> Result<(), DBError> { + Ok(self + .inner + .alter_table_primary_key_mut_tx(tx, table_id, primary_key)?) + } + pub(crate) fn alter_table_row_type( &self, tx: &mut MutTx, diff --git a/crates/core/src/db/update.rs b/crates/core/src/db/update.rs index 896a1325e83..842476b6b8e 100644 --- a/crates/core/src/db/update.rs +++ b/crates/core/src/db/update.rs @@ -5,7 +5,7 @@ use spacetimedb_datastore::locking_tx_datastore::MutTxId; use spacetimedb_lib::db::auth::StTableType; use spacetimedb_lib::identity::AuthCtx; use spacetimedb_lib::AlgebraicValue; -use spacetimedb_primitives::{ColSet, TableId}; +use spacetimedb_primitives::{ColId, ColSet, TableId}; use spacetimedb_schema::auto_migrate::{AutoMigratePlan, ManualMigratePlan, MigratePlan}; use spacetimedb_schema::def::{TableDef, ViewDef}; use spacetimedb_schema::schema::{column_schemas_from_defs, IndexSchema, Schema, SequenceSchema, TableSchema}; @@ -86,6 +86,11 @@ macro_rules! log { }; } +fn view_public_pk_to_backing_pk(view: &ViewDef, public_pk: ColId) -> ColId { + let offset = (if view.is_anonymous { 0 } else { 1 }) + (if view.param_columns.is_empty() { 0 } else { 1 }); + ColId::from(offset + public_pk.idx()) +} + /// Automatically migrate a database. fn auto_migrate_database( stdb: &RelationalDB, @@ -160,7 +165,50 @@ fn auto_migrate_database( let view_id = stdb.view_id_from_name_mut(tx, view_name)?.unwrap(); stdb.drop_view(tx, view_id)?; } - spacetimedb_schema::auto_migrate::AutoMigrateStep::UpdateView(_) => { + spacetimedb_schema::auto_migrate::AutoMigrateStep::UpdateView(view_name) => { + let old_view: &ViewDef = plan.old.expect_lookup(view_name); + let new_view: &ViewDef = plan.new.expect_lookup(view_name); + + if let (Some(old_pk), None) = (old_view.primary_key, new_view.primary_key) { + let table_id = stdb + .table_id_from_name_mut(tx, &old_view.name)? + .ok_or_else(|| anyhow::anyhow!("view backing table `{}` not found", old_view.name))?; + let old_backing_pk = view_public_pk_to_backing_pk(old_view, old_pk); + + // Drop all unique constraints and indexes that include the old PK column. + // This keeps the backing table intact while removing PK semantics. + let constraint_ids = stdb + .schema_for_table_mut(tx, table_id)? + .constraints + .iter() + .filter_map(|constraint| { + constraint + .data + .unique_columns() + .filter(|columns| columns.contains(old_backing_pk)) + .map(|_| constraint.constraint_id) + }) + .collect::>(); + + for constraint_id in constraint_ids { + stdb.drop_constraint(tx, constraint_id)?; + } + + let index_ids = stdb + .schema_for_table_mut(tx, table_id)? + .indexes + .iter() + .filter(|index| index.index_algorithm.columns().iter().any(|col| col == old_backing_pk)) + .map(|index| index.index_id) + .collect::>(); + + for index_id in index_ids { + stdb.drop_index(tx, index_id)?; + } + + stdb.alter_table_primary_key(tx, table_id, None)?; + } + // if we already have to disconnect clients, no need to set // `EvaluateSubscribedViews` as clients will be disconnected anyway if !matches!(res, UpdateResult::RequiresClientDisconnect) { diff --git a/crates/datastore/src/locking_tx_datastore/committed_state.rs b/crates/datastore/src/locking_tx_datastore/committed_state.rs index fc38f821225..4a0207f012d 100644 --- a/crates/datastore/src/locking_tx_datastore/committed_state.rs +++ b/crates/datastore/src/locking_tx_datastore/committed_state.rs @@ -1305,6 +1305,11 @@ impl CommittedState { let table = self.tables.get_mut(&table_id)?; table.with_mut_schema(|s| s.table_access = access); } + // A table's primary key was changed. Change back to the old one. + TableAlterPrimaryKey(table_id, primary_key) => { + let table = self.tables.get_mut(&table_id)?; + table.with_mut_schema(|s| s.primary_key = primary_key); + } // A table's row type was changed. Change back to the old one. // The row representation of old rows hasn't changed, // so it's safe to not rewrite the rows and merely change the type back. diff --git a/crates/datastore/src/locking_tx_datastore/datastore.rs b/crates/datastore/src/locking_tx_datastore/datastore.rs index a08b6386b36..f341cb8d2ce 100644 --- a/crates/datastore/src/locking_tx_datastore/datastore.rs +++ b/crates/datastore/src/locking_tx_datastore/datastore.rs @@ -34,7 +34,7 @@ use spacetimedb_durability::TxOffset; use spacetimedb_lib::{db::auth::StAccess, metrics::ExecutionMetrics}; use spacetimedb_lib::{ConnectionId, Identity}; use spacetimedb_paths::server::SnapshotDirPath; -use spacetimedb_primitives::{ColList, ConstraintId, IndexId, SequenceId, TableId, ViewId}; +use spacetimedb_primitives::{ColId, ColList, ConstraintId, IndexId, SequenceId, TableId, ViewId}; use spacetimedb_sats::{ algebraic_value::de::ValueDeserializer, bsatn, buffer::BufReader, AlgebraicValue, ProductValue, }; @@ -330,6 +330,15 @@ impl Locking { tx.alter_table_access(table_id, access) } + pub fn alter_table_primary_key_mut_tx( + &self, + tx: &mut MutTxId, + table_id: TableId, + primary_key: Option, + ) -> Result<()> { + tx.alter_table_primary_key(table_id, primary_key) + } + pub fn alter_table_row_type_mut_tx( &self, tx: &mut MutTxId, diff --git a/crates/datastore/src/locking_tx_datastore/mut_tx.rs b/crates/datastore/src/locking_tx_datastore/mut_tx.rs index 4c9ec984b84..6f9d01bff69 100644 --- a/crates/datastore/src/locking_tx_datastore/mut_tx.rs +++ b/crates/datastore/src/locking_tx_datastore/mut_tx.rs @@ -1083,6 +1083,30 @@ impl MutTxId { Ok(()) } + /// Set the table primary key of `table_id` to `primary_key`. + pub(crate) fn alter_table_primary_key(&mut self, table_id: TableId, primary_key: Option) -> Result<()> { + // Write to the table in the tx state. + let ((tx_table, ..), (commit_table, ..)) = self.get_or_create_insert_table_mut(table_id)?; + tx_table.with_mut_schema_and_clone(commit_table, |s| s.primary_key = primary_key); + + // Update system tables. + let old_primary_key = self.update_st_table_row(table_id, |st| { + mem::replace(&mut st.table_primary_key, primary_key.map(ColList::new)) + })?; + let old_primary_key = old_primary_key + .map(|pk| { + pk.as_singleton().ok_or_else(|| { + anyhow::anyhow!("table_primary_key should be a single column, found {pk:?}") + }) + }) + .transpose()?; + + // Remember the pending change so we can undo if necessary. + self.push_schema_change(PendingSchemaChange::TableAlterPrimaryKey(table_id, old_primary_key)); + + Ok(()) + } + /// Change the row type of the table identified by `table_id`. /// /// In practice, this should not error, diff --git a/crates/datastore/src/locking_tx_datastore/tx_state.rs b/crates/datastore/src/locking_tx_datastore/tx_state.rs index 945fbdd4612..2fd57db7a58 100644 --- a/crates/datastore/src/locking_tx_datastore/tx_state.rs +++ b/crates/datastore/src/locking_tx_datastore/tx_state.rs @@ -2,7 +2,7 @@ use super::{delete_table::DeleteTable, sequence::Sequence}; use core::ops::RangeBounds; use spacetimedb_data_structures::map::IntMap; use spacetimedb_lib::db::auth::StAccess; -use spacetimedb_primitives::{ColList, ConstraintId, IndexId, SequenceId, TableId}; +use spacetimedb_primitives::{ColId, ColList, ConstraintId, IndexId, SequenceId, TableId}; use spacetimedb_sats::{memory_usage::MemoryUsage, AlgebraicValue}; use spacetimedb_schema::schema::{ColumnSchema, ConstraintSchema, IndexSchema, SequenceSchema}; use spacetimedb_table::{ @@ -119,6 +119,9 @@ pub enum PendingSchemaChange { /// The access of the table with [`TableId`] was changed. /// The old access was stored. TableAlterAccess(TableId, StAccess), + /// The primary key of the table with [`TableId`] was changed. + /// The old primary key was stored. + TableAlterPrimaryKey(TableId, Option), /// The row type of the table with [`TableId`] was changed. /// The old column schemas was stored. /// Only non-representational row-type changes are allowed here, @@ -146,6 +149,7 @@ impl MemoryUsage for PendingSchemaChange { Self::TableRemoved(table_id, table) => table_id.heap_usage() + table.heap_usage(), Self::TableAdded(table_id) => table_id.heap_usage(), Self::TableAlterAccess(table_id, st_access) => table_id.heap_usage() + st_access.heap_usage(), + Self::TableAlterPrimaryKey(table_id, primary_key) => table_id.heap_usage() + primary_key.heap_usage(), Self::TableAlterRowType(table_id, column_schemas) => table_id.heap_usage() + column_schemas.heap_usage(), Self::ConstraintRemoved(table_id, constraint_schema) => { table_id.heap_usage() + constraint_schema.heap_usage() diff --git a/crates/lib/src/db/raw_def/v10.rs b/crates/lib/src/db/raw_def/v10.rs index a801ea286be..d723d52741c 100644 --- a/crates/lib/src/db/raw_def/v10.rs +++ b/crates/lib/src/db/raw_def/v10.rs @@ -498,7 +498,7 @@ pub struct RawViewDefV10 { pub params: ProductType, /// The return type of the view. - /// Either `T`, `Option`, or `Vec` where `T` is a `SpacetimeType`. + /// Either `Option`, `Vec`, or `{ __query__: T }` where `T` is a `SpacetimeType`. /// /// More strictly `T` must be a SATS `ProductType`, /// however this will be validated by the server on publish. diff --git a/crates/lib/src/db/raw_def/v9.rs b/crates/lib/src/db/raw_def/v9.rs index 7d5a03905fa..cad42bdc275 100644 --- a/crates/lib/src/db/raw_def/v9.rs +++ b/crates/lib/src/db/raw_def/v9.rs @@ -122,12 +122,27 @@ impl RawModuleDefV9 { /// Find and return the product type ref for a view in this module def fn type_ref_for_view(&self, view_name: &str) -> Option { + const QUERY_VIEW_RETURN_TAG: &str = "__query__"; + 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()) + let query_product_ref = return_type.as_product().and_then(|product| { + let [field] = product.elements.as_ref() else { + return None; + }; + if !field.has_name(QUERY_VIEW_RETURN_TAG) { + return None; + } + field.algebraic_type.as_ref().copied() + }); + + query_product_ref + .or_else(|| { + return_type + .as_option() + .and_then(|inner| inner.clone().into_ref().ok()) + }) .or_else(|| { return_type .as_array() @@ -523,7 +538,7 @@ pub struct RawViewDefV9 { pub params: ProductType, /// The return type of the view. - /// Either `T`, `Option`, or `Vec` where `T` is a `SpacetimeType`. + /// Either `Option`, `Vec`, or `{ __query__: T }` where `T` is a `SpacetimeType`. /// /// More strictly `T` must be a SATS `ProductType`, /// however this will be validated by the server on publish. diff --git a/crates/query-builder/src/lib.rs b/crates/query-builder/src/lib.rs index 9b5382967c5..62920ab8e7f 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/auto_migrate.rs b/crates/schema/src/auto_migrate.rs index ab4dfc14fb4..2a62eba8e77 100644 --- a/crates/schema/src/auto_migrate.rs +++ b/crates/schema/src/auto_migrate.rs @@ -608,7 +608,13 @@ fn auto_migrate_view<'def>(plan: &mut AutoMigratePlan<'def>, old: &'def ViewDef, }) .collect(); - if old.is_anonymous != new.is_anonymous || incompatible_return_type || incompatible_param_types { + let requires_view_replacement = + old.is_anonymous != new.is_anonymous + || incompatible_return_type + || incompatible_param_types + || (old.primary_key != new.primary_key && new.primary_key.is_some()); + + if requires_view_replacement { plan.steps.push(AutoMigrateStep::AddView(new.key())); plan.steps.push(AutoMigrateStep::RemoveView(old.key())); diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index 2162a9c9abb..a67c80ee7e4 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -774,18 +774,52 @@ impl From for TableDef { let ViewDef { name, is_public, + primary_key, product_type_ref, return_columns, accessor_name, .. } = def; + + let mut indexes = StrMap::default(); + let mut constraints = StrMap::default(); + + if let Some(pk_col) = primary_key { + let pk_column_name = return_columns[pk_col.idx()].name.as_raw().clone(); + let pk_accessor_name = return_columns[pk_col.idx()].accessor_name.clone(); + let index_name = RawIdentifier::new(format!("{name}_{pk_column_name}_idx_btree")); + let constraint_name = RawIdentifier::new(format!("{name}_{pk_column_name}_key")); + + indexes.insert( + index_name.clone(), + IndexDef { + name: index_name.clone(), + source_name: index_name, + accessor_name: Some(pk_accessor_name), + algorithm: IndexAlgorithm::BTree(BTreeAlgorithm { + columns: ColList::new(pk_col), + }), + }, + ); + + constraints.insert( + constraint_name.clone(), + ConstraintDef { + name: constraint_name, + data: ConstraintData::Unique(UniqueConstraintData { + columns: ColSet::from(pk_col), + }), + }, + ); + } + Self { name, product_type_ref, - primary_key: None, + primary_key, columns: return_columns.into_iter().map(ColumnDef::from).collect(), - indexes: <_>::default(), - constraints: <_>::default(), + indexes, + constraints, sequences: <_>::default(), schedule: None, table_type: TableType::User, @@ -1507,12 +1541,21 @@ pub struct ViewDef { pub params_for_generate: ProductTypeDef, /// The return type of the view. - /// Either `Option` or `Vec` where: + /// Either: + /// + /// 1. `Option` for procedural views, + /// 2. `Vec` for procedural views, or + /// 3. `{ __query__: T }` for query-builder views + /// + /// 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 - /// 4. `Vec` refers to [`AlgebraicType::array()`] + /// 4. `Vec` refers to [`AlgebraicType::array()`]. + /// + /// Query-builder view execution is still list-valued at runtime. + /// The `{ __query__: T }` shape is metadata for distinguishing query-builder views. pub return_type: AlgebraicType, /// The return type of the view, formatted for client codegen. @@ -1520,11 +1563,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 returns `Option`, `Vec`, or `{ __query__: T }`, /// 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 rows, if known. + /// + /// This is only populated for query-builder views whose row source is + /// unambiguously mapped to a table with a primary key. + 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. @@ -1559,6 +1608,18 @@ impl From for RawViewDefV9 { fn_ptr: index, .. } = val; + let return_type = match return_type.as_product().and_then(|product| { + let [field] = product.elements.as_ref() else { + return None; + }; + if !field.has_name("__query__") { + return None; + } + field.algebraic_type.as_ref().copied() + }) { + Some(row_type_ref) => AlgebraicType::array(row_type_ref.into()), + None => return_type, + }; RawViewDefV9 { name: name.into(), index: index.into(), diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index a8e8a13cb10..ed0545996a7 100644 --- a/crates/schema/src/def/validate/v10.rs +++ b/crates/schema/src/def/validate/v10.rs @@ -13,6 +13,8 @@ use crate::error::ValidationError; use crate::type_for_generate::ProductTypeDef; use crate::{def::validate::Result, error::TypeLocation}; +const QUERY_VIEW_RETURN_TAG: &str = "__query__"; + // Utitility struct to look up canonical names for tables, functions, and indexes based on the // explicit names provided in the `RawModuleDefV10`. #[derive(Default)] @@ -135,27 +137,27 @@ pub fn validate(def: RawModuleDefV10) -> Result { // Later on, in `check_function_names_are_unique`, we'll transform this into an `IndexMap`. .collect_all_errors::>(); - let views = def - .views() + let tables = def + .tables() .cloned() .into_iter() .flatten() - .map(|view| { + .map(|table| { validator - .validate_view_def(view, &typespace_with_accessor_names) - .map(|view_def| (view_def.name.clone(), view_def)) + .validate_table_def(table, &typespace_with_accessor_names) + .map(|table_def| (table_def.name.clone(), table_def)) }) .collect_all_errors(); - let tables = def - .tables() + let views = def + .views() .cloned() .into_iter() .flatten() - .map(|table| { + .map(|view| { validator - .validate_table_def(table, &typespace_with_accessor_names) - .map(|table_def| (table_def.name.clone(), table_def)) + .validate_view_def(view, &typespace_with_accessor_names, tables.as_ref().ok()) + .map(|view_def| (view_def.name.clone(), view_def)) }) .collect_all_errors(); @@ -667,7 +669,12 @@ impl<'a> ModuleValidatorV10<'a> { }) } - fn validate_view_def(&mut self, view_def: RawViewDefV10, typespace_with_accessor: &Typespace) -> Result { + fn validate_view_def( + &mut self, + view_def: RawViewDefV10, + typespace_with_accessor: &Typespace, + tables: Option<&HashMap>, + ) -> Result { let RawViewDefV10 { source_name: accessor_name, is_public, @@ -684,16 +691,31 @@ impl<'a> ModuleValidatorV10<'a> { }) }; - let product_type_ref = return_type - .as_option() - .and_then(AlgebraicType::as_ref) + let query_builder_product_type_ref = return_type.as_product().and_then(|product| { + let [field] = product.elements.as_ref() else { + return None; + }; + if !field.has_name(QUERY_VIEW_RETURN_TAG) { + return None; + } + field.algebraic_type.as_ref().cloned() + }); + + let (product_type_ref, is_query_builder) = query_builder_product_type_ref + .map(|product_type_ref| (product_type_ref, true)) .or_else(|| { return_type - .as_array() - .map(|array_type| array_type.elem_ty.as_ref()) + .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() + .map(|product_type_ref| (product_type_ref, false)) }) - .cloned() .ok_or_else(invalid_return_type)?; let product_type = self @@ -716,11 +738,17 @@ impl<'a> ModuleValidatorV10<'a> { arg_name, })?; + let return_type_for_generate_ty = if is_query_builder { + AlgebraicType::array(product_type_ref.into()) + } else { + return_type.clone() + }; + let return_type_for_generate = self.core.validate_for_type_use( || TypeLocation::ViewReturn { view_name: accessor_name.clone(), }, - &return_type, + &return_type_for_generate_ty, ); let name = self.core.resolve_function_ident(accessor_name.clone())?; @@ -760,6 +788,25 @@ impl<'a> ModuleValidatorV10<'a> { let (return_type_for_generate, return_columns, param_columns) = (return_type_for_generate, return_columns, param_columns).combine_errors()?; + let primary_key = is_query_builder + .then(|| { + let mut inferred = None; + for table in tables + .into_iter() + .flat_map(|tables| tables.values()) + .filter(|table| table.product_type_ref == product_type_ref) + { + let table_pk = table.primary_key?; + match inferred { + None => inferred = Some(table_pk), + Some(existing_pk) if existing_pk == table_pk => {} + Some(_) => return None, + } + } + inferred + }) + .flatten(); + Ok(ViewDef { name, accessor_name: identifier(accessor_name)?, @@ -774,6 +821,7 @@ impl<'a> ModuleValidatorV10<'a> { return_type, return_type_for_generate, product_type_ref, + primary_key, return_columns, param_columns, }) @@ -1613,6 +1661,38 @@ mod tests { }); } + #[test] + fn query_builder_view_marker_return_type_is_accepted() { + let mut builder = RawModuleDefV10Builder::new(); + + let row_type_ref = builder.add_algebraic_type( + [], + "Player", + AlgebraicType::product([("id", AlgebraicType::U64), ("name", AlgebraicType::String)]), + false, + ); + + builder + .build_table("player", row_type_ref) + .with_primary_key(0) + .with_unique_constraint(0) + .with_index_no_accessor_name(btree(0), "player_id_idx") + .finish(); + + builder.add_view( + "all_players", + 0, + true, + true, + ProductType::unit(), + AlgebraicType::product([("__query__", row_type_ref.into())]), + ); + + let module: ModuleDef = builder.finish().try_into().unwrap(); + let view = module.views.get("all_players").unwrap(); + assert_eq!(view.primary_key, Some(ColId(0))); + } + fn make_case_conversion_builder() -> (RawModuleDefV10Builder, AlgebraicTypeRef) { let mut builder = RawModuleDefV10Builder::new(); diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index 37a40469dcc..9a7746e9102 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -14,6 +14,8 @@ use spacetimedb_lib::ProductType; use spacetimedb_primitives::col_list; use spacetimedb_sats::{bsatn::de::Deserializer, de::DeserializeSeed, WithTypespace}; +const QUERY_VIEW_RETURN_TAG: &str = "__query__"; + /// Validate a `RawModuleDefV9` and convert it into a `ModuleDef`, /// or return a stream of errors if the definition is invalid. pub fn validate(def: RawModuleDefV9) -> Result { @@ -88,6 +90,15 @@ pub fn validate(def: RawModuleDefV9) -> Result { // Later on, in `check_function_names_are_unique`, we'll transform this into an `IndexMap`. .collect_all_errors::>(); + let tables = tables + .into_iter() + .map(|table| { + validator + .validate_table_def(table) + .map(|table_def| (table_def.name.clone(), table_def)) + }) + .collect_all_errors(); + let views = views .into_iter() .map(|view| { @@ -98,20 +109,11 @@ pub fn validate(def: RawModuleDefV9) -> Result { }) .map(|view| { validator - .validate_view_def(view) + .validate_view_def(view, tables.as_ref().ok()) .map(|view_def| (view_def.name.clone(), view_def)) }) .collect_all_errors(); - let tables = tables - .into_iter() - .map(|table| { - validator - .validate_table_def(table) - .map(|table_def| (table_def.name.clone(), table_def)) - }) - .collect_all_errors(); - let row_level_security_raw = row_level_security .into_iter() .map(|rls| (rls.sql.clone(), rls)) @@ -421,7 +423,11 @@ impl ModuleValidatorV9<'_> { } /// Validate a view definition. - fn validate_view_def(&mut self, view_def: RawViewDefV9) -> Result { + fn validate_view_def( + &mut self, + view_def: RawViewDefV9, + tables: Option<&HashMap>, + ) -> Result { let RawViewDefV9 { name, is_public, @@ -438,20 +444,31 @@ impl ModuleValidatorV9<'_> { }) }; - // The possible return types of a view are `Vec` or `Option`, - // 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) + let query_builder_product_type_ref = return_type.as_product().and_then(|product| { + let [field] = product.elements.as_ref() else { + return None; + }; + if !field.has_name(QUERY_VIEW_RETURN_TAG) { + return None; + } + field.algebraic_type.as_ref().cloned() + }); + + let (product_type_ref, is_query_builder) = query_builder_product_type_ref + .map(|product_type_ref| (product_type_ref, true)) .or_else(|| { return_type - .as_array() - .map(|array_type| array_type.elem_ty.as_ref()) + .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() + .map(|product_type_ref| (product_type_ref, false)) }) - .cloned() .ok_or_else(invalid_return_type)?; let product_type = self @@ -474,11 +491,17 @@ impl ModuleValidatorV9<'_> { arg_name, })?; + let return_type_for_generate_ty = if is_query_builder { + AlgebraicType::array(product_type_ref.into()) + } else { + return_type.clone() + }; + let return_type_for_generate = self.core.validate_for_type_use( || TypeLocation::ViewReturn { view_name: name.clone(), }, - &return_type, + &return_type_for_generate_ty, ); let mut view_in_progress = ViewValidator::new( @@ -512,6 +535,25 @@ impl ModuleValidatorV9<'_> { let (name, return_type_for_generate, return_columns, param_columns) = (name, return_type_for_generate, return_columns, param_columns).combine_errors()?; + let primary_key = is_query_builder + .then(|| { + let mut inferred = None; + for table in tables + .into_iter() + .flat_map(|tables| tables.values()) + .filter(|table| table.product_type_ref == product_type_ref) + { + let table_pk = table.primary_key?; + match inferred { + None => inferred = Some(table_pk), + Some(existing_pk) if existing_pk == table_pk => {} + Some(_) => return None, + } + } + inferred + }) + .flatten(); + Ok(ViewDef { name: name.clone(), is_anonymous, @@ -525,6 +567,7 @@ impl ModuleValidatorV9<'_> { return_type, return_type_for_generate, product_type_ref, + primary_key, return_columns, param_columns, accessor_name: name, @@ -676,7 +719,11 @@ impl CoreValidator<'_> { for element in &mut product.elements.iter_mut() { // Convert the element name if it exists if let Some(name) = element.name() { - let new_name = convert(name.clone(), case_policy); + let new_name = if &**name == QUERY_VIEW_RETURN_TAG { + name.to_string() + } else { + convert(name.clone(), case_policy) + }; element.name = Some(new_name.into()); } // Recursively convert the element's type diff --git a/crates/schema/src/schema.rs b/crates/schema/src/schema.rs index 3a64a66aff7..f2fb759b554 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, + BTreeAlgorithm, ColumnDef, ConstraintData, ConstraintDef, IndexAlgorithm, IndexDef, ModuleDef, ModuleDefLookup, + 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, .. @@ -785,18 +786,44 @@ impl TableSchema { is_anonymous: *is_anonymous, }; + let mut indexes = Vec::new(); + let mut constraints = Vec::new(); + + if let Some(pk_col) = *primary_key { + let pk_col_name = return_columns[pk_col.idx()].name.as_raw().clone(); + + indexes.push(IndexSchema { + index_id: IndexId::SENTINEL, + table_id: TableId::SENTINEL, + index_name: RawIdentifier::new(format!("{name}_{pk_col_name}_idx_btree")), + index_algorithm: IndexAlgorithm::BTree(BTreeAlgorithm { + columns: ColList::new(pk_col), + }), + alias: None, + }); + + constraints.push(ConstraintSchema { + table_id: TableId::SENTINEL, + constraint_id: ConstraintId::SENTINEL, + constraint_name: RawIdentifier::new(format!("{name}_{pk_col_name}_key")), + data: ConstraintData::Unique(UniqueConstraintData { + columns: ColSet::from(pk_col), + }), + }); + } + TableSchema::new( TableId::SENTINEL, TableName::new(name.clone()), Some(view_info), columns, - vec![], - vec![], + indexes, + constraints, vec![], StTableType::User, table_access, None, - None, + *primary_key, false, None, ) @@ -842,6 +869,7 @@ impl TableSchema { name, is_public, is_anonymous, + primary_key, param_columns, return_columns, accessor_name, @@ -851,12 +879,12 @@ 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 meta_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; + meta_index_name += "_"; + meta_index_name += name; columns.push(ColumnSchema { table_id: TableId::SENTINEL, col_pos: columns.len().into(), @@ -883,23 +911,72 @@ impl TableSchema { .map(|(col_pos, schema)| ColumnSchema { col_pos, ..schema }), ); - let index_schema = |col_list: ColList| { - index_name += "idx_btree"; + let index_schema = |name: RawIdentifier, col_list: ColList| { IndexSchema { index_id: IndexId::SENTINEL, table_id: TableId::SENTINEL, - index_name: RawIdentifier::new(index_name), + index_name: name, index_algorithm: IndexAlgorithm::BTree(col_list.into()), alias: None, } }; - let indexes = match meta_cols { - 1 => vec![index_schema(col_list![0])], - 2 => vec![index_schema(col_list![0, 1])], + let mut indexes = match meta_cols { + 1 => vec![index_schema(RawIdentifier::new(format!("{meta_index_name}_idx_btree")), col_list![0])], + 2 => vec![index_schema( + RawIdentifier::new(format!("{meta_index_name}_idx_btree")), + col_list![0, 1], + )], _ => vec![], }; + let mut constraints = Vec::new(); + let mut backing_table_primary_key = None; + + if let Some(public_pk_col) = *primary_key { + let physical_pk_col = ColId::from(meta_cols + public_pk_col.idx()); + backing_table_primary_key = Some(physical_pk_col); + + let pk_col_name = return_columns[public_pk_col.idx()].name.as_raw().clone(); + + // Keep a dedicated index on the public PK column to support joins in subscription queries. + // If there are no metadata columns, the unique PK index below already serves this purpose. + if meta_cols > 0 { + indexes.push(index_schema( + RawIdentifier::new(format!("{name}_{pk_col_name}_idx_btree")), + ColList::new(physical_pk_col), + )); + } + + // Enforce uniqueness per subscriber-argument partition. + let mut unique_cols = ColList::with_capacity((meta_cols + 1) as _); + let mut unique_col_names = Vec::with_capacity(meta_cols + 1); + if !is_anonymous { + unique_cols.push(ColId(0)); + unique_col_names.push("sender".to_string()); + } + if !param_columns.is_empty() { + unique_cols.push(ColId(unique_cols.len() as u16)); + unique_col_names.push("arg_id".to_string()); + } + unique_cols.push(physical_pk_col); + unique_col_names.push(pk_col_name.to_string()); + + let unique_suffix = unique_col_names.join("_"); + let unique_index_name = RawIdentifier::new(format!("{name}_{unique_suffix}_idx_btree")); + let unique_constraint_name = RawIdentifier::new(format!("{name}_{unique_suffix}_key")); + + indexes.push(index_schema(unique_index_name, unique_cols.clone())); + constraints.push(ConstraintSchema { + table_id: TableId::SENTINEL, + constraint_id: ConstraintId::SENTINEL, + constraint_name: unique_constraint_name, + data: ConstraintData::Unique(UniqueConstraintData { + columns: ColSet::from(unique_cols), + }), + }); + } + let table_access = if *is_public { StAccess::Public } else { @@ -918,12 +995,12 @@ impl TableSchema { Some(view_info), columns, indexes, - vec![], + constraints, vec![], StTableType::User, table_access, None, - None, + backing_table_primary_key, false, Some(accessor_name.clone()), ) diff --git a/crates/smoketests/tests/smoketests/views.rs b/crates/smoketests/tests/smoketests/views.rs index b03864fb10c..6ac170017da 100644 --- a/crates/smoketests/tests/smoketests/views.rs +++ b/crates/smoketests/tests/smoketests/views.rs @@ -22,7 +22,7 @@ export const my_player = spacetimedb.view( export const all_players = spacetimedb.anonymousView( { public: true }, - t.array(playerState.rowType), + t.query(playerState.rowType), ctx => ctx.from.playerState ); diff --git a/modules/sdk-test-view-pk-ts/src/index.ts b/modules/sdk-test-view-pk-ts/src/index.ts index 9bedf6d72d5..3845b4e5460 100644 --- a/modules/sdk-test-view-pk-ts/src/index.ts +++ b/modules/sdk-test-view-pk-ts/src/index.ts @@ -21,7 +21,7 @@ export default spacetimedb; export const all_view_pk_players = spacetimedb.view( { public: true }, - t.array(viewPkPlayer.rowType), + t.query(viewPkPlayer.rowType), ctx => ctx.from.viewPkPlayer ); From ae3a7e8314a8df7bf62941cbb92c0e72ef363777 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Tue, 3 Mar 2026 23:31:34 -0800 Subject: [PATCH 5/9] fix lints --- .../bindings-typescript/src/server/views.ts | 5 +---- crates/core/src/db/relational_db.rs | 4 +--- .../src/locking_tx_datastore/mut_tx.rs | 5 ++--- crates/lib/src/db/raw_def/v9.rs | 6 +----- crates/schema/src/auto_migrate.rs | 9 ++++---- crates/schema/src/schema.rs | 21 ++++++++++--------- sdks/rust/tests/view-pk-client/src/main.rs | 2 +- 7 files changed, 21 insertions(+), 31 deletions(-) diff --git a/crates/bindings-typescript/src/server/views.ts b/crates/bindings-typescript/src/server/views.ts index 0a49a6e744c..4b5fc8344c2 100644 --- a/crates/bindings-typescript/src/server/views.ts +++ b/crates/bindings-typescript/src/server/views.ts @@ -117,10 +117,7 @@ export type AnonymousViewFn< ctx: AnonymousViewCtx, params: InferTypeOfRow ) => RowTypedQuery>, ExtractArrayProduct> - : ( - ctx: AnonymousViewCtx, - params: InferTypeOfRow - ) => Infer; + : (ctx: AnonymousViewCtx, params: InferTypeOfRow) => Infer; type QueryViewReturnTypeBuilder = QueryBuilderViewReturnBuilder< TypeBuilder diff --git a/crates/core/src/db/relational_db.rs b/crates/core/src/db/relational_db.rs index 20d3132d57e..600f92108e2 100644 --- a/crates/core/src/db/relational_db.rs +++ b/crates/core/src/db/relational_db.rs @@ -997,9 +997,7 @@ impl RelationalDB { table_id: TableId, primary_key: Option, ) -> Result<(), DBError> { - Ok(self - .inner - .alter_table_primary_key_mut_tx(tx, table_id, primary_key)?) + Ok(self.inner.alter_table_primary_key_mut_tx(tx, table_id, primary_key)?) } pub(crate) fn alter_table_row_type( diff --git a/crates/datastore/src/locking_tx_datastore/mut_tx.rs b/crates/datastore/src/locking_tx_datastore/mut_tx.rs index 6f9d01bff69..d484587891c 100644 --- a/crates/datastore/src/locking_tx_datastore/mut_tx.rs +++ b/crates/datastore/src/locking_tx_datastore/mut_tx.rs @@ -1095,9 +1095,8 @@ impl MutTxId { })?; let old_primary_key = old_primary_key .map(|pk| { - pk.as_singleton().ok_or_else(|| { - anyhow::anyhow!("table_primary_key should be a single column, found {pk:?}") - }) + pk.as_singleton() + .ok_or_else(|| anyhow::anyhow!("table_primary_key should be a single column, found {pk:?}")) }) .transpose()?; diff --git a/crates/lib/src/db/raw_def/v9.rs b/crates/lib/src/db/raw_def/v9.rs index cad42bdc275..6c4e2f11a7e 100644 --- a/crates/lib/src/db/raw_def/v9.rs +++ b/crates/lib/src/db/raw_def/v9.rs @@ -138,11 +138,7 @@ impl RawModuleDefV9 { }); query_product_ref - .or_else(|| { - return_type - .as_option() - .and_then(|inner| inner.clone().into_ref().ok()) - }) + .or_else(|| return_type.as_option().and_then(|inner| inner.clone().into_ref().ok())) .or_else(|| { return_type .as_array() diff --git a/crates/schema/src/auto_migrate.rs b/crates/schema/src/auto_migrate.rs index 2a62eba8e77..595ecbac711 100644 --- a/crates/schema/src/auto_migrate.rs +++ b/crates/schema/src/auto_migrate.rs @@ -608,11 +608,10 @@ fn auto_migrate_view<'def>(plan: &mut AutoMigratePlan<'def>, old: &'def ViewDef, }) .collect(); - let requires_view_replacement = - old.is_anonymous != new.is_anonymous - || incompatible_return_type - || incompatible_param_types - || (old.primary_key != new.primary_key && new.primary_key.is_some()); + let requires_view_replacement = old.is_anonymous != new.is_anonymous + || incompatible_return_type + || incompatible_param_types + || (old.primary_key != new.primary_key && new.primary_key.is_some()); if requires_view_replacement { plan.steps.push(AutoMigrateStep::AddView(new.key())); diff --git a/crates/schema/src/schema.rs b/crates/schema/src/schema.rs index f2fb759b554..6895a9252fd 100644 --- a/crates/schema/src/schema.rs +++ b/crates/schema/src/schema.rs @@ -911,18 +911,19 @@ impl TableSchema { .map(|(col_pos, schema)| ColumnSchema { col_pos, ..schema }), ); - let index_schema = |name: RawIdentifier, col_list: ColList| { - IndexSchema { - index_id: IndexId::SENTINEL, - table_id: TableId::SENTINEL, - index_name: name, - index_algorithm: IndexAlgorithm::BTree(col_list.into()), - alias: None, - } + let index_schema = |name: RawIdentifier, col_list: ColList| IndexSchema { + index_id: IndexId::SENTINEL, + table_id: TableId::SENTINEL, + index_name: name, + index_algorithm: IndexAlgorithm::BTree(col_list.into()), + alias: None, }; let mut indexes = match meta_cols { - 1 => vec![index_schema(RawIdentifier::new(format!("{meta_index_name}_idx_btree")), col_list![0])], + 1 => vec![index_schema( + RawIdentifier::new(format!("{meta_index_name}_idx_btree")), + col_list![0], + )], 2 => vec![index_schema( RawIdentifier::new(format!("{meta_index_name}_idx_btree")), col_list![0, 1], @@ -956,7 +957,7 @@ impl TableSchema { unique_col_names.push("sender".to_string()); } if !param_columns.is_empty() { - unique_cols.push(ColId(unique_cols.len() as u16)); + unique_cols.push(ColId(unique_cols.len())); unique_col_names.push("arg_id".to_string()); } unique_cols.push(physical_pk_col); diff --git a/sdks/rust/tests/view-pk-client/src/main.rs b/sdks/rust/tests/view-pk-client/src/main.rs index 9ef194c3033..a315c21edb1 100644 --- a/sdks/rust/tests/view-pk-client/src/main.rs +++ b/sdks/rust/tests/view-pk-client/src/main.rs @@ -1,9 +1,9 @@ mod module_bindings; use module_bindings::*; -use spacetimedb_sdk::{error::InternalError, DbContext, Table}; #[cfg(feature = "expect_view_pk_on_update")] use spacetimedb_sdk::TableWithPrimaryKey; +use spacetimedb_sdk::{error::InternalError, DbContext, Table}; use test_counter::TestCounter; const LOCALHOST: &str = "http://localhost:3000"; From 59bcced4a9320c7836e26efd33f055b2fff2c7d5 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 4 Mar 2026 11:53:38 -0800 Subject: [PATCH 6/9] fix csharp codegen --- crates/bindings-csharp/Codegen/Module.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bindings-csharp/Codegen/Module.cs b/crates/bindings-csharp/Codegen/Module.cs index 4e425c6f44e..050063db543 100644 --- a/crates/bindings-csharp/Codegen/Module.cs +++ b/crates/bindings-csharp/Codegen/Module.cs @@ -1242,7 +1242,7 @@ public string GenerateViewDef(uint Index) { var returnTypeExpr = ReturnsQuery && QueryRowTypeBSATNName is { } rowTypeBSATNName - ? $"new global::SpacetimeDB.AlgebraicType.Product([new(\"{QueryViewReturnTag}\", new {rowTypeBSATNName}().GetAlgebraicType(registrar))])" + ? $"new global::SpacetimeDB.BSATN.AlgebraicType.Product([new global::SpacetimeDB.BSATN.AggregateElement(\"{QueryViewReturnTag}\", new {rowTypeBSATNName}().GetAlgebraicType(registrar))])" : $"new {ReturnType.BSATNName}().GetAlgebraicType(registrar)"; return $$$""" From da628a2110ddfa83f580b5a85f8d45d74ee5f499 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 4 Mar 2026 13:04:34 -0800 Subject: [PATCH 7/9] fix tests --- pnpm-lock.yaml | 6 ++++ pnpm-workspace.yaml | 1 + .../view-pk-client/Program.cs | 36 +++---------------- 3 files changed, 12 insertions(+), 31 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 195c53848e4..b30b9d4bdca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -288,6 +288,12 @@ importers: specifier: workspace:^ version: link:../../crates/bindings-typescript + modules/sdk-test-view-pk-ts: + dependencies: + spacetimedb: + specifier: workspace:^ + version: link:../../crates/bindings-typescript + templates/angular-ts: dependencies: '@angular/common': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 68ba113176a..40dd2375876 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -18,5 +18,6 @@ packages: - 'templates/chat-react-ts/spacetimedb' - 'modules/sdk-test-connect-disconnect-ts' - 'modules/sdk-test-procedure-ts' + - 'modules/sdk-test-view-pk-ts' - 'modules/sdk-test-ts' - 'docs' diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/Program.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/Program.cs index 43d9d8219fd..0852a1379a7 100644 --- a/sdks/csharp/examples~/regression-tests/view-pk-client/Program.cs +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/Program.cs @@ -10,7 +10,7 @@ const string DBNAME = "view-pk-tests"; const int TIMEOUT_SECONDS = 20; -DbConnection Connect(Action onConnect) +DbConnection Connect(DbConnectionBuilder.ConnectCallback onConnect) { return DbConnection.Builder() .WithUri(HOST) @@ -73,21 +73,8 @@ void RunOnUpdateTest() onUpdateCalled = true; }; - ctx.Procedures.InsertViewPkPlayer(1UL, "before", (_, result) => - { - if (!result.IsSuccess) - { - throw result.Error!; - } - }); - - ctx.Procedures.UpdateViewPkPlayer(1UL, "after", (_, result) => - { - if (!result.IsSuccess) - { - throw result.Error!; - } - }); + ctx.Reducers.InsertViewPkPlayer(1UL, "before"); + ctx.Reducers.UpdateViewPkPlayer(1UL, "after"); }) .OnError((_, err) => { @@ -126,21 +113,8 @@ void RunJoinQueryBuilderTest() onInsertCalled = true; }; - ctx.Procedures.InsertViewPkPlayer(2UL, "joined", (_, result) => - { - if (!result.IsSuccess) - { - throw result.Error!; - } - }); - - ctx.Procedures.InsertViewPkMembership(1UL, 2UL, (_, result) => - { - if (!result.IsSuccess) - { - throw result.Error!; - } - }); + ctx.Reducers.InsertViewPkPlayer(2UL, "joined"); + ctx.Reducers.InsertViewPkMembership(1UL, 2UL); }) .OnError((_, err) => { From 44c6eb86023c8f87a1898c5fb7213606cd6f6561 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 4 Mar 2026 15:04:13 -0800 Subject: [PATCH 8/9] regen bindings --- .../module_bindings/SpacetimeDBClient.g.cs | 2 +- sdks/rust/tests/test.rs | 7 +-- sdks/rust/tests/view-pk-client/Cargo.toml | 3 -- sdks/rust/tests/view-pk-client/src/main.rs | 8 ---- .../all_view_pk_players_table.rs | 48 +++++++++++++++++++ .../view-pk-client/src/module_bindings/mod.rs | 7 +-- 6 files changed, 55 insertions(+), 20 deletions(-) diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs index e6d99f9b15b..03779ea51af 100644 --- a/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs @@ -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.0 (commit 6a6b5a6616f0578aa641bc0689691f953b13feb8). +// This was generated using spacetimedb cli version 2.0.3 (commit afb08d8dfdd5409df366a021008b75c06fe375e2). #nullable enable diff --git a/sdks/rust/tests/test.rs b/sdks/rust/tests/test.rs index 64c9714556b..358c0ed6898 100644 --- a/sdks/rust/tests/test.rs +++ b/sdks/rust/tests/test.rs @@ -504,11 +504,8 @@ macro_rules! view_pk_tests { .with_client(CLIENT) .with_language("rust") .with_bindings_dir("src/module_bindings") - .with_compile_command("cargo build --features expect_view_pk_on_update") - .with_run_command(format!( - "cargo run --features expect_view_pk_on_update -- {}", - subcommand - )) + .with_compile_command("cargo build") + .with_run_command(format!("cargo run -- {}", subcommand)) .build() } diff --git a/sdks/rust/tests/view-pk-client/Cargo.toml b/sdks/rust/tests/view-pk-client/Cargo.toml index 19131e5fa7c..f872b1e7f17 100644 --- a/sdks/rust/tests/view-pk-client/Cargo.toml +++ b/sdks/rust/tests/view-pk-client/Cargo.toml @@ -4,9 +4,6 @@ version.workspace = true edition.workspace = true license-file = "LICENSE" -[features] -expect_view_pk_on_update = [] - [dependencies] spacetimedb-sdk = { path = "../.." } test-counter = { path = "../test-counter" } diff --git a/sdks/rust/tests/view-pk-client/src/main.rs b/sdks/rust/tests/view-pk-client/src/main.rs index a315c21edb1..9349d83ce39 100644 --- a/sdks/rust/tests/view-pk-client/src/main.rs +++ b/sdks/rust/tests/view-pk-client/src/main.rs @@ -1,7 +1,6 @@ mod module_bindings; use module_bindings::*; -#[cfg(feature = "expect_view_pk_on_update")] use spacetimedb_sdk::TableWithPrimaryKey; use spacetimedb_sdk::{error::InternalError, DbContext, Table}; use test_counter::TestCounter; @@ -56,7 +55,6 @@ fn connect_then( conn } -#[cfg(feature = "expect_view_pk_on_update")] fn subscribe_these_then( ctx: &impl RemoteDbContext, queries: &[&str], @@ -68,7 +66,6 @@ fn subscribe_these_then( .subscribe(queries); } -#[cfg(feature = "expect_view_pk_on_update")] fn exec_view_pk_on_update() { let test_counter = TestCounter::new(); let mut on_update = Some(test_counter.add_test("on_update")); @@ -104,11 +101,6 @@ fn exec_view_pk_on_update() { test_counter.wait_for_all(); } -#[cfg(not(feature = "expect_view_pk_on_update"))] -fn exec_view_pk_on_update() { - panic!("This test must be run with --features expect_view_pk_on_update"); -} - fn exec_view_pk_join_query_builder() { let test_counter = TestCounter::new(); let mut joined_insert = Some(test_counter.add_test("join_insert")); 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 c4213d6a729..03ac17bce74 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 8cb2038f8553b92cea512a8e697ad0525c56aecd). +// This was generated using spacetimedb cli version 2.0.3 (commit afb08d8dfdd5409df366a021008b75c06fe375e2). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; @@ -132,8 +132,9 @@ impl __sdk::DbUpdate for DbUpdate { 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); + 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 } From e573ed9b853ed3a926ffe1ea60f00117891c935a Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 4 Mar 2026 15:57:49 -0800 Subject: [PATCH 9/9] more bindings regen --- .../module_bindings/SpacetimeDBClient.g.cs | 2 +- sdks/rust/tests/view-pk-client/README.md | 11 +++++++++++ .../module_bindings/all_view_pk_players_type.rs | 13 ------------- .../insert_view_pk_membership_type.rs | 16 ---------------- .../insert_view_pk_player_type.rs | 16 ---------------- .../view-pk-client/src/module_bindings/mod.rs | 10 +--------- .../update_view_pk_player_type.rs | 16 ---------------- 7 files changed, 13 insertions(+), 71 deletions(-) create mode 100644 sdks/rust/tests/view-pk-client/README.md delete mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/all_view_pk_players_type.rs delete mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_membership_type.rs delete mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_player_type.rs delete mode 100644 sdks/rust/tests/view-pk-client/src/module_bindings/update_view_pk_player_type.rs diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs index 03779ea51af..5c8ee801a92 100644 --- a/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs @@ -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 afb08d8dfdd5409df366a021008b75c06fe375e2). +// This was generated using spacetimedb cli version 2.0.3 (commit 44c6eb86023c8f87a1898c5fb7213606cd6f6561). #nullable enable 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..1c1e7329a7b --- /dev/null +++ b/sdks/rust/tests/view-pk-client/README.md @@ -0,0 +1,11 @@ +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 +``` +I \ No newline at end of file diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/all_view_pk_players_type.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/all_view_pk_players_type.rs deleted file mode 100644 index 8965a07fa67..00000000000 --- a/sdks/rust/tests/view-pk-client/src/module_bindings/all_view_pk_players_type.rs +++ /dev/null @@ -1,13 +0,0 @@ -// 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 AllViewPkPlayers {} - -impl __sdk::InModule for AllViewPkPlayers { - type Module = super::RemoteModule; -} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_membership_type.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_membership_type.rs deleted file mode 100644 index df07dbfea17..00000000000 --- a/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_membership_type.rs +++ /dev/null @@ -1,16 +0,0 @@ -// 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 InsertViewPkMembership { - pub id: u64, - pub player_id: u64, -} - -impl __sdk::InModule for InsertViewPkMembership { - type Module = super::RemoteModule; -} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_player_type.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_player_type.rs deleted file mode 100644 index 64a50975905..00000000000 --- a/sdks/rust/tests/view-pk-client/src/module_bindings/insert_view_pk_player_type.rs +++ /dev/null @@ -1,16 +0,0 @@ -// 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 InsertViewPkPlayer { - pub id: u64, - pub name: String, -} - -impl __sdk::InModule for InsertViewPkPlayer { - type Module = super::RemoteModule; -} 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 03ac17bce74..85c277b7abc 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,32 +1,24 @@ // 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 afb08d8dfdd5409df366a021008b75c06fe375e2). +// This was generated using spacetimedb cli version 2.0.3 (commit 44c6eb86023c8f87a1898c5fb7213606cd6f6561). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; pub mod all_view_pk_players_table; -pub mod all_view_pk_players_type; pub mod insert_view_pk_membership_reducer; -pub mod insert_view_pk_membership_type; pub mod insert_view_pk_player_reducer; -pub mod insert_view_pk_player_type; pub mod update_view_pk_player_reducer; -pub mod update_view_pk_player_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 all_view_pk_players_type::AllViewPkPlayers; pub use insert_view_pk_membership_reducer::insert_view_pk_membership; -pub use insert_view_pk_membership_type::InsertViewPkMembership; pub use insert_view_pk_player_reducer::insert_view_pk_player; -pub use insert_view_pk_player_type::InsertViewPkPlayer; pub use update_view_pk_player_reducer::update_view_pk_player; -pub use update_view_pk_player_type::UpdateViewPkPlayer; pub use view_pk_membership_table::*; pub use view_pk_membership_type::ViewPkMembership; pub use view_pk_player_table::*; diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/update_view_pk_player_type.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/update_view_pk_player_type.rs deleted file mode 100644 index ed00e519826..00000000000 --- a/sdks/rust/tests/view-pk-client/src/module_bindings/update_view_pk_player_type.rs +++ /dev/null @@ -1,16 +0,0 @@ -// 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 UpdateViewPkPlayer { - pub id: u64, - pub name: String, -} - -impl __sdk::InModule for UpdateViewPkPlayer { - type Module = super::RemoteModule; -}