From 5384635545fdbf548ea55c94e2b55f3ae488f5c8 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Fri, 22 May 2026 15:58:17 -0700 Subject: [PATCH 1/5] Add primary key support for procedural views to rust and ts modules --- Cargo.lock | 21 + Cargo.toml | 2 + crates/bindings-macro/src/view.rs | 84 ++ .../src/lib/autogen/types.ts | 9 + crates/bindings-typescript/src/lib/schema.ts | 7 + .../bindings-typescript/src/server/schema.ts | 17 +- .../src/server/view.test-d.ts | 13 + .../bindings-typescript/src/server/views.ts | 126 +++ crates/bindings/src/rt.rs | 15 + crates/bindings/tests/ui/views.rs | 22 +- crates/bindings/tests/ui/views.stderr | 240 ++--- crates/lib/src/db/raw_def/v10.rs | 58 ++ crates/schema/src/auto_migrate.rs | 6 +- crates/schema/src/def.rs | 33 +- crates/schema/src/def/validate/v10.rs | 96 +- crates/schema/src/error.rs | 11 + crates/schema/src/schema.rs | 8 + crates/smoketests/modules/Cargo.lock | 14 + crates/smoketests/modules/Cargo.toml | 2 + .../Cargo.toml | 11 + .../src/lib.rs | 19 + .../views-primary-key-auto-migrate/Cargo.toml | 11 + .../views-primary-key-auto-migrate/src/lib.rs | 14 + crates/smoketests/tests/smoketests/views.rs | 30 + modules/sdk-test-procedural-view-pk-ts/.npmrc | 1 + .../package.json | 14 + .../src/index.ts | 52 ++ .../tsconfig.json | 26 + .../sdk-test-procedural-view-pk/Cargo.toml | 14 + .../sdk-test-procedural-view-pk/src/lib.rs | 56 ++ pnpm-lock.yaml | 6 + pnpm-workspace.yaml | 1 + .../procedural-view-pk-client/Cargo.toml | 42 + .../tests/procedural-view-pk-client/README.md | 10 + .../procedural-view-pk-client/src/lib.rs | 14 + .../procedural-view-pk-client/src/main.rs | 23 + .../module_bindings/insert_left_reducer.rs | 72 ++ .../module_bindings/insert_right_reducer.rs | 72 ++ .../src/module_bindings/left_source_table.rs | 157 ++++ .../src/module_bindings/left_source_type.rs | 57 ++ .../src/module_bindings/mod.rs | 869 ++++++++++++++++++ .../src/module_bindings/right_source_table.rs | 157 ++++ .../src/module_bindings/right_source_type.rs | 57 ++ .../module_bindings/sender_left_view_table.rs | 157 ++++ .../sender_right_view_table.rs | 157 ++++ .../module_bindings/update_left_reducer.rs | 72 ++ .../src/test_handlers.rs | 223 +++++ sdks/rust/tests/test.rs | 38 + .../view-pk-client/src/module_bindings/mod.rs | 2 +- .../tests/view-pk-client/src/test_handlers.rs | 11 +- 50 files changed, 3107 insertions(+), 122 deletions(-) create mode 100644 crates/smoketests/modules/views-primary-key-auto-migrate-updated/Cargo.toml create mode 100644 crates/smoketests/modules/views-primary-key-auto-migrate-updated/src/lib.rs create mode 100644 crates/smoketests/modules/views-primary-key-auto-migrate/Cargo.toml create mode 100644 crates/smoketests/modules/views-primary-key-auto-migrate/src/lib.rs create mode 100644 modules/sdk-test-procedural-view-pk-ts/.npmrc create mode 100644 modules/sdk-test-procedural-view-pk-ts/package.json create mode 100644 modules/sdk-test-procedural-view-pk-ts/src/index.ts create mode 100644 modules/sdk-test-procedural-view-pk-ts/tsconfig.json create mode 100644 modules/sdk-test-procedural-view-pk/Cargo.toml create mode 100644 modules/sdk-test-procedural-view-pk/src/lib.rs create mode 100644 sdks/rust/tests/procedural-view-pk-client/Cargo.toml create mode 100644 sdks/rust/tests/procedural-view-pk-client/README.md create mode 100644 sdks/rust/tests/procedural-view-pk-client/src/lib.rs create mode 100644 sdks/rust/tests/procedural-view-pk-client/src/main.rs create mode 100644 sdks/rust/tests/procedural-view-pk-client/src/module_bindings/insert_left_reducer.rs create mode 100644 sdks/rust/tests/procedural-view-pk-client/src/module_bindings/insert_right_reducer.rs create mode 100644 sdks/rust/tests/procedural-view-pk-client/src/module_bindings/left_source_table.rs create mode 100644 sdks/rust/tests/procedural-view-pk-client/src/module_bindings/left_source_type.rs create mode 100644 sdks/rust/tests/procedural-view-pk-client/src/module_bindings/mod.rs create mode 100644 sdks/rust/tests/procedural-view-pk-client/src/module_bindings/right_source_table.rs create mode 100644 sdks/rust/tests/procedural-view-pk-client/src/module_bindings/right_source_type.rs create mode 100644 sdks/rust/tests/procedural-view-pk-client/src/module_bindings/sender_left_view_table.rs create mode 100644 sdks/rust/tests/procedural-view-pk-client/src/module_bindings/sender_right_view_table.rs create mode 100644 sdks/rust/tests/procedural-view-pk-client/src/module_bindings/update_left_reducer.rs create mode 100644 sdks/rust/tests/procedural-view-pk-client/src/test_handlers.rs diff --git a/Cargo.lock b/Cargo.lock index f952227d74f..19741975d2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5757,6 +5757,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "procedural-view-pk-client" +version = "2.3.0" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "env_logger 0.10.2", + "spacetimedb-sdk", + "test-counter", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", +] + [[package]] name = "procedure-client" version = "2.3.0" @@ -7153,6 +7167,13 @@ dependencies = [ "spacetimedb 2.3.0", ] +[[package]] +name = "sdk-test-procedural-view-pk" +version = "0.1.0" +dependencies = [ + "spacetimedb 2.3.0", +] + [[package]] name = "sdk-test-procedure-module" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2968240b375..baf31d875f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ members = [ "modules/sdk-test-view", "modules/sdk-test-case-conversion", "modules/sdk-test-view-pk", + "modules/sdk-test-procedural-view-pk", "modules/sdk-test-event-table", "sdks/rust/tests/test-client", "sdks/rust/tests/test-counter", @@ -58,6 +59,7 @@ members = [ "sdks/rust/tests/view-client", "sdks/rust/tests/case-conversion-client", "sdks/rust/tests/view-pk-client", + "sdks/rust/tests/procedural-view-pk-client", "sdks/rust/tests/event-table-client", "tools/ci", "tools/upgrade-version", diff --git a/crates/bindings-macro/src/view.rs b/crates/bindings-macro/src/view.rs index 63bed482f74..d0af0ccfe65 100644 --- a/crates/bindings-macro/src/view.rs +++ b/crates/bindings-macro/src/view.rs @@ -12,15 +12,49 @@ use crate::util::{check_duplicate_msg, match_meta}; pub(crate) struct ViewArgs { name: Option, accessor: Ident, + primary_key: Option, #[allow(unused)] public: bool, } +/// Argument accepted by `#[view(primary_key = ...)]`. +/// +/// Both identifier and string literal syntax is supported. +enum ViewPrimaryKeyArg { + Ident(Ident), + Literal(LitStr), +} + +impl ViewPrimaryKeyArg { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + if input.peek(LitStr) { + input.parse().map(Self::Literal) + } else { + input.parse().map(Self::Ident) + } + } + + fn name(&self) -> String { + match self { + Self::Ident(ident) => ident.unraw().to_string(), + Self::Literal(lit) => lit.value(), + } + } + + fn ident(&self) -> Option<&Ident> { + match self { + Self::Ident(ident) => Some(ident), + Self::Literal(_) => None, + } + } +} + impl ViewArgs { /// Parse `#[view(accessor = ..., public)]` where both `name` and `public` are required. pub(crate) fn parse(input: TokenStream, func_ident: &Ident) -> syn::Result { let mut name = None; let mut accessor = None; + let mut primary_key = None; let mut public = None; syn::meta::parser(|meta| { match_meta!(match meta { @@ -36,6 +70,10 @@ impl ViewArgs { check_duplicate_msg(&accessor, &meta, "`accessor` already specified")?; accessor = Some(meta.value()?.parse()?); } + sym::primary_key => { + check_duplicate_msg(&primary_key, &meta, "`primary_key` already specified")?; + primary_key = Some(ViewPrimaryKeyArg::parse(meta.value()?)?); + } }); Ok(()) }) @@ -51,6 +89,7 @@ impl ViewArgs { .ok_or_else(|| syn::Error::new(Span::call_site(), "views must be `public`, e.g. `#[view(public)]`"))?; Ok(Self { name, + primary_key, public: true, accessor, }) @@ -74,6 +113,29 @@ fn extract_impl_query_inner(ty: &syn::Type) -> Option<&syn::Type> { None } +/// If `ty` is a supported view return type, returns the row type `T`. +fn extract_view_return_row_type(ty: &syn::Type) -> Option<&syn::Type> { + if let Some(inner) = extract_impl_query_inner(ty) { + return Some(inner); + } + + let syn::Type::Path(path) = ty else { + return None; + }; + + let seg = path.path.segments.last()?; + if !matches!(seg.ident.to_string().as_str(), "Vec" | "Option" | "RawQuery") { + return None; + } + let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { + return None; + }; + let Some(syn::GenericArgument::Type(inner)) = args.args.first() else { + return None; + }; + Some(inner) +} + pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Result { let vis = &original_function.vis; let func_name = &original_function.sig.ident; @@ -221,6 +283,24 @@ pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Resu }; let eff_ret_ty = &effective_ret_ty; + let primary_key_column_name = args.primary_key.as_ref().map(ViewPrimaryKeyArg::name); + let primary_key_field_check = args + .primary_key + .as_ref() + .and_then(ViewPrimaryKeyArg::ident) + .zip(extract_view_return_row_type(ret_ty)) + .map(|(primary_key, row_ty)| { + quote! { + const _: () = { + fn _assert_view_primary_key_column #lt_params (__row: &#row_ty) #lt_where_clause { + let _ = &__row.#primary_key; + } + }; + } + }); + let primary_key_column_const = primary_key_column_name + .as_ref() + .map(|primary_key| quote! { const VIEW_PRIMARY_KEY_COLUMNS: &'static [&'static str] = &[#primary_key]; }); Ok(quote! { #emitted_fn @@ -243,6 +323,8 @@ pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Resu } }; + #primary_key_field_check + impl #func_name { fn invoke(__ctx: #ctx_ty, __args: &[u8]) -> Vec { spacetimedb::rt::ViewDispatcher::<#ctx_ty>::invoke::<_, _, _>(#func_name, __ctx, __args) @@ -266,6 +348,8 @@ pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Resu /// The pointer for invoking this function const INVOKE: Self::Invoke = #func_name::invoke; + #primary_key_column_const + /// The return type of this function fn return_type( ts: &mut impl spacetimedb::sats::typespace::TypespaceBuilder diff --git a/crates/bindings-typescript/src/lib/autogen/types.ts b/crates/bindings-typescript/src/lib/autogen/types.ts index 0ce535eca0f..ef80ed6c953 100644 --- a/crates/bindings-typescript/src/lib/autogen/types.ts +++ b/crates/bindings-typescript/src/lib/autogen/types.ts @@ -358,6 +358,9 @@ export const RawModuleDefV10Section = __t.enum('RawModuleDefV10Section', { get ExplicitNames() { return ExplicitNames; }, + get ViewPrimaryKeys() { + return __t.array(RawViewPrimaryKeyDefV10); + }, }); export type RawModuleDefV10Section = __Infer; @@ -642,6 +645,12 @@ export const RawViewDefV9 = __t.object('RawViewDefV9', { }); export type RawViewDefV9 = __Infer; +export const RawViewPrimaryKeyDefV10 = __t.object('RawViewPrimaryKeyDefV10', { + viewSourceName: __t.string(), + columns: __t.array(__t.string()), +}); +export type RawViewPrimaryKeyDefV10 = __Infer; + export const ReducerDef = __t.object('ReducerDef', { name: __t.string(), get args() { diff --git a/crates/bindings-typescript/src/lib/schema.ts b/crates/bindings-typescript/src/lib/schema.ts index be9edc9e113..93fbc2c66d3 100644 --- a/crates/bindings-typescript/src/lib/schema.ts +++ b/crates/bindings-typescript/src/lib/schema.ts @@ -194,6 +194,7 @@ export class ModuleContext { schedules: [], procedures: [], views: [], + viewPrimaryKeys: [], lifeCycleReducers: [], caseConversionPolicy: { tag: 'SnakeCase' }, explicitNames: { @@ -220,6 +221,12 @@ export class ModuleContext { push(module.reducers && { tag: 'Reducers', value: module.reducers }); push(module.procedures && { tag: 'Procedures', value: module.procedures }); push(module.views && { tag: 'Views', value: module.views }); + push( + module.viewPrimaryKeys && { + tag: 'ViewPrimaryKeys', + value: module.viewPrimaryKeys, + } + ); push(module.schedules && { tag: 'Schedules', value: module.schedules }); push( module.lifeCycleReducers && { diff --git a/crates/bindings-typescript/src/server/schema.ts b/crates/bindings-typescript/src/server/schema.ts index b9eb258762b..cba25982156 100644 --- a/crates/bindings-typescript/src/server/schema.ts +++ b/crates/bindings-typescript/src/server/schema.ts @@ -38,6 +38,7 @@ import { type ViewFn, type ViewOpts, type ViewReturnTypeBuilder, + type ValidateViewPrimaryKey, type Views, } from './views'; import type { UntypedTableDef } from '../lib/table'; @@ -347,7 +348,11 @@ export class Schema implements ModuleDefaultExport { view>( opts: ViewOpts, ret: Ret, - fn: F + fn: F, + // Compile-time-only guard: this rest parameter is `[]` for valid return + // builders, but becomes a required error tuple when a returned row builder + // marks more than one column with `.primaryKey()`. + ..._: ValidateViewPrimaryKey ): ViewExport { return makeViewExport(this.#ctx, opts, {}, ret, fn); } @@ -380,7 +385,15 @@ export class Schema implements ModuleDefaultExport { anonymousView< Ret extends ViewReturnTypeBuilder, F extends AnonymousViewFn, - >(opts: ViewOpts, ret: Ret, fn: F): ViewExport { + >( + opts: ViewOpts, + ret: Ret, + fn: F, + // Compile-time-only guard: this rest parameter is `[]` for valid return + // builders, but becomes a required error tuple when a returned row builder + // marks more than one column with `.primaryKey()`. + ..._: ValidateViewPrimaryKey + ): ViewExport { return makeAnonViewExport(this.#ctx, opts, {}, ret, fn); } diff --git a/crates/bindings-typescript/src/server/view.test-d.ts b/crates/bindings-typescript/src/server/view.test-d.ts index 1ce372f3190..3336dd8710a 100644 --- a/crates/bindings-typescript/src/server/view.test-d.ts +++ b/crates/bindings-typescript/src/server/view.test-d.ts @@ -73,11 +73,24 @@ const spacetime = schema({ const arrayRetValue = t.array(person.rowType); const optionalPerson = t.option(person.rowType); +const multiplePrimaryKeyRows = t.array( + t.row('MultiplePrimaryKeyRows', { + id: t.u32().primaryKey(), + name: t.string().primaryKey(), + }) +); spacetime.anonymousView({ name: 'v1', public: true }, arrayRetValue, ctx => { return ctx.from.person.build(); }); +// @ts-expect-error views can have at most one primaryKey column on the returned row type. +spacetime.anonymousView( + { name: 'multiplePrimaryRows', public: true }, + multiplePrimaryKeyRows, + () => [] +); + spacetime.anonymousView( { name: 'optionalPerson', public: true }, optionalPerson, diff --git a/crates/bindings-typescript/src/server/views.ts b/crates/bindings-typescript/src/server/views.ts index accd0c92563..cd43fade0a4 100644 --- a/crates/bindings-typescript/src/server/views.ts +++ b/crates/bindings-typescript/src/server/views.ts @@ -10,10 +10,15 @@ import type { OptionAlgebraicType } from '../lib/option'; import type { ParamsObj } from '../lib/reducers'; import { type UntypedSchemaDef } from '../lib/schema'; import { + ArrayBuilder, + OptionBuilder, RowBuilder, + type ColumnBuilder, + type ColumnMetadata, type Infer, type InferSpacetimeTypeOfTypeBuilder, type InferTypeOfRow, + type RowObj, type TypeBuilder, } from '../lib/type_builders'; import { bsatnBaseSize, toPascalCase } from '../lib/util'; @@ -90,6 +95,77 @@ export type ViewOpts = { type FlattenedArray = T extends readonly (infer E)[] ? E : never; +// Compile-time mirror of `viewReturnRow` below. Views currently return either +// `array(row(...))` or `option(row(...))`; this extracts the row object type +// from those builders so we can inspect column metadata at the type level. +// Non-row returns collapse to `never`, which makes the primary-key validation +// below a no-op for unsupported shapes. +type ViewReturnRow = + Ret extends ArrayBuilder + ? Element extends RowBuilder + ? Row + : never + : Ret extends OptionBuilder + ? Value extends RowBuilder + ? Row + : never + : never; + +// Produces a union of the returned row's column names marked with +// `.primaryKey()`. For example, `{ id: t.u32().primaryKey(), name: t.string() }` +// becomes `"id"`, while two marked columns becomes `"id" | "name"`. +type PrimaryKeyColumnNames = { + [K in keyof Row & string]: Row[K] extends ColumnBuilder + ? M extends { isPrimaryKey: true } + ? K + : never + : never; +}[keyof Row & string]; + +// Standard conditional-type trick for distinguishing a single type from a +// union. We use it because zero or one primary-key column is valid, but a union +// of two or more column names means the row builder marked multiple primary +// keys. +type IsUnion = [T] extends [never] + ? false + : T extends any + ? [U] extends [T] + ? false + : true + : false; + +// In generic code, row keys may widen from literal names like "id" | "name" +// to plain `string`. That means "unknown column name", not "multiple primary +// keys", so avoid a false-positive type error and rely on the runtime check. +type HasMultiplePrimaryKeys = + string extends PrimaryKeyColumnNames + ? false + : IsUnion>; + +type MultiplePrimaryKeyColumns = + PrimaryKeyColumnNames>; + +type ERROR_view_return_type_can_have_at_most_one_primaryKey< + Columns extends string, +> = { + _primaryKeyColumns: Columns; + _fix: 'Remove primaryKey() from all but one column on the returned row type'; +}; + +// Used as a rest parameter type on `Schema.view` and `Schema.anonymousView`. +// Valid return builders produce `[]`, so callers pass no extra arguments. If +// the returned row has multiple `.primaryKey()` columns, this becomes a +// one-element tuple containing an explanatory error type, which makes the +// normal three-argument call fail to type-check. +export type ValidateViewPrimaryKey = + HasMultiplePrimaryKeys> extends true + ? [ + error: ERROR_view_return_type_can_have_at_most_one_primaryKey< + MultiplePrimaryKeyColumns + >, + ] + : []; + // // If we allowed functions to return either. // type ViewReturn = // | Infer @@ -163,6 +239,22 @@ export function registerView< returnType, }); + // Runtime counterpart to `ValidateViewPrimaryKey`: the type-level check gives + // users an early diagnostic in normal code, but this still protects dynamic + // or widened builders and is the source of the raw module-def metadata. + const primaryKeyColumns = viewPrimaryKeyColumns(ret); + if (primaryKeyColumns.length > 1) { + throw new TypeError( + `View '${exportName}' can have at most one primaryKey() column on its returned row type; found ${primaryKeyColumns.join(', ')}` + ); + } + if (primaryKeyColumns.length === 1) { + ctx.moduleDef.viewPrimaryKeys.push({ + viewSourceName: exportName, + columns: primaryKeyColumns, + }); + } + if (opts.name != null) { ctx.moduleDef.explicitNames.entries.push({ tag: 'Function', @@ -193,6 +285,40 @@ export function registerView< }); } +// Inspect the returned row builder and collect the column property names marked +// with `.primaryKey()`. These names are the TypeScript row-builder keys, which +// are also the raw column names in the module definition emitted by the TS SDK. +function viewPrimaryKeyColumns(ret: ViewReturnTypeBuilder): string[] { + const row = viewReturnRow(ret); + if (row == null) { + return []; + } + + return Object.entries(row.row) + .filter( + ( + entry + ): entry is [string, ColumnBuilder>] => + entry[1].columnMetadata.isPrimaryKey === true + ) + .map(([name]) => name); +} + +// Views can return either `array(row(...))` or `option(row(...))`. The primary +// key marker lives on the inner `RowBuilder`, so unwrap those two supported +// shapes and ignore anything else. +function viewReturnRow( + ret: ViewReturnTypeBuilder +): RowBuilder | undefined { + if (ret instanceof ArrayBuilder && ret.element instanceof RowBuilder) { + return ret.element; + } + if (ret instanceof OptionBuilder && ret.value instanceof RowBuilder) { + return ret.value; + } + return undefined; +} + type ViewInfo = { fn: F; deserializeParams: Deserializer; diff --git a/crates/bindings/src/rt.rs b/crates/bindings/src/rt.rs index d6d55eba5f4..f3413105e9b 100644 --- a/crates/bindings/src/rt.rs +++ b/crates/bindings/src/rt.rs @@ -166,6 +166,11 @@ pub trait FnInfo: ExplicitNames { /// A description of the parameter names of the function. const ARG_NAMES: &'static [Option<&'static str>]; + /// The source/accessor names of this view's primary key columns. + /// + /// Currently only views use this metadata. + const VIEW_PRIMARY_KEY_COLUMNS: &'static [&'static str] = &[]; + /// The function to invoke. const INVOKE: Self::Invoke; @@ -826,6 +831,11 @@ where module .inner .add_view(I::NAME, module.views.len(), true, false, params, return_type); + if !I::VIEW_PRIMARY_KEY_COLUMNS.is_empty() { + module + .inner + .add_view_primary_key(I::NAME, I::VIEW_PRIMARY_KEY_COLUMNS.iter().copied()); + } module.views.push(I::INVOKE); module.inner.add_explicit_names(I::explicit_names()); @@ -845,6 +855,11 @@ where module .inner .add_view(I::NAME, module.views_anon.len(), true, true, params, return_type); + if !I::VIEW_PRIMARY_KEY_COLUMNS.is_empty() { + module + .inner + .add_view_primary_key(I::NAME, I::VIEW_PRIMARY_KEY_COLUMNS.iter().copied()); + } module.views_anon.push(I::INVOKE); module.inner.add_explicit_names(I::explicit_names()); diff --git a/crates/bindings/tests/ui/views.rs b/crates/bindings/tests/ui/views.rs index 06ba581d84d..3bd9055bc76 100644 --- a/crates/bindings/tests/ui/views.rs +++ b/crates/bindings/tests/ui/views.rs @@ -1,4 +1,6 @@ -use spacetimedb::{reducer, table, view, AnonymousViewContext, Identity, Query, ReducerContext, ViewContext}; +use spacetimedb::{ + reducer, table, view, AnonymousViewContext, Identity, Query, ReducerContext, SpacetimeType, ViewContext, +}; #[table(accessor = test)] struct Test { @@ -195,4 +197,22 @@ fn view_nonexistent_table(ctx: &ViewContext) -> impl Query { ctx.from.xyz().build() } +/// The declared view primary key must refer to a column in the returned row type. +#[view(accessor = view_primary_key_missing_column, public, primary_key = missing_identity)] +fn view_primary_key_missing_column(_: &ViewContext) -> Vec { + vec![] +} + +#[derive(SpacetimeType)] +struct CustomAccessorViewRow { + #[sats(name = "identity")] + renamed_identity: Identity, +} + +/// The declared view primary key must use the Rust accessor/source name, not the canonical column name. +#[view(accessor = view_primary_key_uses_canonical_name, public, primary_key = identity)] +fn view_primary_key_uses_canonical_name(_: &ViewContext) -> Vec { + vec![] +} + fn main() {} diff --git a/crates/bindings/tests/ui/views.stderr b/crates/bindings/tests/ui/views.stderr index 1baf379ceea..f9b5140b2ae 100644 --- a/crates/bindings/tests/ui/views.stderr +++ b/crates/bindings/tests/ui/views.stderr @@ -1,110 +1,110 @@ error: views must be `public`, e.g. `#[view(public)]` - --> tests/ui/views.rs:69:1 + --> tests/ui/views.rs:71:1 | -69 | #[view(accessor = view_def_no_public)] +71 | #[view(accessor = view_def_no_public)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) error: `public` already specified - --> tests/ui/views.rs:75:48 + --> tests/ui/views.rs:77:48 | -75 | #[view(accessor = view_def_dup_public, public, public)] +77 | #[view(accessor = view_def_dup_public, public, public)] | ^^^^^^ error: expected string literal - --> tests/ui/views.rs:81:45 + --> tests/ui/views.rs:83:45 | -81 | #[view(accessor = view_def_dup_name, name = view_def_dup_name, public)] +83 | #[view(accessor = view_def_dup_name, name = view_def_dup_name, public)] | ^^^^^^^^^^^^^^^^^ -error: expected one of: `name`, `public`, `accessor` - --> tests/ui/views.rs:87:53 +error: expected one of: `name`, `public`, `accessor`, `primary_key` + --> tests/ui/views.rs:89:53 | -87 | #[view(accessor = view_def_unsupported_arg, public, anonymous)] +89 | #[view(accessor = view_def_unsupported_arg, public, anonymous)] | ^^^^^^^^^ error: Views must always have a context parameter: `&ViewContext` or `&AnonymousViewContext` - --> tests/ui/views.rs:94:1 + --> tests/ui/views.rs:96:1 | -94 | fn view_def_no_context() -> Vec { +96 | fn view_def_no_context() -> Vec { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: The first parameter of a view must be a context parameter: `&ViewContext` or `&AnonymousViewContext`; passed by reference - --> tests/ui/views.rs:106:38 + --> tests/ui/views.rs:108:38 | -106 | fn view_def_pass_context_by_value(_: ViewContext) -> Vec { +108 | fn view_def_pass_context_by_value(_: ViewContext) -> Vec { | ^^^^^^^^^^^ error: Views do not take parameters other than `&ViewContext` or `&AnonymousViewContext` - --> tests/ui/views.rs:112:1 + --> tests/ui/views.rs:114:1 | -112 | fn view_def_wrong_context_position(_: &u32, _: &ViewContext) -> Vec { +114 | fn view_def_wrong_context_position(_: &u32, _: &ViewContext) -> Vec { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: views must return `Vec` or `Option` where `T` is a `SpacetimeType` - --> tests/ui/views.rs:118:1 + --> tests/ui/views.rs:120:1 | -118 | fn view_def_no_return(_: &ViewContext) {} +120 | fn view_def_no_return(_: &ViewContext) {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Views do not take parameters other than `&ViewContext` or `&AnonymousViewContext` - --> tests/ui/views.rs:136:1 + --> tests/ui/views.rs:138:1 | -136 | fn sched_table_view(_: &ViewContext, _args: ScheduledTable) -> Vec { +138 | fn sched_table_view(_: &ViewContext, _args: ScheduledTable) -> Vec { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error[E0425]: cannot find type `ScheduledTable` in this scope - --> tests/ui/views.rs:136:45 + --> tests/ui/views.rs:138:45 | -136 | fn sched_table_view(_: &ViewContext, _args: ScheduledTable) -> Vec { +138 | fn sched_table_view(_: &ViewContext, _args: ScheduledTable) -> Vec { | ^^^^^^^^^^^^^^ not found in this scope error[E0425]: cannot find type `T` in this scope - --> tests/ui/views.rs:194:60 + --> tests/ui/views.rs:196:60 | -194 | fn view_nonexistent_table(ctx: &ViewContext) -> impl Query { +196 | fn view_nonexistent_table(ctx: &ViewContext) -> impl Query { | ^ not found in this scope | help: you might be missing a type parameter | -194 | fn view_nonexistent_table(ctx: &ViewContext) -> impl Query { +196 | fn view_nonexistent_table(ctx: &ViewContext) -> impl Query { | +++ error[E0425]: cannot find type `T` in this scope - --> tests/ui/views.rs:194:60 + --> tests/ui/views.rs:196:60 | -194 | fn view_nonexistent_table(ctx: &ViewContext) -> impl Query { +196 | fn view_nonexistent_table(ctx: &ViewContext) -> impl Query { | ^ not found in this scope error[E0277]: the trait bound `spacetimedb::rt::ViewKind: ViewKindTrait` is not satisfied - --> tests/ui/views.rs:99:1 - | -99 | #[view(accessor = view_def_wrong_context, public)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `ViewKindTrait` is not implemented for `spacetimedb::rt::ViewKind` - | + --> tests/ui/views.rs:101:1 + | +101 | #[view(accessor = view_def_wrong_context, public)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `ViewKindTrait` is not implemented for `spacetimedb::rt::ViewKind` + | help: the following other types implement trait `ViewKindTrait` - --> src/rt.rs - | - | impl ViewKindTrait for ViewKind { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `spacetimedb::rt::ViewKind` + --> src/rt.rs + | + | impl ViewKindTrait for ViewKind { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `spacetimedb::rt::ViewKind` ... - | impl ViewKindTrait for ViewKind { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `spacetimedb::rt::ViewKind` - = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) + | impl ViewKindTrait for ViewKind { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `spacetimedb::rt::ViewKind` + = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0276]: impl has stricter requirements than trait - --> tests/ui/views.rs:99:1 - | -99 | #[view(accessor = view_def_wrong_context, public)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ impl has extra requirement `spacetimedb::rt::ViewKind: ViewKindTrait` - | - = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) + --> tests/ui/views.rs:101:1 + | +101 | #[view(accessor = view_def_wrong_context, public)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ impl has extra requirement `spacetimedb::rt::ViewKind: ViewKindTrait` + | + = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0599]: no method named `iter` found for reference `&test__ViewHandle` in the current scope - --> tests/ui/views.rs:15:34 + --> tests/ui/views.rs:17:34 | -15 | for _ in read_only.db.test().iter() {} +17 | for _ in read_only.db.test().iter() {} | ^^^^ method not found in `&test__ViewHandle` | = help: items from traits can only be used if the trait is implemented and in scope @@ -113,9 +113,9 @@ error[E0599]: no method named `iter` found for reference `&test__ViewHandle` in candidate #2: `spacetimedb::Table` error[E0599]: no method named `insert` found for reference `&test__ViewHandle` in the current scope - --> tests/ui/views.rs:22:25 + --> tests/ui/views.rs:24:25 | -22 | read_only.db.test().insert(Test { id: 0, x: 0 }); +24 | read_only.db.test().insert(Test { id: 0, x: 0 }); | ^^^^^^ method not found in `&test__ViewHandle` | = help: items from traits can only be used if the trait is implemented and in scope @@ -127,9 +127,9 @@ error[E0599]: no method named `insert` found for reference `&test__ViewHandle` i candidate #5: `spacetimedb::Table` error[E0599]: no method named `try_insert` found for reference `&test__ViewHandle` in the current scope - --> tests/ui/views.rs:29:25 + --> tests/ui/views.rs:31:25 | -29 | read_only.db.test().try_insert(Test { id: 0, x: 0 }); +31 | read_only.db.test().try_insert(Test { id: 0, x: 0 }); | ^^^^^^^^^^ | = help: items from traits can only be used if the trait is implemented and in scope @@ -143,9 +143,9 @@ help: there is a method `try_into` with a similar name, but with different argum | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error[E0599]: no method named `delete` found for reference `&test__ViewHandle` in the current scope - --> tests/ui/views.rs:36:25 + --> tests/ui/views.rs:38:25 | -36 | read_only.db.test().delete(Test { id: 0, x: 0 }); +38 | read_only.db.test().delete(Test { id: 0, x: 0 }); | ^^^^^^ method not found in `&test__ViewHandle` | = help: items from traits can only be used if the trait is implemented and in scope @@ -154,38 +154,38 @@ error[E0599]: no method named `delete` found for reference `&test__ViewHandle` i candidate #2: `spacetimedb::Table` error[E0599]: no method named `delete` found for struct `UniqueColumnReadOnly` in the current scope - --> tests/ui/views.rs:43:30 + --> tests/ui/views.rs:45:30 | -43 | read_only.db.test().id().delete(&0); +45 | read_only.db.test().id().delete(&0); | ^^^^^^ method not found in `UniqueColumnReadOnly` error[E0599]: no method named `update` found for struct `UniqueColumnReadOnly` in the current scope - --> tests/ui/views.rs:50:30 + --> tests/ui/views.rs:52:30 | -50 | read_only.db.test().id().update(Test { id: 0, x: 0 }); +52 | read_only.db.test().id().update(Test { id: 0, x: 0 }); | ^^^^^^ method not found in `UniqueColumnReadOnly` error[E0599]: no method named `delete` found for struct `RangedIndexReadOnly` in the current scope - --> tests/ui/views.rs:57:29 + --> tests/ui/views.rs:59:29 | -57 | read_only.db.test().x().delete(0u32..); +59 | read_only.db.test().x().delete(0u32..); | ^^^^^^ method not found in `RangedIndexReadOnly` error[E0599]: no function or associated item named `register` found for struct `ViewRegistrar` in the current scope - --> tests/ui/views.rs:99:1 - | -99 | #[view(accessor = view_def_wrong_context, public)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function or associated item not found in `ViewRegistrar` - | - = note: the function or associated item was found for - - `ViewRegistrar` - - `ViewRegistrar` - = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) + --> tests/ui/views.rs:101:1 + | +101 | #[view(accessor = view_def_wrong_context, public)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function or associated item not found in `ViewRegistrar` + | + = note: the function or associated item was found for + - `ViewRegistrar` + - `ViewRegistrar` + = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: The first parameter of a `#[view]` must be `&ViewContext` or `&AnonymousViewContext` - --> tests/ui/views.rs:100:31 + --> tests/ui/views.rs:102:31 | -100 | fn view_def_wrong_context(_: &ReducerContext) -> Vec { +102 | fn view_def_wrong_context(_: &ReducerContext) -> Vec { | ^^^^^^^^^^^^^^ the trait `ViewContextArg` is not implemented for `ReducerContext` | help: the following other types implement trait `ViewContextArg` @@ -197,20 +197,20 @@ help: the following other types implement trait `ViewContextArg` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `AnonymousViewContext` error[E0599]: no function or associated item named `invoke` found for struct `ViewDispatcher` in the current scope - --> tests/ui/views.rs:99:1 - | -99 | #[view(accessor = view_def_wrong_context, public)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function or associated item not found in `ViewDispatcher` - | - = note: the function or associated item was found for - - `ViewDispatcher` - - `ViewDispatcher` - = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) + --> tests/ui/views.rs:101:1 + | +101 | #[view(accessor = view_def_wrong_context, public)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function or associated item not found in `ViewDispatcher` + | + = note: the function or associated item was found for + - `ViewDispatcher` + - `ViewDispatcher` + = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: invalid view signature - --> tests/ui/views.rs:121:1 + --> tests/ui/views.rs:123:1 | -121 | #[view(accessor = view_def_wrong_return, public)] +123 | #[view(accessor = view_def_wrong_return, public)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid | = help: the trait `spacetimedb::rt::View<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ViewContext) -> Player {view_def_wrong_return}` @@ -230,15 +230,15 @@ note: required by a bound in `ViewRegistrar::::register` = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: Views must return `Vec` or `Option` where `T` is a `SpacetimeType` - --> tests/ui/views.rs:122:46 + --> tests/ui/views.rs:124:46 | -122 | fn view_def_wrong_return(_: &ViewContext) -> Player { +124 | fn view_def_wrong_return(_: &ViewContext) -> Player { | ^^^^^^ unsatisfied trait bound | help: the trait `ViewReturn` is not implemented for `Player` - --> tests/ui/views.rs:61:1 + --> tests/ui/views.rs:63:1 | - 61 | struct Player { + 63 | struct Player { | ^^^^^^^^^^^^^ = help: the following other types implement trait `ViewReturn`: FromWhere @@ -250,9 +250,9 @@ help: the trait `ViewReturn` is not implemented for `Player` spacetimedb::spacetimedb_query_builder::Table error[E0277]: invalid view signature - --> tests/ui/views.rs:121:1 + --> tests/ui/views.rs:123:1 | -121 | #[view(accessor = view_def_wrong_return, public)] +123 | #[view(accessor = view_def_wrong_return, public)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid | = help: the trait `spacetimedb::rt::View<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ViewContext) -> Player {view_def_wrong_return}` @@ -272,9 +272,9 @@ note: required by a bound in `ViewDispatcher::::invoke` = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: invalid anonymous view signature - --> tests/ui/views.rs:129:1 + --> tests/ui/views.rs:131:1 | -129 | #[view(accessor = view_def_returns_not_a_spacetime_type, public)] +131 | #[view(accessor = view_def_returns_not_a_spacetime_type, public)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid | = help: the trait `AnonymousView<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a AnonymousViewContext) -> Option {view_def_returns_not_a_spacetime_type}` @@ -294,15 +294,15 @@ note: required by a bound in `ViewRegistrar::::register` = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `NotSpacetimeType: SpacetimeType` is not satisfied - --> tests/ui/views.rs:130:71 + --> tests/ui/views.rs:132:71 | -130 | fn view_def_returns_not_a_spacetime_type(_: &AnonymousViewContext) -> Option { +132 | fn view_def_returns_not_a_spacetime_type(_: &AnonymousViewContext) -> Option { | ^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound | help: the trait `SpacetimeType` is not implemented for `NotSpacetimeType` - --> tests/ui/views.rs:66:1 + --> tests/ui/views.rs:68:1 | - 66 | struct NotSpacetimeType {} + 68 | struct NotSpacetimeType {} | ^^^^^^^^^^^^^^^^^^^^^^^ = note: if you own the type, try adding `#[derive(SpacetimeType)]` to its definition = help: the following other types implement trait `SpacetimeType`: @@ -318,15 +318,15 @@ help: the trait `SpacetimeType` is not implemented for `NotSpacetimeType` = note: required for `Option` to implement `ViewReturn` error[E0277]: the trait bound `NotSpacetimeType: Serialize` is not satisfied - --> tests/ui/views.rs:130:71 + --> tests/ui/views.rs:132:71 | -130 | fn view_def_returns_not_a_spacetime_type(_: &AnonymousViewContext) -> Option { +132 | fn view_def_returns_not_a_spacetime_type(_: &AnonymousViewContext) -> Option { | ^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound | help: the trait `Serialize` is not implemented for `NotSpacetimeType` - --> tests/ui/views.rs:66:1 + --> tests/ui/views.rs:68:1 | - 66 | struct NotSpacetimeType {} + 68 | struct NotSpacetimeType {} | ^^^^^^^^^^^^^^^^^^^^^^^ = help: the following other types implement trait `Serialize`: &T @@ -341,9 +341,9 @@ help: the trait `Serialize` is not implemented for `NotSpacetimeType` = note: required for `Option` to implement `ViewReturn` error[E0277]: invalid anonymous view signature - --> tests/ui/views.rs:129:1 + --> tests/ui/views.rs:131:1 | -129 | #[view(accessor = view_def_returns_not_a_spacetime_type, public)] +131 | #[view(accessor = view_def_returns_not_a_spacetime_type, public)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid | = help: the trait `AnonymousView<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a AnonymousViewContext) -> Option {view_def_returns_not_a_spacetime_type}` @@ -363,15 +363,15 @@ note: required by a bound in `ViewDispatcher::::invoke` = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `NotSpacetimeType: SpacetimeType` is not satisfied - --> tests/ui/views.rs:130:71 + --> tests/ui/views.rs:132:71 | -130 | fn view_def_returns_not_a_spacetime_type(_: &AnonymousViewContext) -> Option { +132 | fn view_def_returns_not_a_spacetime_type(_: &AnonymousViewContext) -> Option { | ^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound | help: the trait `SpacetimeType` is not implemented for `NotSpacetimeType` - --> tests/ui/views.rs:66:1 + --> tests/ui/views.rs:68:1 | - 66 | struct NotSpacetimeType {} + 68 | struct NotSpacetimeType {} | ^^^^^^^^^^^^^^^^^^^^^^^ = note: if you own the type, try adding `#[derive(SpacetimeType)]` to its definition = help: the following other types implement trait `SpacetimeType`: @@ -387,9 +387,9 @@ help: the trait `SpacetimeType` is not implemented for `NotSpacetimeType` = note: required for `Option` to implement `SpacetimeType` error[E0277]: the trait bound `{integer}: RHS` is not satisfied - --> tests/ui/views.rs:152:49 + --> tests/ui/views.rs:154:49 | -152 | ctx.from.player().r#where(|a| a.identity.eq(42)).build() +154 | ctx.from.player().r#where(|a| a.identity.eq(42)).build() | -- ^^ the trait `RHS` is not implemented for `{integer}` | | | required by a bound introduced by this call @@ -411,9 +411,9 @@ note: required by a bound in `Col::::eq` | ^^^^^^^^^ required by this bound in `Col::::eq` error[E0277]: the trait bound `u32: RHS` is not satisfied - --> tests/ui/views.rs:158:49 + --> tests/ui/views.rs:160:49 | -158 | ctx.from.player_info().r#where(|a| a.age.eq(4200u32)).build() +160 | ctx.from.player_info().r#where(|a| a.age.eq(4200u32)).build() | -- ^^^^^^^ the trait `RHS` is not implemented for `u32` | | | required by a bound introduced by this call @@ -433,9 +433,9 @@ note: required by a bound in `Col::::eq` = note: this error originates in the macro `impl_rhs` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0308]: mismatched types - --> tests/ui/views.rs:167:62 + --> tests/ui/views.rs:169:62 | -167 | .left_semijoin(ctx.from.player(), |a, b| a.weight.eq(b.identity)) +169 | .left_semijoin(ctx.from.player(), |a, b| a.weight.eq(b.identity)) | -- ^^^^^^^^^^ expected `IxCol`, found `IxCol` | | | arguments to this method are incorrect @@ -449,15 +449,31 @@ note: method defined here | ^^ error[E0609]: no field `age` on type `&PlayerInfoIxCols` - --> tests/ui/views.rs:177:72 + --> tests/ui/views.rs:179:72 | -177 | .right_semijoin(ctx.from.player_info(), |a, b| a.identity.eq(b.age)) +179 | .right_semijoin(ctx.from.player_info(), |a, b| a.identity.eq(b.age)) | ^^^ unknown field | = note: available fields are: `identity`, `weight` error[E0599]: no method named `xyz` found for struct `QueryBuilder` in the current scope - --> tests/ui/views.rs:195:14 + --> tests/ui/views.rs:197:14 | -195 | ctx.from.xyz().build() +197 | ctx.from.xyz().build() | ^^^ method not found in `QueryBuilder` + +error[E0609]: no field `missing_identity` on type `&Player` + --> tests/ui/views.rs:201:74 + | +201 | #[view(accessor = view_primary_key_missing_column, public, primary_key = missing_identity)] + | ^^^^^^^^^^^^^^^^ unknown field + | + = note: available field is: `identity` + +error[E0609]: no field `identity` on type `&CustomAccessorViewRow` + --> tests/ui/views.rs:213:79 + | +213 | #[view(accessor = view_primary_key_uses_canonical_name, public, primary_key = identity)] + | ^^^^^^^^ unknown field + | + = note: available field is: `renamed_identity` diff --git a/crates/lib/src/db/raw_def/v10.rs b/crates/lib/src/db/raw_def/v10.rs index a801ea286be..460cdb18e3d 100644 --- a/crates/lib/src/db/raw_def/v10.rs +++ b/crates/lib/src/db/raw_def/v10.rs @@ -89,6 +89,9 @@ pub enum RawModuleDefV10Section { /// Names provided explicitly by the user that do not follow from the case conversion policy. ExplicitNames(ExplicitNames), + + /// Primary key metadata for views. + ViewPrimaryKeys(Vec), } #[derive(Debug, Clone, Copy, Default, SpacetimeType)] @@ -509,6 +512,21 @@ pub struct RawViewDefV10 { pub return_type: AlgebraicType, } +/// Primary key metadata for a view. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawViewPrimaryKeyDefV10 { + /// The source/accessor name of the view this primary key applies to. + pub view_source_name: RawIdentifier, + + /// The source/accessor names of the columns that make up the primary key. + /// + /// Currently only a single column is supported, but this is a vector to keep + /// the raw definition compatible with future composite view primary keys. + pub columns: Vec, +} + impl RawModuleDefV10 { /// Get the types section, if present. pub fn types(&self) -> Option<&Vec> { @@ -558,6 +576,14 @@ impl RawModuleDefV10 { }) } + /// Get the view primary keys section, if present. + pub fn view_primary_keys(&self) -> Option<&Vec> { + self.sections.iter().find_map(|s| match s { + RawModuleDefV10Section::ViewPrimaryKeys(primary_keys) => Some(primary_keys), + _ => None, + }) + } + /// Get the schedules section, if present. pub fn schedules(&self) -> Option<&Vec> { self.sections.iter().find_map(|s| match s { @@ -702,6 +728,26 @@ impl RawModuleDefV10Builder { } } + /// Get mutable access to the view primary keys section, creating it if missing. + fn view_primary_keys_mut(&mut self) -> &mut Vec { + let idx = self + .module + .sections + .iter() + .position(|s| matches!(s, RawModuleDefV10Section::ViewPrimaryKeys(_))) + .unwrap_or_else(|| { + self.module + .sections + .push(RawModuleDefV10Section::ViewPrimaryKeys(Vec::new())); + self.module.sections.len() - 1 + }); + + match &mut self.module.sections[idx] { + RawModuleDefV10Section::ViewPrimaryKeys(primary_keys) => primary_keys, + _ => unreachable!("Just ensured ViewPrimaryKeys section exists"), + } + } + /// Get mutable access to the schedules section, creating it if missing. fn schedules_mut(&mut self) -> &mut Vec { let idx = self @@ -996,6 +1042,18 @@ impl RawModuleDefV10Builder { }); } + /// Add primary key metadata for a view. + pub fn add_view_primary_key(&mut self, view_source_name: impl Into, columns: I) + where + C: Into, + I: IntoIterator, + { + self.view_primary_keys_mut().push(RawViewPrimaryKeyDefV10 { + view_source_name: view_source_name.into(), + columns: columns.into_iter().map(Into::into).collect(), + }); + } + /// Add a lifecycle reducer assignment to the module. /// /// The function must be a previously-added reducer. diff --git a/crates/schema/src/auto_migrate.rs b/crates/schema/src/auto_migrate.rs index f2fba813fa8..5e0f81987eb 100644 --- a/crates/schema/src/auto_migrate.rs +++ b/crates/schema/src/auto_migrate.rs @@ -631,7 +631,11 @@ 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 { + if old.is_anonymous != new.is_anonymous + || old.primary_key != new.primary_key + || incompatible_return_type + || incompatible_param_types + { 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 89c201e3f85..90c2d9970ea 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -34,7 +34,7 @@ use spacetimedb_lib::db::raw_def; use spacetimedb_lib::db::raw_def::v10::{ ExplicitNames, RawConstraintDefV10, RawIndexDefV10, RawLifeCycleReducerDefV10, RawModuleDefV10, RawModuleDefV10Section, RawProcedureDefV10, RawReducerDefV10, RawRowLevelSecurityDefV10, RawScheduleDefV10, - RawScopedTypeNameV10, RawSequenceDefV10, RawTableDefV10, RawTypeDefV10, RawViewDefV10, + RawScopedTypeNameV10, RawSequenceDefV10, RawTableDefV10, RawTypeDefV10, RawViewDefV10, RawViewPrimaryKeyDefV10, }; use spacetimedb_lib::db::raw_def::v9::{ Lifecycle, RawColumnDefaultValueV9, RawConstraintDataV9, RawConstraintDefV9, RawIndexAlgorithm, RawIndexDefV9, @@ -42,6 +42,7 @@ use spacetimedb_lib::db::raw_def::v9::{ RawScheduleDefV9, RawScopedTypeNameV9, RawSequenceDefV9, RawSql, RawTableDefV9, RawTypeDefV9, RawUniqueConstraintDataV9, RawViewDefV9, TableAccess, TableType, }; +use spacetimedb_lib::db::view::{extract_view_return_product_type_ref, ViewKind}; use spacetimedb_lib::{ProductType, RawModuleDef}; use spacetimedb_primitives::{ColId, ColList, ColOrCols, ColSet, ProcedureId, ReducerId, TableId, ViewFnPtr}; use spacetimedb_sats::raw_identifier::RawIdentifier; @@ -575,6 +576,7 @@ impl From for RawModuleDefV10 { } // Collect ExplicitNames for views: accessor_name → source_name, name → canonical_name. + let mut raw_view_primary_keys = Vec::new(); let raw_views: Vec = views .into_values() .map(|vd| { @@ -582,12 +584,35 @@ impl From for RawModuleDefV10 { RawIdentifier::from(vd.accessor_name.clone()), RawIdentifier::from(vd.name.clone()), ); + // Only explicit procedural-view primary keys are serialized into + // `ViewPrimaryKeys`. Primary keys for query builder views are + // inferred again during validation from the returned table type. + if vd.is_procedural() + && let Some(primary_key) = vd.primary_key + && let Some(column) = vd.return_columns.get(primary_key.idx()) + { + raw_view_primary_keys.push(RawViewPrimaryKeyDefV10 { + view_source_name: RawIdentifier::from(vd.accessor_name.clone()), + // Use the already canonicalized column name, because this raw + // module def is being emitted from an already canonicalized + // `ModuleDef`. The `Typespace` section we emit below contains + // those canonical column names, and V10 has no column-level + // `ExplicitNames` section to translate source column names + // during the next validation pass. Therefore the serialized + // primary-key column must match the canonical name present in + // the emitted return type. + columns: vec![RawIdentifier::from(column.name.clone())], + }); + } vd.into() }) .collect(); if !raw_views.is_empty() { sections.push(RawModuleDefV10Section::Views(raw_views)); } + if !raw_view_primary_keys.is_empty() { + sections.push(RawModuleDefV10Section::ViewPrimaryKeys(raw_view_primary_keys)); + } if !schedules.is_empty() { sections.push(RawModuleDefV10Section::Schedules(schedules)); @@ -1554,6 +1579,12 @@ impl ViewDef { pub fn get_param_by_name(&self, name: &Identifier) -> Option<&ViewParamDef> { self.param_columns.iter().find(|c| &c.name == name) } + + /// Is this a procedural view or query builder view? + pub fn is_procedural(&self) -> bool { + use extract_view_return_product_type_ref as extract_kind; + matches!(extract_kind(&self.return_type), Some((_, ViewKind::Procedural))) + } } impl From for RawViewDefV9 { diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index e840770a8c2..5ddf32bf338 100644 --- a/crates/schema/src/def/validate/v10.rs +++ b/crates/schema/src/def/validate/v10.rs @@ -81,6 +81,7 @@ pub fn validate(def: RawModuleDefV10) -> Result { .cloned() .map(ExplicitNamesLookup::new) .unwrap_or_default(); + let view_primary_keys = def.view_primary_keys().cloned().unwrap_or_default(); // Original `typespace` needs to be preserved to be assign `accesor_name`s to columns. let typespace_with_accessor_names = typespace.clone(); @@ -244,6 +245,7 @@ pub fn validate(def: RawModuleDefV10) -> Result { check_scheduled_functions_exist(&mut tables, &reducers, &procedures)?; change_scheduled_functions_and_lifetimes_visibility(&tables, &mut reducers, &mut procedures)?; + attach_view_primary_keys(&mut views, view_primary_keys)?; assign_query_view_primary_keys(&tables, &mut views); Ok((tables, types, reducers, procedures, views)) @@ -835,6 +837,92 @@ fn attach_schedules_to_tables( Ok(()) } +/// Attach explicit view primary-key metadata from the raw V10 `ViewPrimaryKeys` +/// section to validated [`ViewDef`]s. +/// +/// Entries in the raw section refer to view and column source/accessor names, +/// not canonical names. This resolves those names against the already validated +/// views, checks the current single-column restriction, and stores the resolved +/// [`ColId`] on `ViewDef::primary_key`. +fn attach_view_primary_keys( + views: &mut IndexMap, + primary_keys: Vec, +) -> Result<()> { + let mut errors = Vec::new(); + let mut seen = Vec::new(); + + // `ViewPrimaryKeys` is a separate V10 section keyed by view source/accessor + // name. Validation happens after all views have been built so that we can + // resolve each entry against the validated `ViewDef` and its validated return + // columns. + for primary_key in primary_keys { + let RawViewPrimaryKeyDefV10 { + view_source_name, + columns, + } = primary_key; + + // Keep one primary-key declaration per view. Multiple section entries + // for the same view are treated as a schema error rather than merged so + // typos or duplicate module def output do not silently change behavior. + if seen.contains(&view_source_name) { + errors.push(ValidationError::RepeatedViewPrimaryKey { + view: view_source_name.clone(), + }); + continue; + } + seen.push(view_source_name.clone()); + + // The raw ABI stores a vector so multi-column keys can be added later + // without changing the section shape. The current schema model and + // client caches only support a single view primary-key column. + if columns.len() > 1 { + errors.push(ValidationError::MultipleViewPrimaryKeyColumns { + view: view_source_name.clone(), + columns, + }); + continue; + } + + let Some(column_name) = columns.into_iter().next() else { + continue; + }; + + // Match the view by accessor/source name because this is raw + // module-definition metadata produced before validation applies case + // conversion and explicit-name resolution. The canonical view name may + // differ from the source name. + let Some(view) = views + .values_mut() + .find(|view| view.accessor_name.as_raw() == &view_source_name) + else { + errors.push(ValidationError::ViewPrimaryKeyViewNotFound { view: view_source_name }); + continue; + }; + + // Match return columns by accessor/source name for the same reason. For + // raw schemas produced from a validated `ModuleDef`, the accessor name + // may already be canonical; in both cases it must agree with the return + // type names present in that raw schema. + let Some(column) = view + .return_columns + .iter() + .find(|column| column.accessor_name.as_raw() == &column_name) + else { + errors.push(ValidationError::ViewPrimaryKeyColumnNotFound { + view: view_source_name, + column: column_name, + }); + continue; + }; + + // Store the resolved column id on the canonical view definition. Later + // schema construction and codegen use this just like a table primary key. + view.primary_key = Some(column.col_id); + } + + ValidationErrors::add_extra_errors(Ok(()), errors) +} + fn assign_query_view_primary_keys(tables: &IdentifierMap, views: &mut IndexMap) { let primary_key_for_product_type_ref = |product_type_ref: AlgebraicTypeRef| { let mut primary_key = None; @@ -859,9 +947,11 @@ fn assign_query_view_primary_keys(tables: &IdentifierMap, views: &mut for view in views.values_mut() { view.primary_key = match extract_view_return_product_type_ref(&view.return_type) { - Some((_, ViewKind::Procedural)) => None, - Some((product_type_ref, ViewKind::Query)) => primary_key_for_product_type_ref(product_type_ref), - None => None, + Some((_, ViewKind::Procedural)) => view.primary_key, + Some((product_type_ref, ViewKind::Query)) => view + .primary_key + .or_else(|| primary_key_for_product_type_ref(product_type_ref)), + None => view.primary_key, }; } } diff --git a/crates/schema/src/error.rs b/crates/schema/src/error.rs index 06f284998b5..d9000a607d9 100644 --- a/crates/schema/src/error.rs +++ b/crates/schema/src/error.rs @@ -84,6 +84,17 @@ pub enum ValidationError { view: RawIdentifier, ty: PrettyAlgebraicType, }, + #[error("View {view} referenced by primary key definition not found")] + ViewPrimaryKeyViewNotFound { view: RawIdentifier }, + #[error("View {view} has multiple primary key definitions")] + RepeatedViewPrimaryKey { view: RawIdentifier }, + #[error("View {view} has multiple primary key columns: {columns:?}")] + MultipleViewPrimaryKeyColumns { + view: RawIdentifier, + columns: Vec, + }, + #[error("Primary key column {column} for view {view} not found")] + ViewPrimaryKeyColumnNotFound { view: RawIdentifier, column: RawIdentifier }, #[error("Table {table} has invalid product_type_ref {ref_}")] InvalidProductTypeRef { table: RawIdentifier, diff --git a/crates/schema/src/schema.rs b/crates/schema/src/schema.rs index e62b9dc6963..7eac68d79f1 100644 --- a/crates/schema/src/schema.rs +++ b/crates/schema/src/schema.rs @@ -939,6 +939,14 @@ impl TableSchema { } } else if let Some(pk_col) = view_primary_key { let cols = col_list![ColId(0), pk_col]; + constraints.push(ConstraintSchema { + table_id: TableId::SENTINEL, + constraint_id: ConstraintId::SENTINEL, + constraint_name: make_constraint_name(&cols), + data: ConstraintData::Unique(UniqueConstraintData { + columns: ColSet::from(cols.clone()), + }), + }); indexes.push(IndexSchema { index_id: IndexId::SENTINEL, table_id: TableId::SENTINEL, diff --git a/crates/smoketests/modules/Cargo.lock b/crates/smoketests/modules/Cargo.lock index 09e926aec32..a103dc24050 100644 --- a/crates/smoketests/modules/Cargo.lock +++ b/crates/smoketests/modules/Cargo.lock @@ -917,6 +917,20 @@ dependencies = [ "spacetimedb", ] +[[package]] +name = "smoketest-module-views-primary-key-auto-migrate" +version = "0.1.0" +dependencies = [ + "spacetimedb", +] + +[[package]] +name = "smoketest-module-views-primary-key-auto-migrate-updated" +version = "0.1.0" +dependencies = [ + "spacetimedb", +] + [[package]] name = "smoketest-module-views-query" version = "0.1.0" diff --git a/crates/smoketests/modules/Cargo.toml b/crates/smoketests/modules/Cargo.toml index 61a9e36f519..585634e9bdc 100644 --- a/crates/smoketests/modules/Cargo.toml +++ b/crates/smoketests/modules/Cargo.toml @@ -19,6 +19,8 @@ members = [ "views-sql", "views-auto-migrate", "views-auto-migrate-updated", + "views-primary-key-auto-migrate", + "views-primary-key-auto-migrate-updated", "views-drop-view", "views-trapped", "views-recovered", diff --git a/crates/smoketests/modules/views-primary-key-auto-migrate-updated/Cargo.toml b/crates/smoketests/modules/views-primary-key-auto-migrate-updated/Cargo.toml new file mode 100644 index 00000000000..d53aca26204 --- /dev/null +++ b/crates/smoketests/modules/views-primary-key-auto-migrate-updated/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "smoketest-module-views-primary-key-auto-migrate-updated" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +spacetimedb.workspace = true diff --git a/crates/smoketests/modules/views-primary-key-auto-migrate-updated/src/lib.rs b/crates/smoketests/modules/views-primary-key-auto-migrate-updated/src/lib.rs new file mode 100644 index 00000000000..0164a45c886 --- /dev/null +++ b/crates/smoketests/modules/views-primary-key-auto-migrate-updated/src/lib.rs @@ -0,0 +1,19 @@ +use spacetimedb::{log, ReducerContext, ViewContext}; + +#[spacetimedb::table(accessor = player_state, public)] +pub struct PlayerState { + #[primary_key] + id: u64, + #[index(btree)] + level: u64, +} + +#[spacetimedb::reducer(client_disconnected)] +pub fn identity_disconnected(_ctx: &ReducerContext) { + log::info!("VIEW PRIMARY KEY UPDATE: client disconnected"); +} + +#[spacetimedb::view(accessor = player, public, primary_key = id)] +pub fn player(ctx: &ViewContext) -> Vec { + ctx.db.player_state().level().filter(0u64..).collect() +} diff --git a/crates/smoketests/modules/views-primary-key-auto-migrate/Cargo.toml b/crates/smoketests/modules/views-primary-key-auto-migrate/Cargo.toml new file mode 100644 index 00000000000..4a224ffff22 --- /dev/null +++ b/crates/smoketests/modules/views-primary-key-auto-migrate/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "smoketest-module-views-primary-key-auto-migrate" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +spacetimedb.workspace = true diff --git a/crates/smoketests/modules/views-primary-key-auto-migrate/src/lib.rs b/crates/smoketests/modules/views-primary-key-auto-migrate/src/lib.rs new file mode 100644 index 00000000000..8cee509c497 --- /dev/null +++ b/crates/smoketests/modules/views-primary-key-auto-migrate/src/lib.rs @@ -0,0 +1,14 @@ +use spacetimedb::ViewContext; + +#[spacetimedb::table(accessor = player_state, public)] +pub struct PlayerState { + #[primary_key] + id: u64, + #[index(btree)] + level: u64, +} + +#[spacetimedb::view(accessor = player, public)] +pub fn player(ctx: &ViewContext) -> Vec { + ctx.db.player_state().level().filter(0u64..).collect() +} diff --git a/crates/smoketests/tests/smoketests/views.rs b/crates/smoketests/tests/smoketests/views.rs index f44c6076159..ebd03c8e1f8 100644 --- a/crates/smoketests/tests/smoketests/views.rs +++ b/crates/smoketests/tests/smoketests/views.rs @@ -508,6 +508,36 @@ fn test_auto_migration_add_view() { test.publish_module_clear(false).unwrap(); } +#[test] +fn test_view_primary_key_auto_migration_disconnects_clients() { + let mut test = Smoketest::builder() + .precompiled_module("views-primary-key-auto-migrate") + .build(); + + let sub = test + .subscribe_background_unconfirmed(&["select * from player"], 2) + .unwrap(); + + test.use_precompiled_module("views-primary-key-auto-migrate-updated"); + let identity = test.database_identity.clone().unwrap(); + test.publish_module_with_options(&identity, false, true).unwrap(); + + sub.collect().unwrap(); + + let logs = test.logs(100).unwrap(); + assert!( + logs.iter().any(|l| l.contains("Disconnecting all users")), + "Expected disconnect log in logs: {:?}", + logs + ); + assert!( + logs.iter() + .any(|l| l.contains("VIEW PRIMARY KEY UPDATE: client disconnected")), + "Expected client_disconnected reducer log in logs: {:?}", + logs + ); +} + #[test] fn test_view_accessibility() { let test = Smoketest::builder().precompiled_module("views-callable").build(); diff --git a/modules/sdk-test-procedural-view-pk-ts/.npmrc b/modules/sdk-test-procedural-view-pk-ts/.npmrc new file mode 100644 index 00000000000..44bdf80d1df --- /dev/null +++ b/modules/sdk-test-procedural-view-pk-ts/.npmrc @@ -0,0 +1 @@ +minimum-release-age=1440 diff --git a/modules/sdk-test-procedural-view-pk-ts/package.json b/modules/sdk-test-procedural-view-pk-ts/package.json new file mode 100644 index 00000000000..ed3cefd9de5 --- /dev/null +++ b/modules/sdk-test-procedural-view-pk-ts/package.json @@ -0,0 +1,14 @@ +{ + "name": "sdk-test-procedural-view-pk-ts", + "version": "1.0.0", + "license": "ISC", + "type": "module", + "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" + }, + "dependencies": { + "spacetimedb": "workspace:^" + } +} diff --git a/modules/sdk-test-procedural-view-pk-ts/src/index.ts b/modules/sdk-test-procedural-view-pk-ts/src/index.ts new file mode 100644 index 00000000000..907211d006e --- /dev/null +++ b/modules/sdk-test-procedural-view-pk-ts/src/index.ts @@ -0,0 +1,52 @@ +import { schema, t, table } from 'spacetimedb/server'; + +const LeftSource = t.row('LeftSource', { + id: t.u64().primaryKey(), + sender: t.identity().index('btree'), + filter: t.u64(), +}); + +const RightSource = t.row('RightSource', { + id: t.u64().primaryKey(), + sender: t.identity().index('btree'), + filter: t.u64(), +}); + +const left_source = table({ public: true }, LeftSource); +const right_source = table({ public: true }, RightSource); + +const spacetimedb = schema({ left_source, right_source }); +export default spacetimedb; + +export const insert_left = spacetimedb.reducer( + { id: t.u64(), filter: t.u64() }, + (ctx, { id, filter }) => { + ctx.db.left_source.insert({ id, sender: ctx.sender, filter }); + } +); + +export const update_left = spacetimedb.reducer( + { id: t.u64(), filter: t.u64() }, + (ctx, { id, filter }) => { + ctx.db.left_source.id.update({ id, sender: ctx.sender, filter }); + } +); + +export const insert_right = spacetimedb.reducer( + { id: t.u64(), filter: t.u64() }, + (ctx, { id, filter }) => { + ctx.db.right_source.insert({ id, sender: ctx.sender, filter }); + } +); + +export const sender_left_view = spacetimedb.view( + { public: true }, + t.array(left_source.rowType), + ctx => Array.from(ctx.db.left_source.sender.filter(ctx.sender)) +); + +export const sender_right_view = spacetimedb.view( + { public: true }, + t.array(right_source.rowType), + ctx => Array.from(ctx.db.right_source.sender.filter(ctx.sender)) +); diff --git a/modules/sdk-test-procedural-view-pk-ts/tsconfig.json b/modules/sdk-test-procedural-view-pk-ts/tsconfig.json new file mode 100644 index 00000000000..9760e3377f1 --- /dev/null +++ b/modules/sdk-test-procedural-view-pk-ts/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + + "strict": true, + "declaration": false, + "emitDeclarationOnly": false, + "noEmit": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowImportingTsExtensions": true, + "noImplicitAny": true, + "moduleResolution": "Bundler", + "isolatedDeclarations": false, + + "esModuleInterop": false, + "allowSyntheticDefaultImports": false, + "useDefineForClassFields": true, + + "verbatimModuleSyntax": true, + "isolatedModules": true + }, + "include": ["src/index.ts"], + "exclude": ["node_modules", "dist/**/*"] +} diff --git a/modules/sdk-test-procedural-view-pk/Cargo.toml b/modules/sdk-test-procedural-view-pk/Cargo.toml new file mode 100644 index 00000000000..36abe64c7d7 --- /dev/null +++ b/modules/sdk-test-procedural-view-pk/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "sdk-test-procedural-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-procedural-view-pk/src/lib.rs b/modules/sdk-test-procedural-view-pk/src/lib.rs new file mode 100644 index 00000000000..846a84aaeeb --- /dev/null +++ b/modules/sdk-test-procedural-view-pk/src/lib.rs @@ -0,0 +1,56 @@ +use spacetimedb::{reducer, table, view, Identity, ReducerContext, Table, ViewContext}; + +#[table(accessor = left_source, public)] +pub struct LeftSource { + #[primary_key] + pub id: u64, + #[index(btree)] + pub sender: Identity, + pub filter: u64, +} + +#[table(accessor = right_source, public)] +pub struct RightSource { + #[primary_key] + pub id: u64, + #[index(btree)] + pub sender: Identity, + pub filter: u64, +} + +#[reducer] +pub fn insert_left(ctx: &ReducerContext, id: u64, filter: u64) { + ctx.db.left_source().insert(LeftSource { + id, + sender: ctx.sender(), + filter, + }); +} + +#[reducer] +pub fn update_left(ctx: &ReducerContext, id: u64, filter: u64) { + ctx.db.left_source().id().update(LeftSource { + id, + sender: ctx.sender(), + filter, + }); +} + +#[reducer] +pub fn insert_right(ctx: &ReducerContext, id: u64, filter: u64) { + ctx.db.right_source().insert(RightSource { + id, + sender: ctx.sender(), + filter, + }); +} + +#[view(accessor = sender_left_view, public, primary_key = id)] +pub fn sender_left_view(ctx: &ViewContext) -> Vec { + ctx.db.left_source().sender().filter(ctx.sender()).collect() +} + +#[view(accessor = sender_right_view, public, primary_key = id)] +pub fn sender_right_view(ctx: &ViewContext) -> Vec { + ctx.db.right_source().sender().filter(ctx.sender()).collect() +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd210edad05..1cf185849bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -304,6 +304,12 @@ importers: specifier: workspace:^ version: link:../../crates/bindings-typescript + modules/sdk-test-procedural-view-pk-ts: + dependencies: + spacetimedb: + specifier: workspace:^ + version: link:../../crates/bindings-typescript + modules/sdk-test-procedure-ts: dependencies: spacetimedb: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8cf1969dbb3..3cc346c4213 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -19,6 +19,7 @@ packages: - 'templates/chat-react-ts/spacetimedb' - 'modules/sdk-test-connect-disconnect-ts' - 'modules/sdk-test-procedure-ts' + - 'modules/sdk-test-procedural-view-pk-ts' - 'modules/sdk-test-ts' - 'modules/sdk-test-case-conversion-ts' - 'docs' diff --git a/sdks/rust/tests/procedural-view-pk-client/Cargo.toml b/sdks/rust/tests/procedural-view-pk-client/Cargo.toml new file mode 100644 index 00000000000..c3f668c260f --- /dev/null +++ b/sdks/rust/tests/procedural-view-pk-client/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "procedural-view-pk-client" +version.workspace = true +edition.workspace = true +license-file = "LICENSE" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["native"] + +native = [ + "dep:env_logger", + "dep:tokio", +] + +browser = [ + "spacetimedb-sdk/browser", + "dep:wasm-bindgen", + "dep:wasm-bindgen-futures", + "dep:console_error_panic_hook", +] + +[[bin]] +name = "procedural-view-pk-client" +path = "src/main.rs" +required-features = ["native"] + +[dependencies] +spacetimedb-sdk = { path = "../.." } +test-counter = { path = "../test-counter" } +anyhow.workspace = true +env_logger = { workspace = true, optional = true } +tokio = { workspace = true, optional = true } + +wasm-bindgen = { version = "0.2.100", optional = true } +wasm-bindgen-futures = { version = "0.4.45", optional = true } +console_error_panic_hook = { version = "0.1.7", optional = true } + +[lints] +workspace = true diff --git a/sdks/rust/tests/procedural-view-pk-client/README.md b/sdks/rust/tests/procedural-view-pk-client/README.md new file mode 100644 index 00000000000..ac34f9e2e0e --- /dev/null +++ b/sdks/rust/tests/procedural-view-pk-client/README.md @@ -0,0 +1,10 @@ +This test client is used with the Rust-only module: + +- [`sdk-test-procedural-view-pk`](/modules/sdk-test-procedural-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-procedural-view-pk +``` diff --git a/sdks/rust/tests/procedural-view-pk-client/src/lib.rs b/sdks/rust/tests/procedural-view-pk-client/src/lib.rs new file mode 100644 index 00000000000..0ffa7e5dacd --- /dev/null +++ b/sdks/rust/tests/procedural-view-pk-client/src/lib.rs @@ -0,0 +1,14 @@ +#![allow(clippy::disallowed_macros)] + +mod module_bindings; +pub mod test_handlers; + +#[cfg(all(target_arch = "wasm32", feature = "browser"))] +use wasm_bindgen::prelude::wasm_bindgen; + +#[cfg(all(target_arch = "wasm32", feature = "browser"))] +#[wasm_bindgen] +pub async fn run(test_name: String, db_name: String) { + console_error_panic_hook::set_once(); + test_handlers::dispatch(&test_name, &db_name).await; +} diff --git a/sdks/rust/tests/procedural-view-pk-client/src/main.rs b/sdks/rust/tests/procedural-view-pk-client/src/main.rs new file mode 100644 index 00000000000..5a8fdf8970e --- /dev/null +++ b/sdks/rust/tests/procedural-view-pk-client/src/main.rs @@ -0,0 +1,23 @@ +use procedural_view_pk_client::test_handlers; + +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 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"); + let db_name = std::env::var("SPACETIME_SDK_TEST_DB_NAME").expect("Failed to read db name from env"); + + tokio::runtime::Runtime::new() + .unwrap() + .block_on(test_handlers::dispatch(&test, &db_name)); +} diff --git a/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/insert_left_reducer.rs b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/insert_left_reducer.rs new file mode 100644 index 00000000000..84f431376cd --- /dev/null +++ b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/insert_left_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 InsertLeftArgs { + pub id: u64, + pub filter: u64, +} + +impl From for super::Reducer { + fn from(args: InsertLeftArgs) -> Self { + Self::InsertLeft { + id: args.id, + filter: args.filter, + } + } +} + +impl __sdk::InModule for InsertLeftArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `insert_left`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait insert_left { + /// Request that the remote module invoke the reducer `insert_left` 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_left:insert_left_then`] to run a callback after the reducer completes. + fn insert_left(&self, id: u64, filter: u64) -> __sdk::Result<()> { + self.insert_left_then(id, filter, |_, _| {}) + } + + /// Request that the remote module invoke the reducer `insert_left` 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_left_then( + &self, + id: u64, + filter: u64, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()>; +} + +impl insert_left for super::RemoteReducers { + fn insert_left_then( + &self, + id: u64, + filter: u64, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()> { + self.imp + .invoke_reducer_with_callback(InsertLeftArgs { id, filter }, callback) + } +} diff --git a/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/insert_right_reducer.rs b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/insert_right_reducer.rs new file mode 100644 index 00000000000..88a81e48a64 --- /dev/null +++ b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/insert_right_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 InsertRightArgs { + pub id: u64, + pub filter: u64, +} + +impl From for super::Reducer { + fn from(args: InsertRightArgs) -> Self { + Self::InsertRight { + id: args.id, + filter: args.filter, + } + } +} + +impl __sdk::InModule for InsertRightArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `insert_right`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait insert_right { + /// Request that the remote module invoke the reducer `insert_right` 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_right:insert_right_then`] to run a callback after the reducer completes. + fn insert_right(&self, id: u64, filter: u64) -> __sdk::Result<()> { + self.insert_right_then(id, filter, |_, _| {}) + } + + /// Request that the remote module invoke the reducer `insert_right` 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_right_then( + &self, + id: u64, + filter: u64, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()>; +} + +impl insert_right for super::RemoteReducers { + fn insert_right_then( + &self, + id: u64, + filter: u64, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()> { + self.imp + .invoke_reducer_with_callback(InsertRightArgs { id, filter }, callback) + } +} diff --git a/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/left_source_table.rs b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/left_source_table.rs new file mode 100644 index 00000000000..a2df242c87c --- /dev/null +++ b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/left_source_table.rs @@ -0,0 +1,157 @@ +// 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::left_source_type::LeftSource; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `left_source`. +/// +/// Obtain a handle from the [`LeftSourceTableAccess::left_source`] method on [`super::RemoteTables`], +/// like `ctx.db.left_source()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.left_source().on_insert(...)`. +pub struct LeftSourceTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `left_source`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait LeftSourceTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`LeftSourceTableHandle`], which mediates access to the table `left_source`. + fn left_source(&self) -> LeftSourceTableHandle<'_>; +} + +impl LeftSourceTableAccess for super::RemoteTables { + fn left_source(&self) -> LeftSourceTableHandle<'_> { + LeftSourceTableHandle { + imp: self.imp.get_table::("left_source"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct LeftSourceInsertCallbackId(__sdk::CallbackId); +pub struct LeftSourceDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for LeftSourceTableHandle<'ctx> { + type Row = LeftSource; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = LeftSourceInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> LeftSourceInsertCallbackId { + LeftSourceInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: LeftSourceInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = LeftSourceDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> LeftSourceDeleteCallbackId { + LeftSourceDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: LeftSourceDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct LeftSourceUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for LeftSourceTableHandle<'ctx> { + type UpdateCallbackId = LeftSourceUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> LeftSourceUpdateCallbackId { + LeftSourceUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: LeftSourceUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `id` unique index on the table `left_source`, +/// which allows point queries on the field of the same name +/// via the [`LeftSourceIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.left_source().id().find(...)`. +pub struct LeftSourceIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> LeftSourceTableHandle<'ctx> { + /// Get a handle on the `id` unique index on the table `left_source`. + pub fn id(&self) -> LeftSourceIdUnique<'ctx> { + LeftSourceIdUnique { + imp: self.imp.get_unique_constraint::("id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> LeftSourceIdUnique<'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::("left_source"); + _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 `LeftSource`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait left_sourceQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `LeftSource`. + fn left_source(&self) -> __sdk::__query_builder::Table; +} + +impl left_sourceQueryTableAccess for __sdk::QueryTableAccessor { + fn left_source(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("left_source") + } +} diff --git a/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/left_source_type.rs b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/left_source_type.rs new file mode 100644 index 00000000000..8cf1b1ba3d6 --- /dev/null +++ b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/left_source_type.rs @@ -0,0 +1,57 @@ +// 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 LeftSource { + pub id: u64, + pub sender: __sdk::Identity, + pub filter: u64, +} + +impl __sdk::InModule for LeftSource { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `LeftSource`. +/// +/// Provides typed access to columns for query building. +pub struct LeftSourceCols { + pub id: __sdk::__query_builder::Col, + pub sender: __sdk::__query_builder::Col, + pub filter: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for LeftSource { + type Cols = LeftSourceCols; + fn cols(table_name: &'static str) -> Self::Cols { + LeftSourceCols { + id: __sdk::__query_builder::Col::new(table_name, "id"), + sender: __sdk::__query_builder::Col::new(table_name, "sender"), + filter: __sdk::__query_builder::Col::new(table_name, "filter"), + } + } +} + +/// Indexed column accessor struct for the table `LeftSource`. +/// +/// Provides typed access to indexed columns for query building. +pub struct LeftSourceIxCols { + pub id: __sdk::__query_builder::IxCol, + pub sender: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for LeftSource { + type IxCols = LeftSourceIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + LeftSourceIxCols { + id: __sdk::__query_builder::IxCol::new(table_name, "id"), + sender: __sdk::__query_builder::IxCol::new(table_name, "sender"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for LeftSource {} diff --git a/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/mod.rs b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/mod.rs new file mode 100644 index 00000000000..1c13e3a11d1 --- /dev/null +++ b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/mod.rs @@ -0,0 +1,869 @@ +// 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.3.0 (commit 3fa87dbec33b6ca9e7fed93b3a8089c60f9ef360). + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +pub mod insert_left_reducer; +pub mod insert_right_reducer; +pub mod left_source_table; +pub mod left_source_type; +pub mod right_source_table; +pub mod right_source_type; +pub mod sender_left_view_table; +pub mod sender_right_view_table; +pub mod update_left_reducer; + +pub use insert_left_reducer::insert_left; +pub use insert_right_reducer::insert_right; +pub use left_source_table::*; +pub use left_source_type::LeftSource; +pub use right_source_table::*; +pub use right_source_type::RightSource; +pub use sender_left_view_table::*; +pub use sender_right_view_table::*; +pub use update_left_reducer::update_left; + +#[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 { + InsertLeft { id: u64, filter: u64 }, + InsertRight { id: u64, filter: u64 }, + UpdateLeft { id: u64, filter: u64 }, +} + +impl __sdk::InModule for Reducer { + type Module = RemoteModule; +} + +impl __sdk::Reducer for Reducer { + fn reducer_name(&self) -> &'static str { + match self { + Reducer::InsertLeft { .. } => "insert_left", + Reducer::InsertRight { .. } => "insert_right", + Reducer::UpdateLeft { .. } => "update_left", + _ => unreachable!(), + } + } + #[allow(clippy::clone_on_copy)] + fn args_bsatn(&self) -> Result, __sats::bsatn::EncodeError> { + match self { + Reducer::InsertLeft { id, filter } => __sats::bsatn::to_vec(&insert_left_reducer::InsertLeftArgs { + id: id.clone(), + filter: filter.clone(), + }), + Reducer::InsertRight { id, filter } => __sats::bsatn::to_vec(&insert_right_reducer::InsertRightArgs { + id: id.clone(), + filter: filter.clone(), + }), + Reducer::UpdateLeft { id, filter } => __sats::bsatn::to_vec(&update_left_reducer::UpdateLeftArgs { + id: id.clone(), + filter: filter.clone(), + }), + _ => unreachable!(), + } + } +} + +#[derive(Default, Debug)] +#[allow(non_snake_case)] +#[doc(hidden)] +pub struct DbUpdate { + left_source: __sdk::TableUpdate, + right_source: __sdk::TableUpdate, + sender_left_view: __sdk::TableUpdate, + sender_right_view: __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[..] { + "left_source" => db_update + .left_source + .append(left_source_table::parse_table_update(table_update)?), + "right_source" => db_update + .right_source + .append(right_source_table::parse_table_update(table_update)?), + "sender_left_view" => db_update + .sender_left_view + .append(sender_left_view_table::parse_table_update(table_update)?), + "sender_right_view" => db_update + .sender_right_view + .append(sender_right_view_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.left_source = cache + .apply_diff_to_table::("left_source", &self.left_source) + .with_updates_by_pk(|row| &row.id); + diff.right_source = cache + .apply_diff_to_table::("right_source", &self.right_source) + .with_updates_by_pk(|row| &row.id); + diff.sender_left_view = cache + .apply_diff_to_table::("sender_left_view", &self.sender_left_view) + .with_updates_by_pk(|row| &row.id); + diff.sender_right_view = cache + .apply_diff_to_table::("sender_right_view", &self.sender_right_view) + .with_updates_by_pk(|row| &row.id); + + diff + } + fn parse_initial_rows(raw: __ws::v2::QueryRows) -> __sdk::Result { + let mut db_update = DbUpdate::default(); + for table_rows in raw.tables { + match &table_rows.table[..] { + "left_source" => db_update + .left_source + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "right_source" => db_update + .right_source + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "sender_left_view" => db_update + .sender_left_view + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "sender_right_view" => db_update + .sender_right_view + .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[..] { + "left_source" => db_update + .left_source + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "right_source" => db_update + .right_source + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "sender_left_view" => db_update + .sender_left_view + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "sender_right_view" => db_update + .sender_right_view + .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> { + left_source: __sdk::TableAppliedDiff<'r, LeftSource>, + right_source: __sdk::TableAppliedDiff<'r, RightSource>, + sender_left_view: __sdk::TableAppliedDiff<'r, LeftSource>, + sender_right_view: __sdk::TableAppliedDiff<'r, RightSource>, + __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::("left_source", &self.left_source, event); + callbacks.invoke_table_row_callbacks::("right_source", &self.right_source, event); + callbacks.invoke_table_row_callbacks::("sender_left_view", &self.sender_left_view, event); + callbacks.invoke_table_row_callbacks::("sender_right_view", &self.sender_right_view, event); + } +} + +#[doc(hidden)] +#[derive(Debug)] +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`]. +#[cfg_attr(not(target_arch = "wasm32"), doc = "- [`DbConnection::run_threaded`].")] +#[cfg_attr(target_arch = "wasm32", doc = "- [`DbConnection::run_background_task`].")] +/// - [`DbConnection::run_async`]. +/// - [`DbConnection::advance_one_message`]. +#[cfg_attr( + not(target_arch = "wasm32"), + doc = "- [`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. + #[cfg(not(target_arch = "wasm32"))] + 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. + #[cfg(not(target_arch = "wasm32"))] + pub fn run_threaded(&self) -> std::thread::JoinHandle<()> { + self.imp.run_threaded() + } + + /// Spawn a background task which processes WebSocket messages as they are received. + #[cfg(target_arch = "wasm32")] + pub fn run_background_task(&self) { + self.imp.run_background_task() + } + + /// 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) { + left_source_table::register_table(client_cache); + right_source_table::register_table(client_cache); + sender_left_view_table::register_table(client_cache); + sender_right_view_table::register_table(client_cache); + } + const ALL_TABLE_NAMES: &'static [&'static str] = + &["left_source", "right_source", "sender_left_view", "sender_right_view"]; +} diff --git a/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/right_source_table.rs b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/right_source_table.rs new file mode 100644 index 00000000000..a9e1bc1a717 --- /dev/null +++ b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/right_source_table.rs @@ -0,0 +1,157 @@ +// 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::right_source_type::RightSource; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `right_source`. +/// +/// Obtain a handle from the [`RightSourceTableAccess::right_source`] method on [`super::RemoteTables`], +/// like `ctx.db.right_source()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.right_source().on_insert(...)`. +pub struct RightSourceTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `right_source`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait RightSourceTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`RightSourceTableHandle`], which mediates access to the table `right_source`. + fn right_source(&self) -> RightSourceTableHandle<'_>; +} + +impl RightSourceTableAccess for super::RemoteTables { + fn right_source(&self) -> RightSourceTableHandle<'_> { + RightSourceTableHandle { + imp: self.imp.get_table::("right_source"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct RightSourceInsertCallbackId(__sdk::CallbackId); +pub struct RightSourceDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for RightSourceTableHandle<'ctx> { + type Row = RightSource; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = RightSourceInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> RightSourceInsertCallbackId { + RightSourceInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: RightSourceInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = RightSourceDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> RightSourceDeleteCallbackId { + RightSourceDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: RightSourceDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct RightSourceUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for RightSourceTableHandle<'ctx> { + type UpdateCallbackId = RightSourceUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> RightSourceUpdateCallbackId { + RightSourceUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: RightSourceUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `id` unique index on the table `right_source`, +/// which allows point queries on the field of the same name +/// via the [`RightSourceIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.right_source().id().find(...)`. +pub struct RightSourceIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> RightSourceTableHandle<'ctx> { + /// Get a handle on the `id` unique index on the table `right_source`. + pub fn id(&self) -> RightSourceIdUnique<'ctx> { + RightSourceIdUnique { + imp: self.imp.get_unique_constraint::("id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> RightSourceIdUnique<'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::("right_source"); + _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 `RightSource`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait right_sourceQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `RightSource`. + fn right_source(&self) -> __sdk::__query_builder::Table; +} + +impl right_sourceQueryTableAccess for __sdk::QueryTableAccessor { + fn right_source(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("right_source") + } +} diff --git a/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/right_source_type.rs b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/right_source_type.rs new file mode 100644 index 00000000000..53e47de61aa --- /dev/null +++ b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/right_source_type.rs @@ -0,0 +1,57 @@ +// 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 RightSource { + pub id: u64, + pub sender: __sdk::Identity, + pub filter: u64, +} + +impl __sdk::InModule for RightSource { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `RightSource`. +/// +/// Provides typed access to columns for query building. +pub struct RightSourceCols { + pub id: __sdk::__query_builder::Col, + pub sender: __sdk::__query_builder::Col, + pub filter: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for RightSource { + type Cols = RightSourceCols; + fn cols(table_name: &'static str) -> Self::Cols { + RightSourceCols { + id: __sdk::__query_builder::Col::new(table_name, "id"), + sender: __sdk::__query_builder::Col::new(table_name, "sender"), + filter: __sdk::__query_builder::Col::new(table_name, "filter"), + } + } +} + +/// Indexed column accessor struct for the table `RightSource`. +/// +/// Provides typed access to indexed columns for query building. +pub struct RightSourceIxCols { + pub id: __sdk::__query_builder::IxCol, + pub sender: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for RightSource { + type IxCols = RightSourceIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + RightSourceIxCols { + id: __sdk::__query_builder::IxCol::new(table_name, "id"), + sender: __sdk::__query_builder::IxCol::new(table_name, "sender"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for RightSource {} diff --git a/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/sender_left_view_table.rs b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/sender_left_view_table.rs new file mode 100644 index 00000000000..1a47fbbb844 --- /dev/null +++ b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/sender_left_view_table.rs @@ -0,0 +1,157 @@ +// 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::left_source_type::LeftSource; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `sender_left_view`. +/// +/// Obtain a handle from the [`SenderLeftViewTableAccess::sender_left_view`] method on [`super::RemoteTables`], +/// like `ctx.db.sender_left_view()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.sender_left_view().on_insert(...)`. +pub struct SenderLeftViewTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `sender_left_view`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait SenderLeftViewTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`SenderLeftViewTableHandle`], which mediates access to the table `sender_left_view`. + fn sender_left_view(&self) -> SenderLeftViewTableHandle<'_>; +} + +impl SenderLeftViewTableAccess for super::RemoteTables { + fn sender_left_view(&self) -> SenderLeftViewTableHandle<'_> { + SenderLeftViewTableHandle { + imp: self.imp.get_table::("sender_left_view"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct SenderLeftViewInsertCallbackId(__sdk::CallbackId); +pub struct SenderLeftViewDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for SenderLeftViewTableHandle<'ctx> { + type Row = LeftSource; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = SenderLeftViewInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> SenderLeftViewInsertCallbackId { + SenderLeftViewInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: SenderLeftViewInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = SenderLeftViewDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> SenderLeftViewDeleteCallbackId { + SenderLeftViewDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: SenderLeftViewDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct SenderLeftViewUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for SenderLeftViewTableHandle<'ctx> { + type UpdateCallbackId = SenderLeftViewUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> SenderLeftViewUpdateCallbackId { + SenderLeftViewUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: SenderLeftViewUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `id` unique index on the table `sender_left_view`, +/// which allows point queries on the field of the same name +/// via the [`SenderLeftViewIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.sender_left_view().id().find(...)`. +pub struct SenderLeftViewIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> SenderLeftViewTableHandle<'ctx> { + /// Get a handle on the `id` unique index on the table `sender_left_view`. + pub fn id(&self) -> SenderLeftViewIdUnique<'ctx> { + SenderLeftViewIdUnique { + imp: self.imp.get_unique_constraint::("id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> SenderLeftViewIdUnique<'ctx> { + /// Find the subscribed row whose `id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &u64) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("sender_left_view"); + _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 `LeftSource`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait sender_left_viewQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `LeftSource`. + fn sender_left_view(&self) -> __sdk::__query_builder::Table; +} + +impl sender_left_viewQueryTableAccess for __sdk::QueryTableAccessor { + fn sender_left_view(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("sender_left_view") + } +} diff --git a/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/sender_right_view_table.rs b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/sender_right_view_table.rs new file mode 100644 index 00000000000..4f41946a165 --- /dev/null +++ b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/sender_right_view_table.rs @@ -0,0 +1,157 @@ +// 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::right_source_type::RightSource; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `sender_right_view`. +/// +/// Obtain a handle from the [`SenderRightViewTableAccess::sender_right_view`] method on [`super::RemoteTables`], +/// like `ctx.db.sender_right_view()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.sender_right_view().on_insert(...)`. +pub struct SenderRightViewTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `sender_right_view`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait SenderRightViewTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`SenderRightViewTableHandle`], which mediates access to the table `sender_right_view`. + fn sender_right_view(&self) -> SenderRightViewTableHandle<'_>; +} + +impl SenderRightViewTableAccess for super::RemoteTables { + fn sender_right_view(&self) -> SenderRightViewTableHandle<'_> { + SenderRightViewTableHandle { + imp: self.imp.get_table::("sender_right_view"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct SenderRightViewInsertCallbackId(__sdk::CallbackId); +pub struct SenderRightViewDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for SenderRightViewTableHandle<'ctx> { + type Row = RightSource; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = SenderRightViewInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> SenderRightViewInsertCallbackId { + SenderRightViewInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: SenderRightViewInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = SenderRightViewDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> SenderRightViewDeleteCallbackId { + SenderRightViewDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: SenderRightViewDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct SenderRightViewUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for SenderRightViewTableHandle<'ctx> { + type UpdateCallbackId = SenderRightViewUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> SenderRightViewUpdateCallbackId { + SenderRightViewUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: SenderRightViewUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `id` unique index on the table `sender_right_view`, +/// which allows point queries on the field of the same name +/// via the [`SenderRightViewIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.sender_right_view().id().find(...)`. +pub struct SenderRightViewIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> SenderRightViewTableHandle<'ctx> { + /// Get a handle on the `id` unique index on the table `sender_right_view`. + pub fn id(&self) -> SenderRightViewIdUnique<'ctx> { + SenderRightViewIdUnique { + imp: self.imp.get_unique_constraint::("id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> SenderRightViewIdUnique<'ctx> { + /// Find the subscribed row whose `id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &u64) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("sender_right_view"); + _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 `RightSource`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait sender_right_viewQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `RightSource`. + fn sender_right_view(&self) -> __sdk::__query_builder::Table; +} + +impl sender_right_viewQueryTableAccess for __sdk::QueryTableAccessor { + fn sender_right_view(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("sender_right_view") + } +} diff --git a/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/update_left_reducer.rs b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/update_left_reducer.rs new file mode 100644 index 00000000000..d9c8c3e5769 --- /dev/null +++ b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/update_left_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 UpdateLeftArgs { + pub id: u64, + pub filter: u64, +} + +impl From for super::Reducer { + fn from(args: UpdateLeftArgs) -> Self { + Self::UpdateLeft { + id: args.id, + filter: args.filter, + } + } +} + +impl __sdk::InModule for UpdateLeftArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `update_left`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait update_left { + /// Request that the remote module invoke the reducer `update_left` 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_left:update_left_then`] to run a callback after the reducer completes. + fn update_left(&self, id: u64, filter: u64) -> __sdk::Result<()> { + self.update_left_then(id, filter, |_, _| {}) + } + + /// Request that the remote module invoke the reducer `update_left` 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_left_then( + &self, + id: u64, + filter: u64, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()>; +} + +impl update_left for super::RemoteReducers { + fn update_left_then( + &self, + id: u64, + filter: u64, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()> { + self.imp + .invoke_reducer_with_callback(UpdateLeftArgs { id, filter }, callback) + } +} diff --git a/sdks/rust/tests/procedural-view-pk-client/src/test_handlers.rs b/sdks/rust/tests/procedural-view-pk-client/src/test_handlers.rs new file mode 100644 index 00000000000..4404d3715b1 --- /dev/null +++ b/sdks/rust/tests/procedural-view-pk-client/src/test_handlers.rs @@ -0,0 +1,223 @@ +use crate::module_bindings::*; +use spacetimedb_sdk::{error::InternalError, DbConnectionBuilder, DbContext, Table, TableWithPrimaryKey}; +use test_counter::TestCounter; + +const LOCALHOST: &str = "http://localhost:3000"; + +type ResultRecorder = Box)>; + +#[cfg(not(target_arch = "wasm32"))] +async fn build_and_run(builder: DbConnectionBuilder) -> DbConnection { + let conn = builder.build().unwrap(); + conn.run_threaded(); + conn +} + +#[cfg(target_arch = "wasm32")] +async fn build_and_run(builder: DbConnectionBuilder) -> DbConnection { + let conn = builder.build().await.unwrap(); + conn.run_background_task(); + conn +} + +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:?}"), + } +} + +async fn connect_then( + db_name: &str, + test_counter: &std::sync::Arc, + callback: impl FnOnce(&DbConnection) + Send + 'static, +) -> DbConnection { + connect_then_named(db_name, test_counter, "on_connect", callback).await +} + +async fn connect_then_named( + db_name: &str, + test_counter: &std::sync::Arc, + connect_test_name: &'static str, + callback: impl FnOnce(&DbConnection) + Send + 'static, +) -> DbConnection { + let connected_result = test_counter.add_test(connect_test_name); + let name = db_name.to_owned(); + 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_and_run(conn).await +} + +async fn exec_sender_scoped_pk_view(db_name: &str) { + let test_counter = TestCounter::new(); + let mut sender_a_update = Some(test_counter.add_test("sender_a_update")); + let mut sender_b_update = Some(test_counter.add_test("sender_b_update")); + + // Keep both clients connected so the test exercises the sender predicate in + // the procedural view. Each client inserts and updates a different primary key. + // Receiving the other client's update would fail the assertions in the callback. + let _sender_a = connect_then(db_name, &test_counter, move |ctx| { + ctx.subscription_builder() + .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) + .on_applied(move |ctx| { + ctx.db.sender_left_view().on_update(move |_, old_row, new_row| { + assert_eq!(old_row.id, 1); + assert_eq!(old_row.filter, 10); + assert_eq!(new_row.id, 1); + assert_eq!(new_row.filter, 11); + put_result(&mut sender_a_update, Ok(())); + }); + + ctx.reducers() + .insert_left_then(1, 10, reducer_callback_assert_committed("insert_left")) + .unwrap(); + ctx.reducers() + .update_left_then(1, 11, reducer_callback_assert_committed("update_left")) + .unwrap(); + }) + .add_query(|q| q.from.sender_left_view().build()) + .subscribe(); + }) + .await; + + let _sender_b = connect_then_named(db_name, &test_counter, "sender_b_on_connect", move |ctx| { + ctx.subscription_builder() + .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) + .on_applied(move |ctx| { + ctx.db.sender_left_view().on_update(move |_, old_row, new_row| { + assert_eq!(old_row.id, 2); + assert_eq!(old_row.filter, 20); + assert_eq!(new_row.id, 2); + assert_eq!(new_row.filter, 21); + put_result(&mut sender_b_update, Ok(())); + }); + + ctx.reducers() + .insert_left_then(2, 20, reducer_callback_assert_committed("insert_left")) + .unwrap(); + ctx.reducers() + .update_left_then(2, 21, reducer_callback_assert_committed("update_left")) + .unwrap(); + }) + .add_query(|q| q.from.sender_left_view().build()) + .subscribe(); + }) + .await; + + test_counter.wait_for_all().await; +} + +// Shared harness for the semijoin tests. The tests intentionally keep their +// query-builder expressions inline because those expressions are the behavior +// under test. This helper only owns the common connection and completion wiring. +async fn exec_semijoin( + db_name: &str, + result_name: &'static str, + subscribe: impl FnOnce(&DbConnection, ResultRecorder) + Send + 'static, +) { + let test_counter = TestCounter::new(); + let joined_insert = test_counter.add_test(result_name); + + connect_then(db_name, &test_counter, move |ctx| { + subscribe(ctx, joined_insert); + }) + .await; + + test_counter.wait_for_all().await; +} + +fn insert_semijoin_source_rows(ctx: &SubscriptionEventContext) { + // Both views contain rows with ids 10 and 20, so the primary-key semijoin + // alone would keep both rows. The side filters then reduce the result to + // just id 10: left.filter == 100 and right.filter == 300. + ctx.reducers() + .insert_left_then(10, 100, reducer_callback_assert_committed("insert_left")) + .unwrap(); + ctx.reducers() + .insert_left_then(20, 200, reducer_callback_assert_committed("insert_left")) + .unwrap(); + ctx.reducers() + .insert_right_then(10, 300, reducer_callback_assert_committed("insert_right")) + .unwrap(); + ctx.reducers() + .insert_right_then(20, 400, reducer_callback_assert_committed("insert_right")) + .unwrap(); +} + +async fn exec_view_pk_left_semijoin(db_name: &str) { + exec_semijoin(db_name, "left_semijoin_insert", move |ctx, joined_insert| { + let mut joined_insert = Some(joined_insert); + ctx.subscription_builder() + .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) + .on_applied(move |ctx| { + ctx.db.sender_left_view().on_insert(move |ctx, row| { + assert_eq!(ctx.db.sender_left_view().count(), 1); + assert_eq!(row.id, 10); + assert_eq!(row.filter, 100); + put_result(&mut joined_insert, Ok(())); + }); + + insert_semijoin_source_rows(ctx); + }) + .add_query(|q| { + q.from + .sender_right_view() + .filter(|right| right.filter.eq(300u64)) + .right_semijoin(q.from.sender_left_view(), |right, left| right.id.eq(left.id)) + .filter(|left| left.filter.eq(100u64)) + .build() + }) + .subscribe(); + }) + .await; +} + +async fn exec_view_pk_right_semijoin(db_name: &str) { + exec_semijoin(db_name, "right_semijoin_insert", move |ctx, joined_insert| { + let mut joined_insert = Some(joined_insert); + ctx.subscription_builder() + .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) + .on_applied(move |ctx| { + ctx.db.sender_right_view().on_insert(move |ctx, row| { + assert_eq!(ctx.db.sender_right_view().count(), 1); + assert_eq!(row.id, 10); + assert_eq!(row.filter, 300); + put_result(&mut joined_insert, Ok(())); + }); + + insert_semijoin_source_rows(ctx); + }) + .add_query(|q| { + q.from + .sender_left_view() + .filter(|left| left.filter.eq(100u64)) + .right_semijoin(q.from.sender_right_view(), |left, right| left.id.eq(right.id)) + .filter(|right| right.filter.eq(300u64)) + .build() + }) + .subscribe(); + }) + .await; +} + +pub async fn dispatch(test: &str, db_name: &str) { + match test { + "sender-scoped-pk-view" => exec_sender_scoped_pk_view(db_name).await, + "view-pk-left-semijoin" => exec_view_pk_left_semijoin(db_name).await, + "view-pk-right-semijoin" => exec_view_pk_right_semijoin(db_name).await, + _ => panic!("Unknown test: {test}"), + } +} diff --git a/sdks/rust/tests/test.rs b/sdks/rust/tests/test.rs index c487661b67d..4858acce0e1 100644 --- a/sdks/rust/tests/test.rs +++ b/sdks/rust/tests/test.rs @@ -768,3 +768,41 @@ macro_rules! view_pk_tests { view_pk_tests!(rust_view_pk, ""); view_pk_tests!(csharp_view_pk, "-cs"); + +macro_rules! procedural_view_pk_tests { + ($mod_name:ident, $module:literal) => { + mod $mod_name { + use spacetimedb_testing::sdk::Test; + + const MODULE: &str = $module; + const CLIENT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/procedural-view-pk-client"); + + fn make_test(subcommand: &str) -> Test { + super::platform_test_builder(CLIENT, Some(subcommand)) + .with_name(subcommand) + .with_module(MODULE) + .with_language("rust") + .with_bindings_dir("src/module_bindings") + .build() + } + + #[test] + fn sender_scoped_procedural_pk_view() { + make_test("sender-scoped-pk-view").run() + } + + #[test] + fn procedural_view_pk_left_semijoin() { + make_test("view-pk-left-semijoin").run() + } + + #[test] + fn procedural_view_pk_right_semijoin() { + make_test("view-pk-right-semijoin").run() + } + } + }; +} + +procedural_view_pk_tests!(rust_procedural_view_pk, "sdk-test-procedural-view-pk"); +procedural_view_pk_tests!(typescript_procedural_view_pk, "sdk-test-procedural-view-pk-ts"); 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 04ee8aedb08..26ecbdcc78e 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.5 (commit ca7484e072f9514fb2f890f26600a5d096f59431). +// This was generated using spacetimedb cli version 2.3.0 (commit 40fd32f523e0e21988c417f6907c25ea9ed52df9). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; diff --git a/sdks/rust/tests/view-pk-client/src/test_handlers.rs b/sdks/rust/tests/view-pk-client/src/test_handlers.rs index 4a7de205d38..6306d8a2562 100644 --- a/sdks/rust/tests/view-pk-client/src/test_handlers.rs +++ b/sdks/rust/tests/view-pk-client/src/test_handlers.rs @@ -42,7 +42,16 @@ async fn connect_then( test_counter: &std::sync::Arc, callback: impl FnOnce(&DbConnection) + Send + 'static, ) -> DbConnection { - let connected_result = test_counter.add_test("on_connect"); + connect_then_named(db_name, test_counter, "on_connect", callback).await +} + +async fn connect_then_named( + db_name: &str, + test_counter: &std::sync::Arc, + connect_test_name: &'static str, + callback: impl FnOnce(&DbConnection) + Send + 'static, +) -> DbConnection { + let connected_result = test_counter.add_test(connect_test_name); let name = db_name.to_owned(); let conn = DbConnection::builder() .with_database_name(name) From d03addfc90a0cedebcd6652c4a57f84950c45f3b Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 28 May 2026 12:59:11 -0700 Subject: [PATCH 2/5] View primary keys must be FilterableValues --- crates/bindings-macro/src/view.rs | 3 ++- crates/bindings/src/lib.rs | 2 ++ crates/bindings/tests/ui/views.rs | 16 ++++++++++++++++ crates/bindings/tests/ui/views.stderr | 26 ++++++++++++++++++++++++++ crates/lib/src/filterable_value.rs | 10 ++++++++++ crates/lib/src/lib.rs | 2 +- 6 files changed, 57 insertions(+), 2 deletions(-) diff --git a/crates/bindings-macro/src/view.rs b/crates/bindings-macro/src/view.rs index d0af0ccfe65..1dc865d4b41 100644 --- a/crates/bindings-macro/src/view.rs +++ b/crates/bindings-macro/src/view.rs @@ -293,7 +293,8 @@ pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Resu quote! { const _: () = { fn _assert_view_primary_key_column #lt_params (__row: &#row_ty) #lt_where_clause { - let _ = &__row.#primary_key; + fn _assert_view_primary_key_column_type(_: &T) {} + _assert_view_primary_key_column_type(&__row.#primary_key); } }; } diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index 12195738971..214f49fd8fc 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -49,6 +49,8 @@ pub use spacetimedb_lib::ScheduleAt; pub use spacetimedb_lib::TimeDuration; pub use spacetimedb_lib::Timestamp; pub use spacetimedb_lib::Uuid; +#[doc(hidden)] +pub use spacetimedb_lib::ViewPrimaryKeyColumn; pub use spacetimedb_primitives::TableId; pub use sys::Errno; pub use table::{ diff --git a/crates/bindings/tests/ui/views.rs b/crates/bindings/tests/ui/views.rs index 3bd9055bc76..0d2285a2d8f 100644 --- a/crates/bindings/tests/ui/views.rs +++ b/crates/bindings/tests/ui/views.rs @@ -215,4 +215,20 @@ fn view_primary_key_uses_canonical_name(_: &ViewContext) -> Vec Vec { + vec![] +} + fn main() {} diff --git a/crates/bindings/tests/ui/views.stderr b/crates/bindings/tests/ui/views.stderr index f9b5140b2ae..2b0c0dd6033 100644 --- a/crates/bindings/tests/ui/views.stderr +++ b/crates/bindings/tests/ui/views.stderr @@ -477,3 +477,29 @@ error[E0609]: no field `identity` on type `&CustomAccessorViewRow` | ^^^^^^^^ unknown field | = note: available field is: `renamed_identity` + +error[E0277]: `&'a NonFilterableViewPrimaryKey` cannot appear as an argument to an index filtering operation + --> tests/ui/views.rs:229:1 + | +229 | #[view(accessor = view_primary_key_non_filterable_column, public, primary_key = identity)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ should be an integer type, `bool`, `String`, `&str`, `Identity`, `Uuid`, `Timestamp`, `ConnectionId`, `Hash` or a no-payload enum which derives `SpacetimeType`, not `&'a NonFilterableViewPrimaryKey` + | + = help: the trait `for<'a> FilterableValue` is not implemented for `&'a NonFilterableViewPrimaryKey` + = note: The allowed set of types are limited to integers, bool, strings, `Identity`, `Uuid`, `Timestamp`, `ConnectionId`, `Hash` and no-payload enums which derive `SpacetimeType`, + = help: the following other types implement trait `FilterableValue`: + &ConnectionId + &FunctionVisibility + &Lifecycle + &TableAccess + &TableType + &bool + ðnum::int::I256 + ðnum::uint::U256 + and $N others + = note: required for `NonFilterableViewPrimaryKey` to implement `ViewPrimaryKeyColumn` +note: required by a bound in `_::_assert_view_primary_key_column::_assert_view_primary_key_column_type` + --> tests/ui/views.rs:229:1 + | +229 | #[view(accessor = view_primary_key_non_filterable_column, public, primary_key = identity)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `_assert_view_primary_key_column_type` + = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/lib/src/filterable_value.rs b/crates/lib/src/filterable_value.rs index ac7c0b52a41..6f7accbf3e9 100644 --- a/crates/lib/src/filterable_value.rs +++ b/crates/lib/src/filterable_value.rs @@ -118,6 +118,16 @@ impl_filterable_value! { // &[u8] => Vec, } +/// Marker trait for column types supported as procedural view primary keys. +#[doc(hidden)] +#[diagnostic::on_unimplemented( + message = "view primary key column type `{Self}` is not supported", + label = "view primary key columns must use an index-filterable key type", + note = "view primary keys must be integer, bool, string, Identity, Uuid, Timestamp, ConnectionId, Hash, or a no-payload enum which derives SpacetimeType" +)] +pub trait ViewPrimaryKeyColumn {} +impl ViewPrimaryKeyColumn for T where for<'a> &'a T: FilterableValue {} + pub enum TermBound { Single(ops::Bound), Range(ops::Bound, ops::Bound), diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 2e8b9c08336..2b20be8c685 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -35,7 +35,7 @@ pub use connection_id::ConnectionId; pub use direct_index_key::{assert_column_type_valid_for_direct_index, DirectIndexKey}; #[doc(hidden)] pub use filterable_value::Private; -pub use filterable_value::{FilterableValue, IndexScanRangeBoundsTerminator, TermBound}; +pub use filterable_value::{FilterableValue, IndexScanRangeBoundsTerminator, TermBound, ViewPrimaryKeyColumn}; pub use identity::Identity; pub use scheduler::ScheduleAt; pub use spacetimedb_sats::hash::{self, hash_bytes, Hash}; From dabe3915a0c953c6b75b77d311344e516c2e2329 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 28 May 2026 13:58:42 -0700 Subject: [PATCH 3/5] typescript tests --- .../src/server/view.test-d.ts | 9 ++ modules/sdk-test-view-pk-ts/.npmrc | 1 + modules/sdk-test-view-pk-ts/package.json | 14 +++ modules/sdk-test-view-pk-ts/src/index.ts | 85 +++++++++++++++++++ modules/sdk-test-view-pk-ts/tsconfig.json | 26 ++++++ pnpm-lock.yaml | 6 ++ pnpm-workspace.yaml | 1 + sdks/rust/tests/test.rs | 1 + 8 files changed, 143 insertions(+) create mode 100644 modules/sdk-test-view-pk-ts/.npmrc 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 diff --git a/crates/bindings-typescript/src/server/view.test-d.ts b/crates/bindings-typescript/src/server/view.test-d.ts index 3336dd8710a..de91adfcb83 100644 --- a/crates/bindings-typescript/src/server/view.test-d.ts +++ b/crates/bindings-typescript/src/server/view.test-d.ts @@ -91,6 +91,15 @@ spacetime.anonymousView( () => [] ); +// @ts-expect-error the same multiple-primary-key check also applies to query-builder views. +spacetime.anonymousView( + { name: 'multiplePrimaryRowsQuery', public: true }, + multiplePrimaryKeyRows, + ctx => { + return ctx.from.person; + } +); + spacetime.anonymousView( { name: 'optionalPerson', public: true }, optionalPerson, diff --git a/modules/sdk-test-view-pk-ts/.npmrc b/modules/sdk-test-view-pk-ts/.npmrc new file mode 100644 index 00000000000..44bdf80d1df --- /dev/null +++ b/modules/sdk-test-view-pk-ts/.npmrc @@ -0,0 +1 @@ +minimum-release-age=1440 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..a03509611e5 --- /dev/null +++ b/modules/sdk-test-view-pk-ts/package.json @@ -0,0 +1,14 @@ +{ + "name": "sdk-test-view-pk-ts", + "version": "1.0.0", + "license": "ISC", + "type": "module", + "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" + }, + "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..76cd967de08 --- /dev/null +++ b/modules/sdk-test-view-pk-ts/src/index.ts @@ -0,0 +1,85 @@ +import { schema, t, table } from 'spacetimedb/server'; + +const ViewPkPlayer = t.row('ViewPkPlayer', { + id: t.u64().primaryKey(), + name: t.string(), +}); + +const ViewPkMembership = t.row('ViewPkMembership', { + id: t.u64().primaryKey(), + player_id: t.u64().index('btree'), +}); + +const ViewPkMembershipSecondary = t.row('ViewPkMembershipSecondary', { + id: t.u64().primaryKey(), + player_id: t.u64().index('btree'), +}); + +const view_pk_player = table({ public: true }, ViewPkPlayer); +const view_pk_membership = table({ public: true }, ViewPkMembership); +const view_pk_membership_secondary = table( + { public: true }, + ViewPkMembershipSecondary +); + +const spacetimedb = schema({ + view_pk_player, + view_pk_membership, + view_pk_membership_secondary, +}); + +export default spacetimedb; + +export const insert_view_pk_player = spacetimedb.reducer( + { id: t.u64(), name: t.string() }, + (ctx, { id, name }) => { + ctx.db.view_pk_player.insert({ id, name }); + } +); + +export const update_view_pk_player = spacetimedb.reducer( + { id: t.u64(), name: t.string() }, + (ctx, { id, name }) => { + ctx.db.view_pk_player.id.update({ id, name }); + } +); + +export const insert_view_pk_membership = spacetimedb.reducer( + { id: t.u64(), player_id: t.u64() }, + (ctx, { id, player_id }) => { + ctx.db.view_pk_membership.insert({ id, player_id }); + } +); + +export const insert_view_pk_membership_secondary = spacetimedb.reducer( + { id: t.u64(), player_id: t.u64() }, + (ctx, { id, player_id }) => { + ctx.db.view_pk_membership_secondary.insert({ id, player_id }); + } +); + +export const all_view_pk_players = spacetimedb.view( + { public: true }, + t.array(view_pk_player.rowType), + ctx => ctx.from.view_pk_player +); + +export const sender_view_pk_players_a = spacetimedb.view( + { public: true }, + t.array(view_pk_player.rowType), + ctx => + ctx.from.view_pk_membership.rightSemijoin( + ctx.from.view_pk_player, + (membership, player) => membership.player_id.eq(player.id) + ) +); + +export const sender_view_pk_players_b = spacetimedb.view( + { public: true }, + t.array(view_pk_player.rowType), + ctx => + ctx.from.view_pk_membership_secondary.rightSemijoin( + ctx.from.view_pk_player, + (membership, player) => membership.player_id.eq(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..9760e3377f1 --- /dev/null +++ b/modules/sdk-test-view-pk-ts/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + + "strict": true, + "declaration": false, + "emitDeclarationOnly": false, + "noEmit": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowImportingTsExtensions": true, + "noImplicitAny": true, + "moduleResolution": "Bundler", + "isolatedDeclarations": false, + + "esModuleInterop": false, + "allowSyntheticDefaultImports": false, + "useDefineForClassFields": true, + + "verbatimModuleSyntax": true, + "isolatedModules": true + }, + "include": ["src/index.ts"], + "exclude": ["node_modules", "dist/**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1cf185849bc..06df1660789 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -310,6 +310,12 @@ importers: specifier: workspace:^ version: link:../../crates/bindings-typescript + modules/sdk-test-view-pk-ts: + dependencies: + spacetimedb: + specifier: workspace:^ + version: link:../../crates/bindings-typescript + modules/sdk-test-procedure-ts: dependencies: spacetimedb: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3cc346c4213..b29d6ecb6b6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -20,6 +20,7 @@ packages: - 'modules/sdk-test-connect-disconnect-ts' - 'modules/sdk-test-procedure-ts' - 'modules/sdk-test-procedural-view-pk-ts' + - 'modules/sdk-test-view-pk-ts' - 'modules/sdk-test-ts' - 'modules/sdk-test-case-conversion-ts' - 'docs' diff --git a/sdks/rust/tests/test.rs b/sdks/rust/tests/test.rs index 4858acce0e1..113586ad2a1 100644 --- a/sdks/rust/tests/test.rs +++ b/sdks/rust/tests/test.rs @@ -768,6 +768,7 @@ macro_rules! view_pk_tests { view_pk_tests!(rust_view_pk, ""); view_pk_tests!(csharp_view_pk, "-cs"); +view_pk_tests!(typescript_view_pk, "-ts"); macro_rules! procedural_view_pk_tests { ($mod_name:ident, $module:literal) => { From 34eb45badf73b5c2176bcd666c160b80accbb53b Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 28 May 2026 15:48:12 -0700 Subject: [PATCH 4/5] Update test-app and add primary key view --- Cargo.lock | 265 ++++++------------ .../test-app/server/Cargo.toml | 2 +- .../test-app/server/src/lib.rs | 17 +- .../test-app/src/module_bindings/index.ts | 16 +- .../my_user_procedural_table.ts | 21 ++ .../tests/db_connection.test.ts | 112 ++++++++ 6 files changed, 241 insertions(+), 192 deletions(-) create mode 100644 crates/bindings-typescript/test-app/src/module_bindings/my_user_procedural_table.ts diff --git a/Cargo.lock b/Cargo.lock index 19741975d2a..c3753956daf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -467,7 +467,7 @@ name = "basic-rs-template-module" version = "0.1.0" dependencies = [ "log", - "spacetimedb 2.3.0", + "spacetimedb", ] [[package]] @@ -475,7 +475,7 @@ name = "benchmarks-module" version = "0.1.0" dependencies = [ "anyhow", - "spacetimedb 2.3.0", + "spacetimedb", ] [[package]] @@ -3707,7 +3707,7 @@ name = "keynote-benchmarks" version = "0.1.0" dependencies = [ "log", - "spacetimedb 2.3.0", + "spacetimedb", ] [[package]] @@ -4113,7 +4113,7 @@ version = "0.0.0" dependencies = [ "anyhow", "log", - "spacetimedb 2.3.0", + "spacetimedb", ] [[package]] @@ -5306,7 +5306,7 @@ name = "perf-test-module" version = "0.1.0" dependencies = [ "log", - "spacetimedb 2.3.0", + "spacetimedb", ] [[package]] @@ -5780,7 +5780,7 @@ dependencies = [ "env_logger 0.10.2", "futures", "serde_json", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-sdk", "test-counter", "tokio", @@ -6001,7 +6001,7 @@ name = "quickstart-chat-module" version = "0.1.0" dependencies = [ "log", - "spacetimedb 2.3.0", + "spacetimedb", ] [[package]] @@ -7147,14 +7147,14 @@ name = "sdk-test-case-conversion" version = "0.1.0" dependencies = [ "log", - "spacetimedb 2.3.0", + "spacetimedb", ] [[package]] name = "sdk-test-event-table-module" version = "0.1.0" dependencies = [ - "spacetimedb 2.3.0", + "spacetimedb", ] [[package]] @@ -7164,14 +7164,14 @@ dependencies = [ "anyhow", "log", "paste", - "spacetimedb 2.3.0", + "spacetimedb", ] [[package]] name = "sdk-test-procedural-view-pk" version = "0.1.0" dependencies = [ - "spacetimedb 2.3.0", + "spacetimedb", ] [[package]] @@ -7181,7 +7181,7 @@ dependencies = [ "anyhow", "log", "paste", - "spacetimedb 2.3.0", + "spacetimedb", ] [[package]] @@ -7191,14 +7191,14 @@ dependencies = [ "anyhow", "log", "paste", - "spacetimedb 2.3.0", + "spacetimedb", ] [[package]] name = "sdk-test-view-pk" version = "0.1.0" dependencies = [ - "spacetimedb 2.3.0", + "spacetimedb", ] [[package]] @@ -7671,26 +7671,7 @@ name = "spacetime-module" version = "0.1.0" dependencies = [ "log", - "spacetimedb 2.3.0", -] - -[[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]] @@ -7708,10 +7689,10 @@ dependencies = [ "rand 0.8.5", "scoped-tls", "serde_json", - "spacetimedb-bindings-macro 2.3.0", - "spacetimedb-bindings-sys 2.3.0", - "spacetimedb-lib 2.3.0", - "spacetimedb-primitives 2.3.0", + "spacetimedb-bindings-macro", + "spacetimedb-bindings-sys", + "spacetimedb-lib", + "spacetimedb-primitives", "spacetimedb-query-builder", "trybuild", ] @@ -7726,7 +7707,7 @@ dependencies = [ "serde_with", "spacetimedb-data-structures", "spacetimedb-jsonwebtoken", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", ] [[package]] @@ -7763,11 +7744,11 @@ dependencies = [ "spacetimedb-data-structures", "spacetimedb-datastore", "spacetimedb-execution", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-paths", - "spacetimedb-primitives 2.3.0", + "spacetimedb-primitives", "spacetimedb-query", - "spacetimedb-sats 2.3.0", + "spacetimedb-sats", "spacetimedb-schema", "spacetimedb-standalone", "spacetimedb-table", @@ -7780,20 +7761,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.3.0" @@ -7802,24 +7769,15 @@ dependencies = [ "humantime", "proc-macro2", "quote", - "spacetimedb-primitives 2.3.0", + "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.3.0" dependencies = [ - "spacetimedb-primitives 2.3.0", + "spacetimedb-primitives", ] [[package]] @@ -7875,9 +7833,9 @@ dependencies = [ "spacetimedb-data-structures", "spacetimedb-fs-utils", "spacetimedb-jsonwebtoken", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-paths", - "spacetimedb-primitives 2.3.0", + "spacetimedb-primitives", "spacetimedb-schema", "syntect", "tabled", @@ -7941,7 +7899,7 @@ dependencies = [ "spacetimedb-data-structures", "spacetimedb-datastore", "spacetimedb-jsonwebtoken", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-paths", "spacetimedb-schema", "tempfile", @@ -7973,9 +7931,9 @@ dependencies = [ "serde_json", "serde_with", "smallvec", - "spacetimedb-lib 2.3.0", - "spacetimedb-primitives 2.3.0", - "spacetimedb-sats 2.3.0", + "spacetimedb-lib", + "spacetimedb-primitives", + "spacetimedb-sats", "strum", "thiserror 1.0.69", ] @@ -7992,8 +7950,8 @@ dependencies = [ "regex", "serde_json", "spacetimedb-data-structures", - "spacetimedb-lib 2.3.0", - "spacetimedb-primitives 2.3.0", + "spacetimedb-lib", + "spacetimedb-primitives", "spacetimedb-schema", "spacetimedb-testing", ] @@ -8023,8 +7981,8 @@ dependencies = [ "spacetimedb-commitlog", "spacetimedb-fs-utils", "spacetimedb-paths", - "spacetimedb-primitives 2.3.0", - "spacetimedb-sats 2.3.0", + "spacetimedb-primitives", + "spacetimedb-sats", "tempfile", "thiserror 1.0.69", "tokio", @@ -8119,14 +8077,14 @@ dependencies = [ "spacetimedb-fs-utils", "spacetimedb-jsonwebtoken", "spacetimedb-jwks", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-memory-usage", "spacetimedb-metrics", "spacetimedb-paths", "spacetimedb-physical-plan", - "spacetimedb-primitives 2.3.0", + "spacetimedb-primitives", "spacetimedb-query", - "spacetimedb-sats 2.3.0", + "spacetimedb-sats", "spacetimedb-schema", "spacetimedb-snapshot", "spacetimedb-subscription", @@ -8197,11 +8155,11 @@ dependencies = [ "spacetimedb-data-structures", "spacetimedb-durability", "spacetimedb-execution", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-metrics", "spacetimedb-paths", - "spacetimedb-primitives 2.3.0", - "spacetimedb-sats 2.3.0", + "spacetimedb-primitives", + "spacetimedb-sats", "spacetimedb-schema", "spacetimedb-snapshot", "spacetimedb-table", @@ -8223,7 +8181,7 @@ dependencies = [ "spacetimedb-commitlog", "spacetimedb-fs-utils", "spacetimedb-paths", - "spacetimedb-sats 2.3.0", + "spacetimedb-sats", "tempfile", "thiserror 1.0.69", "tokio", @@ -8237,10 +8195,10 @@ dependencies = [ "anyhow", "itertools 0.12.1", "spacetimedb-expr", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-physical-plan", - "spacetimedb-primitives 2.3.0", - "spacetimedb-sats 2.3.0", + "spacetimedb-primitives", + "spacetimedb-sats", "spacetimedb-sql-parser", "spacetimedb-table", ] @@ -8255,11 +8213,11 @@ dependencies = [ "derive_more 0.99.20", "ethnum", "pretty_assertions", - "spacetimedb 2.3.0", + "spacetimedb", "spacetimedb-data-structures", - "spacetimedb-lib 2.3.0", - "spacetimedb-primitives 2.3.0", - "spacetimedb-sats 2.3.0", + "spacetimedb-lib", + "spacetimedb-primitives", + "spacetimedb-sats", "spacetimedb-schema", "spacetimedb-sql-parser", "thiserror 1.0.69", @@ -8319,26 +8277,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.3.0" @@ -8360,11 +8298,11 @@ dependencies = [ "ron", "serde", "serde_json", - "spacetimedb-bindings-macro 2.3.0", + "spacetimedb-bindings-macro", "spacetimedb-memory-usage", "spacetimedb-metrics", - "spacetimedb-primitives 2.3.0", - "spacetimedb-sats 2.3.0", + "spacetimedb-primitives", + "spacetimedb-sats", "thiserror 1.0.69", ] @@ -8418,7 +8356,7 @@ dependencies = [ "spacetimedb-auth", "spacetimedb-client-api", "spacetimedb-client-api-messages", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "thiserror 1.0.69", "tokio", ] @@ -8433,26 +8371,13 @@ dependencies = [ "pretty_assertions", "spacetimedb-data-structures", "spacetimedb-expr", - "spacetimedb-lib 2.3.0", - "spacetimedb-primitives 2.3.0", + "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.3.0" @@ -8476,9 +8401,9 @@ dependencies = [ "spacetimedb-client-api-messages", "spacetimedb-execution", "spacetimedb-expr", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-physical-plan", - "spacetimedb-primitives 2.3.0", + "spacetimedb-primitives", "spacetimedb-schema", "spacetimedb-sql-parser", "spacetimedb-table", @@ -8488,33 +8413,7 @@ dependencies = [ name = "spacetimedb-query-builder" version = "2.3.0" dependencies = [ - "spacetimedb-lib 2.3.0", -] - -[[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", + "spacetimedb-lib", ] [[package]] @@ -8545,10 +8444,10 @@ dependencies = [ "serde_json", "sha3", "smallvec", - "spacetimedb-bindings-macro 2.3.0", + "spacetimedb-bindings-macro", "spacetimedb-memory-usage", "spacetimedb-metrics", - "spacetimedb-primitives 2.3.0", + "spacetimedb-primitives", "thiserror 1.0.69", "uuid", ] @@ -8573,10 +8472,10 @@ dependencies = [ "serial_test", "smallvec", "spacetimedb-data-structures", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-memory-usage", - "spacetimedb-primitives 2.3.0", - "spacetimedb-sats 2.3.0", + "spacetimedb-primitives", + "spacetimedb-sats", "spacetimedb-sql-parser", "spacetimedb-testing", "termcolor", @@ -8614,10 +8513,10 @@ dependencies = [ "shlex", "spacetimedb-client-api-messages", "spacetimedb-data-structures", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-metrics", "spacetimedb-query-builder", - "spacetimedb-sats 2.3.0", + "spacetimedb-sats", "spacetimedb-schema", "spacetimedb-testing", "thiserror 1.0.69", @@ -8668,10 +8567,10 @@ dependencies = [ "spacetimedb-datastore", "spacetimedb-durability", "spacetimedb-fs-utils", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-paths", - "spacetimedb-primitives 2.3.0", - "spacetimedb-sats 2.3.0", + "spacetimedb-primitives", + "spacetimedb-sats", "spacetimedb-schema", "spacetimedb-table", "tempfile", @@ -8687,7 +8586,7 @@ name = "spacetimedb-sql-parser" version = "2.3.0" dependencies = [ "derive_more 0.99.20", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "sqlparser", "thiserror 1.0.69", ] @@ -8719,7 +8618,7 @@ dependencies = [ "spacetimedb-client-api-messages", "spacetimedb-core", "spacetimedb-datastore", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-paths", "spacetimedb-pg", "spacetimedb-schema", @@ -8742,9 +8641,9 @@ dependencies = [ "spacetimedb-data-structures", "spacetimedb-execution", "spacetimedb-expr", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-physical-plan", - "spacetimedb-primitives 2.3.0", + "spacetimedb-primitives", "spacetimedb-query", "spacetimedb-schema", ] @@ -8769,10 +8668,10 @@ dependencies = [ "rand 0.9.2", "smallvec", "spacetimedb-data-structures", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-memory-usage", - "spacetimedb-primitives 2.3.0", - "spacetimedb-sats 2.3.0", + "spacetimedb-primitives", + "spacetimedb-sats", "spacetimedb-schema", "thiserror 1.0.69", ] @@ -8798,7 +8697,7 @@ dependencies = [ "spacetimedb-client-api-messages", "spacetimedb-core", "spacetimedb-data-structures", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-paths", "spacetimedb-schema", "spacetimedb-standalone", @@ -8917,8 +8816,8 @@ dependencies = [ "rusqlite", "rust_decimal", "spacetimedb-core", - "spacetimedb-lib 2.3.0", - "spacetimedb-sats 2.3.0", + "spacetimedb-lib", + "spacetimedb-sats", "sqllogictest", "sqllogictest-engines", "tempfile", @@ -10098,7 +9997,7 @@ version = "0.1.0" dependencies = [ "anyhow", "log", - "spacetimedb 1.9.0", + "spacetimedb", ] [[package]] @@ -10107,7 +10006,7 @@ version = "0.1.0" dependencies = [ "anyhow", "log", - "spacetimedb 2.3.0", + "spacetimedb", ] [[package]] @@ -10327,7 +10226,7 @@ dependencies = [ "console_error_panic_hook", "env_logger 0.10.2", "futures", - "spacetimedb-lib 2.3.0", + "spacetimedb-lib", "spacetimedb-sdk", "test-counter", "tokio", @@ -11524,7 +11423,7 @@ dependencies = [ "reqwest 0.12.24", "serde", "serde_json", - "spacetimedb 2.3.0", + "spacetimedb", "spacetimedb-data-structures", "spacetimedb-guard", "thiserror 2.0.17", diff --git a/crates/bindings-typescript/test-app/server/Cargo.toml b/crates/bindings-typescript/test-app/server/Cargo.toml index c61fe890b87..7bc77ce9454 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 = { workspace = true } 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..b2c95d3ea33 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, 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] @@ -36,13 +36,18 @@ pub struct UnindexedPlayer { #[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, }); } + +#[view(accessor = my_user_procedural, public, primary_key = id)] +pub fn my_user_procedural(ctx: &ViewContext) -> Vec { + ctx.db.player().id().find(1u32).into_iter().collect() +} 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..49090beab53 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.3.0 (commit d03addfc90a0cedebcd6652c4a57f84950c45f3b). /* eslint-disable */ /* tslint:disable */ @@ -39,6 +39,7 @@ import CreatePlayerReducer from './create_player_reducer'; // Import all procedure arg schemas // Import all table schema definitions +import MyUserProceduralRow from './my_user_procedural_table'; import PlayerRow from './player_table'; import UnindexedPlayerRow from './unindexed_player_table'; import UserRow from './user_table'; @@ -106,6 +107,14 @@ const tablesSchema = __schema({ }, UserRow ), + my_user_procedural: __table( + { + name: 'my_user_procedural', + indexes: [], + constraints: [], + }, + MyUserProceduralRow + ), }); /** 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. */ @@ -119,7 +128,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.3.0' as const, }, tables: tablesSchema.schemaType.tables, reducers: reducersSchema.reducersType.reducers, @@ -139,6 +148,9 @@ export const reducers = __convertToAccessorMap( reducersSchema.reducersType.reducers ); +/** The procedures available in this remote SpacetimeDB module. */ +export const procedures = __convertToAccessorMap(proceduresSchema.procedures); + /** The context type returned in callbacks for all possible events. */ export type EventContext = __EventContextInterface; /** The context type returned in callbacks for reducer events. */ diff --git a/crates/bindings-typescript/test-app/src/module_bindings/my_user_procedural_table.ts b/crates/bindings-typescript/test-app/src/module_bindings/my_user_procedural_table.ts new file mode 100644 index 00000000000..aeaef4e3810 --- /dev/null +++ b/crates/bindings-typescript/test-app/src/module_bindings/my_user_procedural_table.ts @@ -0,0 +1,21 @@ +// 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'; +import { Point } from './types'; + +export default __t.row({ + id: __t.u32().primaryKey(), + userId: __t.identity().name('user_id'), + name: __t.string(), + get location() { + return Point; + }, +}); diff --git a/crates/bindings-typescript/tests/db_connection.test.ts b/crates/bindings-typescript/tests/db_connection.test.ts index 0580518e280..7a0da07bf52 100644 --- a/crates/bindings-typescript/tests/db_connection.test.ts +++ b/crates/bindings-typescript/tests/db_connection.test.ts @@ -13,6 +13,7 @@ import WebsocketTestAdapter from '../src/sdk/websocket_test_adapter'; import { V2_WS_PROTOCOL, V3_WS_PROTOCOL } from '../src/sdk/websocket_protocols'; import { decodeClientMessagesV3 } from '../src/sdk/websocket_v3_frames.ts'; import { DbConnection } from '../test-app/src/module_bindings'; +import MyUserProcedural from '../test-app/src/module_bindings/my_user_procedural_table'; import User from '../test-app/src/module_bindings/user_table'; import { anIdentity, @@ -139,6 +140,14 @@ function makeReducerInternalErrorResult(requestId: number, error: string) { }); } +type MyUserViewRow = Infer; + +function encodeMyUserProcedural(value: MyUserViewRow): Uint8Array { + const writer = new BinaryWriter(1024); + MyUserProcedural.serialize(writer, value); + return writer.getBuffer(); +} + describe('DbConnection', () => { test('call onConnectError callback after websocket connection failed to be established', async () => { const onConnectErrorPromise = new Deferred(); @@ -801,6 +810,109 @@ describe('DbConnection', () => { expect(client.db.user.count()).toEqual(1n); }); + test('it calls onUpdate for a primary-key procedural view', async () => { + const wsAdapter = new WebsocketTestAdapter(); + const client = DbConnection.builder() + .withUri('ws://127.0.0.1:1234') + .withDatabaseName('db') + .withWSFn(wsAdapter.openWebSocket) + .build(); + + await client['wsPromise']; + wsAdapter.acceptConnection(); + wsAdapter.sendToClient( + ServerMessage.InitialConnection({ + identity: anIdentity, + token: 'a-token', + connectionId: ConnectionId.random(), + }) + ); + + const initialRow: MyUserViewRow = { + id: 1, + userId: anIdentity, + name: 'originalName', + location: { x: 1, y: 2 }, + }; + const updatedRow: MyUserViewRow = { + ...initialRow, + name: 'newName', + }; + const initialInsertPromise = new Deferred(); + const updatePromise = new Deferred(); + const updates: { + oldRow: MyUserViewRow; + newRow: MyUserViewRow; + }[] = []; + + // `onUpdate` is only available when the generated view row binding carries + // primary-key metadata. + client.db.my_user_procedural.onInsert((_ctx, row) => { + expect(row).toEqual(initialRow); + initialInsertPromise.resolve(); + }); + client.db.my_user_procedural.onUpdate((_ctx, oldRow, newRow) => { + updates.push({ + oldRow, + newRow, + }); + updatePromise.resolve(); + }); + + // Seed the underlying table and the view cache with the same row. This + // mirrors the table-like updates the client receives for generated views. + wsAdapter.sendToClient( + ServerMessage.TransactionUpdate({ + querySets: [ + makeQuerySetUpdate(0, 'player', encodePlayer(initialRow)), + makeQuerySetUpdate( + 1, + 'my_user_procedural', + encodeMyUserProcedural(initialRow) + ), + ], + }) + ); + + await initialInsertPromise.promise; + expect(client.db.player.count()).toEqual(1n); + expect(client.db.my_user_procedural.count()).toEqual(1n); + + // A delete and insert with the same primary key in one transaction should + // be coalesced by the client cache into `onUpdate`, not separate delete and + // insert callbacks. This is the behavior primary-key views need. + wsAdapter.sendToClient( + ServerMessage.TransactionUpdate({ + querySets: [ + makeQuerySetUpdate( + 0, + 'player', + encodePlayer(updatedRow), + encodePlayer(initialRow) + ), + makeQuerySetUpdate( + 1, + 'my_user_procedural', + encodeMyUserProcedural(updatedRow), + encodeMyUserProcedural(initialRow) + ), + ], + }) + ); + + await updatePromise.promise; + + expect(updates).toEqual([ + { + oldRow: initialRow, + newRow: updatedRow, + }, + ]); + expect(client.db.player.count()).toEqual(1n); + expect(client.db.my_user_procedural.count()).toEqual(1n); + expect([...client.db.my_user_procedural.iter()][0]).toEqual(updatedRow); + }); + test('Filtering works', async () => { const wsAdapter = new WebsocketTestAdapter(); const client = DbConnection.builder() From 288e0faff154ac7ddc100c53d8249a7000178b7b Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 28 May 2026 16:48:31 -0700 Subject: [PATCH 5/5] autogen --- .../Autogen/RawModuleDefV10Section.g.cs | 3 +- .../Autogen/RawViewPrimaryKeyDefV10.g.cs | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 crates/bindings-csharp/Runtime/Internal/Autogen/RawViewPrimaryKeyDefV10.g.cs diff --git a/crates/bindings-csharp/Runtime/Internal/Autogen/RawModuleDefV10Section.g.cs b/crates/bindings-csharp/Runtime/Internal/Autogen/RawModuleDefV10Section.g.cs index 1a299f93c3a..716b6ca706f 100644 --- a/crates/bindings-csharp/Runtime/Internal/Autogen/RawModuleDefV10Section.g.cs +++ b/crates/bindings-csharp/Runtime/Internal/Autogen/RawModuleDefV10Section.g.cs @@ -19,6 +19,7 @@ public partial record RawModuleDefV10Section : SpacetimeDB.TaggedEnum<( System.Collections.Generic.List LifeCycleReducers, System.Collections.Generic.List RowLevelSecurity, SpacetimeDB.CaseConversionPolicy CaseConversionPolicy, - ExplicitNames ExplicitNames + ExplicitNames ExplicitNames, + System.Collections.Generic.List ViewPrimaryKeys )>; } diff --git a/crates/bindings-csharp/Runtime/Internal/Autogen/RawViewPrimaryKeyDefV10.g.cs b/crates/bindings-csharp/Runtime/Internal/Autogen/RawViewPrimaryKeyDefV10.g.cs new file mode 100644 index 00000000000..854191d7bf6 --- /dev/null +++ b/crates/bindings-csharp/Runtime/Internal/Autogen/RawViewPrimaryKeyDefV10.g.cs @@ -0,0 +1,36 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Internal +{ + [SpacetimeDB.Type] + [DataContract] + public sealed partial class RawViewPrimaryKeyDefV10 + { + [DataMember(Name = "view_source_name")] + public string ViewSourceName; + [DataMember(Name = "columns")] + public System.Collections.Generic.List Columns; + + public RawViewPrimaryKeyDefV10( + string ViewSourceName, + System.Collections.Generic.List Columns + ) + { + this.ViewSourceName = ViewSourceName; + this.Columns = Columns; + } + + public RawViewPrimaryKeyDefV10() + { + this.ViewSourceName = ""; + this.Columns = new(); + } + } +}